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>
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>
Elevator exposes four finish slots — cab / doors / shaft / glass (glass only
when shaft or door style is glass) — on node.slots. The finish materials now
resolve through a per-node material set (context provider) keyed on node.slots
+ scene materials: textures-off joinery role -> node.slots ref -> declared
default (cab preset-softwhite, doors metal-steel, shaft preset-lightgrey,
glass preset-glass), with glass transparency flags re-applied. Cab/door/shaft/
glass meshes tag userData.slotId; buttons, indicators, control panels, and
queue strips stay untagged (functional UI, not paintable). Interaction system
untouched. Monochrome unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Column exposes shaft / base / capital / frame slots on node.slots (base when
baseStyle!=none, capital when capitalStyle!=none, frame only for non-vertical
support styles). The single material context is widened to a per-slot map plus
a ColumnSlotContext; each part subtree is wrapped in <ColumnSlot> so every
mesh primitive resolves its material AND stamps userData.slotId from the same
context. Decorative shaft parts inherit shaft, base/capital carvings inherit
their parent slot. Resolution: textures-off wall role -> node.slots ref ->
legacy material/preset -> declared default (shaft/base/capital concrete-plaster,
frame metal-steel). Monochrome unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stair exposes three paintable slots — treads / body / railing (railing only
when railingMode != none) — on node.slots. The 2-material-index body mesh
maps materialIndex 0->treads, 1->body via userData.slotIds; railing meshes
tag userData.slotId='railing'. Per-slot resolution layers over the viewer's
base body/railing materials only in textures-on mode: node.slots ref ->
legacy per-part field (preserved) -> declared default (treads wood-woodplank48,
body wood-woodfine2, railing metal-steel). A custom preview swaps the targeted
body material-array index and whole-mesh railing materials. Monochrome
unchanged. Stops using DEFAULT_STAIR_MATERIAL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fence joins the node.slots paint model with one paintable 'surface' slot
(merged post/base/rail geometry is a single mesh). Resolution order matches
slab: textures-off joinery role -> node.slots.surface ref -> legacy inline
material/preset -> declared default (library:wood-finewood27). Tags the mesh
userData.slotId='surface' and wires capabilities.slots + paint. Stops
borrowing DEFAULT_STAIR_MATERIAL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the flat #e5e5e5 placeholder on the roof shingle surface (material
index 3) with the catalog roof-weatheredshingles finish via
resolveSlotDefaultMaterial. Wall/trim, deck, and interior surfaces and the
textures-off role array are unchanged. Roof keeps its existing per-segment
role material system (no node.slots migration).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-adds the unified slot-ref field (Record<slotId, MaterialRef>) to the
four kinds being migrated onto the paint-slot model, so per-kind work can
proceed in parallel worktrees without cross-package core edits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove height/displacement/mask/mortar/joint texture files the material
schema never samples (it reads only albedo/normal/roughness/ao/metalness).
Computed as present-on-disk minus catalog-referenced in material-library.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
Lets a host mount the editor's real editing experience on a bare <Viewer>
without the full <Editor> shell.
editor: export Grid, NodeArrowHandles, MoveTool, ToolManager — the selection
handles, the kind-owned mover, the build-tool host, and the drafting grid that
feeds tools their grid:* pointer events. See the doc comment in index.tsx for
how they cooperate with host camera controls and selection.
viewer: two opt-in, non-persisted presentation flags (both default off, so the
editor and every other consumer are unchanged):
- transparentBackground / <Viewer transparent>: emit premultiplied RGBA masked
by geometry + outline alpha (outputColorTransform off on that path) so the
scene can float on any page background. ACES tone-mapping makes a true-white
opaque background impossible, hence transparency.
- inkOpacity: override the per-mode ink-edge opacity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The editor's /icons assets were ~10MB of oversized PNGs (a single
toolbar icon up to 1.4MB). Convert every non-PWA icon to WebP (quality
92) and repoint all /icons/*.png references to .webp across
packages/editor, packages/nodes, and apps/editor. PWA/platform icons
(apple-touch-icon, icon-192, icon-512) stay PNG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Snapshot/preset capture is for framing a thumbnail, not editing, so it
should only move the camera:
- gate the selection manager, transform handles, floating menus, and the
tool manager (which renders the site boundary flags) behind
isCaptureMode, so clicking the scene no longer selects items or shows
editing controls
- hide zone meshes and the HTML zone tags while capturing
- force 3D on entry (the 2D/split floorplan panes render nothing useful
for a thumbnail) and restore the prior view mode on exit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- 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>
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>
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>
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>