event propagation fix + handling current selected level

This commit is contained in:
wass08
2026-01-21 12:09:57 +09:00
parent 48f7d7ffa4
commit 588e19ffc0
2 changed files with 109 additions and 90 deletions
@@ -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 = (event: NodeEvent) => {
if (strategy.isValid(event.node)) {
event.stopPropagation();
useViewer.setState({ hoveredId: null });
} }
};
const onLeave = () => useViewer.setState({ hoveredId: null }) const onClick = (event: NodeEvent) => {
if (!strategy.isValid(event.node)) return;
const onClick = (event: any) => { event.stopPropagation();
if (!strategy.isValid(event.node)) return const isShift = event.nativeEvent?.shiftKey;
strategy.handleSelect(event.node, isShift);
event.stopPropagation() };
const isShift = event.nativeEvent?.shiftKey
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;
} };
+1 -1
View File
@@ -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'