From ce69433a1a9e97fcb8903f007eebb073c9968e9f Mon Sep 17 00:00:00 2001 From: sudhir Date: Wed, 13 May 2026 22:28:19 +0530 Subject: [PATCH] Refine elevator sync and viewer rebuild handling --- .../src/schema/nodes/surface-hole-metadata.ts | 1 + .../systems/elevator/elevator-opening-sync.ts | 15 ++++---- .../src/systems/stair/stair-opening-sync.ts | 7 +--- .../components/editor/selection-manager.tsx | 1 + .../tools/elevator/elevator-tool.tsx | 29 ++++++--------- .../tools/elevator/move-elevator-tool.tsx | 23 +++++++++--- packages/editor/src/lib/elevator-support.ts | 36 ++++++++++++++++--- .../src/components/viewer/post-processing.tsx | 4 ++- 8 files changed, 76 insertions(+), 40 deletions(-) diff --git a/packages/core/src/schema/nodes/surface-hole-metadata.ts b/packages/core/src/schema/nodes/surface-hole-metadata.ts index bb223876..a3d560e8 100644 --- a/packages/core/src/schema/nodes/surface-hole-metadata.ts +++ b/packages/core/src/schema/nodes/surface-hole-metadata.ts @@ -1,6 +1,7 @@ import { z } from 'zod' export const SurfaceHoleMetadata = z.object({ + // Stair/elevator auto-openings use stairId/elevatorId so sync can replace only its own holes. source: z.enum(['manual', 'stair', 'elevator']).default('manual'), stairId: z.string().optional(), elevatorId: z.string().optional(), diff --git a/packages/core/src/systems/elevator/elevator-opening-sync.ts b/packages/core/src/systems/elevator/elevator-opening-sync.ts index c02d1ee8..9fe4adf9 100644 --- a/packages/core/src/systems/elevator/elevator-opening-sync.ts +++ b/packages/core/src/systems/elevator/elevator-opening-sync.ts @@ -1,15 +1,16 @@ import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync' -import type { AnyNode, AnyNodeId, CeilingNode, ElevatorNode, SlabNode } from '../../schema' +import type { + AnyNode, + AnyNodeId, + CeilingNode, + ElevatorNode, + SlabNode, + SurfaceHoleMetadata, +} from '../../schema' import { resolveElevatorServiceLevels } from './elevator-service' type Point2D = [number, number] -type SurfaceHoleMetadata = { - source: 'manual' | 'stair' | 'elevator' - elevatorId?: string - stairId?: string -} - const ELEVATOR_OPENING_PADDING = 0.08 const DEFAULT_ELEVATOR_SHAFT_WALL_THICKNESS = 0.09 diff --git a/packages/core/src/systems/stair/stair-opening-sync.ts b/packages/core/src/systems/stair/stair-opening-sync.ts index 71680e0a..6f4c9f7a 100644 --- a/packages/core/src/systems/stair/stair-opening-sync.ts +++ b/packages/core/src/systems/stair/stair-opening-sync.ts @@ -6,17 +6,12 @@ import type { SlabNode, StairNode, StairSegmentNode, + SurfaceHoleMetadata, } from '../../schema' import { DEFAULT_WALL_HEIGHT } from '../wall/wall-footprint' type Point2D = [number, number] -type SurfaceHoleMetadata = { - source: 'manual' | 'stair' | 'elevator' - elevatorId?: string - stairId?: string -} - type SegmentTransform = { position: [number, number, number] rotation: number diff --git a/packages/editor/src/components/editor/selection-manager.tsx b/packages/editor/src/components/editor/selection-manager.tsx index 1927ed51..81a6059c 100644 --- a/packages/editor/src/components/editor/selection-manager.tsx +++ b/packages/editor/src/components/editor/selection-manager.tsx @@ -56,6 +56,7 @@ import useEditor, { import { boxSelectHandled } from '../tools/select/box-select-tool' const isNodeInCurrentLevel = (node: AnyNode): boolean => { + // Elevators are building-scoped, so they stay selectable across level filters. if (node.type === 'elevator') return true const currentLevelId = useViewer.getState().selection.levelId if (!currentLevelId) return true // No level selected, allow all diff --git a/packages/editor/src/components/tools/elevator/elevator-tool.tsx b/packages/editor/src/components/tools/elevator/elevator-tool.tsx index a5702078..409628f2 100644 --- a/packages/editor/src/components/tools/elevator/elevator-tool.tsx +++ b/packages/editor/src/components/tools/elevator/elevator-tool.tsx @@ -9,7 +9,7 @@ import { } from '@pascal-app/core' import { useEffect, useMemo, useRef } from 'react' import * as THREE from 'three' -import { resolveElevatorSupportY } from '../../../lib/elevator-support' +import { resolveCurrentBuildingId, resolveElevatorSupportY } from '../../../lib/elevator-support' import { sfxEmitter } from '../../../lib/sfx-bus' import { CursorSphere } from '../shared/cursor-sphere' import { @@ -31,21 +31,6 @@ type ElevatorToolProps = { onPlaced?: (elevatorId: AnyNodeId, buildingId: BuildingNode['id']) => void } -function resolveCurrentBuildingId( - buildingId: BuildingNode['id'] | null, - levelId: LevelNode['id'] | null, -): BuildingNode['id'] | null { - if (buildingId) return buildingId as BuildingNode['id'] - if (!levelId) return null - - const level = useScene.getState().nodes[levelId as AnyNodeId] - if (level?.type === 'level' && level.parentId) { - return level.parentId as BuildingNode['id'] - } - - return null -} - function resolveDefaultServiceRange( buildingId: BuildingNode['id'], selectedLevelId: LevelNode['id'] | null, @@ -130,7 +115,11 @@ export const ElevatorTool: React.FC = ({ buildingId, levelId, const previewGeometry = useMemo(() => createElevatorPreviewGeometry(), []) useEffect(() => { - const currentBuildingId = resolveCurrentBuildingId(buildingId, levelId) + const currentBuildingId = resolveCurrentBuildingId({ + buildingId, + levelId, + nodes: useScene.getState().nodes, + }) if (!currentBuildingId) return rotationRef.current = 0 @@ -160,7 +149,11 @@ export const ElevatorTool: React.FC = ({ buildingId, levelId, } const onGridClick = (event: GridEvent) => { - const latestBuildingId = resolveCurrentBuildingId(buildingId, levelId) + const latestBuildingId = resolveCurrentBuildingId({ + buildingId, + levelId, + nodes: useScene.getState().nodes, + }) if (!latestBuildingId) return const gridX = Math.round(event.localPosition[0] * 2) / 2 diff --git a/packages/editor/src/components/tools/elevator/move-elevator-tool.tsx b/packages/editor/src/components/tools/elevator/move-elevator-tool.tsx index 2322c6dc..5a82db0c 100644 --- a/packages/editor/src/components/tools/elevator/move-elevator-tool.tsx +++ b/packages/editor/src/components/tools/elevator/move-elevator-tool.tsx @@ -6,6 +6,8 @@ import { emitter, type GridEvent, type LevelNode, + pauseSceneHistory, + resumeSceneHistory, sceneRegistry, useLiveTransforms, useScene, @@ -36,6 +38,7 @@ export function MoveElevatorTool({ onCommitted?: (nodeId: AnyNodeId) => void }) { const onCommittedRef = useRef(onCommitted) + const historyPausedRef = useRef(false) const previousGridPosRef = useRef<[number, number] | null>(null) const previewPositionRef = useRef([ movingNode.position[0], @@ -57,7 +60,19 @@ export function MoveElevatorTool({ }, [onCommitted]) useEffect(() => { - useScene.temporal.getState().pause() + const pauseHistory = () => { + const temporal = useScene.temporal.getState() + if (historyPausedRef.current || !temporal.isTracking) return + pauseSceneHistory(useScene) + historyPausedRef.current = true + } + const resumeHistory = () => { + if (!historyPausedRef.current) return + resumeSceneHistory(useScene) + historyPausedRef.current = false + } + + pauseHistory() const movingNodeId = (movingNode as { id?: ElevatorNode['id'] }).id const meta = @@ -148,7 +163,7 @@ export function MoveElevatorTool({ wasCommitted = true clearPreview() - useScene.temporal.getState().resume() + resumeHistory() if (movingNodeId && useScene.getState().nodes[movingNodeId as AnyNodeId]) { useScene.getState().updateNode(movingNodeId as AnyNodeId, { position: nextPosition, @@ -188,7 +203,7 @@ export function MoveElevatorTool({ } } resetObject(original.position, original.rotation) - useScene.temporal.getState().resume() + resumeHistory() markToolCancelConsumed() exitMoveMode() } @@ -226,7 +241,7 @@ export function MoveElevatorTool({ }) resetObject(original.position, original.rotation) } - useScene.temporal.getState().resume() + resumeHistory() emitter.off('grid:move', onGridMove) emitter.off('grid:click', onGridClick) emitter.off('tool:cancel', onCancel) diff --git a/packages/editor/src/lib/elevator-support.ts b/packages/editor/src/lib/elevator-support.ts index fc874562..711f2594 100644 --- a/packages/editor/src/lib/elevator-support.ts +++ b/packages/editor/src/lib/elevator-support.ts @@ -1,6 +1,7 @@ import { type AnyNode, type AnyNodeId, + type BuildingNode, type ElevatorNode, type LevelNode, spatialGridManager, @@ -21,6 +22,30 @@ function getBuildingLevels( .sort((left, right) => left.level - right.level) } +export function resolveCurrentBuildingId({ + buildingId, + levelId, + nodes, +}: { + buildingId: BuildingNode['id'] | null + levelId: LevelNode['id'] | null + nodes: Record +}): BuildingNode['id'] | null { + if (buildingId) return buildingId + if (!levelId) return null + + const level = nodes[levelId as AnyNodeId] + if ( + level?.type === 'level' && + level.parentId && + nodes[level.parentId as AnyNodeId]?.type === 'building' + ) { + return level.parentId as BuildingNode['id'] + } + + return null +} + export function resolveElevatorSupportLevelId({ buildingId, preferredLevelId, @@ -29,13 +54,16 @@ export function resolveElevatorSupportLevelId({ preferredLevelId?: string | null }): LevelNode['id'] | null { const nodes = useScene.getState().nodes + const preferred = preferredLevelId ? nodes[preferredLevelId as AnyNodeId] : undefined const levels = getBuildingLevels(buildingId, nodes) - if (levels.length === 0) return null - - const preferred = preferredLevelId + const preferredInBuilding = preferredLevelId ? levels.find((level) => level.id === preferredLevelId) : undefined - return preferred?.id ?? levels[0]?.id ?? null + + if (preferredInBuilding) return preferredInBuilding.id + if (levels.length === 0) return preferred?.type === 'level' ? preferred.id : null + + return levels[0]?.id ?? null } export function resolveElevatorSupportY({ diff --git a/packages/viewer/src/components/viewer/post-processing.tsx b/packages/viewer/src/components/viewer/post-processing.tsx index a64c1e2e..63d07039 100644 --- a/packages/viewer/src/components/viewer/post-processing.tsx +++ b/packages/viewer/src/components/viewer/post-processing.tsx @@ -116,6 +116,7 @@ const PostProcessingPasses = ({ // Subscribe to projectId so the pipeline rebuilds on project switch const projectId = useViewer((s) => s.projectId) + const lastProjectIdRef = useRef(projectId) // Bump this to force a pipeline rebuild (used by retry logic) const [pipelineVersion, setPipelineVersion] = useState(0) @@ -131,7 +132,8 @@ const PostProcessingPasses = ({ // Reset retry state when project changes useEffect(() => { - void projectId + if (lastProjectIdRef.current === projectId) return + lastProjectIdRef.current = projectId retryCountRef.current = 0 if (rebuildTimeoutRef.current !== null) { clearTimeout(rebuildTimeoutRef.current)