diff --git a/packages/nodes/src/column/renderer.tsx b/packages/nodes/src/column/renderer.tsx
index 0e80ddb4..fb070206 100644
--- a/packages/nodes/src/column/renderer.tsx
+++ b/packages/nodes/src/column/renderer.tsx
@@ -2288,11 +2288,7 @@ function ColumnBody({ node }: { node: ColumnNode }) {
-
+
-
+
{
visible={renderNode.visible}
{...handlers}
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
- {entries.map((entry, index) => {
- const column = index % cabButtonColumns
- const row = Math.floor(index / cabButtonColumns)
- const isDisabledLevel = disabledLevelIds.has(entry.id)
- const x =
- cabFloorButtonOffsetX + (column - (cabButtonColumns - 1) / 2) * cabButtonSpacingX
- const y = (row - (cabButtonRows - 1) / 2) * cabButtonSpacingY
+
- return (
-
- )
- })}
-
+
+
+
+
+
+
+
+
-
-
- {entrySpans.map(({ entry, levelTopY }) => {
- const isCurrentLevel = activeLevelId === entry.id
- const isDisabledLevel = disabledLevelIds.has(entry.id)
- const isServiceOnlyLevel = serviceOnlyLevelIds.has(entry.id)
- const isQueuedLevel = !isDisabledLevel && queuedLevelIds.has(entry.id)
- const isPendingLevel = pendingLevelId === entry.id
- const showLandingReadout = isCurrentLevel || isPendingLevel || isQueuedLevel
-
- return (
-
-
+
- {
+ const column = index % cabButtonColumns
+ const row = Math.floor(index / cabButtonColumns)
+ const isDisabledLevel = disabledLevelIds.has(entry.id)
+ const x =
+ cabFloorButtonOffsetX + (column - (cabButtonColumns - 1) / 2) * cabButtonSpacingX
+ const y = (row - (cabButtonRows - 1) / 2) * cabButtonSpacingY
+
+ return (
+
+ )
+ })}
+
-
-
-
- 0.5}
- buttonKind="landing"
- disabled={isDisabledLevel || isServiceOnlyLevel}
- elevatorId={elevatorId}
- levelId={entry.id as AnyNodeId}
- position={[0, 0.06, -0.045]}
- queued={isQueuedLevel}
- radius={0.045}
- />
-
-
- )
- })}
+
+
+ {entrySpans.map(({ entry, levelTopY }) => {
+ const isCurrentLevel = activeLevelId === entry.id
+ const isDisabledLevel = disabledLevelIds.has(entry.id)
+ const isServiceOnlyLevel = serviceOnlyLevelIds.has(entry.id)
+ const isQueuedLevel = !isDisabledLevel && queuedLevelIds.has(entry.id)
+ const isPendingLevel = pendingLevelId === entry.id
+ const showLandingReadout = isCurrentLevel || isPendingLevel || isQueuedLevel
+
+ return (
+
+
+
+
+
+
+ 0.5
+ }
+ buttonKind="landing"
+ disabled={isDisabledLevel || isServiceOnlyLevel}
+ elevatorId={elevatorId}
+ levelId={entry.id as AnyNodeId}
+ position={[0, 0.06, -0.045]}
+ queued={isQueuedLevel}
+ radius={0.045}
+ />
+
+
+
+ )
+ })}
)
diff --git a/packages/nodes/src/elevator/slots.ts b/packages/nodes/src/elevator/slots.ts
index 0bc88ce3..7f1cc048 100644
--- a/packages/nodes/src/elevator/slots.ts
+++ b/packages/nodes/src/elevator/slots.ts
@@ -23,7 +23,8 @@ export function elevatorSlots(node: ElevatorNode): SlotDeclaration[] {
getResolvedElevatorShaftStyle(node.shaftStyle) === 'glass' ||
getResolvedElevatorDoorPanelStyle(node.doorPanelStyle) === 'glass-frame'
- if (hasGlass) slots.push({ slotId: 'glass', label: 'Glass', default: ELEVATOR_GLASS_SLOT_DEFAULT })
+ if (hasGlass)
+ slots.push({ slotId: 'glass', label: 'Glass', default: ELEVATOR_GLASS_SLOT_DEFAULT })
return slots
}
diff --git a/packages/nodes/src/fence/geometry.ts b/packages/nodes/src/fence/geometry.ts
index f4f3f85b..dc1f52aa 100644
--- a/packages/nodes/src/fence/geometry.ts
+++ b/packages/nodes/src/fence/geometry.ts
@@ -5,30 +5,30 @@ import {
createDefaultMaterial,
createMaterial,
createSurfaceRoleMaterial,
- generateFenceGeometry,
+ generateFenceSlotGeometries,
type RenderShading,
resolveMaterialRef,
resolveSlotDefaultMaterial,
} from '@pascal-app/viewer'
import { FrontSide, Group, type Material, Mesh, type Texture } from 'three'
import type { FenceNode } from './schema'
-import { FENCE_SLOT_DEFAULT } from './slots'
+import { FENCE_PANEL_SLOT_DEFAULT, FENCE_RAIL_SLOT_DEFAULT, type FenceSlotId } from './slots'
/**
- * Stage B builder for fence. Reuses the legacy `generateFenceGeometry`
- * (pure function from viewer that returns a merged BufferGeometry of
- * posts + base + top rail + curve spans) and wraps it in a Mesh-in-Group
- * shape the generic `` expects.
+ * Stage B builder for fence. Splits the geometry into two paintable slots —
+ * `panel` (posts / base / infill) and `rail` (the cap rail) — each its own Mesh
+ * with a `userData.slotId` so the unified slot paint resolves and previews per
+ * part.
*
- * Materials follow the unified slot model: the single `surface` slot resolves
- * `node.slots.surface` (a shared scene material or `library:` finish) → the
- * legacy inline `node.material` / `materialPreset` (pre-slot-model scenes) →
- * the declared slot default. Textures-off collapses to themed joinery.
+ * Per slot the material resolves: `node.slots[slotId]` (a shared scene material
+ * or `library:` finish) → the legacy inline `node.material` / `materialPreset`
+ * (pre-slot-model scenes, applied to both parts) → the declared slot default.
+ * Textures-off collapses both parts to the themed joinery role.
*
- * Phase 6 cleanup moves the 280 lines of geometry math out of the
- * legacy `viewer/src/systems/fence/fence-system.tsx` into this folder
- * once the legacy system file is deleted. Until then `generateFenceGeometry`
- * is publicly re-exported from viewer.
+ * Phase 6 cleanup moves the geometry math out of the legacy
+ * `viewer/src/systems/fence/fence-system.tsx` into this folder once the legacy
+ * system file is deleted. Until then `generateFenceSlotGeometries` is publicly
+ * re-exported from viewer.
*/
type FenceMaterial = Material & {
alphaMap?: Texture | null
@@ -37,10 +37,16 @@ type FenceMaterial = Material & {
transparent: boolean
}
+const SLOT_DEFAULTS: Record = {
+ panel: FENCE_PANEL_SLOT_DEFAULT,
+ rail: FENCE_RAIL_SLOT_DEFAULT,
+}
+
const fenceMaterialCache = new Map()
-function getFenceMaterial(
+function getFenceSlotMaterial(
node: FenceNode,
+ slotId: FenceSlotId,
shading: RenderShading,
textures: boolean,
colorPreset: ColorPreset,
@@ -51,7 +57,7 @@ function getFenceMaterial(
return createSurfaceRoleMaterial('joinery', colorPreset, FrontSide, sceneTheme)
}
- const slotRef = node.slots?.surface
+ const slotRef = node.slots?.[slotId]
if (slotRef) {
const resolved = resolveMaterialRef(slotRef, sceneMaterials, shading)
if (resolved) return resolved
@@ -61,7 +67,7 @@ function getFenceMaterial(
return getLegacyFenceMaterial(node, shading)
}
- return resolveSlotDefaultMaterial(FENCE_SLOT_DEFAULT, shading, 0.8)
+ return resolveSlotDefaultMaterial(SLOT_DEFAULTS[slotId], shading, 0.8)
}
function getLegacyFenceMaterial(node: FenceNode, shading: RenderShading): Material {
@@ -105,13 +111,26 @@ export function buildFenceGeometry(
sceneTheme?: string,
): Group {
const group = new Group()
- const geometry = generateFenceGeometry(node)
- const material = getFenceMaterial(node, shading, textures, colorPreset, sceneTheme, ctx?.materials)
- const mesh = new Mesh(geometry, material)
- mesh.castShadow = true
- mesh.receiveShadow = true
- mesh.userData.slotId = 'surface'
- mesh.userData.surfaceRole = 'joinery'
- group.add(mesh)
+ const geometries = generateFenceSlotGeometries(node)
+
+ for (const slotId of ['panel', 'rail'] as const) {
+ const geometry = geometries[slotId]
+ if (geometry.getAttribute('position') === undefined) continue
+ const material = getFenceSlotMaterial(
+ node,
+ slotId,
+ shading,
+ textures,
+ colorPreset,
+ sceneTheme,
+ ctx?.materials,
+ )
+ const mesh = new Mesh(geometry, material)
+ mesh.castShadow = true
+ mesh.receiveShadow = true
+ mesh.userData.slotId = slotId
+ group.add(mesh)
+ }
+
return group
}
diff --git a/packages/nodes/src/fence/paint.ts b/packages/nodes/src/fence/paint.ts
index 39db9247..b1cde84a 100644
--- a/packages/nodes/src/fence/paint.ts
+++ b/packages/nodes/src/fence/paint.ts
@@ -1,8 +1,13 @@
-import type { AnyNode, FenceNode } from '@pascal-app/core'
+import type { AnyNode, FenceNode, PaintResolveArgs } from '@pascal-app/core'
import { createSlotPaintCapability, previewGeometrySlot } from '../shared/slot-paint'
+function resolveFenceRole(args: PaintResolveArgs): string | null {
+ const slotId = (args.hitObject?.userData as { slotId?: unknown } | undefined)?.slotId
+ return slotId === 'panel' || slotId === 'rail' ? slotId : null
+}
+
export const fencePaint = createSlotPaintCapability({
- resolveRole: () => 'surface',
+ resolveRole: resolveFenceRole,
applyPreview: previewGeometrySlot,
legacyEffective: (node: AnyNode) => {
const fence = node as FenceNode
diff --git a/packages/nodes/src/fence/slots.ts b/packages/nodes/src/fence/slots.ts
index 3378ba8b..aa9b3bd7 100644
--- a/packages/nodes/src/fence/slots.ts
+++ b/packages/nodes/src/fence/slots.ts
@@ -1,9 +1,15 @@
import type { SlotDeclaration } from '@pascal-app/core'
-export type FenceSlotId = 'surface'
+export type FenceSlotId = 'panel' | 'rail'
-export const FENCE_SLOT_DEFAULT = 'library:wood-finewood27'
+// Body (posts / base / infill) reads as a dark composite by default; the rail
+// cap as wood. Both are repaintable per slot.
+export const FENCE_PANEL_SLOT_DEFAULT = 'library:preset-charcoal'
+export const FENCE_RAIL_SLOT_DEFAULT = 'library:wood-finewood27'
export function fenceSlots(): SlotDeclaration[] {
- return [{ slotId: 'surface', label: 'Surface', default: FENCE_SLOT_DEFAULT }]
+ return [
+ { slotId: 'panel', label: 'Panel', default: FENCE_PANEL_SLOT_DEFAULT },
+ { slotId: 'rail', label: 'Rail', default: FENCE_RAIL_SLOT_DEFAULT },
+ ]
}
diff --git a/packages/nodes/src/roof/roof-materials.ts b/packages/nodes/src/roof/roof-materials.ts
index 665d155d..daf414f3 100644
--- a/packages/nodes/src/roof/roof-materials.ts
+++ b/packages/nodes/src/roof/roof-materials.ts
@@ -2,8 +2,8 @@ import {
type ColorPreset,
createDefaultMaterial,
createSurfaceRoleMaterial,
- resolveSlotDefaultMaterial,
type RenderShading,
+ resolveSlotDefaultMaterial,
} from '@pascal-app/viewer'
import * as THREE from 'three'
@@ -22,10 +22,12 @@ export function getRoofMaterials(
const materials = textures
? [
- createDefaultMaterial('white', 1, shading, THREE.DoubleSide), // 0: Wall/Trim
- createDefaultMaterial('#e5e5e5', 1, shading, THREE.FrontSide), // 1: Deck
- createDefaultMaterial('white', 1, shading, THREE.DoubleSide), // 2: Interior
- resolveSlotDefaultMaterial('library:roof-weatheredshingles', shading), // 3: Shingle
+ // Mirrors getRoofMaterialArray's catalog defaults (wall/trim concrete,
+ // soft-white deck + soffit, terracotta shingle) for the no-parent path.
+ resolveSlotDefaultMaterial('library:concrete-plate', shading), // 0: Wall/Trim
+ resolveSlotDefaultMaterial('library:preset-softwhite', shading), // 1: Deck
+ resolveSlotDefaultMaterial('library:preset-softwhite', shading), // 2: Interior
+ resolveSlotDefaultMaterial('library:roof-terracottatiles', shading), // 3: Shingle
]
: [
createSurfaceRoleMaterial('roof', colorPreset),
diff --git a/packages/nodes/src/stair/definition.ts b/packages/nodes/src/stair/definition.ts
index 25400b25..7d79d3d0 100644
--- a/packages/nodes/src/stair/definition.ts
+++ b/packages/nodes/src/stair/definition.ts
@@ -406,8 +406,8 @@ import {
stairRotateAffordance,
} from './floorplan-affordances'
import { stairFloorplanMoveTarget } from './floorplan-move'
-import { stairParametrics } from './parametrics'
import { stairPaint } from './paint'
+import { stairParametrics } from './parametrics'
import { StairNode } from './schema'
import { stairSlots } from './slots'
diff --git a/packages/nodes/src/stair/paint.ts b/packages/nodes/src/stair/paint.ts
index e3bc0037..09b3de8b 100644
--- a/packages/nodes/src/stair/paint.ts
+++ b/packages/nodes/src/stair/paint.ts
@@ -8,9 +8,7 @@ function isStairSlotId(value: unknown): value is StairSlotId {
}
function resolveStairPaintRole(args: PaintResolveArgs): StairSlotId | null {
- const userData = args.hitObject?.userData as
- | { slotId?: unknown; slotIds?: unknown }
- | undefined
+ const userData = args.hitObject?.userData as { slotId?: unknown; slotIds?: unknown } | undefined
if (isStairSlotId(userData?.slotId)) {
return userData.slotId
diff --git a/packages/nodes/src/stair/renderer.tsx b/packages/nodes/src/stair/renderer.tsx
index 090a00c6..c038c919 100644
--- a/packages/nodes/src/stair/renderer.tsx
+++ b/packages/nodes/src/stair/renderer.tsx
@@ -160,9 +160,7 @@ export const StairRenderer = ({ node: rawNode }: { node: StairNode }) => {
userData={STAIR_BODY_SLOT_USER_DATA}
/>
) : null}
- {isSegmentBasedStair ? null : (
-
- )}
+ {isSegmentBasedStair ? null : }
{isSegmentBasedStair ? (
diff --git a/packages/nodes/src/stair/slots.ts b/packages/nodes/src/stair/slots.ts
index 876d26a1..fdbb5eca 100644
--- a/packages/nodes/src/stair/slots.ts
+++ b/packages/nodes/src/stair/slots.ts
@@ -3,7 +3,7 @@ import type { SlotDeclaration, StairNode } from '@pascal-app/core'
export type StairSlotId = 'treads' | 'body' | 'railing'
export const STAIR_TREADS_SLOT_DEFAULT = 'library:wood-woodplank48'
-export const STAIR_BODY_SLOT_DEFAULT = 'library:wood-woodfine2'
+export const STAIR_BODY_SLOT_DEFAULT = 'library:preset-lightgrey'
export const STAIR_RAILING_SLOT_DEFAULT = 'library:metal-steel'
export function stairSlots(node: StairNode): SlotDeclaration[] {
diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts
index 6782d31c..0802b53a 100644
--- a/packages/viewer/src/index.ts
+++ b/packages/viewer/src/index.ts
@@ -97,7 +97,11 @@ export { ElevatorInteractionSystem } from './systems/elevator/elevator-interacti
// Fence system follows the wall re-export pattern — composed into the
// registry-driven fence definition's `def.system`. Removed in Phase 6
// alongside the legacy fence mount point.
-export { FenceSystem, generateFenceGeometry } from './systems/fence/fence-system'
+export {
+ FenceSystem,
+ generateFenceGeometry,
+ generateFenceSlotGeometries,
+} from './systems/fence/fence-system'
// Generic floor-elevation system. Lifts the rendered mesh of any kind
// whose definition declares `capabilities.floorPlaced` by the slab
// elevation under its footprint. Replaces the per-kind elevation block
diff --git a/packages/viewer/src/systems/fence/fence-system.tsx b/packages/viewer/src/systems/fence/fence-system.tsx
index 31807dad..7bfc3bd3 100644
--- a/packages/viewer/src/systems/fence/fence-system.tsx
+++ b/packages/viewer/src/systems/fence/fence-system.tsx
@@ -100,16 +100,13 @@ function applyFenceUVs(geometry: THREE.BufferGeometry) {
if (!(position && normal)) return
+ // World-scale triplanar UVs: 1 UV unit = 1 metre, sampled from the part's
+ // local-space (already translated into fence space) coordinates with NO
+ // per-part origin shift. A shared origin keeps a tiled finish continuous
+ // across posts, rails, and infill instead of restarting the tile at each
+ // part's own min corner (the previous behaviour, which broke the 1 m
+ // contract and made adjacent parts mistile).
const uvs = new Float32Array(position.count * 2)
- let minX = Number.POSITIVE_INFINITY
- let minY = Number.POSITIVE_INFINITY
- let minZ = Number.POSITIVE_INFINITY
-
- for (let index = 0; index < position.count; index += 1) {
- minX = Math.min(minX, position.getX(index))
- minY = Math.min(minY, position.getY(index))
- minZ = Math.min(minZ, position.getZ(index))
- }
for (let index = 0; index < position.count; index += 1) {
const px = position.getX(index)
@@ -123,14 +120,14 @@ function applyFenceUVs(geometry: THREE.BufferGeometry) {
let v = 0
if (ny >= nx && ny >= nz) {
- u = px - minX
- v = pz - minZ
+ u = px
+ v = pz
} else if (nx >= nz) {
- u = pz - minZ
- v = py - minY
+ u = pz
+ v = py
} else {
- u = px - minX
- v = py - minY
+ u = px
+ v = py
}
uvs[index * 2] = u
@@ -153,8 +150,16 @@ function getStyleDefaults(style: FenceNode['style']) {
return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 }
}
-function createFenceParts(fence: FenceNode): FencePart[] {
- const parts: FencePart[] = []
+export type FenceSlotParts = {
+ /** Posts, base/kickboard, and vertical infill — the fence body. */
+ panel: FencePart[]
+ /** Top rail (and the floating style's matching bottom rail). */
+ rail: FencePart[]
+}
+
+function createFenceParts(fence: FenceNode): FenceSlotParts {
+ const panel: FencePart[] = []
+ const rail: FencePart[] = []
const length = Math.max(getWallCurveLength(fence), 0.01)
const panelDepth = Math.max(fence.thickness, 0.03)
const clearance = Math.max(fence.groundClearance, 0)
@@ -173,7 +178,7 @@ function createFenceParts(fence: FenceNode): FencePart[] {
const endInsetT = Math.max(0.501, 1 - edgeInset / length)
if (!isFloating) {
- parts.push(
+ panel.push(
...createFenceCurveSpanParts(
fence,
0,
@@ -183,7 +188,7 @@ function createFenceParts(fence: FenceNode): FencePart[] {
panelDepth * 1.05,
),
)
- parts.push(
+ panel.push(
...createFenceCurveSpanParts(
fence,
0,
@@ -208,14 +213,14 @@ function createFenceParts(fence: FenceNode): FencePart[] {
: verticalHeight
const postY = fullHeightPost ? postHeight / 2 : verticalY
- parts.push({
+ panel.push({
position: [frame.point.x, postY, frame.point.y],
rotationY: -frame.tangentAngle,
scale: [postWidth, postHeight, Math.max(panelDepth * 0.35, 0.012)],
})
}
- parts.push(
+ rail.push(
...createFenceCurveSpanParts(
fence,
0,
@@ -227,7 +232,7 @@ function createFenceParts(fence: FenceNode): FencePart[] {
)
if (isFloating) {
- parts.push(
+ rail.push(
...createFenceCurveSpanParts(
fence,
0,
@@ -239,13 +244,11 @@ function createFenceParts(fence: FenceNode): FencePart[] {
)
}
- return parts
+ return { panel, rail }
}
-export function generateFenceGeometry(fence: FenceNode) {
- const parts = createFenceParts(fence)
+function mergeFenceParts(parts: FencePart[]): THREE.BufferGeometry {
const geometries = parts.map(createFencePartGeometry)
-
const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry()
geometries.forEach((geometry) => {
geometry.dispose()
@@ -258,6 +261,24 @@ export function generateFenceGeometry(fence: FenceNode) {
return merged
}
+/**
+ * Geometry split by paint slot: the body (posts / base / infill) and the rail
+ * cap, each a separate merged BufferGeometry so the fence renderer can give
+ * each its own material + `userData.slotId`.
+ */
+export function generateFenceSlotGeometries(fence: FenceNode): {
+ panel: THREE.BufferGeometry
+ rail: THREE.BufferGeometry
+} {
+ const { panel, rail } = createFenceParts(fence)
+ return { panel: mergeFenceParts(panel), rail: mergeFenceParts(rail) }
+}
+
+export function generateFenceGeometry(fence: FenceNode) {
+ const { panel, rail } = createFenceParts(fence)
+ return mergeFenceParts([...panel, ...rail])
+}
+
function updateFenceGeometry(fenceId: FenceNode['id']) {
const node = useScene.getState().nodes[fenceId]
if (!node || node.type !== 'fence') return
diff --git a/packages/viewer/src/systems/roof/roof-materials.ts b/packages/viewer/src/systems/roof/roof-materials.ts
index e4040616..85f2a4db 100644
--- a/packages/viewer/src/systems/roof/roof-materials.ts
+++ b/packages/viewer/src/systems/roof/roof-materials.ts
@@ -10,8 +10,20 @@ import {
createMaterialFromPresetRef,
createSurfaceRoleMaterial,
type RenderShading,
+ resolveSlotDefaultMaterial,
} from '../../lib/materials'
+// Declared catalog defaults for an unpainted roof, per the 4-slot layout
+// (0 wall/trim · 1 deck · 2 interior soffit · 3 shingle top). The wall/trim
+// band mirrors the wall kind's default (WALL_SLOT_DEFAULT = concrete-plate) so
+// a roof reads as continuous with the walls below it.
+const ROOF_DEFAULT_REFS: [string, string, string, string] = [
+ 'library:concrete-plate',
+ 'library:preset-softwhite',
+ 'library:preset-softwhite',
+ 'library:roof-terracottatiles',
+]
+
export type RoofMaterialArray = [THREE.Material, THREE.Material, THREE.Material, THREE.Material]
const roofMaterialArrayCache = new Map()
@@ -77,30 +89,42 @@ export function getRoofMaterialArray(
roofMaterial,
]
+ // Textures-off (monochrome) is the guaranteed escape hatch: themed role
+ // colours, no catalog finishes.
if (!textures) {
roofMaterialArrayCache.set(cacheKey, roleArray)
return roleArray
}
+ // Textures-on default appearance: catalog finishes per slot (terracotta
+ // shingle, soft-white deck/soffit, wall-coloured trim). Used both when the
+ // roof is unpainted and to fill any individual unpainted slot below.
+ const defaultArray: RoofMaterialArray = [
+ resolveSlotDefaultMaterial(ROOF_DEFAULT_REFS[0], shading),
+ resolveSlotDefaultMaterial(ROOF_DEFAULT_REFS[1], shading),
+ resolveSlotDefaultMaterial(ROOF_DEFAULT_REFS[2], shading),
+ resolveSlotDefaultMaterial(ROOF_DEFAULT_REFS[3], shading),
+ ]
+
const topMaterial = createResolvedMaterial(top.material, top.materialPreset, shading)
const edgeMaterial = createResolvedMaterial(edge.material, edge.materialPreset, shading)
const wallMaterial = createResolvedMaterial(wall.material, wall.materialPreset, shading)
if (!(topMaterial || edgeMaterial || wallMaterial)) {
- roofMaterialArrayCache.set(cacheKey, roleArray)
- return roleArray
+ roofMaterialArrayCache.set(cacheKey, defaultArray)
+ return defaultArray
}
- // Each slot resolves to its own role only, then the themed default — never
+ // Each slot resolves to its own role only, then the declared default — never
// another role. Cross-role fallback here used to splatter a single painted
// surface (e.g. the edge) across the shingle and soffit slots. The legacy
// catch-all still fills every role because `getEffectiveRoofSurfaceMaterial`
// returns it for top/edge/wall alike.
const materialArray: RoofMaterialArray = [
- edgeMaterial ?? roofMaterial,
- wallMaterial ?? ceilingMaterial,
- wallMaterial ?? ceilingMaterial,
- topMaterial ?? roofMaterial,
+ edgeMaterial ?? defaultArray[0],
+ wallMaterial ?? defaultArray[1],
+ wallMaterial ?? defaultArray[2],
+ topMaterial ?? defaultArray[3],
]
roofMaterialArrayCache.set(cacheKey, materialArray)