Commit Graph
891 Commits
Author SHA1 Message Date
Wassim SAMADandClaude Opus 4.7 7d9c89c612 feat(ifc-converter): real IFCWALL dims + profile-typed columns
Two converter accuracy refinements:

- Recover plain IFCWALL height/thickness from geometry. These carry
  Brep/mapped geometry that getBodyExtrusionData can't read, so measure
  the mesh in the wall's own axis frame (along/across/vertical) — a
  rotation-invariant projection rather than a world-space AABB (which
  conflated a rotated wall's length and thickness). Gate on the measured
  length matching the known wall length, and free web-ifc geometry
  handles via try/finally on every path.
- Type columns from their IFC swept profile: IfcCircleProfileDef → round
  + radius, IfcRectangleProfileDef → rectangular + width/depth, falling
  back to the width/depth ratio when the profile type is unknown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:45:12 -04:00
Wassim SAMADandClaude Opus 4.7 0df51219d0 feat: IFC → Pascal converter (package + app)
Brings the IFC-to-Pascal converter into the monorepo as a pure-logic
package plus a Next.js app, replacing the standalone repo that consumed
published @pascal-app/* packages (and drifted from their schemas).

packages/ifc-converter — pure conversion. Parses IFC via web-ifc and
maps elements onto @pascal-app/core node schemas (workspace-linked, so
no more version drift). Builds doors/windows, walls, slabs, columns,
roofs, stairs, sites/buildings/levels. Validates each node via the real
Zod schemas at build time (tryParse) and strips undefined metadata.

apps/ifc-converter — the UI: drop zone, example picker, element search,
JSON download, and a 3D preview rendered through the real
@pascal-app/viewer (registry bootstrap + read-only scene) with a custom
toolbar (camera/level/wall/grid/theme), level selector with
camera-focus, auto-fit, and selection bridged to an inspector.

Conversion specifics worth noting:
- Door/window vertical centering (height/2; windows + sill).
- Nearest-wall hosting fallback for files lacking IFCRELFILLSELEMENT,
  preferring walls long enough to contain the opening and clamping the
  along-wall position so cutouts can't overflow and break wall CSG.
- Plain IFCWALL (Brep/mapped geometry) falls back to default
  height/thickness instead of collapsing to zero-height slivers.
- Columns convert as plain structural shafts (no decorative
  base/capital) sized from the IFC profile.
- Beams + items are skipped for now (no Pascal beam type; items need a
  catalog asset) — counted in the conversion summary.

Large example IFCs are fetched from a public bucket at runtime; the four
small ones are committed. web-ifc.wasm is copied into public/ on
install/dev/build. README flags early-alpha + invites contributions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:45:12 -04:00
Jaafar El HarouchiandClaude Sonnet 4.6 3e8b972b8f fix(mcp): templates use string IDs for SiteNode.children
PR #320 changed SiteNode.children from embedded BuildingNode/ItemNode
objects to flat string[] IDs. PR #325 updated the runtime call sites
but missed the three scene templates, which still mutated the site
node's children array to embed full building objects after building
the flat dict. This caused AnyNode.safeParse to fail for site_empty,
site_2br, and site_garden in bun test --cwd packages/mcp.

Remove the obsolete mutation blocks; each template already initialises
site.children with the correct string id (e.g. ['building_empty']).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 15:32:12 +01:00
Jaafar El HarouchiandClaude Sonnet 4.6 01639e42e4 fix: update mcp and nodes for SiteNode.children string[] schema change
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>
2026-05-21 13:17:15 +01:00
Wassim SAMADandClaude Opus 4.7 ffc497c67a editor: gate Load Build behind a verification dialog
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>
2026-05-20 10:31:02 -04:00
Wassim SAMADandClaude Opus 4.7 d218cb7f26 fix: restore fence tool handlers dropped by null-guard commit
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>
2026-05-20 08:15:26 -04:00
open-pascal 251af2ce1d fix: add null guards for cursorRef in fence and wall tool click handlers 2026-05-20 00:57:15 +00:00
open-pascal 3adae8acea style: apply biome lint fixes after portforward 2026-05-20 00:54:11 +00:00
sudhirandopen-pascal d48be77f5d Reduce wall rebuilds during window and door placement 2026-05-20 00:53:10 +00:00
sudhirandopen-pascal fe68dbeebd Constrain wall moves along wall normals 2026-05-20 00:53:10 +00:00
sudhirandopen-pascal c31c08fd90 feat: implement StairOpeningSystem to handle automated stair opening updates and adjust camera distance constraints 2026-05-20 00:53:10 +00:00
sudhirandopen-pascal 8b73d7986b feat: expand material library with new wood and flooring textures while cleaning up legacy assets 2026-05-20 00:52:08 +00:00
sudhirandopen-pascal a9d26239b7 Refactor material catalog for roofing and flooring 2026-05-20 00:52:01 +00:00
sudhirandopen-pascal a3378a666d Preserve elevator stop order and handle viewport resize 2026-05-20 00:52:01 +00:00
sudhirandopen-pascal d7a1ce89f3 Scope first-person overlay and unify elevator colors 2026-05-20 00:51:43 +00:00
sudhirandopen-pascal 5bd2f51d15 Make fence drawing continue between segments 2026-05-20 00:51:43 +00:00
sudhirandopen-pascal 5d53be26e5 Refine wall chaining and door arch clipping 2026-05-20 00:51:01 +00:00
sudhirandopen-pascal 023cd511e9 Remove material strips from roof, stair, and fence panels 2026-05-20 00:50:47 +00:00
sudhirandopen-pascal 4fc57a6fd9 Enhance elevator placement preview 2026-05-20 00:49:48 +00:00
sudhirandopen-pascal 89c62800a3 Refine window panel by family 2026-05-20 00:49:41 +00:00
sudhirandopen-pascal 2ee69e24e2 Organize door panel controls by door family 2026-05-20 00:49:41 +00:00
sudhirandopen-pascal 4a402756fc Fix floorplan labels and door/fence geometry 2026-05-20 00:45:33 +00:00
sudhirandopen-pascal cdde88f9c0 Clamp spiral stair openings at full sweep 2026-05-20 00:45:28 +00:00
sudhirandopen-pascal 3800bd7fc8 fix: increase stair opening buffer constraints and set default offset to 0.15 2026-05-20 00:45:25 +00:00
sudhirandopen-pascal 7cc11f0fd6 feat: add fence move handles and duplication functionality to the floorplan editor 2026-05-20 00:45:25 +00:00
sudhirandopen-pascal 5f17172638 refactor: make onMove optional in FloorplanActionMenuEntry and remove unused wall move handler 2026-05-20 00:41:28 +00:00
sudhirandopen-pascal 2b9bb769f3 feat: add camera-aware wall move handles and UI layer for floorplan editor, and remove unused elevator icon. 2026-05-20 00:41:26 +00:00
sudhirandopen-pascal db5c743e1c Render curved wall measurements along the wall 2026-05-20 00:41:18 +00:00
sudhirandopen-pascal 6af3a0aa82 Add draft angle arcs for walls and fences 2026-05-20 00:41:11 +00:00
sudhirandopen-pascal e009f3658b Use wall face mitering for draft angle arcs 2026-05-20 00:38:21 +00:00
Wassim SAMADandClaude Opus 4.7 2bf824cc91 floor-elevation: restore stripped import + fix column rotation type
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>
2026-05-19 19:54:35 -04:00
Wassim SAMAD e3f2343456 nodes: restore floorPlaced imports stripped by the formatter
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.
2026-05-19 19:49:09 -04:00
Wassim SAMADandClaude Opus 4.7 9a481f2511 registry: generic FloorElevationSystem driven by a floorPlaced capability
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>
2026-05-19 19:44:21 -04:00
Wassim SAMADandClaude Opus 4.7 78e3ed13d8 floorplan-panel: prune dead constants, drag-state types, and unused helpers
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>
2026-05-19 19:26:15 -04:00
Wassim SAMADandClaude Opus 4.7 a3dc48a503 floorplan-panel: prune dead drag state, useEffects, and measurement helpers
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>
2026-05-19 19:19:34 -04:00
Wassim SAMADandClaude Opus 4.7 ff147626bb floorplan-panel: remove dead action menu, handle layers, selected* state and handlers
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>
2026-05-19 19:07:59 -04:00
Wassim SAMADandClaude Opus 4.7 c6bef1ec2f floorplan-panel: dismantle legacy SVG layers replaced by the registry
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>
2026-05-19 17:53:57 -04:00
Wassim SAMADandClaude Opus 4.7 1ec65acc5f registry: post-migration polish — ceiling 3D selection, ceiling item commit, floorplan move drift
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>
2026-05-19 17:32:11 -04:00
Wassim SAMADandClaude Opus 4.7 d747d2f0ea Phase 5 Stage E: full kind migration into packages/nodes
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>
2026-05-19 15:14:12 -04:00
Wassim SAMADandClaude Opus 4.7 0540692bd5 item: fix 2D move — floor items stay parented to the level
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>
2026-05-19 15:12:15 -04:00
Wassim SAMADandClaude Opus 4.7 0dee7747d2 floorplan/zone: name label inside polygon + polygon editor on select
`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>
2026-05-19 15:12:02 -04:00
Wassim SAMADandClaude Opus 4.7 3419cf8587 floorplan/slab: lower fill opacity so zones show through
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>
2026-05-19 15:11:52 -04:00
Wassim SAMADandClaude Opus 4.7 7a92641baa floorplan/registry-layer: z-order buckets + overlay pass + click guard
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>
2026-05-19 15:11:44 -04:00
Wassim SAMADandClaude Opus 4.7 586cec8ad7 registry/move-tool: fix 3D drag — keep rotation + stop transform reset
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>
2026-05-19 15:11:26 -04:00
Wassim SAMADandClaude Opus 4.7 924567293a shelf: v2 — cubby default, withBottom, item hosting, paintable surface
Schema v2 adds style/rows/columns/withBack/withSides/withBottom/bracketStyle
and a `children: ItemNode[]` field for item hosting. Schema-level defaults
preserve the v1 wall-shelf visual so existing scenes load unchanged; the
placement tool spreads `shelfDefinition.defaults()` for fresh shelves
(cubby 3x2 at 1m × 0.5m × 1.8m, thickness 0.05m, back/sides/bottom on).

Four style geometries (wall-shelf / bookshelf / open-rack / cubby) share
the dimensional schema. `shelfRowSurfaceYs` exposes one host surface per
row, plus the bottom-board top when `withBottom` is on for cubby /
bookshelf.

Material is a single paintable surface (same shape walls / slabs / stairs
use); `DEFAULT_SHELF_MATERIAL` aligned with `DEFAULT_WALL_MATERIAL` so
unpainted shelves read as the canonical off-white.

Preview clones each cached material before mutating `transparent / opacity`
on the ghost — without the clone the mutation leaked into the cached
`getShelfMaterial` instance every committed shelf was using, rendering
them all see-through after the first placement preview rendered.

Store hardening: `migrateNodes` patches missing `children: []` on v1
shelves, and `updateNodesAction` reparenting tolerates a missing children
array on the new parent. `MaterialTarget` enum adds `'shelf'` so paint
mode picks up the kind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:11:13 -04:00
Wassim SAMADandClaude Opus 4.7 56e022a093 Phase 5 Stage D wall: port endpoint move, whole-move, placement (1:1 legacy)
Three remaining wall affordances ported into nodes/src/wall/ as
direct copies of the legacy implementations. Wall D is now complete.

  - move-endpoint-tool.tsx (426 LoC) — linked-wall corner cascade +
    Alt-detach + angle label. Mounted via `affordanceTools.move-endpoint`.
  - move-tool.tsx (804 LoC, the most complex tool in the editor) —
    center-drag with axis lock, linked-wall corner cascade via
    `planWallMoveJunctions`, bridge wall ghost previews, auto-slab
    live preview via `planAutoSlabsForLevel`, R/T rotation in 45°
    steps, Shift to bypass grid snap, isNew metadata strip on first
    commit. Mounted via `affordanceTools.move`.
  - tool.tsx (332 LoC) — two-click placement with length/angle HUD,
    Shift to bypass angle snap. Mounted via `def.tool`.

Editor public surface gains:
  - createWallOnCurrentLevel, snapWallDraftPoint, WallPlanPoint
  - MovingWallEndpoint type

ToolManager dispatch for `movingWallEndpoint` routes through the
registry with the legacy fallback (same shape as the fence
move-endpoint dispatch).

Wall is now A  C  D . Stage B still pending (geometry depends on
level-batch miter data, blocked on `ctx.levelData` design decision).
Stage E pending (drop WallPanel — has slider drags + actions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:52:30 -04:00
Wassim SAMADandClaude Opus 4.7 6a1d853dd8 IconRef: add url kind, point registered kinds at palette assets
User pointed at the palette's PNG icons in the bottom toolbar — they
expected the inspector title to use the same artwork, not the iconify
lucide glyphs. Different visual style.

- IconRef gains `{ kind: 'url'; src: string }`. Plain `<img>` render
  in ParametricInspector (no next/image — the inspector is
  `'use client'`).
- All currently-registered kinds switched to URL refs matching their
  palette `iconSrc`:
    fence    → /icons/fence.png
    slab     → /icons/floor.png
    ceiling  → /icons/ceiling.png
    wall     → /icons/wall.png
    spawn    → /icons/site.png
    shelf    → /icons/column.png (placeholder, same as palette)
- Kind-owned panels (slab/ceiling) already pass URL strings to their
  own PanelWrapper; unchanged.

Door/window/item will get URL refs when they register at Stage A.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:41:45 -04:00
Wassim SAMADandClaude Opus 4.7 8c2b03f99b parametric-inspector: render def.presentation.icon before the title
User pointed out the auto-derived panel was title-only; the legacy
panels rendered a small icon in front. Legacy used a URL path (next
Image). The registry-driven path has structured `IconRef` values
(iconify / svg / lazy component) declared on `def.presentation.icon`.

- PanelWrapper's `icon` prop now accepts `string | React.ReactNode`.
  String → next/image (legacy URL behavior). Node → rendered as-is.
- ParametricInspector resolves `def.presentation.icon` to a node:
  iconify → `<Icon icon="lucide:fence" />`, svg → inline svg, component
  → Suspense + lazy.

Kind-owned custom panels (slab/ceiling) keep their existing legacy
URL icons since they pass `<PanelWrapper>` themselves — no change
needed there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:35:18 -04:00
Wassim SAMADandClaude Opus 4.7 282cb22585 parametrics: add custom field kind + restore fence Length / Curve
User noticed the auto-derived fence inspector was missing the legacy
panel's Length + Curve sliders, and the section labelling was wrong
(Posts → Structure). Both Length and Curve are awkward for the
parametrics field model:

- **Length** doesn't map to a single node key — it's derived from
  `start`/`end`, and editing it moves `end` along the existing
  direction.
- **Curve** maps to `curveOffset` but the slider's min/max are
  bounded per-node by the chord length, plus updates need
  `normalizeWallCurveOffset`.

Adds a `kind: 'custom'` field with a kind-supplied
`component: ComponentType<{ node, onUpdate }>`. The inspector mounts
it and lets the kind own rendering + update logic. `key` becomes a
free-form React key/label since it no longer needs to map to a node
property.

Fence parametrics now mirrors the legacy layout 1:1:
- Style (segmented controls + showInfill toggle).
- Dimensions (Length, Curve, Height, Thickness).
- Structure (Base Height, Top Rail, Post Spacing, Post Size, Ground
  Clear, Edge Inset).

Length + Curve live in fence/inspector-editors.tsx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:27:40 -04:00
Wassim SAMADandClaude Opus 4.7 80199dc890 parametrics: add display: 'segmented' enum hint + drop fence color
User pointed out two regressions in the auto-derived fence inspector:

- A `color` field rendered for fence — but the legacy `FencePanel`
  hid it (fence's color is a leftover schema field that isn't part of
  the inspector UX). Dropped from `fenceParametrics`.

- Style + base-style enums rendered as a dropdown, but the legacy
  used the inline segmented switcher (Slat/Rail/Privacy +
  Grounded/Floating). Added a `display?: 'select' | 'segmented'` hint
  to the enum field kind. ParametricInspector renders SegmentedControl
  when set; defaults to dropdown otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:21:56 -04:00