import { useFrame } from "@react-three/fiber"; import * as THREE from "three"; import { sceneRegistry } from "../../hooks/scene-registry/scene-registry"; import { AnyNode, WallNode } from "../../schema"; import useScene from "../../store/useScene"; export const WallSystem = () => { const { nodes, dirtyNodes, clearDirty } = useScene(); useFrame(() => { if (dirtyNodes.size === 0) return; dirtyNodes.forEach((id) => { const node = nodes[id]; if (!node) return; const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh; // 1. If a window is dirty, we actually need to redraw its PARENT wall // if ((node.type === 'window' || node.type === 'door') && node.parentId) { // updateWallGeometry(node.parentId); // return; // } // 2. If the wall itself is dirty if (node.type === "wall" && mesh) { updateWallGeometry(id); } clearDirty(id); // Reset for next frame }); }); return null; }; // Optimization: Logic moved to a vanilla function so it can be called // by the Editor or the System without React overhead function updateWallGeometry(wallId: string) { const node = useScene.getState().nodes[wallId as WallNode["id"]]; if (!node) return; if (node.type !== "wall") return; const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh; if (!mesh) return; const childrenIds = node.children || []; const childrenNodes = childrenIds .map((childId) => useScene.getState().nodes[childId]) .filter((n): n is AnyNode => n !== undefined); // Perform the Extrusion with Holes logic we discussed const newGeo = generateExtrudedWall(node, childrenNodes); mesh.geometry.dispose(); mesh.geometry = newGeo; mesh.position.set(node.start[0], 0, node.start[1]); // Rotate mesh to look at 'end' point const angle = Math.atan2( node.end[1] - node.start[1], node.end[0] - node.start[0] ); mesh.rotation.y = -angle; } export function generateExtrudedWall( wallNode: WallNode, childrenNodes: AnyNode[] ) { // 1. Calculate Wall Dimensions const start = new THREE.Vector2(wallNode.start[0], wallNode.start[1]); const end = new THREE.Vector2(wallNode.end[0], wallNode.end[1]); const length = start.distanceTo(end); const height = wallNode.height || 2.5; const thickness = wallNode.thickness || 0.2; // 2. Create the Main Wall Shape (a rectangle in 2D) // We draw this on the XY plane, where X is "along the wall" and Y is "height" const shape = new THREE.Shape(); shape.moveTo(0, 0); shape.lineTo(length, 0); shape.lineTo(length, height); shape.lineTo(0, height); shape.closePath(); // 3. Process Openings (Holes) // Compute wall's transform info for converting world coords to wall-local coords const wallStart: [number, number] = [wallNode.start[0], wallNode.start[1]]; const wallAngle = Math.atan2( wallNode.end[1] - wallNode.start[1], wallNode.end[0] - wallNode.start[0] ); childrenNodes.forEach((child) => { // Only process items that are intended to be wall cutouts if (child.type !== "item") return; const childMesh = sceneRegistry.nodes.get(child.id); if (!childMesh) return; const cutoutMesh = childMesh.getObjectByName("cutout") as THREE.Mesh; if (!cutoutMesh) return; const holePath = createPathFromCutout(cutoutMesh, wallStart, wallAngle); if (holePath) { shape.holes.push(holePath); } }); // 4. Extrude the Shape into 3D const geometry = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false, }); // 5. Pivot Alignment // Center the geometry thickness so the "start/end" line is in the middle of the wall geometry.translate(0, 0, -thickness / 2); return geometry; } /** * Creates a Path from a cutout mesh geometry, transforming vertices * from world space to wall-local space. * * Wall-local space: * - Origin at wall start point * - X axis runs along the wall (toward end point) * - Y axis is height (world Y) */ function createPathFromCutout( cutoutMesh: THREE.Mesh, wallStart: [number, number], wallAngle: number ): THREE.Path | null { const geometry = cutoutMesh.geometry; if (!geometry) return null; const positions = geometry.attributes.position; if (!positions) return null; // Update world matrix to get correct world positions cutoutMesh.updateWorldMatrix(true, false); // Collect unique vertices (buffer geometry has duplicates for triangulation) const uniquePoints: THREE.Vector2[] = []; const seen = new Set(); const v3 = new THREE.Vector3(); // Precompute sin/cos for rotation const cosAngle = Math.cos(-wallAngle); const sinAngle = Math.sin(-wallAngle); for (let i = 0; i < positions.count; i++) { v3.fromBufferAttribute(positions, i); // Transform to world space v3.applyMatrix4(cutoutMesh.matrixWorld); // Transform from world space to wall-local space: // 1. Translate so wall start is at origin (in XZ plane) const worldX = v3.x - wallStart[0]; const worldZ = v3.z - wallStart[1]; // 2. Rotate around Y axis to align wall with local X axis // The wall shape is drawn on XY plane, so we need: // - localX = distance along wall // - localY = height (world Y) const localX = worldX * cosAngle - worldZ * sinAngle; const localY = v3.y; // Height stays the same // Create a key for deduplication (with small tolerance) const key = `${localX.toFixed(4)},${localY.toFixed(4)}`; if (!seen.has(key)) { seen.add(key); uniquePoints.push(new THREE.Vector2(localX, localY)); } } if (uniquePoints.length < 3) return null; // Sort points in counter-clockwise order around centroid const centroid = new THREE.Vector2(0, 0); for (const p of uniquePoints) { centroid.add(p); } centroid.divideScalar(uniquePoints.length); uniquePoints.sort((a, b) => { const angleA = Math.atan2(a.y - centroid.y, a.x - centroid.x); const angleB = Math.atan2(b.y - centroid.y, b.x - centroid.x); return angleA - angleB; }); // Create the path const path = new THREE.Path(); path.moveTo(uniquePoints[0]?.x || 0, uniquePoints[0]?.y || 0); for (let i = 1; i < uniquePoints.length; i++) { path.lineTo(uniquePoints[i]?.x || 0, uniquePoints[i]?.y || 0); } path.closePath(); return path; }