draft custom door
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { ThreeEvent } from '@react-three/fiber'
|
||||
import mitt from 'mitt'
|
||||
import type { BuildingNode, CeilingNode, ItemNode, LevelNode, RoofNode, SiteNode, SlabNode, WallNode, WindowNode, ZoneNode } from '../schema'
|
||||
import type { BuildingNode, CeilingNode, DoorNode, ItemNode, LevelNode, RoofNode, SiteNode, SlabNode, WallNode, WindowNode, ZoneNode } from '../schema'
|
||||
import type { AnyNode } from '../schema/types'
|
||||
|
||||
// Base event interfaces
|
||||
@@ -28,6 +28,7 @@ export type SlabEvent = NodeEvent<SlabNode>
|
||||
export type CeilingEvent = NodeEvent<CeilingNode>
|
||||
export type RoofEvent = NodeEvent<RoofNode>
|
||||
export type WindowEvent = NodeEvent<WindowNode>
|
||||
export type DoorEvent = NodeEvent<DoorNode>
|
||||
|
||||
// Event suffixes - exported for use in hooks
|
||||
export const eventSuffixes = [
|
||||
@@ -83,6 +84,7 @@ type EditorEvents = GridEvents &
|
||||
NodeEvents<'ceiling', CeilingEvent> &
|
||||
NodeEvents<'roof', RoofEvent> &
|
||||
NodeEvents<'window', WindowEvent> &
|
||||
NodeEvents<'door', DoorEvent> &
|
||||
CameraControlEvents &
|
||||
ToolEvents
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export const sceneRegistry = {
|
||||
scan: new Set<string>(),
|
||||
guide: new Set<string>(),
|
||||
window: new Set<string>(),
|
||||
door: new Set<string>(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ export type {
|
||||
BuildingEvent,
|
||||
CameraControlEvent,
|
||||
CeilingEvent,
|
||||
DoorEvent,
|
||||
EventSuffix,
|
||||
GridEvent,
|
||||
ItemEvent,
|
||||
@@ -43,6 +44,7 @@ export * from './schema'
|
||||
export { default as useScene } from './store/use-scene'
|
||||
// Systems
|
||||
export { CeilingSystem } from './systems/ceiling/ceiling-system'
|
||||
export { DoorSystem } from './systems/door/door-system'
|
||||
export { ItemSystem } from './systems/item/item-system'
|
||||
export { RoofSystem } from './systems/roof/roof-system'
|
||||
export { SlabSystem } from './systems/slab/slab-system'
|
||||
|
||||
@@ -17,6 +17,7 @@ export { RoofNode } from './nodes/roof'
|
||||
export { ScanNode } from './nodes/scan'
|
||||
export { GuideNode } from './nodes/guide'
|
||||
export type { AnyNodeId, AnyNodeType } from './types'
|
||||
export { DoorNode, DoorSegment } from './nodes/door'
|
||||
export { WindowNode } from './nodes/window'
|
||||
// Union types
|
||||
export { AnyNode } from './types'
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import dedent from 'dedent'
|
||||
import { z } from 'zod'
|
||||
import { BaseNode, nodeType, objectId } from '../base'
|
||||
|
||||
export const DoorSegment = z.object({
|
||||
type: z.enum(['panel', 'glass', 'empty']),
|
||||
heightRatio: z.number(),
|
||||
|
||||
// Each segment controls its own column split
|
||||
columnRatios: z.array(z.number()).default([1]),
|
||||
dividerThickness: z.number().default(0.03),
|
||||
|
||||
// panel-specific
|
||||
panelDepth: z.number().default(0.01), // + raised, - recessed
|
||||
panelInset: z.number().default(0.04),
|
||||
})
|
||||
|
||||
export type DoorSegment = z.infer<typeof DoorSegment>
|
||||
|
||||
export const DoorNode = BaseNode.extend({
|
||||
id: objectId('door'),
|
||||
type: nodeType('door'),
|
||||
|
||||
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]),
|
||||
side: z.enum(['front', 'back']).optional(),
|
||||
wallId: z.string().optional(),
|
||||
|
||||
// Overall dimensions
|
||||
width: z.number().default(0.9),
|
||||
height: z.number().default(2.1),
|
||||
|
||||
// Frame
|
||||
frameThickness: z.number().default(0.05),
|
||||
frameDepth: z.number().default(0.07),
|
||||
threshold: z.boolean().default(true),
|
||||
thresholdHeight: z.number().default(0.02),
|
||||
|
||||
// Swing
|
||||
hingesSide: z.enum(['left', 'right']).default('left'),
|
||||
swingDirection: z.enum(['inward', 'outward']).default('inward'),
|
||||
|
||||
// Leaf segments — stacked top to bottom, each with its own column split
|
||||
segments: z.array(DoorSegment).default([
|
||||
{ type: 'panel', heightRatio: 0.4, columnRatios: [1], dividerThickness: 0.03, panelDepth: 0.01, panelInset: 0.04 },
|
||||
{ type: 'panel', heightRatio: 0.6, columnRatios: [1], dividerThickness: 0.03, panelDepth: 0.01, panelInset: 0.04 },
|
||||
]),
|
||||
|
||||
// Handle
|
||||
handle: z.boolean().default(true),
|
||||
handleHeight: z.number().default(1.05),
|
||||
handleSide: z.enum(['left', 'right']).default('right'),
|
||||
|
||||
// Emergency / commercial hardware
|
||||
doorCloser: z.boolean().default(false),
|
||||
panicBar: z.boolean().default(false),
|
||||
panicBarHeight: z.number().default(1.0),
|
||||
|
||||
}).describe(dedent`Door node - a parametric door placed on a wall
|
||||
- position: center of the door in wall-local coordinate system (Y = height/2, always at floor)
|
||||
- segments: rows stacked top to bottom, each defining its own columnRatios
|
||||
- type 'empty' = flush flat fill, 'panel' = raised/recessed panel, 'glass' = glazed
|
||||
- hingesSide/swingDirection: which way the door opens
|
||||
- doorCloser/panicBar: commercial and emergency hardware options
|
||||
`)
|
||||
|
||||
export type DoorNode = z.infer<typeof DoorNode>
|
||||
@@ -8,6 +8,7 @@ import { RoofNode } from './nodes/roof'
|
||||
import { ScanNode } from './nodes/scan'
|
||||
import { SiteNode } from './nodes/site'
|
||||
import { SlabNode } from './nodes/slab'
|
||||
import { DoorNode } from './nodes/door'
|
||||
import { WallNode } from './nodes/wall'
|
||||
import { WindowNode } from './nodes/window'
|
||||
import { ZoneNode } from './nodes/zone'
|
||||
@@ -25,6 +26,7 @@ export const AnyNode = z.discriminatedUnion('type', [
|
||||
ScanNode,
|
||||
GuideNode,
|
||||
WindowNode,
|
||||
DoorNode,
|
||||
])
|
||||
|
||||
export type AnyNode = z.infer<typeof AnyNode>
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu'
|
||||
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
|
||||
import type { AnyNodeId, DoorNode } from '../../schema'
|
||||
import useScene from '../../store/use-scene'
|
||||
|
||||
const frameMaterial = new MeshStandardNodeMaterial({
|
||||
name: 'door-frame',
|
||||
color: '#e8e8e8',
|
||||
roughness: 0.6,
|
||||
metalness: 0,
|
||||
})
|
||||
|
||||
const leafMaterial = new MeshStandardNodeMaterial({
|
||||
name: 'door-leaf',
|
||||
color: '#d0c8b8',
|
||||
roughness: 0.5,
|
||||
metalness: 0,
|
||||
})
|
||||
|
||||
const panelMaterial = new MeshStandardNodeMaterial({
|
||||
name: 'door-panel',
|
||||
color: '#c5bdb0',
|
||||
roughness: 0.5,
|
||||
metalness: 0,
|
||||
})
|
||||
|
||||
const glassMaterial = new MeshStandardNodeMaterial({
|
||||
name: 'door-glass',
|
||||
color: 'lightblue',
|
||||
roughness: 0.05,
|
||||
metalness: 0.1,
|
||||
transparent: true,
|
||||
opacity: 0.35,
|
||||
side: DoubleSide,
|
||||
depthWrite: false,
|
||||
})
|
||||
|
||||
const thresholdMaterial = new MeshStandardNodeMaterial({
|
||||
name: 'door-threshold',
|
||||
color: '#999',
|
||||
roughness: 0.4,
|
||||
metalness: 0.5,
|
||||
})
|
||||
|
||||
const handleMaterial = new MeshStandardNodeMaterial({
|
||||
name: 'door-handle',
|
||||
color: '#bbb',
|
||||
roughness: 0.2,
|
||||
metalness: 0.8,
|
||||
})
|
||||
|
||||
const closerMaterial = new MeshStandardNodeMaterial({
|
||||
name: 'door-closer',
|
||||
color: '#333',
|
||||
roughness: 0.4,
|
||||
metalness: 0.3,
|
||||
})
|
||||
|
||||
// Invisible material for root mesh — used as selection hitbox only
|
||||
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false })
|
||||
|
||||
export const DoorSystem = () => {
|
||||
const dirtyNodes = useScene((state) => state.dirtyNodes)
|
||||
const clearDirty = useScene((state) => state.clearDirty)
|
||||
|
||||
useFrame(() => {
|
||||
if (dirtyNodes.size === 0) return
|
||||
|
||||
const nodes = useScene.getState().nodes
|
||||
|
||||
dirtyNodes.forEach((id) => {
|
||||
const node = nodes[id]
|
||||
if (!node || node.type !== 'door') return
|
||||
|
||||
const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh
|
||||
if (!mesh) return // Keep dirty until mesh mounts
|
||||
|
||||
updateDoorMesh(node as DoorNode, mesh)
|
||||
clearDirty(id as AnyNodeId)
|
||||
|
||||
// Rebuild the parent wall so its cutout reflects the updated door geometry
|
||||
if ((node as DoorNode).parentId) {
|
||||
useScene.getState().dirtyNodes.add((node as DoorNode).parentId as AnyNodeId)
|
||||
}
|
||||
})
|
||||
}, 3)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function addBox(
|
||||
parent: THREE.Object3D,
|
||||
material: THREE.Material,
|
||||
w: number, h: number, d: number,
|
||||
x: number, y: number, z: number,
|
||||
) {
|
||||
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material)
|
||||
m.position.set(x, y, z)
|
||||
parent.add(m)
|
||||
}
|
||||
|
||||
function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
||||
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
||||
mesh.geometry.dispose()
|
||||
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth)
|
||||
mesh.material = hitboxMaterial
|
||||
|
||||
// Sync transform from node (React may lag behind the system by a frame during drag)
|
||||
mesh.position.set(node.position[0], node.position[1], node.position[2])
|
||||
mesh.rotation.set(node.rotation[0], node.rotation[1], node.rotation[2])
|
||||
|
||||
// Dispose and remove all old visual children; preserve 'cutout'
|
||||
for (const child of [...mesh.children]) {
|
||||
if (child.name === 'cutout') continue
|
||||
if (child instanceof THREE.Mesh) child.geometry.dispose()
|
||||
mesh.remove(child)
|
||||
}
|
||||
|
||||
const {
|
||||
width, height, frameThickness, frameDepth, threshold, thresholdHeight,
|
||||
segments, handle, handleHeight, handleSide,
|
||||
doorCloser, panicBar, panicBarHeight,
|
||||
} = node
|
||||
|
||||
// Leaf occupies the full opening (no bottom frame bar — door opens to floor)
|
||||
const leafW = width - 2 * frameThickness
|
||||
const leafH = height - frameThickness // only top frame
|
||||
const leafDepth = 0.04
|
||||
// Leaf center is shifted down from door center by half the top frame
|
||||
const leafCenterY = -frameThickness / 2
|
||||
|
||||
// ── Frame members ──
|
||||
// Left post — full height
|
||||
addBox(mesh, frameMaterial, frameThickness, height, frameDepth, -width / 2 + frameThickness / 2, 0, 0)
|
||||
// Right post — full height
|
||||
addBox(mesh, frameMaterial, frameThickness, height, frameDepth, width / 2 - frameThickness / 2, 0, 0)
|
||||
// Head (top bar) — full width
|
||||
addBox(mesh, frameMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0)
|
||||
|
||||
// ── Threshold ──
|
||||
if (threshold) {
|
||||
addBox(mesh, thresholdMaterial, width, thresholdHeight, frameDepth, 0, -height / 2 + thresholdHeight / 2, 0)
|
||||
}
|
||||
|
||||
// ── Door leaf — full backing ──
|
||||
addBox(mesh, leafMaterial, leafW, leafH, leafDepth, 0, leafCenterY, 0)
|
||||
|
||||
// ── Segments (stacked top to bottom within leaf area) ──
|
||||
const totalRatio = segments.reduce((sum, s) => sum + s.heightRatio, 0)
|
||||
const leafTop = leafCenterY + leafH / 2
|
||||
|
||||
let segY = leafTop
|
||||
for (const seg of segments) {
|
||||
const segH = (seg.heightRatio / totalRatio) * leafH
|
||||
const segCenterY = segY - segH / 2
|
||||
|
||||
const numCols = seg.columnRatios.length
|
||||
const colSum = seg.columnRatios.reduce((a, b) => a + b, 0)
|
||||
const usableW = leafW - (numCols - 1) * seg.dividerThickness
|
||||
const colWidths = seg.columnRatios.map(r => (r / colSum) * usableW)
|
||||
|
||||
// Column x-centers
|
||||
const colXCenters: number[] = []
|
||||
let cx = -leafW / 2
|
||||
for (let c = 0; c < numCols; c++) {
|
||||
colXCenters.push(cx + colWidths[c]! / 2)
|
||||
cx += colWidths[c]!
|
||||
if (c < numCols - 1) cx += seg.dividerThickness
|
||||
}
|
||||
|
||||
// Column dividers within this segment
|
||||
cx = -leafW / 2
|
||||
for (let c = 0; c < numCols - 1; c++) {
|
||||
cx += colWidths[c]!
|
||||
addBox(mesh, leafMaterial, seg.dividerThickness, segH, leafDepth + 0.001, cx + seg.dividerThickness / 2, segCenterY, 0)
|
||||
cx += seg.dividerThickness
|
||||
}
|
||||
|
||||
// Segment content per column
|
||||
for (let c = 0; c < numCols; c++) {
|
||||
const colW = colWidths[c]!
|
||||
const colX = colXCenters[c]!
|
||||
|
||||
if (seg.type === 'glass') {
|
||||
const glassDepth = Math.max(0.004, leafDepth * 0.15)
|
||||
addBox(mesh, glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0)
|
||||
} else if (seg.type === 'panel') {
|
||||
const panelW = colW - 2 * seg.panelInset
|
||||
const panelH = segH - 2 * seg.panelInset
|
||||
if (panelW > 0.01 && panelH > 0.01) {
|
||||
const effectiveDepth = Math.abs(seg.panelDepth) < 0.002 ? 0.005 : Math.abs(seg.panelDepth)
|
||||
const panelZ = leafDepth / 2 + effectiveDepth / 2
|
||||
addBox(mesh, panelMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ)
|
||||
}
|
||||
}
|
||||
// 'empty' → leaf backing is already there, nothing extra
|
||||
}
|
||||
|
||||
segY -= segH
|
||||
}
|
||||
|
||||
// ── Handle ──
|
||||
if (handle) {
|
||||
// Convert from floor-based height to mesh-center-based Y
|
||||
const handleY = handleHeight - height / 2
|
||||
// Handle grip sits on the front face (+Z) of the leaf
|
||||
const faceZ = leafDepth / 2
|
||||
|
||||
// X position: handleSide refers to which side the grip is on
|
||||
const handleX = handleSide === 'right'
|
||||
? leafW / 2 - 0.045
|
||||
: -leafW / 2 + 0.045
|
||||
|
||||
// Backplate
|
||||
addBox(mesh, handleMaterial, 0.028, 0.14, 0.01, handleX, handleY, faceZ + 0.005)
|
||||
// Grip lever
|
||||
addBox(mesh, handleMaterial, 0.022, 0.1, 0.035, handleX, handleY, faceZ + 0.025)
|
||||
}
|
||||
|
||||
// ── Door closer (commercial hardware at top) ──
|
||||
if (doorCloser) {
|
||||
const closerY = leafCenterY + leafH / 2 - 0.04
|
||||
// Body
|
||||
addBox(mesh, closerMaterial, 0.28, 0.055, 0.055, 0, closerY, leafDepth / 2 + 0.03)
|
||||
// Arm (simplified as thin bar to frame side)
|
||||
addBox(mesh, closerMaterial, 0.14, 0.015, 0.015, leafW / 4, closerY + 0.025, leafDepth / 2 + 0.015)
|
||||
}
|
||||
|
||||
// ── Panic bar ──
|
||||
if (panicBar) {
|
||||
const barY = panicBarHeight - height / 2
|
||||
addBox(mesh, handleMaterial, leafW * 0.72, 0.04, 0.055, 0, barY, leafDepth / 2 + 0.03)
|
||||
}
|
||||
|
||||
// ── Cutout (for wall CSG) — always full door dimensions, 1m deep ──
|
||||
let cutout = mesh.getObjectByName('cutout') as THREE.Mesh | undefined
|
||||
if (!cutout) {
|
||||
cutout = new THREE.Mesh()
|
||||
cutout.name = 'cutout'
|
||||
mesh.add(cutout)
|
||||
}
|
||||
cutout.geometry.dispose()
|
||||
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0)
|
||||
cutout.visible = false
|
||||
}
|
||||
@@ -306,7 +306,7 @@ function collectCutoutBrushes(
|
||||
const wallMatrixInverse = wallMesh.matrixWorld.clone().invert()
|
||||
|
||||
for (const child of childrenNodes) {
|
||||
if (child.type !== 'item' && child.type !== 'window') continue
|
||||
if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door') continue
|
||||
|
||||
const childMesh = sceneRegistry.nodes.get(child.id)
|
||||
if (!childMesh) continue
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useRegistry, type DoorNode } from '@pascal-app/core'
|
||||
import { useRef } from 'react'
|
||||
import type { Mesh } from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
|
||||
export const DoorRenderer = ({ node }: { node: DoorNode }) => {
|
||||
const ref = useRef<Mesh>(null!)
|
||||
|
||||
useRegistry(node.id, 'door', ref)
|
||||
const handlers = useNodeEvents(node, 'door')
|
||||
|
||||
return (
|
||||
<mesh
|
||||
ref={ref}
|
||||
castShadow
|
||||
receiveShadow
|
||||
visible={node.visible}
|
||||
position={node.position}
|
||||
rotation={node.rotation}
|
||||
{...handlers}
|
||||
>
|
||||
{/* DoorSystem replaces this geometry each time the node is dirty */}
|
||||
<boxGeometry args={[0, 0, 0]} />
|
||||
<meshStandardMaterial color="#d1d5db" />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { type AnyNode, useScene } from '@pascal-app/core'
|
||||
import { BuildingRenderer } from './building/building-renderer'
|
||||
import { CeilingRenderer } from './ceiling/ceiling-renderer'
|
||||
import { DoorRenderer } from './door/door-renderer'
|
||||
import { GuideRenderer } from './guide/guide-renderer'
|
||||
import { ItemRenderer } from './item/item-renderer'
|
||||
import { LevelRenderer } from './level/level-renderer'
|
||||
@@ -28,6 +29,7 @@ export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => {
|
||||
{node.type === 'item' && <ItemRenderer node={node} />}
|
||||
{node.type === 'slab' && <SlabRenderer node={node} />}
|
||||
{node.type === 'wall' && <WallRenderer node={node} />}
|
||||
{node.type === 'door' && <DoorRenderer node={node} />}
|
||||
{node.type === 'window' && <WindowRenderer node={node} />}
|
||||
{node.type === 'zone' && <ZoneRenderer node={node} />}
|
||||
{node.type === 'roof' && <RoofRenderer node={node} />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { CeilingSystem, ItemSystem, RoofSystem, SlabSystem, WallSystem, WindowSystem } from '@pascal-app/core'
|
||||
import { CeilingSystem, DoorSystem, ItemSystem, RoofSystem, SlabSystem, WallSystem, WindowSystem } from '@pascal-app/core'
|
||||
import { Bvh } from '@react-three/drei'
|
||||
import { Canvas, extend, type ThreeToJSXElements } from '@react-three/fiber'
|
||||
import * as THREE from 'three/webgpu'
|
||||
@@ -61,6 +61,7 @@ const Viewer: React.FC<ViewerProps> = ({ children, selectionManager = 'default'
|
||||
<WallCutout />
|
||||
{/* Core systems */}
|
||||
<CeilingSystem />
|
||||
<DoorSystem />
|
||||
<ItemSystem />
|
||||
<RoofSystem />
|
||||
<SlabSystem />
|
||||
|
||||
@@ -29,6 +29,7 @@ type SelectableNodeType =
|
||||
| 'zone'
|
||||
| 'wall'
|
||||
| 'window'
|
||||
| 'door'
|
||||
| 'item'
|
||||
| 'slab'
|
||||
| 'ceiling'
|
||||
@@ -86,8 +87,8 @@ const isNodeOnLevel = (node: AnyNode, levelId: string): boolean => {
|
||||
// Direct child of level
|
||||
if (node.parentId === levelId) return true
|
||||
|
||||
// Wall-attached items (windows/doors): check if parent wall is on the level
|
||||
if (node.type === 'item' && node.parentId) {
|
||||
// Wall-attached nodes (window/door/item): check if parent wall is on the level
|
||||
if ((node.type === 'item' || node.type === 'window' || node.type === 'door') && node.parentId) {
|
||||
const parentNode = nodes[node.parentId as keyof typeof nodes]
|
||||
if (parentNode?.type === 'wall' && parentNode.parentId === levelId) {
|
||||
return true
|
||||
@@ -200,9 +201,9 @@ const getStrategy = (): SelectionStrategy | null => {
|
||||
}
|
||||
}
|
||||
|
||||
// Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs, windows)
|
||||
// Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs, windows, doors)
|
||||
return {
|
||||
types: ['wall', 'item', 'slab', 'ceiling', 'roof', 'window'],
|
||||
types: ['wall', 'item', 'slab', 'ceiling', 'roof', 'window', 'door'],
|
||||
handleClick: (node) => {
|
||||
const { selectedIds } = useViewer.getState().selection
|
||||
// Toggle selection - if already selected, deselect; otherwise select
|
||||
@@ -224,7 +225,7 @@ const getStrategy = (): SelectionStrategy | null => {
|
||||
}
|
||||
},
|
||||
isValid: (node) => {
|
||||
const validTypes = ['wall', 'item', 'slab', 'ceiling', 'roof', 'window']
|
||||
const validTypes = ['wall', 'item', 'slab', 'ceiling', 'roof', 'window', 'door']
|
||||
if (!validTypes.includes(node.type)) return false
|
||||
return isNodeInZone(node, levelId, zoneId)
|
||||
},
|
||||
@@ -277,6 +278,7 @@ export const SelectionManager = () => {
|
||||
'ceiling',
|
||||
'roof',
|
||||
'window',
|
||||
'door',
|
||||
]
|
||||
for (const type of allTypes) {
|
||||
emitter.on(`${type}:enter`, onEnter)
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
type BuildingNode,
|
||||
type CeilingEvent,
|
||||
type CeilingNode,
|
||||
type DoorEvent,
|
||||
type DoorNode,
|
||||
type EventSuffix,
|
||||
emitter,
|
||||
type ItemEvent,
|
||||
@@ -36,6 +38,7 @@ type NodeConfig = {
|
||||
ceiling: { node: CeilingNode; event: CeilingEvent }
|
||||
roof: { node: RoofNode; event: RoofEvent }
|
||||
window: { node: WindowNode; event: WindowEvent }
|
||||
door: { node: DoorNode; event: DoorEvent }
|
||||
}
|
||||
|
||||
type NodeType = keyof NodeConfig
|
||||
|
||||
Reference in New Issue
Block a user