43 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f66dcd438b |
feat(mcp): natural-language measurements for tool inputs via @pascal-app/lingo
Measurement/angle parameters on the scene tools now accept a bare number OR a
natural-language string ("6 in", "180cm", "45°", "1.57rad"), canonicalized to
the unit the handler already expects (meters for length, radians/degrees for
angles) with min/max bounds and model-readable errors. Makes LLM tool calls
safer — "6 in" no longer has to be pre-converted to 0.1524, and a bad value is
rejected with a message the model can self-correct from.
A shared `measurement(kind, unit)` zod field wraps lingo's parseQuantity as a
`number | string` union+transform; numbers pass through unchanged (backward
compatible), so no handler changes were needed. Covers create_wall, cut_opening,
place_item, create_level, create_story_shell, create_roof,
create_stair_between_levels, create_room, add_door, add_window, photo_to_scene.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
bf25af6add
|
viewer: Fix Dutch roof trim artifacts (#452)
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * Fix spiral stair openings and fence handle arrows * Implement roof trim planes and ridge vent clipping * Fix mansard roof and ridge vent placement * Fix mansard merged roof cutouts * Fix Dutch roof gable overhang * Refactor roof segment, ridge vent, and surface geometry Remove Dutch ridge axis abstraction and rework roof edit system, ridge vent clipping geometry, and roof surface placement. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Simplify Dutch roof shape * Add Dutch roof gable top geometry controls * Fix Dutch roof slope material slots * Render dutch roof tops as double-sided faces * Add auto ridge vent toggle to roof segments Track ridge vent auto-generation via an `autoRidgeVent` metadata flag so geometry changes only regenerate default vents when enabled, treating legacy segments with generated vents as auto-enabled for back-compat. Expose a panel toggle to opt in/out per segment. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Snap new walls to the floor below Feed the walls of the level directly beneath the active one into the draft snap pipeline as extra references, so a new wall can align with the floor below. They share the same local XZ origin, and the list is kept separate from the current-level walls so the measurement HUD and wall splitting only act on the active level. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Set Dutch roof shape defaults on type switch Seed the Dutch shape parameters (waist width/height/length, top rake thickness/length) with sensible defaults whenever a segment is created as or switched to Dutch, so the gablet is well-formed regardless of leftover values from the previous roof type. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use green accent for corner and endpoint snap markers Color the corner/endpoint snap markers and the vertical cursor pillar green across the 2D floorplan beacon, the 3D alignment guide dots, and the wall snap beacon so snap targets read as a consistent accent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add magnetic wall snapping to the roof tool Snap roof draft corners onto wall corners, midpoints, crossings, and bodies on the active level and the floor below, reusing the wall tool's snap pipeline so the beacon and coloring match. The cursor's ground dot/ring is hidden while a wall snap is active to avoid overlapping the beacon glyph. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Update auto-generated Next.js route types path Regenerated next-env.d.ts now references ./.next/dev/types/routes.d.ts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Show cutaway outline while dragging roof trim Slice an untrimmed segment volume generated from the live node instead of the registry mesh, whose CSG rebuild lags a few frames behind the drag and may still hold placeholder geometry — so the section outline now renders deterministically. Use LineBasicNodeMaterial so the outline draws under the WebGPU pipeline, and export generateRoofSegmentGeometry for the slice source. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fill and clip roof trim cutaway, gate it to active drag Add a violet silhouette fill behind the cutaway outline, extend the section slicing to angled diagonal/corner trims via a generic vertical cut plane, and clip each slice to its footprint span so the infinite plane no longer sprouts stray lines across the rest of the roof. The cutaway now renders only while a trim handle is being dragged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Separate and extend Dutch roof end slopes Pull the Dutch hip end slopes out of the watertight shingle shell into their own slab wedge so they can be reshaped independently, and extend each end slope inward up its own hip plane until the top edge meets the gablet's inner triangle. Refactor roof-segment shape geometry into a shared roof-segment-shape module. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Render roof trim cutaway as a material-only section cut Replace the triangle-mesh slicer with a CSG intersection of a thin slab against the untrimmed roof shell, so the cutaway shows red only on real material (wall + deck bands) and leaves the hollow attic empty. Add an analytic surface-edge outline, style both solid red like a SketchUp section, and make the cutaway persist whenever a segment is trimmed. Keep the merged roof shell visible during trim editing (re-trimmed live from each segment's drag override) instead of swapping in the per-segment meshes, whose abutting end-cap faces showed as stray white planes the commit never had. Extend each slab past free cut-line ends only — trimmed ends clamp to the cut line — so the red section stays inside the trim box. Re-export INTERSECTION from the viewer CSG surface for the editor. * Outline roof cutaway by fill silhouette, restyle to destructive red Derive the section-cut outline from the fill geometry's edges (EdgesGeometry) so it traces the real cut shape — wall/deck band boundaries and the hollow-attic edge — instead of just the top surface line. Drop the fill to 85% opacity and recolor both fill and outline to the app's destructive red, matching the delete/destructive UI. * Include roof accessories in trim clipping and red cutaway Roof accessories (chimney, vents, skylight, dormer, gutter, downspout, solar-panel, cupola) now slice at the trim plane like the roof shell and appear in the red section-cut while dragging a trim handle: - Export clipGeometryBySegmentTrim from the viewer as a reusable segment-local trim-clip primitive. - Add a shared useSegmentTrimClippedGeometry hook + TrimClippedMesh wrapper (nodes) that slice accessory geometry by the host segment's live trim override, so the cut tracks the drag. - Wire the clip into all 11 accessory renderers, including skylight glass panes and dormer window glass/frame/sill. - Feed every hosted accessory mesh into the editor's red cutaway, welding triangle-soup geometry (e.g. ridge vent) so CSG INTERSECTION yields a cross-section. - Register skylight in the scene-graph tree-node map so it shows in the outliner when placed on a roof. Co-Authored-By: Claude <noreply@anthropic.com> * Add smooth spline fences with editable curve handles Fences can now be drawn as one continuous Catmull-Rom/Bezier curve via an optional `path` (+ per-point `tangents`), selectable in a Straight/Curved mode toggle. Selected spline fences expose draggable control-point dots (hexagon) and symmetric tangent handles (circle) joined by a violet line, editable in both 2D plan and 3D. Side-move arrows are dropped for splines. Co-Authored-By: Claude <noreply@anthropic.com> * Fix dutch roof ridge vent handling * Fix Dutch ridge vent placement and support * Fix Dutch roof trim artifacts * Fix Dutch roof trim preview geometry * Tag roof trim overlay meshes with EDITOR_LAYER Child meshes relied on a parent group's layer, which three.js does not propagate, so the trim section/rail/plane overlays rendered on the scene layer — getting inked/SSGI-darkened and leaking into thumbnail exports. Co-Authored-By: Claude <noreply@anthropic.com> * Apply Biome cleanup * fix(core): address Dutch roof review feedback * chore: apply biome check cleanup * fix(core): relax Dutch roof surface helper input * fix * Fix biome checks and dev verification * fixes * Remove unsupported Biome noShadow override * Improve roof interactions and fence editing * Fix fence drag and ridge vent default handling * editor: drop wall-snap debug log, gate curved-fence finish hint on draft start Remove the leftover TEMP DIAGNOSTIC console.log in the wall tool's onMove hot path. Curved fences commit on a closing gesture (double-click / Enter) rather than per-click, so surface a 'Finish curve' hint in the fence HUD — but only once a point has been placed and a curve is actually in flight. The draft point count is published from SplineFenceDraft into a small ephemeral editor store (useFenceCurveDraft) that the contextual helper reads, mirroring the existing useSegmentDraftChain pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Wassim SAMAD <wass08@gmail.com> |
||
|
|
1b4c656901 |
chore(ts7): adopt the TypeScript 7 native compiler (tsgo)
Make the editor packages compatible with, and benefit from, the native Go
compiler (TypeScript 7 / tsgo) while keeping the npm publish path on stable tsc.
- Add @typescript/native-preview (pinned 7.0.0-dev.20260624.1). tsgo coexists
with typescript@6, which Next typegen and the IDE plugin still need until the
TS 7.1 programmatic API ships.
- Fix two react-three-fiber JSX augmentations the native checker rejects but
tsc tolerated:
- viewer: map only LineBasicNodeMaterial (the one webgpu node material used as
a JSX tag) instead of the whole three/webgpu namespace (TS2320/TS2590).
- plane-box-select-tool: drop the redundant `IntrinsicElements extends
ThreeElements` block — it duplicated R3F's global augmentation and, by
referencing @react-three/fiber's ThreeElements directly, hit a bun peer-dep
variant-directory duplicate under tsgo (TS2320). r3f.d.ts already covers
those JSX intrinsics. Types-only — no runtime change.
- check-types -> tsgo --noEmit (editor, @repo/ui, editor app, ifc-converter app)
- emit-package dev watch -> tsgo --build --watch
- build/publish stay tsc --build (prepublishOnly + release.yml unchanged;
emitted .d.ts is byte-identical to tsc output)
- bump next 16.2.6 -> 16.2.9
tsc remains the source of truth and fallback. Published artifacts unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ab3c256804 | release: @pascal-app/core@0.9.1 @pascal-app/viewer@0.9.1 @pascal-app/editor@0.9.1 @pascal-app/mcp@0.3.1 @pascal-app/ifc-converter@0.1.1 | ||
|
|
9bfd75c2c7 |
chore(release): align core/viewer/editor to 0.9.0 ahead of v0.9.1
The v0.9.0 GitHub release declared the trio at 0.9.0 but the npm publish never ran (npm still serves 0.8.0). Aligning manifests so the release workflow's bump=patch lands everything at the announced-next 0.9.1 (mcp 0.3.0 -> 0.3.1, ifc-converter 0.1.0 -> 0.1.1 keep their own lines, matching the per-package version table style of previous releases). Inter-package peer/dev refs move to ^0.9.0 so the workspace stays self-linked during the release run; the workflow re-syncs them to the final versions after bumping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9d24e260c3 |
chore(deps): June 2026 dependency refresh
Range edits: - next 16.2.1 -> 16.2.7 in apps/editor + apps/ifc-converter. 16.2.5/16.2.6 fixed six high-severity advisories (middleware bypass, DoS, SSRF, XSS, RSC cache poisoning); 16.2.7 is bugfix backports. No API/config changes. - typescript 6.0.2 -> 6.0.3 everywhere, including aligning the 5.9.3 stragglers (@pascal-app/mcp, @repo/ui, @repo/eslint-config) that date from the original scaffold and were never intentionally held back. - @types/node in @pascal-app/mcp: ^25.5.0 -> ^22.19.20 — mcp was the only workspace typed against non-LTS node 25; 22 matches the release workflow's node and every other workspace. - @number-flow/react ^0.5.14 -> ^0.6.0 (packages/editor, apps/editor); the only breaking change (removed --number-flow-char-height CSS var) is unused here. - agentation ^2.3.2 -> ^3.0.2 (apps/editor devtool; v3 is a drop-in for the props-less <Agentation /> usage, additions are opt-in). - Root overrides: @types/react 19.2.14 -> 19.2.17, @types/three 0.184.0 -> 0.184.1 (types-only fixes). Lockfile refresh within existing ranges picks up react 19.2.7 (pairs with next 16.2.7 — 19.2.6 had a server-action FormData regression), motion 12.40.0, three-mesh-bvh 0.9.10, @react-three/uikit-lucide 1.0.73, the June radix wave, lucide-react 1.17.0, zustand 5.0.14, tailwind-merge 3.6.0, geist 1.7.2 (fixes Geist Mono ligature regression), react-grab 0.1.44, biome 2.4.16 + ultracite 7.8.2, turbo 2.9.17. Held: three stays 0.184.0 (npm latest; single-instance constraint), tailwindcss 4.3.0 + lightningcss 1.32.0 already match the pinned optionalDependencies native binaries, eslint 10 major not taken (@repo/ui is unconsumed legacy scaffold). Verified: turbo build, check-types, biome check, and 931 package tests green; bun.lock stays lockfileVersion 1 (CI bun 1.3.0 compatible); single next/three resolution confirmed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ee98c55b65
|
Polish: window preset preview + smooth shelf item placement + Shelf build tool (#367)
* fix(nodes,viewer): window preset live wall preview + shaped-cutout move tracking MoveWindowTool marked the moving window `isTransient` unconditionally, but WindowSystem only rebuilds the host wall's cutout for non-transient windows — so a window *preset* (isNew) showed no live hole on the wall and couldn't be placed consecutively without leaving/re-entering. Guard the transient mark on `!isNew`, matching MoveDoorTool. Separately, shaped openings (arch / rounded / `opening`) rebuild their cutout brush from `node.position`, which a same-wall move doesn't write (it mutates the mesh directly and publishes to `useLiveTransforms`). The wall-system's `getEffectiveNode` only merges `useLiveNodeOverrides` (resize arrows), so shaped cutouts lagged the move while rectangular ones (rebuilt from the live mesh matrixWorld) tracked. Fold `useLiveTransforms` into door/window children before collecting cutouts so shaped holes follow the live move too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(editor,viewer,nodes): smooth item placement on shelves Three issues made hosting an item on a shelf janky (item-on-item was already smooth because plain items have no `def.geometry` to rebuild): - Jitter: the draft mesh intercepted the cursor ray, so the shelf-row hit was re-derived from the moving item each frame. Disable raycasting on the draft during placement (incl. async GLB children, reconciled per frame) so the ray passes through to the surface beneath — mirrors MoveRegistryNodeTool. - Reparent/vanish at the edges: `onShelfLeave` flipped state to floor without reparenting the draft off the shelf, so the floor strategy's level-local position rendered compounded with the shelf transform. The grid handler now owns the shelf→floor transition. - In/out oscillation: reparenting the draft onto the shelf dirtied the shelf, and GeometrySystem disposed+rebuilt its boards, making r3f fire a spurious shelf:leave→enter that thrashed placement between the row and the floor. Add an opt-in `def.geometryKey` so GeometrySystem skips the rebuild when geometry inputs are unchanged (shelf boards don't depend on hosted children). Keep the item sticky by testing the cursor ray against the shelf's bounding box: a ray that slips through a gap and lands on the floor behind the shelf still counts as "on the shelf"; only a ray that misses the shelf detaches to the floor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(editor-app): add Shelf to the standalone editor Build tab The shelf kind is fully wired (def.tool, presentation icon, StructureTool id) but was absent from the standalone editor's Build palette. Add it between Column and Spawn Point. Community has its own build-tab and is left untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(mcp): biome-format coordinate-conventions-demo.json The committed example violated Biome formatting (expanded polygon arrays), which broke the `mcp-ci` Biome check on main (#356) and every branch since. Format it so CI is green. Pure formatting — no content change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c986ced14d
|
docs(mcp): document plan↔world coordinate convention (#356)
Adds a Coordinate conventions section to packages/mcp/README.md and a 71-node MCP demo scene that exercises every claim it makes. Closes #337. Covers: right-handed scene with X/Z ground plane and Y up; metres for lengths and radians for rotations as Euler [x, y, z]; the [x, z] → (x, y, z) plan-to-world mapping with no sign flip in the stored contract; level/building-local framing (world only under identity transform); rotation-not-reflection caveat for the 2-D plan panel and the iso-default top-down azimuth offset; a worked 30° rotated slab example; and the wall-local-metres trap for door/window/place_item coordinates. Companion: examples/coordinate-conventions-demo.{json,md} with a reference compass at the origin and Demos A/B/C/D illustrating axis-aligned baseline, the rotated example, and the page-intent vs world-result L pair. Co-authored-by: Marcel Gruber <marcel@grubertech.com> |
||
|
|
a984410b3f | release: add ifc-converter to release workflow; bump @pascal-app/mcp to 0.3.0 | ||
|
|
87384cfbab
|
feat(roof-system): six roof-accessory kinds (chimney, dormer, skylight, solar-panel, ridge-vent, box-vent) on the registry model (#330)
* Add roof surface placement support for items
Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.
- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fixed conflict
* feat(box-vent): port to packages/nodes registry shape
Move box-vent from the legacy scattered layout (core schema +
viewer/systems/renderers + editor/tools/panels/sidebar) into a single
`packages/nodes/src/box-vent/` folder following the Phase 5 Stage E
pattern. The kind now self-registers via the built-in plugin.
- schema lives in `core/schema/nodes/box-vent.ts` (referenced by the
hand-maintained AnyNode union) and re-exports from the kind folder.
- `def.renderer` reads the parent roof-segment from useScene, applies
the slope tilt + segment yaw + node rotation stack, and follows the
segment's useLiveTransforms override during a parent drag.
- geometry builder is pure and shared by renderer / preview / tool /
unit tests. `computeBoxVentSlopeTilt` is lifted as a helper for
future reuse by other roof-mounted kinds (skylight / solar-panel).
- placement tool listens to `roof:*` events, snaps to the segment
under the cursor, creates a new BoxVentNode parented to that
segment.
- BoxVentEvent + `NodeEvents<'box-vent', BoxVentEvent>` added to the
event bus so `useNodeEvents(node, 'box-vent')` type-checks.
Verified: workspace `bun run build` + `bun run check-types` pass; 13
new unit tests in `__tests__/{schema,geometry}.test.ts` pass.
Worked example for porting the remaining roof-system kinds (ridge-vent,
chimney, solar-panel, skylight, dormer) — see `.claude/PORT-CHEATSHEET.md`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ridge-vent): port to packages/nodes registry shape
Same pattern as box-vent (`752ace83`): one folder under
`packages/nodes/src/ridge-vent/`, schema in core, registration via the
built-in plugin. No outside-the-folder edits beyond core schema/types,
the event bus, and the plugin index.
- pure geometry builder shared by renderer / preview / tool / tests,
covering all three styles (curved cap / shingled / metal) and the
optional end caps.
- custom `def.renderer` reads the parent roof-segment, follows
useLiveTransforms during a parent drag. No slope tilt — the ridge
IS the high line of the segment so the transform stack is one
level shallower than box-vent.
- placement tool snaps the cursor to the ridge (segment-local Z=0)
wherever the cursor lands on a segment, then commits on click with
Z=0 baked into the new node's position.
- RidgeVentEvent + NodeEvents<'ridge-vent', ...> added to the event bus.
Verified: workspace build green, 9 new tests pass alongside the
13 box-vent tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chimney): port to packages/nodes registry shape (Option C)
Same shape as box-vent (`752ace83`) and ridge-vent (`10d489d6`).
**Scope — Option C.** Chimney lands in the registry with solid
geometry; the CSG-driven decoration (cap flue holes, body cavity,
panels, bands, and the roof-trim that hides the chimney bottom inside
the deck) is preserved in the schema but NOT rendered yet. These
re-light when roof-segment migrates to Stage B and introduces a
`roofCutout` capability the parent segment can read.
Visual consequence: a placed chimney intersects the roof at the deck
line instead of having a clean CSG-cut hole around it. Placement,
move (via the legacy floating-vent-actions until the affordance tool
is ported), paint, inspector edits, undo, and delete all work
correctly.
- pure builder returns `{ body, cap, flues, cricket }` so each piece
carries its own material (body/top split matches the schema's
`material` vs `topMaterial`). Body height derived from the parent
segment's `wallHeight + (flat ? 0 : roofHeight) + heightAboveRidge`.
- custom `def.renderer` reads the parent segment via `useScene`,
follows `useLiveTransforms` during a parent drag.
- placement tool listens to `roof:*` events, creates a new
ChimneyNode parented to the targeted segment with segment-local
coordinates.
- ChimneyEvent + NodeEvents<'chimney', ChimneyEvent> added to the
event bus.
- ChimneyMaterialRole helper re-exported from core (used by the
paint-mode picker — keeps the legacy multi-surface signature).
Verified: workspace build green, 11 new tests pass (36 total across
box-vent / ridge-vent / chimney).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(solar-panel): port to packages/nodes registry shape (Option C)
Fourth roof-mounted kind, same shape as box-vent (`752ace83`),
ridge-vent (`10d489d6`), and chimney (`45038713`).
- pure builder generates the rows × columns cell grid as a single
merged BufferGeometry with two render groups (frame + glass) so
one mesh can take a `[frameMaterial, panelMaterial]` array.
- analytical roof-surface helpers (`getSurfaceY`, `getAnalyticalNormal`,
`surfaceQuatFromNormal`) live alongside the geometry builder and
drive both the renderer (when `surfaceNormal` is absent from the
node) and the placement preview/commit.
- placement tool stores the analytical surfaceNormal on the new node
so the runtime renderer and the placement preview produce the same
orientation.
- `solar-panel-presets.ts` moved into core (it was already imported
from the schema there) and re-exported through `@pascal-app/core`.
- inspector parametrics cover preset, grid, panel dims, mounting
(flush/tilted with `tiltAngle` shown only when tilted), standoff,
and frame.
- SolarPanelEvent + NodeEvents<'solar-panel', ...> on the bus.
**Option C still applies**: panels visually sit on the roof surface
but the roof is NOT cut beneath them; the legacy renderer's
useFrame-driven quaternion smoothing is replaced by a static
quaternion computed once per render. Surface tracking under live
parent rotation comes back when roof-segment migrates to Stage B.
Verified: workspace build green, 16 new tests (52 total across the
four ported kinds).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(skylight): port to packages/nodes registry shape (stub)
Fifth roof-mounted kind. Schema is complete, but the geometry and
animation surfaces are intentionally stubbed — this commit lands the
registration so the kind is present in palette / inspector / sidebar
/ undo, and follow-up commits flesh out the type-specific geometry
and the animation system.
**Scope.**
- Schema: every field from the archive ports verbatim (25 fields,
five `skylightType` variants, opening/sliding state, lantern
proportions, curb).
- Geometry: frame + glass rendered as plain boxes regardless of
`skylightType`. Lantern slope, opening swing tilt, and sliding
panel offset from the archive are not yet rebuilt.
- Animation: `operationState` and `slideFraction` round-trip via the
inspector but don't drive geometry yet and don't interpolate over
time. The legacy animation lived in `useInteractive.skylight
Animations`, which doesn't exist on main — re-introducing that
surface is a focused follow-up.
- Inherits Option C from chimney: no CSG cutout into the roof; no
frame CSG (4 box rails instead).
**Why ship the stub now**: the framework wiring (schema in core,
event bus entry, plugin registration, inspector descriptor, custom
renderer with parent-segment lookup, placement tool) is the part
that's reusable across all five `skylightType` variants. Wiring
+ box geometry takes the kind from "doesn't exist" to "place / move
/ paint / delete / undo all work" without committing to the harder
type-specific geometry decisions.
Follow-up commits:
- type-specific geometry (lantern slope, opening tilt, sliding offset)
- animation system + `useInteractive.skylightAnimations` extension
Verified: workspace build green, 7 new tests (59 total across the
five ported kinds).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dormer): port to packages/nodes registry shape (stub)
Sixth and final roof-mounted kind. Schema complete; geometry stubbed
as a house silhouette (box body + triangular gable). Same Option C
inheritance as chimney, solar-panel, and skylight.
**Key call: window is inlined, not a hosted child.** The archive's
dormer carries its window opening as parametric fields on its own
schema (`windowWidth`, `windowColumns`, `windowSill`, etc.) — not a
hosted `WindowNode` child. So `relations.hosts` stays unset and the
kind doesn't need a `children` field. The 17 window-* fields stay in
the schema; geometry beyond the silhouette stub picks them up later.
- per-surface material resolution (`getEffectiveDormerSurfaceMaterial`)
ports verbatim into core with the same cross-fallback semantics
(top → material, side ↔ wall, then legacy `material`).
- placement tool follows the established pattern (`roof:*` events,
segment-local commit, analytical surfaceNormal stored).
- `RoofType` import resolved from the existing `roof-segment` schema
on main (the archive's `./roof-type` file is consolidated there).
- DormerEvent + NodeEvents<'dormer', DormerEvent> on the bus.
**Stub scope.** Geometry renders gable-only regardless of `roofType`;
no window opening cutout, no window frame, no sill, no roof trim
where the dormer meets the host segment. The archive's geometry
relies on `getDormerExposedFaces` + `generateDormerGeometry` from
the legacy roof-system, neither of which exists in `packages/nodes`.
Follow-up commits add per-roofType dormer roofs, the window
opening+frame+sill, and the trim/CSG against the parent segment.
Verified: workspace build green, 12 new tests pass (71 total across
all six ported kinds; pre-existing spawn parity failures unrelated).
All six roof-system kinds now live in the registry shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: porting cheat-sheet for the roof-system kind migrations
Reference doc kept alongside the six kind ports (box-vent, ridge-vent,
chimney, solar-panel, skylight, dormer) so future kind authors can
follow the same shape. Captures:
- the per-kind folder layout (13 files, what each one owns)
- the three-checkbox composition model (`geometry` / `renderer` / `system`)
- every `NodeDefinition` field with usage notes
- the wiring touch-points outside the kind folder
(`packages/nodes/src/index.ts`, `packages/core/src/events/bus.ts`,
the AnyNode union, the core schema exports)
- per-kind decisions for the six roof-system kinds (which checkboxes
each one ticks, what gets stubbed, what's deferred)
- pitfalls hit while porting (material-cache leaks, group-transform
mutation, host-kind children fields, Path 1 vs Path 2 floorplan
move)
- a pre-PR checklist
Kept under `.claude/` (not `wiki/`) since it's a working note for the
in-flight migration, not authoritative project documentation. Move
into `wiki/architecture/` later if it earns its keep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: wire paint targets + skylight animation surface for new kinds
Two small additions cherry-picked from roof-system-archive that the
six kind ports depend on. Both are mechanical and unblock follow-up
work without changing existing behavior.
**Paint targets.** Add `chimney`, `skylight`, `dormer` to
`MaterialTarget` enum so the paint picker surfaces these kinds. Wire
`chimney` and `dormer` into the relevant material-library target
arrays (WALL_TARGETS, SLAB_TARGETS, WALL_AND_SLAB_TARGETS,
ROOF_TARGETS) so wall / slab / roof material catalog entries are
offered when painting a chimney or dormer. Without this the new
kinds' `material` / `materialPreset` fields can be set
programmatically but the user-facing paint flow has nothing to
target.
**Skylight animation surface.** Port `SkylightInteractiveState` +
`SkylightAnimationState` types, `skylights` / `skylightAnimations`
store fields, and four actions (`setSkylightOpenState`,
`removeSkylightOpenState`, `startSkylightAnimation`,
`cancelSkylightAnimation`) onto `useInteractive`. Mirrors the
existing door / window animation surfaces one-for-one. This is the
prerequisite the skylight stub commit (`6dcee1ee`) called out — the
follow-up commit that adds the skylight animation system component
+ wires `operationState` into the renderer's geometry now has
something to consume.
Neither change touches the six kind folders or their definitions —
the kinds will pick up the new paint targets automatically and the
skylight animation surface is dormant until a consumer ports
forward.
Verified: workspace build green, 71/71 kind tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(palette): surface the six roof accessories in StructureTools
Closes the UX gap from the kind ports: box-vent / ridge-vent /
chimney / solar-panel / skylight / dormer are registered in the
registry with `def.tool` and `presentation`, but the top palette
(`StructureTools`) is currently driven by a hand-coded `tools`
array, not by the registry. So the new kinds existed in the
codebase but had no entry point in the running editor — the user
had no way to add them.
- Extend `StructureTool` union in `use-editor.tsx` with the six new
kind IDs so `setTool('chimney')` typechecks.
- Add six entries to the `tools` array in `structure-tools.tsx`.
All use the existing `/icons/roof.png` (a kind-specific icon set
is a follow-up).
The ToolManager already dispatches `nodeRegistry.get(tool)?.tool`
(`tool-manager.tsx:28`), so clicking a new palette button activates
the kind's registered `def.tool` automatically — no further wiring
needed.
Follow-up: a `parametrics.customPanel` on `roofDefinition` that
surfaces inline "Add Chimney / Skylight / Dormer / ..." buttons in
the roof inspector (matching the legacy `roof-panel.tsx` UX). For
now, top palette is the entry point.
Verified: workspace build green, 71/71 kind tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(roof): add inline "Add Element" section to the roof inspector
When a roof is selected, the inspector now shows six quick-add buttons
(Chimney, Dormer, Skylight, Solar Panel, Box Vent, Ridge Vent) in an
"Add element" section between Position and Actions. Closes the
discoverability gap from the kind ports — the user no longer has to
hunt for the kind in the top palette.
- Lives in `packages/nodes/src/roof/panel.tsx` (the roof's existing
customPanel — it already escapes the auto-derived inspector to render
Segments + Position + Actions).
- Each button calls `useEditor.getState().setTool(kind)` to activate
the kind's registered `def.tool`. The ToolManager dispatches via
`nodeRegistry.get(tool)?.tool` (`tool-manager.tsx:28`), so this
reuses the same code path as clicking the kind in the top palette.
- Tools listen for `roof:*` events — after clicking "Add Chimney" the
user clicks anywhere on a roof segment to commit the new node
parented to that segment.
Mirrors the legacy `roof-panel.tsx` UX (which had inline Add buttons
that created hidden nodes + entered move mode); the registry-shaped
equivalent activates the placement tool instead so the user sees a
preview that follows the cursor.
Verified: workspace build green, 71/71 kind tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ux(palette): remove roof accessories from the top palette
Six kinds (box-vent / ridge-vent / chimney / solar-panel / skylight /
dormer) only make sense in context of a selected roof segment — putting
them in the top palette clutters it for users not actively editing a
roof. They're entered through the roof inspector's "Add element"
section instead (added in 275af8f4), which routes to the same
registry-driven placement tools.
- Remove the six entries from the `tools` array in `structure-tools.tsx`.
- Keep `StructureTool` union additions in `use-editor.tsx` since
`setTool('chimney')` etc. still need to typecheck from the roof
panel's `activateTool` callback.
Verified: workspace build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(placement): always resolve a roof segment on click so add commits
Likely root cause of "Add Element clicks not adding anything." The
six roof-mounted placement tools each had a private
`resolveSegmentFromWorldPoint` that returned null when the click's
segment-local (x, z) fell outside `width/2 × depth/2` — but the
visible merged-roof mesh extends past those bounds by the segment's
overhang. Clicks landing anywhere in the eave band, or beyond every
segment's nominal footprint, silently no-op'd: `onClick` early-returned
on `if (!hit) return` and no node was created.
- Extract a shared `resolveRoofSegmentHit` into
`packages/nodes/src/roof/segment-hit.ts`.
- Bounds check now includes `seg.overhang` on each side, matching the
visible roof mesh.
- If no segment passes the exact check, fall back to the FIRST
segment with the click point projected into its local frame. Same
policy the legacy `roof-panel.tsx` used (it parented all add
operations to `segments[0]` and let the user move afterward).
- Rewire box-vent, ridge-vent, chimney, solar-panel, skylight, and
dormer placement tools to use the shared helper. Drop the per-tool
copies (and the now-unused `RoofSegmentNode` import in 5 of them).
After this, clicking "Add Chimney" / etc. in the roof inspector
followed by a click anywhere on the visible roof commits the new
node every time.
Verified: workspace build green, 71/71 kind tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(roof): mount accessory children of roof-segments so Add actually adds
Closes "Add Element click adds nothing to the scene." After the click,
the new chimney/skylight/dormer/box-vent/ridge-vent/solar-panel node
was being created in `useScene.nodes` with `parentId: <segmentId>` —
but nothing mounted it visually. Two missing pieces:
1. RoofSegmentNode had no `children` array. `createNodesAction`
appends `newNode.id` to `parent.children` only when the parent
declares the field (`node-actions.ts:355`). Without it the
parent-side write was a no-op, so the accessory existed in the
store but nothing ever fired its `<NodeRenderer>` mount.
2. Even with the schema field, `roof-segment/renderer.tsx` was a leaf
`<mesh>` — no recursive `<NodeRenderer>` mount of `node.children`.
Fix:
- `core/src/schema/nodes/roof-segment.ts`: add
`children: z.array(z.string()).default([])`.
- `nodes/src/roof-segment/renderer.tsx`: emit a `<group>` alongside
the placeholder mesh that iterates `node.children` and mounts each
via `<NodeRenderer>`. The group carries the same transform as the
mesh so accessories inherit the segment's local frame — matching
the segment-local coordinates each accessory renderer assumes.
- `nodes/src/roof/renderer.tsx`: drop the `visible={false}` segments
wrapper. `RoofSystem` only fills the parent roof's `merged-roof`
mesh (`viewer/systems/roof/roof-system.tsx:172` via
`getObjectByName('merged-roof')`), so segment placeholder meshes
stay empty and don't z-fight with the visible roof. Mounting
segments inside a visible wrapper is what lets accessory grand-
children render at all.
Also unblocks the user's `roof/panel.tsx` accessory-list selectors
(which loop `seg.children` for chimneys/dormers/skylights/etc.) by
giving the schema the field they expect.
Verified: workspace build green, 71/71 kind tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(roof-accessories): drop double-applied segment transform from renderers
After the previous fix (segments host accessories via recursive
NodeRenderer), each accessory was being positioned at *twice* the
segment offset — the renderer's outer group still applied
`segment.position` and `segment.rotation`, and the React parent (the
segment's group) was already at that transform too. Result: chimneys,
skylights, dormers, etc. landed in the scene graph but rendered far
off-screen — invisible from any normal camera view.
Fix the six accessory renderers (box-vent, ridge-vent, chimney,
solar-panel, skylight, dormer) to assume the segment's transform is
inherited from the React tree:
- Drop the outer `<group position={segmentPosition} rotation-y={...}>`
wrapper.
- Apply `node.position` (segment-local) directly to the ref'd outer
group, with the kind-specific tilt / quaternion / yaw on inner
groups.
- Drop `useLiveTransforms` lookup for the segment — React tree
re-renders propagate parent transform changes automatically.
- Keep the `useScene` segment lookup; it's still needed for kind-
specific math (slope tilt, analytical surface normal, base Y from
wallHeight) that reads segment fields beyond just the transform.
Chimney's outer group sits at `[0, 0, 0]` because `applyNodeTransform`
in `geometry.ts` already bakes `node.position` and `node.rotation`
into the chimney's vertex positions (which also bake `baseY` from
`segment.wallHeight`). No double application there either.
After this, Add Element → click → place puts the accessory at the
clicked spot on the roof, visible and selectable.
Verified: workspace build green, 71/71 kind tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(scene): migrate existing roof-segments to add `children: []`
Closes "Add Element click adds to side panel but not to scene graph."
The previous fix added `children: z.array(z.string()).default([])`
to RoofSegmentNode, but that default only applies when zod parses
the segment fresh. Every roof-segment already in a loaded scene
(saved before the schema change) carries no `children` field at
runtime.
`createNodesAction` (`node-actions.ts:355`) appends the new child id
to `parent.children` only when `'children' in parent && Array.isArray(...)`
is true. For un-migrated segments that check fails — the chimney /
skylight / dormer / etc. is added to `useScene.nodes` (so it shows
up in the sidebar tree) but the parent-side write is a no-op, so
the segment's children array remains undefined, the segment
renderer's recursive `<NodeRenderer>` finds nothing to mount, and
nothing appears in the 3D scene.
Mirror the existing shelf migration (`use-scene.ts:351`): when the
scene loads, patch every roof-segment whose `children` isn't an
array to `children: []`. Existing scenes get the field on next
load; new segments get it from the schema default. After this the
"Add Element" click commits visibly.
Verified: workspace build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chimney): port archive's bespoke tabbed panel as customPanel
The auto-derived inspector worked but couldn't reproduce the
archive's tabbed UI (Cap / Flues / Shoulder / Bands / Cricket /
Panels) where each sub-section gets its own dedicated controls
laid out as a grid of selectable cards. Users selecting a chimney
got a flat parametric form instead of the dense bespoke editor.
- Drop the legacy `packages/editor/src/components/ui/panels/chimney-panel.tsx`
into `packages/nodes/src/chimney/panel.tsx` (the kind's customPanel
slot). Rewrites:
* Helper imports collapsed to one barrel from `@pascal-app/editor`
(`ActionButton`, `ActionGroup`, `PanelSection`, `PanelWrapper`,
`SegmentedControl`, `SliderControl`, `triggerSFX`).
* `sfxEmitter.emit(...)` → `triggerSFX(...)` (same SFX, registry-
safe export from `@pascal-app/editor`).
* Inline a 3-line `cn` helper since editor doesn't re-export the
legacy `lib/utils` one.
* `ChimneyPanel` becomes `default export` so customPanel's lazy
loader can pick it up.
- Wire `chimneyParametrics.customPanel = () => import('./panel')` so
the registry's parametric inspector defers to the bespoke component.
- Keep `groups` in `chimneyParametrics` for MCP / fallback consumers
(the parametric data is still authoritative).
User-visible: clicking a chimney now opens the tabbed inspector with
the exact category layout from the archive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chimney): CSG-trim the body against the roof so it carves cleanly
Closes "chimney is not getting trimmed just like in roof-system."
Previously the chimney mesh was rendered as solid geometry that
intersected the roof shell visually at the deck line — Option C
debt called out in `45038713`. Now the body is CSG-cut against the
parent segment so only the portion above the shingles is visible,
matching the archive's UX.
- `packages/viewer/src/lib/csg-utils.ts`: port `csgEvaluator`,
`csgGeometry`, `csgMaterials`, `computeGeometryBoundsTree`,
`prepareBrushForCSG`, and the `Brush` / `SUBTRACTION` re-exports
from `roof-system-archive`. Lives in viewer because
`three-bvh-csg` + `three-mesh-bvh` are viewer-only deps.
- `packages/viewer/src/index.ts`: expose the CSG primitives + the
existing `getRoofSegmentBrushes` (which was already defined on
main but not in the package surface). Adding `getRoofSegmentBrushes`
to the export — internal already; this just opens it for kinds
living in `@pascal-app/nodes`.
- `packages/nodes/src/chimney/roof-trim.ts`: new helper
`trimChimneyBodyAgainstRoof(body, segment, node)`. Wraps the body
in a `Brush`, runs a two-pass `SUBTRACTION` (chimney - wallBrush -
shinSlab), returns the trimmed `BufferGeometry`. Returns the input
unchanged on any CSG failure so the chimney still renders.
- `packages/nodes/src/chimney/renderer.tsx`: memoize a `trimmedBody`
alongside the existing geo memo (keyed on the segment shape fields
that drive the roof brushes) and pass it to the body mesh instead
of `geo.body`. Disposal updated to release whichever buffer is
actually live.
Deferred (Option C still): bands and panels CSG. They were the same
flow but operate on additional pieces; they re-light in a follow-up
once the chimney's bands / panels geometry comes back online.
Verified: workspace build green, chimney unit tests pass (14/14).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(skylight): port full v1 implementation (panel, 5-type 3D, CSG cut, animations)
Brings the skylight node from a box-only stub up to feature parity with the
roof-system branch. Mirrors the chimney port pattern established in 9fd42e33
and 65eec685.
UI
- packages/nodes/src/skylight/panel.tsx — bespoke tabbed inspector
(type card picker + per-variant controls: lantern height/scale,
opening angle/side/motor, sliding direction/track width, curb, frame,
position, rotation). Wired via parametrics.customPanel.
3D
- packages/nodes/src/skylight/{renderer,geometry,frame-csg}.tsx —
full 5-variant geometry (flat / walk-on / lantern / opening / sliding)
with frame ring CSG and type-specific glass (lantern pyramid +
cylindrical frame bars; opening hinged glass with optional motor
housing; sliding two-pane on tracks).
- packages/nodes/src/skylight/preview.tsx — uses the real frame-csg
builder so placement ghost matches the committed mesh.
Placement / move
- packages/nodes/src/skylight/tool.tsx — commits hit.localY so the
skylight lands on the outer shingle surface, not the bare-rafter
analytical Y (was sinking into the deck).
- packages/nodes/src/skylight/move-tool.tsx — kind-owned drag wired
via def.affordanceTools.move. Uses SkylightPreview as the ghost so
drag and duplicate both show the real frame following the roof
raycast. Reparents across segments and dirties old+new for CSG
re-cut.
CSG cutout
- packages/viewer/src/systems/roof/roof-system.tsx — buildSkylightCutBrush
added; the per-child loop in updateMergedRoofGeometry subtracts every
skylight from shin/deck/wall in segment-local before the segment
transform stacks on (matches v1).
- Ported v1's getRoofOuterSurfaceFrameAtPoint helper (raycast against the
actual outer-shingle module mesh) and made both the cut and the
renderer read surface point + normal from it — keeps frame and cut
aligned on every roof type incl. hip 4-faces, gambrel, mansard, dutch.
- mergeVertices on the cut box before computeBoundsTree — without it
three-bvh-csg silently no-ops on the BoxGeometry after applyQuaternion
tilts the cut ~90° about the surface normal (hip short faces).
- Renderer wraps content in an outer <group position={segment.position}
rotation-y={segment.rotation}> so the frame inherits the same segment
transform that applyTransform bakes into the cut brush (skylight is
rendered under <group name="roof-elements"> at the roof level, not
under the segment, so the renderer has to apply it explicitly).
- Skylight dirty propagation in RoofSystem: edits/moves dirty the host
segment so the parent roof rebuilds.
- packages/viewer/src/index.ts — exposes getRoofOuterSurfaceFrameAtPoint,
SurfaceFrame, getRoofSegmentBrushes, csg primitives so @pascal-app/nodes
can compose roof-aware cuts without a layer violation.
Animations
- packages/editor/src/lib/skylight-interaction.ts — verbatim port of v1
(toggleSkylightOpenState, closeSkylightOpenState, isOperableSkylightType,
SKYLIGHT_TOGGLE_ANIMATION_MS = 520).
- packages/editor/src/hooks/use-keyboard.ts — R toggles, T closes
operable skylights, mirroring door/window.
- packages/nodes/src/skylight/system.tsx — SkylightAnimationSystem
ported as def.system; advances skylightAnimations and writes
operationState back to useInteractive.skylights.
- Dropped the per-tick markSkylightDirty in the animation system. The
renderer subscribes to useInteractive directly, so the glass swings/
slides via Zustand re-renders without dirtying the scene — the cut
geometry doesn't depend on operationState, so re-CSG'ing the merged
roof on every animation frame was pure waste (caused visible lag).
- packages/core/src/index.ts — exports SkylightInteractiveState and
SkylightAnimationState (interaction lib uses them).
Drag / duplicate ghost
- Floating action menu's setMovingNode → MoveTool → registry affordance
now resolves to the kind-owned move tool. Duplicate already worked
through structuredClone + def.schema.parse + setMovingNode; the new
move-tool provides the ghost both flows use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Persist roof segment materials across refresh
* feat(solar-panel): full port from archive + placement/orientation fixes
Solar panel — ported from roof-system archive to registry shape:
**Inspector & presets**
- Custom panel (panel.tsx) with preset card grid (Residential / Residential
Large / Compact / Frameless); picking a preset now writes all four dim
fields (panelWidth / panelHeight / frameThickness / frameDepth) so the
inspector immediately reflects the selection.
- Auto-fit to roof, Flip orientation, Custom label when no preset matches.
- Live preview: renderer subscribes to useLiveNodeOverrides so slider drags
update the mesh before the value is committed to the Zustand store.
- Registered via parametrics.customPanel (same pattern as chimney).
**Texture / materials**
- Procedural cell texture (createSolarPanelTexture): chamfered cell shape,
dark blue gradient, finger-line and busbar detail drawn on a 256×256
canvas, tiled per cell via stretched UVs in buildSolarPanelGeometry.
- getDefaultPanelMaterial singleton uses MeshStandardNodeMaterial (WebGPU-
native) so the material integrates with the MRT pass without triggering
"Color target has no corresponding fragment stage output / writeMask not
zero" GPU validation errors on segment reparent.
- defaultFrameMaterial and move-tool previewMaterial also switched to
WebGPU-safe types (MeshStandardNodeMaterial / MeshBasicMaterial).
**Default grid size**
- Schema defaults changed from 4 rows × 5 columns → 2 rows × 3 columns.
- Placement ghost and move-tool ghost use a compact 2×3 footprint;
committed panels also default to 2×3.
**Placement tool**
- Commit position uses raycast hit Y (hit.localY from segObj.worldToLocal)
instead of analytical getSurfaceY so the panel lands exactly where the
ghost was shown rather than sinking into the deck/shingle layers.
- Ghost orientation uses the same analytical-normal + explicit-yaw pattern
as the placement tool for correctness on rotated segments.
**Move tool**
- Rewrote ghost to use resolveRoofSegmentHit + getAnalyticalNormal
(segment-local) + explicit rotation-y group, matching the placement
tool's ghost layout exactly. Dropped unreliable event.normal / world-
space quat path that gave wrong tilt on any segment with rotation ≠ 0.
- Committed surfaceNormal is now segment-local (not world-space) so the
renderer's surfaceQuat + outer segment.rotation group compose correctly
without double-rotating the panel.
- Uses shared resolveRoofSegmentHit (with surface-Y disambiguation) instead
of the private copy, so segment hopping respects the correct face.
- Reparents children arrays on segment hop.
**Renderer**
- Applies segment.position + segment.rotation explicitly (roof accessories
are mounted under roof-elements group which has no transform, not under
segment subtree).
- Merges useLiveNodeOverrides so slider drags update the 3D mesh in real
time (same pattern as elevator/skylight renderers).
**Scene graph**
- SolarPanelTreeNode added; registered in tree-node.tsx type map so panels
appear under their parent roof-segment in the sidebar.
**Segment-hit disambiguation**
- resolveRoofSegmentHit now scores all bbox-passing candidates by
|localY − analyticalSurfaceY(localX, localZ)| and picks the smallest,
fixing the long-standing bug where hip/gable segments at the same roof
origin all pass the axis-aligned bbox test and the first-match (always
segments[0]) was returned regardless of which slope was clicked.
Benefits all roof-accessory placement tools (chimney, box-vent, skylight,
dormer, solar-panel).
**Hip-roof normal fix**
- getAnalyticalNormal for hip now uses slopeReach = min(w,d)/2 for the Y
component on all four faces. The old code used depth/2 for front/back and
width/2 for sides, which was only correct for square (w==d) hips; for any
other aspect ratio the long-axis faces tilted the panel at the wrong angle.
**Dormer, dormer move-tool, window-frame, ridge-vent, box-vent, skylight**
- Assorted in-progress work: dormer window-frame geometry, move-tool port,
panel refinements, ridge-vent / box-vent panel additions, skylight CSG
frame refinements, roof-system geometry improvements, material-paint
support, post-processing cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(chimney): fix hook order + share materials + memoise segment brushes
- renderer.tsx: hoist `surfaceArray` useMemo above the `!segment || !geo`
early return so hook call order stays stable across renders (the
previous order would have crashed React the first time segment or geo
flipped to null mid-session).
- renderer.tsx: replace the module-scoped `bodyMaterial` / `topMaterial`
singletons with per-instance fallback materials so a paint-mode or
debug mutation on one chimney can't bleed into every other unpainted
chimney on the scene; dispose them on unmount.
- renderer.tsx: collapse the 36-field hand-maintained dep array on the
`geo` useMemo (and the 10-field one on `trimmedBody`) down to the
memoised `node` / `segment` references — adding a new schema field
no longer risks stale geometry from a forgotten dep, and the
`eslint-disable react-hooks/exhaustive-deps` lines are gone.
- renderer.tsx + roof-trim.ts: memoise `getRoofSegmentBrushes(segment)`
per-segment-shape in the renderer instead of rebuilding the four
CSG-ready brushes inside `trimChimneyBodyAgainstRoof` on every call.
A chimney slider drag changes `node.*` but not the segment, so the
brushes now survive the entire drag instead of being rebuilt and
disposed every frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(scene): pin reparent behavior for roof-mounted kinds under repeated A→B→A→B
Investigating a reported crash where moving a vent across roof
segments three times in a row crashes the scene. The hypothesis was
duplicate IDs in the host segments' `children` arrays. These tests
prove that's NOT the cause: at the store level, the auto-reparent
inside `updateNodesAction` leaves children lists clean under repeated
hopping for every roof-mounted kind (box-vent, chimney, skylight,
dormer, solar-panel, ridge-vent), and even the redundant manual-then-
auto pattern the vent move-tools use converges to the same correct
state.
Crash root cause still under investigation, but these pins prevent
the obvious-and-tempting regression where someone "fixes" reparent
by hand and accidentally lets duplicates through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(chimney): give each sub-mesh its own name
Body, cap, flues, cricket, and bands were all named `chimney-surface`,
so hover/selection couldn't distinguish them and panel breadcrumbs
couldn't say "Chimney cap" vs "Chimney body". Rename to
`chimney-{body,cap,flues,cricket,bands}`. No code looked up the old
literal, so this is a pure naming improvement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chimney): smooth round shading, radial cap UVs, cap reveal, flue pots
Four visual upgrades to the chimney builder, all in `geometry.ts`:
- **Round chimneys now render smooth, not faceted.** The previous
`pushCylinderFaces` emitted unindexed triangles, so
`computeVertexNormals()` baked per-triangle face normals into every
vertex — the 24 polygon segments of a round body / cap / band were
visible as flats. Round paths now build per-tier
`THREE.CylinderGeometry` (indexed, side vertices shared across
radial segments) and merge via `mergeGeometries`. Crisp rim edges
are preserved because CylinderGeometry uses separate cap vertices.
- **Radial cap UVs.** Old `pushCylinderFaces` pushed `(0,0)` for every
vertex on the top/bottom fan, so any texture on a round chimney
smeared to a point at the caps. CylinderGeometry gives proper
radial UVs (0.5 ± 0.5·cos/sin) for free.
- **Cap reveal.** The cap used to sit flush on the body, reading as
glued on. New `CAP_REVEAL = 0.003` (3 mm) air gap above the body
catches a shadow line and sells the cap as a separate stone / metal
piece. `capTopY` (used for flue placement) updates so flues still
sit on the actual cap top.
- **Flue pots, not drainpipes.** Each flue was a single straight
cylinder / box — visually a "drainpipe", not a chimney. New
two-tier silhouette: a tall straight shaft topped by a short
overhanging rim (12 % of height, capped at 4 cm; rim radius
flares 12 %). Reads as a terracotta pot. Total height still
equals `flueHeight`, so the bore cutter in `holes.ts` covers the
whole envelope unchanged.
Removed the now-unused `pushCylinderFaces` helper. Slab path
unchanged — square chimneys keep their crisp 90° corners.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chimney): cornerBevel for square bodies / caps / bands
Square chimneys read as plastic boxes at any distance because every
vertical edge is a perfect 90° corner that catches no light. New
`cornerBevel` field (default 0 → existing scenes unchanged) replaces
each corner with a 45° chamfer face. Real masonry chimneys often
ship the same detail — a small bevel (~1-2 cm) breaks up the
silhouette and reads as stone or chamfered brick.
- Schema: add `cornerBevel: z.number().default(0)` to ChimneyNode.
- Geometry: extend `pushSlabFaces` with an optional `bevel` param.
When > 0, dispatch to a new `pushOctagonalSlabFaces` that emits
an 8-vertex ring per y-level (axis-aligned faces + 45° chamfer
faces) plus fan-triangulated octagonal caps. UVs follow the same
physical-meter convention as the unchamfered path so a brick
texture tiles at a consistent rate with and without bevel.
- Thread `node.cornerBevel` through `buildBodyGeometry`,
`buildCapGeometry`, and `buildBandsGeometry` (square paths only —
round bodies have no corners to bevel).
- Parametrics: expose under the Body group with `visibleIf` gating
on square body for the MCP / fallback inspector.
- Panel: add a "Corner Bevel" SliderControl in the Footprint section,
same conditional visibility, clamped at `min(width, depth) / 2`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chimney): style presets — Brick / Stone / Modern / Round
The chimney panel exposes 30+ sliders; landing on a coherent
silhouette (corbeled stone with a sloped cap and a cricket vs. a
straight brick with a double band) takes a dozen edits even when
you know what you want. New "Style" segmented control at the top
of the panel applies a curated bundle of fields in one click.
Presets only touch shape / silhouette / accessory fields:
`bodyShape`, `shoulderStyle*`, `cap*`, `band*`, `cricket*`,
`cornerBevel`, `panel*`, `flue*`. Dimensions (`width`, `depth`,
`heightAboveRidge`), placement (`position`, `rotation`,
`roofSegmentId`), and paint (`material*`, `topMaterial*`) are
deliberately left alone — applying a preset to an already-sized,
already-painted chimney resizes nothing and doesn't overwrite the
user's material choices.
- `presets.ts`: four preset bundles + `detectActiveChimneyPreset`
helper for highlighting the matching preset in the segmented
control.
- `panel.tsx`: new "Style" PanelSection above Footprint, segmented
control wired to `commitProp(chimneyPresets[key])`. Renders with
no segment highlighted ("custom") when the current node doesn't
match any preset exactly.
- `__tests__/presets.test.ts`: round-trip each preset, confirm
fresh-default chimneys are NOT detected as any preset, and
confirm non-preset fields (dims / materials / placement) don't
knock a chimney out of a preset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(chimney): drop the Stone preset
Leaves Brick / Modern / Round. The parameterised round-trip test
auto-adjusts via `CHIMNEY_PRESET_KEYS`; no test code change needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dormer): bugs, perf, UX, refactor + bundled pitch WIP
Dormer review-backlog batch across five passes. Plus the previously
unstaged pitch/roofHeight migration on RoofSegmentNode bundled in per
session continuity.
Dormer — bugs / dead code:
- implement the windowSill (toggle was UI-only before)
- ghost preview reads wallSkirtHeight and branches on roofType=flat
- live-override slider drag swaps the heavy CSG for the fallback
- consolidate arch/rounded shape builders between viewer CSG and frame
- drop unused surfaceNormal field
- collapse getEffectiveDormerSurfaceMaterial fall-through
- confine the panel's updateWorldMatrix into a single useMemo
- preserve position Y on panel commits (was being zeroed)
Dormer — schema hygiene:
- DORMER_DEFAULTS named constants replace inline magic numbers
- collapse windowCornerRadius + windowRadiusMode + windowCornerRadii
into the tuple alone; "All vs Individual" is derived UI state
- drop the `as never` id casts; rely on objectId default factory
Dormer — tactile UX:
- R / Shift+R rotates the placement ghost by ±15°
- auto-number new dormer names ("Dormer N", smallest free integer)
- DORMER_PLACEMENT_SNAP_M + ROTATION_STEP constants extracted
Dormer — code shape:
- new use-dormer-placement hook dedupes tool + move-tool (~90% shared)
- new <DormerWindowAssembly> isolates the frame/glass/sill JSX
- panel.tsx 788 -> 295 lines; Position / Window / Actions sections
extracted into per-file components
Bundled pitch WIP (pre-existing, unrelated to dormer):
- RoofSegmentNode.roofHeight removed; pitch (degrees) added
- new helpers in roof-segment: getActiveRoofHeight,
getPitchFromActiveRoofHeight, getSegmentSlopeFrame,
ROOF_SHAPE_DEFAULTS
- migration in use-scene.ts converts legacy roofHeight to pitch
- consumers updated: chimney, box-vent, ridge-vent, solar-panel,
roof-segment, roof, segment-hit, roof-tool, mcp construction-tools
Verification: 12/12 dormer tests pass; targeted tsc on dormer files
clean. Workspace bun build of nodes is also affected by the pitch WIP,
which is included here per request.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(roof): per-segment top/edge/wall materials + restore legacy UV flow
- RoofSegmentNode gains optional topMaterial / edgeMaterial / wallMaterial
fields mirroring the parent roof. getEffectiveSegmentSurfaceMaterial
resolves through segment-role → segment-legacy → parent fallback.
- Segment renderer builds the 4-slot array per role with the parent's
array as a fallback so paint at any level reaches the right surface.
- Painting a segment directly (segment edit mode hover) writes to the
segment's role fields via buildRoofSegmentSurfaceMaterialPatch — the
parent roof and other segments are untouched.
- Segments with any material override render as their own per-segment
mesh inside a new always-visible 'painted-segments' group; the merged-
roof CSG skips them (hasSegmentMaterialOverride) so we don't double-
paint with the roof's default array.
- Paint preview now dispatches to a segment-aware path
(applyRoofSegmentPaintPreview) so hover effects land on the visible
per-segment mesh instead of the hidden merged-roof.
- Drop the createRoofUvGeometry post-CSG re-projection. UVs now flow
through CSG (csgEvaluator.attributes includes 'uv') exactly as in the
legacy roof-system branch.
- Drop the stray [skylight-cut] console.log left in the hot path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(roof-system): registry capabilities for roof accessories, paint, and keyboard
Replace kind-name branches in framework code with registry-driven dispatch.
Three new NodeDefinition slots back the migration:
- capabilities.roofAccessory — host roof cascade + optional CSG cut.
Lets viewer's RoofSystem iterate dirty children and call buildCut on
any kind that declares it, instead of switching on node.type. Dormer
+ skylight cut builders moved into packages/nodes/<kind>/.
- capabilities.paint — resolveRole / buildPatch / applyPreview /
getEffectiveMaterial. Chimney, dormer, and wall now route through it;
per-kind arms deleted from selection-manager + material-paint.
- keyboardActions — R / T handlers contributed by the kind. Skylight's
open/close logic moved from editor/lib to nodes/skylight/interaction.
Dormer + skylight kind code (geometry, fallback shape, exposed-face math,
window-dim resolver, CSG cut builders) now lives under packages/nodes/
src/<kind>/ instead of packages/viewer/src/systems/roof/roof-system.tsx.
The viewer keeps only roof-generic primitives (roof-segment brushes,
surface-frame query, CSG dummy mats, material-slot remap).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review: remove PORT-CHEATSHEET, document roofAccessory/paint/keyboardActions, fix double registry lookup
- Remove .claude/PORT-CHEATSHEET.md (AI authoring aid, not for public repo)
- Document three new NodeDefinition capabilities in wiki/architecture/node-definitions.md:
roofAccessory, paint, keyboardActions
- Fix double nodeRegistry.get() lookup in use-keyboard.ts: replace !.keyboardActions!.r!.run()
with ?.keyboardActions?.r?.run() for both R and T arms
* fix(item-placement): memoize preview/dimension callbacks to stop placement loop
updatePreviewGeometry and updateDimensionGuides were declared as plain
functions in the component body, so they got a fresh identity every
render. Both sit in the placement setup effect's dependency array, which
made React tear the effect down and re-run it on every commit — its
teardown deletes the draft node while the setup re-creates it, producing
an infinite delete/recreate loop ("Maximum update depth exceeded") when
opening furnish mode.
Wrap both in useCallback with empty deps (they only close over stable
refs, module-level helpers, and the setDimensionBounds setter).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(item-placement): correct preview box dimensions and floor rotation
Two issues surfaced after the placement-loop memoization removed the
accidental every-render recompute that was masking them:
1. Preview box used stale (asset-default) dimensions at draft creation
because nothing recomputed it once the imperative draft was made.
Recompute the box from the freshly-created draft in `ensureDraft` and
the chained next-draft path in `onGridClick`.
2. The green/red box (and the live transform the 2D floorplan mirrors)
ignored the floor item's rotation:
- `floorStrategy.move` returned a hardcoded `cursorRotationY: 0`; now
returns the draft's rotation (`rotY`).
- `onGridMove` never applied `result.cursorRotationY` to the cursor
group; now it does, so box + floorplan track the draft on every move.
- the init seed used the mesh world quaternion, which double-counts
building rotation for floor items; floor now seeds from the node's
local Y rotation (wall/ceiling keep the world-quaternion path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: open-pascal <open@pascal.app>
Co-authored-by: Wassim SAMAD <wass08@gmail.com>
|
||
|
|
3e8b972b8f |
fix(mcp): templates use string IDs for SiteNode.children
PR #320 changed SiteNode.children from embedded BuildingNode/ItemNode objects to flat string[] IDs. PR #325 updated the runtime call sites but missed the three scene templates, which still mutated the site node's children array to embed full building objects after building the flat dict. This caused AnyNode.safeParse to fail for site_empty, site_2br, and site_garden in bun test --cwd packages/mcp. Remove the obsolete mutation blocks; each template already initialises site.children with the correct string id (e.g. ['building_empty']). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
01639e42e4 |
fix: update mcp and nodes for SiteNode.children string[] schema change
Fixes breakage introduced by #320 which changed `SiteNode.children` from embedded `BuildingNode | ItemNode` objects to flat `string[]` IDs. - `packages/mcp/src/lib/rehydrate-site-children.ts`: replace now-obsolete re-embedding logic with a no-op passthrough (call-site compatible) - `packages/mcp/src/tools/variants/generate-variants.ts`: drop the inline copy of the same function and its call - `packages/nodes/src/site/renderer.tsx`: cast `childId as AnyNodeId` since `SiteNode.children` is now `string[]`, not `AnyNodeId[]` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
8921e341cc |
Bump inter-package peerDeps to ^0.8.0 + fix release.yml sync bug
Two bugs in one fix: 1. The 0.8.0 release commit only bumped .version, not the inter-package peerDependencies / devDependencies. So the published packages still declared peer constraints like "@pascal-app/viewer": "^0.7.0". Caret in semver 0.x doesn't allow 0.8.0 to satisfy ^0.7.0, so bun resolves workspace consumers (and apps/editor's deep import paths) to the stale npm-published 0.7.0 instead of the workspace 0.8.0 — meaning local edits never show up in apps/editor or any linked consumer. 2. The release.yml sync step was using $GITHUB_ENV to read back the new versions in the same step, which doesn't work — env-file writes only surface in subsequent steps. Switched to a bash associative array (NEW_VERSIONS) for in-step lookup, kept the $GITHUB_ENV write for the downstream publish/commit/tag steps. Also added a final "refs after sync" debug print so this is visible in the workflow log. After this lands and you bun install, packages/editor/node_modules/@pascal-app/viewer should symlink to the workspace packages/viewer/, not to .bun/@pascal-app+viewer@0.7.0. Future releases will sync peerDeps correctly on their own. |
||
|
|
60dbfcd3fe | release: @pascal-app/core@0.8.0 @pascal-app/viewer@0.8.0 @pascal-app/editor@0.8.0 @pascal-app/mcp@0.2.0 | ||
|
|
e618175bba |
Reset core/viewer/editor/mcp packages and apps/editor from private-editor
Wholesale swap of packages/{core,viewer,editor,mcp} and apps/editor with the
versions from the private editor repo, which is the production source of truth.
Setup changes:
- packages/{core,viewer,editor} versions held at 0.7.0 baseline (matching
the most recent published release) so a bump=minor publishes 0.8.0
- packages/mcp held at 0.1.1 (never published; first publish will go through
the new release.yml flow)
- peerDependencies and devDependencies for inter-package @pascal-app/*
references pinned to ^0.7.0 instead of '*' / 'workspace:*' so they are
valid for npm consumers
- Root package.json: TypeScript bumped to 6.0.2, added overrides for
@types/react, @types/react-dom, @types/three to prevent JSX namespace
fragmentation across the workspace
- release.yml extended to also publish editor and mcp; 'both' option renamed
to 'all'; added a sync step that updates inter-package peerDeps/devDeps to
match the new versions on every bump (so viewer/editor/mcp tarballs always
reference the version of core they were built against)
- Root scripts gained release:editor and release:mcp shortcuts
Verification:
- bun install --frozen-lockfile is consistent
- packages/{core,viewer,mcp} build cleanly, dist/index.d.ts emitted
- packages/editor check-types reports 21 pre-existing errors, identical to
what private-editor currently reports
Open PRs against editor-v2 will need rebasing/conflict resolution.
|
||
|
|
723ae14e9b | release: v0.6.1 | ||
|
|
e761084635 | Normalize root scene nodes and fix level duplication | ||
|
|
8dfd1a9429 | fix(mcp): add shared operations and secure scene APIs | ||
|
|
058ee747c9 | fix(mcp): distinguish roof support levels from stories | ||
|
|
3d5c87a651 | feat(mcp): add guided construction workflows | ||
|
|
b3d1f663f6 | fix(mcp): use local sqlite scene storage | ||
|
|
06d00566f0 |
docs(mcp): fix residual stale-number references in PR description
Two lines still cited the pre-A2 state (294 tests / 40 files, "known gaps remain at save_scene / POST boundary"). Corrected against the actual 302/41 and the Phase 10 A2 closure of those boundaries via the shared apiGraphSchema. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7ed34f5b4f |
docs(mcp): update PR description with post-A2 state (302 tests, 20 commits)
A5 wrote the initial PR description before the A2 security fixes landed, so it cited 294 tests / 9 commits and listed several now-fixed items as follow-ups. Updated: - 294 -> 302 tests (8 new SSRF guard tests in safe-fetch.test.ts) - 40 -> 41 test files - 9 -> 20 commits with the actual Phase 5-10 series listed - Security-notes section: document safeFetch + shared apiGraphSchema wiring on both POST and PUT; remove "tracked follow-up" for save_scene URL validation (done); add "tracked follow-up" keeps same-id write race + auth layer - Checklist: flip two items from [ ] to [x] for URL validation and SSRF protection Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6ace4bf7c8 |
docs(mcp): add Phase 10 pre-push audit reports (5 agents)
Five parallel audit agents reviewed the branch before open-sourcing
the PR to pascalorg/editor:
- a1-secrets.md: SAFE TO PUSH. Scanned 176 files / 40,768 diff lines.
Zero secrets, tokens, API keys, PEM blocks, JWTs, or cookies.
Only MEDIUM finding: absolute /Users/adrian paths in test-report
scripts (cosmetic, not security).
- a2-security.md: FOUND 2 HIGH-severity issues, both FIXED in
commit 8757de0:
* PUT /api/scenes/[id] still had the loose graphSchema that POST
got fixed in Phase 8 P4. Shared schema extracted to
apps/editor/lib/graph-schema.ts so both routes re-validate.
* photo_to_scene + analyze_floorplan_image + analyze_room_photo
all did raw fetch(url) on user-supplied URLs - a textbook SSRF
to 169.254.169.254 cloud metadata. Added safe-fetch.ts with
private-IP / link-local / .local-hostname denylists, manual
redirect revalidation, size cap, timeout, env-allowlist.
- a3-code-quality.md: READY FOR REVIEW. Zero production `any`, all
tools Zod-validated in+out, uniform error handling,
conventional-commits. Two non-blocking follow-ups: client editor
components (SceneLoader, SaveButton) have no tests; document
check_collisions n^2 scaling.
- a4-performance.md: SHIP WITH NOTES. MCP dist 904 KB, Supabase
lazy-imported (zero editor bundle impact), v0.1 hot paths
sub-200ms. Flagged: FilesystemSceneStore.index.json O(n) per
write (fine <1k scenes), concurrency races (documented in P8),
client render at 5k nodes unverified.
- a5-pr-description.md: polished final PR description that
corrected stale test counts (294 not 142), disclosed all 5
cross-cutting surfaces, named the known failures honestly,
split the checklist, expanded the scope to the real Phase 7
deliverables.
Overall verdict: READY TO PUSH after the A2 fixes landed. No
blockers remain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8757de0c36 |
fix(mcp,editor): close PUT-route URL bypass + vision-tool SSRF (Phase 10 A2)
Pre-push audit agent A2 flagged two HIGH-severity security issues that would have shipped in the PR had we not checked: 1. PUT /api/scenes/[id] URL-validation bypass. Phase 8 P4 fixed the POST /api/scenes route by replacing a loose z.unknown() graph schema with AnyNode superRefine. The fix never made it to the PUT handler - an attacker could resubmit the same javascript:/file:/// payloads via PUT. Fixed by extracting the tight validator into apps/editor/lib/graph-schema.ts and sharing it across both routes. 2. SSRF in photo_to_scene / analyze_floorplan_image / analyze_room_photo. All three tools called raw fetch(image) on user-supplied URLs with no validation - a direct http://169.254.169.254/latest/meta-data/ exfil primitive on any cloud host. Added packages/mcp/src/lib/safe-fetch.ts that: - Blocks loopback (127.0.0.0/8, ::1) - Blocks link-local incl. cloud metadata (169.254.0.0/16) - Blocks private ranges (10/8, 172.16/12, 192.168/16, fc00::/7) - Blocks .local/.internal/.corp hostnames + localhost variants - Blocks v4-mapped IPv6 loopback (::ffff:127.0.0.1) - Manual redirects (max 3), revalidating the allowlist per hop - 20 MB response-size cap (streamed, enforced per-chunk) - 10s timeout - Optional PASCAL_ALLOWED_ASSET_ORIGINS env allowlist Tests: 8 new SSRF guard tests, all vision tests still pass, full suite 302/302. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
08e7b6db71 |
test(mcp): add populate-gallery script for post-ship demo
Seeds the shared scene store with Casa del Sol + 3 templates + 3 Villa Azul variants so /scenes can be browsed end-to-end after a fresh `bun dev` + `pascal-mcp --http` restart. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f230d9a401 |
test(mcp): Villa Azul + 10-agent deep verification
Builds a larger, richer house than Casa del Sol via MCP save_scene (no injection hack), then dispatches 10 parallel verifiers across schema, geometry, dimensions, openings, HTTP, page render, parentage, round-trip, spatial, visual. Villa Azul — 56 nodes, validate_scene=true, 44KB on disk: - 15x10m building envelope (vs Casa del Sol's 12x8) - 9 interior zones (master bed/bath, bed 2/3, shared bath, living/dining, kitchen, entry hall, corridor) - 10 doors + 12 windows (all cut successfully) - 4 exterior zones (pool 8x4 + basin slab at -2m, outdoor kitchen, driveway, back patio) - 5 rail-style fences (vs Casa del Sol's privacy) with 2m entrance gap Verification: 108 checks, 104 PASS, 4 findings: - V1 schema: 56/56 - V2 geometry: 7/7 (perimeter closes, interior T-junctions, no zone overlaps, fence gap verified) - V3 dimensions: 13/13 zone areas exact (1 spec mismatch on site polygon default, not a build bug) - V4 openings: 22/22 dimensional fit, surfaced a tool gap in cut_opening (no adjacency check) + my build packed too tightly - V5 HTTP: 10/10 (GET/PUT/PATCH/DELETE/HEAD, If-Match conflicts) - V6 page: 14/14 (/scene/:id 81KB, /scenes 20KB, 404 fallback) - V7 parentage: surfaced CROSS_CUTTING §2 site->building->level parentId=null (pre-existing in core's loadScene) - V8 round-trip: 10/10 byte-equal, duplicate_level -> 110 nodes - V9 spatial: 12/12 (find_nodes, measure, constraints resource) - V10 visual: HTML fallback (Chrome extension disconnected during run); API layer intact Follow-up tracked: `cut_opening` should check opening-adjacency on the same wall (minimum gap) to catch tight packing during patch construction. Currently returns success and relies on the UI to visualise the overlap. Live at http://localhost:3002/scene/a6e7919eacbe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0b84e7b7b1 |
fix(mcp,editor): close URL-validation bypasses surfaced by Phase 8 P4
Phase 8 parallel validation flagged two boundaries where malicious URLs
(javascript:, file:, external http:, data:text/html, ...) could be
persisted despite the AssetUrl allowlist added in Phase 7 A7:
1. `save_scene({ includeCurrentScene: false, graph })` — the graph arg
was treated as opaque (`z.record(z.string(), z.unknown())`) and
written to the store without re-running AnyNode.safeParse.
2. `POST /api/scenes { graph }` in the editor API — same issue; the
Zod `graphSchema` accepted anything object-shaped.
Fixes:
- `save-scene.ts`: when `includeCurrentScene === false`, iterate every
node and run `AnyNode.safeParse`; collect issues and throw
`McpError(InvalidParams, 'graph_invalid', { errors })` on any
failure.
- `app/api/scenes/route.ts`: replace `graphSchema` with a structured
`z.object({ nodes, rootNodeIds, collections? })` + `superRefine`
that runs `AnyNode.safeParse` on every node. Invalid → 400 with
detailed issue paths.
Tests:
- Added `save_scene` regression test for the P4 attack
(item.asset.src = 'javascript:alert(1)') — expected error.
- Fixed the existing `includeCurrentScene=false` test to use a
schema-compliant site node id (the prior `id: 'root'` now fails
the AnyNode parse, which is the desired strict behaviour).
- Full suite: 294 pass / 0 fail.
Also adds Phase 8 test-reports/phase8/** (10 agents, ~15 scripts +
markdown reports) documenting the validation run, plus minor biome
cleanups to the Phase 5/7 test artefacts (removed stale
`// biome-ignore` suppression comments that now resolve to the
already-off `noConsole` rule).
Phase 8 result summary (10 parallel agents, stdio MCP transport with
isolated data dirs):
- P1 templates: 18/18 PASS
- P2 variants: 6/7 mutations + determinism + save + combined + error
- P3 locking: 12/12 PASS (MCP + editor HTTP If-Match)
- P4 URL hardening: fixed 2 bypasses (see above)
- P5 photo-to-scene: 6/6 PASS
- P6 Casa del Sol via save_scene: 13/13 PASS
- P7 editor HTTP API: 18/18 PASS
- P8 concurrency: 4/5 PASS, flagged 2 real filesystem-store races
(expectedVersion CAS gap + .index.json drift under parallel writes)
- P9 edge cases: 13/13 PASS (size cap, slug safety, bad inputs)
- P10 full sweep: 37/37 PASS (30 tools + 4 resources + 3 prompts)
Known follow-ups:
- FilesystemSceneStore needs a proper lockfile / atomic CAS to fix
the P8 concurrency bugs (low priority: single-writer MCP is the
typical case).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e8d0b13ff5 |
feat(mcp,editor): Option A+B storage + 10 agent deliverables (Phase 7)
Ships the combined filesystem/Supabase storage adapter + MCP scene lifecycle tools + Next.js API routes + editor /scene/[id] route, so an MCP save is directly openable at /scene/<id> without any injection hack. End-to-end verified: 10/10 e2e steps pass. Storage (A1/A2/A3): - SceneStore interface + error classes + slug helpers - FilesystemSceneStore at $PASCAL_DATA_DIR (defaults XDG/~/.pascal) with atomic writes, .index sidecar, optimistic locking - SupabaseSceneStore with scenes + scene_revisions tables, RLS migration SQL, mock-backed unit tests - createSceneStore(env) auto-selects based on SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY MCP tools (A4, A8, A9, A10): - save_scene / load_scene / list_scenes / delete_scene / rename_scene - list_templates / create_from_template (3 seed templates: empty-studio, two-bedroom, garden-house) - generate_variants (7 mutation kinds, seeded RNG, save=true|false) - photo_to_scene (vision sampling → scene graph → save) Editor (A5, A6): - /api/scenes + /api/scenes/[id] with RFC 7232 If-Match locking - /scene/[id] and /scenes route pages with save button, SceneLoader - Removed the window.__pascalScene dev injection hack Security + UX edges (A7, A8): - AssetUrl Zod validator: asset:// blob: data:image/ /path https: (http://localhost for dev) + PASCAL_ALLOWED_ASSET_ORIGINS env allowlist. Hardens scan.url, guide.url, item.asset.src, material.texture.url, MaterialMaps.*Map - Auto-frame camera on empty→non-empty scene transition (camera-controls:fit-scene emitter event) Shared utilities: - rehydrateSiteChildren() extracted to packages/mcp/src/lib/ and used by both create-from-template and generate-variants to work around the SiteNode.children-as-objects vs. ids inconsistency (CROSS_CUTTING §2) - Storage + MCP subpath exports added to packages/mcp/package.json (CROSS_CUTTING §4) Tests: 293 pass / 0 fail across 40 files (was 142 pre-Phase-7). Biome: clean. Phase-7 e2e script at packages/mcp/test-reports/phase7-e2e.ts: MCP HTTP + editor Next.js both point at $PASCAL_DATA_DIR = /tmp/pascal-e2e, save_scene from MCP, GET /api/scenes/<id> from editor server, /scenes list page renders all saved scenes, scene page renders SceneLoader, delete_scene works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
42bd05db9c |
docs(mcp): add 10-agent research on scene-save workflow
10 parallel research agents (R1-R10) investigated whether MCP can save scenes as saveable entities that the user later opens in the editor — rather than injecting via a dev-only window.__pascalScene hook. Key findings: - Editor is already backend-agnostic via onLoad/onSave callbacks (R2,R7) - Current persistence is localStorage-only, single key (R1) - Zero dynamic routes and no backend code yet — env declared (R4,R5) - File import exists as "Load Build" but lacks Zod validation (R6) - MCP-written scenes load cleanly into the editor today (R6, Casa del Sol test already proved this) - Best path: filesystem-handoff this week, Supabase in weeks 2-4, Supabase Realtime for live mode in Q2. Skip Yjs for now. (R8) - 4-5 weeks to private beta, 10-14 to GA (R9) - 10 high-value ideas ranked; "photo -> scene" is the unblocker (R10) SYNTHESIS.md pulls all 10 reports together with the recommended implementation plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a2e287144b |
test(mcp): Casa del Sol — full house built end-to-end via MCP
Design and live-built a 3-bed 2-bath single-story house with pool and
privacy-screened perimeter ("Casa del Sol") using only @pascal-app/mcp
tools. Exercises every mutation tool in a realistic scenario.
- DESIGN.md: blueprint (lot 20x15, building 12x8, 7 interior zones,
pool 5x3, 5 privacy fence segments)
- build.ts: MCP client orchestrator
- scene.json: full exported scene (26.7 KB)
- BUILD_REPORT.md: per-step status + final totals
Final scene: 39 nodes pre-duplicate (1 site, 1 building, 1 level,
9 walls, 9 zones, 6 doors, 6 windows, 5 fences, 1 slab), 76 nodes
post-duplicate_level, valid=true, errors=0.
Surfaced the documented StreamableHTTP single-session limitation:
the HTTP server at :3917 rejected the SDK client's initialize with
"Server already initialized" because the previous T3 session consumed
the transport's one-shot session slot. Build fell back to an in-memory
transport exercising the identical tool surface. Fix tracked for v0.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b37e88cb83 |
fix(mcp): apply_patch preserves schema-defaulted ids in multi-op batches
SceneBridge.applyPatch now threads the Zod-parsed node through to the apply phase instead of the raw input. Previously, when a caller sent a create patch without an `id` field, the schema's objectId default ran during dry-run parsing but only in `res.data`; the apply phase pushed the unparsed `p.node` (no id) to the store, so subsequent tools that walked `level.children` crashed on undefined entries (e.g. duplicate_level -> cloneLevelSubtree -> extractIdPrefix(undefined)). Also adds test-reports/ artefacts from live end-to-end testing: - t1-stdio: 21/21 tools pass via stdio (~106ms) - t2-http: connect/single-session behaviour (HTTP transport quirk documented) - t3-scenario: 2-bedroom apartment built end-to-end — 12/12 steps after this fix (24 final nodes, validate=true, apartment.json exported) - t4-errors: 24/24 invalid-input cases rejected with proper MCP errors - t5-resources-prompts: 4/4 resources, 3/3 prompts, dev server /api/health 200 OK Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a6f1c4140f |
docs(mcp): add PR_DESCRIPTION.md
Full PR description summarising the 9-commit series, tool/resource/prompt inventory, architecture, verification checklist, and follow-ups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fc6bbdf376 |
chore(mcp): add CI workflow and document cross-cutting changes
- .github/workflows/mcp-ci.yml: runs on pushes to main and PRs touching packages/mcp/, packages/core/, or bun.lock. Installs with Bun 1.3.0, builds core then mcp, runs bun test, and runs biome. - packages/mcp/CROSS_CUTTING.md: documents the two additive changes outside the package boundary (core exports map, this CI workflow) plus observations about SiteNode.children inconsistency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3406dad8f5 |
docs(mcp): add README, examples, and changelog
- README.md: install/quick start; configs for Claude Desktop, Claude Code, and Cursor; programmatic usage; tables covering all 21 tools, 4 resources, and 3 prompts; limitations; development commands. - CHANGELOG.md: 0.1.0 entry in Keep a Changelog format. - examples/generate-apartment.md: prose transcript using from_brief to build an 80 m² 2-bed apartment, showing apply_patch, set_zone, cut_opening, validate_scene. - examples/renovate-from-photos.md: prose transcript using the vision tools + renovation_from_photos prompt. - examples/embed-in-agent.ts: compilable TypeScript showing programmatic usage via InMemoryTransport. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
441e97b2b6 |
feat(mcp): add stdio + streamable HTTP transports, CLI, and smoke test
- connectStdio(server): wires the MCP server to StdioServerTransport. - connectHttp(server, port): wires the MCP server to StreamableHTTPServerTransport over node:http. Returns a handle with port and close(). - bin/pascal-mcp.ts: CLI entrypoint with --stdio (default), --http, --port, --scene, --help, --version. Loads node-shims before any core import. Logs to stderr (stdio transport uses stdout for JSON-RPC). - scripts/smoke.ts: end-to-end smoke — spawns the stdio bin, connects as an MCP Client, exercises get_scene, create_level, validate_scene, undo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4dbfbb1e1a |
feat(mcp): add multimodal vision tools via MCP sampling
analyze_floorplan_image and analyze_room_photo defer the vision work to the host via MCP sampling (server.server.createMessage). Validates host capability before calling, fetches URL inputs and base64-encodes them, constrains output to a Zod schema, and returns structured content. No vision model is bundled. 9 tests, all passing via a mocked sampling-capable client. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
570c605446 |
feat(mcp): add resources and prompts
Resources: pascal://scene/current (JSON), /scene/current/summary
(markdown with per-level counts, floor areas, bbox), /catalog/items
(returns catalog_unavailable in headless mode), and the templated
pascal://constraints/{levelId} which exposes slabs + wall footprints
via @pascal-app/core/wall helpers.
Prompts: from_brief (generate scene from a natural-language brief),
iterate_on_feedback (minimal-diff patch proposals), and
renovation_from_photos (orchestrates the vision tools).
17 tests, all passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
58ad89e80b |
feat(mcp): implement 19 scene query and mutation tools
Scene querying: get_scene, get_node, describe_node, find_nodes, measure. Scene mutation (undoable, atomic): apply_patch, create_level, create_wall, place_item, cut_opening, set_zone, duplicate_level, delete_node. Undo/redo: undo, redo. Export: export_json, export_glb (not_implemented stub). Validation: validate_scene, check_collisions. Each tool has: - Exported Zod input + output schemas - register<Tool>(server, bridge) wiring function - Bun test with happy-path + error-path coverage via InMemoryTransport 59 tool tests, all passing end-to-end through MCP protocol. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
07ed429d58 |
feat(mcp): add headless scene bridge with RAF polyfill
SceneBridge class wraps @pascal-app/core's Zustand store for Node, exposing a clean programmatic API for scene load/mutate/export plus Zundo undo/redo. Requires a requestAnimationFrame polyfill loaded before any core import to work around the store's RAF-batched dirty marking. - 51 tests, 99.68% line coverage on scene-bridge.ts - All-or-nothing applyPatch with Zod dry-run validation - Safeguards against prototype-polluting keys in loadJSON - Resolves children through the flat nodes dict (handles the SiteNode.children-as-objects inconsistency) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
eebbef502d |
feat(mcp): finalize scaffolding and factory entry
Adds packages/mcp/.gitignore, src/index.ts (public API barrel), src/server.ts (createPascalMcpServer factory), and a smoke test for the public API. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2ae932f384 |
feat(mcp): scaffold package and confirm headless bridge viability
- Add packages/mcp/ with package.json, tsconfig, PLAN.md, CROSS_CUTTING.md - Extend @pascal-app/core exports map with subpaths (./schema, ./store, ./material-library, ./spatial-grid, ./wall) so Node consumers can skip the graphics-bound systems re-export chain. - Add scripts/spike.ts that proves useScene + temporal work in Node with a requestAnimationFrame polyfill. Spike passes end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |