Refine elevator sync and viewer rebuild handling
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user