Refine elevator sync and viewer rebuild handling

This commit is contained in:
sudhir
2026-05-13 22:28:19 +05:30
parent 9838d2cd4f
commit ce69433a1a
8 changed files with 76 additions and 40 deletions
@@ -1,6 +1,7 @@
import { z } from 'zod' import { z } from 'zod'
export const SurfaceHoleMetadata = z.object({ 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'), source: z.enum(['manual', 'stair', 'elevator']).default('manual'),
stairId: z.string().optional(), stairId: z.string().optional(),
elevatorId: z.string().optional(), elevatorId: z.string().optional(),
@@ -1,15 +1,16 @@
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync' 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' import { resolveElevatorServiceLevels } from './elevator-service'
type Point2D = [number, number] type Point2D = [number, number]
type SurfaceHoleMetadata = {
source: 'manual' | 'stair' | 'elevator'
elevatorId?: string
stairId?: string
}
const ELEVATOR_OPENING_PADDING = 0.08 const ELEVATOR_OPENING_PADDING = 0.08
const DEFAULT_ELEVATOR_SHAFT_WALL_THICKNESS = 0.09 const DEFAULT_ELEVATOR_SHAFT_WALL_THICKNESS = 0.09
@@ -6,17 +6,12 @@ import type {
SlabNode, SlabNode,
StairNode, StairNode,
StairSegmentNode, StairSegmentNode,
SurfaceHoleMetadata,
} from '../../schema' } from '../../schema'
import { DEFAULT_WALL_HEIGHT } from '../wall/wall-footprint' import { DEFAULT_WALL_HEIGHT } from '../wall/wall-footprint'
type Point2D = [number, number] type Point2D = [number, number]
type SurfaceHoleMetadata = {
source: 'manual' | 'stair' | 'elevator'
elevatorId?: string
stairId?: string
}
type SegmentTransform = { type SegmentTransform = {
position: [number, number, number] position: [number, number, number]
rotation: number rotation: number
@@ -56,6 +56,7 @@ import useEditor, {
import { boxSelectHandled } from '../tools/select/box-select-tool' import { boxSelectHandled } from '../tools/select/box-select-tool'
const isNodeInCurrentLevel = (node: AnyNode): boolean => { const isNodeInCurrentLevel = (node: AnyNode): boolean => {
// Elevators are building-scoped, so they stay selectable across level filters.
if (node.type === 'elevator') return true if (node.type === 'elevator') return true
const currentLevelId = useViewer.getState().selection.levelId const currentLevelId = useViewer.getState().selection.levelId
if (!currentLevelId) return true // No level selected, allow all if (!currentLevelId) return true // No level selected, allow all
@@ -9,7 +9,7 @@ import {
} from '@pascal-app/core' } from '@pascal-app/core'
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo, useRef } from 'react'
import * as THREE from 'three' 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 { sfxEmitter } from '../../../lib/sfx-bus'
import { CursorSphere } from '../shared/cursor-sphere' import { CursorSphere } from '../shared/cursor-sphere'
import { import {
@@ -31,21 +31,6 @@ type ElevatorToolProps = {
onPlaced?: (elevatorId: AnyNodeId, buildingId: BuildingNode['id']) => void 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( function resolveDefaultServiceRange(
buildingId: BuildingNode['id'], buildingId: BuildingNode['id'],
selectedLevelId: LevelNode['id'] | null, selectedLevelId: LevelNode['id'] | null,
@@ -130,7 +115,11 @@ export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId,
const previewGeometry = useMemo(() => createElevatorPreviewGeometry(), []) const previewGeometry = useMemo(() => createElevatorPreviewGeometry(), [])
useEffect(() => { useEffect(() => {
const currentBuildingId = resolveCurrentBuildingId(buildingId, levelId) const currentBuildingId = resolveCurrentBuildingId({
buildingId,
levelId,
nodes: useScene.getState().nodes,
})
if (!currentBuildingId) return if (!currentBuildingId) return
rotationRef.current = 0 rotationRef.current = 0
@@ -160,7 +149,11 @@ export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId,
} }
const onGridClick = (event: GridEvent) => { const onGridClick = (event: GridEvent) => {
const latestBuildingId = resolveCurrentBuildingId(buildingId, levelId) const latestBuildingId = resolveCurrentBuildingId({
buildingId,
levelId,
nodes: useScene.getState().nodes,
})
if (!latestBuildingId) return if (!latestBuildingId) return
const gridX = Math.round(event.localPosition[0] * 2) / 2 const gridX = Math.round(event.localPosition[0] * 2) / 2
@@ -6,6 +6,8 @@ import {
emitter, emitter,
type GridEvent, type GridEvent,
type LevelNode, type LevelNode,
pauseSceneHistory,
resumeSceneHistory,
sceneRegistry, sceneRegistry,
useLiveTransforms, useLiveTransforms,
useScene, useScene,
@@ -36,6 +38,7 @@ export function MoveElevatorTool({
onCommitted?: (nodeId: AnyNodeId) => void onCommitted?: (nodeId: AnyNodeId) => void
}) { }) {
const onCommittedRef = useRef(onCommitted) const onCommittedRef = useRef(onCommitted)
const historyPausedRef = useRef(false)
const previousGridPosRef = useRef<[number, number] | null>(null) const previousGridPosRef = useRef<[number, number] | null>(null)
const previewPositionRef = useRef<ElevatorNode['position']>([ const previewPositionRef = useRef<ElevatorNode['position']>([
movingNode.position[0], movingNode.position[0],
@@ -57,7 +60,19 @@ export function MoveElevatorTool({
}, [onCommitted]) }, [onCommitted])
useEffect(() => { 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 movingNodeId = (movingNode as { id?: ElevatorNode['id'] }).id
const meta = const meta =
@@ -148,7 +163,7 @@ export function MoveElevatorTool({
wasCommitted = true wasCommitted = true
clearPreview() clearPreview()
useScene.temporal.getState().resume() resumeHistory()
if (movingNodeId && useScene.getState().nodes[movingNodeId as AnyNodeId]) { if (movingNodeId && useScene.getState().nodes[movingNodeId as AnyNodeId]) {
useScene.getState().updateNode(movingNodeId as AnyNodeId, { useScene.getState().updateNode(movingNodeId as AnyNodeId, {
position: nextPosition, position: nextPosition,
@@ -188,7 +203,7 @@ export function MoveElevatorTool({
} }
} }
resetObject(original.position, original.rotation) resetObject(original.position, original.rotation)
useScene.temporal.getState().resume() resumeHistory()
markToolCancelConsumed() markToolCancelConsumed()
exitMoveMode() exitMoveMode()
} }
@@ -226,7 +241,7 @@ export function MoveElevatorTool({
}) })
resetObject(original.position, original.rotation) resetObject(original.position, original.rotation)
} }
useScene.temporal.getState().resume() resumeHistory()
emitter.off('grid:move', onGridMove) emitter.off('grid:move', onGridMove)
emitter.off('grid:click', onGridClick) emitter.off('grid:click', onGridClick)
emitter.off('tool:cancel', onCancel) emitter.off('tool:cancel', onCancel)
+32 -4
View File
@@ -1,6 +1,7 @@
import { import {
type AnyNode, type AnyNode,
type AnyNodeId, type AnyNodeId,
type BuildingNode,
type ElevatorNode, type ElevatorNode,
type LevelNode, type LevelNode,
spatialGridManager, spatialGridManager,
@@ -21,6 +22,30 @@ function getBuildingLevels(
.sort((left, right) => left.level - right.level) .sort((left, right) => left.level - right.level)
} }
export function resolveCurrentBuildingId({
buildingId,
levelId,
nodes,
}: {
buildingId: BuildingNode['id'] | null
levelId: LevelNode['id'] | null
nodes: Record<string, AnyNode>
}): 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({ export function resolveElevatorSupportLevelId({
buildingId, buildingId,
preferredLevelId, preferredLevelId,
@@ -29,13 +54,16 @@ export function resolveElevatorSupportLevelId({
preferredLevelId?: string | null preferredLevelId?: string | null
}): LevelNode['id'] | null { }): LevelNode['id'] | null {
const nodes = useScene.getState().nodes const nodes = useScene.getState().nodes
const preferred = preferredLevelId ? nodes[preferredLevelId as AnyNodeId] : undefined
const levels = getBuildingLevels(buildingId, nodes) const levels = getBuildingLevels(buildingId, nodes)
if (levels.length === 0) return null const preferredInBuilding = preferredLevelId
const preferred = preferredLevelId
? levels.find((level) => level.id === preferredLevelId) ? levels.find((level) => level.id === preferredLevelId)
: undefined : 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({ export function resolveElevatorSupportY({
@@ -116,6 +116,7 @@ const PostProcessingPasses = ({
// Subscribe to projectId so the pipeline rebuilds on project switch // Subscribe to projectId so the pipeline rebuilds on project switch
const projectId = useViewer((s) => s.projectId) const projectId = useViewer((s) => s.projectId)
const lastProjectIdRef = useRef(projectId)
// Bump this to force a pipeline rebuild (used by retry logic) // Bump this to force a pipeline rebuild (used by retry logic)
const [pipelineVersion, setPipelineVersion] = useState(0) const [pipelineVersion, setPipelineVersion] = useState(0)
@@ -131,7 +132,8 @@ const PostProcessingPasses = ({
// Reset retry state when project changes // Reset retry state when project changes
useEffect(() => { useEffect(() => {
void projectId if (lastProjectIdRef.current === projectId) return
lastProjectIdRef.current = projectId
retryCountRef.current = 0 retryCountRef.current = 0
if (rebuildTimeoutRef.current !== null) { if (rebuildTimeoutRef.current !== null) {
clearTimeout(rebuildTimeoutRef.current) clearTimeout(rebuildTimeoutRef.current)