Preserve elevator stop order and handle viewport resize

This commit is contained in:
sudhir
2026-05-20 00:52:01 +00:00
committed by open-pascal
parent d7a1ce89f3
commit a3378a666d
5 changed files with 107 additions and 64 deletions
@@ -49,6 +49,7 @@ export type ElevatorInteractiveState = {
phase: ElevatorPhase phase: ElevatorPhase
phaseStartedAt: number | null phaseStartedAt: number | null
queue: AnyNodeId[] queue: AnyNodeId[]
requestedStops: AnyNodeId[]
} }
type InteractiveStore = { type InteractiveStore = {
@@ -241,6 +242,7 @@ export const useInteractive = create<InteractiveStore>((set, get) => ({
phase: 'idle', phase: 'idle',
phaseStartedAt: null, phaseStartedAt: null,
queue: [], queue: [],
requestedStops: [],
}, },
}, },
})) }))
@@ -30,7 +30,9 @@ describe('elevator runtime helpers', () => {
const duplicated = queueElevatorRequest(queued, upperLevelId) const duplicated = queueElevatorRequest(queued, upperLevelId)
expect(queued.queue).toEqual([upperLevelId]) expect(queued.queue).toEqual([upperLevelId])
expect(queued.requestedStops).toEqual([upperLevelId])
expect(duplicated.queue).toEqual([upperLevelId]) expect(duplicated.queue).toEqual([upperLevelId])
expect(duplicated.requestedStops).toEqual([upperLevelId])
}) })
test('opens doors only when the elevator is not moving', () => { test('opens doors only when the elevator is not moving', () => {
@@ -78,5 +80,6 @@ describe('elevator runtime helpers', () => {
expect(arrived.phase).toBe('opening') expect(arrived.phase).toBe('opening')
expect(open.phase).toBe('open') expect(open.phase).toBe('open')
expect(open.queue).toEqual([]) expect(open.queue).toEqual([])
expect(open.requestedStops).toEqual([upperLevelId])
}) })
}) })
@@ -23,6 +23,7 @@ export function createElevatorInteractiveState(
phase: 'idle', phase: 'idle',
phaseStartedAt: null, phaseStartedAt: null,
queue: [], queue: [],
requestedStops: [],
} }
} }
@@ -64,6 +65,9 @@ export function queueElevatorRequest(
return { return {
...state, ...state,
queue: [...state.queue, levelId], queue: [...state.queue, levelId],
requestedStops: state.requestedStops.includes(levelId)
? state.requestedStops
: [...state.requestedStops, levelId],
} }
} }
@@ -124,6 +128,7 @@ export function stepElevatorRuntimeState({
phase: 'idle', phase: 'idle',
phaseStartedAt: null, phaseStartedAt: null,
queue: [], queue: [],
requestedStops: [],
doorOpen: 0, doorOpen: 0,
} }
} }
@@ -147,7 +152,11 @@ export function stepElevatorRuntimeState({
doorOpen: Math.max(0, state.doorOpen - doorStep), doorOpen: Math.max(0, state.doorOpen - doorStep),
} }
} }
return state if (state.requestedStops.length === 0) return state
return {
...state,
requestedStops: [],
}
} }
return { return {
@@ -180,6 +189,7 @@ export function stepElevatorRuntimeState({
targetLevelId: null, targetLevelId: null,
phase: 'idle', phase: 'idle',
queue: [], queue: [],
requestedStops: [],
} }
} }
+60 -62
View File
@@ -170,6 +170,7 @@ export default function ElevatorPanel() {
if (!state) return null if (!state) return null
return { return {
currentLevelId: state.currentLevelId, currentLevelId: state.currentLevelId,
requestedStops: state.requestedStops,
queue: state.queue, queue: state.queue,
targetLevelId: state.targetLevelId, targetLevelId: state.targetLevelId,
} }
@@ -463,14 +464,9 @@ export default function ElevatorPanel() {
: fromLevelId || levels[0]?.id) ?? : fromLevelId || levels[0]?.id) ??
null null
const destinationOrderByLevelId = new Map<string, number>() const destinationOrderByLevelId = new Map<string, number>()
const orderedDestinationIds: string[] = [] for (const [index, levelId] of (runtime?.requestedStops ?? []).entries()) {
if (runtime?.targetLevelId) orderedDestinationIds.push(runtime.targetLevelId)
for (const levelId of runtime?.queue ?? []) {
if (!orderedDestinationIds.includes(levelId)) orderedDestinationIds.push(levelId)
}
orderedDestinationIds.forEach((levelId, index) => {
destinationOrderByLevelId.set(levelId, index + 1) destinationOrderByLevelId.set(levelId, index + 1)
}) }
return ( return (
<PanelWrapper <PanelWrapper
@@ -592,6 +588,63 @@ export default function ElevatorPanel() {
</div> </div>
</PanelSection> </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"> <PanelSection title="Cab">
<MetricControl <MetricControl
label="Width" label="Width"
@@ -753,61 +806,6 @@ export default function ElevatorPanel() {
/> />
</PanelSection> </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"> <PanelSection title="Access">
<div className="space-y-2"> <div className="space-y-2">
{servedLevels.map((level) => { {servedLevels.map((level) => {
@@ -127,11 +127,12 @@ const PostProcessingPasses = ({
}: { }: {
hoverStyles?: HoverStyles hoverStyles?: HoverStyles
}) => { }) => {
const { gl: renderer, invalidate, scene, camera } = useThree() const { gl: renderer, invalidate, scene, camera, size } = useThree()
const renderPipelineRef = useRef<RenderPipeline | null>(null) const renderPipelineRef = useRef<RenderPipeline | null>(null)
const hasPipelineErrorRef = useRef(false) const hasPipelineErrorRef = useRef(false)
const retryCountRef = useRef(0) const retryCountRef = useRef(0)
const rebuildTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null) 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. // 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. // 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 // Build / rebuild the post-processing pipeline
useEffect(() => { useEffect(() => {
const width = Math.floor(size.width)
const height = Math.floor(size.height)
if (!(renderer && scene && camera)) { if (!(renderer && scene && camera)) {
console.warn('[viewer/post-processing] Skipping pipeline build — missing dependency.', { console.warn('[viewer/post-processing] Skipping pipeline build — missing dependency.', {
hasRenderer: !!renderer, hasRenderer: !!renderer,
@@ -216,6 +220,24 @@ const PostProcessingPasses = ({
return 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 perfDisable = readPerfDisableFlags()
const ssgiEnabled = SSGI_PARAMS.enabled && !perfDisable.ao const ssgiEnabled = SSGI_PARAMS.enabled && !perfDisable.ao
const denoiseEnabled = ssgiEnabled && !perfDisable.denoise const denoiseEnabled = ssgiEnabled && !perfDisable.denoise
@@ -230,6 +252,8 @@ const PostProcessingPasses = ({
hoverHighlightMode, hoverHighlightMode,
projectId, projectId,
rendererCtor: (renderer as any).constructor?.name, rendererCtor: (renderer as any).constructor?.name,
width,
height,
}) })
hasPipelineErrorRef.current = false hasPipelineErrorRef.current = false
@@ -413,10 +437,16 @@ const PostProcessingPasses = ({
projectId, projectId,
renderer, renderer,
scene, scene,
size.height,
size.width,
zoneLayers, zoneLayers,
]) ])
useFrame((_, delta) => { useFrame((_, delta) => {
if (size.width < 1 || size.height < 1) {
return
}
// Animate background colour toward the current theme target (same lerp as AnimatedBackground) // Animate background colour toward the current theme target (same lerp as AnimatedBackground)
bgTarget.current.set(useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG) bgTarget.current.set(useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG)
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4) bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)