event propagation fix + handling current selected level
This commit is contained in:
@@ -3,180 +3,199 @@ import {
|
|||||||
type BuildingNode,
|
type BuildingNode,
|
||||||
emitter,
|
emitter,
|
||||||
type ItemNode,
|
type ItemNode,
|
||||||
|
type NodeEvent,
|
||||||
|
resolveLevelId,
|
||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
} from '@pascal-app/core'
|
useScene,
|
||||||
|
} from "@pascal-app/core";
|
||||||
|
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from "react";
|
||||||
import useEditor from '@/store/use-editor'
|
import useEditor from "@/store/use-editor";
|
||||||
|
|
||||||
type SelectableNodeType = 'wall' | 'item' | 'building'
|
const isNodeInCurrentLevel = (node: AnyNode): boolean => {
|
||||||
|
const currentLevelId = useViewer.getState().selection.levelId;
|
||||||
|
if (!currentLevelId) return true; // No level selected, allow all
|
||||||
|
const nodeLevelId = resolveLevelId(node, useScene.getState().nodes);
|
||||||
|
return nodeLevelId === currentLevelId;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectableNodeType = "wall" | "item" | "building";
|
||||||
|
|
||||||
interface SelectionStrategy {
|
interface SelectionStrategy {
|
||||||
types: SelectableNodeType[]
|
types: SelectableNodeType[];
|
||||||
handleSelect: (node: AnyNode, isShift: boolean) => void
|
handleSelect: (node: AnyNode, isShift: boolean) => void;
|
||||||
handleDeselect: () => void
|
handleDeselect: () => void;
|
||||||
isValid: (node: AnyNode) => boolean
|
isValid: (node: AnyNode) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
||||||
site: {
|
site: {
|
||||||
types: ['building'],
|
types: ["building"],
|
||||||
handleSelect: (node) => {
|
handleSelect: (node) => {
|
||||||
useViewer
|
useViewer
|
||||||
.getState()
|
.getState()
|
||||||
.setSelection({ buildingId: (node as BuildingNode).id })
|
.setSelection({ buildingId: (node as BuildingNode).id });
|
||||||
},
|
},
|
||||||
handleDeselect: () => {
|
handleDeselect: () => {
|
||||||
useViewer.getState().setSelection({ buildingId: null })
|
useViewer.getState().setSelection({ buildingId: null });
|
||||||
},
|
},
|
||||||
isValid: (node) => node.type === 'building',
|
isValid: (node) => node.type === "building",
|
||||||
},
|
},
|
||||||
|
|
||||||
structure: {
|
structure: {
|
||||||
types: ['wall', 'item'],
|
types: ["wall", "item"],
|
||||||
handleSelect: (node, isShift) => {
|
handleSelect: (node, isShift) => {
|
||||||
const { selection, setSelection } = useViewer.getState()
|
const { selection, setSelection } = useViewer.getState();
|
||||||
const nextIds = isShift
|
const nextIds = isShift
|
||||||
? selection.selectedIds.includes(node.id)
|
? selection.selectedIds.includes(node.id)
|
||||||
? selection.selectedIds.filter((id) => id !== node.id)
|
? selection.selectedIds.filter((id) => id !== node.id)
|
||||||
: [...selection.selectedIds, node.id]
|
: [...selection.selectedIds, node.id]
|
||||||
: [node.id]
|
: [node.id];
|
||||||
setSelection({ selectedIds: nextIds })
|
setSelection({ selectedIds: nextIds });
|
||||||
},
|
},
|
||||||
handleDeselect: () => {
|
handleDeselect: () => {
|
||||||
useViewer.getState().setSelection({ selectedIds: [] })
|
useViewer.getState().setSelection({ selectedIds: [] });
|
||||||
},
|
},
|
||||||
isValid: (node) => {
|
isValid: (node) => {
|
||||||
if (node.type === 'wall') return true
|
if (!isNodeInCurrentLevel(node)) return false;
|
||||||
if (node.type === 'item') {
|
if (node.type === "wall") return true;
|
||||||
|
if (node.type === "item") {
|
||||||
return (
|
return (
|
||||||
(node as ItemNode).asset.category === 'door' ||
|
(node as ItemNode).asset.category === "door" ||
|
||||||
(node as ItemNode).asset.category === 'window'
|
(node as ItemNode).asset.category === "window"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
furnish: {
|
furnish: {
|
||||||
types: ['item'],
|
types: ["item"],
|
||||||
handleSelect: (node, isShift) => {
|
handleSelect: (node, isShift) => {
|
||||||
const { selection, setSelection } = useViewer.getState()
|
const { selection, setSelection } = useViewer.getState();
|
||||||
const nextIds = isShift
|
const nextIds = isShift
|
||||||
? selection.selectedIds.includes(node.id)
|
? selection.selectedIds.includes(node.id)
|
||||||
? selection.selectedIds.filter((id) => id !== node.id)
|
? selection.selectedIds.filter((id) => id !== node.id)
|
||||||
: [...selection.selectedIds, node.id]
|
: [...selection.selectedIds, node.id]
|
||||||
: [node.id]
|
: [node.id];
|
||||||
setSelection({ selectedIds: nextIds })
|
setSelection({ selectedIds: nextIds });
|
||||||
},
|
},
|
||||||
handleDeselect: () => {
|
handleDeselect: () => {
|
||||||
useViewer.getState().setSelection({ selectedIds: [] })
|
useViewer.getState().setSelection({ selectedIds: [] });
|
||||||
},
|
},
|
||||||
isValid: (node) => {
|
isValid: (node) => {
|
||||||
if (node.type !== 'item') return false
|
if (!isNodeInCurrentLevel(node)) return false;
|
||||||
const item = node as ItemNode
|
if (node.type !== "item") return false;
|
||||||
return item.asset.category !== 'door' && item.asset.category !== 'window'
|
const item = node as ItemNode;
|
||||||
|
return item.asset.category !== "door" && item.asset.category !== "window";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const SelectionManager = () => {
|
export const SelectionManager = () => {
|
||||||
const phase = useEditor((s) => s.phase)
|
const phase = useEditor((s) => s.phase);
|
||||||
const mode = useEditor((s) => s.mode)
|
const mode = useEditor((s) => s.mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== 'select') return
|
if (mode !== "select") return;
|
||||||
|
|
||||||
const strategy = SELECTION_STRATEGIES[phase]
|
const strategy = SELECTION_STRATEGIES[phase];
|
||||||
if (!strategy) return
|
if (!strategy) return;
|
||||||
|
|
||||||
const onEnter = (event: any) => {
|
const onEnter = (event: NodeEvent) => {
|
||||||
if (strategy.isValid(event.node)) {
|
if (strategy.isValid(event.node)) {
|
||||||
useViewer.setState({ hoveredId: event.node.id })
|
event.stopPropagation();
|
||||||
|
useViewer.setState({ hoveredId: event.node.id });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const onLeave = () => useViewer.setState({ hoveredId: null })
|
const onLeave = (event: NodeEvent) => {
|
||||||
|
if (strategy.isValid(event.node)) {
|
||||||
|
event.stopPropagation();
|
||||||
|
useViewer.setState({ hoveredId: null });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onClick = (event: any) => {
|
const onClick = (event: NodeEvent) => {
|
||||||
if (!strategy.isValid(event.node)) return
|
if (!strategy.isValid(event.node)) return;
|
||||||
|
|
||||||
event.stopPropagation()
|
event.stopPropagation();
|
||||||
const isShift = event.nativeEvent?.shiftKey
|
const isShift = event.nativeEvent?.shiftKey;
|
||||||
strategy.handleSelect(event.node, isShift)
|
strategy.handleSelect(event.node, isShift);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Bind listeners for all potential types this strategy might care about
|
// Bind listeners for all potential types this strategy might care about
|
||||||
strategy.types.forEach((type) => {
|
strategy.types.forEach((type) => {
|
||||||
emitter.on(`${type}:enter`, onEnter)
|
emitter.on(`${type}:enter`, onEnter);
|
||||||
emitter.on(`${type}:leave`, onLeave)
|
emitter.on(`${type}:leave`, onLeave);
|
||||||
emitter.on(`${type}:click`, onClick)
|
emitter.on(`${type}:click`, onClick);
|
||||||
})
|
});
|
||||||
|
|
||||||
const onGridClick = () => strategy.handleDeselect()
|
const onGridClick = () => strategy.handleDeselect();
|
||||||
emitter.on('grid:click', onGridClick)
|
emitter.on("grid:click", onGridClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
strategy.types.forEach((type) => {
|
strategy.types.forEach((type) => {
|
||||||
emitter.off(`${type}:enter`, onEnter)
|
emitter.off(`${type}:enter`, onEnter);
|
||||||
emitter.off(`${type}:leave`, onLeave)
|
emitter.off(`${type}:leave`, onLeave);
|
||||||
emitter.off(`${type}:click`, onClick)
|
emitter.off(`${type}:click`, onClick);
|
||||||
})
|
});
|
||||||
emitter.off('grid:click', onGridClick)
|
emitter.off("grid:click", onGridClick);
|
||||||
}
|
};
|
||||||
}, [phase, mode])
|
}, [phase, mode]);
|
||||||
|
|
||||||
return <EditorOutlinerSync />
|
return <EditorOutlinerSync />;
|
||||||
}
|
};
|
||||||
|
|
||||||
const EditorOutlinerSync = () => {
|
const EditorOutlinerSync = () => {
|
||||||
const phase = useEditor((s) => s.phase)
|
const phase = useEditor((s) => s.phase);
|
||||||
const selection = useViewer((s) => s.selection)
|
const selection = useViewer((s) => s.selection);
|
||||||
const hoveredId = useViewer((s) => s.hoveredId)
|
const hoveredId = useViewer((s) => s.hoveredId);
|
||||||
const outliner = useViewer((s) => s.outliner)
|
const outliner = useViewer((s) => s.outliner);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let idsToHighlight: string[] = []
|
let idsToHighlight: string[] = [];
|
||||||
|
|
||||||
// 1. Determine what should be highlighted based on Phase
|
// 1. Determine what should be highlighted based on Phase
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case 'site':
|
case "site":
|
||||||
// Only highlight the building if one is selected
|
// Only highlight the building if one is selected
|
||||||
if (selection.buildingId) idsToHighlight = [selection.buildingId]
|
if (selection.buildingId) idsToHighlight = [selection.buildingId];
|
||||||
break
|
break;
|
||||||
|
|
||||||
case 'structure':
|
case "structure":
|
||||||
// Highlight selected items (walls/slabs)
|
// Highlight selected items (walls/slabs)
|
||||||
// We IGNORE buildingId even if it's set in the store
|
// We IGNORE buildingId even if it's set in the store
|
||||||
idsToHighlight = selection.selectedIds
|
idsToHighlight = selection.selectedIds;
|
||||||
break
|
break;
|
||||||
|
|
||||||
case 'furnish':
|
case "furnish":
|
||||||
// Highlight selected furniture/items
|
// Highlight selected furniture/items
|
||||||
idsToHighlight = selection.selectedIds
|
idsToHighlight = selection.selectedIds;
|
||||||
break
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Pure Viewer mode: Highlight based on the "deepest" selection
|
// Pure Viewer mode: Highlight based on the "deepest" selection
|
||||||
if (selection.selectedIds.length > 0) idsToHighlight = selection.selectedIds
|
if (selection.selectedIds.length > 0)
|
||||||
else if (selection.levelId) idsToHighlight = [selection.levelId]
|
idsToHighlight = selection.selectedIds;
|
||||||
else if (selection.buildingId) idsToHighlight = [selection.buildingId]
|
else if (selection.levelId) idsToHighlight = [selection.levelId];
|
||||||
|
else if (selection.buildingId) idsToHighlight = [selection.buildingId];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Sync with the imperative outliner arrays (mutate in place to keep references)
|
// 2. Sync with the imperative outliner arrays (mutate in place to keep references)
|
||||||
outliner.selectedObjects.length = 0
|
outliner.selectedObjects.length = 0;
|
||||||
for (const id of idsToHighlight) {
|
for (const id of idsToHighlight) {
|
||||||
const obj = sceneRegistry.nodes.get(id)
|
const obj = sceneRegistry.nodes.get(id);
|
||||||
if (obj) outliner.selectedObjects.push(obj)
|
if (obj) outliner.selectedObjects.push(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
outliner.hoveredObjects.length = 0
|
outliner.hoveredObjects.length = 0;
|
||||||
if (hoveredId) {
|
if (hoveredId) {
|
||||||
const obj = sceneRegistry.nodes.get(hoveredId)
|
const obj = sceneRegistry.nodes.get(hoveredId);
|
||||||
if (obj) outliner.hoveredObjects.push(obj)
|
if (obj) outliner.hoveredObjects.push(obj);
|
||||||
}
|
}
|
||||||
}, [phase, selection, hoveredId, outliner])
|
}, [phase, selection, hoveredId, outliner]);
|
||||||
|
|
||||||
return null
|
return null;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export {
|
|||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
useRegistry,
|
useRegistry,
|
||||||
} from './hooks/scene-registry/scene-registry'
|
} from './hooks/scene-registry/scene-registry'
|
||||||
export { initSpatialGridSync } from './hooks/spatial-grid/spatial-grid-sync'
|
export { initSpatialGridSync, resolveLevelId } from './hooks/spatial-grid/spatial-grid-sync'
|
||||||
export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query'
|
export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query'
|
||||||
// Schema
|
// Schema
|
||||||
export * from './schema'
|
export * from './schema'
|
||||||
|
|||||||
Reference in New Issue
Block a user