Preserve elevator stop order and handle viewport resize
This commit is contained in:
@@ -49,6 +49,7 @@ export type ElevatorInteractiveState = {
|
||||
phase: ElevatorPhase
|
||||
phaseStartedAt: number | null
|
||||
queue: AnyNodeId[]
|
||||
requestedStops: AnyNodeId[]
|
||||
}
|
||||
|
||||
type InteractiveStore = {
|
||||
@@ -241,6 +242,7 @@ export const useInteractive = create<InteractiveStore>((set, get) => ({
|
||||
phase: 'idle',
|
||||
phaseStartedAt: null,
|
||||
queue: [],
|
||||
requestedStops: [],
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -30,7 +30,9 @@ describe('elevator runtime helpers', () => {
|
||||
const duplicated = queueElevatorRequest(queued, upperLevelId)
|
||||
|
||||
expect(queued.queue).toEqual([upperLevelId])
|
||||
expect(queued.requestedStops).toEqual([upperLevelId])
|
||||
expect(duplicated.queue).toEqual([upperLevelId])
|
||||
expect(duplicated.requestedStops).toEqual([upperLevelId])
|
||||
})
|
||||
|
||||
test('opens doors only when the elevator is not moving', () => {
|
||||
@@ -78,5 +80,6 @@ describe('elevator runtime helpers', () => {
|
||||
expect(arrived.phase).toBe('opening')
|
||||
expect(open.phase).toBe('open')
|
||||
expect(open.queue).toEqual([])
|
||||
expect(open.requestedStops).toEqual([upperLevelId])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ export function createElevatorInteractiveState(
|
||||
phase: 'idle',
|
||||
phaseStartedAt: null,
|
||||
queue: [],
|
||||
requestedStops: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +65,9 @@ export function queueElevatorRequest(
|
||||
return {
|
||||
...state,
|
||||
queue: [...state.queue, levelId],
|
||||
requestedStops: state.requestedStops.includes(levelId)
|
||||
? state.requestedStops
|
||||
: [...state.requestedStops, levelId],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +128,7 @@ export function stepElevatorRuntimeState({
|
||||
phase: 'idle',
|
||||
phaseStartedAt: null,
|
||||
queue: [],
|
||||
requestedStops: [],
|
||||
doorOpen: 0,
|
||||
}
|
||||
}
|
||||
@@ -147,7 +152,11 @@ export function stepElevatorRuntimeState({
|
||||
doorOpen: Math.max(0, state.doorOpen - doorStep),
|
||||
}
|
||||
}
|
||||
return state
|
||||
if (state.requestedStops.length === 0) return state
|
||||
return {
|
||||
...state,
|
||||
requestedStops: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -180,6 +189,7 @@ export function stepElevatorRuntimeState({
|
||||
targetLevelId: null,
|
||||
phase: 'idle',
|
||||
queue: [],
|
||||
requestedStops: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -170,6 +170,7 @@ export default function ElevatorPanel() {
|
||||
if (!state) return null
|
||||
return {
|
||||
currentLevelId: state.currentLevelId,
|
||||
requestedStops: state.requestedStops,
|
||||
queue: state.queue,
|
||||
targetLevelId: state.targetLevelId,
|
||||
}
|
||||
@@ -463,14 +464,9 @@ export default function ElevatorPanel() {
|
||||
: fromLevelId || levels[0]?.id) ??
|
||||
null
|
||||
const destinationOrderByLevelId = new Map<string, number>()
|
||||
const orderedDestinationIds: string[] = []
|
||||
if (runtime?.targetLevelId) orderedDestinationIds.push(runtime.targetLevelId)
|
||||
for (const levelId of runtime?.queue ?? []) {
|
||||
if (!orderedDestinationIds.includes(levelId)) orderedDestinationIds.push(levelId)
|
||||
}
|
||||
orderedDestinationIds.forEach((levelId, index) => {
|
||||
for (const [index, levelId] of (runtime?.requestedStops ?? []).entries()) {
|
||||
destinationOrderByLevelId.set(levelId, index + 1)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelWrapper
|
||||
@@ -592,6 +588,63 @@ export default function ElevatorPanel() {
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Service">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1.5">
|
||||
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
From
|
||||
</div>
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-2 text-sm text-foreground"
|
||||
onChange={(event) =>
|
||||
handleServiceBoundaryChange('fromLevelId', event.target.value)
|
||||
}
|
||||
value={fromLevelId}
|
||||
>
|
||||
{levels.map((level) => (
|
||||
<option key={level.id} value={level.id}>
|
||||
{level.name || `Level ${level.level}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
To
|
||||
</div>
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-2 text-sm text-foreground"
|
||||
onChange={(event) => handleServiceBoundaryChange('toLevelId', event.target.value)}
|
||||
value={toLevelId}
|
||||
>
|
||||
{levels.map((level) => (
|
||||
<option key={level.id} value={level.id}>
|
||||
{level.name || `Level ${level.level}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Default Floor
|
||||
</div>
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm text-foreground"
|
||||
onChange={(event) => handleUpdate({ defaultLevelId: event.target.value || null })}
|
||||
value={selectedDefaultLevelId}
|
||||
>
|
||||
{defaultLevelOptions.map((level) => (
|
||||
<option key={level.id} value={level.id}>
|
||||
{level.name || `Level ${level.level}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Cab">
|
||||
<MetricControl
|
||||
label="Width"
|
||||
@@ -753,61 +806,6 @@ export default function ElevatorPanel() {
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Service">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1.5">
|
||||
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
From
|
||||
</div>
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-2 text-sm text-foreground"
|
||||
onChange={(event) => handleServiceBoundaryChange('fromLevelId', event.target.value)}
|
||||
value={fromLevelId}
|
||||
>
|
||||
{levels.map((level) => (
|
||||
<option key={level.id} value={level.id}>
|
||||
{level.name || `Level ${level.level}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
To
|
||||
</div>
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-2 text-sm text-foreground"
|
||||
onChange={(event) => handleServiceBoundaryChange('toLevelId', event.target.value)}
|
||||
value={toLevelId}
|
||||
>
|
||||
{levels.map((level) => (
|
||||
<option key={level.id} value={level.id}>
|
||||
{level.name || `Level ${level.level}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Default Floor
|
||||
</div>
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm text-foreground"
|
||||
onChange={(event) => handleUpdate({ defaultLevelId: event.target.value || null })}
|
||||
value={selectedDefaultLevelId}
|
||||
>
|
||||
{defaultLevelOptions.map((level) => (
|
||||
<option key={level.id} value={level.id}>
|
||||
{level.name || `Level ${level.level}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Access">
|
||||
<div className="space-y-2">
|
||||
{servedLevels.map((level) => {
|
||||
|
||||
@@ -127,11 +127,12 @@ const PostProcessingPasses = ({
|
||||
}: {
|
||||
hoverStyles?: HoverStyles
|
||||
}) => {
|
||||
const { gl: renderer, invalidate, scene, camera } = useThree()
|
||||
const { gl: renderer, invalidate, scene, camera, size } = useThree()
|
||||
const renderPipelineRef = useRef<RenderPipeline | null>(null)
|
||||
const hasPipelineErrorRef = useRef(false)
|
||||
const retryCountRef = useRef(0)
|
||||
const rebuildTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const skippedZeroSizeRef = useRef(false)
|
||||
|
||||
// Background color uniform — updated every frame via lerp, read by the TSL pipeline.
|
||||
// Initialised from the current theme so there's no flash on first render.
|
||||
@@ -207,6 +208,9 @@ const PostProcessingPasses = ({
|
||||
|
||||
// Build / rebuild the post-processing pipeline
|
||||
useEffect(() => {
|
||||
const width = Math.floor(size.width)
|
||||
const height = Math.floor(size.height)
|
||||
|
||||
if (!(renderer && scene && camera)) {
|
||||
console.warn('[viewer/post-processing] Skipping pipeline build — missing dependency.', {
|
||||
hasRenderer: !!renderer,
|
||||
@@ -216,6 +220,24 @@ const PostProcessingPasses = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (width < 1 || height < 1) {
|
||||
skippedZeroSizeRef.current = true
|
||||
hasPipelineErrorRef.current = false
|
||||
if (renderPipelineRef.current) {
|
||||
renderPipelineRef.current.dispose()
|
||||
}
|
||||
renderPipelineRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
if (skippedZeroSizeRef.current) {
|
||||
console.log('[viewer/post-processing] Rebuilding pipeline after zero-sized viewport.', {
|
||||
width,
|
||||
height,
|
||||
})
|
||||
skippedZeroSizeRef.current = false
|
||||
}
|
||||
|
||||
const perfDisable = readPerfDisableFlags()
|
||||
const ssgiEnabled = SSGI_PARAMS.enabled && !perfDisable.ao
|
||||
const denoiseEnabled = ssgiEnabled && !perfDisable.denoise
|
||||
@@ -230,6 +252,8 @@ const PostProcessingPasses = ({
|
||||
hoverHighlightMode,
|
||||
projectId,
|
||||
rendererCtor: (renderer as any).constructor?.name,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
|
||||
hasPipelineErrorRef.current = false
|
||||
@@ -413,10 +437,16 @@ const PostProcessingPasses = ({
|
||||
projectId,
|
||||
renderer,
|
||||
scene,
|
||||
size.height,
|
||||
size.width,
|
||||
zoneLayers,
|
||||
])
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (size.width < 1 || size.height < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
// Animate background colour toward the current theme target (same lerp as AnimatedBackground)
|
||||
bgTarget.current.set(useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG)
|
||||
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
|
||||
|
||||
Reference in New Issue
Block a user