Commit Graph
991 Commits
Author SHA1 Message Date
Wassim SAMADandGitHub 071319e102 Merge pull request #417 from pascalorg/feat/paint-slots-node-defaults
feat(paint-slots): unified slot defaults + paint for slab, ceiling, wall (phase 5)
2026-06-17 14:49:51 -04:00
Wassim SAMADandClaude Opus 4.8 4818807360 refactor(paint-slots): share scene IBL as viewer SceneEnvironment
Replace the editor-only EditorEnvironment wrapper with a SceneEnvironment
component exported from @pascal-app/viewer, mounted as an opt-in <Viewer>
child (still not baked into the Viewer component). One source of truth the
editor and the community public viewer both inject; embed/thumbnail
surfaces simply don't mount it. Sunset preset at environmentIntensity 0.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:49:35 -04:00
Wassim SAMADandClaude Opus 4.8 37c56788f3 feat(paint-slots): add drei sunset environment as an editor viewer child
Inject an EditorEnvironment wrapper (drei prefiltered sunset HDRI at
environmentIntensity 0.6) as a child of the editor Viewer, not baked into
the Viewer component, so read-only/embed viewers stay lightweight. This
gives PBR metals their reflections and lifts lighting on vertical walls
that flat directional + hemisphere lights cannot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:30:23 -04:00
Wassim SAMADandClaude Opus 4.8 8aa179e9cb feat(paint-slots): per-part paint for windows + doors, chrome/brass, world-scale UVs
Builds on the explicit per-mesh slot tagging (currentDoorSlot/currentWindowSlot):

- Per-part painting: door = panel/frame/glass/hardware, window = frame/glass,
  each independently paintable. The recessed door/window body sits behind the
  wall, so the proud invisible cutout wins the scene raycast over the wall and
  the shared resolveSlotByReRaycast() re-raycasts the kind's own subtree to pick
  the exact part under the cursor (panel↔frame↔glass↔hardware). Hover tracks the
  cursor via a  re-eval (idempotent, no flicker).
- Door frame is its own slot (separate frameMaterial); hardware = new flat
  'metal-chrome'.
- Library defaults (generic): panel/frame -> library:preset-softwhite, glass ->
  library:preset-glass (flipped preset-glass to FrontSide — DoubleSide poisons
  the WebGPU MRT pass; it's the only glass we use).
- Catalog: add flat (non-PBR) 'metal-chrome' + 'metal-brass'; drop metal
  metalness 1 -> 0.6 so metals are lit by existing lights (no env needed).
- World-scale UVs (1 unit = 1m) on door/window box meshes via shared box-uv.ts,
  so finishes tile at real-world scale instead of stretching.
- PaintResolveArgs gains an optional  for subtree re-raycasting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:01:37 -04:00
Wassim SAMADandClaude Opus 4.8 c3bd065f2f fix(paint-slots): disable the CSG cutout's raycast so window/door paint hits the slots
The real interceptor was the 'cutout' mesh, not the hitbox: it's a 1m-deep CSG
helper whose front face sits 0.5m proud of the glass, and current three.js
raycasts invisible meshes — so every paint/hover ray resolved to 'cutout'
(no slotId → role null). Disable its raycast; combined with the hitbox noop,
paint/hover rays now land on the tagged frame/glass (panel/glass) children.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:42:48 -04:00
Wassim SAMADandClaude Opus 4.8 531dbb6e68 perf(post-processing): don't rebuild the pipeline on hover + temp paint debug logs
- post-processing: hoverHighlightMode was a dependency of the pipeline-build
  effect, so every hover rebuilt the entire pipeline. The hover style is already
  pushed to uniforms in a separate effect, so the rebuild was pure waste —
  removed it from the deps (and the build log).
- selection-manager: temporary [paint-debug] logs for window/door hover to trace
  why their paint dispatch drops (to be removed once diagnosed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:36:58 -04:00
Wassim SAMADandClaude Opus 4.8 cebb29460d fix(paint-slots): make window/door paint hits land on the tagged children
The window/door root is an invisible hitbox the system gives full-depth
BoxGeometry; its front face intercepted every paint/hover ray, so the hit
resolved to the hitbox (no slotId) → role null → paint silently disabled.

Disable the hitbox's own raycast in the visual path so R3F's recursive
intersect returns the tagged frame/glass (panel/glass) children instead;
selection still works because those child hits bubble to the root's event
handlers. Restored to the default raycast each build, and kept for 'opening'
windows/doors (no visuals to paint, still need a selectable hitbox). The
'cutout' child is visible=false so the raycaster already skips it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:10:43 -04:00
Wassim SAMADandClaude Opus 4.8 9f1627e923 feat(paint-slots): paintable slots for windows + doors (frame/glass, panel/glass)
Windows and doors build all visuals in their viewer systems from module-global
materials, so this threads per-node slot materials + userData.slotId tags
through those builders without restructuring them:

- window: 'frame' + 'glass' slots. door: 'panel' (body = casing + leaf) +
  'glass'; the opening reveal keeps its own material.
- Each system captures per-frame viewer state, then updateWindow/DoorMesh points
  the builder-facing base/glass materials at the node's resolved slot override
  (recomputed per node, so the next node resets without a restore). Meshes are
  auto-tagged in the shared addBox/addShape helpers by which material they got.
- Textures-off still collapses to the role material (escape hatch); a slot
  override only applies in colored mode.
- Editing a referenced scene material re-dirties the window/door (these systems
  aren't covered by GeometrySystem's scene-material re-dirty).
- New paint capabilities (resolve role from userData.slotId, preview by
  userData.slotId) + capabilities.slots; window/door dropped from the paint
  disabled list. Shared previewSlotByUserData helper.

Defaults unchanged: unpainted windows/doors render exactly as before (the slot
fallback is the existing frame/glass material), so no visual regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 08:54:18 -04:00
Wassim SAMADandClaude Opus 4.8 737c4e9d1d feat(paint-slots): concrete-plate finish + try defaults (wall=concrete, slab=wood plank 48, ceiling=soft white)
- material-library: add 'concrete-plate' KTX2 finish (512, fabric/leather-style
  pipeline) + editor-app texture mirror.
- viewer: shared resolveSlotDefaultMaterial(colour|library ref) so a kind's slot
  default can be a catalog finish, not just a flat colour.
- wall default -> library:concrete-plate (interior + exterior).
- slab default -> library:wood-woodplank48 (slab geometry resolves it via the
  shared helper).
- ceiling default -> soft white #f2eee6 (ceiling renders flat-tinted).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 08:25:48 -04:00
Wassim SAMADandClaude Opus 4.8 967a905e3b feat(paint-slots): unified slot defaults + paint for slab, ceiling, wall (phase 5)
Brings slab, ceiling, and wall onto the unified slot contract the shelf
established, so each declares its paintable slots with a declarative default and
(slab/ceiling) is painted through the registry capabilities.paint dispatch.

- Shared helper packages/nodes/src/shared/slot-paint.ts: a node.slots-based
  PaintCapability factory (commit/resolve/effective generic; preview injected).
  Distinct from surface-paint.ts, which writes the legacy inline node.material.
- slab: schema slots; def.geometry resolves node.slots.surface -> legacy
  material -> declared default, tags the mesh userData.slotId; slabPaint +
  capabilities.slots. Retires DEFAULT_SLAB_MATERIAL in the slab path.
- ceiling: schema slots; material builders extracted to ceiling/materials.ts
  (shared by renderer + paint preview, built BackSide so the hover preview is
  visible from below); renderer resolves the slot; ceilingPaint + slots.
- wall: WALL_SLOT_DEFAULT in core; the viewer's getMaterialsForWall renders an
  unpainted face with its declared default instead of the themed wall role;
  capabilities.slots (interior/exterior). wallPaint's inline interior/exterior
  fields are unchanged (node.slots migration is a later step).
- selection-manager + material-paint: drop slab/ceiling from the legacy
  single-surface arms (now registry-driven).

Behavior change (intended, matches the shelf precedent + the phase-5 plan):
colored-mode UNPAINTED slab/ceiling/wall surfaces now render their fixed slot
default (#e5e5e5 / #f5f5dc / #ffffff) instead of the theme role colour. The
textures-off (monochrome) role collapse is unchanged — the escape hatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 07:51:37 -04:00
Wassim SAMADandGitHub b0f4e1b8ee Merge pull request #415 from pascalorg/feat/paint-slots-phase-4-finish-library
feat(paint-slots): KTX2 finish library — fabric/leather/concrete/metal (phase 4)
2026-06-16 16:14:28 -04:00
Wassim SAMADandClaude Opus 4.8 042f855586 feat(paint-slots): KTX2 finish library — fabric/leather/concrete/metal (phase 4)
Wire KTX2 into the catalog finish path and add 15 textured finishes across four
new families, all as GPU-compressed KTX2 (512px) + a webp picker thumbnail.

- Shared `ktx2-loader.ts`: one KTX2Loader (Basis transcoder) used by both the
  GLB loader and catalog textures; `ensureKtx2Support(renderer)` runs once at
  viewer init (GPUDeviceWatcher) so `.ktx2` finishes load even with no GLB in
  the scene. `use-gltf-ktx2` now reuses the shared instance.
- `materials.ts`: texture loaders pick the KTX2 loader for `.ktx2` urls, the
  image loader otherwise (all three load paths).
- `material-library.ts`: 15 entries (fabric ×6, leather ×2, concrete ×4,
  metal ×3). Neutral albedo for tinting, `flipY: false` (compressed textures
  can't flip), `repeat` per real-world tile size, metals `metalness: 1`.
  Normals encoded UASTC, data maps ETC1S/linear.

Assets generated from raw sources via the new community
`scripts/build-material-textures.ts`. 1024/256 tiers + raws kept out of git.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:37:23 -04:00
Wassim SAMADandGitHub 0e8a8c3d14 Merge pull request #414 from pascalorg/feat/paint-slots-material-catalog
feat(paint-slots): material catalog families + 45-color palette + preset-style picker
2026-06-16 13:29:32 -04:00
Wassim SAMADandClaude Opus 4.8 dbefcf763c feat(paint-slots): recurate material catalog into families + expand colors
- Replace location-based categories (wood/flooring/roof/other) with material
  families: colors, wood, stone, brick, tile, concrete, metal, fabric, leather,
  roofing, ground, glass
- Add MaterialSurface tags (floor/wall/ceiling/roof/furniture/outdoor); retag all
  65 existing finishes; ids unchanged so library refs keep resolving
- Expand curated colors 15 -> 45, ordered by hue; unify on catalog library items
  and retire CURATED_COLORS (picker reads the colors family)
- Paint picker: wrapping rounded category chips (was horizontal scroll), empty
  families auto-hide, preset-style swatch cards (name label, selection-only ring,
  hover bg + SFX)

Phase 4 (finish-library content) of editor-paint-slots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:28:31 -04:00
Wassim SAMADandGitHub 8a0f6cfe27 Merge pull request #412 from pascalorg/feat/paint-slots-phase-5-shelf-slots
feat(paint-slots): paintable slots on the procedural shelf (phase 5)
2026-06-16 13:08:12 -04:00
Wassim SAMADandClaude Opus 4.8 e079615196 fix(shelf): floor-anchor cubby no-bottom divider + embed bookshelf divider depth
- Cubby without a bottom board: the lowest cell opens onto the floor, but its
  column divider still started at the (missing) bottom board's top, leaving it
  floating ~thickness above the floor. Anchor it at y=0 when there's no bottom.

- Bookshelf full-height divider: it crosses the continuous shelves, so it can't
  share their depth plane (proud at the front, coplanar with the back panel —
  z-fighting down the centre back). Recess its depth to sit fully INSIDE the
  boards' depth: embedded at each shelf crossing (board occludes it) and tucked
  inside the back panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:05:47 -04:00
Wassim SAMADandClaude Opus 4.8 c68dfa2095 fix(shelf): match cubby divider depth to the boards (no front overflow, no back seam)
The cubby column divider was full-depth while the boards are depth-recessed,
so it (a) sat proud of the shelf fronts — the earlier Y-tuck then made it poke
past the top/bottom boards — and (b) shared the back panel's rear plane,
z-fighting down the centre of the back.

Give the divider the same depth recess as the boards (via boardGeometry) and
drop the Y-tuck: it now sits flush with the shelf fronts and its back tucks
inside the back panel. Flush top/bottom is fine — those board faces are
back-to-back, not co-facing, so they don't fight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:57:00 -04:00
Wassim SAMADandClaude Opus 4.8 c717c0c13d fix(shelf): tuck cubby cell dividers into the boards they meet
Cubby per-cell dividers landed exactly flush on the board below and under the
board above — coplanar faces that shimmer and read as merged into the shelf.
Extend each divider 1mm into both boards (centre unchanged) so it tucks under
the top board and onto the bottom board with a solid, seam-free join.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:43:13 -04:00
Wassim SAMADandClaude Opus 4.8 b602e04b70 fix(shelf): stop top-face z-fighting + avoid plate/side gaps
Two follow-ups to the plate-recess fix:

- Top surface: full-height frame members that pass UNDER the top board
  (back panel, bookshelf column dividers, corner posts) had their top face at
  unitHeight — coplanar with the top board's top — so they z-fought through it
  (the seam on the top). Drop their top 1mm (cappedFrameY); bottoms stay on the
  floor so nothing floats.

- Plate width: the previous pass recessed every board's width, which opened a
  1mm gap where boards ABUT the side panels (cubby / bookshelf-with-sides).
  Width is now recessed only where boards span OVER posts (open-rack /
  no-sides bookshelf); abutting boards keep full width and meet the sides
  flush. Depth stays recessed everywhere (gap-free, fixes the back-panel fight).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:19:17 -04:00
Wassim SAMADandClaude Opus 4.8 e17c298284 fix(shelf): recess plates 1mm to stop z-fighting with the frame
Open-rack / no-sides shelves span their boards across the full footprint, so a
board's end / front / back faces land exactly on the corner posts' outer faces
(and, with a back panel, on its rear face). Coplanar surfaces the depth buffer
can't order flicker as z-fighting (visible as seams where plates meet posts).

Recess every board 1mm on width + depth via a `boardGeometry` helper so each
plate sits just inside the frame — the meshes still overlap (no visible gap),
but no two faces are coplanar. Frame parts (posts/sides/back/dividers/brackets)
keep their full size as the silhouette.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:10:34 -04:00
Wassim SAMADandClaude Opus 4.8 101341d98d feat(paint-slots): paintable slots on the procedural shelf (phase 5)
Proves the unified (nodeId, slotId) slot contract on a procedural generator,
beyond items and walls. A shelf now exposes three paintable slots — shelves /
frame / back — painted through the same PaintCapability dispatch and the same
node.slots: Record<slotId, MaterialRef> shape items use.

Foundation (shared, reusable by future procedural kinds):
- core: SlotDeclaration type + capabilities.slots(node) registry declaration;
  GeometryContext gains `materials` so a pure builder can resolve scene:<id>
  slot refs without importing useScene.
- viewer GeometrySystem: threads the scene material library into every builder
  ctx, and re-dirties (bypassing the geometryKey skip) any geometry node that
  references a scene material when that material changes — so editing a custom
  colour propagates to every shelf using it, matching items.

Shelf:
- schema: slots: Record<string, MaterialRef> (mirrors ItemNode).
- geometry: per-slot material resolution (slot override -> legacy whole-shelf
  -> declared default colour); every mesh stamped with userData.slotId;
  DEFAULT_SHELF_MATERIAL retired (declared default gives identical off-white).
- paint.ts: PaintCapability (resolveRole from userData.slotId, scene-material
  commit for one-off colours, preview restricted to __fromGeometry meshes so
  hosted items aren't ghosted, getEffectiveMaterial incl. legacy fallback).
- definition: paint + slots capabilities; slots folded into geometryKey.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:59:59 -04:00
Wassim SAMADandGitHub 89a7232a67 Merge pull request #410 from pascalorg/feat/paint-slots-phase-4-uv-worldscale
feat(paint-slots): world-scale pool slab floor UVs + metre UV contract
2026-06-16 09:57:30 -04:00
Wassim SAMADandClaude Opus 4.8 937cf02aff feat(paint-slots): world-scale pool slab floor UVs + document the metre UV contract
Pool slabs (generatePoolGeometry) were the one procedural surface emitting
normalized [0..1] floor UVs, so a textured finish stretched to fit the pool
instead of tiling at real-world scale like every other surface. Switch the
floor to shape-space metres (x, -z) — the same mapping generatePositiveSlab-
Geometry already uses for its caps. Pool walls were already in metres.

Also document the contract in wiki/architecture/materials-and-themes.md: every
procedural surface generates UVs in metres (1 UV unit = 1 m), GLB slots follow
the same ~1 unit/m authoring convention, and a catalog material's `repeat` is
therefore a per-material world-scale setting (tiles per metre), identical for
every surface that uses it — never per-item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:51:54 -04:00
Wassim SAMADandClaude Opus 4.8 138543ea8c chore(render-modes): use US spelling "colors" in Materials toggle detail
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:42:26 -04:00
Wassim SAMADandGitHub 0b25e4f020 Merge pull request #409 from pascalorg/feat/colored-monochrome-items
Colored/Monochrome: items show real materials + Render-menu toggle
2026-06-15 17:44:55 -04:00
Wassim SAMADandClaude Fable 5 4411b8ab5f feat(render-modes): add Colored / Monochrome toggle to the Render menu
The textures axis had no UI control. Add Colored (textures on — show item
materials, textures, vertex colours) / Monochrome (textures off — flat clay
by surface role) options to the Render dropdown, wired to useViewer.textures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 17:41:38 -04:00
Wassim SAMADandClaude Fable 5 3d5a36c73d feat(render-modes): items show real materials in colored mode (not flat clay)
In colored mode (textures on), every item now renders its authored material
— textures, vertex colours, and default colours — instead of being stripped
to the uniform off-white baseMaterial. AI-generated and vertex-coloured items
show their real appearance; slot resolution (override -> curated -> authored)
is unchanged. Monochrome (textures off) still collapses to the furnishing
clay role colour (the escape hatch). Drops the now-unused isAuthored marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 17:17:44 -04:00
Wassim SAMADandGitHub f740f5993b Merge pull request #406 from pascalorg/feat/paint-slots-phase-2a
Paint slots 2a: render pascal_material curated defaults
2026-06-15 11:36:14 -04:00
Wassim SAMADandClaude Fable 5 d52cbe6757 feat(paint-slots): read pascal_material glTF extras as curated slot default
Insert the plan's middle resolution step for item slots: a curated
default baked into the GLB as a per-material 'pascal_material' extra
(three's GLTFLoader copies material extras to userData). Resolution per
slot is now: node.slots[slotId] override -> pascal_material curated
default -> authored GLB material. Bare ids resolve as library:<id>;
unresolved refs fall through. Runtime reads only the GLB (no DB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 11:32:05 -04:00
Wassim SAMADandGitHub e0179c480a Merge pull request #404 from pascalorg/feat/paint-slots-phase-1
Paint slots: phase 1 + paint unification
2026-06-15 10:59:49 -04:00
Wassim SAMADandClaude Fable 5 2ac9f6762d feat(paint-slots): persist scene materials + collections via autosave
The editor autosave/load is the single source of truth for what travels
with a scene. Include document-level state (collections, materials) so it
survives reload in every embedder (community cloud save + standalone
localStorage):

- SceneGraph carries optional collections + materials.
- useAutoSave triggers on collections/materials reference changes (not just
  nodes) and writes them into the saved graph + the unload flush.
- applySceneGraphToEditor restores them via setScene's extras arg on load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 10:43:09 -04:00
Wassim SAMADandClaude Fable 5 b3d840a39b feat(paint-slots): scene-material library panel + Colors swatches
Phase 3 paint UI.

- Extract reusable MaterialPropertiesEditor (shared by the custom active-
  paint editor and the scene-material editor).
- Curated Colors swatch row in the picker; catalog presets fork-on-tweak
  by seeding a custom material from the entry's previewColor.
- Scene materials section in MaterialPaintPanel (shown once any exist):
  list with swatch, inline rename, 'used by N parts', paint-with,
  edit (live-propagates to every referencing part via the renderer's
  sceneMaterials dep), duplicate, delete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 10:13:05 -04:00
Wassim SAMADandClaude Fable 5 7afb286e47 feat(paint-slots): authored item materials + unified slot painting
Phase 1 + paint unification of the paint-slots plan.

- core: scene-material data layer (materials map mirroring collections,
  undo/partialize/setScene full-graph support), SceneMaterial schema,
  scene:/library: MaterialRef helpers + parseMaterialRef, slot id helpers
  (deriveSlotId/slotLabelFromId), slots map on ItemNode, hitObject on
  PaintResolveArgs, optional PaintCapability.commit.
- viewer: resolveMaterialRef (library:/scene: -> three material, null on
  dangling).
- nodes(item): renderer keeps authored GLB materials for slot-authored
  assets and applies per-slot overrides per-instance (never mutates the
  shared cached GLB); textures-off still collapses to furnishing role;
  non-authored items unchanged. Item paint capability + registration.
- editor: item joins the unified (nodeId, slotId) paint dispatch; item
  paint target + slot reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 10:01:08 -04:00
cf24b62c44 fix(viewer): UI flicker on camera move from interactive overlays + dirty-mark leaks (#401)
* chore: sync bun.lock with 0.9.1 workspace versions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(viewer): stop interactive overlays from starving frames and flickering the UI

Every interactive item mounted a drei <Html occlude> overlay
unconditionally — invisible (opacity 0) when no zone was selected, but
still alive. With `occlude` as a bare boolean, drei raycasts the entire
scene per overlay on every camera-move frame, and rewrites each
element's z-index while toggling display when the occlusion flips. On
scenes with hundreds of interactive items (recessed lights, ceiling
fans) this starved the frame budget and made the whole DOM UI blink
during camera moves while the WebGPU canvas stayed healthy.

Overlays now mount only while a zone is selected and the item sits
inside its polygon, fade in/out over 300ms (the child components stay
rendered so the exit transition can play before the <Html> unmounts),
and drop `occlude` entirely. eps=-1 works around a drei mount bug: its
mount path writes the element transform without the distanceFactor
scale, and with a static camera the eps guard never re-applies it, so
freshly mounted overlays stayed mis-scaled until the camera moved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(viewer): break down dirty nodes by kind in the perf overlay

DIRTY now reads e.g. "29 (12 wall, 9 ceiling, 8 item)" — sorted by
count, only non-zero kinds, with a "missing" bucket for dirty ids whose
node no longer exists. Makes dirty-mark leaks attributable at a glance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(core): skip dirty marks for kinds with no dirty consumer

dirtyNodes is consumed by GeometrySystem (def.geometry),
FloorElevationSystem (capabilities.floorPlaced), and the legacy
per-kind viewer systems. Site, building, level, zone, and guide match
none of those, so their marks were never cleared: they accumulated for
the whole session (every child create/delete dirties its parent),
permanently defeated every consumer's empty-set early exit each frame,
and polluted the perf overlay's DIRTY readout.

NodeDefinition gains an explicit dirtyTracking?: boolean opt-out
(default tracked — no derivable predicate exists since wall's dirty
consumption lives in the viewer while zone/guide/level declare
def.system for unrelated per-frame work). markDirty consults the
registry; the five structural kinds opt out.

Also fixes a second leak: deleteNodesAction never removed deleted ids
from the dirty set, and every consumer skips missing nodes without
clearing them, so marks on deleted nodes lived forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: replace concise-arrow forEach with for...of in deleteNodesAction

biome's useIterableCallbackReturn rejects forEach callbacks that
implicitly return a value (Set.add / clearDirty).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:05:06 -04:00
Aymeric RabotandGitHub 5411f5abc8 fix(editor): improve guided manipulation and snap affordances 2026-06-11 23:59:15 -04:00
Aymeric RabotandGitHub aab48e053f fix(editor): harden editor interactions and WebGPU rendering
Fix editor bug sweep regressions, WebGPU CSG/material crashes, Shift snap bypass behavior, arrow handle drag projection, and the wall preview null guard covered by the Sentry follow-up PRs.
2026-06-11 13:03:34 -04:00
Aymeric RabotandGitHub 2d2dba5dba Merge pull request #395 from pascalorg/feat/editor-ux-rendering-placement
editor: roof wall openings — doors/windows/items on roof-segment wall faces
2026-06-10 14:39:37 -04:00
Aymeric RabotandClaude Fable 5 ee7bb39605 style: biome format/import-order for refactored tool files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:36:19 -04:00
Aymeric RabotandClaude Fable 5 aa3b0ef758 refactor: release-review cleanup for roof wall openings
Dual review pass (Claude multi-angle + Codex release-quality). One
correctness fix and the agreed do-now cleanups:

- fix: clone-scene-graph remaps roofSegmentId like wallId in both
  clone paths — duplicated scenes/levels kept pointing roof-hosted
  children at the original segments.
- extract the settled, stateless roof target/cursor math shared by the
  four door/window tools into shared/roof-wall-opening-placement.ts
  (resolveRoofWallOpeningTarget + getRoofWallOpeningCursorPose +
  worldToSelectedBuildingLocal); tools keep the stateful lifecycle
  (drafts, undo/temporal, commit field lists). −199 net lines.
- rename host-generic state: currentWallId→currentHostId,
  markWallDirty→markHostDirty (they hold segment ids too); capability
  cascadesViaHostSegment→dirtyHandledByOwnSystem (behavior-facing,
  before the public API hardens).
- drop getRoofAccessoryKinds from core's public API — its only caller
  was the standalone Build tab, which now enumerates the registry
  inline with its app-specific filter.
- window move-tool uses the shared stripPlacementMetadataFlags; stale
  "segment-local" comment fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:33:13 -04:00
Aymeric Rabot b8dfa90762 Merge remote-tracking branch 'origin/main' into feat/editor-ux-rendering-placement 2026-06-10 14:21:35 -04:00
Aymeric Rabot 07d01b18e7 feat(editor): action menu tracks live geometry changes
Floating action menu re-derives its anchor when the selected node's
geometry rebuilds (position/index attribute versions as the key) and
accounts for roof height via getActiveRoofHeight + effective nodes.
2026-06-10 14:21:24 -04:00
Aymeric RabotandClaude Fable 5 7879b83df3 feat: face-frame hosting for roof wall children + items on roof walls
Wall-mounted items join doors/windows on roof-segment wall faces, and
the storage model moves to FACE-LOCAL coordinates so hosted children
track segment edits live.

- Children store roofFace + position [u, v, z-from-mid-plane] with
  rotation 0 in-frame — the exact wall-child conventions (the wall
  volume's mid-plane lands on the nominal footprint). A shared
  <RoofFaceHostFrame> derives segment pose + face frame from the
  live-override-merged segment: children follow resize handle drags in
  real time and never jump on commit. No re-anchor cascade needed —
  position is authoritative, the frame is derived. migrateNodes
  converts branch-era segment-local data.
- Items: roofWallStrategy + roof:* handlers in the placement
  coordinator (surface 'roof-wall'), Shift free-place normalized with
  walls, ItemSystem wall-side push extended to segment hosts, correct
  2D plan glyphs via face→segment→roof pose composition. The roof
  hit resolver + overlap guard moved to @pascal-app/editor (the
  coordinator lives there; nodes already depends on editor).
- Cuts: subtractAccessoryCuts extracted and applied in BOTH the
  merged-shell and per-segment CSG paths (full edit mode / painted
  segments used to lose every hole), built from the CURRENT host
  geometry and live-effective children so holes follow segment and
  opening drags.
- Handle rig: the grandparent portal now maps the node's world pose
  into the portal frame instead of composing parent+node registry
  poses — correct for any nesting (the face-frame group broke the
  old assumption), identical for walls.
- Host-field hygiene: useDraftNode.commit/adopt and the window panel
  duplicate forward roofSegmentId/roofFace/wallId; every roof↔wall
  re-anchor clears and every revert restores them.

Codex-reviewed (design consultation, adversarial rounds on the
replaced cascade and on this refactor); frame conventions locked by
unit tests. Record: private-editor plans/editor-roof-wall-openings.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:21:14 -04:00
Aymeric Rabot 5fd9be05df Fix spawn floorplan move handle 2026-06-10 14:11:50 -04:00
d36e8fcc78 fix: zone grid snapping, shelf position panel, deselect on item placement (#394)
* fix: zone grid snapping, shelf position panel, deselect on item placement

- Zone tool reads the editor's gridSnapStep (0.5/0.25/0.1/0.05) instead
  of a hardcoded 0.5 for both cursor move and click snapping.
- Shelf inspector gains a Position group (vec3 X/Y/Z sliders), matching
  the item panel.
- Item catalog clears the viewer selection before arming placement so
  shortcuts (rotate & co) don't hit both the ghost and the selected node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: move level display naming into @pascal-app/core

getDefaultLevelName / getLevelDisplayName ("Ground Floor" / "Floor N" /
"Basement N") lived in packages/editor's internal lib, so viewer-only
surfaces couldn't reach them and fell back to hand-rolled "Level N"
labels. The helpers are pure domain logic, so they move to core and
export from its barrel; the editor package's seven call sites now import
them from there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:56:24 -04:00
Aymeric Rabot 3a1ba56246 Polish editor interaction behavior 2026-06-10 11:28:12 -04:00
7539387ff5 editor: fix preset double-commit ghosts, show compass in all view modes (#393)
* fix: guard preset placement against double commit from duplicate click events

A single physical click reaches commitAtCursor twice: node clicks are
synthesized on pointerup (use-node-events) while grid:click rides the
native click event from a canvas DOM listener (use-grid-events) that
deliberately ignores stopPropagation. The second pass found the fresh
draft already deleted and took the orphan re-create path, minting a
hidden ghost copy, replaying the placement SFX, and abandoning the
re-armed clone as a second ghost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: show the compass in all view modes via the viewer-area container

The floorplan compass now portals onto the always-visible viewer area
instead of living inside the (display:none in 3D) floorplan pane. The
2D/3D navigation poses already sync through navigationSyncPose, so the
needle and align-to-north stay correct in 2d, 3d, and split alike.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 09:05:00 -04:00
MateoSaettoneandGitHub 11135b608b fix: respect imperial units in wall panel (#321) 2026-06-10 07:48:34 -04:00
Aymeric RabotandClaude Fable 5 bdfee058bd feat: doors and windows on roof-segment wall faces
Openings now host on the walls a roof segment generates — the base
walls under the roof and the coplanar gable/shed/gambrel end faces, so
a window can sit in a gable pediment.

- core: roof-segment-walls.ts models the four vertical faces as 2D
  frames (u along face, v height) with convex profile polygons that
  mirror the wall volume getRoofSegmentBrushes builds; rect-in-profile
  clamping and anchored resize limits via half-plane algebra.
- schemas: optional roofSegmentId on door/window; position is the
  segment-local wall mid-plane center, rotation[1] the face yaw.
- cut: reuses capabilities.roofAccessory.buildCut; new cutScope: 'wall'
  subtracts from the wall brush only. cascadesViaHostSegment keeps the
  roof-merge loop from consuming door/window dirty marks (their own
  systems cascade via parentId).
- tools: roof:* handlers in door/window tool + move-tool (the Build-tab
  preset path), with roofSegmentId cleared/restored across every
  roof<->wall re-anchor and revert; shared hit resolver normalizes
  normals through world space (merged mesh vs painted segment frames).
- fix: RoofSystem no longer rebuilds per-segment CSG in accessory-reveal
  mode — the uncut rebuild used to draw over the merged shell's fresh
  opening until deselect.
- fix: the walkthrough collider world now prunes by renderer-effective
  visibility; stale uncut segment CSG inside the hidden segments-wrapper
  blocked the player at openings the merged shell had cut through.

Known gap: painted segments render per-segment CSG without accessory
cuts (pre-existing, also affects skylight/dormer). Twice Codex-reviewed;
details in private-editor plans/editor-roof-wall-openings.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:39:13 -04:00
Aymeric RabotandClaude Fable 5 1487328ec7 fix: never drop walkthrough player below the site ground plane
The site ground collider was sized to the scene footprint with a 30 m
minimum — exactly the default site polygon, so one step past the site
boundary left nothing under the character controller and the player
fell into the void. The ground collider now extends 2 km (still a
single BVH box) so the ground plane acts unbounded, and a respawn net
in the first-person frame loop recovers the controller if it ever ends
up below every collider (e.g. scenes with no site node), preferring the
live spawn node over the mount-time start position.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:14:38 -04:00
github-actions[bot] 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 2026-06-10 03:16:48 +00:00