fix typing

This commit is contained in:
wass08
2026-01-21 11:50:56 +09:00
parent 9117d42841
commit 67f9111d03
@@ -1,165 +1,182 @@
import { emitter, sceneRegistry } from "@pascal-app/core"; import {
type AnyNode,
type BuildingNode,
emitter,
type ItemNode,
sceneRegistry,
} 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'
const SELECTION_STRATEGIES = { type SelectableNodeType = 'wall' | 'item' | 'building'
interface SelectionStrategy {
types: SelectableNodeType[]
handleSelect: (node: AnyNode, isShift: boolean) => void
handleDeselect: () => void
isValid: (node: AnyNode) => boolean
}
const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
site: { site: {
types: ["building"], types: ['building'],
handleSelect: (node: any, isShift: boolean) => { handleSelect: (node) => {
useViewer.getState().setSelection({ buildingId: node.id }); useViewer
.getState()
.setSelection({ buildingId: (node as BuildingNode).id })
}, },
handleDeselect: () => { handleDeselect: () => {
useViewer.getState().setSelection({ buildingId: null }); useViewer.getState().setSelection({ buildingId: null })
}, },
isValid: (node: any) => node.type === "building", isValid: (node) => node.type === 'building',
}, },
structure: { structure: {
types: ["wall", "item"], types: ['wall', 'item'],
handleSelect: (node: any, isShift: boolean) => { 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: any) => { isValid: (node) => {
if (node.type === "wall") return true; if (node.type === 'wall') return true
if (node.type === "item") { if (node.type === 'item') {
return node.category === "door" || node.category === "window"; return (
(node as ItemNode).asset.category === 'door' ||
(node as ItemNode).asset.category === 'window'
)
} }
return false; return false
}, },
}, },
furnish: { furnish: {
types: ["item"], types: ['item'],
handleSelect: (node: any, isShift: boolean) => { 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: any) => { isValid: (node) => {
return ( if (node.type !== 'item') return false
node.type === "item" && const item = node as ItemNode
node.category !== "door" && return item.asset.category !== 'door' && item.asset.category !== 'window'
node.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: any) => {
if (strategy.isValid(event.node)) { if (strategy.isValid(event.node)) {
useViewer.setState({ hoveredId: event.node.id }); useViewer.setState({ hoveredId: event.node.id })
} }
}; }
const onLeave = () => useViewer.setState({ hoveredId: null }); const onLeave = () => useViewer.setState({ hoveredId: null })
const onClick = (event: any) => { const onClick = (event: any) => {
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) if (selection.selectedIds.length > 0) idsToHighlight = selection.selectedIds
idsToHighlight = selection.selectedIds; else if (selection.levelId) idsToHighlight = [selection.levelId]
else if (selection.levelId) idsToHighlight = [selection.levelId]; else if (selection.buildingId) idsToHighlight = [selection.buildingId]
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
}; }