Improve elevator floorplan resize and drag controls
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user