Preserve elevator stop order and handle viewport resize
This commit is contained in:
@@ -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: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user