This commit is contained in:
wass08
2026-01-21 10:03:29 +09:00
parent 14b0e6a98d
commit 5de0818633
53 changed files with 1241 additions and 1420 deletions
+93 -99
View File
@@ -1,19 +1,19 @@
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/use-scene";
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
import type { AnyNode, WallNode } from '../../schema'
import useScene from '../../store/use-scene'
export const WallSystem = () => {
const { nodes, dirtyNodes, clearDirty } = useScene();
const { nodes, dirtyNodes, clearDirty } = useScene()
useFrame(() => {
if (dirtyNodes.size === 0) return;
if (dirtyNodes.size === 0) return
dirtyNodes.forEach((id) => {
const node = nodes[id];
if (!node) return;
const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh;
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) {
@@ -22,117 +22,111 @@ export const WallSystem = () => {
// }
// 2. If the wall itself is dirty
if (node.type === "wall" && mesh) {
updateWallGeometry(id);
if (node.type === 'wall' && mesh) {
updateWallGeometry(id)
}
clearDirty(id); // Reset for next frame
});
});
clearDirty(id) // Reset for next frame
})
})
return null;
};
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 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 mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
if (!mesh) return
const childrenIds = node.children || [];
const childrenIds = node.children || []
const childrenNodes = childrenIds
.map((childId) => useScene.getState().nodes[childId])
.filter((n): n is AnyNode => n !== undefined);
.filter((n): n is AnyNode => n !== undefined)
// Generate visual geometry with holes
const newGeo = generateExtrudedWall(node, childrenNodes);
const newGeo = generateExtrudedWall(node, childrenNodes)
mesh.geometry.dispose();
mesh.geometry = newGeo;
mesh.geometry.dispose()
mesh.geometry = newGeo
// Update collision mesh with solid geometry (no holes)
const collisionMesh = mesh.getObjectByName("collision-mesh") as THREE.Mesh;
const collisionMesh = mesh.getObjectByName('collision-mesh') as THREE.Mesh
if (collisionMesh) {
const collisionGeo = generateExtrudedWall(node, []); // No children = no holes
collisionMesh.geometry.dispose();
collisionMesh.geometry = collisionGeo;
const collisionGeo = generateExtrudedWall(node, []) // No children = no holes
collisionMesh.geometry.dispose()
collisionMesh.geometry = collisionGeo
}
mesh.position.set(node.start[0], 0, node.start[1]);
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;
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[],
) {
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.1;
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.1
// 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();
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 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],
);
)
// Get the wall mesh's world Y position (from level offset)
const wallMesh = sceneRegistry.nodes.get(wallNode.id) as THREE.Mesh;
const wallWorldY = wallMesh?.getWorldPosition(new THREE.Vector3()).y ?? 0;
const wallMesh = sceneRegistry.nodes.get(wallNode.id) as THREE.Mesh
const wallWorldY = wallMesh?.getWorldPosition(new THREE.Vector3()).y ?? 0
childrenNodes.forEach((child) => {
// Only process items that are intended to be wall cutouts
if (child.type !== "item") return;
if (child.type !== 'item') return
const childMesh = sceneRegistry.nodes.get(child.id);
const childMesh = sceneRegistry.nodes.get(child.id)
if (!childMesh) {
return;
return
}
const cutoutMesh = childMesh.getObjectByName("cutout") as THREE.Mesh;
if (!cutoutMesh) return;
const cutoutMesh = childMesh.getObjectByName('cutout') as THREE.Mesh
if (!cutoutMesh) return
const holePath = createPathFromCutout(cutoutMesh, wallStart, wallAngle, wallWorldY);
const holePath = createPathFromCutout(cutoutMesh, wallStart, wallAngle, wallWorldY)
if (holePath) {
shape.holes.push(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);
geometry.translate(0, 0, -thickness / 2)
return geometry;
return geometry
}
/**
@@ -150,72 +144,72 @@ function createPathFromCutout(
wallAngle: number,
wallWorldY: number,
): THREE.Path | null {
const geometry = cutoutMesh.geometry;
if (!geometry) return null;
const geometry = cutoutMesh.geometry
if (!geometry) return null
const positions = geometry.attributes.position;
if (!positions) return null;
const positions = geometry.attributes.position
if (!positions) return null
// Update world matrix to get correct world positions
cutoutMesh.updateWorldMatrix(true, false);
cutoutMesh.updateWorldMatrix(true, false)
// Collect unique vertices (buffer geometry has duplicates for triangulation)
const uniquePoints: THREE.Vector2[] = [];
const seen = new Set<string>();
const v3 = new THREE.Vector3();
const uniquePoints: THREE.Vector2[] = []
const seen = new Set<string>()
const v3 = new THREE.Vector3()
// Precompute sin/cos for rotation
const cosAngle = Math.cos(-wallAngle);
const sinAngle = Math.sin(-wallAngle);
const cosAngle = Math.cos(-wallAngle)
const sinAngle = Math.sin(-wallAngle)
for (let i = 0; i < positions.count; i++) {
v3.fromBufferAttribute(positions, i);
v3.fromBufferAttribute(positions, i)
// Transform to world space
v3.applyMatrix4(cutoutMesh.matrixWorld);
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];
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 relative to wall's Y position
const localX = worldX * cosAngle - worldZ * sinAngle;
const localY = v3.y - wallWorldY; // Subtract wall's world Y to get local height
const localX = worldX * cosAngle - worldZ * sinAngle
const localY = v3.y - wallWorldY // Subtract wall's world Y to get local height
// Create a key for deduplication (with small tolerance)
const key = `${localX.toFixed(4)},${localY.toFixed(4)}`;
const key = `${localX.toFixed(4)},${localY.toFixed(4)}`
if (!seen.has(key)) {
seen.add(key);
uniquePoints.push(new THREE.Vector2(localX, localY));
seen.add(key)
uniquePoints.push(new THREE.Vector2(localX, localY))
}
}
if (uniquePoints.length < 3) return null;
if (uniquePoints.length < 3) return null
// Sort points in counter-clockwise order around centroid
const centroid = new THREE.Vector2(0, 0);
const centroid = new THREE.Vector2(0, 0)
for (const p of uniquePoints) {
centroid.add(p);
centroid.add(p)
}
centroid.divideScalar(uniquePoints.length);
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;
});
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);
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.lineTo(uniquePoints[i]?.x || 0, uniquePoints[i]?.y || 0)
}
path.closePath();
path.closePath()
return path;
return path
}