diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index b712e4d7..c884a874 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -14,7 +14,7 @@ import { useGridEvents, useViewer, Viewer } from "@pascal-app/viewer"; import { useFrame, useThree } from "@react-three/fiber"; import { useEffect, useMemo, useRef } from "react"; import { Color, MathUtils, Mesh, Object3D, Vector3 } from "three"; -import { outline } from "three/addons/tsl/display/OutlineNode.js"; + import { color, float, @@ -34,8 +34,6 @@ import { AppSidebar } from "../ui/sidebar/app-sidebar"; import { SidebarProvider } from "../ui/primitives/sidebar"; import { CustomCameraControls } from "./custom-camera-controls"; -const selectedObjects: Object3D[] = []; - initSpatialGridSync(); useScene.getState().loadScene(); @@ -54,7 +52,6 @@ export default function Editor() { {/* */} - @@ -87,65 +84,6 @@ const TestUndo = () => { ); }; -export const Passes = ({}) => { - const { gl: renderer, scene, camera } = useThree(); - const postProcessingRef = useRef(null); - - useEffect(() => { - if (!renderer || !scene || !camera) { - return; - } - - const scenePass = pass(scene, camera); - - // Get texture nodes - const outputPass = scenePass.getTextureNode("output"); - - const edgeStrength = uniform(3.0); - const edgeGlow = uniform(0.0); - const edgeThickness = uniform(1.0); - const pulsePeriod = uniform(0); - const visibleEdgeColor = uniform(new Color(0xffffff)); - const hiddenEdgeColor = uniform(new Color(0x4e3636)); - - const outlinePass = outline(scene, camera, { - selectedObjects, - edgeGlow, - edgeThickness, - }); - 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 outlineColor = visibleEdge - .mul(visibleEdgeColor) - .add(hiddenEdge.mul(hiddenEdgeColor)) - .mul(edgeStrength); - const outlinePulse = pulsePeriod - .greaterThan(0) - .select(outlineColor.mul(osc), outlineColor); - - // Setup post-processing - const postProcessing = new PostProcessing(renderer); - - postProcessing.outputNode = outlinePulse.add(scenePass); - postProcessingRef.current = postProcessing; - - return () => { - postProcessingRef.current = null; - }; - }, [renderer, scene, camera]); - - useFrame(() => { - if (postProcessingRef.current) { - postProcessingRef.current.render(); - } - }, 1); - - return null; -}; - const Grid = ({ cellSize = 0.5, cellThickness = 0.5, @@ -266,12 +204,12 @@ const DraftSelector = () => { emitter.on("building:enter", (event) => { // console.log("Entered building:", event.node.id); const itemMesh = sceneRegistry.nodes.get(event.node.id); - selectedObjects.length = 0; - selectedObjects.push(itemMesh); + useViewer.getState().outliner.hoveredObjects.length = 0; + useViewer.getState().outliner.hoveredObjects.push(itemMesh); }); emitter.on("building:leave", (event) => { // console.log("Leaving building:", event.node.id); - selectedObjects.length = 0; + useViewer.getState().outliner.hoveredObjects.length = 0; }); // emitter.on("item:click", (event) => { @@ -302,13 +240,13 @@ const DraftSelector = () => { if (selectedItemId.current === event.node.id) { selectedItemId.current = null; console.log("Deselected item:", event.node.id); - selectedObjects.length = 0; + useViewer.getState().outliner.selectedObjects.length = 0; return; } selectedItemId.current = event.node.id; const itemMesh = sceneRegistry.nodes.get(event.node.id); if (!itemMesh) return; - selectedObjects.push(itemMesh); + useViewer.getState().outliner.selectedObjects.push(itemMesh); console.log("Selected item:", event.node.id); }); diff --git a/packages/viewer/src/components/viewer/viewer.tsx b/packages/viewer/src/components/viewer/index.tsx similarity index 94% rename from packages/viewer/src/components/viewer/viewer.tsx rename to packages/viewer/src/components/viewer/index.tsx index 99f3fd33..2cc75cae 100644 --- a/packages/viewer/src/components/viewer/viewer.tsx +++ b/packages/viewer/src/components/viewer/index.tsx @@ -8,6 +8,7 @@ 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" { interface ThreeElements extends ThreeToJSXElements {} @@ -41,6 +42,7 @@ const Viewer: React.FC = ({ children }) => { {/* Default Systems */} + {children} diff --git a/packages/viewer/src/components/viewer/post-processing.tsx b/packages/viewer/src/components/viewer/post-processing.tsx new file mode 100644 index 00000000..1ee0e189 --- /dev/null +++ b/packages/viewer/src/components/viewer/post-processing.tsx @@ -0,0 +1,99 @@ +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"; + +const PostProcessingPasses = ({}) => { + const { gl: renderer, scene, camera } = useThree(); + const postProcessingRef = useRef(null); + + useEffect(() => { + if (!renderer || !scene || !camera) { + return; + } + + 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 outlinePass = outline(scene, camera, { + selectedObjects: useViewer.getState().outliner.selectedObjects, + edgeGlow, + edgeThickness, + }); + const { visibleEdge, hiddenEdge } = outlinePass; + + const outlineColor = visibleEdge + .mul(visibleEdgeColor) + .add(hiddenEdge.mul(hiddenEdgeColor)) + .mul(edgeStrength); + + 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 outlinePass = outline(scene, camera, { + selectedObjects: useViewer.getState().outliner.hoveredObjects, + edgeGlow, + edgeThickness, + }); + 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 outlineColor = visibleEdge + .mul(visibleEdgeColor) + .add(hiddenEdge.mul(hiddenEdgeColor)) + .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 selectedOutlinePass = generateSelectedOutlinePass(); + const hoverOutlinePass = generateHoverOutlinePass(); + + postProcessing.outputNode = selectedOutlinePass + .add(hoverOutlinePass) + .add(scenePass); + postProcessingRef.current = postProcessing; + + return () => { + if (postProcessingRef.current) { + postProcessingRef.current.dispose(); + } + postProcessingRef.current = null; + }; + }, [renderer, scene, camera]); + + useFrame(() => { + if (postProcessingRef.current) { + postProcessingRef.current.render(); + } + }, 1); + + return null; +}; + +export default PostProcessingPasses; diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index 969357f8..c8d80d17 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -1,7 +1,5 @@ -import { useGridEvents } from "./hooks/use-grid-events"; - -export { default as Viewer } from "./components/viewer/viewer"; +export { default as Viewer } from "./components/viewer"; export { default as useViewer } from "./store/use-viewer"; -export { useGridEvents } from "./hooks/use-grid-events"; \ No newline at end of file +export { useGridEvents } from "./hooks/use-grid-events"; diff --git a/packages/viewer/src/store/use-viewer.ts b/packages/viewer/src/store/use-viewer.ts index 6d3d9a63..856e1e11 100644 --- a/packages/viewer/src/store/use-viewer.ts +++ b/packages/viewer/src/store/use-viewer.ts @@ -1,6 +1,8 @@ "use client"; import { BuildingNode, ItemNode, LevelNode, Zone } from "@pascal-app/core"; +import { Object3D } from "three"; + import { create } from "zustand"; type SelectionPath = { @@ -10,6 +12,11 @@ type SelectionPath = { selectedIds: ItemNode["id"][]; // For items/assets (multi-select) }; +type Outliner = { + selectedObjects: Object3D[]; + hoveredObjects: Object3D[]; +}; + type ViewerState = { selection: SelectionPath; levelMode: "stacked" | "exploded" | "solo" | "manual"; @@ -20,6 +27,8 @@ type ViewerState = { // Smart selection update setSelection: (updates: Partial) => void; resetSelection: () => void; + + outliner: Outliner; // No setter as we will manipulate directly the arrays }; const useViewer = create()((set, get) => ({ @@ -55,6 +64,8 @@ const useViewer = create()((set, get) => ({ selectedIds: [], }, }), + + outliner: { selectedObjects: [], hoveredObjects: [] }, })); export default useViewer;