@pascal-app/core 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/events/bus.d.ts +45 -6
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +2 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +164 -6
- package/dist/index.d.ts +8 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -14
- package/dist/lib/door-operation.d.ts +7 -0
- package/dist/lib/door-operation.d.ts.map +1 -0
- package/dist/lib/door-operation.js +25 -0
- package/dist/lib/polygon-geometry.d.ts.map +1 -1
- package/dist/lib/slab-polygon.d.ts +3 -0
- package/dist/lib/slab-polygon.d.ts.map +1 -0
- package/dist/lib/slab-polygon.js +58 -0
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +10 -8
- package/dist/material-library.d.ts +5 -3
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +28 -32
- package/dist/schema/asset-url.d.ts +34 -0
- package/dist/schema/asset-url.d.ts.map +1 -0
- package/dist/schema/asset-url.js +79 -0
- package/dist/schema/asset-url.test.d.ts +2 -0
- package/dist/schema/asset-url.test.d.ts.map +1 -0
- package/dist/schema/asset-url.test.js +134 -0
- package/dist/schema/index.d.ts +7 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +5 -3
- package/dist/schema/material.d.ts +1 -0
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +13 -11
- package/dist/schema/nodes/column.d.ts +520 -0
- package/dist/schema/nodes/column.d.ts.map +1 -0
- package/dist/schema/nodes/column.js +385 -0
- package/dist/schema/nodes/door.d.ts +72 -0
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +39 -2
- package/dist/schema/nodes/fence.d.ts +1 -1
- package/dist/schema/nodes/fence.js +2 -2
- package/dist/schema/nodes/guide.d.ts +17 -0
- package/dist/schema/nodes/guide.d.ts.map +1 -1
- package/dist/schema/nodes/guide.js +11 -1
- package/dist/schema/nodes/item.d.ts +20 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +30 -1
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +6 -0
- package/dist/schema/nodes/roof-segment.d.ts +2 -2
- package/dist/schema/nodes/roof.d.ts +2 -2
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +5 -5
- package/dist/schema/nodes/scan.d.ts.map +1 -1
- package/dist/schema/nodes/scan.js +2 -1
- package/dist/schema/nodes/site.d.ts +7 -0
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/spawn.d.ts +24 -0
- package/dist/schema/nodes/spawn.d.ts.map +1 -0
- package/dist/schema/nodes/spawn.js +8 -0
- package/dist/schema/nodes/stair.d.ts +6 -6
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +9 -7
- package/dist/schema/nodes/window.d.ts +55 -0
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +29 -0
- package/dist/schema/types.d.ts +320 -5
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +13 -10
- package/dist/store/use-interactive.d.ts +43 -0
- package/dist/store/use-interactive.d.ts.map +1 -1
- package/dist/store/use-interactive.js +66 -0
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +69 -5
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +41 -7
- package/dist/systems/stair/stair-opening-sync.test.d.ts +2 -0
- package/dist/systems/stair/stair-opening-sync.test.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-sync.test.js +63 -0
- package/dist/systems/wall/wall-curve.d.ts +1 -1
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +1 -1
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +2 -6
- package/package.json +34 -5
- package/dist/materials.d.ts +0 -10
- package/dist/materials.d.ts.map +0 -1
- package/dist/materials.js +0 -22
- package/dist/systems/ceiling/ceiling-system.d.ts +0 -8
- package/dist/systems/ceiling/ceiling-system.d.ts.map +0 -1
- package/dist/systems/ceiling/ceiling-system.js +0 -92
- package/dist/systems/door/door-system.d.ts +0 -2
- package/dist/systems/door/door-system.d.ts.map +0 -1
- package/dist/systems/door/door-system.js +0 -195
- package/dist/systems/fence/fence-system.d.ts +0 -2
- package/dist/systems/fence/fence-system.d.ts.map +0 -1
- package/dist/systems/fence/fence-system.js +0 -187
- package/dist/systems/item/item-system.d.ts +0 -2
- package/dist/systems/item/item-system.d.ts.map +0 -1
- package/dist/systems/item/item-system.js +0 -48
- package/dist/systems/roof/roof-system.d.ts +0 -16
- package/dist/systems/roof/roof-system.d.ts.map +0 -1
- package/dist/systems/roof/roof-system.js +0 -797
- package/dist/systems/slab/slab-system.d.ts +0 -8
- package/dist/systems/slab/slab-system.d.ts.map +0 -1
- package/dist/systems/slab/slab-system.js +0 -214
- package/dist/systems/stair/stair-system.d.ts +0 -2
- package/dist/systems/stair/stair-system.d.ts.map +0 -1
- package/dist/systems/stair/stair-system.js +0 -776
- package/dist/systems/wall/wall-system.d.ts +0 -12
- package/dist/systems/wall/wall-system.d.ts.map +0 -1
- package/dist/systems/wall/wall-system.js +0 -455
- package/dist/systems/window/window-system.d.ts +0 -2
- package/dist/systems/window/window-system.d.ts.map +0 -1
- package/dist/systems/window/window-system.js +0 -131
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
2
|
-
import type { AnyNode, WallNode } from '../../schema';
|
|
3
|
-
import { type WallMiterData } from './wall-mitering';
|
|
4
|
-
export declare const WallSystem: () => null;
|
|
5
|
-
/**
|
|
6
|
-
* Generates extruded wall geometry with mitering and cutouts
|
|
7
|
-
*
|
|
8
|
-
* Key insight from demo: polygon is built in WORLD coordinates first,
|
|
9
|
-
* then we transform to wall-local for the 3D mesh.
|
|
10
|
-
*/
|
|
11
|
-
export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
|
|
12
|
-
//# sourceMappingURL=wall-system.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,OAAO,KAAK,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAIhE,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAA;AAsRxB,eAAO,MAAM,UAAU,YAuDtB,CAAA;AA0DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,oFA2GlB"}
|
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
import { useFrame } from '@react-three/fiber';
|
|
2
|
-
import * as THREE from 'three';
|
|
3
|
-
import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
4
|
-
import { computeBoundsTree } from 'three-mesh-bvh';
|
|
5
|
-
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
|
|
6
|
-
import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager';
|
|
7
|
-
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
|
|
8
|
-
import useScene from '../../store/use-scene';
|
|
9
|
-
import { getWallCurveFrameAt, getWallSurfacePolygon, isCurvedWall } from './wall-curve';
|
|
10
|
-
import { DEFAULT_WALL_HEIGHT, getWallPlanFootprint, getWallThickness } from './wall-footprint';
|
|
11
|
-
import { calculateLevelMiters, getAdjacentWallIds, getWallMiterBoundaryPoints, pointToKey, } from './wall-mitering';
|
|
12
|
-
// Reusable CSG evaluator for better performance
|
|
13
|
-
const csgEvaluator = new Evaluator();
|
|
14
|
-
const CURVED_WALL_3D_ENDPOINT_INSET = 0.0015;
|
|
15
|
-
const WALL_FACE_NORMAL_Y_EPSILON = 0.6;
|
|
16
|
-
const WALL_FACE_EDGE_DISTANCE_EPSILON = 0.003;
|
|
17
|
-
function ensureUv2Attribute(geometry) {
|
|
18
|
-
const uv = geometry.getAttribute('uv');
|
|
19
|
-
if (!uv)
|
|
20
|
-
return;
|
|
21
|
-
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
|
|
22
|
-
}
|
|
23
|
-
function insetCurvedWallBoundaryPointsFor3D(wall, boundaryPoints, miterData) {
|
|
24
|
-
if (!boundaryPoints || !isCurvedWall(wall)) {
|
|
25
|
-
return boundaryPoints;
|
|
26
|
-
}
|
|
27
|
-
const insetDistance = Math.min(CURVED_WALL_3D_ENDPOINT_INSET, Math.max((wall.thickness ?? 0.1) * 0.01, 0.0005));
|
|
28
|
-
if (insetDistance <= 0) {
|
|
29
|
-
return boundaryPoints;
|
|
30
|
-
}
|
|
31
|
-
const next = { ...boundaryPoints };
|
|
32
|
-
const startJunction = miterData.junctions.get(pointToKey({ x: wall.start[0], y: wall.start[1] }));
|
|
33
|
-
const endJunction = miterData.junctions.get(pointToKey({ x: wall.end[0], y: wall.end[1] }));
|
|
34
|
-
if (startJunction && startJunction.connectedWalls.length > 1) {
|
|
35
|
-
const frame = getWallCurveFrameAt(wall, 0);
|
|
36
|
-
next.startLeft = {
|
|
37
|
-
x: next.startLeft.x + frame.tangent.x * insetDistance,
|
|
38
|
-
y: next.startLeft.y + frame.tangent.y * insetDistance,
|
|
39
|
-
};
|
|
40
|
-
next.startRight = {
|
|
41
|
-
x: next.startRight.x + frame.tangent.x * insetDistance,
|
|
42
|
-
y: next.startRight.y + frame.tangent.y * insetDistance,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (endJunction && endJunction.connectedWalls.length > 1) {
|
|
46
|
-
const frame = getWallCurveFrameAt(wall, 1);
|
|
47
|
-
next.endLeft = {
|
|
48
|
-
x: next.endLeft.x - frame.tangent.x * insetDistance,
|
|
49
|
-
y: next.endLeft.y - frame.tangent.y * insetDistance,
|
|
50
|
-
};
|
|
51
|
-
next.endRight = {
|
|
52
|
-
x: next.endRight.x - frame.tangent.x * insetDistance,
|
|
53
|
-
y: next.endRight.y - frame.tangent.y * insetDistance,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
return next;
|
|
57
|
-
}
|
|
58
|
-
function addTaggedWallBoundaryEdge(edges, points, startIndex, endIndex, tag) {
|
|
59
|
-
const start = points[startIndex];
|
|
60
|
-
const end = points[endIndex];
|
|
61
|
-
if (!(start && end))
|
|
62
|
-
return;
|
|
63
|
-
if (Math.hypot(end.x - start.x, end.z - start.z) < 1e-6)
|
|
64
|
-
return;
|
|
65
|
-
edges.push({
|
|
66
|
-
start: new THREE.Vector2(start.x, start.z),
|
|
67
|
-
end: new THREE.Vector2(end.x, end.z),
|
|
68
|
-
tag,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
function buildTaggedWallBoundaryEdges(wall, localPoints, miterData) {
|
|
72
|
-
if (localPoints.length < 2)
|
|
73
|
-
return [];
|
|
74
|
-
const edges = [];
|
|
75
|
-
if (isCurvedWall(wall)) {
|
|
76
|
-
const sidePointCount = Math.floor(localPoints.length / 2);
|
|
77
|
-
if (sidePointCount < 2)
|
|
78
|
-
return edges;
|
|
79
|
-
for (let index = 0; index < sidePointCount - 1; index += 1) {
|
|
80
|
-
addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'back');
|
|
81
|
-
}
|
|
82
|
-
addTaggedWallBoundaryEdge(edges, localPoints, sidePointCount - 1, sidePointCount, 'base');
|
|
83
|
-
for (let index = sidePointCount; index < localPoints.length - 1; index += 1) {
|
|
84
|
-
addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'front');
|
|
85
|
-
}
|
|
86
|
-
addTaggedWallBoundaryEdge(edges, localPoints, localPoints.length - 1, 0, 'base');
|
|
87
|
-
return edges;
|
|
88
|
-
}
|
|
89
|
-
const startKey = pointToKey({ x: wall.start[0], y: wall.start[1] });
|
|
90
|
-
const startJunction = miterData.junctionData.get(startKey)?.get(wall.id);
|
|
91
|
-
const startLeftIndex = startJunction ? localPoints.length - 2 : localPoints.length - 1;
|
|
92
|
-
const endLeftIndex = startJunction ? localPoints.length - 3 : localPoints.length - 2;
|
|
93
|
-
addTaggedWallBoundaryEdge(edges, localPoints, 0, 1, 'back');
|
|
94
|
-
for (let index = 1; index < endLeftIndex; index += 1) {
|
|
95
|
-
addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'base');
|
|
96
|
-
}
|
|
97
|
-
addTaggedWallBoundaryEdge(edges, localPoints, endLeftIndex, startLeftIndex, 'front');
|
|
98
|
-
for (let index = startLeftIndex; index < localPoints.length - 1; index += 1) {
|
|
99
|
-
addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'base');
|
|
100
|
-
}
|
|
101
|
-
addTaggedWallBoundaryEdge(edges, localPoints, localPoints.length - 1, 0, 'base');
|
|
102
|
-
return edges;
|
|
103
|
-
}
|
|
104
|
-
function distanceToWallBoundaryEdge(point, edge) {
|
|
105
|
-
const edgeDx = edge.end.x - edge.start.x;
|
|
106
|
-
const edgeDz = edge.end.y - edge.start.y;
|
|
107
|
-
const pointDx = point.x - edge.start.x;
|
|
108
|
-
const pointDz = point.y - edge.start.y;
|
|
109
|
-
const edgeLengthSq = edgeDx * edgeDx + edgeDz * edgeDz;
|
|
110
|
-
if (edgeLengthSq < 1e-12) {
|
|
111
|
-
return point.distanceTo(edge.start);
|
|
112
|
-
}
|
|
113
|
-
const t = THREE.MathUtils.clamp((pointDx * edgeDx + pointDz * edgeDz) / edgeLengthSq, 0, 1);
|
|
114
|
-
const closestX = edge.start.x + edgeDx * t;
|
|
115
|
-
const closestZ = edge.start.y + edgeDz * t;
|
|
116
|
-
return Math.hypot(point.x - closestX, point.y - closestZ);
|
|
117
|
-
}
|
|
118
|
-
function getWallFaceMaterialIndex(wall, face) {
|
|
119
|
-
const semantic = face === 'front' ? wall.frontSide : wall.backSide;
|
|
120
|
-
const fallback = face === 'front' ? 1 : 2;
|
|
121
|
-
if (semantic === 'interior')
|
|
122
|
-
return 1;
|
|
123
|
-
if (semantic === 'exterior')
|
|
124
|
-
return 2;
|
|
125
|
-
return fallback;
|
|
126
|
-
}
|
|
127
|
-
function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
|
|
128
|
-
const position = geometry.getAttribute('position');
|
|
129
|
-
if (!position)
|
|
130
|
-
return;
|
|
131
|
-
const index = geometry.getIndex();
|
|
132
|
-
const triangleCount = index ? Math.floor(index.count / 3) : Math.floor(position.count / 3);
|
|
133
|
-
if (triangleCount === 0) {
|
|
134
|
-
geometry.clearGroups();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const triangleMaterials = new Array(triangleCount).fill(0);
|
|
138
|
-
const a = new THREE.Vector3();
|
|
139
|
-
const b = new THREE.Vector3();
|
|
140
|
-
const c = new THREE.Vector3();
|
|
141
|
-
const ab = new THREE.Vector3();
|
|
142
|
-
const ac = new THREE.Vector3();
|
|
143
|
-
const normal = new THREE.Vector3();
|
|
144
|
-
const centroid = new THREE.Vector3();
|
|
145
|
-
const projectedCentroid = new THREE.Vector2();
|
|
146
|
-
const maxBoundaryDistance = Math.max(getWallThickness(wall) * 0.02, WALL_FACE_EDGE_DISTANCE_EPSILON);
|
|
147
|
-
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
|
|
148
|
-
const baseIndex = triangleIndex * 3;
|
|
149
|
-
const ia = index ? index.getX(baseIndex) : baseIndex;
|
|
150
|
-
const ib = index ? index.getX(baseIndex + 1) : baseIndex + 1;
|
|
151
|
-
const ic = index ? index.getX(baseIndex + 2) : baseIndex + 2;
|
|
152
|
-
a.fromBufferAttribute(position, ia);
|
|
153
|
-
b.fromBufferAttribute(position, ib);
|
|
154
|
-
c.fromBufferAttribute(position, ic);
|
|
155
|
-
ab.subVectors(b, a);
|
|
156
|
-
ac.subVectors(c, a);
|
|
157
|
-
normal.crossVectors(ab, ac);
|
|
158
|
-
if (normal.lengthSq() < 1e-12) {
|
|
159
|
-
triangleMaterials[triangleIndex] = 0;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
normal.normalize();
|
|
163
|
-
if (Math.abs(normal.y) >= WALL_FACE_NORMAL_Y_EPSILON) {
|
|
164
|
-
triangleMaterials[triangleIndex] = 0;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
centroid
|
|
168
|
-
.copy(a)
|
|
169
|
-
.add(b)
|
|
170
|
-
.add(c)
|
|
171
|
-
.multiplyScalar(1 / 3);
|
|
172
|
-
projectedCentroid.set(centroid.x, centroid.z);
|
|
173
|
-
let nearestTag = null;
|
|
174
|
-
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
175
|
-
for (const edge of boundaryEdges) {
|
|
176
|
-
const distance = distanceToWallBoundaryEdge(projectedCentroid, edge);
|
|
177
|
-
if (distance < nearestDistance) {
|
|
178
|
-
nearestDistance = distance;
|
|
179
|
-
nearestTag = edge.tag;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (!nearestTag || nearestDistance > maxBoundaryDistance) {
|
|
183
|
-
triangleMaterials[triangleIndex] = 0;
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (nearestTag === 'base') {
|
|
187
|
-
triangleMaterials[triangleIndex] = 0;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag);
|
|
191
|
-
}
|
|
192
|
-
geometry.clearGroups();
|
|
193
|
-
let currentMaterial = triangleMaterials[0] ?? 0;
|
|
194
|
-
let groupStart = 0;
|
|
195
|
-
for (let triangleIndex = 1; triangleIndex < triangleCount; triangleIndex += 1) {
|
|
196
|
-
const materialIndex = triangleMaterials[triangleIndex] ?? 0;
|
|
197
|
-
if (materialIndex === currentMaterial)
|
|
198
|
-
continue;
|
|
199
|
-
geometry.addGroup(groupStart * 3, (triangleIndex - groupStart) * 3, currentMaterial);
|
|
200
|
-
groupStart = triangleIndex;
|
|
201
|
-
currentMaterial = materialIndex;
|
|
202
|
-
}
|
|
203
|
-
geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
|
|
204
|
-
}
|
|
205
|
-
// ============================================================================
|
|
206
|
-
// WALL SYSTEM
|
|
207
|
-
// ============================================================================
|
|
208
|
-
let useFrameNb = 0;
|
|
209
|
-
export const WallSystem = () => {
|
|
210
|
-
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
211
|
-
const clearDirty = useScene((state) => state.clearDirty);
|
|
212
|
-
useFrame(() => {
|
|
213
|
-
if (dirtyNodes.size === 0)
|
|
214
|
-
return;
|
|
215
|
-
const nodes = useScene.getState().nodes;
|
|
216
|
-
// Collect dirty walls and their levels
|
|
217
|
-
const dirtyWallsByLevel = new Map();
|
|
218
|
-
useFrameNb += 1;
|
|
219
|
-
dirtyNodes.forEach((id) => {
|
|
220
|
-
const node = nodes[id];
|
|
221
|
-
if (!node || node.type !== 'wall')
|
|
222
|
-
return;
|
|
223
|
-
const levelId = node.parentId;
|
|
224
|
-
if (!levelId)
|
|
225
|
-
return;
|
|
226
|
-
if (!dirtyWallsByLevel.has(levelId)) {
|
|
227
|
-
dirtyWallsByLevel.set(levelId, new Set());
|
|
228
|
-
}
|
|
229
|
-
dirtyWallsByLevel.get(levelId)?.add(id);
|
|
230
|
-
});
|
|
231
|
-
// Process each level that has dirty walls
|
|
232
|
-
for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
|
|
233
|
-
const levelWalls = getLevelWalls(levelId);
|
|
234
|
-
const miterData = calculateLevelMiters(levelWalls);
|
|
235
|
-
// Update dirty walls
|
|
236
|
-
for (const wallId of dirtyWallIds) {
|
|
237
|
-
const mesh = sceneRegistry.nodes.get(wallId);
|
|
238
|
-
if (mesh) {
|
|
239
|
-
updateWallGeometry(wallId, miterData);
|
|
240
|
-
clearDirty(wallId);
|
|
241
|
-
}
|
|
242
|
-
// If mesh not found, keep it dirty for next frame
|
|
243
|
-
}
|
|
244
|
-
// Update adjacent walls that share junctions
|
|
245
|
-
const adjacentWallIds = getAdjacentWallIds(levelWalls, dirtyWallIds);
|
|
246
|
-
for (const wallId of adjacentWallIds) {
|
|
247
|
-
if (!dirtyWallIds.has(wallId)) {
|
|
248
|
-
const mesh = sceneRegistry.nodes.get(wallId);
|
|
249
|
-
if (mesh) {
|
|
250
|
-
updateWallGeometry(wallId, miterData);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}, 4);
|
|
256
|
-
return null;
|
|
257
|
-
};
|
|
258
|
-
/**
|
|
259
|
-
* Gets all walls that belong to a level
|
|
260
|
-
*/
|
|
261
|
-
function getLevelWalls(levelId) {
|
|
262
|
-
const { nodes } = useScene.getState();
|
|
263
|
-
const level = nodes[levelId];
|
|
264
|
-
if (!level || level.type !== 'level')
|
|
265
|
-
return [];
|
|
266
|
-
const walls = [];
|
|
267
|
-
for (const childId of level.children) {
|
|
268
|
-
const child = nodes[childId];
|
|
269
|
-
if (child?.type === 'wall') {
|
|
270
|
-
walls.push(child);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return walls;
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Updates the geometry for a single wall
|
|
277
|
-
*/
|
|
278
|
-
function updateWallGeometry(wallId, miterData) {
|
|
279
|
-
const nodes = useScene.getState().nodes;
|
|
280
|
-
const node = nodes[wallId];
|
|
281
|
-
if (!node || node.type !== 'wall')
|
|
282
|
-
return;
|
|
283
|
-
const mesh = sceneRegistry.nodes.get(wallId);
|
|
284
|
-
if (!mesh)
|
|
285
|
-
return;
|
|
286
|
-
const levelId = resolveLevelId(node, nodes);
|
|
287
|
-
const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end);
|
|
288
|
-
const childrenIds = node.children || [];
|
|
289
|
-
const childrenNodes = childrenIds
|
|
290
|
-
.map((childId) => nodes[childId])
|
|
291
|
-
.filter((n) => n !== undefined);
|
|
292
|
-
const newGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation);
|
|
293
|
-
mesh.geometry.dispose();
|
|
294
|
-
mesh.geometry = newGeo;
|
|
295
|
-
// Update collision mesh
|
|
296
|
-
const collisionMesh = mesh.getObjectByName('collision-mesh');
|
|
297
|
-
if (collisionMesh) {
|
|
298
|
-
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation);
|
|
299
|
-
collisionMesh.geometry.dispose();
|
|
300
|
-
collisionMesh.geometry = collisionGeo;
|
|
301
|
-
}
|
|
302
|
-
mesh.position.set(node.start[0], slabElevation, node.start[1]);
|
|
303
|
-
const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
|
|
304
|
-
mesh.rotation.y = -angle;
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Generates extruded wall geometry with mitering and cutouts
|
|
308
|
-
*
|
|
309
|
-
* Key insight from demo: polygon is built in WORLD coordinates first,
|
|
310
|
-
* then we transform to wall-local for the 3D mesh.
|
|
311
|
-
*/
|
|
312
|
-
export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0) {
|
|
313
|
-
const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
|
|
314
|
-
const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
|
|
315
|
-
// Positive slab: shift the whole wall up (full height preserved)
|
|
316
|
-
// Negative slab: extend wall downward so top stays fixed at wallNode.height
|
|
317
|
-
const wallHeight = wallNode.height ?? DEFAULT_WALL_HEIGHT;
|
|
318
|
-
const height = slabElevation > 0 ? wallHeight : wallHeight - slabElevation;
|
|
319
|
-
const thickness = getWallThickness(wallNode);
|
|
320
|
-
// Wall direction and normal (exactly like demo)
|
|
321
|
-
const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y };
|
|
322
|
-
const L = Math.sqrt(v.x * v.x + v.y * v.y);
|
|
323
|
-
if (L < 1e-9) {
|
|
324
|
-
return new THREE.BufferGeometry();
|
|
325
|
-
}
|
|
326
|
-
const boundaryPoints = getWallMiterBoundaryPoints(wallNode, miterData);
|
|
327
|
-
const polyPoints = isCurvedWall(wallNode)
|
|
328
|
-
? getWallSurfacePolygon(wallNode, 24, insetCurvedWallBoundaryPointsFor3D(wallNode, boundaryPoints, miterData) ?? undefined)
|
|
329
|
-
: getWallPlanFootprint(wallNode, miterData);
|
|
330
|
-
if (polyPoints.length < 3) {
|
|
331
|
-
return new THREE.BufferGeometry();
|
|
332
|
-
}
|
|
333
|
-
// Transform world coordinates to wall-local coordinates
|
|
334
|
-
// Wall-local: x along wall, z perpendicular (thickness direction)
|
|
335
|
-
const wallAngle = Math.atan2(v.y, v.x);
|
|
336
|
-
const cosA = Math.cos(-wallAngle);
|
|
337
|
-
const sinA = Math.sin(-wallAngle);
|
|
338
|
-
const worldToLocal = (worldPt) => {
|
|
339
|
-
const dx = worldPt.x - wallStart.x;
|
|
340
|
-
const dy = worldPt.y - wallStart.y;
|
|
341
|
-
return {
|
|
342
|
-
x: dx * cosA - dy * sinA,
|
|
343
|
-
z: dx * sinA + dy * cosA,
|
|
344
|
-
};
|
|
345
|
-
};
|
|
346
|
-
// Convert polygon to local coordinates
|
|
347
|
-
const localPoints = polyPoints.map(worldToLocal);
|
|
348
|
-
const boundaryEdges = buildTaggedWallBoundaryEdges(wallNode, localPoints, miterData);
|
|
349
|
-
// Build THREE.js shape
|
|
350
|
-
// Shape uses (x, y) where we map: shape.x = local.x, shape.y = -local.z
|
|
351
|
-
// The negation is needed because after rotateX(-PI/2), shape.y becomes -geometry.z
|
|
352
|
-
const footprint = new THREE.Shape();
|
|
353
|
-
footprint.moveTo(localPoints[0].x, -localPoints[0].z);
|
|
354
|
-
for (let i = 1; i < localPoints.length; i++) {
|
|
355
|
-
footprint.lineTo(localPoints[i].x, -localPoints[i].z);
|
|
356
|
-
}
|
|
357
|
-
footprint.closePath();
|
|
358
|
-
// Extrude along Z by height
|
|
359
|
-
const geometry = new THREE.ExtrudeGeometry(footprint, {
|
|
360
|
-
depth: height,
|
|
361
|
-
bevelEnabled: false,
|
|
362
|
-
});
|
|
363
|
-
// Rotate so extrusion direction (Z) becomes height direction (Y)
|
|
364
|
-
geometry.rotateX(-Math.PI / 2);
|
|
365
|
-
geometry.computeVertexNormals();
|
|
366
|
-
assignWallMaterialGroups(geometry, wallNode, boundaryEdges);
|
|
367
|
-
ensureUv2Attribute(geometry);
|
|
368
|
-
// Apply CSG subtraction for cutouts (doors/windows)
|
|
369
|
-
const cutoutBrushes = collectCutoutBrushes(wallNode, childrenNodes, thickness);
|
|
370
|
-
if (cutoutBrushes.length === 0) {
|
|
371
|
-
return geometry;
|
|
372
|
-
}
|
|
373
|
-
// Create wall brush from geometry
|
|
374
|
-
// Pre-compute BVH with new API to avoid deprecation warning
|
|
375
|
-
geometry.computeBoundsTree = computeBoundsTree;
|
|
376
|
-
geometry.computeBoundsTree({ maxLeafSize: 10 });
|
|
377
|
-
const wallBrush = new Brush(geometry);
|
|
378
|
-
wallBrush.updateMatrixWorld();
|
|
379
|
-
// Subtract each cutout from the wall
|
|
380
|
-
let resultBrush = wallBrush;
|
|
381
|
-
for (const cutoutBrush of cutoutBrushes) {
|
|
382
|
-
cutoutBrush.updateMatrixWorld();
|
|
383
|
-
const newResult = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
|
|
384
|
-
if (resultBrush !== wallBrush) {
|
|
385
|
-
resultBrush.geometry.dispose();
|
|
386
|
-
}
|
|
387
|
-
resultBrush = newResult;
|
|
388
|
-
}
|
|
389
|
-
// Clean up
|
|
390
|
-
wallBrush.geometry.dispose();
|
|
391
|
-
for (const brush of cutoutBrushes) {
|
|
392
|
-
brush.geometry.dispose();
|
|
393
|
-
}
|
|
394
|
-
const resultGeometry = resultBrush.geometry;
|
|
395
|
-
resultGeometry.computeVertexNormals();
|
|
396
|
-
assignWallMaterialGroups(resultGeometry, wallNode, boundaryEdges);
|
|
397
|
-
ensureUv2Attribute(resultGeometry);
|
|
398
|
-
return resultGeometry;
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Collects cutout brushes from child items for CSG subtraction
|
|
402
|
-
* The cutout mesh is a plane, so we extrude it into a box that goes through the wall
|
|
403
|
-
*/
|
|
404
|
-
function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
405
|
-
const brushes = [];
|
|
406
|
-
const wallMesh = sceneRegistry.nodes.get(wallNode.id);
|
|
407
|
-
if (!wallMesh)
|
|
408
|
-
return brushes;
|
|
409
|
-
// Get wall's world matrix inverse to transform cutouts to wall-local space
|
|
410
|
-
wallMesh.updateMatrixWorld();
|
|
411
|
-
const wallMatrixInverse = wallMesh.matrixWorld.clone().invert();
|
|
412
|
-
for (const child of childrenNodes) {
|
|
413
|
-
if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door')
|
|
414
|
-
continue;
|
|
415
|
-
const childMesh = sceneRegistry.nodes.get(child.id);
|
|
416
|
-
if (!childMesh)
|
|
417
|
-
continue;
|
|
418
|
-
const cutoutMesh = childMesh.getObjectByName('cutout');
|
|
419
|
-
if (!cutoutMesh)
|
|
420
|
-
continue;
|
|
421
|
-
// Get the cutout's bounding box in world space
|
|
422
|
-
cutoutMesh.updateMatrixWorld();
|
|
423
|
-
const positions = cutoutMesh.geometry?.attributes?.position;
|
|
424
|
-
if (!positions)
|
|
425
|
-
continue;
|
|
426
|
-
// Calculate bounds in wall-local space
|
|
427
|
-
const v3 = new THREE.Vector3();
|
|
428
|
-
let minX = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY;
|
|
429
|
-
let minY = Number.POSITIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY;
|
|
430
|
-
for (let i = 0; i < positions.count; i++) {
|
|
431
|
-
v3.fromBufferAttribute(positions, i);
|
|
432
|
-
v3.applyMatrix4(cutoutMesh.matrixWorld);
|
|
433
|
-
v3.applyMatrix4(wallMatrixInverse);
|
|
434
|
-
minX = Math.min(minX, v3.x);
|
|
435
|
-
maxX = Math.max(maxX, v3.x);
|
|
436
|
-
minY = Math.min(minY, v3.y);
|
|
437
|
-
maxY = Math.max(maxY, v3.y);
|
|
438
|
-
}
|
|
439
|
-
if (!Number.isFinite(minX))
|
|
440
|
-
continue;
|
|
441
|
-
// Create a box geometry that extends through the wall thickness
|
|
442
|
-
const width = maxX - minX;
|
|
443
|
-
const height = maxY - minY;
|
|
444
|
-
const depth = wallThickness * 2; // Extend beyond wall to ensure clean cut
|
|
445
|
-
const boxGeo = new THREE.BoxGeometry(width, height, depth);
|
|
446
|
-
// Position box at the center of the cutout
|
|
447
|
-
boxGeo.translate(minX + width / 2, minY + height / 2, 0);
|
|
448
|
-
// Pre-compute BVH with new API to avoid deprecation warning
|
|
449
|
-
boxGeo.computeBoundsTree = computeBoundsTree;
|
|
450
|
-
boxGeo.computeBoundsTree({ maxLeafSize: 10 });
|
|
451
|
-
const brush = new Brush(boxGeo);
|
|
452
|
-
brushes.push(brush);
|
|
453
|
-
}
|
|
454
|
-
return brushes;
|
|
455
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAUA,eAAO,MAAM,YAAY,YA2BxB,CAAA"}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { useFrame } from '@react-three/fiber';
|
|
2
|
-
import * as THREE from 'three';
|
|
3
|
-
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
|
|
4
|
-
import { baseMaterial, glassMaterial } from '../../materials';
|
|
5
|
-
import useScene from '../../store/use-scene';
|
|
6
|
-
// Invisible material for root mesh — used as selection hitbox only
|
|
7
|
-
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
|
|
8
|
-
export const WindowSystem = () => {
|
|
9
|
-
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
10
|
-
const clearDirty = useScene((state) => state.clearDirty);
|
|
11
|
-
useFrame(() => {
|
|
12
|
-
if (dirtyNodes.size === 0)
|
|
13
|
-
return;
|
|
14
|
-
const nodes = useScene.getState().nodes;
|
|
15
|
-
dirtyNodes.forEach((id) => {
|
|
16
|
-
const node = nodes[id];
|
|
17
|
-
if (!node || node.type !== 'window')
|
|
18
|
-
return;
|
|
19
|
-
const mesh = sceneRegistry.nodes.get(id);
|
|
20
|
-
if (!mesh)
|
|
21
|
-
return; // Keep dirty until mesh mounts
|
|
22
|
-
updateWindowMesh(node, mesh);
|
|
23
|
-
clearDirty(id);
|
|
24
|
-
// Rebuild the parent wall so its cutout reflects the updated window geometry
|
|
25
|
-
if (node.parentId) {
|
|
26
|
-
useScene.getState().dirtyNodes.add(node.parentId);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
}, 3);
|
|
30
|
-
return null;
|
|
31
|
-
};
|
|
32
|
-
function addBox(parent, material, w, h, d, x, y, z) {
|
|
33
|
-
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
|
|
34
|
-
m.position.set(x, y, z);
|
|
35
|
-
parent.add(m);
|
|
36
|
-
}
|
|
37
|
-
function updateWindowMesh(node, mesh) {
|
|
38
|
-
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
39
|
-
mesh.geometry.dispose();
|
|
40
|
-
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
41
|
-
mesh.material = hitboxMaterial;
|
|
42
|
-
// Sync transform from node (React may lag behind the system by a frame during drag)
|
|
43
|
-
mesh.position.set(node.position[0], node.position[1], node.position[2]);
|
|
44
|
-
mesh.rotation.set(node.rotation[0], node.rotation[1], node.rotation[2]);
|
|
45
|
-
// Dispose and remove all old visual children; preserve 'cutout'
|
|
46
|
-
for (const child of [...mesh.children]) {
|
|
47
|
-
if (child.name === 'cutout')
|
|
48
|
-
continue;
|
|
49
|
-
if (child instanceof THREE.Mesh)
|
|
50
|
-
child.geometry.dispose();
|
|
51
|
-
mesh.remove(child);
|
|
52
|
-
}
|
|
53
|
-
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, } = node;
|
|
54
|
-
const innerW = width - 2 * frameThickness;
|
|
55
|
-
const innerH = height - 2 * frameThickness;
|
|
56
|
-
// ── Frame members ──
|
|
57
|
-
// Top / bottom — full width
|
|
58
|
-
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
59
|
-
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
60
|
-
// Left / right — inner height to avoid corner overlap
|
|
61
|
-
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
62
|
-
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
63
|
-
// ── Pane grid ──
|
|
64
|
-
const numCols = columnRatios.length;
|
|
65
|
-
const numRows = rowRatios.length;
|
|
66
|
-
const usableW = innerW - (numCols - 1) * columnDividerThickness;
|
|
67
|
-
const usableH = innerH - (numRows - 1) * rowDividerThickness;
|
|
68
|
-
const colSum = columnRatios.reduce((a, b) => a + b, 0);
|
|
69
|
-
const rowSum = rowRatios.reduce((a, b) => a + b, 0);
|
|
70
|
-
const colWidths = columnRatios.map((r) => (r / colSum) * usableW);
|
|
71
|
-
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
|
|
72
|
-
// Compute column x-centers starting from left edge of inner area
|
|
73
|
-
const colXCenters = [];
|
|
74
|
-
let cx = -innerW / 2;
|
|
75
|
-
for (let c = 0; c < numCols; c++) {
|
|
76
|
-
colXCenters.push(cx + colWidths[c] / 2);
|
|
77
|
-
cx += colWidths[c];
|
|
78
|
-
if (c < numCols - 1)
|
|
79
|
-
cx += columnDividerThickness;
|
|
80
|
-
}
|
|
81
|
-
// Compute row y-centers starting from top edge of inner area (R1 = top)
|
|
82
|
-
const rowYCenters = [];
|
|
83
|
-
let cy = innerH / 2;
|
|
84
|
-
for (let r = 0; r < numRows; r++) {
|
|
85
|
-
rowYCenters.push(cy - rowHeights[r] / 2);
|
|
86
|
-
cy -= rowHeights[r];
|
|
87
|
-
if (r < numRows - 1)
|
|
88
|
-
cy -= rowDividerThickness;
|
|
89
|
-
}
|
|
90
|
-
// Column dividers — full inner height
|
|
91
|
-
cx = -innerW / 2;
|
|
92
|
-
for (let c = 0; c < numCols - 1; c++) {
|
|
93
|
-
cx += colWidths[c];
|
|
94
|
-
addBox(mesh, baseMaterial, columnDividerThickness, innerH, frameDepth, cx + columnDividerThickness / 2, 0, 0);
|
|
95
|
-
cx += columnDividerThickness;
|
|
96
|
-
}
|
|
97
|
-
// Row dividers — per column width, so they don't overlap column dividers (top to bottom)
|
|
98
|
-
cy = innerH / 2;
|
|
99
|
-
for (let r = 0; r < numRows - 1; r++) {
|
|
100
|
-
cy -= rowHeights[r];
|
|
101
|
-
const divY = cy - rowDividerThickness / 2;
|
|
102
|
-
for (let c = 0; c < numCols; c++) {
|
|
103
|
-
addBox(mesh, baseMaterial, colWidths[c], rowDividerThickness, frameDepth, colXCenters[c], divY, 0);
|
|
104
|
-
}
|
|
105
|
-
cy -= rowDividerThickness;
|
|
106
|
-
}
|
|
107
|
-
// Glass panes
|
|
108
|
-
const glassDepth = Math.max(0.004, frameDepth * 0.08);
|
|
109
|
-
for (let c = 0; c < numCols; c++) {
|
|
110
|
-
for (let r = 0; r < numRows; r++) {
|
|
111
|
-
addBox(mesh, glassMaterial, colWidths[c], rowHeights[r], glassDepth, colXCenters[c], rowYCenters[r], 0);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// ── Sill ──
|
|
115
|
-
if (sill) {
|
|
116
|
-
const sillW = width + sillDepth * 0.4; // slightly wider than frame
|
|
117
|
-
// Protrudes from the front face of the frame (+Z)
|
|
118
|
-
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
119
|
-
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
120
|
-
}
|
|
121
|
-
// ── Cutout (for wall CSG) — always full window dimensions, 1m deep ──
|
|
122
|
-
let cutout = mesh.getObjectByName('cutout');
|
|
123
|
-
if (!cutout) {
|
|
124
|
-
cutout = new THREE.Mesh();
|
|
125
|
-
cutout.name = 'cutout';
|
|
126
|
-
mesh.add(cutout);
|
|
127
|
-
}
|
|
128
|
-
cutout.geometry.dispose();
|
|
129
|
-
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0);
|
|
130
|
-
cutout.visible = false;
|
|
131
|
-
}
|