Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/editor/public/icons/elevator.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/core/src/events/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
CeilingNode,
ColumnNode,
DoorNode,
ElevatorNode,
FenceNode,
GuideNode,
ItemNode,
Expand Down Expand Up @@ -66,6 +67,7 @@ export type StairEvent = NodeEvent<StairNode>
export type StairSegmentEvent = NodeEvent<StairSegmentNode>
export type WindowEvent = NodeEvent<WindowNode>
export type DoorEvent = NodeEvent<DoorNode>
export type ElevatorEvent = NodeEvent<ElevatorNode>

// Event suffixes - exported for use in hooks
export const eventSuffixes = [
Expand Down Expand Up @@ -183,6 +185,7 @@ type EditorEvents = GridEvents &
NodeEvents<'item', ItemEvent> &
NodeEvents<'site', SiteEvent> &
NodeEvents<'building', BuildingEvent> &
NodeEvents<'elevator', ElevatorEvent> &
NodeEvents<'level', LevelEvent> &
NodeEvents<'zone', ZoneEvent> &
NodeEvents<'slab', SlabEvent> &
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/hooks/scene-registry/scene-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const sceneRegistry = {
building: new Set<string>(),
ceiling: new Set<string>(),
column: new Set<string>(),
elevator: new Set<string>(),
level: new Set<string>(),
wall: new Set<string>(),
fence: new Set<string>(),
Expand Down
44 changes: 44 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type {
CeilingEvent,
ColumnEvent,
DoorEvent,
ElevatorEvent,
EventSuffix,
FenceEvent,
GridEvent,
Expand Down Expand Up @@ -73,13 +74,56 @@ export {
type ControlValue,
type DoorAnimationState,
type DoorInteractiveState,
type ElevatorInteractiveState,
type ElevatorPhase,
type ItemInteractiveState,
useInteractive,
type WindowAnimationState,
type WindowInteractiveState,
} from './store/use-interactive'
export {
default as useLiveNodeOverrides,
type LiveNodeOverrides,
} 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 {
getElevatorCabCenterZ,
getElevatorCabDepth,
getElevatorCabWidth,
getElevatorDoorLeafSides,
getElevatorDoorLeafWidth,
getElevatorDoorLeafX,
getElevatorShaftDepth,
getElevatorShaftWallThickness,
getElevatorShaftWidth,
getResolvedElevatorDoorPanelStyle,
getResolvedElevatorDoorStyle,
getResolvedElevatorShaftStyle,
type ElevatorDoorSide,
} from './systems/elevator/elevator-geometry'
export { syncAutoElevatorOpenings } from './systems/elevator/elevator-opening-sync'
export { ElevatorOpeningSystem } from './systems/elevator/elevator-opening-system'
export {
createElevatorInteractiveState,
openElevatorDoor,
openElevatorDoorState,
queueElevatorRequest,
requestElevatorLevel,
stepElevatorRuntimeState,
stepElevatorRuntimes,
} from './systems/elevator/elevator-runtime'
export { ElevatorRuntimeSystem } from './systems/elevator/elevator-runtime-system'
export {
DEFAULT_ELEVATOR_LEVEL_HEIGHT,
type ElevatorLevelEntry,
getElevatorLevelHeight,
resolveElevatorBuildingLevels,
resolveElevatorLevels,
resolveElevatorServiceLevelIds,
resolveElevatorServiceLevels,
} from './systems/elevator/elevator-service'
export { syncAutoStairOpenings } from './systems/stair/stair-opening-sync'
export {
getClampedWallCurveOffset,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export {
ColumnSupportStyle,
} from './nodes/column'
export { DoorNode, DoorSegment } from './nodes/door'
export {
ElevatorDoorPanelStyle,
ElevatorDoorStyle,
ElevatorNode,
ElevatorShaftStyle,
} from './nodes/elevator'
export { FenceBaseStyle, FenceNode, FenceStyle } from './nodes/fence'
export { GuideNode, GuideScaleReference } from './nodes/guide'
export type {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/schema/nodes/building.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
import { ElevatorNode } from './elevator'
import { LevelNode } from './level'

export const BuildingNode = BaseNode.extend({
id: objectId('building'),
type: nodeType('building'),
children: z.array(LevelNode.shape.id).default([]),
children: z.array(z.union([LevelNode.shape.id, ElevatorNode.shape.id])).default([]),
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
}).describe(
dedent`
Building node - used to represent a building
- position: position in site coordinate system
- rotation: rotation in site coordinate system
- children: array of level nodes (each level is a tree of floor and wall nodes)
- children: array of level nodes and building-level systems such as elevators
`,
)

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/schema/nodes/ceiling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const CeilingNode = BaseNode.extend({
Ceiling node - used to represent a ceiling in the building
- polygon: array of [x, z] points defining the ceiling boundary
- holes: array of polygons representing holes in the ceiling
- holeMetadata: metadata parallel to holes, used to preserve manual and stair-managed cutouts
- holeMetadata: metadata parallel to holes, used to preserve manual and auto-managed cutouts
- autoFromWalls: whether the ceiling is automatically generated from a closed wall loop
`,
)
Expand Down
64 changes: 64 additions & 0 deletions packages/core/src/schema/nodes/elevator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import dedent from 'dedent'
import { z } from 'zod'
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'),
type: nodeType('elevator'),
material: MaterialSchema.optional(),
materialPreset: z.string().optional(),
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
// Rotation around the Y axis in radians.
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),
dwellMs: z.number().default(1400),
}).describe(
dedent`
Elevator node - a vertical transport core attached to a building.
- parentId: building that owns this elevator
- position: building-local shaft center on the X/Z plane
- rotation: rotation around the Y axis
- 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 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
`,
)

export type ElevatorNode = z.infer<typeof ElevatorNode>
2 changes: 1 addition & 1 deletion packages/core/src/schema/nodes/slab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const SlabNode = BaseNode.extend({
Slab node - used to represent a slab/floor in the building
- polygon: array of [x, z] points defining the slab boundary
- holes: array of [x, z] polygons representing cutouts in the slab
- holeMetadata: metadata parallel to holes, used to preserve manual and stair-managed cutouts
- holeMetadata: metadata parallel to holes, used to preserve manual and auto-managed cutouts
- elevation: elevation in meters
- autoFromWalls: whether the slab is automatically generated from a closed wall loop
`,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/schema/nodes/surface-hole-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { z } from 'zod'

export const SurfaceHoleMetadata = z.object({
source: z.enum(['manual', 'stair']).default('manual'),
// Stair/elevator auto-openings use stairId/elevatorId so sync can replace only its own holes.
source: z.enum(['manual', 'stair', 'elevator']).default('manual'),
stairId: z.string().optional(),
elevatorId: z.string().optional(),
})

export type SurfaceHoleMetadata = z.infer<typeof SurfaceHoleMetadata>
2 changes: 2 additions & 0 deletions packages/core/src/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BuildingNode } from './nodes/building'
import { CeilingNode } from './nodes/ceiling'
import { ColumnNode } from './nodes/column'
import { DoorNode } from './nodes/door'
import { ElevatorNode } from './nodes/elevator'
import { FenceNode } from './nodes/fence'
import { GuideNode } from './nodes/guide'
import { ItemNode } from './nodes/item'
Expand All @@ -22,6 +23,7 @@ import { ZoneNode } from './nodes/zone'
export const AnyNode = z.discriminatedUnion('type', [
SiteNode,
BuildingNode,
ElevatorNode,
LevelNode,
ColumnNode,
WallNode,
Expand Down
66 changes: 66 additions & 0 deletions packages/core/src/store/use-interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,25 @@ export type WindowAnimationState = {
persist: boolean
}

export type ElevatorPhase = 'idle' | 'closing' | 'moving' | 'opening' | 'open'

export type ElevatorInteractiveState = {
currentLevelId: AnyNodeId | null
targetLevelId: AnyNodeId | null
carY: number
doorOpen: number
phase: ElevatorPhase
phaseStartedAt: number | null
queue: AnyNodeId[]
}

type InteractiveStore = {
items: Record<AnyNodeId, ItemInteractiveState>
doors: Record<AnyNodeId, DoorInteractiveState>
doorAnimations: Record<AnyNodeId, DoorAnimationState>
windows: Record<AnyNodeId, WindowInteractiveState>
windowAnimations: Record<AnyNodeId, WindowAnimationState>
elevators: Record<AnyNodeId, ElevatorInteractiveState>

/** Initialize a node's interactive state from its asset definition (idempotent) */
initItem: (itemId: AnyNodeId, interactive: Interactive) => void
Expand Down Expand Up @@ -78,6 +91,15 @@ type InteractiveStore = {

/** Cancel a queued window animation */
cancelWindowAnimation: (windowId: AnyNodeId) => void

/** Initialize an elevator's runtime state from its default served level. */
initElevator: (elevatorId: AnyNodeId, levelId: AnyNodeId, carY: number) => void

/** Merge runtime elevator state. */
setElevatorState: (elevatorId: AnyNodeId, value: Partial<ElevatorInteractiveState>) => void

/** Remove elevator runtime state when its renderer unmounts. */
removeElevator: (elevatorId: AnyNodeId) => void
}

const defaultControlValue = (interactive: Interactive, index: number): ControlValue => {
Expand All @@ -99,6 +121,7 @@ export const useInteractive = create<InteractiveStore>((set, get) => ({
doorAnimations: {},
windows: {},
windowAnimations: {},
elevators: {},

initItem: (itemId, interactive) => {
const { controls } = interactive
Expand Down Expand Up @@ -203,4 +226,47 @@ export const useInteractive = create<InteractiveStore>((set, get) => ({
return { windowAnimations: rest }
})
},

initElevator: (elevatorId, levelId, carY) => {
if (get().elevators[elevatorId]) return

set((state) => ({
elevators: {
...state.elevators,
[elevatorId]: {
currentLevelId: levelId,
targetLevelId: null,
carY,
doorOpen: 0,
phase: 'idle',
phaseStartedAt: null,
queue: [],
},
},
}))
},

setElevatorState: (elevatorId, value) => {
set((state) => {
const current = state.elevators[elevatorId]
if (!current) return state

return {
elevators: {
...state.elevators,
[elevatorId]: {
...current,
...value,
},
},
}
})
},

removeElevator: (elevatorId) => {
set((state) => {
const { [elevatorId]: _, ...rest } = state.elevators
return { elevators: rest }
})
},
}))
31 changes: 31 additions & 0 deletions packages/core/src/store/use-live-node-overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { create } from 'zustand'

export type LiveNodeOverrides = Record<string, unknown>

type LiveNodeOverrideState = {
overrides: Map<string, LiveNodeOverrides>
set(nodeId: string, values: LiveNodeOverrides): void
get(nodeId: string): LiveNodeOverrides | undefined
clear(nodeId: string): void
clearAll(): void
}

const useLiveNodeOverrides = create<LiveNodeOverrideState>((set, get) => ({
overrides: new Map(),
set: (nodeId, values) =>
set((state) => {
const next = new Map(state.overrides)
next.set(nodeId, { ...(next.get(nodeId) ?? {}), ...values })
return { overrides: next }
}),
get: (nodeId) => get().overrides.get(nodeId),
clear: (nodeId) =>
set((state) => {
const next = new Map(state.overrides)
next.delete(nodeId)
return { overrides: next }
}),
clearAll: () => set({ overrides: new Map() }),
}))

export default useLiveNodeOverrides
Loading
Loading