feat(paint-slots): round-2 feedback — fence 2 slots + UVs, roof defaults, stair body

- 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>
This commit is contained in:
Wassim SAMAD
2026-06-17 17:21:58 -04:00
co-authored by Claude Opus 4.8
parent ad227a04a2
commit e92ee05702
15 changed files with 345 additions and 271 deletions
+2 -10
View File
@@ -2288,11 +2288,7 @@ function ColumnBody({ node }: { node: ColumnNode }) {
<ColumnSlot slotId="shaft">
<Shaft height={shaftLayout.shaftHeight} node={node} y={shaftLayout.shaftY} />
<Rings node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
<LatheBands
node={node}
shaftHeight={shaftLayout.shaftHeight}
shaftY={shaftLayout.shaftY}
/>
<LatheBands node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
<Flutes node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
<LowerCarvedBand
node={node}
@@ -2304,11 +2300,7 @@ function ColumnBody({ node }: { node: ColumnNode }) {
shaftHeight={shaftLayout.shaftHeight}
shaftY={shaftLayout.shaftY}
/>
<SpiralRibs
node={node}
shaftHeight={shaftLayout.shaftHeight}
shaftY={shaftLayout.shaftY}
/>
<SpiralRibs node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
</ColumnSlot>
<ColumnSlot slotId="capital">
<Capital
+1 -1
View File
@@ -12,8 +12,8 @@ import {
} from '@pascal-app/core'
import { buildElevatorFloorplan } from './floorplan'
import { elevatorResizeAffordance, elevatorRotateAffordance } from './floorplan-affordances'
import { elevatorParametrics } from './parametrics'
import { elevatorPaint } from './paint'
import { elevatorParametrics } from './parametrics'
import { ElevatorNode } from './schema'
import { elevatorSlots } from './slots'
+186 -182
View File
@@ -1383,218 +1383,222 @@ export const ElevatorRenderer = ({ node }: { node: ElevatorNode }) => {
visible={renderNode.visible}
{...handlers}
>
<BoxPrimitive
castShadow
material={shaftShellMaterial}
position={[0, shaftBodyCenterY, shaftDepth / 2 + shaftWallThickness / 2]}
receiveShadow
scale={[shaftWidth + shaftWallThickness * 2, shaftBodyHeight, shaftWallThickness]}
slotId={shaftShellSlotId}
/>
<BoxPrimitive
castShadow
material={shaftShellMaterial}
position={[-shaftWidth / 2 - shaftWallThickness / 2, shaftBodyCenterY, 0]}
receiveShadow
scale={[shaftWallThickness, shaftBodyHeight, shaftDepth + shaftWallThickness * 2]}
slotId={shaftShellSlotId}
/>
<BoxPrimitive
castShadow
material={shaftShellMaterial}
position={[shaftWidth / 2 + shaftWallThickness / 2, shaftBodyCenterY, 0]}
receiveShadow
scale={[shaftWallThickness, shaftBodyHeight, shaftDepth + shaftWallThickness * 2]}
slotId={shaftShellSlotId}
/>
<BoxPrimitive
castShadow
material={shaftTopMaterial}
position={[0, shaftBaseY + shaftHeight - shaftWallThickness / 2, 0]}
receiveShadow
scale={[
shaftWidth + shaftWallThickness * 2,
shaftWallThickness,
shaftDepth + shaftWallThickness * 2,
]}
slotId="shaft"
/>
<group ref={cabRef} position={[0, cabBaseY, 0]}>
<BoxPrimitive
castShadow
material={CAB_MATERIAL}
position={[0, 0.04, cabCenterZ]}
material={shaftShellMaterial}
position={[0, shaftBodyCenterY, shaftDepth / 2 + shaftWallThickness / 2]}
receiveShadow
scale={[cabWidth, 0.08, cabDepth]}
slotId="cab"
scale={[shaftWidth + shaftWallThickness * 2, shaftBodyHeight, shaftWallThickness]}
slotId={shaftShellSlotId}
/>
<BoxPrimitive
castShadow
material={CAB_MATERIAL}
position={[0, cabHeight - 0.04, cabCenterZ]}
material={shaftShellMaterial}
position={[-shaftWidth / 2 - shaftWallThickness / 2, shaftBodyCenterY, 0]}
receiveShadow
scale={[cabWidth, 0.08, cabDepth]}
slotId="cab"
scale={[shaftWallThickness, shaftBodyHeight, shaftDepth + shaftWallThickness * 2]}
slotId={shaftShellSlotId}
/>
<BoxPrimitive
castShadow
material={CAB_MATERIAL}
position={[0, cabHeight / 2, cabCenterZ + cabDepth / 2 - 0.04]}
material={shaftShellMaterial}
position={[shaftWidth / 2 + shaftWallThickness / 2, shaftBodyCenterY, 0]}
receiveShadow
scale={[cabWidth, cabHeight, 0.08]}
slotId="cab"
scale={[shaftWallThickness, shaftBodyHeight, shaftDepth + shaftWallThickness * 2]}
slotId={shaftShellSlotId}
/>
<BoxPrimitive
castShadow
material={CAB_MATERIAL}
position={[-cabWidth / 2 + 0.04, cabHeight / 2, cabCenterZ]}
material={shaftTopMaterial}
position={[0, shaftBaseY + shaftHeight - shaftWallThickness / 2, 0]}
receiveShadow
scale={[0.08, cabHeight, cabDepth]}
slotId="cab"
scale={[
shaftWidth + shaftWallThickness * 2,
shaftWallThickness,
shaftDepth + shaftWallThickness * 2,
]}
slotId="shaft"
/>
<BoxPrimitive
castShadow
material={CAB_MATERIAL}
position={[cabWidth / 2 - 0.04, cabHeight / 2, cabCenterZ]}
receiveShadow
scale={[0.08, cabHeight, cabDepth]}
slotId="cab"
/>
<ElevatorDoorLeaves
animated={{ elevatorId, kind: 'cab' }}
doorOpen={doorOpen}
doorPanelStyle={doorPanelStyle}
doorStyle={doorStyle}
height={doorHeight}
width={doorWidth}
y={0}
z={frontZ}
/>
<ElevatorFloorIndicator
active={indicatorActive}
direction={indicatorDirection}
faceSign={1}
label={indicatorEntry?.label ?? '-'}
position={[0, doorHeight + 0.13, frontZ + 0.055]}
scale={0.78}
/>
<group position={[cabPanelX, cabPanelY, cabPanelZ]} rotation-y={-Math.PI / 2}>
<group ref={cabRef} position={[0, cabBaseY, 0]}>
<BoxPrimitive
castShadow
material={PANEL_MATERIAL}
material={CAB_MATERIAL}
position={[0, 0.04, cabCenterZ]}
receiveShadow
scale={[cabPanelWidth, cabPanelHeight, 0.045]}
scale={[cabWidth, 0.08, cabDepth]}
slotId="cab"
/>
{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
<BoxPrimitive
castShadow
material={CAB_MATERIAL}
position={[0, cabHeight - 0.04, cabCenterZ]}
receiveShadow
scale={[cabWidth, 0.08, cabDepth]}
slotId="cab"
/>
return (
<ElevatorMeshButton
active={!isDisabledLevel && activeLevelId === entry.id}
buttonKind="cab"
disabled={isDisabledLevel}
elevatorId={elevatorId}
faceSign={1}
key={entry.id}
label={entry.label}
levelId={entry.id as AnyNodeId}
position={[x, y, 0.045]}
queued={!isDisabledLevel && queuedLevelIds.has(entry.id)}
/>
)
})}
<ElevatorMeshButton
action="open-door"
active={doorOpenButtonActive}
buttonKind="cab"
elevatorId={elevatorId}
<BoxPrimitive
castShadow
material={CAB_MATERIAL}
position={[0, cabHeight / 2, cabCenterZ + cabDepth / 2 - 0.04]}
receiveShadow
scale={[cabWidth, cabHeight, 0.08]}
slotId="cab"
/>
<BoxPrimitive
castShadow
material={CAB_MATERIAL}
position={[-cabWidth / 2 + 0.04, cabHeight / 2, cabCenterZ]}
receiveShadow
scale={[0.08, cabHeight, cabDepth]}
slotId="cab"
/>
<BoxPrimitive
castShadow
material={CAB_MATERIAL}
position={[cabWidth / 2 - 0.04, cabHeight / 2, cabCenterZ]}
receiveShadow
scale={[0.08, cabHeight, cabDepth]}
slotId="cab"
/>
<ElevatorDoorLeaves
animated={{ elevatorId, kind: 'cab' }}
doorOpen={doorOpen}
doorPanelStyle={doorPanelStyle}
doorStyle={doorStyle}
height={doorHeight}
width={doorWidth}
y={0}
z={frontZ}
/>
<ElevatorFloorIndicator
active={indicatorActive}
direction={indicatorDirection}
faceSign={1}
glyph="door-open"
position={[cabDoorButtonX, cabDoorButtonY, 0.045]}
queued={false}
radius={0.047}
label={indicatorEntry?.label ?? '-'}
position={[0, doorHeight + 0.13, frontZ + 0.055]}
scale={0.78}
/>
</group>
</group>
{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 (
<group key={entry.id}>
<LandingDoorFrame
doorHeight={doorHeight}
doorWidth={doorWidth}
levelTopY={levelTopY}
levelY={entry.baseY}
shaftWidth={shaftWidth}
z={frontWallZ}
<group position={[cabPanelX, cabPanelY, cabPanelZ]} rotation-y={-Math.PI / 2}>
<BoxPrimitive
castShadow
material={PANEL_MATERIAL}
receiveShadow
scale={[cabPanelWidth, cabPanelHeight, 0.045]}
/>
<LandingDoor
animated={isCurrentLevel}
doorPanelStyle={doorPanelStyle}
doorStyle={doorStyle}
{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 (
<ElevatorMeshButton
active={!isDisabledLevel && activeLevelId === entry.id}
buttonKind="cab"
disabled={isDisabledLevel}
elevatorId={elevatorId}
faceSign={1}
key={entry.id}
label={entry.label}
levelId={entry.id as AnyNodeId}
position={[x, y, 0.045]}
queued={!isDisabledLevel && queuedLevelIds.has(entry.id)}
/>
)
})}
<ElevatorMeshButton
action="open-door"
active={doorOpenButtonActive}
buttonKind="cab"
elevatorId={elevatorId}
doorHeight={doorHeight}
doorOpen={isCurrentLevel ? doorOpen : 0}
doorWidth={doorWidth}
levelId={entry.id as AnyNodeId}
levelY={entry.baseY}
z={frontZ - 0.02}
faceSign={1}
glyph="door-open"
position={[cabDoorButtonX, cabDoorButtonY, 0.045]}
queued={false}
radius={0.047}
/>
<ElevatorFloorIndicator
active={showLandingReadout}
direction={showLandingReadout ? indicatorDirection : null}
label={entry.label}
position={[0, entry.baseY + doorHeight + 0.16, frontZ - 0.055]}
scale={0.62}
showReadout={showLandingReadout}
/>
<group position={[landingPanelX, entry.baseY + panelRelativeY, frontZ - 0.035]}>
<BoxPrimitive
castShadow
material={LANDING_PANEL_MATERIAL}
receiveShadow
scale={[0.18, 0.42, 0.04]}
/>
<ElevatorMeshButton
active={!isDisabledLevel && !isServiceOnlyLevel && isCurrentLevel && doorOpen > 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}
/>
<BoxPrimitive
material={isQueuedLevel ? QUEUE_STRIP_MATERIALS.queued : QUEUE_STRIP_MATERIALS.idle}
position={[0, -0.12, -0.035]}
scale={[0.095, 0.025, 0.012]}
/>
</group>
</group>
)
})}
</group>
{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 (
<group key={entry.id}>
<LandingDoorFrame
doorHeight={doorHeight}
doorWidth={doorWidth}
levelTopY={levelTopY}
levelY={entry.baseY}
shaftWidth={shaftWidth}
z={frontWallZ}
/>
<LandingDoor
animated={isCurrentLevel}
doorPanelStyle={doorPanelStyle}
doorStyle={doorStyle}
elevatorId={elevatorId}
doorHeight={doorHeight}
doorOpen={isCurrentLevel ? doorOpen : 0}
doorWidth={doorWidth}
levelId={entry.id as AnyNodeId}
levelY={entry.baseY}
z={frontZ - 0.02}
/>
<ElevatorFloorIndicator
active={showLandingReadout}
direction={showLandingReadout ? indicatorDirection : null}
label={entry.label}
position={[0, entry.baseY + doorHeight + 0.16, frontZ - 0.055]}
scale={0.62}
showReadout={showLandingReadout}
/>
<group position={[landingPanelX, entry.baseY + panelRelativeY, frontZ - 0.035]}>
<BoxPrimitive
castShadow
material={LANDING_PANEL_MATERIAL}
receiveShadow
scale={[0.18, 0.42, 0.04]}
/>
<ElevatorMeshButton
active={
!isDisabledLevel && !isServiceOnlyLevel && isCurrentLevel && doorOpen > 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}
/>
<BoxPrimitive
material={
isQueuedLevel ? QUEUE_STRIP_MATERIALS.queued : QUEUE_STRIP_MATERIALS.idle
}
position={[0, -0.12, -0.035]}
scale={[0.095, 0.025, 0.012]}
/>
</group>
</group>
)
})}
</group>
</ElevatorMaterialsContext.Provider>
)
+2 -1
View File
@@ -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
}
+44 -25
View File
@@ -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 `<GeometrySystem>` 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<FenceSlotId, string> = {
panel: FENCE_PANEL_SLOT_DEFAULT,
rail: FENCE_RAIL_SLOT_DEFAULT,
}
const fenceMaterialCache = new Map<string, Material>()
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
}
+7 -2
View File
@@ -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
+9 -3
View File
@@ -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 },
]
}
+7 -5
View File
@@ -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),
+1 -1
View File
@@ -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'
+1 -3
View File
@@ -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
+1 -3
View File
@@ -160,9 +160,7 @@ export const StairRenderer = ({ node: rawNode }: { node: StairNode }) => {
userData={STAIR_BODY_SLOT_USER_DATA}
/>
) : null}
{isSegmentBasedStair ? null : (
<CurvedStairBody bodyMaterials={bodyMaterials} stair={node} />
)}
{isSegmentBasedStair ? null : <CurvedStairBody bodyMaterials={bodyMaterials} stair={node} />}
<StairRailings material={railingMaterial} stair={node} />
{isSegmentBasedStair ? (
<group name="segments-wrapper" visible={false}>
+1 -1
View File
@@ -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[] {