hover / selection manager
This commit is contained in:
@@ -1,28 +1,123 @@
|
||||
import {
|
||||
type BuildingNode,
|
||||
emitter,
|
||||
type GridEvent,
|
||||
type ItemEvent,
|
||||
type ItemNode,
|
||||
sceneRegistry,
|
||||
useScene,
|
||||
type WallEvent,
|
||||
type WallNode,
|
||||
} from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import useEditor from "@/store/use-editor";
|
||||
|
||||
export const SelectionManager = () => {
|
||||
const selectedItemId = useRef<ItemNode["id"] | WallNode["id"]>(null);
|
||||
const itemSelectedAt = useRef<number>(0);
|
||||
const phase = useEditor((state) => state.phase);
|
||||
const mode = useEditor((state) => state.mode);
|
||||
useEffect(() => {
|
||||
emitter.on("building:enter", (event) => {
|
||||
// console.log("Entered building:", event.node.id);
|
||||
if (mode !== "select") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (phase === "site") {
|
||||
const onBuildingEnter = (event: { node: BuildingNode }) => {
|
||||
const itemMesh = sceneRegistry.nodes.get(event.node.id);
|
||||
useViewer.getState().outliner.hoveredObjects.length = 0;
|
||||
useViewer.getState().outliner.hoveredObjects.push(itemMesh);
|
||||
});
|
||||
emitter.on("building:leave", (event) => {
|
||||
// console.log("Leaving building:", event.node.id);
|
||||
};
|
||||
const onBuildingLeave = (event: { node: BuildingNode }) => {
|
||||
useViewer.getState().outliner.hoveredObjects.length = 0;
|
||||
});
|
||||
};
|
||||
emitter.on("building:enter", onBuildingEnter);
|
||||
emitter.on("building:leave", onBuildingLeave);
|
||||
return () => {
|
||||
emitter.off("building:enter", onBuildingEnter);
|
||||
emitter.off("building:leave", onBuildingLeave);
|
||||
};
|
||||
}
|
||||
|
||||
if (phase === "furnish") {
|
||||
const onItemEnter = (event: ItemEvent) => {
|
||||
const itemMesh = sceneRegistry.nodes.get(event.node.id);
|
||||
useViewer.getState().outliner.hoveredObjects.length = 0;
|
||||
useViewer.getState().outliner.hoveredObjects.push(itemMesh);
|
||||
};
|
||||
|
||||
const onItemLeave = (event: ItemEvent) => {
|
||||
useViewer.getState().outliner.hoveredObjects.length = 0;
|
||||
};
|
||||
|
||||
const onItemClick = (event: ItemEvent) => {
|
||||
event.stopPropagation();
|
||||
useViewer
|
||||
.getState()
|
||||
.outliner.selectedObjects.push(
|
||||
sceneRegistry.nodes.get(event.node.id),
|
||||
);
|
||||
};
|
||||
|
||||
emitter.on("item:enter", onItemEnter);
|
||||
emitter.on("item:leave", onItemLeave);
|
||||
emitter.on("item:click", onItemClick);
|
||||
return () => {
|
||||
emitter.off("item:enter", onItemEnter);
|
||||
emitter.off("item:leave", onItemLeave);
|
||||
emitter.off("item:click", onItemClick);
|
||||
};
|
||||
}
|
||||
|
||||
if (phase === "structure") {
|
||||
const onGridClick = (event: GridEvent) => {
|
||||
useViewer.getState().outliner.selectedObjects.length = 0;
|
||||
};
|
||||
const onWallEnter = (event: WallEvent) => {
|
||||
event.stopPropagation();
|
||||
const itemMesh = sceneRegistry.nodes.get(event.node.id);
|
||||
useViewer.getState().outliner.hoveredObjects.length = 0;
|
||||
useViewer.getState().outliner.hoveredObjects.push(itemMesh);
|
||||
};
|
||||
const onWallLeave = (event: WallEvent) => {
|
||||
useViewer.getState().outliner.hoveredObjects.length = 0;
|
||||
};
|
||||
|
||||
const onWallClick = (event: WallEvent) => {
|
||||
event.stopPropagation();
|
||||
if (!event.nativeEvent.shiftKey) {
|
||||
useViewer.getState().outliner.selectedObjects.length = 0;
|
||||
useViewer
|
||||
.getState()
|
||||
.outliner.selectedObjects.push(
|
||||
sceneRegistry.nodes.get(event.node.id),
|
||||
);
|
||||
} else {
|
||||
// check if already selected
|
||||
const selectedObjects = useViewer.getState().outliner.selectedObjects;
|
||||
const wallMesh = sceneRegistry.nodes.get(event.node.id);
|
||||
const index = selectedObjects.indexOf(wallMesh);
|
||||
if (index > -1) {
|
||||
// already selected, deselect
|
||||
selectedObjects.splice(index, 1);
|
||||
} else {
|
||||
// not selected, add to selection
|
||||
selectedObjects.push(wallMesh);
|
||||
}
|
||||
}
|
||||
};
|
||||
emitter.on("grid:click", onGridClick);
|
||||
emitter.on("wall:enter", onWallEnter);
|
||||
emitter.on("wall:leave", onWallLeave);
|
||||
emitter.on("wall:click", onWallClick);
|
||||
return () => {
|
||||
emitter.off("wall:enter", onWallEnter);
|
||||
emitter.off("wall:leave", onWallLeave);
|
||||
emitter.off("wall:click", onWallClick);
|
||||
emitter.off("grid:click", onGridClick);
|
||||
};
|
||||
}
|
||||
|
||||
// emitter.on("item:click", (event) => {
|
||||
// event.stopPropagation();
|
||||
@@ -87,7 +182,7 @@ export const SelectionManager = () => {
|
||||
event.position,
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
}, [mode, phase]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,49 +1,52 @@
|
||||
import mitt from 'mitt'
|
||||
import type { BuildingNode, ItemNode, WallNode } from '../schema'
|
||||
import type { AnyNode } from '../schema/types'
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import mitt from "mitt";
|
||||
import type { BuildingNode, ItemNode, WallNode } from "../schema";
|
||||
import type { AnyNode } from "../schema/types";
|
||||
|
||||
// Base event interfaces
|
||||
export interface GridEvent {
|
||||
position: [number, number, number]
|
||||
position: [number, number, number];
|
||||
nativeEvent: ThreeEvent<PointerEvent>;
|
||||
}
|
||||
|
||||
export interface NodeEvent<T extends AnyNode = AnyNode> {
|
||||
node: T
|
||||
position: [number, number, number]
|
||||
localPosition: [number, number, number]
|
||||
normal?: [number, number, number]
|
||||
stopPropagation: () => void
|
||||
node: T;
|
||||
position: [number, number, number];
|
||||
localPosition: [number, number, number];
|
||||
normal?: [number, number, number];
|
||||
stopPropagation: () => void;
|
||||
nativeEvent: ThreeEvent<PointerEvent>;
|
||||
}
|
||||
|
||||
export type WallEvent = NodeEvent<WallNode>
|
||||
export type ItemEvent = NodeEvent<ItemNode>
|
||||
export type BuildingEvent = NodeEvent<BuildingNode>
|
||||
export type WallEvent = NodeEvent<WallNode>;
|
||||
export type ItemEvent = NodeEvent<ItemNode>;
|
||||
export type BuildingEvent = NodeEvent<BuildingNode>;
|
||||
|
||||
// Event suffixes - exported for use in hooks
|
||||
export const eventSuffixes = [
|
||||
'click',
|
||||
'move',
|
||||
'enter',
|
||||
'leave',
|
||||
'pointerdown',
|
||||
'pointerup',
|
||||
'context-menu',
|
||||
'double-click',
|
||||
] as const
|
||||
"click",
|
||||
"move",
|
||||
"enter",
|
||||
"leave",
|
||||
"pointerdown",
|
||||
"pointerup",
|
||||
"context-menu",
|
||||
"double-click",
|
||||
] as const;
|
||||
|
||||
export type EventSuffix = (typeof eventSuffixes)[number]
|
||||
export type EventSuffix = (typeof eventSuffixes)[number];
|
||||
|
||||
type NodeEvents<T extends string, E> = {
|
||||
[K in `${T}:${EventSuffix}`]: E
|
||||
}
|
||||
[K in `${T}:${EventSuffix}`]: E;
|
||||
};
|
||||
|
||||
type GridEvents = {
|
||||
[K in `grid:${EventSuffix}`]: GridEvent
|
||||
}
|
||||
[K in `grid:${EventSuffix}`]: GridEvent;
|
||||
};
|
||||
|
||||
type EditorEvents = GridEvents &
|
||||
NodeEvents<'wall', WallEvent> &
|
||||
NodeEvents<'item', ItemEvent> &
|
||||
NodeEvents<'building', BuildingEvent>
|
||||
NodeEvents<"wall", WallEvent> &
|
||||
NodeEvents<"item", ItemEvent> &
|
||||
NodeEvents<"building", BuildingEvent>;
|
||||
|
||||
export const emitter = mitt<EditorEvents>()
|
||||
export const emitter = mitt<EditorEvents>();
|
||||
|
||||
@@ -1,93 +1,99 @@
|
||||
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'
|
||||
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 PostProcessingPasses = () => {
|
||||
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,34 @@
|
||||
import { type EventSuffix, emitter, type GridEvent } from '@pascal-app/core'
|
||||
import type { 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],
|
||||
}
|
||||
nativeEvent: e,
|
||||
};
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,50 +6,56 @@ import {
|
||||
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'
|
||||
} 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']
|
||||
nativeEvent: e,
|
||||
} 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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user