wall tool draft

This commit is contained in:
wass08
2026-02-17 16:22:45 +09:00
parent 13fd1848d1
commit dd5f63df64
19 changed files with 553 additions and 11 deletions
+3 -1
View File
@@ -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, ZoneNode } from '../schema'
import type { BuildingNode, CeilingNode, ItemNode, LevelNode, RoofNode, SiteNode, SlabNode, WallNode, WindowNode, ZoneNode } from '../schema'
import type { AnyNode } from '../schema/types'
// Base event interfaces
@@ -27,6 +27,7 @@ export type ZoneEvent = NodeEvent<ZoneNode>
export type SlabEvent = NodeEvent<SlabNode>
export type CeilingEvent = NodeEvent<CeilingNode>
export type RoofEvent = NodeEvent<RoofNode>
export type WindowEvent = NodeEvent<WindowNode>
// Event suffixes - exported for use in hooks
export const eventSuffixes = [
@@ -81,6 +82,7 @@ type EditorEvents = GridEvents &
NodeEvents<'slab', SlabEvent> &
NodeEvents<'ceiling', CeilingEvent> &
NodeEvents<'roof', RoofEvent> &
NodeEvents<'window', WindowEvent> &
CameraControlEvents &
ToolEvents
@@ -19,6 +19,7 @@ export const sceneRegistry = {
roof: new Set<string>(),
scan: new Set<string>(),
guide: new Set<string>(),
window: new Set<string>(),
},
};
+3 -2
View File
@@ -3,17 +3,17 @@
export type {
BuildingEvent,
CameraControlEvent,
CeilingEvent,
EventSuffix,
GridEvent,
ItemEvent,
LevelEvent,
NodeEvent,
RoofEvent,
SiteEvent,
SlabEvent,
WallEvent,
ZoneEvent,
CeilingEvent,
RoofEvent,
} from './events/bus'
// Events
export { emitter, eventSuffixes } from './events/bus'
@@ -37,6 +37,7 @@ 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'
export { WindowSystem } from './systems/window/window-system'
export { isObject } from './utils/types'
// Asset storage
+1
View File
@@ -17,5 +17,6 @@ export { RoofNode } from './nodes/roof'
export { ScanNode } from './nodes/scan'
export { GuideNode } from './nodes/guide'
export type { AnyNodeId, AnyNodeType } from './types'
export { WindowNode } from './nodes/window'
// Union types
export { AnyNode } from './types'
+46
View File
@@ -0,0 +1,46 @@
import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
export const WindowNode = BaseNode.extend({
id: objectId('window'),
type: nodeType('window'),
// Position in wall-local coordinate system (center of window)
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(),
// Wall reference
wallId: z.string().optional(),
// Overall dimensions
width: z.number().default(1.5),
height: z.number().default(1.5),
// Frame
frameThickness: z.number().default(0.05),
frameDepth: z.number().default(0.07),
// Divisions — ratios allow non-uniform panes
// [0.5, 0.5] = two equal panes
// [0.6, 0.4] = one larger, one smaller
// [1] = single pane (no division)
columnRatios: z.array(z.number()).default([1]),
rowRatios: z.array(z.number()).default([1]),
dividerThickness: z.number().default(0.03),
// Sill
sill: z.boolean().default(true),
sillDepth: z.number().default(0.08),
sillThickness: z.number().default(0.03),
}).describe(dedent`Window node - a parametric window placed on a wall
- position: center of the window in wall-local coordinate system
- width/height: overall outer dimensions
- frameThickness: width of the frame members
- frameDepth: how deep the frame sits within the wall
- columnRatios/rowRatios: pane division ratios
- sill: whether to show a window sill
`)
export type WindowNode = z.infer<typeof WindowNode>
+2
View File
@@ -9,6 +9,7 @@ import { ScanNode } from './nodes/scan'
import { SiteNode } from './nodes/site'
import { SlabNode } from './nodes/slab'
import { WallNode } from './nodes/wall'
import { WindowNode } from './nodes/window'
import { ZoneNode } from './nodes/zone'
export const AnyNode = z.discriminatedUnion('type', [
@@ -23,6 +24,7 @@ export const AnyNode = z.discriminatedUnion('type', [
RoofNode,
ScanNode,
GuideNode,
WindowNode,
])
export type AnyNode = z.infer<typeof AnyNode>
@@ -309,7 +309,7 @@ function collectCutoutBrushes(
const wallMatrixInverse = wallMesh.matrixWorld.clone().invert()
for (const child of childrenNodes) {
if (child.type !== 'item') continue
if (child.type !== 'item' && child.type !== 'window') continue
const childMesh = sceneRegistry.nodes.get(child.id)
if (!childMesh) continue
@@ -0,0 +1,69 @@
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, WindowNode } from '../../schema'
import useScene from '../../store/use-scene'
const glassMaterial = new MeshStandardNodeMaterial({
name: 'glass',
color: 'lightgray',
roughness: 0.8,
metalness: 0,
transparent: true,
opacity: 0.35,
side: DoubleSide,
depthWrite: false,
})
export const WindowSystem = () => {
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 !== 'window') return
const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh
if (!mesh) return // Keep dirty until mesh mounts
updateWindowMesh(node as WindowNode, mesh)
clearDirty(id as AnyNodeId)
// Rebuild the parent wall so its cutout reflects the updated window geometry
if ((node as WindowNode).parentId) {
useScene.getState().dirtyNodes.add((node as WindowNode).parentId as AnyNodeId)
}
})
})
return null
}
function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
// Replace geometry with a box matching the overall window dimensions
mesh.geometry.dispose()
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth)
mesh.material = glassMaterial
// 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])
// Update (or create) the named cutout mesh used by wall-system for CSG subtraction
let cutout = mesh.getObjectByName('cutout') as THREE.Mesh | undefined
if (!cutout) {
cutout = new THREE.Mesh()
cutout.name = 'cutout'
mesh.add(cutout)
}
cutout.geometry.dispose()
// Extends 1m through the wall so the CSG brush covers full wall thickness
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0)
cutout.visible = false;
}