* feat: railing on the straight stairs and new fence * feat: added spiral and curved stairs with bug fix for fence * feat:fence are linked to each other ... so moving one move the other sharing the same coordinate * fix: update stair railing logic to include front-side attachments for terminal landings * Integrate fence rendering into the fence system * fix: pass nodeId instead of undefined node to WallTreeNode and FenceTreeNode TreeNode was passing `node` (undefined variable) instead of `nodeId` to WallTreeNode and FenceTreeNode, causing a runtime ReferenceError. Updated FenceTreeNode to accept nodeId and look up the node from the scene store internally, consistent with all other tree node components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update fence icon with new isometric design Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Aymeric Rabot <aymeric@pascal.app> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import {
|
|
type BuildingEvent,
|
|
type BuildingNode,
|
|
type CeilingEvent,
|
|
type CeilingNode,
|
|
type DoorEvent,
|
|
type DoorNode,
|
|
type EventSuffix,
|
|
emitter,
|
|
type FenceEvent,
|
|
type FenceNode,
|
|
type ItemEvent,
|
|
type ItemNode,
|
|
type LevelEvent,
|
|
type LevelNode,
|
|
type RoofEvent,
|
|
type RoofNode,
|
|
type RoofSegmentEvent,
|
|
type RoofSegmentNode,
|
|
type SiteEvent,
|
|
type SiteNode,
|
|
type SlabEvent,
|
|
type SlabNode,
|
|
type StairEvent,
|
|
type StairNode,
|
|
type StairSegmentEvent,
|
|
type StairSegmentNode,
|
|
type WallEvent,
|
|
type WallNode,
|
|
type WindowEvent,
|
|
type WindowNode,
|
|
type ZoneEvent,
|
|
type ZoneNode,
|
|
} from '@pascal-app/core'
|
|
import type { ThreeEvent } from '@react-three/fiber'
|
|
import useViewer from '../store/use-viewer'
|
|
|
|
type NodeConfig = {
|
|
site: { node: SiteNode; event: SiteEvent }
|
|
item: { node: ItemNode; event: ItemEvent }
|
|
wall: { node: WallNode; event: WallEvent }
|
|
fence: { node: FenceNode; event: FenceEvent }
|
|
building: { node: BuildingNode; event: BuildingEvent }
|
|
level: { node: LevelNode; event: LevelEvent }
|
|
zone: { node: ZoneNode; event: ZoneEvent }
|
|
slab: { node: SlabNode; event: SlabEvent }
|
|
ceiling: { node: CeilingNode; event: CeilingEvent }
|
|
roof: { node: RoofNode; event: RoofEvent }
|
|
'roof-segment': { node: RoofSegmentNode; event: RoofSegmentEvent }
|
|
stair: { node: StairNode; event: StairEvent }
|
|
'stair-segment': { node: StairSegmentNode; event: StairSegmentEvent }
|
|
window: { node: WindowNode; event: WindowEvent }
|
|
door: { node: DoorNode; event: DoorEvent }
|
|
}
|
|
|
|
type NodeType = keyof NodeConfig
|
|
|
|
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 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,
|
|
stopPropagation: () => e.stopPropagation(),
|
|
nativeEvent: e,
|
|
} as NodeConfig[T]['event']
|
|
|
|
emitter.emit(eventKey, payload)
|
|
}
|
|
|
|
return {
|
|
onPointerDown: (e: ThreeEvent<PointerEvent>) => {
|
|
if (useViewer.getState().cameraDragging) return
|
|
if (e.button !== 0) return
|
|
emit('pointerdown', e)
|
|
},
|
|
onPointerUp: (e: ThreeEvent<PointerEvent>) => {
|
|
if (useViewer.getState().cameraDragging) return
|
|
if (e.button !== 0) return
|
|
emit('pointerup', e)
|
|
// Synthesize a click event on pointer up to be more forgiving than R3F's default onClick
|
|
// which often fails if the mouse moves even 1 pixel.
|
|
emit('click', e)
|
|
},
|
|
onClick: (e: ThreeEvent<PointerEvent>) => {
|
|
// Disable default R3F click since we synthesize it on pointerup
|
|
// This prevents double-clicks from firing twice.
|
|
},
|
|
onPointerEnter: (e: ThreeEvent<PointerEvent>) => {
|
|
if (useViewer.getState().cameraDragging) return
|
|
emit('enter', e)
|
|
},
|
|
onPointerLeave: (e: ThreeEvent<PointerEvent>) => {
|
|
if (useViewer.getState().cameraDragging) return
|
|
emit('leave', e)
|
|
},
|
|
onPointerMove: (e: ThreeEvent<PointerEvent>) => {
|
|
if (useViewer.getState().cameraDragging) return
|
|
emit('move', e)
|
|
},
|
|
onDoubleClick: (e: ThreeEvent<PointerEvent>) => {
|
|
if (useViewer.getState().cameraDragging) return
|
|
emit('double-click', e)
|
|
},
|
|
onContextMenu: (e: ThreeEvent<PointerEvent>) => {
|
|
if (useViewer.getState().cameraDragging) return
|
|
emit('context-menu', e)
|
|
},
|
|
}
|
|
}
|