Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
fb91713
Add node registry primitives (PR 0.1 of node-registry plan)
wass08 May 14, 2026
1d7ec0b
Block @pascal-app/nodes imports from framework packages
wass08 May 14, 2026
fc36b5b
Add @pascal-app/nodes package skeleton with empty builtinPlugin
wass08 May 14, 2026
a41321e
Lock bun.lock for @pascal-app/nodes workspace + sort registry barrel
wass08 May 14, 2026
4d5226c
Wire loadPlugin(builtinPlugin) at editor app bootstrap
wass08 May 14, 2026
4c3feea
Proxy-back sceneRegistry.byType for registry/plugin kinds (shim 1/4)
wass08 May 14, 2026
0fd7af2
Registry-first dispatch in NodeRenderer (shim 2/4)
wass08 May 14, 2026
7dd3ffe
Wrap legacy systems in LegacySystem + mount RegisteredSystems (shim 3/4)
wass08 May 14, 2026
2a8eb3e
Registry-first dispatch in ToolManager (shim 4/4)
wass08 May 14, 2026
c683c04
Add test script to @pascal-app/core so turbo picks up its unit tests
wass08 May 14, 2026
3de098f
Add Presentation + IconRef to NodeDefinition v1 surface
wass08 May 14, 2026
90702e7
Add relations cascade resolver (Phase 1, 1/6)
wass08 May 14, 2026
cf93c32
Add HostingService with cycle/depth/kind validation (Phase 1, 2/6)
wass08 May 14, 2026
bc0c73d
Add SnapServices (grid + angle) to core (Phase 1, 3/6)
wass08 May 14, 2026
386df62
Add MovementService (axis-lock + grid-snap) to core (Phase 1, 4/6)
wass08 May 14, 2026
e799ff7
Add DragSession + useDragAction hook (Phase 1, 5/6)
wass08 May 14, 2026
e8cf853
Add cascade resolver bench harness, p95 gate < 2ms (Phase 1, 6/6)
wass08 May 14, 2026
b6d7720
Phase 2 spike: spawn migration (flagged) + new shelf node
wass08 May 14, 2026
88546d2
Bootstrap: log registered kinds + expose nodeRegistry on globalThis i…
wass08 May 14, 2026
ac71c1a
Fix shelf cursor tracking + bigger snap step; tint registry spawn red
wass08 May 14, 2026
a89a1ef
Fix spawn flag inlining + shelf cursor frame + simpler renderer
wass08 May 14, 2026
76794ce
Wire shelf + spawn placement polish: SFX, cursor, sidebar, selection
wass08 May 14, 2026
d17e083
Add sfx:grid-snap on cursor cell-cross for shelf + spawn
wass08 May 14, 2026
857ddd4
Register 'shelf' in selection managers (5 arrays + 1 type union)
wass08 May 14, 2026
6d97a87
Selection: registry-driven, drop spawn flag, restore green color
wass08 May 14, 2026
a090c22
Move + duplicate for registry kinds via MoveRegistryNodeTool
wass08 May 14, 2026
e84b1ec
Add preview slot to NodeDefinition; show translucent shape during move
wass08 May 14, 2026
c792f9c
Move: drop preview overlay, let the actual mesh follow via live trans…
wass08 May 14, 2026
7bd34e5
Move: update node.position directly per tick (matches item pattern)
wass08 May 14, 2026
166e860
Move: pure imperative via sceneRegistry (smooth, zero re-renders); dr…
wass08 May 14, 2026
d254582
MoveTool: dispatch registry-first so spawn move uses MoveRegistryNode…
wass08 May 14, 2026
9d17683
Add ParametricInspector — auto-derive right-panel UI from definition.…
wass08 May 14, 2026
f874cec
ParametricInspector: fine-grained per-field subscriptions
wass08 May 14, 2026
7f24593
Shelf: move geometry build into a system, slim renderer
wass08 May 15, 2026
7b946ce
wiki: add node-definitions doc for three-checkbox composition model
wass08 May 15, 2026
0a723fa
Wall Phase 3 milestone A: registry skeleton (metadata only)
wass08 May 15, 2026
02aeca8
Wall Phase 3 milestone B: runtime port behind feature flag
wass08 May 15, 2026
375914a
Wall: paired verification logs for registry vs legacy dispatch
wass08 May 15, 2026
60117e8
WallSystem: throttle adjacent-wall rebuild during drag
wass08 May 15, 2026
3f3818f
Phase 4: generic GeometrySystem + ParametricNodeRenderer; shelf ports…
wass08 May 15, 2026
ac36297
Phase 4 follow-on: registry-driven floor-plan rendering + shelf port
wass08 May 15, 2026
d10d7f8
Phase 4 follow-on: floor-plan interaction + def.toolHints + spawn flo…
wass08 May 15, 2026
a3fb5bd
FloorplanRegistryLayer: strip drag-to-move, keep click-to-select
wass08 May 15, 2026
773b58c
Floor-plan: registry-driven action menu + cursor-driven move overlay
wass08 May 15, 2026
713ef50
Floor-plan registry: snap, real-SVG move, commit on pointerup, placem…
wass08 May 15, 2026
9883f1c
Phase 5 first batch kind: fence migrates to registry behind feature flag
wass08 May 15, 2026
6a4de8c
Drop wall + fence feature flags: register unconditionally; remove ver…
wass08 May 15, 2026
71b211d
Fence + wall panels: stable handler refs via nodeRef to fix slider-dr…
wass08 May 15, 2026
fc9a5d0
MoveTool: dispatch by capabilities.movable, not nodeRegistry.has
wass08 May 15, 2026
4891f68
Phase 5 batch kind: slab migrates to registry (always-on)
wass08 May 15, 2026
2dd50fa
Phase 5 batch kind: ceiling migrates to registry (always-on)
wass08 May 15, 2026
9eced06
Phase 5 batch: door + window migrate to registry (always-on)
wass08 May 15, 2026
8d65be1
Phase 5 batch kind: item migrates to registry (always-on)
wass08 May 15, 2026
df07f7b
SelectionManager: route item to furnish phase before registry fallback
wass08 May 15, 2026
969b154
Phase 5 depth-first: spawn C, fence B+C, slab B+C, ceiling C
wass08 May 15, 2026
9bcb25d
Phase 5 Stage C continued: wall, door, window now in registry floor plan
wass08 May 15, 2026
9aec943
Phase 5 Stage C: item floor plan — all 9 kinds now at Stage C
wass08 May 15, 2026
95645d8
SelectionManager: route furnish-category registry kinds through furni…
wass08 May 15, 2026
8ca9686
Phase 5 Stage D: fence curve affordance → registry DragAction
wass08 May 15, 2026
1082b62
useDragAction: activation-click grace + curve fence commit sfx
wass08 May 15, 2026
ee309ec
drag-session: dispose() is now silent — does not fire onCancel
wass08 May 15, 2026
05efa25
Phase 5 Stage D fence: port MoveFenceEndpointTool to DragAction
wass08 May 15, 2026
5e67ffd
fence curve: single-undo dance in commit — fix undo skipping past drag
wass08 May 15, 2026
36c48b7
chore: biome auto-format pass (resolve persistent dirty-tree noise)
wass08 May 15, 2026
c16878c
Phase 5 Stage D fence: port MoveFenceTool to DragAction (whole-fence …
wass08 May 15, 2026
7b5a1c6
Phase 5 Stage D fence: port FenceTool placement to def.tool
wass08 May 15, 2026
b2e5d84
Phase 5 Stage D slab: port placement + move + boundary/hole editors
wass08 May 15, 2026
de3efa1
Phase 5 Stage D ceiling: port placement + move + boundary/hole editors
wass08 May 15, 2026
7f2f9c6
Phase 5 Stage D wall: port CurveWallTool to DragAction (first wall D)
wass08 May 15, 2026
d43cd56
drag-session: tests pinning single-undo-dance behavior
wass08 May 18, 2026
c2c2f66
Phase 5 Stage D: fix bend history + move-tool loop + grid-snap sfx
wass08 May 18, 2026
d1231b4
Phase 5 Stage D moves: live-drag for slab/ceiling + cursor follows po…
wass08 May 18, 2026
da63081
Phase 5 Stage D: revert curve + whole-item move affordances to legacy
wass08 May 18, 2026
1e15e10
Phase 5 Stage D: re-port curve + whole-item move tools 1:1 from legacy
wass08 May 18, 2026
f4ea07e
Phase 5 Stage D moves: live-drag mesh.position for slab / ceiling / f…
wass08 May 18, 2026
82c9b5e
Phase 5 Stage D moves: align useLiveTransforms with the direct mesh.p…
wass08 May 18, 2026
c3e255b
wiki(tools): clarify useLiveTransforms.position semantics for polygon…
wass08 May 18, 2026
3c37ff0
Phase 5 Stage E: fence / slab / ceiling drop their legacy panels
wass08 May 18, 2026
e7cb976
nodes: declare lucide-react as peer dependency
wass08 May 18, 2026
80199dc
parametrics: add `display: 'segmented'` enum hint + drop fence color
wass08 May 18, 2026
282cb22
parametrics: add `custom` field kind + restore fence Length / Curve
wass08 May 18, 2026
8c2b03f
parametric-inspector: render `def.presentation.icon` before the title
wass08 May 18, 2026
6a1d853
IconRef: add `url` kind, point registered kinds at palette assets
wass08 May 18, 2026
56e022a
Phase 5 Stage D wall: port endpoint move, whole-move, placement (1:1 …
wass08 May 18, 2026
9245672
shelf: v2 — cubby default, withBottom, item hosting, paintable surface
wass08 May 19, 2026
586cec8
registry/move-tool: fix 3D drag — keep rotation + stop transform reset
wass08 May 19, 2026
7a92641
floorplan/registry-layer: z-order buckets + overlay pass + click guard
wass08 May 19, 2026
3419cf8
floorplan/slab: lower fill opacity so zones show through
wass08 May 19, 2026
0dee774
floorplan/zone: name label inside polygon + polygon editor on select
wass08 May 19, 2026
0540692
item: fix 2D move — floor items stay parented to the level
wass08 May 19, 2026
11015ea
wiki: document parametric-node + move-tool pitfalls
wass08 May 19, 2026
d747d2f
Phase 5 Stage E: full kind migration into packages/nodes
wass08 May 19, 2026
1ec65ac
registry: post-migration polish — ceiling 3D selection, ceiling item …
wass08 May 19, 2026
c6bef1e
floorplan-panel: dismantle legacy SVG layers replaced by the registry
wass08 May 19, 2026
ff14762
floorplan-panel: remove dead action menu, handle layers, selected* st…
wass08 May 19, 2026
a3dc48a
floorplan-panel: prune dead drag state, useEffects, and measurement h…
wass08 May 19, 2026
78e3ed1
floorplan-panel: prune dead constants, drag-state types, and unused h…
wass08 May 19, 2026
9a481f2
registry: generic FloorElevationSystem driven by a `floorPlaced` capa…
wass08 May 19, 2026
e3f2343
nodes: restore floorPlaced imports stripped by the formatter
wass08 May 19, 2026
2bf824c
floor-elevation: restore stripped import + fix column rotation type
wass08 May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions apps/editor/components/scene-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import '../lib/bootstrap'
import {
applySceneGraphToEditor,
Editor,
Expand Down
50 changes: 50 additions & 0 deletions apps/editor/lib/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { discoverPlugins, loadPlugin, nodeRegistry } from '@pascal-app/core'
import { builtinPlugin } from '@pascal-app/nodes'

// Idempotency guard: HMR can reload this module, but `registerNode` throws on
// duplicate kinds. The flag lives in the module closure so it's reset on a
// hard reload but survives within a session.
let loaded = false

function isDev(): boolean {
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
?.env
return env?.NODE_ENV !== 'production'
}

export async function loadBuiltinNodes(): Promise<void> {
if (loaded) return
loaded = true
await loadPlugin(builtinPlugin)

// Phase 6 plugin discovery hook. Always called; default impl returns
// `[]`. Apps that ship external node packs override the discovery via
// `setPluginDiscovery(...)` before this module loads. See
// `wiki/editor-plugin-authoring.md` for the contract.
const externals = await discoverPlugins()
for (const plugin of externals) {
await loadPlugin(plugin)
}

if (isDev()) {
const kinds = Array.from(nodeRegistry.entries(), ([k]) => k)
if (typeof console !== 'undefined') {
// Visible in the browser dev console — the verification anchor for
// "which path is running this kind?" Empty array = every kind is on
// the legacy path. Kind in the array = registry path is live for it.
console.info(
`[pascal:registry] loaded ${builtinPlugin.id} v${builtinPlugin.apiVersion} (${kinds.length} kinds: ${kinds.join(', ') || '∅'})${externals.length > 0 ? ` + ${externals.length} discovered plugin(s)` : ''}`,
)
}
// Expose the registry on window for ad-hoc dev inspection. In prod the
// registry is reachable through @pascal-app/core's exports only.
if (typeof globalThis !== 'undefined') {
;(globalThis as { __pascalNodeRegistry?: typeof nodeRegistry }).__pascalNodeRegistry =
nodeRegistry
}
}
}

// Run as a side effect on first import so any consumer of this module gets a
// populated registry without remembering to call the function explicitly.
void loadBuiltinNodes()
1 change: 1 addition & 0 deletions apps/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@pascal-app/core": "*",
"@pascal-app/editor": "*",
"@pascal-app/mcp": "*",
"@pascal-app/nodes": "*",
"@pascal-app/viewer": "*",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-three/drei": "^10.7.7",
Expand Down
Binary file added apps/editor/public/icons/shelf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@
}
}
}
},
{
"includes": [
"packages/core/**/*.ts",
"packages/core/**/*.tsx",
"packages/viewer/**/*.ts",
"packages/viewer/**/*.tsx",
"packages/editor/**/*.ts",
"packages/editor/**/*.tsx"
],
"linter": {
"rules": {
"style": {
"noRestrictedImports": {
"level": "error",
"options": {
"paths": {
"@pascal-app/nodes": "Framework packages must not import from @pascal-app/nodes — consult nodeRegistry.get(kind) instead. See plans/editor-node-registry.md."
}
}
}
}
}
}
}
]
}
29 changes: 29 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
"import": "./dist/utils/clone-scene-graph.js",
"default": "./dist/utils/clone-scene-graph.js"
},
"./registry": {
"types": "./dist/registry/index.d.ts",
"import": "./dist/registry/index.js",
"default": "./dist/registry/index.js"
},
"./schema": {
"types": "./dist/schema/index.d.ts",
"import": "./dist/schema/index.js",
Expand Down Expand Up @@ -54,6 +59,8 @@
"scripts": {
"build": "tsc --build",
"dev": "tsc --build --watch",
"test": "bun test",
"bench:registry": "bun run src/registry/__bench__/relations-resolver.bench.ts",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/events/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type {
LevelNode,
RoofNode,
RoofSegmentNode,
ScanNode,
ShelfNode,
SiteNode,
SlabNode,
SpawnNode,
Expand All @@ -35,7 +37,14 @@ export interface GridEvent {
*/
localPosition: [number, number, number]
faceIndex?: number
object: Object3D
/**
* Optional: the hit Three.js object. Present when the grid event was
* synthesized from a R3F mesh hit (the legacy grid-plane mesh path);
* absent when emitted by the canvas-level raycaster in
* `use-grid-events.ts`, where there is no specific mesh to attribute
* the intersection to.
*/
object?: Object3D
nativeEvent: ThreeEvent<PointerEvent>
}

Expand All @@ -57,6 +66,7 @@ export type SiteEvent = NodeEvent<SiteNode>
export type BuildingEvent = NodeEvent<BuildingNode>
export type LevelEvent = NodeEvent<LevelNode>
export type ZoneEvent = NodeEvent<ZoneNode>
export type ShelfEvent = NodeEvent<ShelfNode>
export type SlabEvent = NodeEvent<SlabNode>
export type SpawnEvent = NodeEvent<SpawnNode>
export type CeilingEvent = NodeEvent<CeilingNode>
Expand All @@ -68,6 +78,8 @@ export type StairSegmentEvent = NodeEvent<StairSegmentNode>
export type WindowEvent = NodeEvent<WindowNode>
export type DoorEvent = NodeEvent<DoorNode>
export type ElevatorEvent = NodeEvent<ElevatorNode>
export type ScanEvent = NodeEvent<ScanNode>
export type GuideEvent = NodeEvent<GuideNode>

// Event suffixes - exported for use in hooks
export const eventSuffixes = [
Expand Down Expand Up @@ -189,6 +201,7 @@ type EditorEvents = GridEvents &
NodeEvents<'level', LevelEvent> &
NodeEvents<'zone', ZoneEvent> &
NodeEvents<'slab', SlabEvent> &
NodeEvents<'shelf', ShelfEvent> &
NodeEvents<'spawn', SpawnEvent> &
NodeEvents<'ceiling', CeilingEvent> &
NodeEvents<'column', ColumnEvent> &
Expand All @@ -198,6 +211,8 @@ type EditorEvents = GridEvents &
NodeEvents<'stair-segment', StairSegmentEvent> &
NodeEvents<'window', WindowEvent> &
NodeEvents<'door', DoorEvent> &
NodeEvents<'scan', ScanEvent> &
NodeEvents<'guide', GuideEvent> &
CameraControlEvents &
ToolEvents &
GuideEvents &
Expand Down
80 changes: 46 additions & 34 deletions packages/core/src/hooks/scene-registry/scene-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,75 @@
import { useLayoutEffect } from 'react'
import type * as THREE from 'three'

// `byType` is a Proxy-backed Map keyed by kind. Sets are created lazily on
// first access, so any kind (built-in or plugin-contributed) participates
// without needing a hardcoded seed list. The previous `KNOWN_NODE_KINDS`
// array was a pre-seed for autocomplete; with every kind now flowing
// through `nodeRegistry`, the seed is redundant.
//
// The type expresses that *any* string key returns a `Set<string>` — the
// Proxy auto-creates on first access so there's no `undefined` branch at
// runtime. Without this shape, `noUncheckedIndexedAccess` would force
// every caller to defend against an impossible undefined.
type ByTypeMap = { [kind: string]: Set<string> }
const byTypeStore = new Map<string, Set<string>>()

const byTypeProxy = new Proxy({} as ByTypeMap, {
get(_target, key) {
if (typeof key !== 'string') return undefined
let set = byTypeStore.get(key)
if (!set) {
set = new Set<string>()
byTypeStore.set(key, set)
}
return set
},
ownKeys() {
return Array.from(byTypeStore.keys())
},
has(_target, key) {
return typeof key === 'string' && byTypeStore.has(key)
},
getOwnPropertyDescriptor(_target, key) {
if (typeof key !== 'string') return undefined
const set = byTypeStore.get(key)
if (!set) return undefined
return { configurable: true, enumerable: true, value: set, writable: false }
},
})

export const sceneRegistry = {
// Master lookup: ID -> Object3D
nodes: new Map<string, THREE.Object3D>(),

// Categorized lookups: Type -> Set of IDs
// Using a Set is faster for adding/deleting than an Array
byType: {
site: new Set<string>(),
building: new Set<string>(),
ceiling: new Set<string>(),
column: new Set<string>(),
elevator: new Set<string>(),
level: new Set<string>(),
wall: new Set<string>(),
fence: new Set<string>(),
item: new Set<string>(),
slab: new Set<string>(),
spawn: new Set<string>(),
zone: new Set<string>(),
roof: new Set<string>(),
'roof-segment': new Set<string>(),
stair: new Set<string>(),
'stair-segment': new Set<string>(),
scan: new Set<string>(),
guide: new Set<string>(),
window: new Set<string>(),
door: new Set<string>(),
},
// Categorized lookups: Kind -> Set of IDs. Backed by a Proxy so any kind
// gets a Set on first touch — no hardcoded list.
byType: byTypeProxy,

/** Remove all entries. Call when unloading a scene to prevent stale 3D refs. */
clear() {
this.nodes.clear()
for (const set of Object.values(this.byType)) {
for (const set of byTypeStore.values()) {
set.clear()
}
},
}

export function useRegistry(
id: string,
type: keyof typeof sceneRegistry.byType,
ref: React.RefObject<THREE.Object3D>,
) {
export function useRegistry(id: string, type: string, ref: React.RefObject<THREE.Object3D>) {
useLayoutEffect(() => {
const obj = ref.current
if (!obj) return

// 1. Add to master map
sceneRegistry.nodes.set(id, obj)

// 2. Add to type-specific set
sceneRegistry.byType[type].add(id)
// 2. Add to type-specific set — Proxy auto-creates on first access.
sceneRegistry.byType[type]!.add(id)

// 4. Cleanup when component unmounts
// 3. Cleanup when component unmounts
return () => {
sceneRegistry.nodes.delete(id)
sceneRegistry.byType[type].delete(id)
sceneRegistry.byType[type]!.delete(id)
}
}, [id, type, ref])
}
Loading
Loading