This commit is contained in:
wass08
2026-01-27 09:25:40 +09:00
parent fd4c097d5b
commit e3e3602d91
19 changed files with 612 additions and 19 deletions
+3 -1
View File
@@ -1,6 +1,6 @@
import type { ThreeEvent } from '@react-three/fiber'
import mitt from 'mitt'
import type { BuildingNode, ItemNode, SlabNode, WallNode, ZoneNode } from '../schema'
import type { BuildingNode, CeilingNode, ItemNode, SlabNode, WallNode, ZoneNode } from '../schema'
import type { AnyNode } from '../schema/types'
// Base event interfaces
@@ -23,6 +23,7 @@ export type ItemEvent = NodeEvent<ItemNode>
export type BuildingEvent = NodeEvent<BuildingNode>
export type ZoneEvent = NodeEvent<ZoneNode>
export type SlabEvent = NodeEvent<SlabNode>
export type CeilingEvent = NodeEvent<CeilingNode>
// Event suffixes - exported for use in hooks
export const eventSuffixes = [
@@ -60,6 +61,7 @@ type EditorEvents = GridEvents &
NodeEvents<'item', ItemEvent> &
NodeEvents<'building', BuildingEvent> &
NodeEvents<'zone', ZoneEvent> &
NodeEvents<'ceiling', CeilingEvent> &
CameraControlEvents
export const emitter = mitt<EditorEvents>()
@@ -9,6 +9,7 @@ export const sceneRegistry = {
// Using a Set is faster for adding/deleting than an Array
byType: {
building: new Set<string>(),
ceiling: new Set<string>(),
level: new Set<string>(),
wall: new Set<string>(),
item: new Set<string>(),
+2
View File
@@ -10,6 +10,7 @@ export type {
SlabEvent,
WallEvent,
ZoneEvent,
CeilingEvent,
} from './events/bus'
// Events
export { emitter, eventSuffixes } from './events/bus'
@@ -27,6 +28,7 @@ export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query'
export * from './schema'
export { default as useScene } from './store/use-scene'
// Systems
export { CeilingSystem } from './systems/ceiling/ceiling-system'
export { SlabSystem } from './systems/slab/slab-system'
export { WallSystem } from './systems/wall/wall-system'
+4 -3
View File
@@ -2,15 +2,16 @@
export { BaseNode, generateId, Material, nodeType, objectId } from './base'
// Camera
export { CameraSchema } from './camera'
export { BuildingNode } from './nodes/building'
export type { AssetInput } from './nodes/item'
export { ItemNode } from './nodes/item'
export { LevelNode } from './nodes/level'
// Nodes
export { SiteNode } from './nodes/site'
export { SlabNode, SlabPolygon } from './nodes/slab'
export { SlabNode } from './nodes/slab'
export { WallNode } from './nodes/wall'
export type { ZonePolygon } from './nodes/zone'
export { BuildingNode } from './nodes/building'
export { CeilingNode } from './nodes/ceiling'
export { ZoneNode } from './nodes/zone'
export type { AnyNodeId, AnyNodeType } from './types'
// Union types
+19
View File
@@ -0,0 +1,19 @@
import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
export const CeilingNode = BaseNode.extend({
id: objectId('ceiling'),
type: nodeType('ceiling'),
// Specific props
// Polygon boundary - array of [x, z] coordinates defining the ceiling
polygon: z.array(z.tuple([z.number(), z.number()])),
height: z.number().default(2.5), // Height in meters
}).describe(
dedent`
Ceiling node - used to represent a ceiling in the building
- polygon: array of [x, z] points defining the ceiling boundary
`,
)
export type CeilingNode = z.infer<typeof CeilingNode>
+2 -1
View File
@@ -1,6 +1,7 @@
import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
import { CeilingNode } from './ceiling'
import { SlabNode } from './slab'
import { WallNode } from './wall'
import { ZoneNode } from './zone'
@@ -8,7 +9,7 @@ import { ZoneNode } from './zone'
export const LevelNode = BaseNode.extend({
id: objectId('level'),
type: nodeType('level'),
children: z.array(z.union([WallNode.shape.id, ZoneNode.shape.id, SlabNode.shape.id])).default([]),
children: z.array(z.union([WallNode.shape.id, ZoneNode.shape.id, SlabNode.shape.id, CeilingNode.shape.id])).default([]),
// Specific props
level: z.number().default(0),
}).describe(
+1 -5
View File
@@ -2,15 +2,12 @@ import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
// Polygon boundary for zone area - array of [x, z] coordinates
export const SlabPolygon = z.array(z.tuple([z.number(), z.number()]))
export const SlabNode = BaseNode.extend({
id: objectId('slab'),
type: nodeType('slab'),
// Specific props
// Polygon boundary - array of [x, z] coordinates defining the slab
polygon: SlabPolygon,
polygon: z.array(z.tuple([z.number(), z.number()])),
elevation: z.number().default(0.05), // Elevation in meters
}).describe(
dedent`
@@ -21,4 +18,3 @@ export const SlabNode = BaseNode.extend({
)
export type SlabNode = z.infer<typeof SlabNode>
export type SlabPolygon = z.infer<typeof SlabPolygon>
+1 -5
View File
@@ -2,15 +2,12 @@ import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
// Polygon boundary for zone area - array of [x, z] coordinates
export const ZonePolygon = z.array(z.tuple([z.number(), z.number()]))
export const ZoneNode = BaseNode.extend({
id: objectId('zone'),
type: nodeType('zone'),
name: z.string(),
// Polygon boundary - array of [x, z] coordinates defining the zone
polygon: ZonePolygon,
polygon: z.array(z.tuple([z.number(), z.number()])),
// Visual styling
color: z.string().default('#3b82f6'), // Default blue
metadata: z.json().optional().default({}),
@@ -28,4 +25,3 @@ export const ZoneNode = BaseNode.extend({
)
export type ZoneNode = z.infer<typeof ZoneNode>
export type ZonePolygon = z.infer<typeof ZonePolygon>
+2
View File
@@ -1,5 +1,6 @@
import z from 'zod'
import { BuildingNode } from './nodes/building'
import { CeilingNode } from './nodes/ceiling'
import { ItemNode } from './nodes/item'
import { LevelNode } from './nodes/level'
import { SiteNode } from './nodes/site'
@@ -15,6 +16,7 @@ export const AnyNode = z.discriminatedUnion('type', [
ItemNode,
ZoneNode,
SlabNode,
CeilingNode,
])
export type AnyNode = z.infer<typeof AnyNode>
@@ -0,0 +1,78 @@
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
import type { AnyNodeId, CeilingNode } from '../../schema'
import useScene from '../../store/use-scene'
// ============================================================================
// CEILING SYSTEM
// ============================================================================
export const CeilingSystem = () => {
const { nodes, dirtyNodes, clearDirty } = useScene()
useFrame(() => {
if (dirtyNodes.size === 0) return
// Process dirty ceilings
dirtyNodes.forEach((id) => {
const node = nodes[id]
if (!node || node.type !== 'ceiling') return
const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh
if (mesh) {
updateCeilingGeometry(node as CeilingNode, mesh)
}
clearDirty(id as AnyNodeId)
})
})
return null
}
/**
* Updates the geometry for a single ceiling
*/
function updateCeilingGeometry(node: CeilingNode, mesh: THREE.Mesh) {
const newGeo = generateCeilingGeometry(node)
mesh.geometry.dispose()
mesh.geometry = newGeo
// Position at the ceiling height
mesh.position.y = node.height ?? 2.5
}
/**
* Generates flat ceiling geometry from polygon (no extrusion)
*/
export function generateCeilingGeometry(ceilingNode: CeilingNode): THREE.BufferGeometry {
const polygon = ceilingNode.polygon
if (polygon.length < 3) {
return new THREE.BufferGeometry()
}
// Create shape from polygon
// Shape is in X-Y plane, we'll rotate to X-Z plane
const shape = new THREE.Shape()
const firstPt = polygon[0]!
// Negate Y (which becomes Z) to get correct orientation after rotation
shape.moveTo(firstPt[0], -firstPt[1])
for (let i = 1; i < polygon.length; i++) {
const pt = polygon[i]!
shape.lineTo(pt[0], -pt[1])
}
shape.closePath()
// Create flat shape geometry (no extrusion)
const geometry = new THREE.ShapeGeometry(shape)
// Rotate so the shape lies flat in X-Z plane
geometry.rotateX(-Math.PI / 2)
geometry.computeVertexNormals()
return geometry
}