Improve elevator floorplan resize and drag controls

This commit is contained in:
sudhir
2026-05-10 12:20:53 +05:30
parent fdf08c091f
commit 909f96ef09
10 changed files with 1531 additions and 232 deletions
+1
View File
@@ -85,6 +85,7 @@ export {
} from './store/use-live-node-overrides'
export { default as useLiveTransforms, type LiveTransform } from './store/use-live-transforms'
export { clearSceneHistory, default as useScene } from './store/use-scene'
export { resolveElevatorDispatchTarget } from './systems/elevator/elevator-dispatch'
export { syncAutoElevatorOpenings } from './systems/elevator/elevator-opening-sync'
export {
resolveElevatorBuildingLevels,
+6 -1
View File
@@ -42,7 +42,12 @@ export {
ColumnSupportStyle,
} from './nodes/column'
export { DoorNode, DoorSegment } from './nodes/door'
export { ElevatorDoorStyle, ElevatorNode } from './nodes/elevator'
export {
ElevatorDoorPanelStyle,
ElevatorDoorStyle,
ElevatorNode,
ElevatorShaftStyle,
} from './nodes/elevator'
export { FenceBaseStyle, FenceNode, FenceStyle } from './nodes/fence'
export { GuideNode, GuideScaleReference } from './nodes/guide'
export type {
+19 -2
View File
@@ -4,8 +4,12 @@ import { BaseNode, nodeType, objectId } from '../base'
import { MaterialSchema } from '../material'
export const ElevatorDoorStyle = z.enum(['center-opening', 'single-left', 'single-right'])
export const ElevatorDoorPanelStyle = z.enum(['glass-frame', 'solid-panel', 'segmented-panel'])
export const ElevatorShaftStyle = z.enum(['solid', 'glass'])
export type ElevatorDoorPanelStyle = z.infer<typeof ElevatorDoorPanelStyle>
export type ElevatorDoorStyle = z.infer<typeof ElevatorDoorStyle>
export type ElevatorShaftStyle = z.infer<typeof ElevatorShaftStyle>
export const ElevatorNode = BaseNode.extend({
id: objectId('elevator'),
@@ -17,13 +21,20 @@ export const ElevatorNode = BaseNode.extend({
rotation: z.number().default(0),
width: z.number().default(1.6),
depth: z.number().default(1.6),
shaftWidth: z.number().optional(),
shaftDepth: z.number().optional(),
shaftWallThickness: z.number().default(0.09),
shaftStyle: ElevatorShaftStyle.default('solid'),
cabHeight: z.number().default(2.35),
doorWidth: z.number().default(0.95),
doorHeight: z.number().default(2.1),
doorStyle: ElevatorDoorStyle.default('center-opening'),
doorPanelStyle: ElevatorDoorPanelStyle.default('glass-frame'),
fromLevelId: z.string().nullable().default(null),
toLevelId: z.string().nullable().default(null),
servedLevelIds: z.array(z.string()).optional(),
disabledLevelIds: z.array(z.string()).default([]),
serviceOnlyLevelIds: z.array(z.string()).default([]),
defaultLevelId: z.string().nullable().default(null),
speed: z.number().default(2.2),
doorDurationMs: z.number().default(900),
@@ -34,11 +45,17 @@ export const ElevatorNode = BaseNode.extend({
- parentId: building that owns this elevator
- position: building-local shaft center on the X/Z plane
- rotation: rotation around the Y axis
- width/depth: shaft and cab footprint
- width/depth: cab footprint
- shaftWidth/shaftDepth: optional clear shaft footprint; falls back to cab footprint
- shaftWallThickness: visible shaft shell thickness
- shaftStyle: solid or glass shaft shell presentation
- cabHeight: visible elevator cab height
- doorWidth/doorHeight/doorStyle: landing and cab door presentation
- doorWidth/doorHeight/doorStyle: landing and cab door movement/opening presentation
- doorPanelStyle: visual leaf style for glass-frame, solid-panel, or segmented-panel doors
- fromLevelId / toLevelId: source and destination levels used for service range and auto cutouts
- servedLevelIds: legacy optional explicit level list; used only when from/to are missing
- disabledLevelIds: stops visible in the service range but unavailable for public/cab requests
- serviceOnlyLevelIds: stops unavailable from landing calls but available from cab/admin controls
- defaultLevelId: starting/resting level, falling back to the lowest served level
- speed/doorDurationMs/dwellMs: runtime animation defaults
`,
@@ -0,0 +1,90 @@
import type { AnyNode, AnyNodeId, ElevatorNode } from '../../schema'
import type { ElevatorInteractiveState } from '../../store/use-interactive'
import { resolveElevatorServiceLevels } from './elevator-service'
type ElevatorRuntimeMap = Record<AnyNodeId, ElevatorInteractiveState>
type ResolveElevatorDispatchTargetArgs = {
elevators: ElevatorRuntimeMap
levelId: AnyNodeId
nodes: Record<string, AnyNode>
requestedElevatorId: AnyNodeId
}
function getRuntimeLevelId(runtime: ElevatorInteractiveState | undefined, elevator: ElevatorNode) {
return runtime?.currentLevelId ?? elevator.defaultLevelId ?? elevator.fromLevelId ?? null
}
function scoreDispatchCandidate({
elevator,
elevators,
levelId,
nodes,
}: {
elevator: ElevatorNode
elevators: ElevatorRuntimeMap
levelId: AnyNodeId
nodes: Record<string, AnyNode>
}) {
if ((elevator.disabledLevelIds ?? []).includes(levelId)) return null
if ((elevator.serviceOnlyLevelIds ?? []).includes(levelId)) return null
const serviceLevels = resolveElevatorServiceLevels(elevator, nodes)
const targetIndex = serviceLevels.findIndex((level) => level.id === levelId)
if (targetIndex < 0) return null
const runtime = elevators[elevator.id as AnyNodeId]
const runtimeLevelId = getRuntimeLevelId(runtime, elevator)
const currentIndex = runtimeLevelId
? serviceLevels.findIndex((level) => level.id === runtimeLevelId)
: -1
const resolvedCurrentIndex = currentIndex >= 0 ? currentIndex : 0
const distance = Math.abs(targetIndex - resolvedCurrentIndex)
const queuePenalty = (runtime?.queue.length ?? 0) * 2 + (runtime?.targetLevelId ? 1 : 0)
const motionPenalty = runtime?.phase === 'moving' || runtime?.phase === 'closing' ? 0.5 : 0
const openPenalty = runtime?.phase === 'open' || runtime?.phase === 'opening' ? 0.2 : 0
return distance + queuePenalty + motionPenalty + openPenalty
}
export function resolveElevatorDispatchTarget({
elevators,
levelId,
nodes,
requestedElevatorId,
}: ResolveElevatorDispatchTargetArgs): AnyNodeId {
const requestedElevator = nodes[requestedElevatorId]
if (!(requestedElevator?.type === 'elevator' && requestedElevator.parentId)) {
return requestedElevatorId
}
const building = nodes[requestedElevator.parentId as AnyNodeId]
if (building?.type !== 'building') {
return requestedElevatorId
}
let bestElevatorId: AnyNodeId | null = null
let bestScore = Number.POSITIVE_INFINITY
for (const childId of building.children) {
const candidate = nodes[childId as AnyNodeId]
if (!(candidate?.type === 'elevator' && candidate.visible !== false)) continue
const score = scoreDispatchCandidate({
elevator: candidate,
elevators,
levelId,
nodes,
})
if (score === null) continue
const tieBreaker = candidate.id === requestedElevatorId ? -0.01 : 0
const finalScore = score + tieBreaker
if (finalScore < bestScore) {
bestScore = finalScore
bestElevatorId = candidate.id as AnyNodeId
}
}
return bestElevatorId ?? requestedElevatorId
}
@@ -11,6 +11,7 @@ type SurfaceHoleMetadata = {
}
const ELEVATOR_OPENING_PADDING = 0.08
const DEFAULT_ELEVATOR_SHAFT_WALL_THICKNESS = 0.09
function pointsEqual(a: Point2D, b: Point2D, tolerance = 1e-5) {
const dx = a[0] - b[0]
@@ -141,8 +142,14 @@ function shouldApplyElevatorToCeiling(
}
function getElevatorOpeningPolygon(elevator: ElevatorNode): Point2D[] {
const halfWidth = Math.max(elevator.width, 0.8) / 2 + ELEVATOR_OPENING_PADDING
const halfDepth = Math.max(elevator.depth, 0.8) / 2 + ELEVATOR_OPENING_PADDING
const wallThickness = Math.max(
elevator.shaftWallThickness ?? DEFAULT_ELEVATOR_SHAFT_WALL_THICKNESS,
0.04,
)
const shaftWidth = Math.max(elevator.shaftWidth ?? elevator.width, elevator.width, 0.8)
const shaftDepth = Math.max(elevator.shaftDepth ?? elevator.depth, elevator.depth, 0.8)
const halfWidth = shaftWidth / 2 + wallThickness + ELEVATOR_OPENING_PADDING
const halfDepth = shaftDepth / 2 + wallThickness + ELEVATOR_OPENING_PADDING
const corners: Point2D[] = [
[-halfWidth, -halfDepth],
[halfWidth, -halfDepth],