@pascal-app/core 0.7.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.
Files changed (76) hide show
  1. package/dist/events/bus.d.ts +9 -6
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/events/bus.js +1 -1
  4. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  5. package/dist/lib/space-detection.d.ts.map +1 -1
  6. package/dist/lib/space-detection.js +10 -8
  7. package/dist/material-library.d.ts.map +1 -1
  8. package/dist/material-library.js +20 -1
  9. package/dist/schema/asset-url.test.js +0 -4
  10. package/dist/schema/material.d.ts +2 -2
  11. package/dist/schema/nodes/ceiling.d.ts +1 -1
  12. package/dist/schema/nodes/column.d.ts +1 -1
  13. package/dist/schema/nodes/door.d.ts +1 -1
  14. package/dist/schema/nodes/fence.d.ts +2 -2
  15. package/dist/schema/nodes/fence.js +2 -2
  16. package/dist/schema/nodes/item.d.ts +12 -0
  17. package/dist/schema/nodes/item.d.ts.map +1 -1
  18. package/dist/schema/nodes/item.js +12 -0
  19. package/dist/schema/nodes/roof-segment.d.ts +3 -3
  20. package/dist/schema/nodes/roof.d.ts +6 -6
  21. package/dist/schema/nodes/roof.d.ts.map +1 -1
  22. package/dist/schema/nodes/roof.js +5 -5
  23. package/dist/schema/nodes/site.d.ts +6 -0
  24. package/dist/schema/nodes/site.d.ts.map +1 -1
  25. package/dist/schema/nodes/slab.d.ts +1 -1
  26. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  27. package/dist/schema/nodes/stair.d.ts +6 -6
  28. package/dist/schema/nodes/stair.d.ts.map +1 -1
  29. package/dist/schema/nodes/stair.js +9 -7
  30. package/dist/schema/nodes/wall.d.ts +3 -3
  31. package/dist/schema/nodes/window.d.ts +1 -1
  32. package/dist/schema/types.d.ts +33 -21
  33. package/dist/schema/types.d.ts.map +1 -1
  34. package/dist/store/actions/node-actions.d.ts.map +1 -1
  35. package/dist/store/actions/node-actions.js +7 -5
  36. package/dist/store/use-scene.d.ts.map +1 -1
  37. package/dist/store/use-scene.js +11 -5
  38. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  39. package/dist/systems/stair/stair-opening-sync.js +17 -44
  40. package/dist/systems/stair/stair-opening-sync.test.js +0 -2
  41. package/dist/systems/wall/wall-curve.d.ts +1 -1
  42. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  43. package/dist/systems/wall/wall-curve.js +1 -1
  44. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  45. package/dist/systems/wall/wall-mitering.js +2 -6
  46. package/package.json +4 -3
  47. package/dist/materials.d.ts +0 -10
  48. package/dist/materials.d.ts.map +0 -1
  49. package/dist/materials.js +0 -22
  50. package/dist/systems/ceiling/ceiling-system.d.ts +0 -8
  51. package/dist/systems/ceiling/ceiling-system.d.ts.map +0 -1
  52. package/dist/systems/ceiling/ceiling-system.js +0 -92
  53. package/dist/systems/door/door-system.d.ts +0 -2
  54. package/dist/systems/door/door-system.d.ts.map +0 -1
  55. package/dist/systems/door/door-system.js +0 -195
  56. package/dist/systems/fence/fence-system.d.ts +0 -2
  57. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  58. package/dist/systems/fence/fence-system.js +0 -187
  59. package/dist/systems/item/item-system.d.ts +0 -2
  60. package/dist/systems/item/item-system.d.ts.map +0 -1
  61. package/dist/systems/item/item-system.js +0 -48
  62. package/dist/systems/roof/roof-system.d.ts +0 -16
  63. package/dist/systems/roof/roof-system.d.ts.map +0 -1
  64. package/dist/systems/roof/roof-system.js +0 -797
  65. package/dist/systems/slab/slab-system.d.ts +0 -8
  66. package/dist/systems/slab/slab-system.d.ts.map +0 -1
  67. package/dist/systems/slab/slab-system.js +0 -214
  68. package/dist/systems/stair/stair-system.d.ts +0 -2
  69. package/dist/systems/stair/stair-system.d.ts.map +0 -1
  70. package/dist/systems/stair/stair-system.js +0 -776
  71. package/dist/systems/wall/wall-system.d.ts +0 -12
  72. package/dist/systems/wall/wall-system.d.ts.map +0 -1
  73. package/dist/systems/wall/wall-system.js +0 -455
  74. package/dist/systems/window/window-system.d.ts +0 -2
  75. package/dist/systems/window/window-system.d.ts.map +0 -1
  76. 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,2 +0,0 @@
1
- export declare const WindowSystem: () => null;
2
- //# sourceMappingURL=window-system.d.ts.map
@@ -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
- }