Fixes breakage introduced by #320 which changed `SiteNode.children` from
embedded `BuildingNode | ItemNode` objects to flat `string[]` IDs.
- `packages/mcp/src/lib/rehydrate-site-children.ts`: replace now-obsolete
re-embedding logic with a no-op passthrough (call-site compatible)
- `packages/mcp/src/tools/variants/generate-variants.ts`: drop the inline
copy of the same function and its call
- `packages/nodes/src/site/renderer.tsx`: cast `childId as AnyNodeId` since
`SiteNode.children` is now `string[]`, not `AnyNodeId[]`
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Loading JSON previously called setScene blindly and crashed when
the file held schema-invalid nodes (e.g. items missing `asset`).
The new dialog parses the file, runs validateBuildJson in core,
and surfaces structure counts (site/building/levels/walls/doors/
windows/items/slabs/ceilings/zones/scans), floor area, and a
per-node Schema Details list grouped by type. Import is blocked
when any hard error exists.
Two schema bugs the validator surfaced are fixed here too:
- SiteNode.children is now an id array like every other node
(was a discriminatedUnion of full objects; three readers carried
a string-or-object ternary that's now dropped). migrateNodes
flattens legacy nested-object children on load. Default-scene
seed and photo-to-scene MCP builder updated to pass `building.id`.
- LevelNode.children now includes shelf — the editor allowed it
but the schema didn't. The schema-vs-registry-as-source-of-truth
discussion is captured in plans/editor-node-registry.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 6 of the node-registry plan landed the @pascal-app/nodes package,
the three-checkbox composition model (def.geometry / def.renderer /
def.system), capability-driven move dispatch, and the deletion of
per-kind dispatch / per-kind files across viewer + editor. Update the
review skill so it catches reintroductions of those legacy patterns
and reviews registry-driven additions against the new contract.
- Add packages/nodes as the 4th layer in the package-boundary pass.
- Add a "Node registry & composition" checklist section.
- Require reviewers to read node-definitions.md and plugin-authoring.md.
- Flag new `case '<kind>':` clauses, kind-specific files in legacy
locations, framework imports of @pascal-app/nodes, force-routing
bespoke-move kinds through MoveRegistryNodeTool, useLiveTransforms-
driven drag motion, builders that import useScene, missing
`__fromGeometry` markers, and v1-plugin-surface overreach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
251af2c added a `?.` on cursorRef in onGridClick but inadvertently
deleted the closing braces, the trailing reset block, and the
onKeyDown/onKeyUp/onCancel handlers — leaving onGridClick syntactically
unterminated and three undefined references at the emitter.on/off sites.
Restores the deleted block verbatim, keeping the optional-chaining null
guard on cursorRef.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone editor app loaded `lib/bootstrap.ts` as a side-effect
import only from `components/scene-loader.tsx`. That worked for the
`/edit/[sceneId]` route but left every other page (homepage, settings,
viewer-only routes) hitting `<Viewer>` with an empty client-side
registry — node materials resolved to `null` and React surfaced a
`<html>`-level hydration mismatch on first paint.
Fix mirrors the community-app side that landed in pascalorg/private-
editor#27:
- New `app/client-bootstrap.tsx` — thin client wrapper that imports
`../lib/bootstrap` and renders children.
- `app/layout.tsx` mounts `<ClientBootstrap>` around `{children}` so
every page in the standalone editor gets the registry populated
before its first `<Viewer>` / `<Editor>` mounts.
- `lib/bootstrap.ts` switched to **synchronous** built-in registration
via `registerNode(def)` per kind instead of `await loadPlugin(...)`.
The previous async kick-off only resolved in a microtask, letting
the first SSR / hydration pass see an empty registry. External
plugin discovery (`discoverPlugins()`) stays async and runs via its
own `loadExternalPlugins()` path, gated by `externalsKickedOff` so
HMR doesn't re-fetch.
- `components/scene-loader.tsx` drops the per-page side-effect import
— the root provider handles it now.
`bun.lock` syncs `@pascal-app/editor` into `@pascal-app/nodes`'s
peerDependencies + devDependencies (already declared in
`packages/nodes/package.json`; only the lockfile lagged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-up fixes after the FloorElevationSystem landed:
- `viewer/src/components/viewer/index.tsx`: the auto-format hook
stripped the `FloorElevationSystem` import a second time, leaving
the JSX mount unresolved.
- `packages/nodes/src/column/definition.ts`: column stores Y rotation
as a scalar `number`, but `floorPlaced.footprint` types the rotation
field as the full Euler tuple. Wrap as `[0, column.rotation, 0]`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit's `floorPlaced` capability used `getScaledDimensions`
in `item/definition.ts` and `ColumnNodeType` in `column/definition.ts`,
but the editor's auto-format hook ran between the import edit and the
body edit and removed both as "unused" — breaking `bun dev`.
Re-add the imports.
Symptom: a shelf placed on a level with a raised slab underneath
visually clipped through it — `ItemSystem` lifted items onto slabs,
but shelves (and other floor-placed registry kinds) had no equivalent
path.
Fix: lift the slab-elevation logic out of `ItemSystem` into a generic
`<FloorElevationSystem>` keyed off a new `capabilities.floorPlaced`
config. Any kind that opts in declares a `footprint(node)` (dimensions
+ rotation used to query overlapping slabs) and an optional `applies`
predicate (skips items whose `asset.attachTo` is wall / ceiling).
The new system runs at frame priority 1 so its `mesh.position.y`
override lands before `ItemSystem` / `GeometrySystem` (priority 2)
clear the dirty mark. The spatial-grid sync's `markNodesOverlappingSlab`
also dropped its hardcoded `item` branch in favour of an iteration over
every registered kind that declares `floorPlaced` — so any new
floor-placed kind picks up slab-driven re-elevation automatically.
Tagged kinds:
- `item` — `footprint = getScaledDimensions`, `applies = !asset.attachTo`
- `shelf` — `footprint = (w, h, d)`
- `column` — `footprint = (w, h, d)`
- `spawn` — `footprint = (0.6, 1.8, 0.6)` (marker)
`ItemSystem` retains only the wall-side z-offset block (`mesh.position.z =
wallThickness / 2`). The elevation block + its `getScaledDimensions` /
`resolveLevelId` / `spatialGridManager` imports moved to the generic
system.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final sweep after the layers, handlers, and state were dismantled —
pruning the remaining orphan types and constants that only the dead
code referenced.
Removed:
- 27 dead constants: `FLOORPLAN_*_HOVER_*_STROKE_WIDTH` for wall /
item / endpoint (consumed by the deleted legacy SVG layers),
`FLOORPLAN_WALL_OUTER_MEASUREMENT_*` + `FLOORPLAN_WALL_INNER_
MEASUREMENT_*` + `FLOORPLAN_OPENING_MEASUREMENT_*` palette
constants (no live measurement consumer), `FLOORPLAN_ITEM_
CLEARANCE_*` thresholds, `FLOORPLAN_MEASUREMENT_LABEL_*` / `LINE_
OUTLINE_*` + `FLOORPLAN_ACTION_MENU_OFFSET_Y` / `FLOORPLAN_NODE_
FOOTPRINT_*` / `FLOORPLAN_SPAWN_*` / `FLOORPLAN_TRACE_*_FILL_
OPACITY` + several `FLOORPLAN_WALL_*_STROKE_WIDTH` variants.
- Dead drag-state types: `SlabBoundaryDraft`, `SlabHoleBoundaryDraft`,
`SlabVertexDragState`, `SlabHoleVertexDragState`, `SlabHoleMoveDraft`,
`CeilingBoundaryDraft`, `CeilingVertexDragState`,
`CeilingHoleBoundaryDraft`, `CeilingHoleVertexDragState`,
`CeilingHoleMoveDraft`, `ZoneBoundaryDraft`, `ZoneVertexDragState`,
`WallFaceLine`.
- Dead helpers: `getWallMeasurementOverlay` + `getLinearMeasurementOverlay`
(both defined but never called now that the measurement layers are
gone).
Floor-plan panel: 9,006 → 8,651 lines (-355). Cumulative reduction
vs `main`: 17,913 → 8,651 (-9,262 lines, ~52%).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the legacy SVG layers and handler callbacks were dismantled, the
backing drag-state useStates, mirror useRefs, and watch-effects became
orphans, along with a handful of top-level math helpers whose callers
were already gone.
Removed:
- 12 dead `useState` declarations: `slabBoundaryDraft` /
`slabVertexDragState` / `slabHoleBoundaryDraft` /
`slabHoleVertexDragState` / `slabHoleMoveDraft` / `ceilingBoundary
Draft` / `ceilingVertexDragState` / `ceilingHoleBoundaryDraft` /
`ceilingHoleVertexDragState` / `ceilingHoleMoveDraft` /
`zoneBoundaryDraft` / `zoneVertexDragState`. All written only by
the dead vertex-drag handlers and read only by the dead
`clear*BoundaryInteraction` callbacks and `transientFloorplanFit`
checks (which collapse to `false` once their inputs are gone).
- 5 dead `useRef` mirrors of the above (`slabBoundaryDraftRef` etc.)
plus their mirror `useEffect` writes.
- 5 dead `clear*BoundaryInteraction` callbacks (slab, slabHole,
ceiling, ceilingHole, zone) — only called by deleted useEffects
and by `clearDraft`, where the call became a no-op.
- 7 dead `useEffect` watchers for `slabVertexDragState`,
`ceilingVertexDragState`, `slabHoleVertexDragState`,
`slabHoleMoveDraft`, `ceilingHoleVertexDragState`,
`ceilingHoleMoveDraft`, `zoneVertexDragState` (each early-returns
because state is always null, so all listener wiring + commit /
cancel paths inside were unreachable).
- 5 dead `shouldShow*BoundaryHandles` flags + the 5 `useEffect`s
that called `clear*BoundaryInteraction` when they flipped.
- `selectedSlabEditingHoleIndex` / `selectedSlabEditingHole` /
`selectedCeilingEditingHoleIndex` / `selectedCeilingEditingHole`
— derived from now-`null` selectedX entries.
- Dead drag-state guards in `handlePointerMove`, `handleSvgPointer
Move`, and the floor-plan fit `useMemo`.
- Dead top-level helpers: `getRaySegmentIntersection`,
`getSlabHandlePolygon`, `getSlabVisualOffsets`,
`getDraftSlabVisualPolygon`, `WallMeasurementFaceContext` type,
`getWallMeasurementFaceContext`, `getAdjacentOpeningBounds`,
`getSelectedWallMeasurementOverlays`,
`getItemDimensionMeasurementOverlays`, `polygonCentroid`.
Floor-plan panel: 10,466 → 9,006 lines (-1,460). Cumulative reduction
vs `main`: 17,913 → 9,006 (-8,907 lines, **~50%**).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continuing the post-migration cleanup. After registry-driven kinds
absorbed selection chrome, action menus, and boundary editing through
`def.floorplanAffordances`, the legacy plumbing in `floorplan-panel.tsx`
was left mounted with empty inputs — another ~4k LoC of dead code.
Removed:
- `Editor2dFloorplanActionMenuLayer` mount + its 10 `selectedX
ActionMenuPosition` useMemos. All ten action menus computed positions
from empty entry arrays / null `selectedXEntry`, so the layer never
rendered anything. `FloorplanRegistryActionMenu` is the only mount left.
- Legacy handle layers: `FloorplanWallEndpointLayer`,
`FloorplanFenceEndpointLayer`, `FloorplanWallCurveHandleLayer`, and
the four `FloorplanPolygonHandleLayer` mounts for slab / slab-hole /
ceiling / ceiling-hole — all rendered from `wallEndpointHandles` /
`slabVertexHandles` / etc. which were empty after the registry took
over endpoint, curve, and polygon affordances.
- `FloorplanZoneLabelLayer` mount + component. Zone labels are now
emitted as `kind: 'text'` from `def.floorplan` on the zone kind.
- Dead handlers: every `handle*Select`, `handle*PointerDown`,
`handle*DoubleClick`, `handle*HoverChange`, `handleFloorplan*Hover
Enter`, `handleSelected*` (Move/Delete/Duplicate/AddHole/HoleMove/
HoleDelete/Curve for all 10 legacy kinds), `duplicateSelected*`,
`handleSelectedWallCurve`, `handleSlabVertex*`/`Midpoint*`/`Edge*`/
`Hole*` (same for ceiling and zone), `handleWallEndpointPointerDown`,
`handleFenceEndpointPointerDown`, `handleWallCurvePointerDown`,
`emitFloorplanNodeClick`, `syncDeleteHoveredId`,
`handleZoneLabelClick`, `hasDuplicatableFloorplanSelection`,
`handleDuplicateFloorplanSelection`, `FloorplanDuplicateHotkey` mount.
- Dead handle data: `wallEndpointHandles`, `fenceEndpointHandles`,
`wallCurveHandles`, `canCurveSelectedWall`, and all the
slab/slab-hole/ceiling/ceiling-hole/zone vertex+midpoint+edge handle
useMemos.
- Dead selection / measurement useMemos: `selectedItemEntry`,
`selectedOpeningEntry`, `selectedSpawnEntry`, `selectedFenceEntry`,
`selectedStairEntry`, `selectedRoofEntry`, `selectedElevatorEntry`,
`selectedWallEntry`, plus `selectedItemClearanceMeasurements` and
`movingOpeningPlacementMeasurements` (both 100-200 lines of math
that consumed the empty entries).
- Two dead `<FloorplanMeasurementsLayer>` mounts (clearance / opening
placement) — both fed by useMemos that returned `[]`.
- `<FloorplanZoneLayer>` mount — `visibleZonePolygons` is always empty.
Kept:
- `siteVertexHandles` / `siteMidpointHandles` + `<FloorplanPolygonHandle
Layer>` mount for site — site is the only kind not registry-driven.
- `FloorplanStairLayer` mount (preview-only, hover/click props swapped
for noop helpers since the preview isn't interactive).
- Stub `selectedSlabEntry` / `selectedCeilingEntry` / `selectedZoneEntry`
as `null` typed values so the remaining hole-editing fallback code
compiles. Those fallbacks themselves run as no-ops now and can be
torn down in a follow-up.
Floor-plan panel: 14,395 → 10,466 lines (-3,929). Cumulative reduction
vs `main`: 17,913 → 10,466 (-7,447 lines, ~42%).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 5 / 6 migration moved 7 kinds (fence, column, spawn, item,
elevator, stair, roof) to the registry's `def.floorplan` path, but the
legacy SVG layers in `floorplan-panel.tsx` were left mounted with empty
entry arrays — dead code carrying ~3k lines of useless cost in the diff.
Removed:
- `FloorplanGeometryLayer` (~1.7k lines): inline component rendering
walls / slabs / ceilings / openings from `wallPolygons` /
`slabPolygons` / `ceilingPolygons` / `openingsPolygons` — all
permanently empty stubs after the migration. Wall / slab / ceiling
/ door / window now render via `FloorplanRegistryLayer`.
- `FloorplanFenceLayer` (~265 lines): fence entries always empty
post-migration; fence renders via the registry.
- `FloorplanElevatorLayer` (~420 lines): elevator entries always
empty post-migration.
- `FloorplanNodeLayer` (~415 lines): rendered items / spawns / stairs.
Items + spawns are registry-driven; stair only needed the in-flight
preview, which is now mounted directly via `FloorplanStairLayer`
(preserved as a sibling of the registry layer).
- `FloorplanItemImage` (~40 lines): internal helper used only by
`FloorplanNodeLayer`.
- `floorplan-roof-layer.tsx` (113 lines): roof / roof-segment now
registry-driven.
Net: -2,989 lines from `floorplan-panel.tsx`, plus the deleted roof
layer file. The remaining 14k-line monolith still owns the orchestration
state (selection lookups, marquee, helper lifecycles, hit-test plumbing)
— that's a separate teardown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three bugs surfaced after the Stage E node-registry migration:
1. Ceiling intercepts 3D hover/click selection
Selecting via the floor-plan helper or the boundary-editor handles
is the intended flow; a direct 3D click on the ceiling should fall
through to whatever's underneath. `SelectionManager` now early-returns
on `ceiling` in onEnter/onLeave/onClick, so `event.stopPropagation`
is skipped and the ray reaches the item/wall/floor below.
2. Ceiling item placement: final click does nothing
When a ceiling-attached draft hangs in front of the ceiling-grid
mesh, the click ray hits the draft first and fires `item:click`,
not `ceiling:click`. `onItemClick` already forwards self-clicks to
shelf-surface / item-surface hosts; this PR adds the matching
ceiling branch so the commit lands on the ceiling under the cursor.
3. Floor-plan item move drift after the commit click
Two contributing causes, both fixed:
- `usePlacementCoordinator`'s `useFrame` lerped the draft mesh
toward `gridPosition.current` (the item's pre-move spot) every
frame, fighting React's render from `scene.position` while the
2D `FloorplanRegistryMoveOverlay` drove the move. Gated the lerp
on a `has3DPointerDrivenMoveRef` flag set on first 3D pointer
event — pure 3D drags are unchanged.
- The overlay's pointer-up handler skipped a final `session.apply`
and committed at the last pointermove position. Browsers don't
guarantee a pointermove right before pointerup, so a quick click
after a drag could land a few pixels off. Re-apply at pointer-up
coords so commit matches where the user actually released.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wholesale move of every remaining kind into its own subdirectory under
`packages/nodes/src/`, finishing the registry-driven migration. Each
kind now ships its definition, schema (re-exported from core), and any
of `geometry` / `renderer` / `system` / `floorplan` / `tool` /
`move-tool` / `panel` / `floorplan-move` / `floorplan-affordances` /
`parametrics` / `preview` it needs — no per-kind code remains under
`packages/editor/src/components/tools/` or
`packages/viewer/src/components/renderers/`.
Deleted (replaced by registry-driven equivalents):
- `tools/{ceiling,column,door,fence,item,slab,spawn,wall,window}/...`
(boundary editors, hole editors, placement tools, move tools,
endpoint movers, curve tools, helpers, math libs)
- `ui/helpers/{ceiling,slab,wall}-helper.tsx`
- `ui/panels/{column,door,elevator,item,roof,roof-segment,spawn,
stair,stair-segment,wall,window}-panel.tsx`
- `viewer/src/components/renderers/{building,ceiling,column,door,
elevator,fence,guide,item,level,roof,roof-segment,scan,site,slab,
spawn,stair,stair-segment,wall,window,zone}-renderer.tsx`
- `viewer/src/components/viewer/legacy-system.tsx`
Added under `packages/nodes/src/`:
- `building/`, `column/`, `elevator/`, `guide/`, `level/`, `roof/`,
`roof-segment/`, `scan/`, `shared/`, `site/`, `stair/`,
`stair-segment/` packages with definition + schema + renderer / system
/ floorplan / panel as appropriate.
- New `floorplan-move.ts` for every kind that supports 2D moves
(ceiling, door, item, shelf, slab, window) — single registry-driven
dispatch path via `def.floorplanMoveTarget`.
- New `floorplan-affordances.ts` for kinds with polygon / endpoint
drags (ceiling, fence, slab, wall) — using the shared
`polygon-vertex-affordance` factories.
- New per-kind `panel.tsx` for kinds with custom inspector content
(door, item, shelf, spawn, wall, window).
- New per-kind `tool.tsx` for placement (door, item, shelf, window).
- New per-kind `move-tool.tsx` for kinds with custom 3D move flows
(door, item, slab, window).
Coordinator + manager updates in `packages/editor/`:
- `tool-manager.tsx` resolves tools from the registry only — no
hardcoded type→component map.
- `panel-manager.tsx` resolves inspector panels the same way.
- `placement-{coordinator,strategies,types}.ts` extended with
shelf-surface placement.
- `selection-manager.tsx` adds the registry-selectable fallback.
- `floorplan-panel.tsx`, `floorplan-background-placement.ts`,
`floorplan-render-context.tsx` updated for the registry layer's new
contract (props, affordance dispatch, render context).
Viewer updates:
- `viewer/index.tsx` drops legacy renderer mounts.
- `node-renderer.tsx` resolves by registry only.
- `scene-bvh.tsx`, `use-node-events.ts`, `level-system.tsx`,
`wall-cutout.tsx`, `zone-system.tsx`, `materials.ts` adjusted for
the registry-only world.
Sidebar tree nodes for ceiling / fence / slab / shelf / tree-node
updated to read from the registered nodes instead of the deleted
legacy renderer trees.
Wiki: new `plugin-authoring.md` page, README index updated.
Tests in `packages/nodes/src/index.test.ts` validate every registered
kind has the required shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the gotchas surfaced while building the shelf so the next
contributor adding a registry-driven kind doesn't rediscover them.
`node-definitions.md` — new Pitfalls section + a rule that builders
must emit local-space children. Covers:
- `<GeometrySystem>` must NOT mutate `group.position` / `group.rotation`
after rebuild (the renderer binds them via JSX prop).
- Tag geometry-built children with `userData.__fromGeometry` so
rebuilds don't dispose React-mounted hosted children (the
item-disappears-on-shelf bug).
- Previews must clone materials before mutating them when the kind's
builder caches at module scope.
- Host kinds need a `children: z.array(...).default([])` field on their
schema (and a migration patch for older scenes).
`tools.md` — three new move/placement pitfalls:
- Disable raycast on the moved mesh during drag, otherwise it captures
the ray and starves `grid:move` → commits land at the stale start.
- Commit handlers listen to `grid:click` AND every `${kind}:click` to
catch clicks that land on neighbouring 3D geometry first.
- Move tools must preserve the node's actual `rotation[1]` in
`useLiveTransforms` — hardcoding 0 makes the node un-rotate mid-drag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related bugs caused floor items to disappear mid-2D-drag:
1. `startLevelId` did `parent.parentId` unconditionally for non-wall
parents — so for a floor item parented directly to the level (the
canonical convention from `use-placement-coordinator`), it returned
`level.parentId = building.id`. `findContainingSurface` then iterated
the building's children (levels, not slabs) and the fallback
`parentId: startLevelId` reparented the item to the building. Now we
walk the parent chain until we hit a `level` and short-circuit on
`parent.type === 'level'`.
2. `buildSurfaceItemSession` reparented floor items to a slab when the
cursor was over one. Slabs don't carry a `children` field on their
schema (only ceilings + level do), so `updateNodesAction`'s reparent
logic operated on `undefined.children` — the item dropped out of the
level→children DFS the floor-plan layer walks, and the polygon
stopped rendering mid-drag. Split into `buildFloorItemSession`
(always parents to the level, just updates position) and a
ceiling-only `buildSurfaceItemSession`. `findContainingSurface`
narrows to `'ceiling'` as the only valid target.
Matches the 3D `detachItemSurfaceToFloor` convention: floor items live
as level children, not slab children.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`buildZoneFloorplan` now emits a centered name label at the polygon's
area-weighted centroid (Shoelace formula, with bbox-center fallback for
degenerate rings). Label uses the legacy `FloorplanZoneLabel` styling:
`fontSize: 0.2`, white fill, zone-color stroke, `paintOrder: 'stroke'`
for the "outlined text" look that stays legible above any fill.
When the zone is selected the builder also emits the polygon editor —
edge-handle per edge, midpoint-handle per midpoint, endpoint-handle per
vertex — driven by the shared `createPolygonVertexAffordance` /
`createPolygonAddVertexAffordance` / `createPolygonMoveEdgeAffordance`
factories slabs and ceilings already use. Zones have no `holes` field
so the factory's optional `holeIndex` stays undefined and the operations
target `node.polygon` directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The slab body covered the underlying zone fill almost completely —
`opacity: 0.85` (gray) and `opacity: 0.95` (white-on-select) drowned out
any zone color sitting beneath. Switched to independent `fillOpacity`
(0.6 unselected / 0.45 selected) and `strokeOpacity` (0.85 / 0.96) so
the outline stays crisp while the zone color reads through the fill.
On the selected state the hatch overlay carries the visual weight; no
need for an opaque white background underneath.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three layered improvements on the 2D registry layer:
- Z-order buckets so the SVG document order reflects intent: zones (0)
paint first, slabs/ceilings (1) next, every other kind (walls / items /
shelves / columns / stairs / …) on top. Stable sort preserves DFS order
within a bucket.
- Base / overlay split: each entry's `FloorplanGeometry` tree is walked
through `splitFloorplanOverlay`, partitioning into a base group
(polygons, paths, fills, hatches) and an overlay group (interactive
handles + labels — `text` / `endpoint-handle` / `midpoint-handle` /
`edge-handle` / `move-handle` / `dimension` / `dimension-label`). Base
renders rank-sorted; overlays paint after every base entry so polygon-
editor chrome on a selected slab and zone name labels stay legible
above the structural fills sitting on top of them.
- Click guard on the outer layer `<g>`. The base/overlay split means
pointer-down lands on base and pointer-up lands on overlay (selection
mounts the overlay on top mid-gesture). The browser then dispatches
`click` to the lowest common ancestor, ABOVE the entry's
`onClick={handleClickStop}`. Without a higher-level stop, the click
reached the SVG's `handleBackgroundClick` → `clear-elements` and the
selection set on pointer-down vanished a frame later. Scoped to
`onClick` only so pointer / hover / drag still propagate inside the
registry tree.
Also extends the `text` FloorplanGeometry with stroke / strokeWidth /
paintOrder / fontFamily so kinds can match the legacy "white fill +
colored outline" label look (used by the new zone name label).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs caused dragged shelves to snap to a weird position on commit:
1. `MoveRegistryNodeTool` wrote `useLiveTransforms.set(id, { ..., rotation: 0 })`,
so during the drag `ParametricNodeRenderer` applied `<group rotation={[0,0,0]}>`
and the shelf visually un-rotated. On commit the live transform cleared
and the renderer re-read the node's true rotation — the snap-back read
as "reverts to a weird position." Now we capture `originalRotationY`
from the node at mount time and forward it on every set.
2. `<GeometrySystem>` reset `group.position.set(0,0,0)` /
`group.rotation.set(0,0,0)` after every rebuild. That was carry-over
from legacy per-kind systems that didn't bind `position` on the group.
`ParametricNodeRenderer` now drives the transform via JSX prop, and
the reset clobbered it — React doesn't necessarily re-render on a
rebuild tick, so R3F never re-applied the prop and the registered
`<group>` stayed at the origin. Removed the reset; builders are
expected to emit local-space children.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>