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:
co-authored by
Claude Opus 4.8
parent
ad227a04a2
commit
e92ee05702
@@ -2288,11 +2288,7 @@ function ColumnBody({ node }: { node: ColumnNode }) {
|
|||||||
<ColumnSlot slotId="shaft">
|
<ColumnSlot slotId="shaft">
|
||||||
<Shaft height={shaftLayout.shaftHeight} node={node} y={shaftLayout.shaftY} />
|
<Shaft height={shaftLayout.shaftHeight} node={node} y={shaftLayout.shaftY} />
|
||||||
<Rings node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
<Rings node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||||
<LatheBands
|
<LatheBands node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||||
node={node}
|
|
||||||
shaftHeight={shaftLayout.shaftHeight}
|
|
||||||
shaftY={shaftLayout.shaftY}
|
|
||||||
/>
|
|
||||||
<Flutes node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
<Flutes node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||||
<LowerCarvedBand
|
<LowerCarvedBand
|
||||||
node={node}
|
node={node}
|
||||||
@@ -2304,11 +2300,7 @@ function ColumnBody({ node }: { node: ColumnNode }) {
|
|||||||
shaftHeight={shaftLayout.shaftHeight}
|
shaftHeight={shaftLayout.shaftHeight}
|
||||||
shaftY={shaftLayout.shaftY}
|
shaftY={shaftLayout.shaftY}
|
||||||
/>
|
/>
|
||||||
<SpiralRibs
|
<SpiralRibs node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||||
node={node}
|
|
||||||
shaftHeight={shaftLayout.shaftHeight}
|
|
||||||
shaftY={shaftLayout.shaftY}
|
|
||||||
/>
|
|
||||||
</ColumnSlot>
|
</ColumnSlot>
|
||||||
<ColumnSlot slotId="capital">
|
<ColumnSlot slotId="capital">
|
||||||
<Capital
|
<Capital
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { buildElevatorFloorplan } from './floorplan'
|
import { buildElevatorFloorplan } from './floorplan'
|
||||||
import { elevatorResizeAffordance, elevatorRotateAffordance } from './floorplan-affordances'
|
import { elevatorResizeAffordance, elevatorRotateAffordance } from './floorplan-affordances'
|
||||||
import { elevatorParametrics } from './parametrics'
|
|
||||||
import { elevatorPaint } from './paint'
|
import { elevatorPaint } from './paint'
|
||||||
|
import { elevatorParametrics } from './parametrics'
|
||||||
import { ElevatorNode } from './schema'
|
import { ElevatorNode } from './schema'
|
||||||
import { elevatorSlots } from './slots'
|
import { elevatorSlots } from './slots'
|
||||||
|
|
||||||
|
|||||||
@@ -1383,218 +1383,222 @@ export const ElevatorRenderer = ({ node }: { node: ElevatorNode }) => {
|
|||||||
visible={renderNode.visible}
|
visible={renderNode.visible}
|
||||||
{...handlers}
|
{...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
|
<BoxPrimitive
|
||||||
castShadow
|
castShadow
|
||||||
material={CAB_MATERIAL}
|
material={shaftShellMaterial}
|
||||||
position={[0, 0.04, cabCenterZ]}
|
position={[0, shaftBodyCenterY, shaftDepth / 2 + shaftWallThickness / 2]}
|
||||||
receiveShadow
|
receiveShadow
|
||||||
scale={[cabWidth, 0.08, cabDepth]}
|
scale={[shaftWidth + shaftWallThickness * 2, shaftBodyHeight, shaftWallThickness]}
|
||||||
slotId="cab"
|
slotId={shaftShellSlotId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BoxPrimitive
|
<BoxPrimitive
|
||||||
castShadow
|
castShadow
|
||||||
material={CAB_MATERIAL}
|
material={shaftShellMaterial}
|
||||||
position={[0, cabHeight - 0.04, cabCenterZ]}
|
position={[-shaftWidth / 2 - shaftWallThickness / 2, shaftBodyCenterY, 0]}
|
||||||
receiveShadow
|
receiveShadow
|
||||||
scale={[cabWidth, 0.08, cabDepth]}
|
scale={[shaftWallThickness, shaftBodyHeight, shaftDepth + shaftWallThickness * 2]}
|
||||||
slotId="cab"
|
slotId={shaftShellSlotId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BoxPrimitive
|
<BoxPrimitive
|
||||||
castShadow
|
castShadow
|
||||||
material={CAB_MATERIAL}
|
material={shaftShellMaterial}
|
||||||
position={[0, cabHeight / 2, cabCenterZ + cabDepth / 2 - 0.04]}
|
position={[shaftWidth / 2 + shaftWallThickness / 2, shaftBodyCenterY, 0]}
|
||||||
receiveShadow
|
receiveShadow
|
||||||
scale={[cabWidth, cabHeight, 0.08]}
|
scale={[shaftWallThickness, shaftBodyHeight, shaftDepth + shaftWallThickness * 2]}
|
||||||
slotId="cab"
|
slotId={shaftShellSlotId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BoxPrimitive
|
<BoxPrimitive
|
||||||
castShadow
|
castShadow
|
||||||
material={CAB_MATERIAL}
|
material={shaftTopMaterial}
|
||||||
position={[-cabWidth / 2 + 0.04, cabHeight / 2, cabCenterZ]}
|
position={[0, shaftBaseY + shaftHeight - shaftWallThickness / 2, 0]}
|
||||||
receiveShadow
|
receiveShadow
|
||||||
scale={[0.08, cabHeight, cabDepth]}
|
scale={[
|
||||||
slotId="cab"
|
shaftWidth + shaftWallThickness * 2,
|
||||||
|
shaftWallThickness,
|
||||||
|
shaftDepth + shaftWallThickness * 2,
|
||||||
|
]}
|
||||||
|
slotId="shaft"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BoxPrimitive
|
<group ref={cabRef} position={[0, cabBaseY, 0]}>
|
||||||
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}>
|
|
||||||
<BoxPrimitive
|
<BoxPrimitive
|
||||||
castShadow
|
castShadow
|
||||||
material={PANEL_MATERIAL}
|
material={CAB_MATERIAL}
|
||||||
|
position={[0, 0.04, cabCenterZ]}
|
||||||
receiveShadow
|
receiveShadow
|
||||||
scale={[cabPanelWidth, cabPanelHeight, 0.045]}
|
scale={[cabWidth, 0.08, cabDepth]}
|
||||||
|
slotId="cab"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{entries.map((entry, index) => {
|
<BoxPrimitive
|
||||||
const column = index % cabButtonColumns
|
castShadow
|
||||||
const row = Math.floor(index / cabButtonColumns)
|
material={CAB_MATERIAL}
|
||||||
const isDisabledLevel = disabledLevelIds.has(entry.id)
|
position={[0, cabHeight - 0.04, cabCenterZ]}
|
||||||
const x =
|
receiveShadow
|
||||||
cabFloorButtonOffsetX + (column - (cabButtonColumns - 1) / 2) * cabButtonSpacingX
|
scale={[cabWidth, 0.08, cabDepth]}
|
||||||
const y = (row - (cabButtonRows - 1) / 2) * cabButtonSpacingY
|
slotId="cab"
|
||||||
|
/>
|
||||||
|
|
||||||
return (
|
<BoxPrimitive
|
||||||
<ElevatorMeshButton
|
castShadow
|
||||||
active={!isDisabledLevel && activeLevelId === entry.id}
|
material={CAB_MATERIAL}
|
||||||
buttonKind="cab"
|
position={[0, cabHeight / 2, cabCenterZ + cabDepth / 2 - 0.04]}
|
||||||
disabled={isDisabledLevel}
|
receiveShadow
|
||||||
elevatorId={elevatorId}
|
scale={[cabWidth, cabHeight, 0.08]}
|
||||||
faceSign={1}
|
slotId="cab"
|
||||||
key={entry.id}
|
/>
|
||||||
label={entry.label}
|
|
||||||
levelId={entry.id as AnyNodeId}
|
<BoxPrimitive
|
||||||
position={[x, y, 0.045]}
|
castShadow
|
||||||
queued={!isDisabledLevel && queuedLevelIds.has(entry.id)}
|
material={CAB_MATERIAL}
|
||||||
/>
|
position={[-cabWidth / 2 + 0.04, cabHeight / 2, cabCenterZ]}
|
||||||
)
|
receiveShadow
|
||||||
})}
|
scale={[0.08, cabHeight, cabDepth]}
|
||||||
<ElevatorMeshButton
|
slotId="cab"
|
||||||
action="open-door"
|
/>
|
||||||
active={doorOpenButtonActive}
|
|
||||||
buttonKind="cab"
|
<BoxPrimitive
|
||||||
elevatorId={elevatorId}
|
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}
|
faceSign={1}
|
||||||
glyph="door-open"
|
label={indicatorEntry?.label ?? '-'}
|
||||||
position={[cabDoorButtonX, cabDoorButtonY, 0.045]}
|
position={[0, doorHeight + 0.13, frontZ + 0.055]}
|
||||||
queued={false}
|
scale={0.78}
|
||||||
radius={0.047}
|
|
||||||
/>
|
/>
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
{entrySpans.map(({ entry, levelTopY }) => {
|
<group position={[cabPanelX, cabPanelY, cabPanelZ]} rotation-y={-Math.PI / 2}>
|
||||||
const isCurrentLevel = activeLevelId === entry.id
|
<BoxPrimitive
|
||||||
const isDisabledLevel = disabledLevelIds.has(entry.id)
|
castShadow
|
||||||
const isServiceOnlyLevel = serviceOnlyLevelIds.has(entry.id)
|
material={PANEL_MATERIAL}
|
||||||
const isQueuedLevel = !isDisabledLevel && queuedLevelIds.has(entry.id)
|
receiveShadow
|
||||||
const isPendingLevel = pendingLevelId === entry.id
|
scale={[cabPanelWidth, cabPanelHeight, 0.045]}
|
||||||
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}
|
{entries.map((entry, index) => {
|
||||||
doorPanelStyle={doorPanelStyle}
|
const column = index % cabButtonColumns
|
||||||
doorStyle={doorStyle}
|
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}
|
elevatorId={elevatorId}
|
||||||
doorHeight={doorHeight}
|
faceSign={1}
|
||||||
doorOpen={isCurrentLevel ? doorOpen : 0}
|
glyph="door-open"
|
||||||
doorWidth={doorWidth}
|
position={[cabDoorButtonX, cabDoorButtonY, 0.045]}
|
||||||
levelId={entry.id as AnyNodeId}
|
queued={false}
|
||||||
levelY={entry.baseY}
|
radius={0.047}
|
||||||
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>
|
||||||
)
|
</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>
|
</group>
|
||||||
</ElevatorMaterialsContext.Provider>
|
</ElevatorMaterialsContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export function elevatorSlots(node: ElevatorNode): SlotDeclaration[] {
|
|||||||
getResolvedElevatorShaftStyle(node.shaftStyle) === 'glass' ||
|
getResolvedElevatorShaftStyle(node.shaftStyle) === 'glass' ||
|
||||||
getResolvedElevatorDoorPanelStyle(node.doorPanelStyle) === 'glass-frame'
|
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
|
return slots
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,30 +5,30 @@ import {
|
|||||||
createDefaultMaterial,
|
createDefaultMaterial,
|
||||||
createMaterial,
|
createMaterial,
|
||||||
createSurfaceRoleMaterial,
|
createSurfaceRoleMaterial,
|
||||||
generateFenceGeometry,
|
generateFenceSlotGeometries,
|
||||||
type RenderShading,
|
type RenderShading,
|
||||||
resolveMaterialRef,
|
resolveMaterialRef,
|
||||||
resolveSlotDefaultMaterial,
|
resolveSlotDefaultMaterial,
|
||||||
} from '@pascal-app/viewer'
|
} from '@pascal-app/viewer'
|
||||||
import { FrontSide, Group, type Material, Mesh, type Texture } from 'three'
|
import { FrontSide, Group, type Material, Mesh, type Texture } from 'three'
|
||||||
import type { FenceNode } from './schema'
|
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`
|
* Stage B builder for fence. Splits the geometry into two paintable slots —
|
||||||
* (pure function from viewer that returns a merged BufferGeometry of
|
* `panel` (posts / base / infill) and `rail` (the cap rail) — each its own Mesh
|
||||||
* posts + base + top rail + curve spans) and wraps it in a Mesh-in-Group
|
* with a `userData.slotId` so the unified slot paint resolves and previews per
|
||||||
* shape the generic `<GeometrySystem>` expects.
|
* part.
|
||||||
*
|
*
|
||||||
* Materials follow the unified slot model: the single `surface` slot resolves
|
* Per slot the material resolves: `node.slots[slotId]` (a shared scene material
|
||||||
* `node.slots.surface` (a shared scene material or `library:` finish) → the
|
* or `library:` finish) → the legacy inline `node.material` / `materialPreset`
|
||||||
* legacy inline `node.material` / `materialPreset` (pre-slot-model scenes) →
|
* (pre-slot-model scenes, applied to both parts) → the declared slot default.
|
||||||
* the declared slot default. Textures-off collapses to themed joinery.
|
* Textures-off collapses both parts to the themed joinery role.
|
||||||
*
|
*
|
||||||
* Phase 6 cleanup moves the 280 lines of geometry math out of the
|
* Phase 6 cleanup moves the geometry math out of the legacy
|
||||||
* legacy `viewer/src/systems/fence/fence-system.tsx` into this folder
|
* `viewer/src/systems/fence/fence-system.tsx` into this folder once the legacy
|
||||||
* once the legacy system file is deleted. Until then `generateFenceGeometry`
|
* system file is deleted. Until then `generateFenceSlotGeometries` is publicly
|
||||||
* is publicly re-exported from viewer.
|
* re-exported from viewer.
|
||||||
*/
|
*/
|
||||||
type FenceMaterial = Material & {
|
type FenceMaterial = Material & {
|
||||||
alphaMap?: Texture | null
|
alphaMap?: Texture | null
|
||||||
@@ -37,10 +37,16 @@ type FenceMaterial = Material & {
|
|||||||
transparent: boolean
|
transparent: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SLOT_DEFAULTS: Record<FenceSlotId, string> = {
|
||||||
|
panel: FENCE_PANEL_SLOT_DEFAULT,
|
||||||
|
rail: FENCE_RAIL_SLOT_DEFAULT,
|
||||||
|
}
|
||||||
|
|
||||||
const fenceMaterialCache = new Map<string, Material>()
|
const fenceMaterialCache = new Map<string, Material>()
|
||||||
|
|
||||||
function getFenceMaterial(
|
function getFenceSlotMaterial(
|
||||||
node: FenceNode,
|
node: FenceNode,
|
||||||
|
slotId: FenceSlotId,
|
||||||
shading: RenderShading,
|
shading: RenderShading,
|
||||||
textures: boolean,
|
textures: boolean,
|
||||||
colorPreset: ColorPreset,
|
colorPreset: ColorPreset,
|
||||||
@@ -51,7 +57,7 @@ function getFenceMaterial(
|
|||||||
return createSurfaceRoleMaterial('joinery', colorPreset, FrontSide, sceneTheme)
|
return createSurfaceRoleMaterial('joinery', colorPreset, FrontSide, sceneTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
const slotRef = node.slots?.surface
|
const slotRef = node.slots?.[slotId]
|
||||||
if (slotRef) {
|
if (slotRef) {
|
||||||
const resolved = resolveMaterialRef(slotRef, sceneMaterials, shading)
|
const resolved = resolveMaterialRef(slotRef, sceneMaterials, shading)
|
||||||
if (resolved) return resolved
|
if (resolved) return resolved
|
||||||
@@ -61,7 +67,7 @@ function getFenceMaterial(
|
|||||||
return getLegacyFenceMaterial(node, shading)
|
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 {
|
function getLegacyFenceMaterial(node: FenceNode, shading: RenderShading): Material {
|
||||||
@@ -105,13 +111,26 @@ export function buildFenceGeometry(
|
|||||||
sceneTheme?: string,
|
sceneTheme?: string,
|
||||||
): Group {
|
): Group {
|
||||||
const group = new Group()
|
const group = new Group()
|
||||||
const geometry = generateFenceGeometry(node)
|
const geometries = generateFenceSlotGeometries(node)
|
||||||
const material = getFenceMaterial(node, shading, textures, colorPreset, sceneTheme, ctx?.materials)
|
|
||||||
const mesh = new Mesh(geometry, material)
|
for (const slotId of ['panel', 'rail'] as const) {
|
||||||
mesh.castShadow = true
|
const geometry = geometries[slotId]
|
||||||
mesh.receiveShadow = true
|
if (geometry.getAttribute('position') === undefined) continue
|
||||||
mesh.userData.slotId = 'surface'
|
const material = getFenceSlotMaterial(
|
||||||
mesh.userData.surfaceRole = 'joinery'
|
node,
|
||||||
group.add(mesh)
|
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
|
return group
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
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({
|
export const fencePaint = createSlotPaintCapability({
|
||||||
resolveRole: () => 'surface',
|
resolveRole: resolveFenceRole,
|
||||||
applyPreview: previewGeometrySlot,
|
applyPreview: previewGeometrySlot,
|
||||||
legacyEffective: (node: AnyNode) => {
|
legacyEffective: (node: AnyNode) => {
|
||||||
const fence = node as FenceNode
|
const fence = node as FenceNode
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import type { SlotDeclaration } from '@pascal-app/core'
|
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[] {
|
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 },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import {
|
|||||||
type ColorPreset,
|
type ColorPreset,
|
||||||
createDefaultMaterial,
|
createDefaultMaterial,
|
||||||
createSurfaceRoleMaterial,
|
createSurfaceRoleMaterial,
|
||||||
resolveSlotDefaultMaterial,
|
|
||||||
type RenderShading,
|
type RenderShading,
|
||||||
|
resolveSlotDefaultMaterial,
|
||||||
} from '@pascal-app/viewer'
|
} from '@pascal-app/viewer'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
|
|
||||||
@@ -22,10 +22,12 @@ export function getRoofMaterials(
|
|||||||
|
|
||||||
const materials = textures
|
const materials = textures
|
||||||
? [
|
? [
|
||||||
createDefaultMaterial('white', 1, shading, THREE.DoubleSide), // 0: Wall/Trim
|
// Mirrors getRoofMaterialArray's catalog defaults (wall/trim concrete,
|
||||||
createDefaultMaterial('#e5e5e5', 1, shading, THREE.FrontSide), // 1: Deck
|
// soft-white deck + soffit, terracotta shingle) for the no-parent path.
|
||||||
createDefaultMaterial('white', 1, shading, THREE.DoubleSide), // 2: Interior
|
resolveSlotDefaultMaterial('library:concrete-plate', shading), // 0: Wall/Trim
|
||||||
resolveSlotDefaultMaterial('library:roof-weatheredshingles', shading), // 3: Shingle
|
resolveSlotDefaultMaterial('library:preset-softwhite', shading), // 1: Deck
|
||||||
|
resolveSlotDefaultMaterial('library:preset-softwhite', shading), // 2: Interior
|
||||||
|
resolveSlotDefaultMaterial('library:roof-terracottatiles', shading), // 3: Shingle
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
createSurfaceRoleMaterial('roof', colorPreset),
|
createSurfaceRoleMaterial('roof', colorPreset),
|
||||||
|
|||||||
@@ -406,8 +406,8 @@ import {
|
|||||||
stairRotateAffordance,
|
stairRotateAffordance,
|
||||||
} from './floorplan-affordances'
|
} from './floorplan-affordances'
|
||||||
import { stairFloorplanMoveTarget } from './floorplan-move'
|
import { stairFloorplanMoveTarget } from './floorplan-move'
|
||||||
import { stairParametrics } from './parametrics'
|
|
||||||
import { stairPaint } from './paint'
|
import { stairPaint } from './paint'
|
||||||
|
import { stairParametrics } from './parametrics'
|
||||||
import { StairNode } from './schema'
|
import { StairNode } from './schema'
|
||||||
import { stairSlots } from './slots'
|
import { stairSlots } from './slots'
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ function isStairSlotId(value: unknown): value is StairSlotId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveStairPaintRole(args: PaintResolveArgs): StairSlotId | null {
|
function resolveStairPaintRole(args: PaintResolveArgs): StairSlotId | null {
|
||||||
const userData = args.hitObject?.userData as
|
const userData = args.hitObject?.userData as { slotId?: unknown; slotIds?: unknown } | undefined
|
||||||
| { slotId?: unknown; slotIds?: unknown }
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
if (isStairSlotId(userData?.slotId)) {
|
if (isStairSlotId(userData?.slotId)) {
|
||||||
return userData.slotId
|
return userData.slotId
|
||||||
|
|||||||
@@ -160,9 +160,7 @@ export const StairRenderer = ({ node: rawNode }: { node: StairNode }) => {
|
|||||||
userData={STAIR_BODY_SLOT_USER_DATA}
|
userData={STAIR_BODY_SLOT_USER_DATA}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{isSegmentBasedStair ? null : (
|
{isSegmentBasedStair ? null : <CurvedStairBody bodyMaterials={bodyMaterials} stair={node} />}
|
||||||
<CurvedStairBody bodyMaterials={bodyMaterials} stair={node} />
|
|
||||||
)}
|
|
||||||
<StairRailings material={railingMaterial} stair={node} />
|
<StairRailings material={railingMaterial} stair={node} />
|
||||||
{isSegmentBasedStair ? (
|
{isSegmentBasedStair ? (
|
||||||
<group name="segments-wrapper" visible={false}>
|
<group name="segments-wrapper" visible={false}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { SlotDeclaration, StairNode } from '@pascal-app/core'
|
|||||||
export type StairSlotId = 'treads' | 'body' | 'railing'
|
export type StairSlotId = 'treads' | 'body' | 'railing'
|
||||||
|
|
||||||
export const STAIR_TREADS_SLOT_DEFAULT = 'library:wood-woodplank48'
|
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 const STAIR_RAILING_SLOT_DEFAULT = 'library:metal-steel'
|
||||||
|
|
||||||
export function stairSlots(node: StairNode): SlotDeclaration[] {
|
export function stairSlots(node: StairNode): SlotDeclaration[] {
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ export { ElevatorInteractionSystem } from './systems/elevator/elevator-interacti
|
|||||||
// Fence system follows the wall re-export pattern — composed into the
|
// Fence system follows the wall re-export pattern — composed into the
|
||||||
// registry-driven fence definition's `def.system`. Removed in Phase 6
|
// registry-driven fence definition's `def.system`. Removed in Phase 6
|
||||||
// alongside the legacy fence mount point.
|
// 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
|
// Generic floor-elevation system. Lifts the rendered mesh of any kind
|
||||||
// whose definition declares `capabilities.floorPlaced` by the slab
|
// whose definition declares `capabilities.floorPlaced` by the slab
|
||||||
// elevation under its footprint. Replaces the per-kind elevation block
|
// elevation under its footprint. Replaces the per-kind elevation block
|
||||||
|
|||||||
@@ -100,16 +100,13 @@ function applyFenceUVs(geometry: THREE.BufferGeometry) {
|
|||||||
|
|
||||||
if (!(position && normal)) return
|
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)
|
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) {
|
for (let index = 0; index < position.count; index += 1) {
|
||||||
const px = position.getX(index)
|
const px = position.getX(index)
|
||||||
@@ -123,14 +120,14 @@ function applyFenceUVs(geometry: THREE.BufferGeometry) {
|
|||||||
let v = 0
|
let v = 0
|
||||||
|
|
||||||
if (ny >= nx && ny >= nz) {
|
if (ny >= nx && ny >= nz) {
|
||||||
u = px - minX
|
u = px
|
||||||
v = pz - minZ
|
v = pz
|
||||||
} else if (nx >= nz) {
|
} else if (nx >= nz) {
|
||||||
u = pz - minZ
|
u = pz
|
||||||
v = py - minY
|
v = py
|
||||||
} else {
|
} else {
|
||||||
u = px - minX
|
u = px
|
||||||
v = py - minY
|
v = py
|
||||||
}
|
}
|
||||||
|
|
||||||
uvs[index * 2] = u
|
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 }
|
return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 }
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFenceParts(fence: FenceNode): FencePart[] {
|
export type FenceSlotParts = {
|
||||||
const parts: FencePart[] = []
|
/** 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 length = Math.max(getWallCurveLength(fence), 0.01)
|
||||||
const panelDepth = Math.max(fence.thickness, 0.03)
|
const panelDepth = Math.max(fence.thickness, 0.03)
|
||||||
const clearance = Math.max(fence.groundClearance, 0)
|
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)
|
const endInsetT = Math.max(0.501, 1 - edgeInset / length)
|
||||||
|
|
||||||
if (!isFloating) {
|
if (!isFloating) {
|
||||||
parts.push(
|
panel.push(
|
||||||
...createFenceCurveSpanParts(
|
...createFenceCurveSpanParts(
|
||||||
fence,
|
fence,
|
||||||
0,
|
0,
|
||||||
@@ -183,7 +188,7 @@ function createFenceParts(fence: FenceNode): FencePart[] {
|
|||||||
panelDepth * 1.05,
|
panelDepth * 1.05,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parts.push(
|
panel.push(
|
||||||
...createFenceCurveSpanParts(
|
...createFenceCurveSpanParts(
|
||||||
fence,
|
fence,
|
||||||
0,
|
0,
|
||||||
@@ -208,14 +213,14 @@ function createFenceParts(fence: FenceNode): FencePart[] {
|
|||||||
: verticalHeight
|
: verticalHeight
|
||||||
const postY = fullHeightPost ? postHeight / 2 : verticalY
|
const postY = fullHeightPost ? postHeight / 2 : verticalY
|
||||||
|
|
||||||
parts.push({
|
panel.push({
|
||||||
position: [frame.point.x, postY, frame.point.y],
|
position: [frame.point.x, postY, frame.point.y],
|
||||||
rotationY: -frame.tangentAngle,
|
rotationY: -frame.tangentAngle,
|
||||||
scale: [postWidth, postHeight, Math.max(panelDepth * 0.35, 0.012)],
|
scale: [postWidth, postHeight, Math.max(panelDepth * 0.35, 0.012)],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push(
|
rail.push(
|
||||||
...createFenceCurveSpanParts(
|
...createFenceCurveSpanParts(
|
||||||
fence,
|
fence,
|
||||||
0,
|
0,
|
||||||
@@ -227,7 +232,7 @@ function createFenceParts(fence: FenceNode): FencePart[] {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (isFloating) {
|
if (isFloating) {
|
||||||
parts.push(
|
rail.push(
|
||||||
...createFenceCurveSpanParts(
|
...createFenceCurveSpanParts(
|
||||||
fence,
|
fence,
|
||||||
0,
|
0,
|
||||||
@@ -239,13 +244,11 @@ function createFenceParts(fence: FenceNode): FencePart[] {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts
|
return { panel, rail }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateFenceGeometry(fence: FenceNode) {
|
function mergeFenceParts(parts: FencePart[]): THREE.BufferGeometry {
|
||||||
const parts = createFenceParts(fence)
|
|
||||||
const geometries = parts.map(createFencePartGeometry)
|
const geometries = parts.map(createFencePartGeometry)
|
||||||
|
|
||||||
const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry()
|
const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry()
|
||||||
geometries.forEach((geometry) => {
|
geometries.forEach((geometry) => {
|
||||||
geometry.dispose()
|
geometry.dispose()
|
||||||
@@ -258,6 +261,24 @@ export function generateFenceGeometry(fence: FenceNode) {
|
|||||||
return merged
|
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']) {
|
function updateFenceGeometry(fenceId: FenceNode['id']) {
|
||||||
const node = useScene.getState().nodes[fenceId]
|
const node = useScene.getState().nodes[fenceId]
|
||||||
if (!node || node.type !== 'fence') return
|
if (!node || node.type !== 'fence') return
|
||||||
|
|||||||
@@ -10,8 +10,20 @@ import {
|
|||||||
createMaterialFromPresetRef,
|
createMaterialFromPresetRef,
|
||||||
createSurfaceRoleMaterial,
|
createSurfaceRoleMaterial,
|
||||||
type RenderShading,
|
type RenderShading,
|
||||||
|
resolveSlotDefaultMaterial,
|
||||||
} from '../../lib/materials'
|
} 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]
|
export type RoofMaterialArray = [THREE.Material, THREE.Material, THREE.Material, THREE.Material]
|
||||||
|
|
||||||
const roofMaterialArrayCache = new Map<string, RoofMaterialArray>()
|
const roofMaterialArrayCache = new Map<string, RoofMaterialArray>()
|
||||||
@@ -77,30 +89,42 @@ export function getRoofMaterialArray(
|
|||||||
roofMaterial,
|
roofMaterial,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Textures-off (monochrome) is the guaranteed escape hatch: themed role
|
||||||
|
// colours, no catalog finishes.
|
||||||
if (!textures) {
|
if (!textures) {
|
||||||
roofMaterialArrayCache.set(cacheKey, roleArray)
|
roofMaterialArrayCache.set(cacheKey, roleArray)
|
||||||
return 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 topMaterial = createResolvedMaterial(top.material, top.materialPreset, shading)
|
||||||
const edgeMaterial = createResolvedMaterial(edge.material, edge.materialPreset, shading)
|
const edgeMaterial = createResolvedMaterial(edge.material, edge.materialPreset, shading)
|
||||||
const wallMaterial = createResolvedMaterial(wall.material, wall.materialPreset, shading)
|
const wallMaterial = createResolvedMaterial(wall.material, wall.materialPreset, shading)
|
||||||
|
|
||||||
if (!(topMaterial || edgeMaterial || wallMaterial)) {
|
if (!(topMaterial || edgeMaterial || wallMaterial)) {
|
||||||
roofMaterialArrayCache.set(cacheKey, roleArray)
|
roofMaterialArrayCache.set(cacheKey, defaultArray)
|
||||||
return roleArray
|
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
|
// 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
|
// surface (e.g. the edge) across the shingle and soffit slots. The legacy
|
||||||
// catch-all still fills every role because `getEffectiveRoofSurfaceMaterial`
|
// catch-all still fills every role because `getEffectiveRoofSurfaceMaterial`
|
||||||
// returns it for top/edge/wall alike.
|
// returns it for top/edge/wall alike.
|
||||||
const materialArray: RoofMaterialArray = [
|
const materialArray: RoofMaterialArray = [
|
||||||
edgeMaterial ?? roofMaterial,
|
edgeMaterial ?? defaultArray[0],
|
||||||
wallMaterial ?? ceilingMaterial,
|
wallMaterial ?? defaultArray[1],
|
||||||
wallMaterial ?? ceilingMaterial,
|
wallMaterial ?? defaultArray[2],
|
||||||
topMaterial ?? roofMaterial,
|
topMaterial ?? defaultArray[3],
|
||||||
]
|
]
|
||||||
|
|
||||||
roofMaterialArrayCache.set(cacheKey, materialArray)
|
roofMaterialArrayCache.set(cacheKey, materialArray)
|
||||||
|
|||||||
Reference in New Issue
Block a user