Harden stair reload and spiral post-processing
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
import { type AnyNodeId, type StairNode, type StairSegmentNode, useRegistry, useScene } from '@pascal-app/core'
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type StairNode,
|
||||
type StairSegmentNode,
|
||||
useRegistry,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import { createMaterial, createMaterialFromPresetRef, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials'
|
||||
import {
|
||||
createMaterial,
|
||||
createMaterialFromPresetRef,
|
||||
DEFAULT_STAIR_MATERIAL,
|
||||
} from '../../../lib/materials'
|
||||
import { NodeRenderer } from '../node-renderer'
|
||||
|
||||
type SegmentTransform = {
|
||||
@@ -37,6 +47,7 @@ type LandingChainNextStair = {
|
||||
|
||||
export const StairRenderer = ({ node }: { node: StairNode }) => {
|
||||
const ref = useRef<THREE.Group>(null!)
|
||||
const isSegmentBasedStair = node.stairType === 'straight'
|
||||
|
||||
useRegistry(node.id, 'stair', ref)
|
||||
|
||||
@@ -52,7 +63,13 @@ export const StairRenderer = ({ node }: { node: StairNode }) => {
|
||||
const mat = node.material
|
||||
if (!mat) return DEFAULT_STAIR_MATERIAL
|
||||
return createMaterial(mat)
|
||||
}, [node.materialPreset, node.material, node.material?.preset, node.material?.properties, node.material?.texture])
|
||||
}, [
|
||||
node.materialPreset,
|
||||
node.material,
|
||||
node.material?.preset,
|
||||
node.material?.properties,
|
||||
node.material?.texture,
|
||||
])
|
||||
|
||||
return (
|
||||
<group
|
||||
@@ -63,18 +80,20 @@ export const StairRenderer = ({ node }: { node: StairNode }) => {
|
||||
visible={node.visible}
|
||||
{...handlers}
|
||||
>
|
||||
<mesh castShadow material={material} name="merged-stair" receiveShadow>
|
||||
<boxGeometry args={[0, 0, 0]} />
|
||||
</mesh>
|
||||
{node.stairType === 'curved' || node.stairType === 'spiral' ? (
|
||||
<CurvedStairBody material={material} stair={node} />
|
||||
{isSegmentBasedStair ? (
|
||||
<mesh castShadow material={material} name="merged-stair" receiveShadow>
|
||||
<boxGeometry args={[0, 0, 0]} />
|
||||
</mesh>
|
||||
) : null}
|
||||
{!isSegmentBasedStair ? <CurvedStairBody material={material} stair={node} /> : null}
|
||||
<StairRailings material={material} stair={node} />
|
||||
<group name="segments-wrapper" visible={false}>
|
||||
{(node.children ?? []).map((childId) => (
|
||||
<NodeRenderer key={childId} nodeId={childId} />
|
||||
))}
|
||||
</group>
|
||||
{isSegmentBasedStair ? (
|
||||
<group name="segments-wrapper" visible={false}>
|
||||
{(node.children ?? []).map((childId) => (
|
||||
<NodeRenderer key={childId} nodeId={childId} />
|
||||
))}
|
||||
</group>
|
||||
) : null}
|
||||
</group>
|
||||
)
|
||||
}
|
||||
@@ -86,11 +105,17 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
||||
() =>
|
||||
(stair.children ?? [])
|
||||
.map((childId) => nodes[childId as AnyNodeId] as StairSegmentNode | undefined)
|
||||
.filter((node): node is StairSegmentNode => node?.type === 'stair-segment' && node.visible !== false),
|
||||
.filter(
|
||||
(node): node is StairSegmentNode =>
|
||||
node?.type === 'stair-segment' && node.visible !== false,
|
||||
),
|
||||
[nodes, stair.children],
|
||||
)
|
||||
|
||||
const railPaths = useMemo(() => buildStairRailPaths(segments, stair.railingMode ?? 'none'), [segments, stair.railingMode])
|
||||
const railPaths = useMemo(
|
||||
() => buildStairRailPaths(segments, stair.railingMode ?? 'none'),
|
||||
[segments, stair.railingMode],
|
||||
)
|
||||
|
||||
const railHeight = stair.railingHeight ?? 0.92
|
||||
const midRailHeight = Math.max(railHeight * 0.45, 0.35)
|
||||
@@ -103,10 +128,14 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
||||
|
||||
if (stair.stairType === 'curved' || stair.stairType === 'spiral') {
|
||||
const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10))
|
||||
const sweepAngle = stair.sweepAngle ?? (stair.stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2)
|
||||
const sweepAngle =
|
||||
stair.sweepAngle ?? (stair.stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2)
|
||||
const stepSweep = sweepAngle / stepCount
|
||||
const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / stepCount
|
||||
const innerRadius = Math.max(stair.stairType === 'spiral' ? 0.05 : 0.2, stair.innerRadius ?? 0.9)
|
||||
const innerRadius = Math.max(
|
||||
stair.stairType === 'spiral' ? 0.05 : 0.2,
|
||||
stair.innerRadius ?? 0.9,
|
||||
)
|
||||
const outerRadius = innerRadius + Math.max(stair.width ?? 1, 0.4)
|
||||
const leftRadius = sweepAngle >= 0 ? innerRadius + 0.04 : outerRadius - 0.04
|
||||
const rightRadius = sweepAngle >= 0 ? outerRadius - 0.04 : innerRadius + 0.04
|
||||
@@ -183,7 +212,11 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
||||
{railPaths.map((segmentPath, index) => (
|
||||
<group
|
||||
key={`${segmentPath.layout.segment.id}-railing`}
|
||||
position={[segmentPath.layout.center[0], segmentPath.layout.elevation, segmentPath.layout.center[1]]}
|
||||
position={[
|
||||
segmentPath.layout.center[0],
|
||||
segmentPath.layout.elevation,
|
||||
segmentPath.layout.center[1],
|
||||
]}
|
||||
rotation-y={segmentPath.layout.rotation}
|
||||
>
|
||||
{segmentPath.sidePaths.map((sidePath, sideIndex) => (
|
||||
@@ -204,7 +237,9 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
||||
if (!nextPoint) return null
|
||||
|
||||
return (
|
||||
<group key={`${segmentPath.layout.segment.id}-${sidePath.side}-rail-${pointIndex}`}>
|
||||
<group
|
||||
key={`${segmentPath.layout.segment.id}-${sidePath.side}-rail-${pointIndex}`}
|
||||
>
|
||||
<RailSegment
|
||||
end={[nextPoint[2], nextPoint[1] + railHeight, nextPoint[0]]}
|
||||
material={material}
|
||||
@@ -240,14 +275,15 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
||||
const lastPoint = entry.points[entry.points.length - 1]
|
||||
return {
|
||||
entry,
|
||||
distance: lastPoint ? distance3(toWorldRailPoint(previousPath.layout, lastPoint), currentWorldPoint) : Number.POSITIVE_INFINITY,
|
||||
distance: lastPoint
|
||||
? distance3(toWorldRailPoint(previousPath.layout, lastPoint), currentWorldPoint)
|
||||
: Number.POSITIVE_INFINITY,
|
||||
}
|
||||
})
|
||||
.sort((left, right) => left.distance - right.distance)[0]?.entry
|
||||
const previousPoint =
|
||||
previousSidePath && previousSidePath.points.length
|
||||
? previousSidePath.points[previousSidePath.points.length - 1]
|
||||
: null
|
||||
const previousPoint = previousSidePath?.points.length
|
||||
? previousSidePath.points[previousSidePath.points.length - 1]
|
||||
: null
|
||||
|
||||
if (!(previousPoint && currentPoint)) {
|
||||
return null
|
||||
@@ -256,18 +292,36 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
||||
const previousWorldPoint = toWorldRailPoint(previousPath.layout, previousPoint)
|
||||
|
||||
return (
|
||||
<group key={`${previousPath.layout.segment.id}-${segmentPath.layout.segment.id}-${sideIndex}`}>
|
||||
<group
|
||||
key={`${previousPath.layout.segment.id}-${segmentPath.layout.segment.id}-${sideIndex}`}
|
||||
>
|
||||
<RailSegment
|
||||
end={[currentWorldPoint[0], currentWorldPoint[1] + railHeight, currentWorldPoint[2]]}
|
||||
end={[
|
||||
currentWorldPoint[0],
|
||||
currentWorldPoint[1] + railHeight,
|
||||
currentWorldPoint[2],
|
||||
]}
|
||||
material={material}
|
||||
radius={railRadius}
|
||||
start={[previousWorldPoint[0], previousWorldPoint[1] + railHeight, previousWorldPoint[2]]}
|
||||
start={[
|
||||
previousWorldPoint[0],
|
||||
previousWorldPoint[1] + railHeight,
|
||||
previousWorldPoint[2],
|
||||
]}
|
||||
/>
|
||||
<RailSegment
|
||||
end={[currentWorldPoint[0], currentWorldPoint[1] + midRailHeight, currentWorldPoint[2]]}
|
||||
end={[
|
||||
currentWorldPoint[0],
|
||||
currentWorldPoint[1] + midRailHeight,
|
||||
currentWorldPoint[2],
|
||||
]}
|
||||
material={material}
|
||||
radius={railRadius * 0.8}
|
||||
start={[previousWorldPoint[0], previousWorldPoint[1] + midRailHeight, previousWorldPoint[2]]}
|
||||
start={[
|
||||
previousWorldPoint[0],
|
||||
previousWorldPoint[1] + midRailHeight,
|
||||
previousWorldPoint[2],
|
||||
]}
|
||||
/>
|
||||
</group>
|
||||
)
|
||||
@@ -296,10 +350,17 @@ function RailSegment({
|
||||
const direction = useMemo(() => endVector.clone().sub(startVector), [endVector, startVector])
|
||||
const length = Math.max(direction.length(), 0.01)
|
||||
const quaternion = useMemo(
|
||||
() => new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize()),
|
||||
() =>
|
||||
new THREE.Quaternion().setFromUnitVectors(
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
direction.clone().normalize(),
|
||||
),
|
||||
[direction],
|
||||
)
|
||||
const midpoint = useMemo(() => startVector.clone().add(endVector).multiplyScalar(0.5), [endVector, startVector])
|
||||
const midpoint = useMemo(
|
||||
() => startVector.clone().add(endVector).multiplyScalar(0.5),
|
||||
[endVector, startVector],
|
||||
)
|
||||
|
||||
return (
|
||||
<mesh
|
||||
@@ -327,11 +388,16 @@ function CurvedStairBody({ stair, material }: { stair: StairNode; material: THRE
|
||||
const fillToFloor = stair.fillToFloor ?? true
|
||||
const spiralColumnRadius = Math.max(0.05, Math.min(innerRadius * 0.72, innerRadius - 0.03))
|
||||
const spiralColumnHeight = totalRise + thickness
|
||||
const spiralLandingDepth = Math.max(0.3, stair.topLandingDepth ?? Math.max((stair.width ?? 1) * 0.9, 0.8))
|
||||
const spiralLandingDepth = Math.max(
|
||||
0.3,
|
||||
stair.topLandingDepth ?? Math.max((stair.width ?? 1) * 0.9, 0.8),
|
||||
)
|
||||
const spiralLandingSweep =
|
||||
isSpiral && (stair.topLandingMode ?? 'none') === 'integrated'
|
||||
? Math.min(Math.PI * 0.75, spiralLandingDepth / Math.max(innerRadius + (stair.width ?? 1) / 2, 0.1)) *
|
||||
Math.sign(sweepAngle || 1)
|
||||
? Math.min(
|
||||
Math.PI * 0.75,
|
||||
spiralLandingDepth / Math.max(innerRadius + (stair.width ?? 1) / 2, 0.1),
|
||||
) * Math.sign(sweepAngle || 1)
|
||||
: 0
|
||||
const spiralLastStepTop = stepHeight * Math.max(stepCount - 1, 0) + thickness
|
||||
const spiralLandingThickness =
|
||||
@@ -342,28 +408,52 @@ function CurvedStairBody({ stair, material }: { stair: StairNode; material: THRE
|
||||
return (
|
||||
<group name={isSpiral ? 'spiral-stair' : 'curved-stair'}>
|
||||
{isSpiral && (stair.showCenterColumn ?? true) ? (
|
||||
<mesh castShadow receiveShadow material={material} position={[0, spiralColumnHeight / 2, 0]}>
|
||||
<cylinderGeometry args={[spiralColumnRadius, spiralColumnRadius, spiralColumnHeight, 10]} />
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
material={material}
|
||||
position={[0, spiralColumnHeight / 2, 0]}
|
||||
>
|
||||
<cylinderGeometry
|
||||
args={[spiralColumnRadius, spiralColumnRadius, spiralColumnHeight, 10]}
|
||||
/>
|
||||
</mesh>
|
||||
) : null}
|
||||
{Array.from({ length: stepCount }).map((_, index) => {
|
||||
const currentHeight = stepHeight * (index + 1)
|
||||
const actualStepHeight = isSpiral ? thickness : fillToFloor ? Math.max(currentHeight, thickness) : thickness
|
||||
const actualStepHeight = isSpiral
|
||||
? thickness
|
||||
: fillToFloor
|
||||
? Math.max(currentHeight, thickness)
|
||||
: thickness
|
||||
const startAngle = -sweepAngle / 2 + stepSweep * index
|
||||
const endAngle = startAngle + stepSweep
|
||||
const stepY = isSpiral ? stepHeight * index : fillToFloor ? 0 : Math.max(currentHeight - thickness, 0)
|
||||
const stepY = isSpiral
|
||||
? stepHeight * index
|
||||
: fillToFloor
|
||||
? 0
|
||||
: Math.max(currentHeight - thickness, 0)
|
||||
const midAngle = startAngle + stepSweep / 2
|
||||
|
||||
return (
|
||||
<group key={`${stair.id}-${isSpiral ? 'spiral' : 'curved'}-step-${index}`} position-y={stepY}>
|
||||
<group
|
||||
key={`${stair.id}-${isSpiral ? 'spiral' : 'curved'}-step-${index}`}
|
||||
position-y={stepY}
|
||||
>
|
||||
{isSpiral && (stair.showStepSupports ?? true) ? (
|
||||
<mesh
|
||||
castShadow
|
||||
material={material}
|
||||
position={[
|
||||
Math.cos(midAngle) * (spiralColumnRadius + Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 - 0.02),
|
||||
Math.cos(midAngle) *
|
||||
(spiralColumnRadius +
|
||||
Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 -
|
||||
0.02),
|
||||
Math.max(thickness * 0.55, 0.025) / 2,
|
||||
Math.sin(midAngle) * (spiralColumnRadius + Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 - 0.02),
|
||||
Math.sin(midAngle) *
|
||||
(spiralColumnRadius +
|
||||
Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) / 2 -
|
||||
0.02),
|
||||
]}
|
||||
receiveShadow
|
||||
rotation-y={-midAngle}
|
||||
@@ -426,11 +516,20 @@ function CurvedStepMesh({
|
||||
material: THREE.Material
|
||||
}) {
|
||||
const geometry = useMemo(
|
||||
() => buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle, Math.max(stepHeight, thickness)),
|
||||
() =>
|
||||
buildCurvedStepGeometry(
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
Math.max(stepHeight, thickness),
|
||||
),
|
||||
[endAngle, innerRadius, outerRadius, startAngle, stepHeight, thickness],
|
||||
)
|
||||
|
||||
return <mesh castShadow geometry={geometry} material={material} position-y={positionY} receiveShadow />
|
||||
return (
|
||||
<mesh castShadow geometry={geometry} material={material} position-y={positionY} receiveShadow />
|
||||
)
|
||||
}
|
||||
|
||||
function buildCurvedStepGeometry(
|
||||
@@ -445,7 +544,15 @@ function buildCurvedStepGeometry(
|
||||
const y1 = clampedHeight
|
||||
const sweepAngle = endAngle - startAngle
|
||||
const sweepDirection = Math.sign(sweepAngle) || 1
|
||||
const segmentCount = Math.max(4, Math.min(24, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 18) + Math.max(0, (outerRadius - innerRadius) * 3))))
|
||||
const segmentCount = Math.max(
|
||||
4,
|
||||
Math.min(
|
||||
24,
|
||||
Math.ceil(
|
||||
Math.abs(sweepAngle) / (Math.PI / 18) + Math.max(0, (outerRadius - innerRadius) * 3),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const positions: number[] = []
|
||||
const normals: number[] = []
|
||||
@@ -453,7 +560,12 @@ function buildCurvedStepGeometry(
|
||||
const pointOnArc = (radius: number, angle: number, y: number) =>
|
||||
new THREE.Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius)
|
||||
|
||||
const pushTriangle = (a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3, normal: THREE.Vector3) => {
|
||||
const pushTriangle = (
|
||||
a: THREE.Vector3,
|
||||
b: THREE.Vector3,
|
||||
c: THREE.Vector3,
|
||||
normal: THREE.Vector3,
|
||||
) => {
|
||||
const edgeAB = b.clone().sub(a)
|
||||
const edgeAC = c.clone().sub(a)
|
||||
const faceNormal = edgeAB.cross(edgeAC)
|
||||
@@ -464,7 +576,13 @@ function buildCurvedStepGeometry(
|
||||
}
|
||||
}
|
||||
|
||||
const pushQuad = (a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3, d: THREE.Vector3, normal: THREE.Vector3) => {
|
||||
const pushQuad = (
|
||||
a: THREE.Vector3,
|
||||
b: THREE.Vector3,
|
||||
c: THREE.Vector3,
|
||||
d: THREE.Vector3,
|
||||
normal: THREE.Vector3,
|
||||
) => {
|
||||
pushTriangle(a, b, c, normal)
|
||||
pushTriangle(a, c, d, normal)
|
||||
}
|
||||
@@ -548,7 +666,10 @@ function buildStairRailPaths(
|
||||
return layouts.map((layout, index) => {
|
||||
const previousLayout = index > 0 ? layouts[index - 1] : undefined
|
||||
const nextLayout = layouts[index + 1]
|
||||
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index)
|
||||
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(
|
||||
layouts,
|
||||
index,
|
||||
)
|
||||
const hideLandingRailing =
|
||||
layout.segment.segmentType === 'landing' &&
|
||||
previousLayout?.segment.segmentType === 'stair' &&
|
||||
@@ -565,32 +686,39 @@ function buildStairRailPaths(
|
||||
? (['front', 'left'] as const)
|
||||
: (['left', 'right'] as const)
|
||||
: hideLandingRailing
|
||||
? visualTurnSide === 'left'
|
||||
? (['front', 'right'] as const)
|
||||
: visualTurnSide === 'right'
|
||||
? (['front', 'left'] as const)
|
||||
: (['left', 'right'] as const)
|
||||
: layout.segment.segmentType === 'landing'
|
||||
? nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'left'
|
||||
? visualTurnSide === 'left'
|
||||
? (['front', 'right'] as const)
|
||||
: nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'right'
|
||||
: visualTurnSide === 'right'
|
||||
? (['front', 'left'] as const)
|
||||
: visualTurnSide === 'left'
|
||||
? (['right'] as const)
|
||||
: visualTurnSide === 'right'
|
||||
? (['left'] as const)
|
||||
: (['left', 'right'] as const)
|
||||
: (['left', 'right'] as const)
|
||||
: (['left', 'right'] as const)
|
||||
: layout.segment.segmentType === 'landing'
|
||||
? nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'left'
|
||||
? (['front', 'right'] as const)
|
||||
: nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'right'
|
||||
? (['front', 'left'] as const)
|
||||
: visualTurnSide === 'left'
|
||||
? (['right'] as const)
|
||||
: visualTurnSide === 'right'
|
||||
? (['left'] as const)
|
||||
: (['left', 'right'] as const)
|
||||
: (['left', 'right'] as const)
|
||||
|
||||
return {
|
||||
layout,
|
||||
sidePaths:
|
||||
isStraightLineDoubleLandingLayout && index === 1
|
||||
? (['left', 'right'] as const).map((side) => buildSegmentRailPath(layouts, index, side, landingInset))
|
||||
: sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)),
|
||||
? (['left', 'right'] as const).map((side) =>
|
||||
buildSegmentRailPath(layouts, index, side, landingInset),
|
||||
)
|
||||
: sideCandidates.map((side) =>
|
||||
buildSegmentRailPath(layouts, index, side, landingInset),
|
||||
),
|
||||
connectFromPrevious:
|
||||
index > 0 &&
|
||||
!(previousLayout?.segment.segmentType === 'landing' && layout.segment.segmentType === 'landing'),
|
||||
!(
|
||||
previousLayout?.segment.segmentType === 'landing' &&
|
||||
layout.segment.segmentType === 'landing'
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -607,7 +735,10 @@ function buildStairRailPaths(
|
||||
return layouts.map((layout, index) => {
|
||||
const previousLayout = index > 0 ? layouts[index - 1] : undefined
|
||||
const nextLayout = layouts[index + 1]
|
||||
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index)
|
||||
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(
|
||||
layouts,
|
||||
index,
|
||||
)
|
||||
const isMiddleLandingBetweenFlights =
|
||||
layout.segment.segmentType === 'landing' &&
|
||||
previousLayout?.segment.segmentType === 'stair' &&
|
||||
@@ -626,28 +757,29 @@ function buildStairRailPaths(
|
||||
suppressMiddleLandingOnPreferredTurnSide
|
||||
const landingContinuesOnPreferredSide =
|
||||
layout.segment.segmentType === 'landing'
|
||||
? nextAttachmentSide == null || nextAttachmentSide === 'front' || nextAttachmentSide === railingMode
|
||||
? nextAttachmentSide == null ||
|
||||
nextAttachmentSide === 'front' ||
|
||||
nextAttachmentSide === railingMode
|
||||
: true
|
||||
|
||||
const sideCandidates =
|
||||
suppressLandingRailing
|
||||
? ([] as StairRailPathSide[])
|
||||
: layout.segment.segmentType !== 'landing'
|
||||
? [railingMode]
|
||||
: isTerminalLandingBeforeStair
|
||||
? railingMode === 'left'
|
||||
? terminalNextAttachmentSide === 'right'
|
||||
? (['front', 'left'] as const)
|
||||
const sideCandidates = suppressLandingRailing
|
||||
? ([] as StairRailPathSide[])
|
||||
: layout.segment.segmentType !== 'landing'
|
||||
? [railingMode]
|
||||
: isTerminalLandingBeforeStair
|
||||
? railingMode === 'left'
|
||||
? terminalNextAttachmentSide === 'right'
|
||||
? (['front', 'left'] as const)
|
||||
: terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null
|
||||
? (['left'] as const)
|
||||
: ([] as StairRailPathSide[])
|
||||
: railingMode === 'right'
|
||||
? terminalNextAttachmentSide === 'left'
|
||||
? (['front', 'right'] as const)
|
||||
: terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null
|
||||
? (['left'] as const)
|
||||
? (['right'] as const)
|
||||
: ([] as StairRailPathSide[])
|
||||
: railingMode === 'right'
|
||||
? terminalNextAttachmentSide === 'left'
|
||||
? (['front', 'right'] as const)
|
||||
: terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null
|
||||
? (['right'] as const)
|
||||
: ([] as StairRailPathSide[])
|
||||
: [railingMode]
|
||||
: [railingMode]
|
||||
: isStraightLineDoubleLandingLayout
|
||||
? [railingMode]
|
||||
: isMiddleLandingBetweenFlights && railingMode === 'left'
|
||||
@@ -667,7 +799,9 @@ function buildStairRailPaths(
|
||||
|
||||
return {
|
||||
layout,
|
||||
sidePaths: sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)),
|
||||
sidePaths: sideCandidates.map((side) =>
|
||||
buildSegmentRailPath(layouts, index, side, landingInset),
|
||||
),
|
||||
connectFromPrevious:
|
||||
index > 0 &&
|
||||
!suppressLandingRailing &&
|
||||
@@ -677,7 +811,10 @@ function buildStairRailPaths(
|
||||
})
|
||||
}
|
||||
|
||||
function resolveLandingChainNextStair(layouts: StairRailLayout[], index: number): LandingChainNextStair {
|
||||
function resolveLandingChainNextStair(
|
||||
layouts: StairRailLayout[],
|
||||
index: number,
|
||||
): LandingChainNextStair {
|
||||
const layout = layouts[index]
|
||||
if (!layout || layout.segment.segmentType !== 'landing') {
|
||||
return { isTerminalLandingBeforeStair: false }
|
||||
@@ -728,9 +865,13 @@ function buildSegmentRailPath(
|
||||
const stepHeight = segment.segmentType === 'landing' ? 0 : segment.height / steps
|
||||
const flightSideOffset = side === 'left' ? segment.width / 2 - 0.045 : -segment.width / 2 + 0.045
|
||||
const flightStartX =
|
||||
previousLayout?.segment.segmentType === 'landing' ? -segment.length / 2 + landingInset : -segment.length / 2
|
||||
previousLayout?.segment.segmentType === 'landing'
|
||||
? -segment.length / 2 + landingInset
|
||||
: -segment.length / 2
|
||||
const flightEndX =
|
||||
nextLayout?.segment.segmentType === 'landing' ? segment.length / 2 - landingInset : segment.length / 2
|
||||
nextLayout?.segment.segmentType === 'landing'
|
||||
? segment.length / 2 - landingInset
|
||||
: segment.length / 2
|
||||
const landingFrontX =
|
||||
previousLayout?.segment.segmentType === 'stair' &&
|
||||
segment.attachmentSide &&
|
||||
@@ -767,7 +908,13 @@ function buildSegmentRailPath(
|
||||
return {
|
||||
side,
|
||||
points: [
|
||||
...(previousLayout?.segment.segmentType === 'landing' ? [] : ([[flightStartX, stepHeight > 0 ? stepHeight : 0, flightSideOffset]] as [number, number, number][])),
|
||||
...(previousLayout?.segment.segmentType === 'landing'
|
||||
? []
|
||||
: ([[flightStartX, stepHeight > 0 ? stepHeight : 0, flightSideOffset]] as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
][])),
|
||||
...Array.from({ length: steps }).map(
|
||||
(_, index) =>
|
||||
[
|
||||
@@ -783,7 +930,10 @@ function buildSegmentRailPath(
|
||||
}
|
||||
}
|
||||
|
||||
function toWorldRailPoint(layout: StairRailLayout, point: [number, number, number]): [number, number, number] {
|
||||
function toWorldRailPoint(
|
||||
layout: StairRailLayout,
|
||||
point: [number, number, number],
|
||||
): [number, number, number] {
|
||||
const [localX, localY, localZ] = point
|
||||
const [offsetX, offsetZ] = rotateXZ(localZ, localX, layout.rotation)
|
||||
return [layout.center[0] + offsetX, layout.elevation + localY, layout.center[1] + offsetZ]
|
||||
@@ -798,7 +948,10 @@ function computeSegmentTransforms(segments: StairSegmentNode[]): SegmentTransfor
|
||||
const segment = segments[i]!
|
||||
|
||||
if (i === 0) {
|
||||
transforms.push({ position: [currentPos.x, currentPos.y, currentPos.z], rotation: currentRot })
|
||||
transforms.push({
|
||||
position: [currentPos.x, currentPos.y, currentPos.z],
|
||||
rotation: currentRot,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useFrame, useThree } from '@react-three/fiber'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Color, Layers, UnsignedByteType } from 'three'
|
||||
import { Color, Layers, type Object3D, UnsignedByteType } from 'three'
|
||||
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
||||
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
|
||||
import {
|
||||
@@ -47,6 +47,21 @@ const RETRY_DELAY_MS = 500
|
||||
const DARK_BG = '#1f2433'
|
||||
const LIGHT_BG = '#ffffff'
|
||||
|
||||
function sanitizeOutlineObjects(objects: Object3D[]) {
|
||||
let nextIndex = 0
|
||||
|
||||
for (const object of objects) {
|
||||
if (!(object && typeof object.id === 'number' && object.parent)) {
|
||||
continue
|
||||
}
|
||||
|
||||
objects[nextIndex] = object
|
||||
nextIndex++
|
||||
}
|
||||
|
||||
objects.length = nextIndex
|
||||
}
|
||||
|
||||
const PostProcessingPasses = () => {
|
||||
const { gl: renderer, scene, camera } = useThree()
|
||||
const renderPipelineRef = useRef<RenderPipeline | null>(null)
|
||||
@@ -138,6 +153,8 @@ const PostProcessingPasses = () => {
|
||||
// Clear outliner arrays synchronously to prevent stale Object3D refs
|
||||
// from the previous project leaking into the new pipeline's outline passes.
|
||||
const outliner = useViewer.getState().outliner
|
||||
sanitizeOutlineObjects(outliner.selectedObjects)
|
||||
sanitizeOutlineObjects(outliner.hoveredObjects)
|
||||
outliner.selectedObjects.length = 0
|
||||
outliner.hoveredObjects.length = 0
|
||||
|
||||
@@ -289,6 +306,10 @@ const PostProcessingPasses = () => {
|
||||
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
|
||||
bgUniform.current.value.copy(bgCurrent.current)
|
||||
|
||||
const outliner = useViewer.getState().outliner
|
||||
sanitizeOutlineObjects(outliner.selectedObjects)
|
||||
sanitizeOutlineObjects(outliner.hoveredObjects)
|
||||
|
||||
if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
|
||||
try {
|
||||
if ((renderer as any).setClearAlpha) {
|
||||
|
||||
Reference in New Issue
Block a user