197 Commits

Author SHA1 Message Date
Wassim SAMAD
df07f7bcb2 SelectionManager: route item to furnish phase before registry fallback
User report: items needed a double click to select after the item kind
registered (Phase 5). Root cause: `getSelectionTarget` checked
`isRegistrySelectable(node.type)` as part of the FIRST branch (which
routes to structure phase), matching `item` before the item-specific
branch below could route door/window-category items to structure +
everything else to furnish.

Effect: clicking an item triggered phase switch (structure ← furnish),
then the next click selected. Hence the double click.

Fix:
 1. Item-specific case moved to the TOP of getSelectionTarget. Its
    asset.category-driven routing (door/window items → structure;
    everything else → furnish) beats any generic registry fallback.
 2. Generic registry fallback at the bottom now reads `def.category`
    to pick the phase — `category: 'furnish'` → furnish phase,
    everything else → structure/elements. Future furnish-category
    kinds (only shelf right now) route correctly without a special
    case.
 3. `isRegistrySelectable(node.type)` clause removed from the
    structure branch — replaced by the def.category check at the
    bottom.

Net: single-click selection works for items again, and the routing
logic is now cleanly capability/category-driven instead of "all
registered kinds → structure" which was a Stage A simplification
that broke as soon as a furnish-category kind registered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:29:06 -04:00
Wassim SAMAD
8d65be17fa Phase 5 batch kind: item migrates to registry (always-on)
Item is the first kind to use the `def.renderer` escape hatch (custom
React component with `useGLTF` + drei + interactive widgets) — not
expressible as a pure `def.geometry`. Catalog-backed + multi-host
(free / wall / wall-side / ceiling).

Files added (packages/nodes/src/item/):
 - schema.ts: re-exports ItemNode from core.
 - parametrics.ts: empty groups[]. Item parametrics come from the
   asset's catalog-defined interactive controls (toggles / sliders /
   temperature) — too dynamic for the auto-inspector at Stage A.
   Legacy ItemPanel renders the catalog-driven controls; Phase 5
   Stage E will likely use parametrics.customPanel.
 - definition.ts: capabilities (no `movable` — item move is bespoke
   MoveItemContent that handles attachTo transitions floor↔wall↔
   ceiling mid-drag; capability-driven dispatch keeps legacy mover),
   parametrics, renderer (wrap-export of ItemRenderer), system
   bundling ItemSystem + ItemLightSystem, toolHints matching the
   user's screenshot (Place item / R rotate ccw / T rotate cw /
   Shift free place / Esc cancel). defaults() casts an object
   literal with a stub asset since asset is required by the schema;
   createNode re-parses through ItemNode at runtime.
 - renderer.tsx: wrap-export of legacy ItemRenderer (~280 lines with
   useGLTF + interactive widgets — too much to duplicate at Stage A).
 - system.tsx: bundles ItemSystem + ItemLightSystem.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new public exports for ItemRenderer,
   ItemSystem, ItemLightSystem.
 - packages/nodes/src/index.ts: appends itemDefinition.
 - packages/editor/src/components/ui/panels/item-panel.tsx: panel
   slider-drag fix recipe applied (nodeRef pattern, drop subscribed
   updateNode dep, drop node from useCallback deps). Item panel has
   scale + position + rotation sliders all subject to the cascade.

Item is the registry's stress test for `def.renderer` escape hatch.
GLB loading via useGLTF + drei works as-is; nothing in the registry
forces a pure-geometry shape on kinds that don't fit.

Phase 5 progress: shelf ✅ spawn ✅ wall ✅ fence ✅ slab ✅ ceiling ✅
door ✅ window ✅ item ✅. Nine kinds on the registry. Stair / roof /
zone / containers remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:23:08 -04:00
Wassim SAMAD
9eced06f32 Phase 5 batch: door + window migrate to registry (always-on)
Both kinds share traits — hosted on walls, cuttable, animated open/
close state via a geometry system + animation system. Stage A
migration: register + wrap-export the legacy renderer + bundle both
per-kind systems. Pure geometry + floor-plan ports are later
milestones.

Files added (packages/nodes/src/door/, packages/nodes/src/window/):
 - schema.ts: re-export from core.
 - parametrics.ts: minimal — dimensions only. Door has 29 sliders +
   segmented controls + presets in its legacy panel; window has 15+
   sliders. Auto-inspector can't cover them at Stage A — legacy
   panel keeps rendering via panel-manager.tsx case fall-through.
   Stage E may extend parametrics or use parametrics.customPanel
   escape hatch.
 - definition.ts: capabilities (no `movable` — wall-bound drag is
   bespoke; capability-driven dispatch keeps legacy MoveDoorTool /
   MoveWindowTool), parametrics, renderer, system. defaults() uses
   `DoorNode.parse({...stub})` to leverage zod's schema-level
   `.default()` annotations — door has 40+ fields, window has 20+;
   listing them inline duplicates the schema.
 - renderer.tsx: wrap-export of legacy DoorRenderer / WindowRenderer
   (thin 33-36 lines each).
 - system.tsx: bundles each kind's TWO systems — DoorSystem +
   DoorAnimationSystem, WindowSystem + WindowAnimationSystem. Both
   per-kind systems mount via RegisteredSystems when the kind is
   registry-driven; `<LegacySystem kind="door|window">` wrappers
   around each individual system short-circuit.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new public exports for DoorRenderer,
   DoorSystem, DoorAnimationSystem, WindowRenderer, WindowSystem,
   WindowAnimationSystem.
 - packages/nodes/src/index.ts: appends doorDefinition + windowDefinition.
 - packages/editor/src/components/ui/panels/door-panel.tsx + window-
   panel.tsx: panel slider-drag fix recipe applied. Drop the
   subscribed `updateNode` action, drop the `node` dep from
   handleUpdate / previewDoorUpdate / commitDoorPreview useCallbacks.
   Use useScene.getState() inside. Door panel has 29 SliderControls,
   window 15+ — both at high risk of the Maximum update depth
   cascade without the fix.

Phase 5 progress: shelf ✅, spawn ✅, wall ✅, fence ✅, slab ✅, ceiling ✅,
door ✅, window ✅. Eight kinds on the registry. Item / stair / roof /
zone / containers remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:10:17 -04:00
Wassim SAMAD
2dd50fa5be Phase 5 batch kind: ceiling migrates to registry (always-on)
Structurally identical to slab. Stage A migration: registers the kind,
wraps the legacy renderer + system, applies the panel slider-drag fix
recipe.

Files added (packages/nodes/src/ceiling/):
 - schema.ts: re-exports CeilingNode from core.
 - parametrics.ts: height slider only. Polygon + holes via floor-plan
   editors.
 - definition.ts: capabilities (no `movable`, `surfaces.top` mapped to
   `height`), relations (hosts: ['item'] for ceiling-mounted lights /
   fans, cascadeDelete: 'descendants'), toolHints, parametrics.
 - renderer.tsx: wrap-export of legacy CeilingRenderer. The legacy
   renderer uses TSL shader code for grid-line patterns (~100 lines);
   not worth duplicating at Stage A. Per-stage migration plan in
   plans/editor-node-registry.md moves the renderer body into this
   folder at Stage B/F.
 - system.tsx: re-exports legacy CeilingSystem.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new public exports for CeilingRenderer
   + CeilingSystem.
 - packages/nodes/src/index.ts: appends ceilingDefinition.
 - packages/editor/src/components/ui/panels/ceiling-panel.tsx: panel
   slider-drag fix recipe applied (nodeRef pattern, useScene.getState()
   inside handler, drop subscribed updateNode dep) so the height
   slider doesn't trigger the same cascade fence + wall + slab fixed.

Phase 5 progress: shelf ✅, spawn ✅, wall ✅, fence ✅, slab ✅, ceiling ✅.
Six kinds on the registry. Door / window / item / stair / roof / zone
follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:32:01 -04:00
Wassim SAMAD
4891f681f3 Phase 5 batch kind: slab migrates to registry (always-on)
Same shape as fence — thin renderer + system re-export, capabilities
declared, panel slider-drag fix recipe applied. Pure geometry +
floor-plan ports are later milestones.

Files added (packages/nodes/src/slab/):
 - schema.ts: re-exports SlabNode from core.
 - parametrics.ts: elevation slider only. Polygon + holes edited
   via floor-plan boundary / hole editors, not number inputs.
 - definition.ts: capabilities (no `movable` — slab move is bespoke
   whole-translation through MoveSlabTool that integrates with the
   boundary editor; capability-driven dispatch keeps the legacy mover),
   surfaces.top with elevation-as-height for stacked items, relations
   (hosts: ['item'], cascadeDelete: 'descendants'), toolHints (trace /
   finish / cancel for the placement tool).
 - renderer.tsx: thin placeholder mesh + markDirty on mount + node
   events + cached material via the same getSlabMaterial pattern as
   the legacy renderer (preset apply on shared material instance).
 - system.tsx: re-exports the legacy SlabSystem from viewer.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: exports SlabSystem (already had
   DEFAULT_SLAB_MATERIAL, applyMaterialPresetToMaterials, createMaterial
   from earlier exports).
 - packages/nodes/src/index.ts: appends slabDefinition unconditionally
   to builtinPlugin.nodes.
 - packages/editor/src/components/ui/panels/slab-panel.tsx: applied
   the panel slider-drag fix recipe from plans/editor-node-registry.md
   prophylactically (nodeRef pattern, useScene.getState().updateNode
   inside handler, drop subscribed updateNode dep). Slab's elevation
   slider is the only drag-driven control in the panel — would have
   triggered the same Maximum update depth cascade as wall/fence.

No behavior change. Slab now mounts via the registry path, but the
legacy SlabSystem still does the actual polygon triangulation + hole
CSG work (re-exported, not duplicated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:12:45 -04:00
Wassim SAMAD
fc9a5d02a0 MoveTool: dispatch by capabilities.movable, not nodeRegistry.has
User report: after wall registered, the move tool's smart sims-style
arrow UX (endpoint handles + linked-wall corner cascade + ALT-detach)
was replaced by a generic whole-wall-translate. The dispatch was
unconditionally routing every registered kind through
MoveRegistryNodeTool — but MoveRegistryNodeTool is for kinds whose
move semantics are "translate position on X/Z plane" (shelf, spawn,
single-position items). Wall / fence / slab / stair endpoint drags
are bespoke and need their legacy movers until each gets a proper
DragAction-based affordance port.

Fix: gate the registry-mover dispatch on `def.capabilities.movable`.
When a kind opts in (`movable: { axes, gridSnap }`), use the generic
mover; when a kind omits the capability deliberately (wall and fence
do), fall through to the legacy per-kind branch below.

This is the registry-aware analogue of "the registry doesn't limit
custom behavior — it lets kinds opt in to generic dispatch". Adding
`movable` is an opt-in; omitting it is an opt-out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:02:29 -04:00
Wassim SAMAD
71b211de97 Fence + wall panels: stable handler refs via nodeRef to fix slider-drag loop
User report: dragging the Length slider on a fence triggered
"Maximum update depth exceeded" in updateNodesAction.

Root cause: the panels' `handleUpdate` / `handleUpdateLength`
useCallback deps included the subscribed `node` and `updateNode`
references. On every store tick during slider drag (one per
pointermove), Zustand notified subscribers → panel re-rendered →
new `node` ref → new handler refs → SliderControl re-rendered with
new onChange prop → its `useCallback([..., onChange])` for
handleLabelPointerMove rebuilt while pointer capture was active.
Combined with float drift in `getWallCurveLength` recomputing per
render, React eventually flagged the cascade as a
componentWillUpdate / componentDidUpdate loop.

Fix:
 - Mirror `node` into a `nodeRef` updated on every render. Handlers
   read from `nodeRef.current` instead of closing over `node`.
 - Drop the subscribed `updateNode` dep: use `useScene.getState().
   updateNode(...)` inside the handler. Same pattern ParametricInspector
   already uses for its registry-driven inspector.
 - Drop the now-redundant `useScene.getState().dirtyNodes.add(id)`
   call — updateNode's RAF markDirty already covers it.

Net effect: handler refs are stable across slider drags (only change
when `selectedId` changes). SliderControl's pointer listeners no longer
churn mid-drag. Cycle broken.

Same fix applied to wall-panel.tsx prophylactically — it has the
identical pattern and would exhibit the same loop under the right
float-drift / drag conditions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:20:01 -04:00
Wassim SAMAD
713ef5009e Floor-plan registry: snap, real-SVG move, commit on pointerup, placement events
Four issues in one pass:

1. Move snaps to grid

   MoveOverlay's cursor is now snapped via snapPointToGrid([m.x, m.y],
   GRID_STEP=0.5). Matches the 3D shelf tool's placement step so 2D
   and 3D placement feel identical.

2. Move translates the actual rendered SVG, not a ghost

   MoveOverlay no longer portals a 50%-opacity ghost. Instead it finds
   the rendered [data-node-id] <g> inside the floor-plan scene and sets
   its `transform` attribute imperatively each pointermove. The inner
   group's translate(px pz) rotate(deg) stays untouched — the outer
   transform composes as a pure delta. Same "smooth move" pattern as
   the 3D MoveRegistryNodeTool: no React re-renders, no zundo bloat,
   the actual shape follows the cursor with full fidelity.

3. Click commits the position (previously did nothing)

   Switched from `window click` (with capture + composedPath check)
   to `window pointerup`. Pointerup fires reliably regardless of
   click-vs-drag semantics in the floor-plan panel's pointer-down
   handlers (which can preventDefault on certain modes and suppress
   the synthesized click). Target check uses
   `target.closest('[data-floorplan-scene]')` instead of composedPath
   for cross-browser SVG reliability.

4. Clicking in floor plan with shelf tool active creates a shelf

   Root cause: `isFloorplanGridInteractionActive` is a hardcoded OR of
   build/move modes that doesn't include registry kinds, so the panel
   never emits `grid:click` / `grid:move` for them. Shelf tool listens
   on those events; without them, clicks were silently dropped.

   Fix: new `isRegistryToolBuildActive` derived from
   `mode === 'build' && tool != null && nodeRegistry.has(tool)` — added
   to the OR chain. Future Phase 5 kinds (fence, item, etc.) inherit
   floor-plan placement automatically the moment they register a tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:35:15 -04:00
Wassim SAMAD
773b58ccd4 Floor-plan: registry-driven action menu + cursor-driven move overlay
Two new files in packages/editor/src/components/editor-2d/ keep the
work out of the 18k-line floorplan-panel.tsx monolith. The panel
itself gets only four tiny additions (two imports, two component
mounts, one data attribute).

<FloorplanRegistryActionMenu>
 - Reads useViewer.selection — when a registered kind is selected and
   we're not in a move state, queries the rendered [data-node-id] <g>
   for its bounding rect (polled via rAF for pan/zoom/move reactivity).
 - Portals an HTML overlay above the bounding box with the existing
   <NodeActionMenu>. Buttons gated by def.capabilities:
     * Move → setMovingNode(node)
     * Duplicate → structuredClone + schema.parse + createNode + set
       movingNode (placement cursor) — matches 3D duplicate UX.
     * Delete → deleteNode(id) + clear selection.
 - Same visual styling as the legacy <FloorplanActionMenuLayer> per
   kind, but driven by registry data.

<FloorplanRegistryMoveOverlay>
 - Activates when useEditor.movingNode is a kind with def.floorplan.
 - Listens on window for pointermove (to track cursor in floor plan
   meters via the scene <g>'s getScreenCTM — matches the legacy
   getSvgPointFromClientPoint coordinate path so cursor → meters
   accounts for pan/zoom/building rotation).
 - Renders a 50%-opacity ghost via portal into the floor-plan scene
   <g>. Builder reused from def.floorplan — no per-kind ghost code.
 - Click commits via updateNode({ position: [cx, oldY, cz] }) and
   clears movingNode. Clears `isNew` metadata on duplicates so they
   don't loop. Esc cancels.

floorplan-panel.tsx touches:
 - Two imports (action menu + move overlay).
 - data-floorplan-scene="" attribute on the floorplanSceneRef <g>
   so the overlay can find the scene without sharing a ref.
 - <FloorplanRegistryActionMenu /> mounted alongside the legacy
   action menu layer.
 - <FloorplanRegistryMoveOverlay /> mounted inside the SVG tree
   alongside the registry render layer.

FloorplanRegistryLayer: also stopPropagation on click events so the
outer SVG's onClick={handleBackgroundClick} doesn't deselect right
after our pointerDown sets selection. Fixes "click-in-2D doesn't
select" bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:18:45 -04:00
Wassim SAMAD
a3fb5bd622 FloorplanRegistryLayer: strip drag-to-move, keep click-to-select
The drag-with-grab-cursor model was wrong design — 3D doesn't drag,
it uses select → inspector "Move" button → click-to-place. Floor
plan should match. Drag also had a coord-conversion bug (used outer
<svg> CTM instead of the floorplanSceneRef <g>, so screen→meter
conversion didn't account for the floor plan's pan/zoom/building-
rotation transforms).

Strips the drag pointerdown/move/up handlers, the imperative
transform override, the temporal pause bracketing, and the global
window listeners. Cursor goes back to 'pointer'. Only click-to-
select remains.

The right pattern (move via inspector / action menu + cursor-driven
placement) needs:
 - Registry-aware FloorplanActionMenuLayer path
 - Generic movingNode handler in floor-plan-panel for any registered
   kind with capabilities.movable
 - Shared floorplanSceneRef for accurate coord conversion
Both flagged in the plan as Phase 4 follow-on gaps with their
acceptance criteria. 3D-realtime-sync-while-moving is documented as
deferred (legacy doesn't do it for any kind either; design + ship
in a dedicated PR later).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:02:44 -04:00
Wassim SAMAD
d10d7f8deb Phase 4 follow-on: floor-plan interaction + def.toolHints + spawn floorplan builder
Three additions on top of the floor-plan registry contract:

1. def.toolHints + RegisteredToolHelper (registry contract for the
   shortcut hint panel)

   - New ToolHint type in core: { key, label } static array.
   - Added `def.toolHints?: ToolHint[]` to NodeDefinition.
   - New <RegisteredToolHelper hints={...}> in editor — same visual
     styling as WallHelper / ItemHelper but data-driven.
   - HelperManager: registry-first check before falling through to the
     hand-written per-tool switch. Per-tool helper files get deleted
     as their kind migrates `toolHints` in.
   - Shelf + spawn definitions ship toolHints today; wall ports in
     Phase 3 Milestone C alongside its tool/affordance port.

2. Floor-plan interaction layer (selection + drag-to-move)

   - <FloorplanRegistryLayer> now wraps each entry in an interactive
     <g>:
       * Click → useViewer.setSelection({ selectedIds: [id] }).
         Selection visual is a thicker accent-colored stroke applied
         via withSelectionStyle() recursion through the FloorplanGeometry
         tree — kinds don't author selection decoration.
       * Drag → imperative SVG transform during the gesture, single
         updateNode commit on pointerup. Same "smooth move" pattern as
         MoveRegistryNodeTool for 3D drag: no per-tick store update,
         no React re-render storm, no zundo bloat. Coordinate
         conversion via svg.getScreenCTM().inverse().
       * useScene.temporal.pause/resume brackets the gesture so one
         drag = one undo step.
   - Global pointermove / pointerup listeners so the gesture survives
     the cursor leaving the entry's bounding box (matches the legacy
     elevator-resize-drag and item-drag patterns in floorplan-panel).

3. Spawn floor-plan builder (deferred wiring)

   - buildSpawnFloorplan written but NOT wired on the definition —
     spawn already renders in the legacy floorplan-panel.tsx via
     `floorplanSpawnEntries`, and wiring def.floorplan now would
     double-render. The pure builder lives in nodes/src/spawn/
     floorplan.ts ready to wire when the legacy inline branch is
     removed (Phase 5 spawn-floorplan migration PR — same shape as
     wall's feature flag, but per kind inside the legacy panel).

Plan updated: floor-plan interaction section locks the click/drag
contract in, wall-floor-plan-as-legacy note flags everything advanced
the user sees today as legacy that ports alongside Milestone C.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:51:22 -04:00
Wassim SAMAD
ac36297e7e Phase 4 follow-on: registry-driven floor-plan rendering + shelf port
Adds the floor-plan side of the three-checkbox composition model
documented in wiki/architecture/node-definitions.md. Mirrors the 3D
side (def.geometry → <GeometrySystem> → <ParametricNodeRenderer>) but
emits SVG primitives instead of three.js Object3Ds, and runs inside
the floor-plan panel.

Type-side (packages/core/src/registry/types.ts):
 - New FloorplanGeometry tagged union covering path / polygon /
   polyline / rect / circle / line / group. FloorplanStyle props map
   straight to SVG attributes. Coordinates are level-local meters;
   rotations are radians (three.js convention).
 - New `def.floorplan?: (node, ctx) => FloorplanGeometry | null` field
   on NodeDefinition, independent of `geometry` and `renderer`. Re-
   exported via packages/core/src/registry/index.ts.

Runtime (packages/editor):
 - <FloorplanGeometryRenderer> walks the FloorplanGeometry tree and
   emits the matching React-SVG elements. Pure data → DOM; no per-kind
   logic.
 - <FloorplanRegistryLayer> reads the active levelId, walks the level
   subtree, looks up each node's def.floorplan, builds a
   GeometryContext, calls the builder, and renders the output via
   FloorplanGeometryRenderer.
 - Mounted in floorplan-panel.tsx just before <FloorplanMarqueeLayer>
   so registry-driven kinds layer above legacy inline content.

Shelf migration (proof port):
 - New nodes/src/shelf/floorplan.ts — buildShelfFloorplan(node) emits
   a group with the rotation/translation transform and a width × depth
   rectangle in the shelf's color. Brackets omitted (hidden under top
   board from above).
 - Wired to shelfDefinition.floorplan. Shelf now appears in the floor
   plan view for the first time (was missing from the legacy panel's
   inline switch).

Pattern proven; every future kind migrating in Phase 5 follows the
same shape: a pure (node, ctx) => FloorplanGeometry function. As kinds
register their floor-plan builders, the corresponding inline branches
in floorplan-panel.tsx become dead code and can be deleted in the
same PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:34:42 -04:00
Wassim SAMAD
f874cecf8f ParametricInspector: fine-grained per-field subscriptions
Subscribe to node.type at the top and to each field's value individually
inside FieldRenderer. Slider drags previously re-rendered the entire
inspector + every field every tick because the panel subscribed to the
whole node object (which gets a new reference on every updateNode).
Primitive field values stay === equal across unrelated mutations, so
now only the dragged field re-renders.

Handlers (move/delete/update) use useScene.getState() inside callbacks
instead of subscribing — they only need the current value, not a
reactive read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:06:02 -04:00
Wassim SAMAD
9d17683feb Add ParametricInspector — auto-derive right-panel UI from definition.parametrics
First slice of Phase 4 work pulled in: an inspector that reads any
registered node's `parametrics` descriptor and renders the right-side
panel automatically. Verifies the parametric descriptor design end-to-end
on shelf and proves the loop (descriptor → UI → store update → render).

ParametricInspector component:
- Reads `nodeRegistry.get(node.type)?.parametrics`.
- Renders one `<PanelSection>` per group, one control per field.
- Field kinds supported in v1:
  * `number` → SliderControl with min/max/step/unit from descriptor
  * `enum`   → dark-themed <select> with prettified labels
  * `color`  → native color picker + hex input pair
  * `vec3`   → 3× SliderControl (X / Y / Z)
- Honors `field.visibleIf(node)` to gate conditional fields.
- Generic Actions footer with Move / Delete, gated on
  `capabilities.movable` / `capabilities.deletable !== false`.
- Title from `presentation.label`, defaults to `node.type`.

Wired as the `default:` arm of panel-manager.tsx's switch — registered
kinds without a hardcoded case (shelf, future kinds) get the auto-derived
panel. Spawn keeps its hand-written panel (the legacy switch catches
it first); we'll switch to registry-first dispatch when Phase 4 finishes
and the hand-written panels can be deleted.

What you can verify after this:
- Click a shelf → right panel shows Dimensions (width/depth/thickness/
  height sliders with units + bounds from the schema) + Style (bracket
  style select + color picker) + Actions (Move / Delete).
- Drag a slider → mesh updates live (store update → renderer re-renders).
- Try to set width > 3.0 — schema rejects, no update fires (the
  parametric bounds are enforced by Zod, same source of truth as MCP
  bound generation in Phase 4's MCP work).

Not in scope for this commit:
- `parametrics.customPanel?` escape hatch.
- `material` / `ref` field kinds.
- `invariants` validation feedback in the UI.
- Migration of legacy panels (spawn/column/etc.) to the auto-generated
  path — those keep working unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:45:57 -04:00
Wassim SAMAD
d254582597 MoveTool: dispatch registry-first so spawn move uses MoveRegistryNodeTool
Shim ordering bug: the `nodeRegistry.has(movingNode.type)` check sat at
the END of the dispatch chain, AFTER the per-kind `if (movingNode.type
=== 'spawn') return <MoveSpawnTool>` branches. Spawn (now registered
in builtinPlugin) was therefore still routing to the legacy
MoveSpawnTool — which uses the broken useLiveTransforms pattern and
makes the spawn mesh disappear during drag.

Moved the registry check to the TOP, matching the registry-first
dispatch model the Phase 0 shims use everywhere else (NodeRenderer,
ToolManager, system guards). Now any kind registered via
@pascal-app/nodes routes to MoveRegistryNodeTool — same smooth
imperative drag for shelf, spawn, and every future kind. Legacy
per-kind movers below run only for kinds not yet in the registry.

This is exactly how the Phase 5 progressive consolidation works: as
kinds migrate to the registry, their legacy movers stop being reached
and can be deleted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:26:29 -04:00
Wassim SAMAD
166e860bfe Move: pure imperative via sceneRegistry (smooth, zero re-renders); drop dev logs
User feedback: updating the store per tick caused tons of React
re-renders → laggy drag. Switched to pure imperative.

MoveRegistryNodeTool now:
- Mutates `sceneRegistry.nodes.get(id).position` directly per
  grid:move tick. No useScene.updateNode during drag. No store
  change → no renderer re-render → R3F doesn't reapply
  `position={node.position}` → the imperative mutation sticks.
- On commit: single tracked `useScene.updateNode(id, { position })`.
  Undo replays one step (original → final), no per-tick spam.
- On cancel / unmount: imperatively snap the mesh back to original.
  Store was never touched so no data revert needed.

Trade-off vs the items pattern (which does update the store per tick
and re-renders per tick): our approach is faster but assumes the
renderer doesn't re-render mid-drag. Items get away with constant
re-renders because their renderer is heavily optimized; for parametric
shelves (and future kinds) the imperative path is simpler and faster.

Cleanup: removed the dev `[shelf] rendered` and `[shelf] placed`
console.info logs from the shelf renderer and tool. They were Phase 2
verification scaffolding — no longer needed now that everything works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:19:37 -04:00
Wassim SAMAD
7bd34e5ff0 Move: update node.position directly per tick (matches item pattern)
The useLiveTransforms + sceneRegistry.position.set approach used by
MoveColumnTool is broken — column ALSO disappears during move, per
user observation. The mesh doesn't visibly follow the cursor.

Items work because their move tool directly updates the scene store's
node.position on every grid:move tick (with history paused), and the
renderer reads node.position. Matching that pattern here.

MoveRegistryNodeTool now:
- Snapshots the original position at mount (for cancel / commit
  revert path).
- Pauses scene history so per-tick updateNode calls don't fill undo.
- On grid:move: `useScene.updateNode(id, { position })`. The kind's
  registered renderer reads node.position and re-renders, so the
  actual mesh visibly follows the cursor.
- On commit: revert to original while still paused → resume → final
  update (single tracked action) → re-pause. Undo replays one step,
  not the per-tick spam.
- On cancel / unmount-without-commit: restore original position with
  history still paused (won't enter undo), then resume.

The cursor sphere stays as the aim indicator alongside the moving
mesh. No translucent ghost — the actual mesh IS the preview now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:12:42 -04:00
Wassim SAMAD
c792f9cff2 Move: drop preview overlay, let the actual mesh follow via live transforms
User feedback: the move tool shouldn't render a separate translucent
preview. The actual mesh (registered with sceneRegistry through the
kind's renderer) should follow the cursor — that's what's already
happening via useLiveTransforms + the imperative position.set on the
registered Object3D.

Removed from MoveRegistryNodeTool:
- The lazy `def.preview` load + Suspense-wrapped <Preview> render.
- Now only CursorSphere shows as the aim indicator. The shelf's
  actual rendered mesh follows the cursor via:
  - `useLiveTransforms.set(...)` triggers ShelfRenderer to re-render
    with `position={liveTransform.position}`.
  - `sceneRegistry.nodes.get(node.id).position.set(...)` is a
    defensive imperative update so motion feels snappy.

Added: `sfx:grid-snap` emit on grid-cell cross, matching the placement
tools' behavior. Move now sounds like placement.

The `preview` slot on NodeDefinition stays — still used by ShelfTool
for the placement cursor (where no real mesh exists yet). Phase 4 may
consolidate placement preview with the renderer too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:05:40 -04:00
Wassim SAMAD
e84b1ec8bc Add preview slot to NodeDefinition; show translucent shape during move
User feedback: the move tool's CursorSphere is just a vertical line,
hard to tell what you're moving. Placement gets a translucent shape
that follows the cursor; move should too.

NodeDefinition.preview?: () => Promise<{ default: ComponentType<{ node }> }>
opt-in lazy component that renders a translucent ghost of the node.
Used by:
- The placement tool (ShelfTool) — renders the preview at the cursor
  position so the user sees the shape they're placing.
- The move tool (MoveRegistryNodeTool) — renders the preview at the
  drag target alongside the CursorSphere. Plus the original node is
  also dragged via live transforms, so the user sees both: the actual
  node moving + a translucent ghost at the same spot.

Implementation:
- New nodes/shelf/preview.tsx: ShelfPreview component. Renders the
  same shape as ShelfRenderer but `transparent: true, opacity: 0.5`.
- shelfDefinition.preview = () => import('./preview').
- ShelfTool's placement preview now uses <ShelfPreview node={defaults} />
  instead of an inline copy of the box geometry.
- MoveRegistryNodeTool lazy-loads `def.preview` (cached by loader,
  Suspense-wrapped). If a kind doesn't define `preview`, only the
  CursorSphere shows — matches today's behavior.

Phase 4 may merge `preview` with `renderer` behind an `opacity` prop
so kinds don't duplicate JSX between the solid and translucent
versions; until then defining `preview` is opt-in and one extra file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:01:32 -04:00
Wassim SAMAD
a090c22f42 Move + duplicate for registry kinds via MoveRegistryNodeTool
Shelf had the floating action menu (move/delete) showing thanks to
the previous registry-driven selection commit, but clicking move
did nothing and duplicate silently failed. Two hardcoded chains:

1) FloatingActionMenu.handleMove guarded `setMovingNode` behind a
   hardcoded `node.type === 'item' || ... || node.type === 'spawn'`
   chain. Added `|| isRegistrySelectable(node.type)` so any
   registry kind triggers the move flow.

2) MoveTool dispatched per-kind components (MoveItemContent,
   MoveColumnTool, MoveWallTool, ...). The default fallback
   mounted MoveItemContent, which assumes the node is an ItemNode
   with asset/scale/metadata — crashes for shelf. Added a generic
   MoveRegistryNodeTool (kind-agnostic clone of MoveColumnTool):
   pure position+rotation drag with grid snap, re-parses orphan
   re-creates via `nodeRegistry.get(kind).schema.parse(...)`.
   MoveTool dispatches to it for any `nodeRegistry.has(movingNode.type)`
   before the MoveItemContent fallback.

3) FloatingActionMenu.handleDuplicate had a hardcoded
   `node.type === 'door' ? DoorNode.parse(...) : ...` chain. Added
   a registry-driven fallback after it:
   `const def = nodeRegistry.get(node.type); duplicate = def.schema.parse(duplicateInfo)`.
   Then the createNode + setMovingNode branches also augment with
   `nodeRegistry.has(duplicate.type)` so the new shelf gets
   created in the scene and handed off to the move tool for
   placement.

After this:
- Click shelf → move icon in floating menu → cursor follows mouse,
  click to place at new position.
- Click shelf → duplicate icon → new shelf appears, offset by (1,0,1),
  handed to move tool so the user can position it.

Phase 4 will collapse MoveRegistryNodeTool with the per-kind movers
once they all reduce to the same position+rotation shape, and read
`capabilities.movable` to gate handleMove instead of the OR chain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:39:36 -04:00
Wassim SAMAD
6d97a87547 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>
2026-05-14 15:22:51 -04:00
Wassim SAMAD
857ddd4d95 Register 'shelf' in selection managers (5 arrays + 1 type union)
Shelf renderer was emitting `shelf:click` / `shelf:enter` / `shelf:leave`
via useNodeEvents from the previous commit, but no listener subscribed
— the SelectionManager components (one in editor, one in viewer) each
maintain hardcoded allTypes arrays that didn't include 'shelf'.

Adds 'shelf' to:
- editor/selection-manager: 5 allTypes arrays (one per selection strategy
  — structure, structure-hover, furnish, site, deselect-also-listens-to).
- viewer/selection-manager: the SelectableNodeType union + allTypes
  array.

Shelves can now be clicked / hovered in the 3D canvas and the
selection state updates correctly.

The hardcoded arrays are exactly the kind of cross-cutting friction
the registry is supposed to eliminate. Phase 4 should derive these
lists from `nodeRegistry.entries().filter(d => d.capabilities.selectable)`
so adding a new kind doesn't require editing two files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:05:22 -04:00
Wassim SAMAD
76794ceb7d Wire shelf + spawn placement polish: SFX, cursor, sidebar, selection
User-visible follow-ups after first running the Phase 2 spike.

Spawn tool now matches legacy UX:
- CursorSphere from @pascal-app/editor for the placement indicator
  (ring + line + tool-icon tooltip) — was a plain sphere mesh.
- Emits sfx:structure-build on commit + setTool(null) + setMode
  ('select') to exit build mode, matching legacy spawn-tool.

Shelf tool placement:
- Emits sfx:structure-build on commit.
- Cursor preview now shows top board + brackets (was just the top),
  matching what gets placed.

Shelf selectable from the 3D canvas:
- ShelfEvent type added to @pascal-app/core/events/bus.
- 'shelf' added to NodeConfig in useNodeEvents.
- ShelfRenderer wires `useNodeEvents(node, 'shelf')` handlers onto
  every mesh. Clicks/hovers now bubble through the editor's selection
  manager and update useViewer.selection.

Shelf appears in the sidebar:
- ShelfTreeNode component (mirrors spawn-tree-node's shape +
  selection/hover/rename wiring; lucide Layers icon).
- TreeNode dispatcher adds a `case 'shelf':` arm.

Framework changes:
- @pascal-app/editor exports CursorSphere alongside triggerSFX.
- @pascal-app/nodes now declares @pascal-app/editor as peer/dev dep.

Pre-existing typecheck errors in @pascal-app/editor (ceiling-tree-node,
fence-tree-node, slab-tree-node, scene.ts) are unchanged — present on
main and not introduced by this commit.

630 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:55:03 -04:00
Wassim SAMAD
b6d77206b4 Phase 2 spike: spawn migration (flagged) + new shelf node
The first time registry-driven nodes actually run in the editor.

Spawn migration (under NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN flag):
- New packages/nodes/src/spawn/ folder with renderer, tool, schema
  (re-exported from core), parametrics, definition, index.
- Spawn definition appended to builtinPlugin.nodes only when the flag
  is set. With the flag off, the Phase 0 shims fall through and the
  legacy SpawnRenderer / SpawnTool keep ownership.
- New no-props SpawnTool reads activeLevelId from useViewer directly,
  matches legacy placement behavior (half-meter snap, singleton-per-
  level, replace-on-reclick).
- Structural parity test (9 cases) validates definition shape +
  schema identity. Pixel-diff defers to Phase 4 alongside more nodes.

New shelf node (no legacy — registered unconditionally):
- ShelfNode schema in core/schema/nodes/shelf.ts (hand-maintained
  AnyNode union for now; Phase 6 derives the union from the registry
  and moves the schema fully into nodes/shelf/).
- packages/nodes/src/shelf/ folder: pure geometry builder
  (buildShelfGeometry returns a Three.js Group of top board +
  brackets), R3F renderer that mounts the built group, no-props
  placement tool, parametrics descriptor (width/depth/thickness/
  height/bracketStyle/color), definition with surfaces.top stackable
  surface for future stacking, and presentation metadata for the
  palette.
- 13 unit tests across schema bounds and geometry behavior.
- Palette wiring: 'shelf' added to StructureTool union + an entry in
  the structure-tools array (placeholder icon, replaced in Phase 4
  when palette is registry-driven).

Framework changes:
- @pascal-app/viewer now exports useNodeEvents from its public barrel
  so node bundles in @pascal-app/nodes can subscribe to node-specific
  pointer events. (Used by spawn renderer; shelf renderer skips it
  for now since useNodeEvents has a hardcoded kind list — Phase 4
  generalizes it via the registry.)
- @pascal-app/nodes gains @pascal-app/viewer as a peer + dev dep so
  node bundles can import from it.

630 tests pass across 76 files (22 new this phase). Editor app
continues to ship green with both legacy spawn and the new shelf
node co-existing through the Phase 0 dispatch shims.

To validate end-to-end in dev:
- bun dev:community → open editor → click 'Shelf' in structure
  toolbar → click to place. Confirms full registry path
  (NodeRenderer dispatch + ToolManager dispatch + sceneRegistry
  byType Proxy).
- Set NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN=1, restart dev, place spawn
  → visually identical to legacy. Confirms parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:07:38 -04:00
Wassim SAMAD
e799ff70da Add DragSession + useDragAction hook (Phase 1, 5/6)
Pure orchestrator in core, thin React wrapper in editor:

- `core/services/drag-session.ts` — `createDragSession(action, scene,
  options)` returns an imperative session with `start / move / commit
  / cancel / dispose / isActive / getDraft`. Pauses history on start,
  resumes on terminate. Per-move runs preview → snap → apply, then
  cascades dirty marks via the relations resolver (deduped across
  ticks). Re-entry guard, idempotent dispose, fires onCommit/onCancel
  callbacks. All tested in bun:test — no React needed.

- `editor/src/hooks/use-drag-action.ts` — wraps the session with the
  editor's grid-event emitter and an Esc-to-cancel keyboard listener.
  Builds a `SceneApi` once via `createSceneApi(useScene)` at module
  init. The hook itself is small enough to read top-to-bottom; all
  behavior lives in the session.

Tests (13 cases) cover the hard parts: history pause/resume bracket,
explicit cancel restoring all touched nodes, dispose mid-drag, commit
returning false short-circuiting to cancel, snap callback wired in,
re-entry rejected, deduped dirty-mark across multiple move ticks,
hosts cascade from the registry firing in apply.

No callers yet — Phase 2 column and shelf tools are the first
consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:43:12 -04:00
Wassim SAMAD
2a8eb3e1de Registry-first dispatch in ToolManager (shim 4/4)
Before mounting any legacy build tool, ToolManager checks whether the
active tool's kind has a registered NodeDefinition with a tool
contribution. If yes, the registry tool wins; the legacy tool map and
special-cased spawn/column/elevator branches are skipped for that kind.

Lazy-loaded via React.lazy (cached by loader) and wrapped in Suspense.

Today the registry is empty, so useRegistryTool is always false and
every code path renders unchanged. The moment a kind registers (Phase
2+), its registry tool takes over without further edits here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:39:31 -04:00
sudhir
1ffa3d2d73 Restore item controls and guard placement asset bounds 2026-05-13 22:45:58 +05:30
sudhir
ce69433a1a Refine elevator sync and viewer rebuild handling 2026-05-13 22:28:19 +05:30
sudhir
9838d2cd4f Refactor elevator runtime into core 2026-05-13 01:56:06 +05:30
sudhir
b782ce9cf2 Merge branch 'main' into feat/elevator-system
# Conflicts:
#	packages/editor/src/components/tools/item/move-tool.tsx
#	packages/editor/src/components/tools/tool-manager.tsx
#	packages/editor/src/components/ui/panels/panel-manager.tsx
#	packages/editor/src/store/use-editor.tsx
#	packages/viewer/src/components/renderers/site/site-renderer.tsx
#	packages/viewer/src/components/viewer/ground-occluder.tsx
#	packages/viewer/src/components/viewer/index.tsx
#	packages/viewer/src/components/viewer/post-processing.tsx
2026-05-13 01:38:55 +05:30
Wassim SAMAD
9c248b7e0c fix max panel height 2026-05-12 12:06:27 -04:00
sudhir
70c85d7760 Refine wall move previews and expose auto-slab planning 2026-05-12 12:15:45 +05:30
sudhir
f3444d4275 Add wall drag previews and fix move arrow handles 2026-05-12 11:00:27 +05:30
sudhir
564bfa229e Merge remote-tracking branch 'origin/main' into fix/fri-8-may
# Conflicts:
#	packages/editor/src/components/tools/fence/move-fence-endpoint-tool.tsx
#	packages/editor/src/components/tools/select/box-select-tool.tsx
#	packages/editor/src/components/tools/wall/move-wall-tool.tsx
#	packages/editor/src/components/ui/panels/column-panel.tsx
#	packages/editor/src/components/ui/panels/door-panel.tsx
#	packages/editor/src/components/ui/panels/window-panel.tsx
#	packages/viewer/src/components/renderers/column/column-renderer.tsx
#	packages/viewer/src/components/viewer/index.tsx
#	packages/viewer/src/systems/slab/slab-system.tsx
2026-05-12 00:27:06 +05:30
sudhir
d78308b366 Hide inapplicable door and window panel settings 2026-05-12 00:05:41 +05:30
sudhir
be41511474 Fix wall move junction ownership 2026-05-11 23:46:57 +05:30
Wawa
8921e341cc Bump inter-package peerDeps to ^0.8.0 + fix release.yml sync bug
Two bugs in one fix:

1. The 0.8.0 release commit only bumped .version, not the inter-package
   peerDependencies / devDependencies. So the published packages still
   declared peer constraints like "@pascal-app/viewer": "^0.7.0". Caret
   in semver 0.x doesn't allow 0.8.0 to satisfy ^0.7.0, so bun resolves
   workspace consumers (and apps/editor's deep import paths) to the
   stale npm-published 0.7.0 instead of the workspace 0.8.0 — meaning
   local edits never show up in apps/editor or any linked consumer.

2. The release.yml sync step was using $GITHUB_ENV to read back the new
   versions in the same step, which doesn't work — env-file writes only
   surface in subsequent steps. Switched to a bash associative array
   (NEW_VERSIONS) for in-step lookup, kept the $GITHUB_ENV write for the
   downstream publish/commit/tag steps. Also added a final "refs after
   sync" debug print so this is visible in the workflow log.

After this lands and you bun install, packages/editor/node_modules/@pascal-app/viewer
should symlink to the workspace packages/viewer/, not to .bun/@pascal-app+viewer@0.7.0.
Future releases will sync peerDeps correctly on their own.
2026-05-10 13:12:08 +00:00
sudhir
4956de4910 Refine column support selection and polygon handling 2026-05-10 12:51:57 +05:30
sudhir
92a9846193 Refactor elevator systems across core, viewer, and editor 2026-05-10 12:38:44 +05:30
sudhir
909f96ef09 Improve elevator floorplan resize and drag controls 2026-05-10 12:20:53 +05:30
sudhir
fdf08c091f Add elevator floorplan overlay 2026-05-10 10:50:22 +05:30
github-actions[bot]
60dbfcd3fe release: @pascal-app/core@0.8.0 @pascal-app/viewer@0.8.0 @pascal-app/editor@0.8.0 @pascal-app/mcp@0.2.0 2026-05-10 00:41:38 +00:00
sudhir
263228cbf0 Add cab door-open control and align elevator buttons 2026-05-10 02:28:00 +05:30
Wawa
e618175bba Reset core/viewer/editor/mcp packages and apps/editor from private-editor
Wholesale swap of packages/{core,viewer,editor,mcp} and apps/editor with the
versions from the private editor repo, which is the production source of truth.

Setup changes:
- packages/{core,viewer,editor} versions held at 0.7.0 baseline (matching
  the most recent published release) so a bump=minor publishes 0.8.0
- packages/mcp held at 0.1.1 (never published; first publish will go through
  the new release.yml flow)
- peerDependencies and devDependencies for inter-package @pascal-app/*
  references pinned to ^0.7.0 instead of '*' / 'workspace:*' so they are
  valid for npm consumers
- Root package.json: TypeScript bumped to 6.0.2, added overrides for
  @types/react, @types/react-dom, @types/three to prevent JSX namespace
  fragmentation across the workspace
- release.yml extended to also publish editor and mcp; 'both' option renamed
  to 'all'; added a sync step that updates inter-package peerDeps/devDeps to
  match the new versions on every bump (so viewer/editor/mcp tarballs always
  reference the version of core they were built against)
- Root scripts gained release:editor and release:mcp shortcuts

Verification:
- bun install --frozen-lockfile is consistent
- packages/{core,viewer,mcp} build cleanly, dist/index.d.ts emitted
- packages/editor check-types reports 21 pre-existing errors, identical to
  what private-editor currently reports

Open PRs against editor-v2 will need rebasing/conflict resolution.
2026-05-09 20:52:26 +00:00
open-pascal
146f0a846f release: v0.7.0 2026-05-09 14:37:55 +00:00
sudhir
2480be91b1 Optimize elevator editing performance with live previews 2026-05-09 19:09:48 +05:30
Pascal
723ae14e9b release: v0.6.1 2026-05-09 02:25:15 +00:00
sudhir
0514363136 Add column hit testing and align column defaults 2026-05-08 14:31:58 +05:30
sudhir
53393b7def Add procedural support column variants 2026-05-08 12:54:59 +05:30
Wassim SAMAD
e348ccdb08
Merge pull request #296 from sudhir9297/wed-6-may-bug-fix
fix: Improve item surface placement, floorplan resizing, and placement preview stability
2026-05-07 13:09:20 -04:00