c3199e75404d0108cdd31658526132c466a11f88
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c3199e7540 |
editor: green/red placement box for shelf + tool sync for presets (#366)
* feat(editor): shelf placement validity box + sync tool for positioned presets Give the generic move tool a green/red footprint box for shelf placement, matching the GLB item cursor, and drop the vertical-arrow CursorSphere for shelves. Box colour comes from canPlaceOnFloor; an invalid (red) drop is refused unless Shift forces it, and R/T play the rotate sfx. Re-sync the box transform on node change so a re-armed clone isn't left at the previous rotation/position. Extract the box wireframe geometry helpers into a shared placement-box-geometry module reused by the item coordinator and the new declarative PlacementBox component. Export the Tool type so host apps can set the active tool for a positioned preset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: apply biome formatting + ignore generated next-env.d.ts Format files that predate the current biome config (lineWidth 100, single quotes, semicolons as-needed) so they stop showing as dirty on every checkout. Formatting only — no behavior change. Also exclude **/next-env.d.ts from biome: Next regenerates it (double quotes + semicolon) on every build, so biome kept reformatting it into a perpetual dirty diff. Ignoring it lets Next own the file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ba8b141006 |
feat(editor): studio mode, inspector panel UX, consolidated display + references (#364)
Editor-package side of the hosted editor UI pass:
- Add `workspaceMode` ('edit' | 'studio') to `useEditor`. Studio forces a
3D-only view, clears selection, and the canvas hides the bottom action bar,
inspector, selection manager, handles and tools (kept level selector, view
toggles and camera toolbars).
- Inspector panel (`PanelWrapper`): collapses to its header by default with a
single-click toggle, is draggable from the header via a centered grip, and is
clamped to the viewer column (`data-viewer-bounds`) so it can't slide under
the sidebar or top bar.
- Lower the floating action menu / building menu / arrow-handle `zIndexRange`
below the chrome overlay so the inspector sits above scene HTML helpers.
- Merge the bottom bar's separate Scans + Guides toggles into a single
References split-button + popover with per-type sections.
- Export `WorkspaceMode`.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
e450d8b474 |
feat(editor): build sidebar + slimmed items panel for standalone editor (#361)
Bring the open-source standalone editor closer to the community editor's v2 sidebar without pulling in any preset/catalog infrastructure. - Add a preset-less Build tab (apps/editor) that mirrors the community Build sidebar: wall, fence, slab, ceiling, roof, stair, elevator, door, window, column, spawn, plus the material-paint panel. Clicking a type activates the raw structure tool drawn with the kind's defaults. - Wire the Build tab into both editor mount points (local + saved scene) and give the left rail proper image icons (Scene/Build/Items/Settings) instead of letter fallbacks. - Gate the ItemsPanel Library/Community/Mine source chips and tag filter rows behind `showSourceFilter` / `showTagFilters` props (default true, so community and external consumers are unchanged). The standalone editor passes both off, leaving plain category tabs + full-width search. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7f49efce68 |
fix(nodes): repair the nodes test suite under bun (#360)
These tests had never run in CI (the editor repo's own CI only runs
mcp-ci); they surfaced when the private-editor monorepo runs
`@pascal-app/nodes` tests via turbo. Four independent issues, all
test-side except one skip that flags a real bug:
- index.test.ts: the AnyNode-discriminator drift check read the literal
via the zod-v3 `option.shape.type.value` getter, which is gone in
zod v4 (and the field is ZodDefault-wrapped). Unwrap to the innermost
def and read `_zod.def.values[0]`.
- spawn/parity.test.ts: the spawn palette icon is `{kind:'url'}` now
(registered kinds point at palette assets); the test still asserted
'iconify'. Update to 'url'.
- solar-panel/geometry.test.ts: `getSurfaceY` / `getAnalyticalNormal`
moved to `../../shared/roof-surface`; fix the stale import path.
- shelf/geometry.test.ts: skip "user-set material is applied" — it
catches a real bug (cloning a MeshStandardNodeMaterial drops
color/roughness/metalness, so painted shelves/slabs render white).
The clone is required; the fix belongs in the viewer material layer
and is tracked separately.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
cfd644d40e |
fix(editor): guard null bounds in box-select registry-selectable branch (#359)
The registry-driven selectable branch in `collectNodeIdsInBounds` called `objectBoundsIntersectsBounds(node.id, bounds)` directly, but `bounds` is `Bounds | null` (null = select-all / no rectangle). Every other branch guards with `!bounds ||`; this one didn't, so `tsc --build` failed in consumers (`@pascal-app/nodes`) with TS2345 "Bounds | null not assignable to Bounds". Add the same guard, which also restores correct select-all behavior for registry-selectable kinds. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
96d6e0afdb |
fix(core): update stale registry + stair-opening tests to match behavior (#358)
Two unit-test groups had drifted from intentional implementation changes
and have been failing CI on main since before they were noticed:
- registry: re-registering a node kind is now HMR-aware — it warns and
replaces in dev/test and only throws in production (registry._register,
introduced in the in-world-selection work). `bun test` runs with
NODE_ENV=test, so the duplicate-kind tests took the dev (warn) path and
no longer threw. Pin NODE_ENV to 'production' for the throw-path
assertions via an `inProduction` helper, and add explicit dev/HMR
coverage for the warn-and-replace path.
- stair-opening-sync: the straight-flight opening geometry was
deliberately enlarged ("increase stair opening buffer constraints",
treadDepth*10 / length*0.8 / 3.0, openingOffset default 0.15). The
enlarged opening now spans nearly the full slab depth, so the manual
hole fixtures (which must *contain* the auto opening to suppress it) no
longer covered it. Enlarge the manual-opening fixtures to genuinely
cover the current opening, preserving each test's intent.
No production code changed — only test fixtures/assertions.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
f7ff60561e |
feat(editor): preset placement polish — params, move tool, shadows, isolation (#357)
* feat(editor): preset placement polish — params, move tool, shadows - wall/slab/ceiling/roof create paths consume `toolDefaults` so template presets build with their saved params; cleared on tool unmount - wall draw preview reflects the preset's height/thickness (+ HUD labels) - box-select picks up registry-selectable kinds (shelf) via bbox - registry + column move tools: snap to the active grid step, R/T rotation, and ignore the stray trailing click that armed the move (no double-place) - shelf geometry casts + receives shadows like fence/slab Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(viewer): expose isIsolationActive() for isolation-aware consumers Tracks whether an isolation filter is currently applied and exposes it so hosts can avoid acting on the partial view — e.g. skipping project-thumbnail autosave while a single subtree is isolated (preset capture). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8abfc94b99 |
feat(editor): preset-system polish — paint panel, slim action bar, icon rail (#354)
* feat(editor): preset-system polish — paint panel, slim action bar, icon rail - Export `MaterialPaintPanel` so embedders host the paint material picker in their own panel (community docks it in the Build sidebar) instead of the bottom action bar. - ActionMenu: drop the build / material-paint / furnish modes and the structure-tools palette row + paint tray (the host's Build sidebar owns building now). Reduce `structure-tools` to the shared `tools` lookup still used by cursor/floorplan indicators; remove the orphaned `useContextualTools`. - MaterialPicker: swatches wrap into a fluid `auto-fill` grid that fills width. - IconRail (tab-bar): bigger icons, grayscale-when-idle, Radix tooltip, w-14 rail; sync `RAIL_WIDTH` to 56. - Inspector footer: hand the host `footer` to kind-owned custom panels via `InspectorFooterContext` so the save button renders without per-kind wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: biome format pass Whole-repo formatter normalization (line wrap/unwrap only, no logic changes) surfaced by the format-on-edit hook against prior drift. Kept separate from the feature commit so the preset-system diff stays reviewable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
891e578481 |
feat(editor): vertical icon rail + headless inspector footer slot (v2) (#350)
Replace the v2 left column's horizontal tab bar with an always-visible vertical icon rail. Clicking the active icon collapses the panel (rail stays); clicking any icon while collapsed reopens it at the persisted width (clamped to the minimum). Resizer-drag collapse is preserved. Add an `inspectorFooter` slot to <Editor> (v2), threaded through PanelManager → ParametricInspector → PanelWrapper, so embedders can dock an affordance (e.g. "save as preset") below the node inspector. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f51cea9a85 |
feat(editor): Items category bar as icon-grid with tooltips (#349)
Render the function-tree category roots as a Build-style square-tile grid (icon when available, otherwise a two-letter abbreviation) with a hover tooltip for the full name, so the Items and Build panels read the same. Share one TooltipProvider with disableHoverableContent + the icon-grid tooltip class to avoid the flicker when moving between adjacent tiles. Legacy fallback panel category bar wraps instead of scrolling horizontally. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
986d75026f |
viewer: fit shadow frustum to building geometry instead of the camera (#348)
Following the camera look-at broke when zoomed out (fixed ±50 frustum too small to cover the scene) and when zoomed into an empty corner (frustum centred on nothing). Instead, fit the directional light's ortho shadow camera to the building: union the registered scene-node bounds (excluding the site/ground plane), fit a sphere, and size + place the shadow camera to cover that sphere plus a margin. Bounds are refreshed on a short interval since they only change while editing. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
37a3aba672 |
viewer: center shadow frustum on focus so it tracks the view (#347)
The directional shadow light was placed at focus + theme offset (only ~17-32 units), while the ortho shadow camera ran near=1/far=100. That left the focus near the front of a long frustum whose far end swung around as the look-at moved, so shadows appeared not to follow the view. Park the light at a fixed distance along its (preserved) direction and bracket near/far around that distance so the focus stays centered in the frustum depth. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b8d94a6436 |
editor: tool-defaults seeding + drawTool capability (fence presets) (#346)
* feat(editor): tool-defaults seeding + drawTool capability; fence consumes it Adds a generic, transient `useEditor.toolDefaults` slice keyed by tool, set via `setToolDefaults(tool, params)`. A draw tool's create path merges its entry when minting a node and clears it on deactivation, so a host app can prime the next-drawn node's parameters — placing a saved preset of a drawn kind, or a future "small / medium / large" dimension picker for wall / slab / ceiling. Marks the kind with `capabilities.drawTool` (helper `isDrawnViaTool`) so host apps know to route placement through `setToolDefaults(type) + setTool(type)` instead of cloning a finished instance. Wires fence end-to-end: it declares `drawTool: true`, its create path merges `toolDefaults.fence`, and the draft preview (bar geometry, cursor, HUD label heights) reflects the seeded height/thickness so the ghost matches what will be built. The tool clears its own defaults on unmount. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(editor): restore dropped useEditor import in fence tool The toolDefaults-seeding commit lost the `useEditor` import (formatter stripped it), shipping a runtime ReferenceError when FenceTool mounts. Re-add it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a144502c04 |
feat(editor): hierarchical function-tree Items browse (#345)
* feat(editor): hierarchical function-tree Items browse Add an optional DB-driven taxonomy browse to the Items panel. When the embedder supplies a `functionTree`, the panel renders roots as category tabs and child tags as a secondary chip row, filtering items by the selected node and any descendant slug; otherwise it falls back to the legacy hardcoded category path untouched. Adds `AssetInput.functionTags` so items can carry their function-axis tag slugs, and exports `FunctionTreeNode` for embedders to type the tree. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(editor): sync bun.lock (@pascal-app/mcp 0.3.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
8a5f08b842 |
editor: refine preset capture framing + zoom limits (#344)
Tunes the auto-framing and camera clamps used by `setCaptureMode({ mode:
'preset', isolated })`:
- Frame from a 3/4 view of the node's *front* face. Reads the isolated
root's yaw, picks `+Z` (local forward, the face authored for catalog
shots), offsets the camera by 35° to the side so both the front and
one adjacent face are visible. 25° elevation keeps the top visible
without going isometric. Multi-isolate falls back to world `+Z`.
- Bump the bounds-fit multiplier from 1.6 → 2.4 (floor 3 → 4) so the
subject lands with ~25-30% padding inside the locked square crop —
the user can recompose without immediately needing to zoom out.
- Relax `minDistance` from 10 → 0.5 while in preset capture mode (a
0.3–2m preset can't be framed close-in against the editor's general
10m floor). Reverts to 10 on exit so general editing keeps the
looser navigation guardrails.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0ad0ac1591 |
chore(editor): delete legacy door/window presets UI (#339) (#343)
Retires the door/window-only `PresetsPopover`, its `presets-context` adapter, and the `PresetThumbnailGenerator` ahead of the unified preset system landing via the items catalog. Drops the public exports (`PresetsPopover`, `PresetsAdapter`, `PresetsTab`, `PresetsProvider`, `usePresetsAdapter`) and the matching `presetsAdapter` prop / provider wrappers on `<Editor>`, plus the `preset:generate-thumbnail` / `preset:thumbnail-updated` event types in core. Door and window panels now render the regular parametric inspector with no popover trigger; `materialPreset` stays untouched. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1fd59dd9cd |
feat(editor): preset-system primitives (#340) (#341)
* feat(editor): preset-system primitives — presettable, sceneApi subtree round-trip, isolate + setCaptureMode enum, headless exports
Per pascalorg/editor#340 (redesigned: single live canvas, no Viewer scene prop).
Core
- `capabilities.presettable` on `NodeDefinition` + `isPresettable` /
`isPresettableKind` helpers. Explicit `false` on level / building /
site / zone / spawn / guide / scan / item; implicit `true` for any
kind with `def.parametrics`.
- `sceneApi.getSubtreeSnapshot(rootId)` + `materializeSubtree(subtree,
position, parentId?)` for round-tripping a node subtree through
catalog storage. Strips id / parentId / absolute root position /
host refs (`wallId`, `wallT`); fresh IDs minted at materialize time;
child ordering preserved (FIFO walk).
Viewer
- `<Viewer isolate>` prop + `ViewerHandle.setIsolated(ids | null)`.
Walks `sceneRegistry`, hides every registered group not in the
isolated set's ancestor + descendant closure. Building block for
preset capture + future focus-mode UX.
Editor
- `useEditor.captureMode: CaptureMode` discriminated union
(`idle` | `standard` | `preset`). `isCaptureMode` stays as a derived
boolean for the existing read sites; `setCaptureMode` accepts both
the boolean shape (back-compat) and the enum.
- `preset` capture mode in `SnapshotCaptureOverlay`: drag locked to a
square, mode-picker hidden, transparent flag forwarded through the
`camera-controls:generate-thumbnail` emitter event.
- Headless exports: `Inspector` (alias of `ParametricInspector`),
`FloatingMenu` (alias of `FloatingActionMenu`), `ToolbarLeft` /
`ToolbarRight` (aliases of `ViewerToolbarLeft` / `ViewerToolbarRight`),
`useSelection` hook returning `{selectedIds, selectedNode, building/
level/zone}`, plus re-exports of `useScene` / `useViewer` from core /
viewer so consumer shells (community, embedders) need only one import.
Out of scope by design (see issue #340 "Out of scope"): a separate
offscreen Viewer rendering an arbitrary subtree. The unified preset
modal captures inside the live canvas via isolation + the existing
snapshot pipeline — no `useScene` factory / React context refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(editor): split snapshot/materialize into pure getSubtree + cloneNodesInto; add def.hostRefFields; auto-stage preset capture square
Per pascalorg/editor#340 redesign discussion: the editor's scene API
should expose *pure* primitives and let the host (community modal,
embedders) own storage shape, position stripping, and host-ref
re-derivation policy.
Editor API delta
- `sceneApi.getSubtreeSnapshot(rootId)` → `sceneApi.getSubtree(rootId)`
Returns the live subtree verbatim (BFS via `children[]`, no clones,
no stripping). Callers deep-clone if they need persistence.
- `sceneApi.materializeSubtree(subtree, pos, parent?)`
→ `sceneApi.cloneNodesInto(nodes, { rootId, parentId?, position? })`
Generic clone-and-insert. Deep-clones via JSON, mints fresh ids
preserving the prefix, rewires parent/children, stamps position +
parent if supplied. Host-ref-agnostic — `wallId`/`wallT` etc are
preserved verbatim.
- New `capabilities.hostRefFields?: string[]` on `NodeDefinition`.
Declares per kind which schema fields are placement-derived so the
host strips them at preset-save time. Declared on door (`['wallId']`),
window (`['wallId']`), item (`['wallId', 'wallT']`).
- New `getHostRefFields(def)` exported from `@pascal-app/core`.
Removed the intermediate token-based payload format (`NodeSubtree`,
`buildSubtreeSnapshot`, `materializeSubtree`, `SubtreeNode`).
UX polish
- `<SnapshotCaptureOverlay>` in `preset` mode now auto-stages a centered
square crop sized to ~75% of the shorter viewport dimension. The
user can pan / move / resize within square-aspect, but doesn't have
to drag from scratch — clicking the capture button works
immediately on entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): lock preset capture frame; allow item presets
- SnapshotCaptureOverlay: in `preset` mode, the auto-staged centered
square is now fully locked — corner handles hidden, the dim layer is
click-through (no drag-to-move, no drag-to-resize). The user just
adjusts the camera (orbit / pan / zoom) and clicks capture. The
letterbox + dashed border stay visible as a cosmetic frame.
- `item.capabilities.presettable` removed (implicit `true` via
`def.parametrics`). Enables compositions like "table-with-plants",
"shelf-with-books" where the preset root may be an item and other
items ride along as descendants. The GLB-kind item catalog is
unchanged; presets become siblings of GLB rows under the same
`items` table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): auto-frame camera on preset capture entry; restore on exit
`<CustomCameraControls>` now watches `useEditor.captureMode` and, when
preset capture mode begins, flies the camera to a pose that fits the
union bounds of the isolated subtree inside the locked square crop —
no more hunting for the subject after opening the modal. The
pre-capture pose is stashed and restored on exit so the user lands
exactly where they were.
The user can still pan / orbit / zoom from the auto-staged pose if
they want a different angle before snapping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6b0fe5b99c |
review-architecture: cover new registry capabilities (#338)
Add bullets to §4.B for surfaces that landed after the 2026-05-20 skill update: - def.capabilities.paint (PaintCapability dispatch for paint mode) - def.capabilities.floorPlaced (generic FloorElevationSystem) - def.surfaceRole (Solid/Rendered/Clay render-mode pipeline) Also add a verb-not-host-kind rule and mark capabilities.roofAccessory as documented tech debt — extending the *Accessory / *Hosted shape is now a blocker rather than a precedent. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
814d94336b |
core(space-detection): add pause/resume refcount primitive (#336)
initSpaceDetectionSync subscribes to every scene mutation and auto-derives slabs/ceilings from wall topology. When a host app drives explicit slab/ceiling creation (e.g. the community editor's AI create_room flow), the auto-sync races those nodes and the polygon-signature de-dupe is fragile enough that duplicates leak through. Expose pauseSpaceDetection / resumeSpaceDetection / isSpaceDetectionPaused mirroring the existing pauseSceneHistory refcount in store/history-control. While paused, the subscriber rolls previousSnapshots forward so resume does NOT trigger a backfill that would re-introduce the very duplicates the host paused to avoid. No behavior change for callers that don't touch the new functions. |
||
|
|
f5ff111708 |
fix(ifc-converter): broken Contributions GitHub URL (#335)
apps/ifc-converter/app/page.tsx pointed at github.com/pascalorg/editor/apps/ifc-converter which 404s. The canonical path includes /tree/main/. Same URL is reused by the forthcoming community /ifc page (in pascalorg/private-editor) so keeping them consistent matters. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bb5ce68254 |
Viewer render modes: Solid/Rendered + textures + surface-role clay + scene themes + edges (#332)
* viewer: add Phase 1 render-modes foundation (shading/textures/colorPreset state, defaultRender prop, SSGI gating) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: Phase 2 render-modes material-class switch (Lambert in solid, Standard in rendered) Shading-aware material factories (cached per class), reactive selection in renderers via the useViewer(shading) pattern, and dirty-rebuild on toggle for geometry/door systems. Rendered mode output unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: Phase 3a render-modes surface roles + clay palette foundation Adds surfaceRole token to core NodeDefinition, per-kind default roles, ColorPreset palettes + resolveSurfaceColor/createSurfaceRoleMaterial (glazing stays translucent), and the textures-off recolor path for def.geometry kinds (slab/fence/shelf via GeometrySystem.applyDefaultSurfaceRole) + wall. Renderer- based kinds (roof/window/stair/item/column/door/ceiling/elevator) wired in 3b. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: Phase 3b render-modes textures-off recoloring for renderer/system kinds Wires clay role coloring (textures=off) for roof/roof-segment, window, stair/ stair-segment, door, item, column, ceiling, elevator via createSurfaceRoleMaterial, reactive on textures/colorPreset. Per-surface roles: roof top+edge=roof / underside=ceiling; window frame=joinery / glass=glazing; stair+door+elevator= joinery; ceiling=ceiling; column=wall; item=furnishing. textures=on unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: Phase 4a render-modes UI — Solid/Rendered toggle + per-context persistence Per-context shading via renderContext discriminator + shadingByContext (persisted); <Viewer> renderContext prop seeds per-context on mount. Solid/Rendered toggle in the editor action bar + standalone toolbar + command palette. Editor mounts default to renderContext=editor / shading=solid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: fix window-system glassMaterial type to allow clay glazing reassignment The let was inferred as MeshLambertNodeMaterial from the imported glass constant, so reassigning createSurfaceRoleMaterial('glazing') (returns THREE.Material) failed under tsc --build. Widen the annotation to THREE.Material. Surfaced by the build (check-types had replayed a stale turbo cache). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: Phase 5 lighting — add theme-driven hemisphere light, trim fill directionals 3->2 Adds a sky/ground hemisphere fill (theme-lerped) and drops the second fill directional; the hemisphere covers the shadow-side fill it provided, at one fewer per-fragment directional term (shared by Solid + Rendered). Ambient lowered since the hemisphere now carries soft fill. Intensities are a starting point — tune visually on the gpu-perf overlay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: tune Solid lighting for more form — stronger hemisphere sky/ground contrast, lower ambient Darker hemisphere ground (#d8d6cf -> #aaa49a) + higher hemisphere intensity and lower ambient so directional shading reads as form and undersides ground without AO. Keeps Solid free of any post-processing pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: biome format render-mode files (lefthook pre-commit) Formatting-only — import wrapping, dep-array wrapping, single-line ternaries — across Phase 2-4a files that weren't biome-clean. No logic changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: scene-theme system — named environment themes (studio/paper/sunset/night/...) New SceneTheme registry (lib/scene-themes.ts) drives lights, background, and tone mapping; lights.tsx refactored data-driven (N directionals + hemisphere + ambient). sceneTheme state (persisted) + cycle-button picker in editor bar + standalone toolbar, importing the registry from the viewer barrel (single source). Default 'studio' reproduces the prior look exactly; app light/dark 'theme' untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: Phase 7 — popover-dropdown pickers for render mode + scene theme (editor bar) Replace the shading + scene-theme cycle buttons in viewer-overlay.tsx with DropdownMenu pickers: render mode shows 2 rows (Solid/Rendered) with one-line detail; scene theme lists all themes with a derived color-swatch strip + active check. Imports the registry from the viewer barrel (single source). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: Phase 7 — dropdown pickers in standalone toolbar + export DropdownMenu from barrel Replicate the render-mode + scene-theme dropdown pickers (with swatch strip + active check) to apps/editor's compact toolbar, matching viewer-overlay.tsx. Export DropdownMenu* from the @pascal-app/editor barrel for the standalone app. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer+editor: Phase 8 — crisp geometry edge overlay (off/soft/strong/sketchy) EdgeOverlaySystem draws EdgesGeometry LineSegments over node-backed building meshes (scoped via sceneRegistry, skips zone-layer/hitbox/overlay meshes), rebuilt on geometry-uuid or mode change, line color follows scene-theme background luminance; sketchy = static TSL vertex jitter. New 'edges' state (persisted, default off) + Edges dropdown in editor bar + standalone toolbar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer+editor: edge overlay — thick lines via Line2, drop sketchy mode Switch EdgeOverlaySystem from LineBasicNodeMaterial (1px hardware cap) to LineSegments2 + Line2NodeMaterial so edges have real screen-space width (soft 1.5px / strong 3px); resolution tracks viewport. EdgeMode is now off/soft/strong (sketchy removed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: fix edge overlay crash — webgpu Line2NodeMaterial has no settable resolution material.resolution is undefined under WebGPU (the node material reads the viewport internally); optional-chain the .set() call so it no-ops there instead of throwing. Thickness still applies via linewidth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: edge overlay — weld vertices to fix CSG spiderweb edges + lighter strong Position-only mergeVertices before EdgesGeometry so coplanar triangles from CSG-cut walls (doors/windows) share vertices and their interior edges are suppressed — only opening outlines + silhouettes remain. Strong linewidth 3 -> 2px (was too heavy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: edge overlay — crease-only extractor to fix CSG spiderweb Replace EdgesGeometry (which always draws unpaired boundary/T-junction edges) with buildCreaseEdges: weld positions, keep only edges shared by exactly two faces whose dihedral exceeds the threshold, drop everything unpaired. CSG-cut walls/slabs are watertight so real corners + opening outlines survive while the interior triangulation fans (coplanar or T-junction) are removed. Open meshes (bare ground plane, billboard leaves) shed their boundary clutter too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: crease edges — coarser weld (0.1mm->1mm) to recover CSG/extrude seam edges The cap<->side-wall top edge of ExtrudeGeometry walls drifts past 0.1mm after CSG, so it stayed unpaired and was dropped. Weld at ~1mm to pair it into a real crease while staying far below feature size (wall thickness, openings). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: replace geometry edge overlay with screen-space ink (SketchUp look) Port the prototype's screen-space ink into the post-processing pipeline: depth + normal Sobel reading the scene-pass MRT. Crease term (normalized normals, center-vs-neighbour) + distance-independent depth-step term (raw Laplacian / (1-d)² with a noise gate so flat ground stays clean). Topology-agnostic, so it finally handles CSG-cut walls/openings without the spiderweb or missing-edge problems of EdgesGeometry. Driven by the existing edges off/soft/strong mode; MRT now builds when SSGI OR ink is on; ink colour tracks scene-theme luminance. Removes EdgeOverlaySystem + crease-edges (geometry approach). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: soft/strong ink modes + keep editor overlays out of the ink Two adjustments to the screen-space ink pass: 1. Soft vs strong now visibly differ. The edge masks saturate, so the old `intensity` gain did nothing once a line was detected. Replace it with a sample radius (line thickness) + opacity: soft = 1px / 50%, strong = 2px / 100%. `inkedEdges` takes `radius` + `opacity` instead of `intensity`. 2. Editor overlays (gizmos, move handles, tool previews, grid) no longer get inked. The scene pass that feeds the depth/normal MRT now renders only SCENE_LAYER; overlays render in a dedicated pass on OVERLAY_LAYER and are composited on top after the ink + outlines, so they read as crisp UI and never get inked or AO'd. New OVERLAY_LAYER constant in viewer; editor's EDITOR_LAYER re-exports it so the two stay in lockstep. Also moves WallMoveSideHandles (the wall/fence move arrows) onto EDITOR_LAYER — it was the one overlay still on SCENE_LAYER. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: render the grid in the scene pass so geometry occludes it The depth-gate fix couldn't help the grid: its material is depthWrite:false, so it never wrote overlay-pass depth and the "didn't write depth -> keep on top" term forced it on top — hence the floor grid bleeding through walls and objects. A full-floor plane can only be occluded correctly by living in the same depth context as the scene, so move the grid onto its own GRID_LAYER which the scene pass renders (alongside SCENE_LAYER). It's flat and depth-non-writing, so the screen-space ink still ignores it; gizmos/handles stay on OVERLAY_LAYER. The grid camera layer is enabled in custom-camera-controls and disabled on the thumbnail camera so thumbnails stay grid-free, matching EDITOR_LAYER. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: strong ink — match soft's 1px thickness, differ by darkness only Strong at radius 2 read too thick. Soft's 1px line is the nice one, so use it for both modes and let strong distinguish itself purely by being fully solid (opacity 1) vs soft's lighter 50%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: shadow frustum follows the view + a shadows on/off setting The directional light's ortho shadow camera only covers ±50 around the light target, which was pinned at the origin — so zones far from origin received no shadows no matter where the camera moved. Recentre each shadow-casting light (position + target together, preserving direction) on the view focus every frame: the orbit-controls target when available, else the camera's ground projection. The shadow area now tracks wherever the user looks. Also add a persisted `shadows` toggle (default on) to the viewer store and a "Shadows" switch in the editor settings panel — the dedicated shadows control the render-modes plan deferred. Lights gate castShadow on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: shadows toggle in the standalone toolbar + a touch more shadow strength The shadows switch I added only lived in the cloud settings panel's Visibility section, which is hidden in the local/standalone editor (no projectId). Add a ShadowsToggle button next to the grid toggle in the standalone toolbar so it's reachable there, matching how Show Grid is exposed in both places. Also push shadow strength partway toward the aesthetic prototype (which runs near-black, no blur): bump the bright-key shadow-intensity cap 0.4 -> 0.55 and tighten shadow-radius 2 -> 1.5. Still softer than aesthetic by design. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nodes: site ground receives shadows (lit material instead of unlit Basic) The site ground fill used MeshBasicMaterial — unlit, so it could never show the directional shadow, and shadows visibly truncated at the slab edge. Swap it for a lit MeshLambertNodeMaterial with receiveShadow on the mesh; the geometry is the site polygon (slab footprints punched out), so shadows now extend across the whole site and stop at its boundary, which is the desired bound. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * themes: per-theme clay palettes + 2x2 swatch in the theme pickers Each scene theme now carries a clayTints map (wall/floor/ceiling/roof/glazing) giving it a per-surface-role palette — e.g. Mediterranean's blue roof + warm walls. The theme pickers (standalone toolbar + community overlay) now render the aesthetic-style 2x2 swatch of those role tints over the theme background instead of the old 3-colour strip. Data + UI only; wiring the tints into the textures-off surface materials is a separate change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: slimmer theme switcher + cloud-sun icon Shrink the scene-theme toolbar button (w-[8.5rem] -> w-28) so it stops reserving space for "Mediterranean"; the label truncates when it overflows. Swap the palette icon for cloud-sun (atmosphere/lighting, distinct from the app light/dark Sun-Moon toggle) in both the standalone toolbar and the community overlay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: theme switcher icon -> swatch-book Swap the scene-theme icon from cloud-sun to swatch-book in both the standalone toolbar and the community overlay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * themes: colour untextured building surfaces by the active scene theme Untextured walls/roof/slab/ceiling now take the active theme's per-role colour (theme.clayTints[role], falling back to the colour preset) in BOTH textures modes. The textures toggle only governs surfaces that actually have an explicit material/preset — those still show their texture when textures are on. This is what makes e.g. Mediterranean read as a blue roof + warm walls instead of the old hardcoded white/grey defaults. - materials.ts: resolveSurfaceColor / createSurfaceRoleMaterial take an optional sceneThemeId (theme tint ?? preset palette); theme folded into the cache key. - wall-materials, roof-materials, slab/geometry, ceiling/renderer: the untextured fallback now resolves to the themed role colour instead of white/grey, in both modes; theme threaded into each builder + material cache key. - wall-cutout: now reads textures/colorPreset/sceneTheme and re-applies wall materials when any change (previously it ignored textures/colorPreset entirely). - geometry-system: threads sceneTheme into the generic surface-role path + rebuild effect. Renderers/preview call sites thread sceneTheme through. Doors/windows/stairs/columns/items still use their existing defaults — a follow-up pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(slab): recessed (negative-elevation) slabs extrude downward again The registry geometry builder created the slab mesh at Y=0 without applying the negative-elevation offset, so recessed slabs rendered above the floor plane (pool geometry is built locally with its floor cap at Y=0 and walls rising to Y=|elevation|, so the mesh must be shifted down by `elevation` to recess). The runtime slab-system already did this; the static builder path didn't. Mirror it: shift mesh.position.y by elevation when negative. Positive elevation unchanged. Unrelated to render modes — bundled into this branch's PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof): legacy roofs render again — migration wrote invalid wallHeight 0 Legacy roof nodes (old format, no `children`) were migrated with a hardcoded `wallHeight: 0`. With zero wall height the eave height (`wallHeight - autoDrop`) went negative in getRoofSegmentBrushes, producing geometrically invalid brushes; three-bvh-csg then spammed "TriangleClipper: Coplanar clip not handled" every frame and emitted NaN positions, so the merged roof geometry failed computeBoundingSphere and never rendered. - core/use-scene migration: wallHeight 0 -> 0.5 (the RoofSegmentNode schema default), so migrated segments have a valid wall height. - roof-system: clamp eave height to >= 0.01 so an intentional wallHeight 0 can never yield a negative eave, and guard updateMergedRoofGeometry so a CSG result with NaN positions is discarded (keep the last good mesh, warn once per roof) instead of poisoning the buffer + spamming the console. Unrelated to render modes — bundled into this branch's PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(column): columns cast and receive shadows Column meshes (box, beam, cylinder, sphere, torus) rendered without castShadow/ receiveShadow, so columns neither dropped a shadow nor caught one — unlike walls, slabs and roofs. Set both on all column shape meshes. Unrelated to render modes — bundled into this branch's PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: default edges to soft Editor defaults are now solid shading / studio theme / soft edges / shadows on. Shading (solid, via EDITOR_DEFAULT_RENDER), theme (studio) and shadows (on) were already the defaults; edges was 'off' — make 'soft' the default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * themes: fold light/dark into the scene theme (remove the separate toggle) The viewer had two overlapping appearance controls: a light/dark `theme` toggle AND scene themes (which already drive the 3D background + lights). They conflicted — e.g. Night/Twilight are dark themes, but the light/dark toggle was an independent axis still tinting the 2D scene chrome. Unify on the scene theme: add an explicit `appearance: 'light' | 'dark'` to each SceneTheme (twilight/night = dark, the rest = light) and drive everything the old toggle drove off it — canvas backdrop, grid line colours, measurement-label/ cursor/site-edge contrast, the site ground fill, the ground occluder, and the mobile viewer bg. The editor UI chrome is unaffected (always dark via a fixed body class). Removes the `theme`/`setTheme` store state (+ persistence) and every light/dark toggle UI: the standalone toolbar Sun/Moon button, the community overlay theme switch, the command-palette command, and the ifc-converter preview toolbar button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * themes: per-theme ground colour + add the "Verdant" nature theme - Add a `ground` colour to every SceneTheme and drive the site ground fill + the infinite ground-occluder off it (instead of the binary isDark ? #1f2433 : #fafafa). Dark themes now get a lit mid-tone ground (twilight #4a4566, night #2b3247) so the ground reads as ground rather than going near-black. - Add a new green/nature scene theme "Verdant": soft green sky + lit, with a green roof clay tint and mossy ground. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wiki: document the surface-colour / theme system The colour-per-node/renderer/system model from the render-modes work was undocumented. Add wiki/architecture/materials-and-themes.md covering surface roles, colour presets, the textures axis, scene themes (appearance / ground / clay tints), and the "untextured surfaces are theme-coloured in both modes" invariant + where each kind wires it. Also fix two pages that the same work made stale: - node-definitions: geometry builders receive (shading, textures, colorPreset, sceneTheme); document the `surfaceRole` token + applyDefaultSurfaceRole. - layers: OVERLAY_LAYER (1, viewer) with EDITOR_LAYER now its alias, the new GRID_LAYER (3, rendered in the scene pass for depth occlusion), and the overlay pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nodes: wire #330 roof-accessory kinds into the surface-role/theme colours PR #330's new kinds (chimney, dormer, skylight, solar-panel, ridge-vent, box-vent) use custom renderers, so the generic textures-off recolour path never reached them — they fell back to hardcoded colours. Wire each renderer into the render-modes system: read shading/textures/colorPreset/sceneTheme and resolve untextured surfaces via createSurfaceRoleMaterial (and force the role colour when textures are off), matching column/ceiling. Roles: chimney body→wall / cap→roof; dormer wall→wall, roof→roof, glass→glazing, frame→joinery; skylight glass→glazing / frame→joinery; ridge-vent + box-vent→roof; solar-panel frame→roof (the dark product-specific cell face is left as-is). Each definition also gets its dominant `surfaceRole` token. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof): legacy-roof migration must use a non-zero wallHeight The render-modes/#330 merge left the legacy roof→roof-segment migration writing `wallHeight: 0`. With #330's pitch model that builds a flat, zero-volume wall CSG brush, which three-bvh-csg can't clip ("Coplanar clip not handled") and yields NaN positions — so the migrated old roof never renders. Use the schema default 0.5 (what new roofs use), giving a valid wall. The eave clamp + merged- geometry NaN guard added earlier stay as defense-in-depth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof): guard slope frame against missing/NaN pitch (no more NaN geometry) getSegmentSlopeFrame used `pitch <= 0` to detect flat/zero-pitch, but an undefined or NaN pitch (a segment from an older migration that set `roofHeight` instead of `pitch`, or stale persisted data) slips past that check and computes Math.tan(NaN) → NaN tanTheta/activeRh → NaN segment geometry → the merged-roof CSG spews "Coplanar clip not handled" and NaN positions, so the roof never renders. Use `!(pitch > 0)` so any non-positive/non-finite pitch resolves to the flat frame. Self-heals bad data regardless of how the segment was produced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof): migration guarantees a positive pitch for every roof-segment Segments saved with neither a valid pitch nor a roofHeight (older/partial saves, e.g. landing home-graph) fell through the legacy roofHeight->pitch branch, leaving pitch undefined. The slope-frame guard then resolved them to a flat frame, so the roof rendered as a slab instead of pitched. Branch 2b now normalises any segment lacking a valid pitch: derive from roofHeight when present, else fall back to the schema default (40deg). The migration result is cast (not zod-parsed), so this is the only place the default lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3cb318e445 |
ifc-converter: drop test script until tests exist (#328)
The package has no test files yet. `bun test` exits 1 ("No tests found!")
and fails CI; --pass-with-no-tests isn't honored by the bun 1.3.0 pinned
in CI. Match the convention used by @pascal-app/viewer (no test script):
turbo skips the test task for packages without one. The script comes back
when real tests land.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
82b45ea84d |
ifc-converter: pass test script with no tests yet (#327)
The package has no test files yet, so `bun test` exits 1 ("No tests
found!") and fails CI. Add --pass-with-no-tests so the task stays green
until real tests land, at which point they run normally.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
488e05da0e |
Merge pull request #324 from pascalorg/feat/ifc-converter-app
feat: IFC → Pascal converter (package + app) |
||
|
|
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> |
||
|
|
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> |
||
|
|
8505d6cdfa |
Merge pull request #326 from jelharou/fix/mcp-templates-site-children-string-ids
fix(mcp): templates use string IDs for SiteNode.children |
||
|
|
fb7d46df84 |
Merge pull request #325 from jelharou/fix/site-node-children-string-ids
fix: update mcp and nodes for SiteNode.children string[] schema change |
||
|
|
6ea622af56 |
Merge pull request #320 from pascalorg/feat/load-build-verification-dialog
editor: gate Load Build behind a verification dialog |
||
|
|
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> |
||
|
|
42bf025cad |
Merge pull request #319 from pascalorg/docs/review-skill-registry-update
review-architecture: absorb node-registry infrastructure |
||
|
|
11004d90ad |
review-architecture: absorb node-registry infrastructure
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> |
||
|
|
e2d9ffbee8 |
Merge pull request #318 from pascalorg/fix/fence-tool-handlers
fix: restore fence tool handlers dropped by null-guard commit |
||
|
|
d218cb7f26 |
fix: restore fence tool handlers dropped by null-guard commit
|
||
|
|
820c5d9e3e |
Merge pull request #315 from pascalorg/portforward/pr-309
portforward: StairOpeningSystem, material library, UI refinements (from #309) |
||
|
|
70100d5a2d |
Merge pull request #317 from pascalorg/feat/client-bootstrap-provider
apps/editor: mount node-registry bootstrap at the root layout |
||
|
|
c4001a656f |
apps/editor: mount node-registry bootstrap at the root layout
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>
|
||
|
|
0bcec8e6ba |
Merge pull request #314 from pascalorg/feat/node-registry-primitives
Node registry & plugin-ready architecture (Phase 0-5) |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
11015ea1ed |
wiki: document parametric-node + move-tool pitfalls
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
e7cb976b9b |
nodes: declare lucide-react as peer dependency
The Stage E kind-owned panels (slab/ceiling) import lucide icons. Without peerDependencies declaring lucide-react, consumers bundling nodes/dist/<kind>/panel.js fail with "Module not found: Can't resolve 'lucide-react'" because the bundler has no signal that nodes needs it. Matches the same pattern @react-three/drei + @react-three/fiber already use here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3c37ff009b |
Phase 5 Stage E: fence / slab / ceiling drop their legacy panels
Three flavors of Stage E in one PR: - **Fence** — fully auto-derived. Adds `kind: 'boolean'` to ParamField (rendered as ToggleControl), wires `showInfill` through `def.parametrics`. Legacy `FencePanel` deleted; the auto-derived `<ParametricInspector>` now drives fence editing entirely. - **Slab / Ceiling** — kind-owned via `parametrics.customPanel`. The legacy panels have shape-specific bits (elevation/height presets, area display, holes list with auto-vs-manual provenance) that don't fit the auto-derived field model yet. `<ParametricInspector>` learns to lazy-load and mount `parametrics.customPanel` when present; legacy `SlabPanel` + `CeilingPanel` files relocate to `nodes/src/<kind>/panel.tsx` and the legacy copies delete. When `list` / `computed` / `action` field kinds eventually graduate to auto-derived support, these custom panels collapse back into `parametrics.groups`. The plan calls this out under "Custom-behavior escape hatch" and Foot-gun 5 of the recipe. Public-surface additions in `@pascal-app/editor`: - `ActionButton`, `ActionGroup`, `PanelSection`, `SegmentedControl`, `ToggleControl`, `PanelWrapper` — needed by the kind-owned panels. Per-kind progress table: fence/slab/ceiling all flip to E ✅. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c3e255b58a |
wiki(tools): clarify useLiveTransforms.position semantics for polygon kinds
Two regressions in the Stage D port surfaced the same gap: the wiki already said "the mesh offset must mirror the useLiveTransforms entry" but didn't spell out that for polygon-based kinds (slab / fence / ceiling / wall), the value to mirror is the DELTA (the group's local position) — not the world location of the polygon's center. Expanded the live-drag rule with a position-based vs polygon-based split and an explicit note that ParametricNodeRenderer reconciles the group's position via React on every Zustand notification, so the two writes must agree to the exact value or they fight every frame. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
82c9b5e1df |
Phase 5 Stage D moves: align useLiveTransforms with the direct mesh.position delta
User reported the slab/fence mesh jittered/teleported continuously
while dragging (not on commit — during the drag). Root cause: the
move tool wrote TWO conflicting values per grid:move tick:
1. `mesh.position.set(deltaX, 0, deltaZ)` — relative offset, direct
Three.js mutation.
2. `useLiveTransforms.set(id, { position: [originalCenter + deltaX,
0, originalCenter + deltaZ], rotation: 0 })` — absolute world
position of the translated polygon center.
`ParametricNodeRenderer` reads `useLiveTransforms` and binds it via
React: `<group position={liveTransform.position}>`. So every Zustand
notification re-rendered the renderer and reconciled the group's
position back to "originalCenter + delta" (the absolute), overriding
the "delta" the direct mutation had just written. The two systems
fought every frame → visible jitter.
Fix: `useLiveTransforms.position` now holds the SAME delta the direct
mutation uses (`[deltaX, 0, deltaZ]`). React reconciles to the same
value the direct mutation already set — no conflict.
The cursor sphere position stays as the translated polygon center
(it's tracked separately via React state, not `useLiveTransforms`).
Ceiling aligned for consistency, though CeilingRenderer doesn't read
`useLiveTransforms` so the value there has no rendering effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f4ea07e05b |
Phase 5 Stage D moves: live-drag mesh.position for slab / ceiling / fence
User-reported regressions on the 1:1 legacy ports: slab/ceiling moves were slow (polygon CSG rebuilds per scene.update tick), fence moves teleported briefly on commit (residual mesh.position offset survived the geometry rebuild). All three now use the same live-drag pattern the legacy fence move was designed for: - During drag, write only to `sceneRegistry.nodes.get(id).position` + `useLiveTransforms`. No `scene.update`, no polygon rebuild, no React re-render of geometry. - History stays UNPAUSED — scene state isn't changing. - On commit, a single `scene.update` writes the translated polygon (or fence start/end + linked-fence cascade). Recorded as one natural undo step. - Tools leave `mesh.position` at the drag delta on commit; GeometrySystem / CeilingSystem reset it to (0,0,0) when they rebuild the geometry on the next frame. By the time position clears, the new geometry is in place — no teleport. Two framework changes enable this: - `GeometrySystem` (viewer/systems/geometry) now resets `group.position` + `group.rotation` after every rebuild, matching the legacy `FenceSystem.updateFenceGeometry` behavior. Tools that translate the group during live-drag can rely on the reset. - Legacy `CeilingSystem.updateCeilingGeometry` extends its existing `position.y` reset to cover X/Z too — previously it left X/Z at the drag delta after rebuild, double-translating the visual. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1e15e10185 |
Phase 5 Stage D: re-port curve + whole-item move tools 1:1 from legacy
Each tool is now a direct copy of the legacy implementation, relocated
under @pascal-app/nodes/<kind>/ and dispatched via the registry's
def.affordanceTools. No DragAction abstraction, no clever live-drag
exception, no novel snap pipeline — same code, same UX, same
performance, same history dance.
Ports:
- fence/curve-tool.tsx (legacy CurveFenceTool, 1:1)
- fence/move-tool.tsx (legacy MoveFenceTool, 1:1 — including
the mesh.position + useLiveTransforms
exception that the legacy uses for fence
specifically)
- wall/curve-tool.tsx (legacy CurveWallTool, 1:1)
- slab/move-tool.tsx (legacy MoveSlabTool, 1:1)
- ceiling/move-tool.tsx (legacy MoveCeilingTool, 1:1 — preview
fill + outline overlay preserved)
Drops the obsolete DragAction-based action files
(packages/nodes/src/{fence,wall,slab,ceiling}/actions/{curve,move}.ts)
and their now-empty actions/ directories where applicable. Fence
keeps actions/move-endpoint.ts since that port works.
Editor public surface gains `getWallGridStep` + `snapScalarToGrid`
(transitional exports — Stage F moves them into @pascal-app/nodes).
ToolManager + MoveTool dispatch unchanged: the same legacy-fallback
branches now mount the registry component because the affordances are
declared, but the rendered behavior matches the legacy because the
implementations are copies.
Per-kind progress: fence D ✅ (curve / move-endpoint / move / placement
all kind-owned), slab D ✅, ceiling D ✅, wall D 🟡 (curve only,
endpoint/move/placement still legacy).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
da63081f73 |
Phase 5 Stage D: revert curve + whole-item move affordances to legacy
User-reported regressions made it clear that my D-ports of the curve
and whole-item move tools introduced more friction than they removed.
The legacy CurveFenceTool / CurveWallTool / MoveFenceTool /
MoveSlabTool / MoveCeilingTool ship more polish than the new ports do
right now:
- Legacy curve tools pre-snap the pointer position to the 0.5m grid
BEFORE projecting onto the chord normal, and they Shift-toggle to
a free-place mode. The ports skipped both — finer math, but the
user-visible UX regressed (laggy because the cascade resolver fires
per move, history feels broken near the no-op threshold).
- Legacy whole-item moves use scene.update per tick which keeps
hosted children visually aligned. The live-drag mesh.position port
cleared the offset before the GeometrySystem could rebuild,
producing a one-frame teleport on commit.
Drop the affordance registrations for those tools — the ToolManager /
MoveTool dispatch falls back to the legacy per-kind tools when the
registry doesn't declare the affordance. Stage D progress preserved
for: fence move-endpoint (linked cascade + alt-detach), slab/ceiling
boundary + hole editors, fence placement, slab placement, ceiling
placement.
The kind-owned files (curve-tool.tsx, move-tool.tsx, actions/curve.ts,
actions/move.ts) stay on disk for the next iteration — when they
reach parity with legacy UX, re-add the affordance entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d1231b4901 |
Phase 5 Stage D moves: live-drag for slab/ceiling + cursor follows pointer
User-reported regressions after the previous Stage D move ports: 1. Slab/ceiling moves were sluggish because the actions wrote scene.update(polygon) every grid:move tick → React re-render + CSG-with-holes geometry rebuild per frame. Apply the live-drag exception (same recipe fence move already used): visual translation via `sceneRegistry.nodes.get(id).position` + `useLiveTransforms`; scene.polygon is only written on commit. Polygon center precomputed in ctx, mesh-offset clears on commit/cancel. 2. Cursor sphere sat at the polygon center (offset from the user's actual cursor by `originalCenter - first_cursor`). Move wrappers now subscribe to `grid:move` and set `cursorRef.current.position` directly — no React state, no per-tick reconcile. Cursor lands on the user's pointer. 3. Fence move had the same React-reconcile-per-tick cost via its `useLiveTransforms` subscription. Switched to the same direct ref-mutation pattern. Adds a "REAL bend" test pinning that one Ctrl-Z after a real curve drag undoes only the bend, not the create. The previously reported "first undo does nothing" outcome reproduces only for no-op bends (drags within `normalizeWallCurveOffset`'s straight-snap threshold, ≈1.5cm on a 3m fence). For visible bends the dance pushes a real pastState entry and one undo step rolls back the bend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c2c2f66427 |
Phase 5 Stage D: fix bend history + move-tool loop + grid-snap sfx
Three user-reported regressions, all in the Stage D move/curve ports: 1. **Fence/wall bend cancelled the fence creation on Ctrl-Z.** The action.commit's `return false` shortcut on "no offset change" bypassed the dance entirely — pastStates wasn't touched, so the next Ctrl-Z fell through to whatever preceded activation (typically the create step). Fix: always run the dance, even on no-op commits. First Ctrl-Z then absorbs a silent no-op entry, subsequent presses roll back real prior actions. Same fix applied to fence move-endpoint, fence move, slab move, ceiling move. 2. **Slab/ceiling move 'maximum update depth exceeded' loop.** The `useScene` selector in `SlabMoveTool` returned a freshly-allocated `[sx, sz]` tuple on every call. Zustand's `Object.is` equality failed each comparison → re-subscribe → re-render → loop. Fix: subscribe to the stable live-node reference and derive the center via `useMemo`. Same recipe for fence/ceiling move-tools, including memoizing the `originalCenter` fallback that was getting a new array per render. 3. **No grid-snap sfx during move drag.** Action `preview` now tracks the last snapped pointer on a mutable `lastSnapped` ctx field and emits `sfx:grid-snap` when it changes between ticks. Matches the legacy MoveFenceTool's per-tick sound. Locking test for foot-gun 1 lives in `packages/core/src/services/single-undo-dance.test.ts`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d43cd569d6 |
drag-session: tests pinning single-undo-dance behavior
Includes a regression test for the "no-op bend" case that reproduces the user-reported 'fence bend cancels fence creation' bug: when action.commit returns false (draft === original), no pastState push happens during the drag's commit path. The very next Ctrl-Z falls through to whatever was on the stack before activation — typically the fence creation itself. The actual dance with real changes (delta !=0) works correctly — the test exercising the full createDragSession flow with action.commit returning true pins one undo step covering only the drag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7f2f9c685b |
Phase 5 Stage D wall: port CurveWallTool to DragAction (first wall D)
Direct copy of the fence curve recipe — pure `curveWallDragAction` (chord-perpendicular projection + clamp + normalize + single-undo dance) plus a thin wrapper feeding `useDragAction`. Mounted via `def.affordanceTools.curve`. Slight precision difference vs legacy: the legacy CurveWallTool snapped the pointer position to `getWallGridStep()` before projecting onto the chord normal; the ported action skips that pre-snap and relies on `normalizeWallCurveOffset` to settle the final value. The user-visible result is the same magnitude of step, just with the snap applied at the offset level instead of the position level. Remaining wall D affordances (endpoint move, whole-wall move, placement) are larger and queued for future sessions — wall's move tool alone is 804 LoC with the linked-wall corner-cascade logic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
de3efa1b53 |
Phase 5 Stage D ceiling: port placement + move + boundary/hole editors
Replicates the slab Stage D recipe for ceiling: four affordances routed through the registry — def.tool for the placement flow, def.affordanceTools for boundary edit / hole edit / whole-ceiling move. Ceiling-specific bits preserved: - Placement tool keeps the dual-cursor + vertical TSL-gradient connector + ground-shadow lines (1:1 with legacy). - Move tool wrapper renders the translucent preview fill + outline overlay so the user sees the destination before clicking. ToolManager mount sites for CeilingBoundaryEditor / CeilingHoleEditor now route through `getRegistryAffordanceTool` with legacy fallback. Per-kind progress: ceiling A ✅ B (n/a, def.renderer escape hatch preserved) C ✅ D ✅; E + F pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b2e5d84986 |
Phase 5 Stage D slab: port placement + move + boundary/hole editors
Replicates the fence Stage D recipe for slab — three drag affordances
+ one placement tool, all routed through the registry:
affordanceTools:
'boundary-edit' → thin <PolygonEditor> wrapper (vertex/edge drag)
'hole-edit' → same for a single hole polygon
move → DragAction with single-undo dance
tool: () => placement (multi-click polygon with axis/45° snap)
`PolygonEditor` + `PolygonEditorProps` exported from
`@pascal-app/editor` as Stage D transitional surface (Stage F cleanup
moves them into `@pascal-app/nodes`).
ToolManager mount sites for SlabBoundaryEditor / SlabHoleEditor now
route through `getRegistryAffordanceTool` with legacy fallback.
Slab move action does not use the live-drag exception (polygon CSG
rebuild every tick) — matches legacy behavior; optimization is a
separate task once we measure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7b5a1c607a |
Phase 5 Stage D fence: port FenceTool placement to def.tool
Fourth and final Stage D fence affordance — the placement tool itself.
Unlike the drag affordances (curve / move / endpoint), placement is a
two-click flow with state across grid events, not a single drag-down →
drag-up lifecycle. `DragAction` doesn't fit; the component owns its
own emitter subscriptions directly. The kind exposes it via
`def.tool: () => import('./tool')` and ToolManager's existing
`getRegistryTool()` lookup mounts it (legacy `tools.structure.fence =
FenceTool` falls through when the registry entry is missing).
Adds transitional exports from `@pascal-app/editor` for the helpers
the kind-owned tool needs at module scope: `createFenceOnCurrentLevel`,
`markToolCancelConsumed`, `EDITOR_LAYER`. Stage F cleanup moves these
into `@pascal-app/nodes` once every consumer is registry-driven.
Fence Stage D is now complete. Per-kind progress: A ✅ B ✅ C ✅ D ✅
(four affordances ported — curve, move-endpoint, move, placement).
E (drop legacy panel) and F (cleanup) pending across all kinds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c16878c876 |
Phase 5 Stage D fence: port MoveFenceTool to DragAction (whole-fence move)
Third Stage D affordance port (302 LoC legacy → action + thin wrapper). The action (`actions/move.ts`) uses the live-drag exception documented in editor/wiki/architecture/tools.md: visual-only updates via `sceneRegistry.nodes.get(id).position` + `useLiveTransforms` during the drag, no scene mutations. Avoids re-rebuilding the fence geometry (many posts + infill panels) every pointer tick. Commit performs the single-undo dance — writes final start/end to scene, geometry rebuilds once, Ctrl-Z reverses the whole drag. Linked-fence cascade follows the same shape as MoveFenceEndpoint — any fence in the same parent that shared an endpoint at activation moves with the drag. `getRegistryAffordanceTool` extracted to `tools/shared/affordance-dispatch.ts` so move-tool.tsx and tool-manager.tsx share the lazy-load helper (no duplicate caches). MoveTool dispatch gains a generic `affordanceTools.move` check after the capability-driven `movable` shortcut — fence routes through here; wall / slab / etc. fall through to their legacy per-kind chain until their D ports land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
36c48b7fc9 |
chore: biome auto-format pass (resolve persistent dirty-tree noise)
These files had been showing as modified in every dev session — biome's canonical formatting (line-length collapses, import sort, type-modifier placement) didn't match the committed state. No semantic changes. Committing now so the working tree stays clean across sessions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5e67ffd4e8 |
fence curve: single-undo dance in commit — fix undo skipping past drag
The previous commit() just returned true, relying on createDragSession.terminate() to resumeHistory. But that alone never captures the drag in zundo's pastStates — the pause window's mutations are skipped entirely. After commit, Ctrl-Z jumped past the curve *and* past the prior fence creation. Adds the same dance now used by move-endpoint: restoreAll → resumeHistory → re-apply the final draft. Zundo records one undo step for the whole drag. cancel() becomes a no-op (orchestrator's restoreAll covers it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
05efa25edb |
Phase 5 Stage D fence: port MoveFenceEndpointTool to DragAction
Second Stage D affordance port (425 LoC legacy → ~430 split across
action + wrapper). All math (snap, linked-fence cascade, alt-detach,
min-length gate, single-undo dance) lives in the pure
`moveFenceEndpointDragAction`. The React wrapper handles the UI
overlays (cursor sphere, Drag/Detach badge, angle label) and the live
state subscriptions.
The action introduces the single-undo dance pattern for multi-write
commits: `commit()` calls `scene.restoreAll()` → `resumeHistory()` →
re-applies the final draft so zundo records the entire drag as one
undo step. Reusable shape for slab/wall/door endpoint ports.
Transitional exports added to `@pascal-app/editor`'s public surface
(`snapFenceDraftPoint`, `isWallLongEnough`, the segment-angle helpers,
`MovingFenceEndpoint`). Stage F cleanup moves these into
`@pascal-app/nodes` once every consumer is registry-driven.
`getRegistryAffordanceTool` is now generic (`ComponentType<any>`) so
affordances with different prop shapes (`{ node }`, `{ target }`, …)
all dispatch through the same helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ee309ece6f |
drag-session: dispose() is now silent — does not fire onCancel
The previous dispose() was documented as "equivalent to cancel()" and fired onCancel. That breaks React StrictMode's mount → cleanup → mount cycle for useDragAction consumers: the first cleanup's onCancel resets the parent state machine (e.g. setCurvingFence(null)), which unmounts the component before the second mount runs. Net result: the tool blinks in and out instantly. dispose() now restores scene state + resumes history but skips onCancel. Explicit cancel() still fires onCancel (Esc / external aborts). New test locks this in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1082b62552 |
useDragAction: activation-click grace + curve fence commit sfx
The legacy CurveFenceTool ignored grid:click for 150ms after mount — otherwise the very click that activates a tool (e.g. the floating menu "curve" button) cascades through the R3F drei <Html> portal into the grid, fires grid:click on the just-mounted tool, and commits the drag before any preview move runs. The new useDragAction was missing this guard, so the Stage D fence curve port "click → place sfx → exit" without ever letting the user adjust. Adds `activationGraceMs` (default 150) on useDragAction; ports the sfx:item-place commit emission into FenceCurveTool so the kind-owned tool matches legacy UX. Same guard will cover the upcoming Stage D ports (endpoint move, whole-fence move, placement, plus slab/ceiling/wall D). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8ca9686b27 |
Phase 5 Stage D: fence curve affordance → registry DragAction
First Stage D port — `CurveFenceTool` (178 LoC legacy) split into a pure `DragAction` primitive (`packages/nodes/src/fence/actions/curve.ts`) plus a thin React wrapper (`packages/nodes/src/fence/curve-tool.tsx`) that feeds it through `useDragAction`. The kind declares the affordance via `def.affordanceTools.curve`; ToolManager lazy-loads it at runtime when `useEditor.curvingFence` activates. Falls back to the legacy `CurveFenceTool` for any kind that hasn't been ported. The lazy-load dispatch dodges the editor→nodes circular dep (nodes already depends on editor for `useDragAction` + `CursorSphere`). Establishes the pattern for the remaining fence affordances (`MoveFenceEndpoint`, `MoveFence`, placement) and for slab/ceiling/wall D ports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
95645d8e37 |
SelectionManager: route furnish-category registry kinds through furnish phase
Shelf clicks in 3D weren't selecting: getSelectionTarget routed shelf (category='furnish') to the furnish phase, but furnish.isValid hard- coded `node.type !== 'item'` and rejected shelf. Click switched phase, nothing selected. Fix: extend furnish.isValid to also accept registry-driven kinds whose def.category === 'furnish' AND def.capabilities.selectable. Item's asset.category door/window special-case stays first. Future furnish-category kinds (tables, lamps, etc.) are selectable in furnish phase without further changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9aec943740 |
Phase 5 Stage C: item floor plan — all 9 kinds now at Stage C
Item is the last Stage A-only kind to gain def.floorplan. Closes Stage C
across every registered kind.
Files added:
- nodes/src/item/floorplan.ts: buildItemFloorplan inlines a self-
contained parent-chain transform walker using `ctx.resolve`. Mirrors
the legacy `getItemFloorplanTransform` math from editor/lib/floorplan/
items.ts:
* Wall parent: rotate item.position by wall's angle, anchor at
wall.start, handle wall-side attachTo via wall.thickness offset.
* Item parent (nested): recurse for parent's transform.
* Level / slab / ceiling parent: item.position is level-local.
Returns a rotated width × depth rectangle. asset.floorPlanUrl image
overlay deferred for Phase 5 follow-up.
Files changed:
- nodes/src/item/definition.ts: wires `floorplan: buildItemFloorplan`.
- floorplan-panel.tsx: floorplanItemEntries useMemo short-circuits to
[] when nodeRegistry.has('item'). Phase 6 deletes the entire useMemo.
Stage C coverage (all 9 registered kinds):
shelf ✅ spawn ✅ fence ✅ slab ✅ ceiling ✅ wall ✅ door ✅ window ✅ item ✅
Next sessions: Stage B for door / window / wall (each large geometry
extraction), Stage D per kind (DragAction affordance ports), Stage E
(drop legacy panels), Phase 6 Stage F cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9bcb25d0aa |
Phase 5 Stage C continued: wall, door, window now in registry floor plan
Three remaining kinds at Stage C this session: wall → C - buildWallFloorplan: uses ctx.siblings to gather other walls in the level, runs calculateLevelMiters, computes plan footprint via getWallPlanFootprint. Same visual output as legacy. - getFloorplanWall thickness exaggeration inlined (~25 lines from editor/lib/floorplan/walls.ts) to keep nodes/wall self-contained. - floorplan-panel.tsx's wallPolygons short-circuits to [] when wall is registered. - Performance note: recomputes level miter data per wall (O(N²) for N walls in a level). Acceptable for typical scenes; ctx.levelData?. miters optimization deferred to Stage B's wall design pass. door → C - buildDoorFloorplan: inlines getOpeningFootprint math from floorplan-panel.tsx (40 lines, pure math). Uses ctx.parent as the wall to compute direction + perpendicular for the cutout footprint. - Returns null when parent isn't a wall (orphaned doors during placement). window → C - buildWindowFloorplan: same shape as door, glass-blue tint to distinguish visually. Both share the legacy openingsPolygons gating: - floorplan-panel.tsx's openingsPolygons useMemo filters per kind so a partial migration still works (e.g., if only door registers, only doors get skipped). When both registered, returns [] entirely. Item C intentionally deferred — needs parent-chain transform helpers (buildFloorplanItemEntry / getItemFloorplanTransform from editor/lib/ floorplan/items.ts) exposed publicly or moved into core. A focused session is the right place to design that boundary. Stage B for door / window / wall still pending — each is a focused session per kind (large geometry math extractions, wall needs ctx. levelData design). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
969b154b08 |
Phase 5 depth-first: spawn C, fence B+C, slab B+C, ceiling C
Depth-first session: drive registered kinds through Stage B (pure
def.geometry, drop system re-export) and Stage C (def.floorplan,
short-circuit legacy inline rendering in floorplan-panel.tsx).
spawn → C
- buildSpawnFloorplan wired on definition (was written but deferred
to avoid double-render).
- floorplan-panel.tsx's floorplanSpawnEntries useMemo short-circuits
to [] when nodeRegistry.has('spawn').
fence → B
- generateFenceGeometry exported from viewer; buildFenceGeometry
wraps it in a Group+Mesh with DEFAULT_STAIR_MATERIAL.
- def.geometry set; renderer + system fields dropped.
- Deleted nodes/src/fence/{renderer.tsx,system.tsx}.
fence → C
- buildFenceFloorplan: polyline along centerline (sampled for curved
fences via sampleWallCenterline from core). Stroke width = node.thickness.
- floorplan-panel.tsx's floorplanFenceEntries short-circuits.
slab → B
- generateSlabGeometry exported from viewer; buildSlabGeometry wraps
it in a Group+Mesh + cached material (preset / custom / default
pattern preserved from legacy renderer).
- def.geometry set; renderer + system fields dropped.
- Deleted nodes/src/slab/{renderer.tsx,system.tsx}.
slab → C
- buildSlabFloorplan: SVG path with outer polygon + hole subpaths
(uses getRenderableSlabPolygon from core for wall-clipping parity).
- floorplan-panel.tsx's slabPolygons short-circuits.
ceiling → B INTENTIONALLY SKIPPED
- Ceiling renderer renders React children (hosted items) + uses TSL
shader materials + named meshes that other systems poke
(getObjectByName('ceiling-grid')). Pure def.geometry can't preserve
that. Ceiling keeps def.renderer (the custom escape hatch) — same
pattern item uses. Documented in ceiling/definition.ts.
ceiling → C
- buildCeilingFloorplan: dashed-outline path with hole subpaths
(visually distinct from slab since ceilings are above).
- floorplan-panel.tsx's ceilingPolygons short-circuits.
Per-kind progress after this session:
- shelf: B ✅ C ✅ (Stage E since brand-new)
- spawn: A ✅ C ✅
- wall: A ✅ (B blocked on ctx.levelData design)
- fence: A ✅ B ✅ C ✅
- slab: A ✅ B ✅ C ✅
- ceiling: A ✅ C ✅ (B intentionally not applicable)
- door / window / item: A ✅ (B+C pending in future sessions)
Known test issue: `bun test` in packages/nodes fails to load
`three-bvh-csg` through the viewer's transitive imports (UMD/ESM
mismatch in Bun's test runner). The Next.js editor build works fine
because it bundles differently. Fix requires either dynamic imports
(breaks sync def.geometry contract) or test env config — deferred.
Other tests (schema, geometry, parity) pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
6a4de8cff5 |
Drop wall + fence feature flags: register unconditionally; remove verification logs
Parity comparison against deployed prod is now cleaner than juggling
env-var flag toggles locally. Both kinds enter builtinPlugin.nodes
unconditionally; the Phase 0 dispatch shims (<LegacySystem kind="X">
wrappers + NodeRenderer's registry-first branch) handle the cutover.
Files deleted:
- packages/nodes/src/wall/feature-flag.ts
- packages/nodes/src/fence/feature-flag.ts
Files changed:
- packages/nodes/src/index.ts: drops isWallRegistryEnabled /
isFenceRegistryEnabled gates; wallDefinition + fenceDefinition
land directly in builtinPlugin.nodes.
- packages/nodes/src/{wall,fence}/index.ts: drop the flag re-export.
- packages/nodes/src/{wall,fence}/renderer.tsx: drop the one-shot
verification console.info. Same for the system.tsx wrappers.
- packages/viewer/src/components/renderers/{wall,fence}/{wall,fence}-
renderer.tsx: drop the paired [X:legacy] verification logs (no
longer comparing flag-toggled paths).
Net DX: no env var to remember when starting `bun dev:community`. To
A/B test, compare against editor.pascal.app deployed prod.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9883f1cdc1 |
Phase 5 first batch kind: fence migrates to registry behind feature flag
Same shape as wall milestone B — thin renderer + system re-export, no geometry / floor-plan / tool ports yet (later milestones). Feature flag NEXT_PUBLIC_USE_REGISTRY_FOR_FENCE gates the dispatch flip. Files added (packages/nodes/src/fence/): - schema.ts: re-exports FenceNode from core. - parametrics.ts: dimensions / posts / style fields for the auto- inspector. Endpoints + curveOffset edited via tools, not in parametrics. - feature-flag.ts: mirrors the wall flag pattern. - definition.ts: capabilities (snappable + surfaces sides + selectable + duplicable + deletable), relations (linkedBy endpoint-match, no hosts, no affectsSpatial — matches legacy), parametrics, renderer, system, toolHints (Left click / Shift / Esc — fence has no helper file today so this adds a panel where there wasn't one). Tool field absent: fence has 4 tools (build, curve, move, move-endpoint) wired through editor state, not the registry dispatch — they keep running unchanged. - renderer.tsx: thin placeholder mesh + markDirty on mount + node events + DEFAULT_STAIR_MATERIAL (matches legacy material reuse). Verification log fires once on first mount. - system.tsx: re-exports the legacy FenceSystem from viewer. Verification log on mount/unmount confirms the bundle activates. - index.ts: barrel. Files changed: - packages/viewer/src/index.ts: new exports for FenceSystem and DEFAULT_STAIR_MATERIAL so the @pascal-app/nodes bundle can compose them without reaching into viewer internals. - packages/viewer/src/components/renderers/fence/fence-renderer.tsx: paired one-shot legacy verification log so the dispatch path is unambiguous from the browser console. - packages/nodes/src/index.ts: conditional fenceEntries appended to builtinPlugin.nodes based on isFenceRegistryEnabled. With the flag off (default), behavior is unchanged; with it on, Phase 0 shims switch fence to the registry path — legacy <FenceRenderer> and <LegacySystem kind="fence"><FenceSystem /></LegacySystem> short- circuit, the bundled system.tsx re-mounts FenceSystem via RegisteredSystems, and the new renderer takes over the dispatch. No behavior change with the flag off. With it on, behavior should be byte-identical (same FenceSystem code, same priority, same geometry path). Phase 5 batch order continues with slab / ceiling / door / window / item / etc. as flagged migrations after fence parity signs off. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
3f3818f3b0 |
Phase 4: generic GeometrySystem + ParametricNodeRenderer; shelf ports off renderer/system files
Lands the three-checkbox composition runtime documented in wiki/architecture/node-definitions.md. A kind with only a pure geometry function now needs zero per-kind React or system code. Type-side additions (packages/core/src/registry/types.ts): - New `GeometryContext` (resolve / children / siblings / parent) — read- only scene access for builders that reference other nodes by ID (wall miters, door cutouts). Most kinds ignore it. - New `geometry?: (node, ctx) => Object3D` field on NodeDefinition, independent of renderer/system. Three orthogonal opt-ins replace the v0 RendererSource union. - Re-exported via packages/core/src/registry/index.ts (consumed by nodes packages through `export * from './registry'`). Runtime (packages/viewer): - New <GeometrySystem> (systems/geometry/geometry-system.tsx) walks dirtyNodes, builds a GeometryContext per dirty node, calls def.geometry, disposes old children, attaches new ones, clearDirty. Frame priority 2 (matches the priority shelf's per-kind system had). Mounted in viewer/index.tsx alongside <RegisteredSystems>. - New <ParametricNodeRenderer> (components/renderers/parametric-node- renderer.tsx) — empty <group> + useRegistry + useNodeEvents + markDirty-on-mount + useLiveTransforms. Mounts hosted children via <NodeRenderer> recursively. The default renderer for any registered kind without a custom def.renderer. - <NodeRenderer> dispatch updated: custom renderer wins, else geometry-only kinds fall through to ParametricNodeRenderer, else null (legacy switch fallback). Documented inline. Shelf migration (proof of the boilerplate collapse): - Deleted nodes/src/shelf/renderer.tsx (was 45 lines of registry + handler boilerplate). - Deleted nodes/src/shelf/system.tsx (was 60 lines of dirty-loop + dispose plumbing). - shelfDefinition now: `geometry: buildShelfGeometry`. One line. buildShelfGeometry is the pure function from geometry.ts that already existed. End-to-end effect: registry-driven shelf now mounts via the framework's generic renderer + system. Parametric edits flow through the same dirty-driven rebuild path, but the kind ships ~100 fewer lines of boilerplate. Every future kind that fits the same shape (item, fence segment, column, etc. as they migrate in Phase 5) follows the same "one line, one pure function" pattern. Wall stays on its dedicated def.renderer + def.system — its mitering needs level-batch context (`ctx.levelData?.miters`, future extension) that the generic system doesn't yet provide. Decided at Phase 3+, not blocking Phase 4 acceptance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
60117e848b |
WallSystem: throttle adjacent-wall rebuild during drag
Endpoint drags fire markDirty(wallId) on every pointermove tick. The old behavior rebuilt the dragged wall AND every wall sharing a junction on every tick — in a 4-corner room with doors, that's 4× the CSG +miter pass per tick. Visible as drag lag. New behavior: the dragged wall rebuilds every tick (so the drag tracks the cursor with full fidelity, cutouts and all). Adjacent walls are queued in pendingAdjacentByLevel and rebuilt on the trailing edge — 80ms after the dirty stream stops. The corners snap into their correct miter joins ~80ms after release, which is the standard CAD-app "rubber-band the dragged element, fix neighbors on commit" pattern. Module-level singleton state for the queue + timestamp — WallSystem is mounted exactly once globally, so module state is the right scope. Expected speedup: - t-junction drag: ~3× (was 3 walls/tick, now 1) - 4-corner room with door per wall: ~4× The trailing flush condition (!hasDirtyWalls && now - lastWallDirtyAtMs >= DRAG_FLUSH_MS) means single edits (non-drag) pay an 80ms latency before neighbors miter correctly. Acceptable for now; the real fix is the affordance/tool port (Milestone C) which will explicitly signal "drag in progress" so we can drop the heuristic. Until then this is a substantial drag-perf win for zero risk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |