biome
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
import { BuildingNode, useRegistry } from "@pascal-app/core";
|
||||
import { useRef } from "react";
|
||||
import { Group } from "three";
|
||||
import { NodeRenderer } from "../node-renderer";
|
||||
import { useNodeEvents } from "../../../hooks/use-node-events";
|
||||
import { type BuildingNode, useRegistry } from '@pascal-app/core'
|
||||
import { useRef } from 'react'
|
||||
import type { Group } from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import { NodeRenderer } from '../node-renderer'
|
||||
|
||||
export const BuildingRenderer = ({ node }: { node: BuildingNode }) => {
|
||||
const ref = useRef<Group>(null!);
|
||||
const ref = useRef<Group>(null!)
|
||||
|
||||
useRegistry(node.id, node.type, ref);
|
||||
const handlers = useNodeEvents(node, "building");
|
||||
useRegistry(node.id, node.type, ref)
|
||||
const handlers = useNodeEvents(node, 'building')
|
||||
return (
|
||||
<group ref={ref} {...handlers}>
|
||||
{node.children.map((childId) => (
|
||||
<NodeRenderer key={childId} nodeId={childId} />
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import {
|
||||
AnyNodeId,
|
||||
emitter,
|
||||
ItemNode,
|
||||
useRegistry,
|
||||
useScene,
|
||||
} from "@pascal-app/core";
|
||||
import { Clone } from "@react-three/drei/core/Clone";
|
||||
import { useGLTF } from "@react-three/drei/core/Gltf";
|
||||
import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events";
|
||||
import { Suspense, useCallback, useEffect, useRef } from "react";
|
||||
import { Group } from "three";
|
||||
import { useNodeEvents } from "../../../hooks/use-node-events";
|
||||
import { type AnyNodeId, type ItemNode, useRegistry, useScene } from '@pascal-app/core'
|
||||
import { Clone } from '@react-three/drei/core/Clone'
|
||||
import { useGLTF } from '@react-three/drei/core/Gltf'
|
||||
import { Suspense, useEffect, useRef } from 'react'
|
||||
import type { Group } from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
|
||||
export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
||||
const ref = useRef<Group>(null!);
|
||||
const ref = useRef<Group>(null!)
|
||||
|
||||
useRegistry(node.id, node.type, ref);
|
||||
useRegistry(node.id, node.type, ref)
|
||||
|
||||
return (
|
||||
<group position={node.position} rotation={node.rotation} ref={ref}>
|
||||
@@ -23,22 +16,22 @@ export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
||||
<ModelRenderer node={node} />
|
||||
</Suspense>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
||||
const { scene, nodes } = useGLTF(node.asset.src);
|
||||
const { scene, nodes } = useGLTF(node.asset.src)
|
||||
|
||||
if (nodes.cutout) {
|
||||
nodes.cutout.visible = false;
|
||||
nodes.cutout.visible = false
|
||||
}
|
||||
|
||||
const handlers = useNodeEvents(node, "item");
|
||||
const handlers = useNodeEvents(node, 'item')
|
||||
|
||||
useEffect(() => {
|
||||
if (!node.parentId) return;
|
||||
useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId);
|
||||
}, []);
|
||||
if (!node.parentId) return
|
||||
useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
|
||||
}, [node.parentId])
|
||||
|
||||
return (
|
||||
<Clone
|
||||
@@ -48,5 +41,5 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
||||
rotation={node.asset.rotation}
|
||||
{...handlers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { LevelNode, useRegistry } from "@pascal-app/core";
|
||||
import { useRef } from "react";
|
||||
import { Group } from "three";
|
||||
import { NodeRenderer } from "../node-renderer";
|
||||
import { type LevelNode, useRegistry } from '@pascal-app/core'
|
||||
import { useRef } from 'react'
|
||||
import type { Group } from 'three'
|
||||
import { NodeRenderer } from '../node-renderer'
|
||||
|
||||
export const LevelRenderer = ({ node }: { node: LevelNode }) => {
|
||||
const ref = useRef<Group>(null!);
|
||||
const ref = useRef<Group>(null!)
|
||||
|
||||
useRegistry(node.id, node.type, ref);
|
||||
useRegistry(node.id, node.type, ref)
|
||||
|
||||
return (
|
||||
<group ref={ref}>
|
||||
@@ -18,5 +18,5 @@ export const LevelRenderer = ({ node }: { node: LevelNode }) => {
|
||||
<NodeRenderer key={childId} nodeId={childId} />
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { AnyNode, useScene } from "@pascal-app/core";
|
||||
import { ItemRenderer } from "./item/item-renderer";
|
||||
import { LevelRenderer } from "./level/level-renderer";
|
||||
import { WallRenderer } from "./wall/wall-renderer";
|
||||
import { BuildingRenderer } from "./building/building-renderer";
|
||||
import { type AnyNode, useScene } from '@pascal-app/core'
|
||||
import { BuildingRenderer } from './building/building-renderer'
|
||||
import { ItemRenderer } from './item/item-renderer'
|
||||
import { LevelRenderer } from './level/level-renderer'
|
||||
import { WallRenderer } from './wall/wall-renderer'
|
||||
|
||||
export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode["id"] }) => {
|
||||
const node = useScene((state) => state.nodes[nodeId]);
|
||||
export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => {
|
||||
const node = useScene((state) => state.nodes[nodeId])
|
||||
|
||||
if (!node) return null;
|
||||
if (!node) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{node.type === "building" && <BuildingRenderer node={node} />}
|
||||
{node.type === "level" && <LevelRenderer node={node} />}
|
||||
{node.type === "item" && <ItemRenderer node={node} />}
|
||||
{node.type === "wall" && <WallRenderer node={node} />}
|
||||
{node.type === 'building' && <BuildingRenderer node={node} />}
|
||||
{node.type === 'level' && <LevelRenderer node={node} />}
|
||||
{node.type === 'item' && <ItemRenderer node={node} />}
|
||||
{node.type === 'wall' && <WallRenderer node={node} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useScene } from "@pascal-app/core";
|
||||
import { NodeRenderer } from "./node-renderer";
|
||||
import { useScene } from '@pascal-app/core'
|
||||
import { NodeRenderer } from './node-renderer'
|
||||
|
||||
export const SceneRenderer = () => {
|
||||
const rootNodes = useScene((state) => state.rootNodeIds);
|
||||
const rootNodes = useScene((state) => state.rootNodeIds)
|
||||
|
||||
return rootNodes.map((nodeId) => (
|
||||
<NodeRenderer key={nodeId} nodeId={nodeId} />
|
||||
));
|
||||
};
|
||||
return rootNodes.map((nodeId) => <NodeRenderer key={nodeId} nodeId={nodeId} />)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useRegistry, WallNode } from "@pascal-app/core";
|
||||
import { useRef } from "react";
|
||||
import { Mesh } from "three";
|
||||
import { NodeRenderer } from "../node-renderer";
|
||||
import { useNodeEvents } from "../../../hooks/use-node-events";
|
||||
import { useRegistry, type WallNode } from '@pascal-app/core'
|
||||
import { useRef } from 'react'
|
||||
import type { Mesh } from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import { NodeRenderer } from '../node-renderer'
|
||||
|
||||
export const WallRenderer = ({ node }: { node: WallNode }) => {
|
||||
const ref = useRef<Mesh>(null!);
|
||||
const ref = useRef<Mesh>(null!)
|
||||
|
||||
useRegistry(node.id, "wall", ref);
|
||||
useRegistry(node.id, 'wall', ref)
|
||||
|
||||
const handlers = useNodeEvents(node, "wall");
|
||||
const handlers = useNodeEvents(node, 'wall')
|
||||
|
||||
return (
|
||||
<mesh ref={ref} castShadow receiveShadow>
|
||||
@@ -24,5 +24,5 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
|
||||
<NodeRenderer key={childId} nodeId={childId} />
|
||||
))}
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { Bvh, Environment, OrbitControls } from "@react-three/drei";
|
||||
import { Canvas, ThreeToJSXElements } from "@react-three/fiber";
|
||||
import { WallSystem } from '@pascal-app/core'
|
||||
import { Bvh, Environment } from '@react-three/drei'
|
||||
import { Canvas, extend, type ThreeToJSXElements } from '@react-three/fiber'
|
||||
import * as THREE from 'three/webgpu'
|
||||
import { LevelSystem } from '../../systems/level/level-system'
|
||||
import { SceneRenderer } from '../renderers/scene-renderer'
|
||||
import PostProcessing from './post-processing'
|
||||
|
||||
import { WallSystem } from "@pascal-app/core";
|
||||
import { extend } from "@react-three/fiber";
|
||||
import * as THREE from "three/webgpu";
|
||||
import { SceneRenderer } from "../renderers/scene-renderer";
|
||||
import { LevelSystem } from "../../systems/level/level-system";
|
||||
import PostProcessing from "./post-processing";
|
||||
|
||||
declare module "@react-three/fiber" {
|
||||
declare module '@react-three/fiber' {
|
||||
interface ThreeElements extends ThreeToJSXElements<typeof THREE> {}
|
||||
}
|
||||
|
||||
extend(THREE as any);
|
||||
extend(THREE as any)
|
||||
|
||||
interface ViewerProps {
|
||||
children?: React.ReactNode;
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
||||
return (
|
||||
<Canvas
|
||||
className={"bg-[#303035]"}
|
||||
className={'bg-[#303035]'}
|
||||
gl={async (props) => {
|
||||
const renderer = new THREE.WebGPURenderer(props as any);
|
||||
await renderer.init();
|
||||
return renderer;
|
||||
const renderer = new THREE.WebGPURenderer(props as any)
|
||||
await renderer.init()
|
||||
return renderer
|
||||
}}
|
||||
shadows
|
||||
camera={{ position: [50, 50, 50], fov: 50 }}
|
||||
>
|
||||
<color attach="background" args={["#ececec"]} />
|
||||
<color attach="background" args={['#ececec']} />
|
||||
|
||||
<Environment preset="sunset" />
|
||||
<Bvh>
|
||||
@@ -46,7 +44,7 @@ const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
||||
|
||||
{children}
|
||||
</Canvas>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Viewer;
|
||||
export default Viewer
|
||||
|
||||
@@ -1,99 +1,93 @@
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Color, Object3D } from "three";
|
||||
import { oscSine, pass, time, uniform } from "three/tsl";
|
||||
import { outline } from "three/addons/tsl/display/OutlineNode.js";
|
||||
import { PostProcessing, WebGPURenderer } from "three/webgpu";
|
||||
import useViewer from "../../store/use-viewer";
|
||||
import { useFrame, useThree } from '@react-three/fiber'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Color } from 'three'
|
||||
import { outline } from 'three/addons/tsl/display/OutlineNode.js'
|
||||
import { oscSine, pass, time, uniform } from 'three/tsl'
|
||||
import { PostProcessing, type WebGPURenderer } from 'three/webgpu'
|
||||
import useViewer from '../../store/use-viewer'
|
||||
|
||||
const PostProcessingPasses = ({}) => {
|
||||
const { gl: renderer, scene, camera } = useThree();
|
||||
const postProcessingRef = useRef<PostProcessing | null>(null);
|
||||
const { gl: renderer, scene, camera } = useThree()
|
||||
const postProcessingRef = useRef<PostProcessing | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!renderer || !scene || !camera) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
const scenePass = pass(scene, camera)
|
||||
|
||||
function generateSelectedOutlinePass() {
|
||||
const edgeStrength = uniform(3);
|
||||
const edgeGlow = uniform(0);
|
||||
const edgeThickness = uniform(1);
|
||||
const visibleEdgeColor = uniform(new Color(0xffffff));
|
||||
const hiddenEdgeColor = uniform(new Color(0xf3ff47));
|
||||
const edgeStrength = uniform(3)
|
||||
const edgeGlow = uniform(0)
|
||||
const edgeThickness = uniform(1)
|
||||
const visibleEdgeColor = uniform(new Color(0xffffff))
|
||||
const hiddenEdgeColor = uniform(new Color(0xf3ff47))
|
||||
|
||||
const outlinePass = outline(scene, camera, {
|
||||
selectedObjects: useViewer.getState().outliner.selectedObjects,
|
||||
edgeGlow,
|
||||
edgeThickness,
|
||||
});
|
||||
const { visibleEdge, hiddenEdge } = outlinePass;
|
||||
})
|
||||
const { visibleEdge, hiddenEdge } = outlinePass
|
||||
|
||||
const outlineColor = visibleEdge
|
||||
.mul(visibleEdgeColor)
|
||||
.add(hiddenEdge.mul(hiddenEdgeColor))
|
||||
.mul(edgeStrength);
|
||||
.mul(edgeStrength)
|
||||
|
||||
return outlineColor;
|
||||
return outlineColor
|
||||
}
|
||||
function generateHoverOutlinePass() {
|
||||
const edgeStrength = uniform(5);
|
||||
const edgeGlow = uniform(0.5);
|
||||
const edgeThickness = uniform(1.5);
|
||||
const pulsePeriod = uniform(3);
|
||||
const visibleEdgeColor = uniform(new Color(0x00aaff));
|
||||
const hiddenEdgeColor = uniform(new Color(0xf3ff47));
|
||||
const edgeStrength = uniform(5)
|
||||
const edgeGlow = uniform(0.5)
|
||||
const edgeThickness = uniform(1.5)
|
||||
const pulsePeriod = uniform(3)
|
||||
const visibleEdgeColor = uniform(new Color(0x00aaff))
|
||||
const hiddenEdgeColor = uniform(new Color(0xf3ff47))
|
||||
|
||||
const outlinePass = outline(scene, camera, {
|
||||
selectedObjects: useViewer.getState().outliner.hoveredObjects,
|
||||
edgeGlow,
|
||||
edgeThickness,
|
||||
});
|
||||
const { visibleEdge, hiddenEdge } = outlinePass;
|
||||
})
|
||||
const { visibleEdge, hiddenEdge } = outlinePass
|
||||
|
||||
const period = time.div(pulsePeriod).mul(2);
|
||||
const osc = oscSine(period).mul(0.5).add(0.5); // osc [ 0.5, 1.0 ]
|
||||
const period = time.div(pulsePeriod).mul(2)
|
||||
const osc = oscSine(period).mul(0.5).add(0.5) // osc [ 0.5, 1.0 ]
|
||||
|
||||
const outlineColor = visibleEdge
|
||||
.mul(visibleEdgeColor)
|
||||
.add(hiddenEdge.mul(hiddenEdgeColor))
|
||||
.mul(edgeStrength);
|
||||
const outlinePulse = pulsePeriod
|
||||
.greaterThan(0)
|
||||
.select(outlineColor.mul(osc), outlineColor);
|
||||
return outlinePulse;
|
||||
.mul(edgeStrength)
|
||||
const outlinePulse = pulsePeriod.greaterThan(0).select(outlineColor.mul(osc), outlineColor)
|
||||
return outlinePulse
|
||||
}
|
||||
|
||||
// Setup post-processing
|
||||
const postProcessing = new PostProcessing(
|
||||
renderer as unknown as WebGPURenderer,
|
||||
);
|
||||
const postProcessing = new PostProcessing(renderer as unknown as WebGPURenderer)
|
||||
|
||||
const selectedOutlinePass = generateSelectedOutlinePass();
|
||||
const hoverOutlinePass = generateHoverOutlinePass();
|
||||
const selectedOutlinePass = generateSelectedOutlinePass()
|
||||
const hoverOutlinePass = generateHoverOutlinePass()
|
||||
|
||||
postProcessing.outputNode = selectedOutlinePass
|
||||
.add(hoverOutlinePass)
|
||||
.add(scenePass);
|
||||
postProcessingRef.current = postProcessing;
|
||||
postProcessing.outputNode = selectedOutlinePass.add(hoverOutlinePass).add(scenePass)
|
||||
postProcessingRef.current = postProcessing
|
||||
|
||||
return () => {
|
||||
if (postProcessingRef.current) {
|
||||
postProcessingRef.current.dispose();
|
||||
postProcessingRef.current.dispose()
|
||||
}
|
||||
postProcessingRef.current = null;
|
||||
};
|
||||
}, [renderer, scene, camera]);
|
||||
postProcessingRef.current = null
|
||||
}
|
||||
}, [renderer, scene, camera])
|
||||
|
||||
useFrame(() => {
|
||||
if (postProcessingRef.current) {
|
||||
postProcessingRef.current.render();
|
||||
postProcessingRef.current.render()
|
||||
}
|
||||
}, 1);
|
||||
}, 1)
|
||||
|
||||
return null;
|
||||
};
|
||||
return null
|
||||
}
|
||||
|
||||
export default PostProcessingPasses;
|
||||
export default PostProcessingPasses
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { emitter, EventSuffix, GridEvent } from "@pascal-app/core";
|
||||
import { ThreeEvent } from "@react-three/fiber";
|
||||
import { type EventSuffix, emitter, type GridEvent } from '@pascal-app/core'
|
||||
import type { ThreeEvent } from '@react-three/fiber'
|
||||
|
||||
export function useGridEvents() {
|
||||
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
|
||||
const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`;
|
||||
const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`
|
||||
const payload: GridEvent = {
|
||||
position: [e.point.x, e.point.y, e.point.z],
|
||||
};
|
||||
}
|
||||
|
||||
emitter.emit(eventKey, payload);
|
||||
};
|
||||
emitter.emit(eventKey, payload)
|
||||
}
|
||||
|
||||
return {
|
||||
onPointerDown: (e: ThreeEvent<PointerEvent>) => {
|
||||
if (e.button !== 0) return;
|
||||
emit("pointerdown", e);
|
||||
if (e.button !== 0) return
|
||||
emit('pointerdown', e)
|
||||
},
|
||||
onPointerUp: (e: ThreeEvent<PointerEvent>) => {
|
||||
if (e.button !== 0) return;
|
||||
emit("pointerup", e);
|
||||
if (e.button !== 0) return
|
||||
emit('pointerup', e)
|
||||
},
|
||||
onClick: (e: ThreeEvent<PointerEvent>) => {
|
||||
if (e.button !== 0) return;
|
||||
emit("click", e);
|
||||
if (e.button !== 0) return
|
||||
emit('click', e)
|
||||
},
|
||||
onPointerEnter: (e: ThreeEvent<PointerEvent>) => emit("enter", e),
|
||||
onPointerLeave: (e: ThreeEvent<PointerEvent>) => emit("leave", e),
|
||||
onPointerMove: (e: ThreeEvent<PointerEvent>) => emit("move", e),
|
||||
onDoubleClick: (e: ThreeEvent<PointerEvent>) => emit("double-click", e),
|
||||
onContextMenu: (e: ThreeEvent<PointerEvent>) => emit("context-menu", e),
|
||||
};
|
||||
onPointerEnter: (e: ThreeEvent<PointerEvent>) => emit('enter', e),
|
||||
onPointerLeave: (e: ThreeEvent<PointerEvent>) => emit('leave', e),
|
||||
onPointerMove: (e: ThreeEvent<PointerEvent>) => emit('move', e),
|
||||
onDoubleClick: (e: ThreeEvent<PointerEvent>) => emit('double-click', e),
|
||||
onContextMenu: (e: ThreeEvent<PointerEvent>) => emit('context-menu', e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,55 @@
|
||||
import {
|
||||
BuildingNode,
|
||||
type BuildingNode,
|
||||
type EventSuffix,
|
||||
emitter,
|
||||
EventSuffix,
|
||||
ItemEvent,
|
||||
ItemNode,
|
||||
WallEvent,
|
||||
WallNode,
|
||||
} from "@pascal-app/core";
|
||||
import { ThreeEvent } from "@react-three/fiber";
|
||||
import { BuildingEvent } from "../../../core/src/events/bus";
|
||||
type ItemEvent,
|
||||
type ItemNode,
|
||||
type WallEvent,
|
||||
type WallNode,
|
||||
} from '@pascal-app/core'
|
||||
import type { ThreeEvent } from '@react-three/fiber'
|
||||
import type { BuildingEvent } from '../../../core/src/events/bus'
|
||||
|
||||
type NodeConfig = {
|
||||
item: { node: ItemNode; event: ItemEvent };
|
||||
wall: { node: WallNode; event: WallEvent };
|
||||
building: { node: BuildingNode; event: BuildingEvent };
|
||||
};
|
||||
item: { node: ItemNode; event: ItemEvent }
|
||||
wall: { node: WallNode; event: WallEvent }
|
||||
building: { node: BuildingNode; event: BuildingEvent }
|
||||
}
|
||||
|
||||
type NodeType = keyof NodeConfig;
|
||||
type NodeType = keyof NodeConfig
|
||||
|
||||
export function useNodeEvents<T extends NodeType>(
|
||||
node: NodeConfig[T]["node"],
|
||||
type: T,
|
||||
) {
|
||||
export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], type: T) {
|
||||
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
|
||||
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`;
|
||||
const localPoint = e.object.worldToLocal(e.point.clone());
|
||||
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`
|
||||
const localPoint = e.object.worldToLocal(e.point.clone())
|
||||
const payload = {
|
||||
node,
|
||||
position: [e.point.x, e.point.y, e.point.z],
|
||||
localPosition: [localPoint.x, localPoint.y, localPoint.z],
|
||||
normal: e.face
|
||||
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
||||
: undefined,
|
||||
normal: e.face ? [e.face.normal.x, e.face.normal.y, e.face.normal.z] : undefined,
|
||||
stopPropagation: () => e.stopPropagation(),
|
||||
} as NodeConfig[T]["event"];
|
||||
} as NodeConfig[T]['event']
|
||||
|
||||
emitter.emit(eventKey, payload);
|
||||
};
|
||||
emitter.emit(eventKey, payload)
|
||||
}
|
||||
|
||||
return {
|
||||
onPointerDown: (e: ThreeEvent<PointerEvent>) => {
|
||||
if (e.button !== 0) return;
|
||||
emit("pointerdown", e);
|
||||
if (e.button !== 0) return
|
||||
emit('pointerdown', e)
|
||||
},
|
||||
onPointerUp: (e: ThreeEvent<PointerEvent>) => {
|
||||
if (e.button !== 0) return;
|
||||
emit("pointerup", e);
|
||||
if (e.button !== 0) return
|
||||
emit('pointerup', e)
|
||||
},
|
||||
onClick: (e: ThreeEvent<PointerEvent>) => {
|
||||
if (e.button !== 0) return;
|
||||
emit("click", e);
|
||||
if (e.button !== 0) return
|
||||
emit('click', e)
|
||||
},
|
||||
onPointerEnter: (e: ThreeEvent<PointerEvent>) => emit("enter", e),
|
||||
onPointerLeave: (e: ThreeEvent<PointerEvent>) => emit("leave", e),
|
||||
onPointerMove: (e: ThreeEvent<PointerEvent>) => emit("move", e),
|
||||
onDoubleClick: (e: ThreeEvent<PointerEvent>) => emit("double-click", e),
|
||||
onContextMenu: (e: ThreeEvent<PointerEvent>) => emit("context-menu", e),
|
||||
};
|
||||
onPointerEnter: (e: ThreeEvent<PointerEvent>) => emit('enter', e),
|
||||
onPointerLeave: (e: ThreeEvent<PointerEvent>) => emit('leave', e),
|
||||
onPointerMove: (e: ThreeEvent<PointerEvent>) => emit('move', e),
|
||||
onDoubleClick: (e: ThreeEvent<PointerEvent>) => emit('double-click', e),
|
||||
onContextMenu: (e: ThreeEvent<PointerEvent>) => emit('context-menu', e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export { default as Viewer } from "./components/viewer";
|
||||
|
||||
export { default as useViewer } from "./store/use-viewer";
|
||||
|
||||
export { useGridEvents } from "./hooks/use-grid-events";
|
||||
export { default as Viewer } from './components/viewer'
|
||||
export { useGridEvents } from './hooks/use-grid-events'
|
||||
export { default as useViewer } from './store/use-viewer'
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { BuildingNode, ItemNode, LevelNode, Zone } from "@pascal-app/core";
|
||||
import { Object3D } from "three";
|
||||
import type { BuildingNode, ItemNode, LevelNode, Zone } from '@pascal-app/core'
|
||||
import type { Object3D } from 'three'
|
||||
|
||||
import { create } from "zustand";
|
||||
import { create } from 'zustand'
|
||||
|
||||
type SelectionPath = {
|
||||
buildingId: BuildingNode["id"] | null;
|
||||
levelId: LevelNode["id"] | null;
|
||||
zoneId: Zone["id"] | null;
|
||||
selectedIds: ItemNode["id"][]; // For items/assets (multi-select)
|
||||
};
|
||||
buildingId: BuildingNode['id'] | null
|
||||
levelId: LevelNode['id'] | null
|
||||
zoneId: Zone['id'] | null
|
||||
selectedIds: ItemNode['id'][] // For items/assets (multi-select)
|
||||
}
|
||||
|
||||
type Outliner = {
|
||||
selectedObjects: Object3D[];
|
||||
hoveredObjects: Object3D[];
|
||||
};
|
||||
selectedObjects: Object3D[]
|
||||
hoveredObjects: Object3D[]
|
||||
}
|
||||
|
||||
type ViewerState = {
|
||||
selection: SelectionPath;
|
||||
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
||||
selection: SelectionPath
|
||||
levelMode: 'stacked' | 'exploded' | 'solo' | 'manual'
|
||||
|
||||
// Actions
|
||||
setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
|
||||
setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void
|
||||
|
||||
// Smart selection update
|
||||
setSelection: (updates: Partial<SelectionPath>) => void;
|
||||
resetSelection: () => void;
|
||||
setSelection: (updates: Partial<SelectionPath>) => void
|
||||
resetSelection: () => void
|
||||
|
||||
outliner: Outliner; // No setter as we will manipulate directly the arrays
|
||||
};
|
||||
outliner: Outliner // No setter as we will manipulate directly the arrays
|
||||
}
|
||||
|
||||
const useViewer = create<ViewerState>()((set, get) => ({
|
||||
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
||||
levelMode: "stacked",
|
||||
levelMode: 'stacked',
|
||||
setLevelMode: (mode) => set({ levelMode: mode }),
|
||||
|
||||
setSelection: (updates) =>
|
||||
set((state) => {
|
||||
const newSelection = { ...state.selection, ...updates };
|
||||
const newSelection = { ...state.selection, ...updates }
|
||||
|
||||
// Hierarchy Guard: If we change a high-level parent, reset the children
|
||||
if (updates.buildingId !== undefined) {
|
||||
newSelection.levelId = null;
|
||||
newSelection.zoneId = null;
|
||||
newSelection.selectedIds = [];
|
||||
newSelection.levelId = null
|
||||
newSelection.zoneId = null
|
||||
newSelection.selectedIds = []
|
||||
} else if (updates.levelId !== undefined) {
|
||||
newSelection.zoneId = null;
|
||||
newSelection.selectedIds = [];
|
||||
newSelection.zoneId = null
|
||||
newSelection.selectedIds = []
|
||||
} else if (updates.zoneId !== undefined) {
|
||||
newSelection.selectedIds = [];
|
||||
newSelection.selectedIds = []
|
||||
}
|
||||
|
||||
return { selection: newSelection };
|
||||
return { selection: newSelection }
|
||||
}),
|
||||
|
||||
resetSelection: () =>
|
||||
@@ -66,6 +66,6 @@ const useViewer = create<ViewerState>()((set, get) => ({
|
||||
}),
|
||||
|
||||
outliner: { selectedObjects: [], hoveredObjects: [] },
|
||||
}));
|
||||
}))
|
||||
|
||||
export default useViewer;
|
||||
export default useViewer
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { LevelNode, sceneRegistry, useScene } from "@pascal-app/core";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { lerp } from "three/src/math/MathUtils.js";
|
||||
import useViewer from "../../store/use-viewer";
|
||||
import { type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { lerp } from 'three/src/math/MathUtils.js'
|
||||
import useViewer from '../../store/use-viewer'
|
||||
|
||||
const LEVEL_HEIGHT = 2.5;
|
||||
const EXPLODED_GAP = 5;
|
||||
const LEVEL_HEIGHT = 2.5
|
||||
const EXPLODED_GAP = 5
|
||||
|
||||
export const LevelSystem = () => {
|
||||
useFrame((_, delta) => {
|
||||
const levelMode = useViewer.getState().levelMode;
|
||||
const levelMode = useViewer.getState().levelMode
|
||||
sceneRegistry.byType.level.forEach((levelId) => {
|
||||
const obj = sceneRegistry.nodes.get(levelId);
|
||||
const obj = sceneRegistry.nodes.get(levelId)
|
||||
if (obj) {
|
||||
const level = useScene.getState().nodes[levelId as LevelNode["id"]];
|
||||
const level = useScene.getState().nodes[levelId as LevelNode['id']]
|
||||
const targetY =
|
||||
((level as any).level || 0) *
|
||||
(LEVEL_HEIGHT + (levelMode === "stacked" ? 0 : EXPLODED_GAP));
|
||||
obj.position.y = lerp(obj.position.y, targetY, delta * 3);
|
||||
(LEVEL_HEIGHT + (levelMode === 'stacked' ? 0 : EXPLODED_GAP))
|
||||
obj.position.y = lerp(obj.position.y, targetY, delta * 3)
|
||||
}
|
||||
});
|
||||
});
|
||||
return null;
|
||||
};
|
||||
})
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user