roof
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, SlabNode, WallNode, ZoneNode } from '../schema'
|
||||
import type { BuildingNode, CeilingNode, ItemNode, RoofNode, SlabNode, WallNode, ZoneNode } from '../schema'
|
||||
import type { AnyNode } from '../schema/types'
|
||||
|
||||
// Base event interfaces
|
||||
@@ -24,6 +24,7 @@ export type BuildingEvent = NodeEvent<BuildingNode>
|
||||
export type ZoneEvent = NodeEvent<ZoneNode>
|
||||
export type SlabEvent = NodeEvent<SlabNode>
|
||||
export type CeilingEvent = NodeEvent<CeilingNode>
|
||||
export type RoofEvent = NodeEvent<RoofNode>
|
||||
|
||||
// Event suffixes - exported for use in hooks
|
||||
export const eventSuffixes = [
|
||||
@@ -61,7 +62,9 @@ type EditorEvents = GridEvents &
|
||||
NodeEvents<'item', ItemEvent> &
|
||||
NodeEvents<'building', BuildingEvent> &
|
||||
NodeEvents<'zone', ZoneEvent> &
|
||||
NodeEvents<'slab', SlabEvent> &
|
||||
NodeEvents<'ceiling', CeilingEvent> &
|
||||
NodeEvents<'roof', RoofEvent> &
|
||||
CameraControlEvents
|
||||
|
||||
export const emitter = mitt<EditorEvents>()
|
||||
|
||||
@@ -15,6 +15,7 @@ export const sceneRegistry = {
|
||||
item: new Set<string>(),
|
||||
slab: new Set<string>(),
|
||||
zone: new Set<string>(),
|
||||
roof: new Set<string>(),
|
||||
scan: new Set<string>(),
|
||||
guide: new Set<string>(),
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ export type {
|
||||
WallEvent,
|
||||
ZoneEvent,
|
||||
CeilingEvent,
|
||||
RoofEvent,
|
||||
} from './events/bus'
|
||||
// Events
|
||||
export { emitter, eventSuffixes } from './events/bus'
|
||||
@@ -30,6 +31,7 @@ export { default as useScene } from './store/use-scene'
|
||||
// Systems
|
||||
export { CeilingSystem } from './systems/ceiling/ceiling-system'
|
||||
export { ItemSystem } from './systems/item/item-system'
|
||||
export { RoofSystem } from './systems/roof/roof-system'
|
||||
export { SlabSystem } from './systems/slab/slab-system'
|
||||
export { WallSystem } from './systems/wall/wall-system'
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export { BuildingNode } from './nodes/building'
|
||||
export { CeilingNode } from './nodes/ceiling'
|
||||
|
||||
export { ZoneNode } from './nodes/zone'
|
||||
export { RoofNode } from './nodes/roof'
|
||||
export { ScanNode } from './nodes/scan'
|
||||
export { GuideNode } from './nodes/guide'
|
||||
export type { AnyNodeId, AnyNodeType } from './types'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from 'zod'
|
||||
import { BaseNode, nodeType, objectId } from '../base'
|
||||
import { CeilingNode } from './ceiling'
|
||||
import { GuideNode } from './guide'
|
||||
import { RoofNode } from './roof'
|
||||
import { ScanNode } from './scan'
|
||||
import { SlabNode } from './slab'
|
||||
import { WallNode } from './wall'
|
||||
@@ -11,7 +12,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, CeilingNode.shape.id, ScanNode.shape.id, GuideNode.shape.id])).default([]),
|
||||
children: z.array(z.union([WallNode.shape.id, ZoneNode.shape.id, SlabNode.shape.id, CeilingNode.shape.id, RoofNode.shape.id, ScanNode.shape.id, GuideNode.shape.id])).default([]),
|
||||
// Specific props
|
||||
level: z.number().default(0),
|
||||
}).describe(
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import dedent from 'dedent'
|
||||
import { z } from 'zod'
|
||||
import { BaseNode, nodeType, objectId } from '../base'
|
||||
|
||||
export const RoofNode = BaseNode.extend({
|
||||
id: objectId('roof'),
|
||||
type: nodeType('roof'),
|
||||
// Position of the roof center (Y should typically be 0)
|
||||
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||
// Rotation around Y axis in radians
|
||||
rotation: z.number().default(0),
|
||||
// Length of the roof along the ridge direction (in meters)
|
||||
length: z.number().default(4),
|
||||
// Height of the roof peak from the base
|
||||
height: z.number().default(1.5),
|
||||
// Width of the left slope (in meters, measured horizontally from ridge)
|
||||
leftWidth: z.number().default(1.5),
|
||||
// Width of the right slope (in meters, measured horizontally from ridge)
|
||||
rightWidth: z.number().default(1.5),
|
||||
}).describe(
|
||||
dedent`
|
||||
Roof node - used to represent a gable roof in the building
|
||||
- position: center position of the roof (Y typically 0)
|
||||
- rotation: rotation around Y axis
|
||||
- length: length of the roof along the ridge
|
||||
- height: height of the roof peak
|
||||
- leftWidth: horizontal width of the left slope (from ridge to eave)
|
||||
- rightWidth: horizontal width of the right slope (from ridge to eave)
|
||||
Total width = leftWidth + rightWidth
|
||||
`,
|
||||
)
|
||||
|
||||
export type RoofNode = z.infer<typeof RoofNode>
|
||||
@@ -4,6 +4,7 @@ import { CeilingNode } from './nodes/ceiling'
|
||||
import { GuideNode } from './nodes/guide'
|
||||
import { ItemNode } from './nodes/item'
|
||||
import { LevelNode } from './nodes/level'
|
||||
import { RoofNode } from './nodes/roof'
|
||||
import { ScanNode } from './nodes/scan'
|
||||
import { SiteNode } from './nodes/site'
|
||||
import { SlabNode } from './nodes/slab'
|
||||
@@ -19,6 +20,7 @@ export const AnyNode = z.discriminatedUnion('type', [
|
||||
ZoneNode,
|
||||
SlabNode,
|
||||
CeilingNode,
|
||||
RoofNode,
|
||||
ScanNode,
|
||||
GuideNode,
|
||||
])
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
|
||||
import type { AnyNodeId, RoofNode } from '../../schema'
|
||||
import useScene from '../../store/use-scene'
|
||||
|
||||
// ============================================================================
|
||||
// ROOF SYSTEM
|
||||
// ============================================================================
|
||||
|
||||
export const RoofSystem = () => {
|
||||
const { nodes, dirtyNodes, clearDirty } = useScene()
|
||||
|
||||
useFrame(() => {
|
||||
if (dirtyNodes.size === 0) return
|
||||
|
||||
// Process dirty roofs
|
||||
dirtyNodes.forEach((id) => {
|
||||
const node = nodes[id]
|
||||
if (!node || node.type !== 'roof') return
|
||||
|
||||
const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh
|
||||
if (mesh) {
|
||||
updateRoofGeometry(node as RoofNode, mesh)
|
||||
}
|
||||
clearDirty(id as AnyNodeId)
|
||||
})
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the geometry and transform for a single roof
|
||||
*/
|
||||
function updateRoofGeometry(node: RoofNode, mesh: THREE.Mesh) {
|
||||
const newGeo = generateRoofGeometry(node)
|
||||
|
||||
mesh.geometry.dispose()
|
||||
mesh.geometry = newGeo
|
||||
|
||||
// Update position and rotation
|
||||
mesh.position.set(node.position[0], node.position[1], node.position[2])
|
||||
mesh.rotation.y = node.rotation
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates gable roof geometry from length, height, leftWidth, rightWidth
|
||||
*
|
||||
* The roof is centered at origin (position applied via mesh transform)
|
||||
* - Ridge runs along the X axis (length direction)
|
||||
* - Left slope goes down toward -Z with horizontal distance leftWidth
|
||||
* - Right slope goes down toward +Z with horizontal distance rightWidth
|
||||
* - Total width = leftWidth + rightWidth
|
||||
* - Gable ends at -X/2 and +X/2
|
||||
*/
|
||||
export function generateRoofGeometry(roofNode: RoofNode): THREE.BufferGeometry {
|
||||
const { length, height, leftWidth, rightWidth } = roofNode
|
||||
|
||||
// Half length for centering
|
||||
const halfLength = length / 2
|
||||
|
||||
// Ridge is at Y = height, centered at Z = 0
|
||||
// Left eave is at Z = -leftWidth, Y = 0
|
||||
// Right eave is at Z = +rightWidth, Y = 0
|
||||
|
||||
const positions: number[] = []
|
||||
const normals: number[] = []
|
||||
const indices: number[] = []
|
||||
|
||||
const addVertex = (x: number, y: number, z: number, nx: number, ny: number, nz: number) => {
|
||||
const idx = positions.length / 3
|
||||
positions.push(x, y, z)
|
||||
normals.push(nx, ny, nz)
|
||||
return idx
|
||||
}
|
||||
|
||||
// Calculate slope normals
|
||||
// Left slope: from (0, height, 0) to (0, 0, -leftWidth)
|
||||
const leftSlopeLen = Math.sqrt(height * height + leftWidth * leftWidth)
|
||||
const leftNormalY = leftWidth / leftSlopeLen
|
||||
const leftNormalZ = height / leftSlopeLen
|
||||
|
||||
// Right slope: from (0, height, 0) to (0, 0, +rightWidth)
|
||||
const rightSlopeLen = Math.sqrt(height * height + rightWidth * rightWidth)
|
||||
const rightNormalY = rightWidth / rightSlopeLen
|
||||
const rightNormalZ = height / rightSlopeLen
|
||||
|
||||
// Left slope (negative Z side) - CCW winding for outward-facing
|
||||
const leftNormal = [0, leftNormalY, -leftNormalZ] as const
|
||||
const v0 = addVertex(-halfLength, 0, -leftWidth, ...leftNormal) // back-left eave
|
||||
const v1 = addVertex(halfLength, 0, -leftWidth, ...leftNormal) // front-left eave
|
||||
const v2 = addVertex(halfLength, height, 0, ...leftNormal) // front ridge
|
||||
const v3 = addVertex(-halfLength, height, 0, ...leftNormal) // back ridge
|
||||
indices.push(v0, v2, v1, v0, v3, v2)
|
||||
|
||||
// Right slope (positive Z side) - CCW winding for outward-facing
|
||||
const rightNormal = [0, rightNormalY, rightNormalZ] as const
|
||||
const v4 = addVertex(halfLength, 0, rightWidth, ...rightNormal) // front-right eave
|
||||
const v5 = addVertex(-halfLength, 0, rightWidth, ...rightNormal) // back-right eave
|
||||
const v6 = addVertex(-halfLength, height, 0, ...rightNormal) // back ridge
|
||||
const v7 = addVertex(halfLength, height, 0, ...rightNormal) // front ridge
|
||||
indices.push(v4, v6, v5, v4, v7, v6)
|
||||
|
||||
// Front gable end (positive X) - CCW winding for outward-facing
|
||||
const frontNormal = [1, 0, 0] as const
|
||||
const v8 = addVertex(halfLength, 0, -leftWidth, ...frontNormal)
|
||||
const v9 = addVertex(halfLength, 0, rightWidth, ...frontNormal)
|
||||
const v10 = addVertex(halfLength, height, 0, ...frontNormal)
|
||||
indices.push(v8, v10, v9)
|
||||
|
||||
// Back gable end (negative X) - CCW winding for outward-facing
|
||||
const backNormal = [-1, 0, 0] as const
|
||||
const v11 = addVertex(-halfLength, 0, rightWidth, ...backNormal)
|
||||
const v12 = addVertex(-halfLength, 0, -leftWidth, ...backNormal)
|
||||
const v13 = addVertex(-halfLength, height, 0, ...backNormal)
|
||||
indices.push(v11, v13, v12)
|
||||
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
||||
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
|
||||
geometry.setIndex(indices)
|
||||
|
||||
return geometry
|
||||
}
|
||||
Reference in New Issue
Block a user