Refactor elevator systems across core, viewer, and editor
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
||||
type LevelNode,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { resolveElevatorSupportY } from '../../../lib/elevator-support'
|
||||
@@ -26,8 +25,16 @@ import {
|
||||
|
||||
const GRID_OFFSET = 0.02
|
||||
|
||||
function resolveCurrentBuildingId(): BuildingNode['id'] | null {
|
||||
const { buildingId, levelId } = useViewer.getState().selection
|
||||
type ElevatorToolProps = {
|
||||
buildingId: BuildingNode['id'] | null
|
||||
levelId: LevelNode['id'] | null
|
||||
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
|
||||
|
||||
@@ -39,12 +46,14 @@ function resolveCurrentBuildingId(): BuildingNode['id'] | null {
|
||||
return null
|
||||
}
|
||||
|
||||
function resolveDefaultServiceRange(buildingId: BuildingNode['id']): {
|
||||
function resolveDefaultServiceRange(
|
||||
buildingId: BuildingNode['id'],
|
||||
selectedLevelId: LevelNode['id'] | null,
|
||||
): {
|
||||
defaultLevelId: LevelNode['id'] | null
|
||||
fromLevelId: LevelNode['id'] | null
|
||||
toLevelId: LevelNode['id'] | null
|
||||
} {
|
||||
const { levelId } = useViewer.getState().selection
|
||||
const nodes = useScene.getState().nodes
|
||||
const building = nodes[buildingId as AnyNodeId]
|
||||
if (building?.type !== 'building') {
|
||||
@@ -55,7 +64,7 @@ function resolveDefaultServiceRange(buildingId: BuildingNode['id']): {
|
||||
.map((childId) => nodes[childId as AnyNodeId])
|
||||
.filter((node): node is LevelNode => node?.type === 'level')
|
||||
.sort((left, right) => left.level - right.level)
|
||||
const selectedLevelIndex = levels.findIndex((level) => level.id === levelId)
|
||||
const selectedLevelIndex = levels.findIndex((level) => level.id === selectedLevelId)
|
||||
const fromIndex = selectedLevelIndex >= 0 ? selectedLevelIndex : 0
|
||||
const fromLevel = levels[fromIndex]
|
||||
const toLevel = levels[Math.min(fromIndex + 1, levels.length - 1)] ?? fromLevel
|
||||
@@ -77,13 +86,15 @@ function createElevatorPreviewGeometry(): THREE.BufferGeometry {
|
||||
|
||||
function commitElevatorPlacement(
|
||||
buildingId: BuildingNode['id'],
|
||||
selectedLevelId: LevelNode['id'] | null,
|
||||
x: number,
|
||||
z: number,
|
||||
rotation: number,
|
||||
onPlaced: ElevatorToolProps['onPlaced'],
|
||||
): void {
|
||||
const { createNode, nodes } = useScene.getState()
|
||||
const elevatorCount = Object.values(nodes).filter((node) => node.type === 'elevator').length
|
||||
const serviceRange = resolveDefaultServiceRange(buildingId)
|
||||
const serviceRange = resolveDefaultServiceRange(buildingId, selectedLevelId)
|
||||
const supportY = resolveElevatorSupportY({
|
||||
buildingId,
|
||||
preferredLevelId: serviceRange.fromLevelId ?? serviceRange.defaultLevelId,
|
||||
@@ -107,30 +118,19 @@ function commitElevatorPlacement(
|
||||
})
|
||||
|
||||
createNode(elevator, buildingId)
|
||||
useViewer.getState().setSelection({ buildingId, selectedIds: [elevator.id] })
|
||||
onPlaced?.(elevator.id as AnyNodeId, buildingId)
|
||||
sfxEmitter.emit('sfx:structure-build')
|
||||
}
|
||||
|
||||
export const ElevatorTool: React.FC = () => {
|
||||
export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId, onPlaced }) => {
|
||||
const cursorRef = useRef<THREE.Group>(null)
|
||||
const previewRef = useRef<THREE.Group>(null)
|
||||
const rotationRef = useRef(0)
|
||||
const previousGridPosRef = useRef<[number, number] | null>(null)
|
||||
const buildingId = useViewer((state) => state.selection.buildingId)
|
||||
const levelId = useViewer((state) => state.selection.levelId)
|
||||
const previewGeometry = useMemo(() => createElevatorPreviewGeometry(), [])
|
||||
|
||||
useEffect(() => {
|
||||
const currentBuildingId =
|
||||
(buildingId as BuildingNode['id'] | null) ??
|
||||
(levelId
|
||||
? (() => {
|
||||
const level = useScene.getState().nodes[levelId as AnyNodeId]
|
||||
return level?.type === 'level' && level.parentId
|
||||
? (level.parentId as BuildingNode['id'])
|
||||
: null
|
||||
})()
|
||||
: null)
|
||||
const currentBuildingId = resolveCurrentBuildingId(buildingId, levelId)
|
||||
if (!currentBuildingId) return
|
||||
|
||||
rotationRef.current = 0
|
||||
@@ -160,12 +160,19 @@ export const ElevatorTool: React.FC = () => {
|
||||
}
|
||||
|
||||
const onGridClick = (event: GridEvent) => {
|
||||
const latestBuildingId = resolveCurrentBuildingId()
|
||||
const latestBuildingId = resolveCurrentBuildingId(buildingId, levelId)
|
||||
if (!latestBuildingId) return
|
||||
|
||||
const gridX = Math.round(event.localPosition[0] * 2) / 2
|
||||
const gridZ = Math.round(event.localPosition[2] * 2) / 2
|
||||
commitElevatorPlacement(latestBuildingId, gridX, gridZ, rotationRef.current)
|
||||
commitElevatorPlacement(
|
||||
latestBuildingId,
|
||||
levelId,
|
||||
gridX,
|
||||
gridZ,
|
||||
rotationRef.current,
|
||||
onPlaced,
|
||||
)
|
||||
}
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -195,7 +202,7 @@ export const ElevatorTool: React.FC = () => {
|
||||
emitter.off('grid:click', onGridClick)
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [buildingId, levelId])
|
||||
}, [buildingId, levelId, onPlaced])
|
||||
|
||||
return (
|
||||
<group>
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
useLiveTransforms,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||
import { resolveElevatorSupportY } from '../../../lib/elevator-support'
|
||||
@@ -29,7 +28,14 @@ function stripMoveMetadata(metadata: ElevatorNode['metadata']) {
|
||||
return nextMeta as ElevatorNode['metadata']
|
||||
}
|
||||
|
||||
export function MoveElevatorTool({ node: movingNode }: { node: ElevatorNode }) {
|
||||
export function MoveElevatorTool({
|
||||
node: movingNode,
|
||||
onCommitted,
|
||||
}: {
|
||||
node: ElevatorNode
|
||||
onCommitted?: (nodeId: AnyNodeId) => void
|
||||
}) {
|
||||
const onCommittedRef = useRef(onCommitted)
|
||||
const previousGridPosRef = useRef<[number, number] | null>(null)
|
||||
const previewPositionRef = useRef<ElevatorNode['position']>([
|
||||
movingNode.position[0],
|
||||
@@ -46,6 +52,10 @@ export function MoveElevatorTool({ node: movingNode }: { node: ElevatorNode }) {
|
||||
useEditor.getState().setMovingNode(null)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
onCommittedRef.current = onCommitted
|
||||
}, [onCommitted])
|
||||
|
||||
useEffect(() => {
|
||||
useScene.temporal.getState().pause()
|
||||
const movingNodeId = (movingNode as { id?: ElevatorNode['id'] }).id
|
||||
@@ -145,7 +155,7 @@ export function MoveElevatorTool({ node: movingNode }: { node: ElevatorNode }) {
|
||||
rotation: pendingRotation,
|
||||
metadata: committedMeta,
|
||||
})
|
||||
useViewer.getState().setSelection({ selectedIds: [movingNodeId] })
|
||||
onCommittedRef.current?.(movingNodeId as AnyNodeId)
|
||||
} else if (movingNode.parentId) {
|
||||
const elevator = ElevatorNodeSchema.parse({
|
||||
...movingNode,
|
||||
@@ -155,7 +165,7 @@ export function MoveElevatorTool({ node: movingNode }: { node: ElevatorNode }) {
|
||||
metadata: committedMeta,
|
||||
})
|
||||
useScene.getState().createNode(elevator, movingNode.parentId as AnyNodeId)
|
||||
useViewer.getState().setSelection({ selectedIds: [elevator.id] })
|
||||
onCommittedRef.current?.(elevator.id as AnyNodeId)
|
||||
}
|
||||
|
||||
sfxEmitter.emit('sfx:item-place')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AnyNodeId,
|
||||
BuildingNode,
|
||||
CeilingNode,
|
||||
ColumnNode,
|
||||
@@ -93,15 +94,17 @@ function MoveItemContent({ movingNode }: { movingNode: ItemNode }) {
|
||||
}
|
||||
|
||||
export const MoveTool: React.FC<{
|
||||
onNodeMoved?: (nodeId: AnyNodeId) => void
|
||||
onSpawnMoved?: (nodeId: SpawnNode['id']) => void
|
||||
}> = ({ onSpawnMoved }) => {
|
||||
}> = ({ onNodeMoved, onSpawnMoved }) => {
|
||||
const movingNode = useEditor((state) => state.movingNode)
|
||||
|
||||
if (!movingNode) return null
|
||||
if (movingNode.type === 'building')
|
||||
return <MoveBuildingContent node={movingNode as BuildingNode} />
|
||||
if (movingNode.type === 'door') return <MoveDoorTool node={movingNode as DoorNode} />
|
||||
if (movingNode.type === 'elevator') return <MoveElevatorTool node={movingNode as ElevatorNode} />
|
||||
if (movingNode.type === 'elevator')
|
||||
return <MoveElevatorTool node={movingNode as ElevatorNode} onCommitted={onNodeMoved} />
|
||||
if (movingNode.type === 'window') return <MoveWindowTool node={movingNode as WindowNode} />
|
||||
if (movingNode.type === 'fence') return <MoveFenceTool node={movingNode as FenceNode} />
|
||||
if (movingNode.type === 'ceiling') return <MoveCeilingTool node={movingNode as CeilingNode} />
|
||||
|
||||
@@ -42,7 +42,6 @@ const tools: Record<Phase, Partial<Record<Tool, React.FC>>> = {
|
||||
slab: SlabTool,
|
||||
ceiling: CeilingTool,
|
||||
roof: RoofTool,
|
||||
elevator: ElevatorTool,
|
||||
stair: StairTool,
|
||||
door: DoorTool,
|
||||
item: ItemTool,
|
||||
@@ -132,12 +131,20 @@ export const ToolManager: React.FC = () => {
|
||||
const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
|
||||
setSelection({ selectedIds: [nodeId] })
|
||||
}
|
||||
const handlePlacedElevatorSelected = (
|
||||
nodeId: AnyNodeId,
|
||||
elevatorBuildingId: BuildingNode['id'],
|
||||
) => {
|
||||
setSelection({ buildingId: elevatorBuildingId, selectedIds: [nodeId] })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSiteBoundaryEditor && <SiteBoundaryEditor />}
|
||||
{/* World-space tools: site boundary and building movement operate in world coordinates */}
|
||||
{movingNode?.type === 'building' && <MoveTool onSpawnMoved={handlePlacedNodeSelected} />}
|
||||
{movingNode?.type === 'building' && (
|
||||
<MoveTool onNodeMoved={handlePlacedNodeSelected} onSpawnMoved={handlePlacedNodeSelected} />
|
||||
)}
|
||||
|
||||
{/* Building-local group: all other tools are relative to the selected building.
|
||||
Cursor visuals set positions in building-local space; this group applies the
|
||||
@@ -162,7 +169,10 @@ export const ToolManager: React.FC = () => {
|
||||
{curvingWall && <CurveWallTool node={curvingWall} />}
|
||||
{curvingFence && <CurveFenceTool node={curvingFence} />}
|
||||
{movingNode && movingNode.type !== 'building' && (
|
||||
<MoveTool onSpawnMoved={handlePlacedNodeSelected} />
|
||||
<MoveTool
|
||||
onNodeMoved={handlePlacedNodeSelected}
|
||||
onSpawnMoved={handlePlacedNodeSelected}
|
||||
/>
|
||||
)}
|
||||
{!movingNode && showBuildTool && tool === 'spawn' && (
|
||||
<SpawnTool currentLevelId={selectedLevelId} onPlaced={handlePlacedNodeSelected} />
|
||||
@@ -170,7 +180,16 @@ export const ToolManager: React.FC = () => {
|
||||
{!movingNode && showBuildTool && tool === 'column' && (
|
||||
<ColumnTool currentLevelId={selectedLevelId} onPlaced={handlePlacedNodeSelected} />
|
||||
)}
|
||||
{!movingNode && BuildToolComponent && tool !== 'column' && <BuildToolComponent />}
|
||||
{!movingNode && showBuildTool && tool === 'elevator' && (
|
||||
<ElevatorTool
|
||||
buildingId={buildingId as BuildingNode['id'] | null}
|
||||
levelId={selectedLevelId}
|
||||
onPlaced={handlePlacedElevatorSelected}
|
||||
/>
|
||||
)}
|
||||
{!movingNode && BuildToolComponent && tool !== 'column' && tool !== 'elevator' && (
|
||||
<BuildToolComponent />
|
||||
)}
|
||||
</group>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user