Wall and fence endpoint-move affordances hard-snapped to the grid via a
hardcoded WALL_GRID_STEP and always ran Figma line-alignment, ignoring the
active snapping mode. Now:
- grid step follows getSegmentGridStep() (0 outside grid mode), so lines /
angles / off no longer force a grid snap the mode chip says is inactive;
- Figma alignment is gated on isMagneticSnapActive() (the lines mode);
- angles mode angle-locks the endpoint off the fixed corner (free length),
mirroring the draft tool;
- fence drops its legacy Shift-bypass to match the wall's unified model
(Alt stays as linked-segment detach).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2D affordance dispatcher (`startAffordanceDrag`) now begins the matching
reshaping interaction scope (boundary / hole / curve / endpoint) on pointer-down
and tears it down on release/cancel, matched by node id. This makes the
contextual snapping HUD show the right chip during polygon vertex/edge and wall
endpoint/curve edits, and lets `getActiveSnapContext()` resolve the correct
per-context snapping mode the affordance snap math already reads.
Wall creation is owned by the 3D `WallTool`, which is dead in 2D-only view
(canvas `display:none`). Mirror the slab/ceiling 2D-only committers: commit
locally via `createWallOnCurrentLevel`, gated on `viewMode === '2d'`, chaining
the next segment from the committed wall's resolved end. Split/3D keep their
single-owner tool commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same fix as slab: ceiling is committed by its 3D registry tool, dead in 2D-only
view. Commit it from the panel on both close paths, gated to viewMode==='2d'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slab is committed by its 3D registry tool, which accumulates the grid:click
vertices the 2D panel emits and commits on close. That path is dead in 2D-only
view — the 3D canvas is display:none, so the tool never commits and the slab is
never created (split/3D work because the 3D side is live). Mirror the zone
pattern: the panel commits the slab itself on both close paths (double-click +
click-first-vertex), gated to viewMode==='2d' so split/3D keep their single-owner
tool commit (no double-create).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shared polygon-vertex affordance snapped via snapPointToGrid(rawPoint), whose
default step is the hardcoded WALL_GRID_STEP (0.5m) — so slab/zone/ceiling vertex,
edge, and add-vertex edits always quantized to half-meters regardless of the
active mode OR the user's grid-step setting (plan open bugs #1-2). Use the
mode-aware getSegmentGridStep() (0 in non-grid modes) so grid quantizes to the
live step, lines/off pass through to the wall-snap/alignment resolver. Drop the
legacy shiftKey bypass from the slab/ceiling magnetic resolvers (they already
gate on isMagneticSnapActive).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Perf: move the per-move wall/fence/roof draft END points into
useFloorplanDraftPreview; a new FloorplanLinearDraftLayer leaf owns the live
draft polygon + fence segment + wall measurement, subscribing to the store. The
shared FloorplanDraftLayer keeps only the per-click anchors. Wall/fence/roof
drafts now have zero per-move panel setState — buttery smooth like slab/zone.
Parity: migrate the remaining legacy Shift=bypass paths to the unified
mode-driven model. roof (move + click) honored only always-grid + bypassSnap —
now grid/lines/off (footprint → no angle). wall + fence click-commit still used
the legacy bypass while their move-preview didn't — now consistent. Wall Alt
stays 'commit single wall' (open product decision, untouched).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The polygon-draft snap path used the legacy model: bypassSnap = shiftPressed and
angleSnap = pointCount > 0 && !bypassSnap, so the 15deg angle lock engaged after
the first vertex regardless of mode — hijacking grid/lines/off into angle-snap
even though the HUD chip showed the right mode.
Migrate all three placement paths (move preview, single-click vertex, double-
click close) to the unified model: angleSnap = isAngleSnapActive(); grid flows
through snapToHalf (step 0 in non-grid modes); wall-snap/alignment already gates
on isMagneticSnapActive(). Behavior now matches the chip — grid quantizes,
angles locks 15deg rays, lines snaps to walls/alignment, off is free. Drop the
now-dead bypassSnap param from snapPolygonDraftPoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2D build/edit tools republish the snapped cursor point AND the screen-space
coordinate-badge position on every pointer move. Both lived in FloorplanPanel
useState, so each move re-rendered the whole ~310ms panel (the badge fires on
every pointermove while any build tool is active — the dominant culprit).
Move both into a useFloorplanDraftPreview store written via getState() (no panel
re-render); render the crosshair + live polygon-draft edge from a
FloorplanDraftCursorLayer leaf and the coordinate badge from a
FloorplanCursorIndicator leaf, each subscribing to the store. Same store+leaf
pattern as the stair build preview. Slab/zone/ceiling drafts now have zero
per-move panel setState. Wall/fence/roof draftEnd are follow-up slices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stair tool held its 2D build preview in FloorplanPanel useState, so every
grid:move re-rendered the whole ~120-220ms panel. Move the preview into a
dedicated useStairBuildPreview store written via getState() (no panel re-render)
and render it from a FloorplanStairBuildPreviewLayer leaf that subscribes to the
store directly — the same pattern that keeps column/elevator placement smooth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate roof/stair/elevator draft tools + the 2D floorplan move overlay off the
legacy Shift/Alt=bypass model onto mode-driven snapping (isGridSnapActive /
isMagneticSnapActive); Alt dropped (no validity gate). stair/elevator now use the
live grid step.
These three are placed as footprints, not directional draws, so the angle-lock
mode was meaningless: add NodeDefinition.snapDraftDirectional (default true; false
for roof/stair/elevator) so their draft resolves to the no-angle 'polygon' context
(grid / lines / off). snapContextOf takes an injected draftDirectionalOf, like
profileOf. Add toolHints to stair/elevator so they route through the contextual
HUD and show the snapping chip. Fix one stale Alt-bypass comment in the item
placement coordinator (#9: already force-only). +snapping-mode test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 5 bespoke MEP movers (duct/pipe-segment, liquid-line, lineset, duct-fitting)
now read the active snapping mode (isGridSnapActive / isMagneticSnapActive)
instead of shiftKey=bypass. The moving scope already carries the node
(setMovingNode → begin('moving')), so the per-kind context resolves with no
extra wiring. Grid and alignment are now independent reads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate all 9 MEP kinds' placement tools onto the unified snapping model:
declare snapProfile ('item' for point-placed hvac-equipment / duct-terminal /
duct-fitting / pipe-fitting / pipe-trap; 'structural' for directional runs
duct-segment / pipe-segment / liquid-line / lineset), and replace the legacy
shiftKey-bypass reads with mode-driven isGridSnapActive / isMagneticSnapActive /
isAngleSnapActive. For runs the 45° lock becomes the cyclable 'angles' mode;
Alt stays the vertical-riser modifier (run drafting has no validity gate to
force). Port mating gated on "mode != off". Dropped stale "⇧ smooth/free" hints.
The bespoke MEP move-tool/selection (endpoint) tools stay on the legacy model —
they use setMovingNode(null) so no moving-scope context resolves yet; migrating
them needs scope-wiring first (follow-up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 3D MoveDoor/MoveWindow tools wrote useScene every frame during a move
(freeFollowAt + applyPreview alternating): the wall:move (R3F) / grid:move
(DOM) de-dup compared event.timeStamp across two event systems with different
clocks, so it never matched and the floor free-follow ran during on-wall
slides too, ping-ponging the host and churning the nodes ref → framerate
collapse in both 2D and 3D. Replace it with a single-clock wall-ownership
window (performance.now, ~4 frames): the floor follow stands down while a
wall/roof hit is fresh. On-wall slides now write no scene per frame (mesh +
useLiveTransforms only). Lower the live wall-cutout throttle 120→60ms now that
the per-frame churn is gone.
Also completes the door/window modifier-model migration (#10): Shift=cycle /
Alt=force-place, fully mode-driven snap, snapProfile:'item'; exclude
ground-line candidates from along-wall opening alignment; emit the move SFX
once per snapped step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate the remaining floor-placed kinds onto the unified snapping/modifier
model and generalize floor collision so any solid floor kind blocks any other.
- shelf/column/spawn declare `snapProfile: 'item'` → contextual snapping chip,
Shift=cycle, Ctrl=grid step during placement; their tools read the active
mode (grid/lines/off) instead of legacy Shift/Alt bypass; spawn fresh
placement now respects alignment ("lines") like its move.
- Resize/radial handles claim the handle-drag scope (new RESIZE_HANDLE_DRAG_LABEL)
so the HUD shows no select-mode shortcuts mid-resize.
- Column move migrated to the generic MoveRegistryNodeTool (declare `movable`,
drop the bespoke move-tool) — gains mode-driven snapping, alignment, R/T,
slab lift, grid SFX, and the collision box for free. 2D move still routes
through `floorplanMoveTarget`.
- Cross-kind floor collision: new declarative `FloorPlacedConfig.collides`
(item/shelf/column opt in; spawn/MEP/stair stay off). `canPlaceOnFloor` now
treats every colliding floor kind as an obstacle (was item-only), reading the
declarative footprint; the generic move tool's red/green placement box gates
on `collides`. Column footprint uses the visible `columnFootprintHalf` extent
so the box/slab-lift/collision track the real (round/square) column size.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generify the snapping/modifier HUD off the FSM scope and node declarations
instead of wall-creation-shaped, leaking pills.
- Per-context snapping (`snappingModeByContext`, persisted): wall / item /
polygon mode-sets with exclusive modes (grid | lines | angles | off), each
doing exactly what its chip says. Context is node-declared via the new
`NodeDefinition.snapProfile` ('item' | 'structural'); the resolver maps
(profile × action) → context with no per-kind switch.
- Scope-driven HUD: helper-manager reads the interaction scope; reshaping
(endpoint/curve/boundary) and item move get their own chip, no select-hint
leak. Rotate R/T rounds to 45°; Alt = force-place only (hidden for
structural kinds); Shift = cycle everywhere.
- Slab/ceiling drafting: Shift=cycle, mode-aware grid/angle, Enter finishes
(minDraftVertices); polygon boundary vertex/edge drag begins a reshaping
scope. Fix grid/angle being ignored on boundary edit + slab creation:
make resolveSurfacePlanPointSnap exclusive (alignment gated on magnetic) so
grid/angles keep the snapped fallback instead of the raw cursor.
- Painter application scope: node-derived (single/object/matching/room) from
the hovered node, cyclable via Shift, single-source HUD chip.
- Remove the redundant GridSnapControl from view-toggles (grid step lives in
the contextual HUD now).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 7th and last of the legacy interaction flags. The node being placed/moved
now lives inside the interaction scope's `placing`/`moving` variant (carried
inline, since fresh-placement/duplicate drafts aren't in the scene yet), read via
`useMovingNode()` / `getMovingNode()` / `movingNodeOf(scope)`.
- scope.ts: `placing`/`moving` carry `node: AnyNode`; add `movingNodeOf`.
- use-interaction-scope.ts: `useMovingNode` (hook) + `getMovingNode` (imperative);
no useRef snapshot needed — the node is set once at `begin`, stable for the gesture.
- use-editor.tsx: drop the `movingNode` field + the `set({ movingNode })` writes.
`setMovingNode` still drives the scope and still sets `movingNodeOrigin` /
`placementDragMode`, so cross-store subscribers keep firing. Param + ~90 call
sites unchanged.
- migrate ~17 reader sites to `useMovingNode()` / `getMovingNode()`; drop
`movingNode` from lib/scene.ts; export `movingNodeOf`.
Every interaction flag is now derived from the single authoritative scope; only
`movingNodeOrigin` + `placementDragMode` intentionally remain as useEditor flags
(they outlive the scope / gate companion behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes for the split-view FPS cliff when moving a wall or opening:
- wall/move-tool: publish the live preview to useLiveNodeOverrides instead of
writing useScene.updateNodes every frame. The store's `nodes` ref no longer
churns per frame (which had re-rendered every useScene(s => s.nodes) subscriber
app-wide). Matches the existing 2D wall drag + 3D wall-system override pattern;
the final plan still commits atomically as one undoable change.
- floorplan-registry-layer: replace the single global siblingEpoch with a
per-node epoch bumped only for the nodes affected by the live drag (dragged
wall -> walls at its old + new junctions + child openings; door/window -> host
wall; gutter -> roof-peer gutters), unioned with the previous frame's live set
so a cancelled drag reverts. Dragging one wall/opening now rebuilds a handful
of geometries instead of all the level's walls + openings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate `curvingWall`, `curvingFence`, `movingWallEndpoint`, `movingFenceEndpoint`
(and their setters) off `useEditor` onto the authoritative interaction scope.
The `reshaping` scope variant gains an `endpoint` discriminator; existence
checks read `useIsCurveReshape()` / `useEndpointReshape()`, and the few sites
that need the node (affordance-tool mounts, wall-vs-fence type checks) read it
from `useReshapingNode()` — a frozen drag-start snapshot, mirroring the old
flags so the tools' own per-frame writes don't feed back. `MovingWallEndpoint`
/ `MovingFenceEndpoint` move to the kind-owned tools that consume them.
`editor-api` is simpler: endpoint engagement is kind-agnostic, and the
`engageMove` reshape clears are gone (the scope is single-owner).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the two pure-mirror interaction flags off `useEditor` onto the
authoritative `useInteractionScope`. Both carried payloads byte-identical to
the scope union, so this is a zero-behaviour-change refactor: readers use the
reference-stable `useEditingHole()` / `useActiveHandleDrag()` hooks (or the
`getEditingHole()` imperative read), and producers drive the scope directly
with guarded `endIf` so clearing one interaction never stomps an unrelated
scope. Adds the `holeEditScope` builder + a no-leaked-flag invariant test.
Closes the first slice of the legacy-flag deletion; the rich-payload flags
(movingNode, curving*, *Endpoint, placementDragMode) remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements plans/editor-placement-interaction-overhaul.md: an authoritative
interaction-scope state machine plus the catalogued placement/interaction
fixes, and split-view floor-plan performance.
- Interaction-scope spine (lib/interaction/* + store/use-interaction-scope),
driven from central useEditor setters; overlay scoping (zone labels,
context badges, floating action menu) reads resolveOverlayPolicy.
- Bug tracks A/B/D/E/F/G/H: handle/cutout raycast, footprint validity,
auto-slab loop, ceiling hosting, B-key tool desync, 2D drop offset,
per-frame jank.
- Snapping modes (grid/lines/angles/off) + contextual HUD chips; modifier
model (Shift=cycle, Alt=free place, Ctrl=grid step).
- Item move now tracks the cursor 1:1 (was a laggy per-frame lerp); handle
rig hides during a whole-node move; rotate gizmo advertises Shift=free
rotation in the HUD and hides the move cross while rotating.
- Floor-plan perf: pause live reactivity while in 3D-only view; per-node
geometry cache so only changed nodes rebuild on a drag; hoist wall miters
to a once-per-pass ctx.levelData (O(N^2) -> O(N) on wall/opening drags).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Selecting a textured wall/slab/item replaced its surface with a flat
purple wash. Root cause: the selection highlight clones the material and
tints it, but `NodeMaterial.clone()` on the WebGPU backend drops the
texture-map node assignments, so the clone rendered flat — and a strong
albedo blend + emissive washed whatever was left.
Fix: re-attach the maps from the source material after cloning (shared by
reference) and drop the albedo tint, keeping only a gentle indigo emissive
so the real material/texture stays readable with a soft "selected" glow.
Applied to both highlight paths:
- generic editor highlight (slabs/items) in selection-manager
- wall path (walls are excluded from the generic one), built lazily +
cached/self-healing so it survives the wall finish's async texture load
Removes the now-dead eager wall `highlightedVisible`/`selection` profile.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a Section B rule to the review-architecture skill: a new node kind or
geometry change that exposes paintable parts must carry overrides in a
schema `slots` record resolved via def.capabilities.paint (and thread it
through clone/duplicate/preset paths), and texturable def.geometry must
emit UVs in metres (1 unit = 1 m) so catalog finishes tile. Surfaced in the
skill description too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a `horizontal` fence style that builds composite-cladding panels —
horizontal boards stacked between square intermediate posts — instead of
the vertical pickets that slat/rail/privacy all produce today.
- New `style: 'horizontal'` (core schema + inspector Style control).
- Posts march along the whole span at `postSpacing`, each with a cap.
- New `postCap` control (none/flat/pyramid, default pyramid); pyramid
caps render as a 4-sided cone fence part.
- New `slatGap` control for the board reveal — 0 collapses to one flush
panel so the stacked-board seams don't read as lines.
- `postCap` + `slatGap` are horizontal-only (visibleIf); postCap uses a
labelled dropdown so its options aren't a context-free switch.
- Lower the `postSpacing` inspector minimum from 0.2 to 0.05 m.
- 2D floor plan reuses the solid-panel marker for horizontal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Duplicating an item drops to the catalog placement flow, which rebuilds the
draft from the asset + transform and never carried node.slots — so the copy
lost every painted slot override. Thread slots through the draft create path:
useDraftNode.create seeds it onto the draft and commit() forwards it to the
final node, the placement coordinator passes it to its lazy wall/ceiling
draft creates, and the item move tool supplies node.slots for both the floor
(direct create) and wall/ceiling (coordinator) duplicate paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Registers a paintApply SFX (with rate/volume jitter + a 60ms gap so rapid
multi-face painting doesn't machine-gun), wires the sfx:paint-apply bus
event, and emits it from the material-paint click chokepoint in the
selection manager (fires on apply, not hover/preview). Adds the audio
asset to the editor app's public resources.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents the content-author contract for catalog item GLBs: slot_
material naming + deriveSlotId rules, authored defaults and
pascal_material extras, the cutout reserved mesh, the ~1 UV unit/m world
scale, vertex colours for fixed detail, and the validated Blender + glTF
export recipe (Custom Properties export gotcha, gltfpack -ke extras
preservation). Indexes the page in the architecture README and links it
from materials-and-themes.md (previously a dangling "phase-6 Blender
recipe" reference).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace 89 bundled item model.glb files with the authored/optimized
versions currently served from prod storage (items/system/<slug>/model.glb),
so the committed source of truth matches what live projects load and a
catalog re-seed no longer clobbers authored geometry. Items with no prod
object (404) are left as-is.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Layout is now: fixed eraser/reset + category tabs (no longer sticky-inside-
scroll), a single scrolling catalog grid, and an always-visible scene-material
footer. The custom-material "+" moved out of the colors-only grid cell onto the
scene-material section header, so a custom material can be added from any
category (creates a blank scene material, selects it as the brush, opens its
inline editor).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MaterialPaintPanel owns its scroll: the eraser/reset row stays pinned and
the category tabs stick to the top, so only the material list scrolls.
- Selected catalog swatch + active scene-material card use the same
`ring-1 ring-primary ring-inset` outline as item/preset tiles.
- Choosing a material category auto-selects its first material.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slabs now expose two paintable faces instead of one: `surface` (top, keeps
the wood floor default and the prior slot id so painted slabs are
unaffected) and `side` (vertical walls + underside, default light grey).
The merged slab buffer is split by per-triangle face normal into a top mesh
and a side mesh, each tagged with its slot id for paint resolve/preview;
legacy whole-slab material maps onto the top only. Textures-off still
collapses to the floor role.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Custom-create now pre-creates a scene material and opens its inline editor
in the build pane (no separate right-side PaintPanel, which is removed);
the brush + "Paint with" use a scene: ref so painting stores the ref and
edits propagate everywhere. Slot preview (shared + item) resolves scene
refs so hover shows the real material.
Material properties editor uses the shared SliderControl for roughness/
metalness/opacity; row action buttons use Tooltip instead of title; the
color input renders as a clean filled swatch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retire the inline material fields on every slot-model kind, moving them
onto the unified node.slots model on load so painting, edit-propagation,
and the picker behave uniformly.
Walls: slots {interior,exterior}; slot-first viewer resolution threading
sceneMaterials (content folded into the wall material hash); WallRenderer
subscribes to the scene-material palette so a scene-material edit
re-renders live; wallPaint rebuilt on createSlotPaintCapability.
Load migration generalizes legacy -> slots across slab/ceiling (surface),
fence (posts/infill/base/rail), column (shaft/base/capital/frame), shelf
(shelves/frame/back), and stair (per-role tread/side/railing). Library/
scene refs pass through; inline customs mint a deduped scene material;
legacy fields cleared. No visual change (renderers already fell back to
the legacy fields). Roof/chimney/dormer/vents intentionally stay on their
role system.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The compass needle rotation was `-floorplanUserRotationDeg`, but the
2D-floorplan SVG and 3D camera projections both place true north at
`+floorplanUserRotationDeg` (= cameraAzimuth − 90°). The negated sign
made the needle rotate opposite to north as the camera orbits, so it
appeared to track the camera instead of pointing north.
The formula was written for the 2D floorplan and reused verbatim when
the compass was portaled into the 3D viewer; both instances had the
flipped sign. "Align to north" is unaffected (userRotation 0 → up).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The earlier gable fix world-referenced only V, leaving U in segment-local
space — so gable and wall lined up vertically but slid horizontally. Replace the
V-only scalar offset with the segment's full world matrix (roof group composed
with segment transform) and project vertical faces exactly like the wall kind:
U = ±worldX/Z, V = 1 - worldY. Gable now tiles continuously into the walls in
both axes. Sloped shingle UVs are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Walls used THREE's ExtrudeGeometry UVs, which restart at each wall's local
start/end — so finishes seamed at wall joins, showed a mid-face axis-switch
stripe, and didn't line up with the roof gable. Re-project the render mesh's
UVs in WORLD space (1 unit = 1 m), matching roof-system's pushRoofUv exactly:
vertical faces U = ±worldX/Z, V = 1 - worldY. De-indexes so each triangle uses
its own face normal (no edge seams). Applied only to the render mesh; collision
and floorplan geometry keep the original UVs. Now walls tile continuously
across segments and meet the gable seamlessly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gable wall band tiled its V in segment-local space, so its texture phase
didn't line up with the wall below (whose ExtrudeGeometry UVs map V = 1 -
height-from-base). Make vertical roof faces tile V in world space
(V = 1 - worldY) via the segment's resolved world Y (parent roof + segment
position), so the band matches a ground-floor wall's vertical tiling at the
eave seam. U is unchanged (stays in the face's local run).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The posts/infill and the accent rail shared the same panelDepth*0.35 depth, so
their faces were coplanar where they cross -> z-fighting. Make the verticals
0.001m shy so the rail wins the depth test cleanly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mergeGeometries throws on an empty array, so an absent slot group (infill with
showInfill off, base on a floating fence) crashed the geometry build. Return an
empty BufferGeometry for an empty group; the renderer already skips meshes with
no position attribute.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add 'concrete-drywall' (Prepared Drywall) catalog finish + 512 KTX2/webp
runtime assets; default walls (WALL_SLOT_DEFAULT interior+exterior) and the
roof wall/trim band to it so roofs read continuous with walls.
- fence: replace the 2-slot panel/rail split with 4 slots that match the panel's
build options — posts / infill (showInfill) / base (grounded only) / rail —
each its own mesh with userData.slotId; conditional slots track the build
state. Defaults: posts/infill/base charcoal, rail wood-finewood27.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- fence: split into two paint slots — panel (posts/base/infill, default charcoal)
and rail (cap, default wood-finewood27) — as separate meshes with userData.slotId.
Fix applyFenceUVs to continuous world-space 1 UV unit = 1 m (drop the per-part
min origin that broke tiling across parts). New generateFenceSlotGeometries.
- roof: real catalog defaults for the segment surfaces via getRoofMaterialArray
(the actual default path): wall/trim concrete-plate (matches walls), deck +
soffit soft-white, shingle terracotta; textures-off role escape hatch kept.
Align nodes getRoofMaterials no-parent fallback to match.
- stair: body slot default -> preset-lightgrey.
- (biome formatting normalization of the round-1 merged renderer files rides along.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>