Selection: registry-driven, drop spawn flag, restore green color
Two concerns from the spike:
1) Selection / floating-action-menu had hardcoded kind lists scattered
across 4 files. Adding 'shelf' to each one per migration was the
wrong abstraction — the user's question "did you make it generic
from the noderegistry?" was the right one. Done now.
Added to @pascal-app/core/registry:
- getSelectableKinds(): string[] — returns all registered kinds
whose definition declares `capabilities.selectable`.
- isRegistrySelectable(kind): boolean — predicate for OR-chains.
Refactored hardcoded sites to merge registry kinds at runtime,
keeping legacy hardcoded lists intact so existing kinds keep
working unchanged:
- editor SelectionManager: 4 subscription loops (enter/leave/click)
+ structure.isValid + getSelectionTarget — all augment with
registry kinds. Phase 6 deletes the hardcoded lists.
- viewer SelectionManager: subscription loop + SelectableNodeType
broadened with `(string & {})` to accept registry kinds.
- floating-action-menu: ALLOWED_TYPES OR'd with isRegistrySelectable.
- Removed the manually-added 'shelf' entries from previous commit
857ddd4; they were redundant once the registry-driven path landed.
Future built-in nodes that declare `capabilities.selectable` get
click-selection + hover + the floating action menu (move/delete
icons) for free, no editing of these 4 files.
2) Spawn parity is signed off. Drop the
NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN flag entirely; spawn registers
unconditionally in builtinPlugin.nodes. Restored SPAWN_COLOR to
the original #22c55e green (was #ef4444 red as a Phase 2
verification marker).
Pre-existing typecheck errors in editor (ceiling/fence/slab tree-node,
scene.ts buildingId) are unchanged.
630 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
857ddd4d95
commit
6d97a87547
@@ -1,4 +1,10 @@
|
|||||||
export { loadPlugin, nodeRegistry, registerNode } from './registry'
|
export {
|
||||||
|
getSelectableKinds,
|
||||||
|
isRegistrySelectable,
|
||||||
|
loadPlugin,
|
||||||
|
nodeRegistry,
|
||||||
|
registerNode,
|
||||||
|
} from './registry'
|
||||||
export {
|
export {
|
||||||
type CascadeContext,
|
type CascadeContext,
|
||||||
type ChildQuery,
|
type ChildQuery,
|
||||||
|
|||||||
@@ -57,6 +57,34 @@ export function registerNode(def: AnyNodeDefinition): void {
|
|||||||
nodeRegistry._register(def)
|
nodeRegistry._register(def)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the set of registered kinds whose definition declares the
|
||||||
|
* `selectable` capability. Callers that maintain hardcoded "selectable kinds"
|
||||||
|
* lists (SelectionManager, FloatingActionMenu) should concat this with their
|
||||||
|
* legacy entries instead of editing the hardcoded list per migration.
|
||||||
|
*
|
||||||
|
* Phase 6 deletes the hardcoded lists entirely and uses this function as the
|
||||||
|
* single source of truth. For now it's additive over the legacy lists so the
|
||||||
|
* existing kinds keep working unchanged.
|
||||||
|
*/
|
||||||
|
export function getSelectableKinds(): string[] {
|
||||||
|
const result: string[] = []
|
||||||
|
for (const [kind, def] of nodeRegistry.entries()) {
|
||||||
|
if (def.capabilities.selectable !== undefined) {
|
||||||
|
result.push(kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the kind is declared selectable in the registry. Use
|
||||||
|
* in expression chains like `if (node.type === 'wall' || isRegistrySelectable(node.type))`.
|
||||||
|
*/
|
||||||
|
export function isRegistrySelectable(kind: string): boolean {
|
||||||
|
return nodeRegistry.get(kind)?.capabilities.selectable !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadPlugin(plugin: Plugin): Promise<void> {
|
export async function loadPlugin(plugin: Plugin): Promise<void> {
|
||||||
if (plugin.apiVersion !== HOST_API_VERSION) {
|
if (plugin.apiVersion !== HOST_API_VERSION) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
FenceNode,
|
FenceNode,
|
||||||
generateId,
|
generateId,
|
||||||
ItemNode,
|
ItemNode,
|
||||||
|
isRegistrySelectable,
|
||||||
RoofSegmentNode,
|
RoofSegmentNode,
|
||||||
type SlabNode,
|
type SlabNode,
|
||||||
SpawnNode,
|
SpawnNode,
|
||||||
@@ -78,7 +79,12 @@ export function FloatingActionMenu() {
|
|||||||
// Subscribe just to the selected node so unrelated scene updates do not
|
// Subscribe just to the selected node so unrelated scene updates do not
|
||||||
// re-render this menu.
|
// re-render this menu.
|
||||||
const node = useScene((s) => (selectedId ? (s.nodes[selectedId as AnyNodeId] ?? null) : null))
|
const node = useScene((s) => (selectedId ? (s.nodes[selectedId as AnyNodeId] ?? null) : null))
|
||||||
const isValidType = node ? ALLOWED_TYPES.includes(node.type) : false
|
// ALLOWED_TYPES is the hardcoded set; registry-driven kinds (any
|
||||||
|
// NodeDefinition with `capabilities.selectable`) get the floating menu
|
||||||
|
// by default too. Phase 4 collapses these into a single registry check.
|
||||||
|
const isValidType = node
|
||||||
|
? ALLOWED_TYPES.includes(node.type) || isRegistrySelectable(node.type)
|
||||||
|
: false
|
||||||
|
|
||||||
// Boolean selector, only re-renders when curving availability actually flips.
|
// Boolean selector, only re-renders when curving availability actually flips.
|
||||||
const canCurveSelectedWall = useScene((s) => {
|
const canCurveSelectedWall = useScene((s) => {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import {
|
|||||||
emitter,
|
emitter,
|
||||||
type FenceNode,
|
type FenceNode,
|
||||||
getMaterialPresetByRef,
|
getMaterialPresetByRef,
|
||||||
|
getSelectableKinds,
|
||||||
type ItemNode,
|
type ItemNode,
|
||||||
|
isRegistrySelectable,
|
||||||
type NodeEvent,
|
type NodeEvent,
|
||||||
type RoofEvent,
|
type RoofEvent,
|
||||||
type RoofNode,
|
type RoofNode,
|
||||||
@@ -576,7 +578,6 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|||||||
'roof-segment',
|
'roof-segment',
|
||||||
'stair',
|
'stair',
|
||||||
'stair-segment',
|
'stair-segment',
|
||||||
'shelf',
|
|
||||||
'spawn',
|
'spawn',
|
||||||
'window',
|
'window',
|
||||||
'door',
|
'door',
|
||||||
@@ -649,6 +650,11 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|||||||
}
|
}
|
||||||
if (node.type === 'window' || node.type === 'door') return true
|
if (node.type === 'window' || node.type === 'door') return true
|
||||||
|
|
||||||
|
// Registry-driven: any kind whose NodeDefinition declares the
|
||||||
|
// `selectable` capability is also selectable in structure phase. Phase 4
|
||||||
|
// makes this the only path and deletes the hardcoded chain above.
|
||||||
|
if (isRegistrySelectable(node.type)) return true
|
||||||
|
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -705,7 +711,10 @@ const getSelectionTarget = (node: AnyNode): SelectionTarget | null => {
|
|||||||
node.type === 'stair-segment' ||
|
node.type === 'stair-segment' ||
|
||||||
node.type === 'spawn' ||
|
node.type === 'spawn' ||
|
||||||
node.type === 'window' ||
|
node.type === 'window' ||
|
||||||
node.type === 'door'
|
node.type === 'door' ||
|
||||||
|
// Registry-driven kinds default to structure/elements (Phase 4 reads
|
||||||
|
// `definition.presentation.paletteSection` to route correctly).
|
||||||
|
isRegistrySelectable(node.type)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
phase: 'structure',
|
phase: 'structure',
|
||||||
@@ -1013,20 +1022,26 @@ export const SelectionManager = () => {
|
|||||||
'roof-segment',
|
'roof-segment',
|
||||||
'stair',
|
'stair',
|
||||||
'stair-segment',
|
'stair-segment',
|
||||||
'shelf',
|
|
||||||
'window',
|
'window',
|
||||||
'door',
|
'door',
|
||||||
'zone',
|
'zone',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
for (const type of allTypes) {
|
// Registry-driven kinds get the same subscriptions as the hardcoded list,
|
||||||
|
// so future built-in nodes don't need to edit allTypes per migration.
|
||||||
|
const registryKinds = getSelectableKinds().filter(
|
||||||
|
(k) => !(allTypes as readonly string[]).includes(k),
|
||||||
|
)
|
||||||
|
const subscribedKinds = [...(allTypes as readonly string[]), ...registryKinds]
|
||||||
|
|
||||||
|
for (const type of subscribedKinds) {
|
||||||
emitter.on(`${type}:enter` as any, onEnter as any)
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
||||||
emitter.on(`${type}:leave` as any, onLeave as any)
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
||||||
emitter.on(`${type}:click` as any, onClick as any)
|
emitter.on(`${type}:click` as any, onClick as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
for (const type of allTypes) {
|
for (const type of subscribedKinds) {
|
||||||
emitter.off(`${type}:enter` as any, onEnter as any)
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
||||||
emitter.off(`${type}:leave` as any, onLeave as any)
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
||||||
emitter.off(`${type}:click` as any, onClick as any)
|
emitter.off(`${type}:click` as any, onClick as any)
|
||||||
@@ -1185,12 +1200,18 @@ export const SelectionManager = () => {
|
|||||||
'roof-segment',
|
'roof-segment',
|
||||||
'stair',
|
'stair',
|
||||||
'stair-segment',
|
'stair-segment',
|
||||||
'shelf',
|
|
||||||
'spawn',
|
'spawn',
|
||||||
'window',
|
'window',
|
||||||
'door',
|
'door',
|
||||||
]
|
]
|
||||||
allTypes.forEach((type) => {
|
// Registry-driven kinds get the same subscriptions as the hardcoded list,
|
||||||
|
// so future built-in nodes don't need to edit allTypes per migration.
|
||||||
|
const registryKinds = getSelectableKinds().filter(
|
||||||
|
(k) => !(allTypes as readonly string[]).includes(k),
|
||||||
|
)
|
||||||
|
const subscribedKinds = [...(allTypes as readonly string[]), ...registryKinds]
|
||||||
|
|
||||||
|
subscribedKinds.forEach((type) => {
|
||||||
emitter.on(`${type}:click` as any, onClick as any)
|
emitter.on(`${type}:click` as any, onClick as any)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1211,7 +1232,7 @@ export const SelectionManager = () => {
|
|||||||
emitter.on('grid:click', onGridClick)
|
emitter.on('grid:click', onGridClick)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
allTypes.forEach((type) => {
|
subscribedKinds.forEach((type) => {
|
||||||
emitter.off(`${type}:click` as any, onClick as any)
|
emitter.off(`${type}:click` as any, onClick as any)
|
||||||
})
|
})
|
||||||
emitter.off('grid:click', onGridClick)
|
emitter.off('grid:click', onGridClick)
|
||||||
@@ -1337,21 +1358,25 @@ export const SelectionManager = () => {
|
|||||||
'roof-segment',
|
'roof-segment',
|
||||||
'stair',
|
'stair',
|
||||||
'stair-segment',
|
'stair-segment',
|
||||||
'shelf',
|
|
||||||
'spawn',
|
'spawn',
|
||||||
'window',
|
'window',
|
||||||
'door',
|
'door',
|
||||||
'zone',
|
'zone',
|
||||||
'site',
|
'site',
|
||||||
]
|
]
|
||||||
allTypes.forEach((type) => {
|
const registryKinds = getSelectableKinds().filter(
|
||||||
|
(k) => !(allTypes as readonly string[]).includes(k),
|
||||||
|
)
|
||||||
|
const subscribedKinds = [...(allTypes as readonly string[]), ...registryKinds]
|
||||||
|
|
||||||
|
subscribedKinds.forEach((type) => {
|
||||||
emitter.on(`${type}:enter` as any, onEnter as any)
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
||||||
emitter.on(`${type}:leave` as any, onLeave as any)
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
||||||
emitter.on(`${type}:double-click` as any, onDoubleClick as any)
|
emitter.on(`${type}:double-click` as any, onDoubleClick as any)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
allTypes.forEach((type) => {
|
subscribedKinds.forEach((type) => {
|
||||||
emitter.off(`${type}:enter` as any, onEnter as any)
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
||||||
emitter.off(`${type}:leave` as any, onLeave as any)
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
||||||
emitter.off(`${type}:double-click` as any, onDoubleClick as any)
|
emitter.off(`${type}:double-click` as any, onDoubleClick as any)
|
||||||
@@ -1412,21 +1437,25 @@ export const SelectionManager = () => {
|
|||||||
'roof-segment',
|
'roof-segment',
|
||||||
'stair',
|
'stair',
|
||||||
'stair-segment',
|
'stair-segment',
|
||||||
'shelf',
|
|
||||||
'spawn',
|
'spawn',
|
||||||
'window',
|
'window',
|
||||||
'door',
|
'door',
|
||||||
'zone',
|
'zone',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
for (const type of allTypes) {
|
const registryKinds = getSelectableKinds().filter(
|
||||||
|
(k) => !(allTypes as readonly string[]).includes(k),
|
||||||
|
)
|
||||||
|
const subscribedKinds = [...(allTypes as readonly string[]), ...registryKinds]
|
||||||
|
|
||||||
|
for (const type of subscribedKinds) {
|
||||||
emitter.on(`${type}:click` as any, onClick as any)
|
emitter.on(`${type}:click` as any, onClick as any)
|
||||||
emitter.on(`${type}:enter` as any, onEnter as any)
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
||||||
emitter.on(`${type}:leave` as any, onLeave as any)
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
for (const type of allTypes) {
|
for (const type of subscribedKinds) {
|
||||||
emitter.off(`${type}:click` as any, onClick as any)
|
emitter.off(`${type}:click` as any, onClick as any)
|
||||||
emitter.off(`${type}:enter` as any, onEnter as any)
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
||||||
emitter.off(`${type}:leave` as any, onLeave as any)
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
||||||
|
|||||||
+11
-33
@@ -2,52 +2,30 @@ import type { AnyNodeDefinition, Plugin } from '@pascal-app/core'
|
|||||||
import { shelfDefinition } from './shelf'
|
import { shelfDefinition } from './shelf'
|
||||||
import { spawnDefinition } from './spawn'
|
import { spawnDefinition } from './spawn'
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature flag for the Phase 2 spike. When `NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN`
|
|
||||||
* is truthy, spawn registers through the registry path; otherwise the legacy
|
|
||||||
* `SpawnRenderer` and `SpawnTool` in viewer/editor packages own the kind.
|
|
||||||
*
|
|
||||||
* Removed in the PR that signs off parity (legacy spawn files deleted in the
|
|
||||||
* same commit). All other built-in node migrations follow the same pattern.
|
|
||||||
*/
|
|
||||||
function isSpawnRegistryEnabled(): boolean {
|
|
||||||
// Next.js / Turbopack inlines `process.env.NEXT_PUBLIC_*` references at
|
|
||||||
// build time, but ONLY when the access is a literal property — dynamic
|
|
||||||
// bracket access (`env[name]`) is not substituted and resolves to
|
|
||||||
// undefined in the browser. Keep this as a literal so the value is baked
|
|
||||||
// into the client bundle.
|
|
||||||
if (typeof process === 'undefined') return false
|
|
||||||
const flag = process.env.NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN
|
|
||||||
return flag === '1' || flag === 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBuiltinNodes(): AnyNodeDefinition[] {
|
|
||||||
const nodes: AnyNodeDefinition[] = [
|
|
||||||
// Shelf is a new kind — no legacy code to flag against. It ships
|
|
||||||
// unconditionally so users can place it from the tool palette.
|
|
||||||
shelfDefinition as unknown as AnyNodeDefinition,
|
|
||||||
]
|
|
||||||
if (isSpawnRegistryEnabled()) {
|
|
||||||
nodes.push(spawnDefinition as unknown as AnyNodeDefinition)
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Built-in plugin bundling every node kind shipped with the Pascal editor.
|
* Built-in plugin bundling every node kind shipped with the Pascal editor.
|
||||||
*
|
*
|
||||||
* Apps load this once at bootstrap (`loadPlugin(builtinPlugin)`) before
|
* Apps load this once at bootstrap (`loadPlugin(builtinPlugin)`) before
|
||||||
* mounting the viewer. New built-in nodes are added by creating a folder
|
* mounting the viewer. New built-in nodes are added by creating a folder
|
||||||
* here under `src/<kind>/` and appending its `NodeDefinition` to `getBuiltinNodes`.
|
* here under `src/<kind>/` and appending its `NodeDefinition` below.
|
||||||
*
|
*
|
||||||
* External plugins follow the exact same shape — same `Plugin` type, same
|
* External plugins follow the exact same shape — same `Plugin` type, same
|
||||||
* `loadPlugin` call path. This is intentional: the API is stress-tested
|
* `loadPlugin` call path. This is intentional: the API is stress-tested
|
||||||
* by built-ins before any third-party plugin lands.
|
* by built-ins before any third-party plugin lands.
|
||||||
|
*
|
||||||
|
* Phase 2 status: shelf is a brand-new kind. Spawn is migrated to the
|
||||||
|
* registry path — the legacy SpawnRenderer / SpawnTool files are still
|
||||||
|
* present in viewer/editor packages but short-circuited by the Phase 0
|
||||||
|
* dispatch shims (`nodeRegistry.has('spawn')` is true → legacy path
|
||||||
|
* yields). Legacy spawn files are deleted in a follow-up PR.
|
||||||
*/
|
*/
|
||||||
export const builtinPlugin: Plugin = {
|
export const builtinPlugin: Plugin = {
|
||||||
id: 'pascal:core',
|
id: 'pascal:core',
|
||||||
apiVersion: 1,
|
apiVersion: 1,
|
||||||
nodes: getBuiltinNodes(),
|
nodes: [
|
||||||
|
shelfDefinition as unknown as AnyNodeDefinition,
|
||||||
|
spawnDefinition as unknown as AnyNodeDefinition,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export { shelfDefinition } from './shelf'
|
export { shelfDefinition } from './shelf'
|
||||||
|
|||||||
@@ -5,13 +5,7 @@ import { useNodeEvents, useViewer } from '@pascal-app/viewer'
|
|||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { Color, type Group, Shape } from 'three'
|
import { Color, type Group, Shape } from 'three'
|
||||||
|
|
||||||
// TEMPORARY (Phase 2 verification): the registry-driven renderer paints
|
const SPAWN_COLOR = new Color('#22c55e')
|
||||||
// spawns RED so you can visually tell which dispatch path is live. The
|
|
||||||
// legacy renderer in @pascal-app/viewer is still green. Revert this to
|
|
||||||
// '#22c55e' once the registry path is signed off for parity. Tracked by
|
|
||||||
// the NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN flag — if a spawn renders red
|
|
||||||
// you're on the new path; green = legacy.
|
|
||||||
const SPAWN_COLOR = new Color('#ef4444')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry-driven spawn renderer. Behaviorally identical to the legacy
|
* Registry-driven spawn renderer. Behaviorally identical to the legacy
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type BuildingNode,
|
type BuildingNode,
|
||||||
type ColumnNode,
|
type ColumnNode,
|
||||||
emitter,
|
emitter,
|
||||||
|
getSelectableKinds,
|
||||||
type ItemNode,
|
type ItemNode,
|
||||||
type LevelNode,
|
type LevelNode,
|
||||||
type NodeEvent,
|
type NodeEvent,
|
||||||
@@ -25,6 +26,10 @@ const tempWorldPos = new Vector3()
|
|||||||
// Tolerance for edge detection (in meters)
|
// Tolerance for edge detection (in meters)
|
||||||
const EDGE_TOLERANCE = 0.5
|
const EDGE_TOLERANCE = 0.5
|
||||||
|
|
||||||
|
// Hardcoded kinds the viewer's selection manager knows about. Registry kinds
|
||||||
|
// (any NodeDefinition with `capabilities.selectable`) are merged in at
|
||||||
|
// runtime via getSelectableKinds() — Phase 6 collapses this into a single
|
||||||
|
// registry-driven list.
|
||||||
type SelectableNodeType =
|
type SelectableNodeType =
|
||||||
| 'building'
|
| 'building'
|
||||||
| 'level'
|
| 'level'
|
||||||
@@ -35,11 +40,11 @@ type SelectableNodeType =
|
|||||||
| 'door'
|
| 'door'
|
||||||
| 'column'
|
| 'column'
|
||||||
| 'item'
|
| 'item'
|
||||||
| 'shelf'
|
|
||||||
| 'slab'
|
| 'slab'
|
||||||
| 'ceiling'
|
| 'ceiling'
|
||||||
| 'roof'
|
| 'roof'
|
||||||
| 'roof-segment'
|
| 'roof-segment'
|
||||||
|
| (string & {})
|
||||||
|
|
||||||
// Expand polygon outward by a small amount to include items on edges
|
// Expand polygon outward by a small amount to include items on edges
|
||||||
const expandPolygon = (polygon: [number, number][], tolerance: number): [number, number][] => {
|
const expandPolygon = (polygon: [number, number][], tolerance: number): [number, number][] => {
|
||||||
@@ -330,7 +335,9 @@ export const SelectionManager = () => {
|
|||||||
useViewer.setState({ hoveredId: null })
|
useViewer.setState({ hoveredId: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to all node types
|
// Subscribe to all node types. Hardcoded kinds + registry-supplied kinds
|
||||||
|
// (any NodeDefinition declaring `capabilities.selectable`). Phase 6
|
||||||
|
// collapses these into a single registry-driven list.
|
||||||
const allTypes: SelectableNodeType[] = [
|
const allTypes: SelectableNodeType[] = [
|
||||||
'building',
|
'building',
|
||||||
'level',
|
'level',
|
||||||
@@ -339,7 +346,6 @@ export const SelectionManager = () => {
|
|||||||
'fence',
|
'fence',
|
||||||
'item',
|
'item',
|
||||||
'column',
|
'column',
|
||||||
'shelf',
|
|
||||||
'slab',
|
'slab',
|
||||||
'ceiling',
|
'ceiling',
|
||||||
'roof',
|
'roof',
|
||||||
@@ -347,17 +353,22 @@ export const SelectionManager = () => {
|
|||||||
'window',
|
'window',
|
||||||
'door',
|
'door',
|
||||||
]
|
]
|
||||||
for (const type of allTypes) {
|
const registryKinds = getSelectableKinds().filter(
|
||||||
emitter.on(`${type}:enter`, onEnter)
|
(k) => !(allTypes as readonly string[]).includes(k),
|
||||||
emitter.on(`${type}:leave`, onLeave)
|
) as SelectableNodeType[]
|
||||||
emitter.on(`${type}:click`, onClick)
|
const subscribedKinds = [...allTypes, ...registryKinds]
|
||||||
|
|
||||||
|
for (const type of subscribedKinds) {
|
||||||
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
||||||
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
||||||
|
emitter.on(`${type}:click` as any, onClick as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
for (const type of allTypes) {
|
for (const type of subscribedKinds) {
|
||||||
emitter.off(`${type}:enter`, onEnter)
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
||||||
emitter.off(`${type}:leave`, onLeave)
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
||||||
emitter.off(`${type}:click`, onClick)
|
emitter.off(`${type}:click` as any, onClick as any)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
Reference in New Issue
Block a user