Harden stair reload and spiral post-processing
This commit is contained in:
@@ -8,9 +8,98 @@ import type { Collection, CollectionId } from '../schema/collections'
|
|||||||
import { generateCollectionId } from '../schema/collections'
|
import { generateCollectionId } from '../schema/collections'
|
||||||
import { LevelNode } from '../schema/nodes/level'
|
import { LevelNode } from '../schema/nodes/level'
|
||||||
import { SiteNode } from '../schema/nodes/site'
|
import { SiteNode } from '../schema/nodes/site'
|
||||||
|
import { StairNode as StairNodeSchema } from '../schema/nodes/stair'
|
||||||
|
import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment'
|
||||||
import type { AnyNode, AnyNodeId } from '../schema/types'
|
import type { AnyNode, AnyNodeId } from '../schema/types'
|
||||||
import * as nodeActions from './actions/node-actions'
|
import * as nodeActions from './actions/node-actions'
|
||||||
|
|
||||||
|
function getFiniteNumber(value: unknown, fallback: number) {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBoolean(value: unknown, fallback: boolean) {
|
||||||
|
return typeof value === 'boolean' ? value : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnumValue<T extends readonly string[]>(
|
||||||
|
value: unknown,
|
||||||
|
allowed: T,
|
||||||
|
fallback: T[number],
|
||||||
|
): T[number] {
|
||||||
|
return typeof value === 'string' && allowed.includes(value) ? value : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNullableString(value: unknown) {
|
||||||
|
return typeof value === 'string' ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringArray(value: unknown) {
|
||||||
|
return Array.isArray(value)
|
||||||
|
? value.filter((entry): entry is string => typeof entry === 'string')
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVector3(value: unknown, fallback: [number, number, number]): [number, number, number] {
|
||||||
|
if (!Array.isArray(value) || value.length < 3) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
getFiniteNumber(value[0], fallback[0]),
|
||||||
|
getFiniteNumber(value[1], fallback[1]),
|
||||||
|
getFiniteNumber(value[2], fallback[2]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStairNode(node: Record<string, unknown>) {
|
||||||
|
const sanitized = {
|
||||||
|
...node,
|
||||||
|
position: getVector3(node.position, [0, 0, 0]),
|
||||||
|
rotation: getFiniteNumber(node.rotation, 0),
|
||||||
|
stairType: getEnumValue(node.stairType, ['straight', 'curved', 'spiral'] as const, 'straight'),
|
||||||
|
fromLevelId: getNullableString(node.fromLevelId),
|
||||||
|
toLevelId: getNullableString(node.toLevelId),
|
||||||
|
slabOpeningMode: getEnumValue(node.slabOpeningMode, ['none', 'destination'] as const, 'none'),
|
||||||
|
openingOffset: getFiniteNumber(node.openingOffset, 0),
|
||||||
|
width: getFiniteNumber(node.width, 1),
|
||||||
|
totalRise: getFiniteNumber(node.totalRise, 2.5),
|
||||||
|
stepCount: getFiniteNumber(node.stepCount, 10),
|
||||||
|
thickness: getFiniteNumber(node.thickness, 0.25),
|
||||||
|
fillToFloor: getBoolean(node.fillToFloor, true),
|
||||||
|
innerRadius: getFiniteNumber(node.innerRadius, 0.9),
|
||||||
|
sweepAngle: getFiniteNumber(node.sweepAngle, Math.PI / 2),
|
||||||
|
topLandingMode: getEnumValue(node.topLandingMode, ['none', 'integrated'] as const, 'none'),
|
||||||
|
topLandingDepth: getFiniteNumber(node.topLandingDepth, 0.9),
|
||||||
|
showCenterColumn: getBoolean(node.showCenterColumn, true),
|
||||||
|
showStepSupports: getBoolean(node.showStepSupports, true),
|
||||||
|
railingMode: getEnumValue(node.railingMode, ['none', 'left', 'right', 'both'] as const, 'none'),
|
||||||
|
railingHeight: getFiniteNumber(node.railingHeight, 0.92),
|
||||||
|
children: getStringArray(node.children),
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = StairNodeSchema.safeParse(sanitized)
|
||||||
|
return parsed.success ? parsed.data : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStairSegmentNode(node: Record<string, unknown>) {
|
||||||
|
const sanitized = {
|
||||||
|
...node,
|
||||||
|
position: getVector3(node.position, [0, 0, 0]),
|
||||||
|
rotation: getFiniteNumber(node.rotation, 0),
|
||||||
|
segmentType: getEnumValue(node.segmentType, ['stair', 'landing'] as const, 'stair'),
|
||||||
|
width: getFiniteNumber(node.width, 1),
|
||||||
|
length: getFiniteNumber(node.length, 3),
|
||||||
|
height: getFiniteNumber(node.height, 2.5),
|
||||||
|
stepCount: getFiniteNumber(node.stepCount, 10),
|
||||||
|
attachmentSide: getEnumValue(node.attachmentSide, ['front', 'left', 'right'] as const, 'front'),
|
||||||
|
fillToFloor: getBoolean(node.fillToFloor, true),
|
||||||
|
thickness: getFiniteNumber(node.thickness, 0.25),
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = StairSegmentNodeSchema.safeParse(sanitized)
|
||||||
|
return parsed.success ? parsed.data : null
|
||||||
|
}
|
||||||
|
|
||||||
function migrateNodes(nodes: Record<string, any>): Record<string, AnyNode> {
|
function migrateNodes(nodes: Record<string, any>): Record<string, AnyNode> {
|
||||||
const patchedNodes = { ...nodes }
|
const patchedNodes = { ...nodes }
|
||||||
for (const [id, node] of Object.entries(patchedNodes)) {
|
for (const [id, node] of Object.entries(patchedNodes)) {
|
||||||
@@ -50,6 +139,20 @@ function migrateNodes(nodes: Record<string, any>): Record<string, AnyNode> {
|
|||||||
children: [segmentId],
|
children: [segmentId],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.type === 'stair') {
|
||||||
|
const normalized = normalizeStairNode(node)
|
||||||
|
if (normalized) {
|
||||||
|
patchedNodes[id] = normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'stair-segment') {
|
||||||
|
const normalized = normalizeStairSegmentNode(node)
|
||||||
|
if (normalized) {
|
||||||
|
patchedNodes[id] = normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return patchedNodes as Record<string, AnyNode>
|
return patchedNodes as Record<string, AnyNode>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { useLayoutEffect, useMemo, useRef } from 'react'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
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'
|
import { NodeRenderer } from '../node-renderer'
|
||||||
|
|
||||||
type SegmentTransform = {
|
type SegmentTransform = {
|
||||||
@@ -37,6 +47,7 @@ type LandingChainNextStair = {
|
|||||||
|
|
||||||
export const StairRenderer = ({ node }: { node: StairNode }) => {
|
export const StairRenderer = ({ node }: { node: StairNode }) => {
|
||||||
const ref = useRef<THREE.Group>(null!)
|
const ref = useRef<THREE.Group>(null!)
|
||||||
|
const isSegmentBasedStair = node.stairType === 'straight'
|
||||||
|
|
||||||
useRegistry(node.id, 'stair', ref)
|
useRegistry(node.id, 'stair', ref)
|
||||||
|
|
||||||
@@ -52,7 +63,13 @@ export const StairRenderer = ({ node }: { node: StairNode }) => {
|
|||||||
const mat = node.material
|
const mat = node.material
|
||||||
if (!mat) return DEFAULT_STAIR_MATERIAL
|
if (!mat) return DEFAULT_STAIR_MATERIAL
|
||||||
return createMaterial(mat)
|
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 (
|
return (
|
||||||
<group
|
<group
|
||||||
@@ -63,18 +80,20 @@ export const StairRenderer = ({ node }: { node: StairNode }) => {
|
|||||||
visible={node.visible}
|
visible={node.visible}
|
||||||
{...handlers}
|
{...handlers}
|
||||||
>
|
>
|
||||||
<mesh castShadow material={material} name="merged-stair" receiveShadow>
|
{isSegmentBasedStair ? (
|
||||||
<boxGeometry args={[0, 0, 0]} />
|
<mesh castShadow material={material} name="merged-stair" receiveShadow>
|
||||||
</mesh>
|
<boxGeometry args={[0, 0, 0]} />
|
||||||
{node.stairType === 'curved' || node.stairType === 'spiral' ? (
|
</mesh>
|
||||||
<CurvedStairBody material={material} stair={node} />
|
|
||||||
) : null}
|
) : null}
|
||||||
|
{!isSegmentBasedStair ? <CurvedStairBody material={material} stair={node} /> : null}
|
||||||
<StairRailings material={material} stair={node} />
|
<StairRailings material={material} stair={node} />
|
||||||
<group name="segments-wrapper" visible={false}>
|
{isSegmentBasedStair ? (
|
||||||
{(node.children ?? []).map((childId) => (
|
<group name="segments-wrapper" visible={false}>
|
||||||
<NodeRenderer key={childId} nodeId={childId} />
|
{(node.children ?? []).map((childId) => (
|
||||||
))}
|
<NodeRenderer key={childId} nodeId={childId} />
|
||||||
</group>
|
))}
|
||||||
|
</group>
|
||||||
|
) : null}
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -86,11 +105,17 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
|||||||
() =>
|
() =>
|
||||||
(stair.children ?? [])
|
(stair.children ?? [])
|
||||||
.map((childId) => nodes[childId as AnyNodeId] as StairSegmentNode | undefined)
|
.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],
|
[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 railHeight = stair.railingHeight ?? 0.92
|
||||||
const midRailHeight = Math.max(railHeight * 0.45, 0.35)
|
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') {
|
if (stair.stairType === 'curved' || stair.stairType === 'spiral') {
|
||||||
const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10))
|
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 stepSweep = sweepAngle / stepCount
|
||||||
const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / 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 outerRadius = innerRadius + Math.max(stair.width ?? 1, 0.4)
|
||||||
const leftRadius = sweepAngle >= 0 ? innerRadius + 0.04 : outerRadius - 0.04
|
const leftRadius = sweepAngle >= 0 ? innerRadius + 0.04 : outerRadius - 0.04
|
||||||
const rightRadius = sweepAngle >= 0 ? outerRadius - 0.04 : innerRadius + 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) => (
|
{railPaths.map((segmentPath, index) => (
|
||||||
<group
|
<group
|
||||||
key={`${segmentPath.layout.segment.id}-railing`}
|
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}
|
rotation-y={segmentPath.layout.rotation}
|
||||||
>
|
>
|
||||||
{segmentPath.sidePaths.map((sidePath, sideIndex) => (
|
{segmentPath.sidePaths.map((sidePath, sideIndex) => (
|
||||||
@@ -204,7 +237,9 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
|||||||
if (!nextPoint) return null
|
if (!nextPoint) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group key={`${segmentPath.layout.segment.id}-${sidePath.side}-rail-${pointIndex}`}>
|
<group
|
||||||
|
key={`${segmentPath.layout.segment.id}-${sidePath.side}-rail-${pointIndex}`}
|
||||||
|
>
|
||||||
<RailSegment
|
<RailSegment
|
||||||
end={[nextPoint[2], nextPoint[1] + railHeight, nextPoint[0]]}
|
end={[nextPoint[2], nextPoint[1] + railHeight, nextPoint[0]]}
|
||||||
material={material}
|
material={material}
|
||||||
@@ -240,14 +275,15 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
|||||||
const lastPoint = entry.points[entry.points.length - 1]
|
const lastPoint = entry.points[entry.points.length - 1]
|
||||||
return {
|
return {
|
||||||
entry,
|
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
|
.sort((left, right) => left.distance - right.distance)[0]?.entry
|
||||||
const previousPoint =
|
const previousPoint = previousSidePath?.points.length
|
||||||
previousSidePath && previousSidePath.points.length
|
? previousSidePath.points[previousSidePath.points.length - 1]
|
||||||
? previousSidePath.points[previousSidePath.points.length - 1]
|
: null
|
||||||
: null
|
|
||||||
|
|
||||||
if (!(previousPoint && currentPoint)) {
|
if (!(previousPoint && currentPoint)) {
|
||||||
return null
|
return null
|
||||||
@@ -256,18 +292,36 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
|||||||
const previousWorldPoint = toWorldRailPoint(previousPath.layout, previousPoint)
|
const previousWorldPoint = toWorldRailPoint(previousPath.layout, previousPoint)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group key={`${previousPath.layout.segment.id}-${segmentPath.layout.segment.id}-${sideIndex}`}>
|
<group
|
||||||
|
key={`${previousPath.layout.segment.id}-${segmentPath.layout.segment.id}-${sideIndex}`}
|
||||||
|
>
|
||||||
<RailSegment
|
<RailSegment
|
||||||
end={[currentWorldPoint[0], currentWorldPoint[1] + railHeight, currentWorldPoint[2]]}
|
end={[
|
||||||
|
currentWorldPoint[0],
|
||||||
|
currentWorldPoint[1] + railHeight,
|
||||||
|
currentWorldPoint[2],
|
||||||
|
]}
|
||||||
material={material}
|
material={material}
|
||||||
radius={railRadius}
|
radius={railRadius}
|
||||||
start={[previousWorldPoint[0], previousWorldPoint[1] + railHeight, previousWorldPoint[2]]}
|
start={[
|
||||||
|
previousWorldPoint[0],
|
||||||
|
previousWorldPoint[1] + railHeight,
|
||||||
|
previousWorldPoint[2],
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<RailSegment
|
<RailSegment
|
||||||
end={[currentWorldPoint[0], currentWorldPoint[1] + midRailHeight, currentWorldPoint[2]]}
|
end={[
|
||||||
|
currentWorldPoint[0],
|
||||||
|
currentWorldPoint[1] + midRailHeight,
|
||||||
|
currentWorldPoint[2],
|
||||||
|
]}
|
||||||
material={material}
|
material={material}
|
||||||
radius={railRadius * 0.8}
|
radius={railRadius * 0.8}
|
||||||
start={[previousWorldPoint[0], previousWorldPoint[1] + midRailHeight, previousWorldPoint[2]]}
|
start={[
|
||||||
|
previousWorldPoint[0],
|
||||||
|
previousWorldPoint[1] + midRailHeight,
|
||||||
|
previousWorldPoint[2],
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
@@ -296,10 +350,17 @@ function RailSegment({
|
|||||||
const direction = useMemo(() => endVector.clone().sub(startVector), [endVector, startVector])
|
const direction = useMemo(() => endVector.clone().sub(startVector), [endVector, startVector])
|
||||||
const length = Math.max(direction.length(), 0.01)
|
const length = Math.max(direction.length(), 0.01)
|
||||||
const quaternion = useMemo(
|
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],
|
[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 (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
@@ -327,11 +388,16 @@ function CurvedStairBody({ stair, material }: { stair: StairNode; material: THRE
|
|||||||
const fillToFloor = stair.fillToFloor ?? true
|
const fillToFloor = stair.fillToFloor ?? true
|
||||||
const spiralColumnRadius = Math.max(0.05, Math.min(innerRadius * 0.72, innerRadius - 0.03))
|
const spiralColumnRadius = Math.max(0.05, Math.min(innerRadius * 0.72, innerRadius - 0.03))
|
||||||
const spiralColumnHeight = totalRise + thickness
|
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 =
|
const spiralLandingSweep =
|
||||||
isSpiral && (stair.topLandingMode ?? 'none') === 'integrated'
|
isSpiral && (stair.topLandingMode ?? 'none') === 'integrated'
|
||||||
? Math.min(Math.PI * 0.75, spiralLandingDepth / Math.max(innerRadius + (stair.width ?? 1) / 2, 0.1)) *
|
? Math.min(
|
||||||
Math.sign(sweepAngle || 1)
|
Math.PI * 0.75,
|
||||||
|
spiralLandingDepth / Math.max(innerRadius + (stair.width ?? 1) / 2, 0.1),
|
||||||
|
) * Math.sign(sweepAngle || 1)
|
||||||
: 0
|
: 0
|
||||||
const spiralLastStepTop = stepHeight * Math.max(stepCount - 1, 0) + thickness
|
const spiralLastStepTop = stepHeight * Math.max(stepCount - 1, 0) + thickness
|
||||||
const spiralLandingThickness =
|
const spiralLandingThickness =
|
||||||
@@ -342,28 +408,52 @@ function CurvedStairBody({ stair, material }: { stair: StairNode; material: THRE
|
|||||||
return (
|
return (
|
||||||
<group name={isSpiral ? 'spiral-stair' : 'curved-stair'}>
|
<group name={isSpiral ? 'spiral-stair' : 'curved-stair'}>
|
||||||
{isSpiral && (stair.showCenterColumn ?? true) ? (
|
{isSpiral && (stair.showCenterColumn ?? true) ? (
|
||||||
<mesh castShadow receiveShadow material={material} position={[0, spiralColumnHeight / 2, 0]}>
|
<mesh
|
||||||
<cylinderGeometry args={[spiralColumnRadius, spiralColumnRadius, spiralColumnHeight, 10]} />
|
castShadow
|
||||||
|
receiveShadow
|
||||||
|
material={material}
|
||||||
|
position={[0, spiralColumnHeight / 2, 0]}
|
||||||
|
>
|
||||||
|
<cylinderGeometry
|
||||||
|
args={[spiralColumnRadius, spiralColumnRadius, spiralColumnHeight, 10]}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
) : null}
|
) : null}
|
||||||
{Array.from({ length: stepCount }).map((_, index) => {
|
{Array.from({ length: stepCount }).map((_, index) => {
|
||||||
const currentHeight = stepHeight * (index + 1)
|
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 startAngle = -sweepAngle / 2 + stepSweep * index
|
||||||
const endAngle = startAngle + stepSweep
|
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
|
const midAngle = startAngle + stepSweep / 2
|
||||||
|
|
||||||
return (
|
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) ? (
|
{isSpiral && (stair.showStepSupports ?? true) ? (
|
||||||
<mesh
|
<mesh
|
||||||
castShadow
|
castShadow
|
||||||
material={material}
|
material={material}
|
||||||
position={[
|
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.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
|
receiveShadow
|
||||||
rotation-y={-midAngle}
|
rotation-y={-midAngle}
|
||||||
@@ -426,11 +516,20 @@ function CurvedStepMesh({
|
|||||||
material: THREE.Material
|
material: THREE.Material
|
||||||
}) {
|
}) {
|
||||||
const geometry = useMemo(
|
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],
|
[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(
|
function buildCurvedStepGeometry(
|
||||||
@@ -445,7 +544,15 @@ function buildCurvedStepGeometry(
|
|||||||
const y1 = clampedHeight
|
const y1 = clampedHeight
|
||||||
const sweepAngle = endAngle - startAngle
|
const sweepAngle = endAngle - startAngle
|
||||||
const sweepDirection = Math.sign(sweepAngle) || 1
|
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 positions: number[] = []
|
||||||
const normals: number[] = []
|
const normals: number[] = []
|
||||||
@@ -453,7 +560,12 @@ function buildCurvedStepGeometry(
|
|||||||
const pointOnArc = (radius: number, angle: number, y: number) =>
|
const pointOnArc = (radius: number, angle: number, y: number) =>
|
||||||
new THREE.Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius)
|
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 edgeAB = b.clone().sub(a)
|
||||||
const edgeAC = c.clone().sub(a)
|
const edgeAC = c.clone().sub(a)
|
||||||
const faceNormal = edgeAB.cross(edgeAC)
|
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, b, c, normal)
|
||||||
pushTriangle(a, c, d, normal)
|
pushTriangle(a, c, d, normal)
|
||||||
}
|
}
|
||||||
@@ -548,7 +666,10 @@ function buildStairRailPaths(
|
|||||||
return layouts.map((layout, index) => {
|
return layouts.map((layout, index) => {
|
||||||
const previousLayout = index > 0 ? layouts[index - 1] : undefined
|
const previousLayout = index > 0 ? layouts[index - 1] : undefined
|
||||||
const nextLayout = layouts[index + 1]
|
const nextLayout = layouts[index + 1]
|
||||||
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index)
|
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(
|
||||||
|
layouts,
|
||||||
|
index,
|
||||||
|
)
|
||||||
const hideLandingRailing =
|
const hideLandingRailing =
|
||||||
layout.segment.segmentType === 'landing' &&
|
layout.segment.segmentType === 'landing' &&
|
||||||
previousLayout?.segment.segmentType === 'stair' &&
|
previousLayout?.segment.segmentType === 'stair' &&
|
||||||
@@ -565,32 +686,39 @@ function buildStairRailPaths(
|
|||||||
? (['front', 'left'] as const)
|
? (['front', 'left'] as const)
|
||||||
: (['left', 'right'] as const)
|
: (['left', 'right'] as const)
|
||||||
: hideLandingRailing
|
: hideLandingRailing
|
||||||
? visualTurnSide === 'left'
|
? 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'
|
|
||||||
? (['front', 'right'] as const)
|
? (['front', 'right'] as const)
|
||||||
: nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'right'
|
: visualTurnSide === 'right'
|
||||||
? (['front', 'left'] as const)
|
? (['front', 'left'] as const)
|
||||||
: visualTurnSide === 'left'
|
: (['left', 'right'] as const)
|
||||||
? (['right'] as const)
|
: layout.segment.segmentType === 'landing'
|
||||||
: visualTurnSide === 'right'
|
? nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'left'
|
||||||
? (['left'] as const)
|
? (['front', 'right'] as const)
|
||||||
: (['left', 'right'] as const)
|
: nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'right'
|
||||||
: (['left', 'right'] as const)
|
? (['front', 'left'] as const)
|
||||||
|
: visualTurnSide === 'left'
|
||||||
|
? (['right'] as const)
|
||||||
|
: visualTurnSide === 'right'
|
||||||
|
? (['left'] as const)
|
||||||
|
: (['left', 'right'] as const)
|
||||||
|
: (['left', 'right'] as const)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
layout,
|
layout,
|
||||||
sidePaths:
|
sidePaths:
|
||||||
isStraightLineDoubleLandingLayout && index === 1
|
isStraightLineDoubleLandingLayout && index === 1
|
||||||
? (['left', 'right'] as const).map((side) => buildSegmentRailPath(layouts, index, side, landingInset))
|
? (['left', 'right'] as const).map((side) =>
|
||||||
: sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)),
|
buildSegmentRailPath(layouts, index, side, landingInset),
|
||||||
|
)
|
||||||
|
: sideCandidates.map((side) =>
|
||||||
|
buildSegmentRailPath(layouts, index, side, landingInset),
|
||||||
|
),
|
||||||
connectFromPrevious:
|
connectFromPrevious:
|
||||||
index > 0 &&
|
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) => {
|
return layouts.map((layout, index) => {
|
||||||
const previousLayout = index > 0 ? layouts[index - 1] : undefined
|
const previousLayout = index > 0 ? layouts[index - 1] : undefined
|
||||||
const nextLayout = layouts[index + 1]
|
const nextLayout = layouts[index + 1]
|
||||||
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index)
|
const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(
|
||||||
|
layouts,
|
||||||
|
index,
|
||||||
|
)
|
||||||
const isMiddleLandingBetweenFlights =
|
const isMiddleLandingBetweenFlights =
|
||||||
layout.segment.segmentType === 'landing' &&
|
layout.segment.segmentType === 'landing' &&
|
||||||
previousLayout?.segment.segmentType === 'stair' &&
|
previousLayout?.segment.segmentType === 'stair' &&
|
||||||
@@ -626,28 +757,29 @@ function buildStairRailPaths(
|
|||||||
suppressMiddleLandingOnPreferredTurnSide
|
suppressMiddleLandingOnPreferredTurnSide
|
||||||
const landingContinuesOnPreferredSide =
|
const landingContinuesOnPreferredSide =
|
||||||
layout.segment.segmentType === 'landing'
|
layout.segment.segmentType === 'landing'
|
||||||
? nextAttachmentSide == null || nextAttachmentSide === 'front' || nextAttachmentSide === railingMode
|
? nextAttachmentSide == null ||
|
||||||
|
nextAttachmentSide === 'front' ||
|
||||||
|
nextAttachmentSide === railingMode
|
||||||
: true
|
: true
|
||||||
|
|
||||||
const sideCandidates =
|
const sideCandidates = suppressLandingRailing
|
||||||
suppressLandingRailing
|
? ([] as StairRailPathSide[])
|
||||||
? ([] as StairRailPathSide[])
|
: layout.segment.segmentType !== 'landing'
|
||||||
: layout.segment.segmentType !== 'landing'
|
? [railingMode]
|
||||||
? [railingMode]
|
: isTerminalLandingBeforeStair
|
||||||
: isTerminalLandingBeforeStair
|
? railingMode === 'left'
|
||||||
? railingMode === 'left'
|
? terminalNextAttachmentSide === 'right'
|
||||||
? terminalNextAttachmentSide === 'right'
|
? (['front', 'left'] as const)
|
||||||
? (['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
|
: terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null
|
||||||
? (['left'] as const)
|
? (['right'] as const)
|
||||||
: ([] as StairRailPathSide[])
|
: ([] as StairRailPathSide[])
|
||||||
: railingMode === 'right'
|
: [railingMode]
|
||||||
? terminalNextAttachmentSide === 'left'
|
|
||||||
? (['front', 'right'] as const)
|
|
||||||
: terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null
|
|
||||||
? (['right'] as const)
|
|
||||||
: ([] as StairRailPathSide[])
|
|
||||||
: [railingMode]
|
|
||||||
: isStraightLineDoubleLandingLayout
|
: isStraightLineDoubleLandingLayout
|
||||||
? [railingMode]
|
? [railingMode]
|
||||||
: isMiddleLandingBetweenFlights && railingMode === 'left'
|
: isMiddleLandingBetweenFlights && railingMode === 'left'
|
||||||
@@ -667,7 +799,9 @@ function buildStairRailPaths(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
layout,
|
layout,
|
||||||
sidePaths: sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)),
|
sidePaths: sideCandidates.map((side) =>
|
||||||
|
buildSegmentRailPath(layouts, index, side, landingInset),
|
||||||
|
),
|
||||||
connectFromPrevious:
|
connectFromPrevious:
|
||||||
index > 0 &&
|
index > 0 &&
|
||||||
!suppressLandingRailing &&
|
!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]
|
const layout = layouts[index]
|
||||||
if (!layout || layout.segment.segmentType !== 'landing') {
|
if (!layout || layout.segment.segmentType !== 'landing') {
|
||||||
return { isTerminalLandingBeforeStair: false }
|
return { isTerminalLandingBeforeStair: false }
|
||||||
@@ -728,9 +865,13 @@ function buildSegmentRailPath(
|
|||||||
const stepHeight = segment.segmentType === 'landing' ? 0 : segment.height / steps
|
const stepHeight = segment.segmentType === 'landing' ? 0 : segment.height / steps
|
||||||
const flightSideOffset = side === 'left' ? segment.width / 2 - 0.045 : -segment.width / 2 + 0.045
|
const flightSideOffset = side === 'left' ? segment.width / 2 - 0.045 : -segment.width / 2 + 0.045
|
||||||
const flightStartX =
|
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 =
|
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 =
|
const landingFrontX =
|
||||||
previousLayout?.segment.segmentType === 'stair' &&
|
previousLayout?.segment.segmentType === 'stair' &&
|
||||||
segment.attachmentSide &&
|
segment.attachmentSide &&
|
||||||
@@ -767,7 +908,13 @@ function buildSegmentRailPath(
|
|||||||
return {
|
return {
|
||||||
side,
|
side,
|
||||||
points: [
|
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(
|
...Array.from({ length: steps }).map(
|
||||||
(_, index) =>
|
(_, 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 [localX, localY, localZ] = point
|
||||||
const [offsetX, offsetZ] = rotateXZ(localZ, localX, layout.rotation)
|
const [offsetX, offsetZ] = rotateXZ(localZ, localX, layout.rotation)
|
||||||
return [layout.center[0] + offsetX, layout.elevation + localY, layout.center[1] + offsetZ]
|
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]!
|
const segment = segments[i]!
|
||||||
|
|
||||||
if (i === 0) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useFrame, useThree } from '@react-three/fiber'
|
import { useFrame, useThree } from '@react-three/fiber'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
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 { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
||||||
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
|
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
|
||||||
import {
|
import {
|
||||||
@@ -47,6 +47,21 @@ const RETRY_DELAY_MS = 500
|
|||||||
const DARK_BG = '#1f2433'
|
const DARK_BG = '#1f2433'
|
||||||
const LIGHT_BG = '#ffffff'
|
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 PostProcessingPasses = () => {
|
||||||
const { gl: renderer, scene, camera } = useThree()
|
const { gl: renderer, scene, camera } = useThree()
|
||||||
const renderPipelineRef = useRef<RenderPipeline | null>(null)
|
const renderPipelineRef = useRef<RenderPipeline | null>(null)
|
||||||
@@ -138,6 +153,8 @@ const PostProcessingPasses = () => {
|
|||||||
// Clear outliner arrays synchronously to prevent stale Object3D refs
|
// Clear outliner arrays synchronously to prevent stale Object3D refs
|
||||||
// from the previous project leaking into the new pipeline's outline passes.
|
// from the previous project leaking into the new pipeline's outline passes.
|
||||||
const outliner = useViewer.getState().outliner
|
const outliner = useViewer.getState().outliner
|
||||||
|
sanitizeOutlineObjects(outliner.selectedObjects)
|
||||||
|
sanitizeOutlineObjects(outliner.hoveredObjects)
|
||||||
outliner.selectedObjects.length = 0
|
outliner.selectedObjects.length = 0
|
||||||
outliner.hoveredObjects.length = 0
|
outliner.hoveredObjects.length = 0
|
||||||
|
|
||||||
@@ -289,6 +306,10 @@ const PostProcessingPasses = () => {
|
|||||||
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
|
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
|
||||||
bgUniform.current.value.copy(bgCurrent.current)
|
bgUniform.current.value.copy(bgCurrent.current)
|
||||||
|
|
||||||
|
const outliner = useViewer.getState().outliner
|
||||||
|
sanitizeOutlineObjects(outliner.selectedObjects)
|
||||||
|
sanitizeOutlineObjects(outliner.hoveredObjects)
|
||||||
|
|
||||||
if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
|
if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
|
||||||
try {
|
try {
|
||||||
if ((renderer as any).setClearAlpha) {
|
if ((renderer as any).setClearAlpha) {
|
||||||
|
|||||||
Reference in New Issue
Block a user