@pascal-app/viewer 0.6.0 → 0.7.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 (103) hide show
  1. package/dist/components/error-boundary.d.ts +7 -3
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/error-boundary.js +3 -1
  4. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -1
  5. package/dist/components/renderers/ceiling/ceiling-renderer.js +19 -3
  6. package/dist/components/renderers/column/column-renderer.d.ts +5 -0
  7. package/dist/components/renderers/column/column-renderer.d.ts.map +1 -0
  8. package/dist/components/renderers/column/column-renderer.js +594 -0
  9. package/dist/components/renderers/door/door-renderer.d.ts.map +1 -1
  10. package/dist/components/renderers/door/door-renderer.js +4 -9
  11. package/dist/components/renderers/fence/fence-renderer.d.ts.map +1 -1
  12. package/dist/components/renderers/guide/guide-renderer.js +1 -1
  13. package/dist/components/renderers/item/item-renderer.d.ts.map +1 -1
  14. package/dist/components/renderers/item/item-renderer.js +2 -1
  15. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  16. package/dist/components/renderers/node-renderer.js +3 -1
  17. package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -1
  18. package/dist/components/renderers/roof/roof-renderer.js +1 -13
  19. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +1 -1
  20. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +5 -20
  21. package/dist/components/renderers/slab/slab-renderer.d.ts.map +1 -1
  22. package/dist/components/renderers/slab/slab-renderer.js +39 -27
  23. package/dist/components/renderers/spawn/spawn-renderer.d.ts +5 -0
  24. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +1 -0
  25. package/dist/components/renderers/spawn/spawn-renderer.js +32 -0
  26. package/dist/components/renderers/stair/stair-renderer.d.ts.map +1 -1
  27. package/dist/components/renderers/stair/stair-renderer.js +5 -23
  28. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +1 -1
  29. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +5 -22
  30. package/dist/components/viewer/index.d.ts +2 -0
  31. package/dist/components/viewer/index.d.ts.map +1 -1
  32. package/dist/components/viewer/index.js +88 -18
  33. package/dist/components/viewer/post-processing.d.ts +13 -1
  34. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  35. package/dist/components/viewer/post-processing.js +71 -14
  36. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  37. package/dist/components/viewer/selection-manager.js +19 -2
  38. package/dist/hooks/use-node-events.d.ts +9 -1
  39. package/dist/hooks/use-node-events.d.ts.map +1 -1
  40. package/dist/index.d.ts +6 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +5 -2
  43. package/dist/lib/materials.d.ts +3 -0
  44. package/dist/lib/materials.d.ts.map +1 -1
  45. package/dist/lib/materials.js +14 -0
  46. package/dist/lib/merged-outline-node.d.ts +2 -0
  47. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  48. package/dist/lib/merged-outline-node.js +25 -5
  49. package/dist/store/use-viewer.d.ts +4 -4
  50. package/dist/store/use-viewer.d.ts.map +1 -1
  51. package/dist/store/use-viewer.js +2 -2
  52. package/dist/systems/ceiling/ceiling-system.d.ts +8 -0
  53. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -0
  54. package/dist/systems/ceiling/ceiling-system.js +91 -0
  55. package/dist/systems/column/column-geometry.d.ts +22 -0
  56. package/dist/systems/column/column-geometry.d.ts.map +1 -0
  57. package/dist/systems/column/column-geometry.js +108 -0
  58. package/dist/systems/door/door-animation-system.d.ts +2 -0
  59. package/dist/systems/door/door-animation-system.d.ts.map +1 -0
  60. package/dist/systems/door/door-animation-system.js +53 -0
  61. package/dist/systems/door/door-system.d.ts +2 -0
  62. package/dist/systems/door/door-system.d.ts.map +1 -0
  63. package/dist/systems/door/door-system.js +867 -0
  64. package/dist/systems/fence/fence-system.d.ts +2 -0
  65. package/dist/systems/fence/fence-system.d.ts.map +1 -0
  66. package/dist/systems/fence/fence-system.js +185 -0
  67. package/dist/systems/item/item-system.d.ts +2 -0
  68. package/dist/systems/item/item-system.d.ts.map +1 -0
  69. package/dist/systems/item/item-system.js +44 -0
  70. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts +14 -0
  71. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts.map +1 -0
  72. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.js +155 -0
  73. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts +6 -0
  74. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts.map +1 -0
  75. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.js +75 -0
  76. package/dist/systems/item-mesh-metadata/sync-request.d.ts +7 -0
  77. package/dist/systems/item-mesh-metadata/sync-request.d.ts.map +1 -0
  78. package/dist/systems/item-mesh-metadata/sync-request.js +25 -0
  79. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  80. package/dist/systems/roof/roof-materials.js +18 -1
  81. package/dist/systems/roof/roof-system.d.ts +16 -0
  82. package/dist/systems/roof/roof-system.d.ts.map +1 -0
  83. package/dist/systems/roof/roof-system.js +797 -0
  84. package/dist/systems/slab/slab-system.d.ts +8 -0
  85. package/dist/systems/slab/slab-system.d.ts.map +1 -0
  86. package/dist/systems/slab/slab-system.js +155 -0
  87. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  88. package/dist/systems/stair/stair-materials.js +25 -2
  89. package/dist/systems/stair/stair-system.d.ts +2 -0
  90. package/dist/systems/stair/stair-system.d.ts.map +1 -0
  91. package/dist/systems/stair/stair-system.js +772 -0
  92. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  93. package/dist/systems/wall/wall-materials.js +2 -2
  94. package/dist/systems/wall/wall-system.d.ts +11 -0
  95. package/dist/systems/wall/wall-system.d.ts.map +1 -0
  96. package/dist/systems/wall/wall-system.js +592 -0
  97. package/dist/systems/window/window-animation-system.d.ts +2 -0
  98. package/dist/systems/window/window-animation-system.d.ts.map +1 -0
  99. package/dist/systems/window/window-animation-system.js +135 -0
  100. package/dist/systems/window/window-system.d.ts +12 -0
  101. package/dist/systems/window/window-system.d.ts.map +1 -0
  102. package/dist/systems/window/window-system.js +1430 -0
  103. package/package.json +4 -2
@@ -1 +1 @@
1
- {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAwB5C,MAAM,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,kBAAkB,EAAE,iBAAiB,CAAA;IACrC,oBAAoB,EAAE,iBAAiB,CAAA;IACvC,YAAY,EAAE,MAAM,CAAA;CACrB;AA4GD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAS9D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CA2DrE;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,iBAAiB,CAE7E"}
1
+ {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAwB5C,MAAM,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,kBAAkB,EAAE,iBAAiB,CAAA;IACrC,oBAAoB,EAAE,iBAAiB,CAAA;IACvC,YAAY,EAAE,MAAM,CAAA;CACrB;AA4GD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAS9D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CA2DrE;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,iBAAiB,CAE7E"}
@@ -1,8 +1,8 @@
1
- import { baseMaterial, getEffectiveWallSurfaceMaterial, getMaterialPresetByRef, getWallSurfaceMaterialSignature, resolveMaterial, } from '@pascal-app/core';
1
+ import { getEffectiveWallSurfaceMaterial, getMaterialPresetByRef, getWallSurfaceMaterialSignature, resolveMaterial, } from '@pascal-app/core';
2
2
  import { Color } from 'three';
3
3
  import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, vec2 } from 'three/tsl';
4
4
  import { MeshStandardNodeMaterial } from 'three/webgpu';
5
- import { createMaterial, createMaterialFromPresetRef } from '../../lib/materials';
5
+ import { baseMaterial, createMaterial, createMaterialFromPresetRef } from '../../lib/materials';
6
6
  const DEFAULT_WALL_COLOR = '#f2f0ed';
7
7
  const WALL_HIGHLIGHT_PROFILES = {
8
8
  delete: {
@@ -0,0 +1,11 @@
1
+ import { type AnyNode, type WallMiterData, type WallNode } from '@pascal-app/core';
2
+ import * as THREE from 'three';
3
+ export declare const WallSystem: () => null;
4
+ /**
5
+ * Generates extruded wall geometry with mitering and cutouts
6
+ *
7
+ * Key insight from demo: polygon is built in WORLD coordinates first,
8
+ * then we transform to wall-local for the 3D mesh.
9
+ */
10
+ export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
11
+ //# sourceMappingURL=wall-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAkBZ,KAAK,aAAa,EAClB,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA6R9B,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,oFA0GlB"}
@@ -0,0 +1,592 @@
1
+ import { calculateLevelMiters, DEFAULT_WALL_HEIGHT, getAdjacentWallIds, getWallCurveFrameAt, getWallMiterBoundaryPoints, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, sceneRegistry, spatialGridManager, useScene, } from '@pascal-app/core';
2
+ import { useFrame } from '@react-three/fiber';
3
+ import * as THREE from 'three';
4
+ import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
5
+ import { computeBoundsTree } from 'three-mesh-bvh';
6
+ // Reusable CSG evaluator for better performance
7
+ const csgEvaluator = new Evaluator();
8
+ const CURVED_WALL_3D_ENDPOINT_INSET = 0.0015;
9
+ const WALL_FACE_NORMAL_Y_EPSILON = 0.6;
10
+ const WALL_FACE_EDGE_DISTANCE_EPSILON = 0.003;
11
+ function computeGeometryBoundsTree(geometry) {
12
+ ;
13
+ geometry.computeBoundsTree = computeBoundsTree;
14
+ geometry.computeBoundsTree({ maxLeafSize: 10 });
15
+ }
16
+ function ensureUv2Attribute(geometry) {
17
+ const uv = geometry.getAttribute('uv');
18
+ if (!uv)
19
+ return;
20
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
21
+ }
22
+ function insetCurvedWallBoundaryPointsFor3D(wall, boundaryPoints, miterData) {
23
+ if (!boundaryPoints || !isCurvedWall(wall)) {
24
+ return boundaryPoints;
25
+ }
26
+ const insetDistance = Math.min(CURVED_WALL_3D_ENDPOINT_INSET, Math.max((wall.thickness ?? 0.1) * 0.01, 0.0005));
27
+ if (insetDistance <= 0) {
28
+ return boundaryPoints;
29
+ }
30
+ const next = { ...boundaryPoints };
31
+ const startJunction = miterData.junctions.get(pointToKey({ x: wall.start[0], y: wall.start[1] }));
32
+ const endJunction = miterData.junctions.get(pointToKey({ x: wall.end[0], y: wall.end[1] }));
33
+ if (startJunction && startJunction.connectedWalls.length > 1) {
34
+ const frame = getWallCurveFrameAt(wall, 0);
35
+ next.startLeft = {
36
+ x: next.startLeft.x + frame.tangent.x * insetDistance,
37
+ y: next.startLeft.y + frame.tangent.y * insetDistance,
38
+ };
39
+ next.startRight = {
40
+ x: next.startRight.x + frame.tangent.x * insetDistance,
41
+ y: next.startRight.y + frame.tangent.y * insetDistance,
42
+ };
43
+ }
44
+ if (endJunction && endJunction.connectedWalls.length > 1) {
45
+ const frame = getWallCurveFrameAt(wall, 1);
46
+ next.endLeft = {
47
+ x: next.endLeft.x - frame.tangent.x * insetDistance,
48
+ y: next.endLeft.y - frame.tangent.y * insetDistance,
49
+ };
50
+ next.endRight = {
51
+ x: next.endRight.x - frame.tangent.x * insetDistance,
52
+ y: next.endRight.y - frame.tangent.y * insetDistance,
53
+ };
54
+ }
55
+ return next;
56
+ }
57
+ function addTaggedWallBoundaryEdge(edges, points, startIndex, endIndex, tag) {
58
+ const start = points[startIndex];
59
+ const end = points[endIndex];
60
+ if (!(start && end))
61
+ return;
62
+ if (Math.hypot(end.x - start.x, end.z - start.z) < 1e-6)
63
+ return;
64
+ edges.push({
65
+ start: new THREE.Vector2(start.x, start.z),
66
+ end: new THREE.Vector2(end.x, end.z),
67
+ tag,
68
+ });
69
+ }
70
+ function buildTaggedWallBoundaryEdges(wall, localPoints, miterData) {
71
+ if (localPoints.length < 2)
72
+ return [];
73
+ const edges = [];
74
+ if (isCurvedWall(wall)) {
75
+ const sidePointCount = Math.floor(localPoints.length / 2);
76
+ if (sidePointCount < 2)
77
+ return edges;
78
+ for (let index = 0; index < sidePointCount - 1; index += 1) {
79
+ addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'back');
80
+ }
81
+ addTaggedWallBoundaryEdge(edges, localPoints, sidePointCount - 1, sidePointCount, 'base');
82
+ for (let index = sidePointCount; index < localPoints.length - 1; index += 1) {
83
+ addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'front');
84
+ }
85
+ addTaggedWallBoundaryEdge(edges, localPoints, localPoints.length - 1, 0, 'base');
86
+ return edges;
87
+ }
88
+ const startKey = pointToKey({ x: wall.start[0], y: wall.start[1] });
89
+ const startJunction = miterData.junctionData.get(startKey)?.get(wall.id);
90
+ const startLeftIndex = startJunction ? localPoints.length - 2 : localPoints.length - 1;
91
+ const endLeftIndex = startJunction ? localPoints.length - 3 : localPoints.length - 2;
92
+ addTaggedWallBoundaryEdge(edges, localPoints, 0, 1, 'back');
93
+ for (let index = 1; index < endLeftIndex; index += 1) {
94
+ addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'base');
95
+ }
96
+ addTaggedWallBoundaryEdge(edges, localPoints, endLeftIndex, startLeftIndex, 'front');
97
+ for (let index = startLeftIndex; index < localPoints.length - 1; index += 1) {
98
+ addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'base');
99
+ }
100
+ addTaggedWallBoundaryEdge(edges, localPoints, localPoints.length - 1, 0, 'base');
101
+ return edges;
102
+ }
103
+ function distanceToWallBoundaryEdge(point, edge) {
104
+ const edgeDx = edge.end.x - edge.start.x;
105
+ const edgeDz = edge.end.y - edge.start.y;
106
+ const pointDx = point.x - edge.start.x;
107
+ const pointDz = point.y - edge.start.y;
108
+ const edgeLengthSq = edgeDx * edgeDx + edgeDz * edgeDz;
109
+ if (edgeLengthSq < 1e-12) {
110
+ return point.distanceTo(edge.start);
111
+ }
112
+ const t = THREE.MathUtils.clamp((pointDx * edgeDx + pointDz * edgeDz) / edgeLengthSq, 0, 1);
113
+ const closestX = edge.start.x + edgeDx * t;
114
+ const closestZ = edge.start.y + edgeDz * t;
115
+ return Math.hypot(point.x - closestX, point.y - closestZ);
116
+ }
117
+ function getWallFaceMaterialIndex(wall, face) {
118
+ const semantic = face === 'front' ? wall.frontSide : wall.backSide;
119
+ const fallback = face === 'front' ? 1 : 2;
120
+ if (semantic === 'interior')
121
+ return 1;
122
+ if (semantic === 'exterior')
123
+ return 2;
124
+ return fallback;
125
+ }
126
+ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
127
+ const position = geometry.getAttribute('position');
128
+ if (!position)
129
+ return;
130
+ const index = geometry.getIndex();
131
+ const triangleCount = index ? Math.floor(index.count / 3) : Math.floor(position.count / 3);
132
+ if (triangleCount === 0) {
133
+ geometry.clearGroups();
134
+ return;
135
+ }
136
+ const triangleMaterials = new Array(triangleCount).fill(0);
137
+ const a = new THREE.Vector3();
138
+ const b = new THREE.Vector3();
139
+ const c = new THREE.Vector3();
140
+ const ab = new THREE.Vector3();
141
+ const ac = new THREE.Vector3();
142
+ const normal = new THREE.Vector3();
143
+ const centroid = new THREE.Vector3();
144
+ const projectedCentroid = new THREE.Vector2();
145
+ const maxBoundaryDistance = Math.max(getWallThickness(wall) * 0.02, WALL_FACE_EDGE_DISTANCE_EPSILON);
146
+ for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
147
+ const baseIndex = triangleIndex * 3;
148
+ const ia = index ? index.getX(baseIndex) : baseIndex;
149
+ const ib = index ? index.getX(baseIndex + 1) : baseIndex + 1;
150
+ const ic = index ? index.getX(baseIndex + 2) : baseIndex + 2;
151
+ a.fromBufferAttribute(position, ia);
152
+ b.fromBufferAttribute(position, ib);
153
+ c.fromBufferAttribute(position, ic);
154
+ ab.subVectors(b, a);
155
+ ac.subVectors(c, a);
156
+ normal.crossVectors(ab, ac);
157
+ if (normal.lengthSq() < 1e-12) {
158
+ triangleMaterials[triangleIndex] = 0;
159
+ continue;
160
+ }
161
+ normal.normalize();
162
+ if (Math.abs(normal.y) >= WALL_FACE_NORMAL_Y_EPSILON) {
163
+ triangleMaterials[triangleIndex] = 0;
164
+ continue;
165
+ }
166
+ centroid
167
+ .copy(a)
168
+ .add(b)
169
+ .add(c)
170
+ .multiplyScalar(1 / 3);
171
+ projectedCentroid.set(centroid.x, centroid.z);
172
+ let nearestTag = null;
173
+ let nearestDistance = Number.POSITIVE_INFINITY;
174
+ for (const edge of boundaryEdges) {
175
+ const distance = distanceToWallBoundaryEdge(projectedCentroid, edge);
176
+ if (distance < nearestDistance) {
177
+ nearestDistance = distance;
178
+ nearestTag = edge.tag;
179
+ }
180
+ }
181
+ if (!nearestTag || nearestDistance > maxBoundaryDistance) {
182
+ triangleMaterials[triangleIndex] = 0;
183
+ continue;
184
+ }
185
+ if (nearestTag === 'base') {
186
+ triangleMaterials[triangleIndex] = 0;
187
+ continue;
188
+ }
189
+ triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag);
190
+ }
191
+ geometry.clearGroups();
192
+ let currentMaterial = triangleMaterials[0] ?? 0;
193
+ let groupStart = 0;
194
+ for (let triangleIndex = 1; triangleIndex < triangleCount; triangleIndex += 1) {
195
+ const materialIndex = triangleMaterials[triangleIndex] ?? 0;
196
+ if (materialIndex === currentMaterial)
197
+ continue;
198
+ geometry.addGroup(groupStart * 3, (triangleIndex - groupStart) * 3, currentMaterial);
199
+ groupStart = triangleIndex;
200
+ currentMaterial = materialIndex;
201
+ }
202
+ geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
203
+ }
204
+ // ============================================================================
205
+ // WALL SYSTEM
206
+ // ============================================================================
207
+ let useFrameNb = 0;
208
+ export const WallSystem = () => {
209
+ const dirtyNodes = useScene((state) => state.dirtyNodes);
210
+ const clearDirty = useScene((state) => state.clearDirty);
211
+ useFrame(() => {
212
+ if (dirtyNodes.size === 0)
213
+ return;
214
+ const nodes = useScene.getState().nodes;
215
+ // Collect dirty walls and their levels
216
+ const dirtyWallsByLevel = new Map();
217
+ useFrameNb += 1;
218
+ dirtyNodes.forEach((id) => {
219
+ const node = nodes[id];
220
+ if (!node || node.type !== 'wall')
221
+ return;
222
+ const levelId = node.parentId;
223
+ if (!levelId)
224
+ return;
225
+ if (!dirtyWallsByLevel.has(levelId)) {
226
+ dirtyWallsByLevel.set(levelId, new Set());
227
+ }
228
+ dirtyWallsByLevel.get(levelId)?.add(id);
229
+ });
230
+ // Process each level that has dirty walls
231
+ for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
232
+ const levelWalls = getLevelWalls(levelId);
233
+ const miterData = calculateLevelMiters(levelWalls);
234
+ // Update dirty walls
235
+ for (const wallId of dirtyWallIds) {
236
+ const mesh = sceneRegistry.nodes.get(wallId);
237
+ if (mesh) {
238
+ updateWallGeometry(wallId, miterData);
239
+ clearDirty(wallId);
240
+ }
241
+ // If mesh not found, keep it dirty for next frame
242
+ }
243
+ // Update adjacent walls that share junctions
244
+ const adjacentWallIds = getAdjacentWallIds(levelWalls, dirtyWallIds);
245
+ for (const wallId of adjacentWallIds) {
246
+ if (!dirtyWallIds.has(wallId)) {
247
+ const mesh = sceneRegistry.nodes.get(wallId);
248
+ if (mesh) {
249
+ updateWallGeometry(wallId, miterData);
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }, 4);
255
+ return null;
256
+ };
257
+ /**
258
+ * Gets all walls that belong to a level
259
+ */
260
+ function getLevelWalls(levelId) {
261
+ const { nodes } = useScene.getState();
262
+ const level = nodes[levelId];
263
+ if (!level || level.type !== 'level')
264
+ return [];
265
+ const walls = [];
266
+ for (const childId of level.children) {
267
+ const child = nodes[childId];
268
+ if (child?.type === 'wall') {
269
+ walls.push(child);
270
+ }
271
+ }
272
+ return walls;
273
+ }
274
+ /**
275
+ * Updates the geometry for a single wall
276
+ */
277
+ function updateWallGeometry(wallId, miterData) {
278
+ const nodes = useScene.getState().nodes;
279
+ const node = nodes[wallId];
280
+ if (!node || node.type !== 'wall')
281
+ return;
282
+ const mesh = sceneRegistry.nodes.get(wallId);
283
+ if (!mesh)
284
+ return;
285
+ const levelId = resolveLevelId(node, nodes);
286
+ const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end);
287
+ const childrenIds = node.children || [];
288
+ const childrenNodes = childrenIds
289
+ .map((childId) => nodes[childId])
290
+ .filter((n) => n !== undefined);
291
+ const newGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation);
292
+ mesh.geometry.dispose();
293
+ mesh.geometry = newGeo;
294
+ // Update collision mesh
295
+ const collisionMesh = mesh.getObjectByName('collision-mesh');
296
+ if (collisionMesh) {
297
+ const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation);
298
+ collisionMesh.geometry.dispose();
299
+ collisionMesh.geometry = collisionGeo;
300
+ }
301
+ mesh.position.set(node.start[0], slabElevation, node.start[1]);
302
+ const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
303
+ mesh.rotation.y = -angle;
304
+ }
305
+ /**
306
+ * Generates extruded wall geometry with mitering and cutouts
307
+ *
308
+ * Key insight from demo: polygon is built in WORLD coordinates first,
309
+ * then we transform to wall-local for the 3D mesh.
310
+ */
311
+ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0) {
312
+ const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
313
+ const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
314
+ // Positive slab: shift the whole wall up (full height preserved)
315
+ // Negative slab: extend wall downward so top stays fixed at wallNode.height
316
+ const wallHeight = wallNode.height ?? DEFAULT_WALL_HEIGHT;
317
+ const height = slabElevation > 0 ? wallHeight : wallHeight - slabElevation;
318
+ const thickness = getWallThickness(wallNode);
319
+ // Wall direction and normal (exactly like demo)
320
+ const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y };
321
+ const L = Math.sqrt(v.x * v.x + v.y * v.y);
322
+ if (L < 1e-9) {
323
+ return new THREE.BufferGeometry();
324
+ }
325
+ const boundaryPoints = getWallMiterBoundaryPoints(wallNode, miterData);
326
+ const polyPoints = isCurvedWall(wallNode)
327
+ ? getWallSurfacePolygon(wallNode, 24, insetCurvedWallBoundaryPointsFor3D(wallNode, boundaryPoints, miterData) ?? undefined)
328
+ : getWallPlanFootprint(wallNode, miterData);
329
+ if (polyPoints.length < 3) {
330
+ return new THREE.BufferGeometry();
331
+ }
332
+ // Transform world coordinates to wall-local coordinates
333
+ // Wall-local: x along wall, z perpendicular (thickness direction)
334
+ const wallAngle = Math.atan2(v.y, v.x);
335
+ const cosA = Math.cos(-wallAngle);
336
+ const sinA = Math.sin(-wallAngle);
337
+ const worldToLocal = (worldPt) => {
338
+ const dx = worldPt.x - wallStart.x;
339
+ const dy = worldPt.y - wallStart.y;
340
+ return {
341
+ x: dx * cosA - dy * sinA,
342
+ z: dx * sinA + dy * cosA,
343
+ };
344
+ };
345
+ // Convert polygon to local coordinates
346
+ const localPoints = polyPoints.map(worldToLocal);
347
+ const boundaryEdges = buildTaggedWallBoundaryEdges(wallNode, localPoints, miterData);
348
+ // Build THREE.js shape
349
+ // Shape uses (x, y) where we map: shape.x = local.x, shape.y = -local.z
350
+ // The negation is needed because after rotateX(-PI/2), shape.y becomes -geometry.z
351
+ const footprint = new THREE.Shape();
352
+ footprint.moveTo(localPoints[0].x, -localPoints[0].z);
353
+ for (let i = 1; i < localPoints.length; i++) {
354
+ footprint.lineTo(localPoints[i].x, -localPoints[i].z);
355
+ }
356
+ footprint.closePath();
357
+ // Extrude along Z by height
358
+ const geometry = new THREE.ExtrudeGeometry(footprint, {
359
+ depth: height,
360
+ bevelEnabled: false,
361
+ });
362
+ // Rotate so extrusion direction (Z) becomes height direction (Y)
363
+ geometry.rotateX(-Math.PI / 2);
364
+ geometry.computeVertexNormals();
365
+ assignWallMaterialGroups(geometry, wallNode, boundaryEdges);
366
+ ensureUv2Attribute(geometry);
367
+ // Apply CSG subtraction for cutouts (doors/windows)
368
+ const cutoutBrushes = collectCutoutBrushes(wallNode, childrenNodes, thickness);
369
+ if (cutoutBrushes.length === 0) {
370
+ return geometry;
371
+ }
372
+ // Create wall brush from geometry
373
+ // Pre-compute BVH with new API to avoid deprecation warning
374
+ computeGeometryBoundsTree(geometry);
375
+ const wallBrush = new Brush(geometry);
376
+ wallBrush.updateMatrixWorld();
377
+ // Subtract each cutout from the wall
378
+ let resultBrush = wallBrush;
379
+ for (const cutoutBrush of cutoutBrushes) {
380
+ cutoutBrush.updateMatrixWorld();
381
+ const newResult = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
382
+ if (resultBrush !== wallBrush) {
383
+ resultBrush.geometry.dispose();
384
+ }
385
+ resultBrush = newResult;
386
+ }
387
+ // Clean up
388
+ wallBrush.geometry.dispose();
389
+ for (const brush of cutoutBrushes) {
390
+ brush.geometry.dispose();
391
+ }
392
+ const resultGeometry = resultBrush.geometry;
393
+ resultGeometry.computeVertexNormals();
394
+ assignWallMaterialGroups(resultGeometry, wallNode, boundaryEdges);
395
+ ensureUv2Attribute(resultGeometry);
396
+ return resultGeometry;
397
+ }
398
+ /**
399
+ * Collects cutout brushes from child items for CSG subtraction
400
+ * The cutout mesh is a plane, so we extrude it into a box that goes through the wall
401
+ */
402
+ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
403
+ const brushes = [];
404
+ const wallMesh = sceneRegistry.nodes.get(wallNode.id);
405
+ if (!wallMesh)
406
+ return brushes;
407
+ // Get wall's world matrix inverse to transform cutouts to wall-local space
408
+ wallMesh.updateMatrixWorld();
409
+ const wallMatrixInverse = wallMesh.matrixWorld.clone().invert();
410
+ for (const child of childrenNodes) {
411
+ if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door')
412
+ continue;
413
+ if ((child.type === 'door' && child.openingKind === 'opening') ||
414
+ (child.type === 'door' &&
415
+ child.openingKind === 'door' &&
416
+ (child.openingShape === 'arch' || child.openingShape === 'rounded')) ||
417
+ (child.type === 'window' && child.openingKind === 'opening') ||
418
+ (child.type === 'window' &&
419
+ child.openingKind === 'window' &&
420
+ (child.openingShape === 'arch' || child.openingShape === 'rounded'))) {
421
+ brushes.push(createShapedOpeningCutoutBrush(child, wallThickness));
422
+ continue;
423
+ }
424
+ const childMesh = sceneRegistry.nodes.get(child.id);
425
+ if (!childMesh)
426
+ continue;
427
+ const cutoutMesh = childMesh.getObjectByName('cutout');
428
+ if (!cutoutMesh)
429
+ continue;
430
+ // Get the cutout's bounding box in world space
431
+ cutoutMesh.updateMatrixWorld();
432
+ const positions = cutoutMesh.geometry?.attributes?.position;
433
+ if (!positions)
434
+ continue;
435
+ // Calculate bounds in wall-local space
436
+ const v3 = new THREE.Vector3();
437
+ let minX = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY;
438
+ let minY = Number.POSITIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY;
439
+ for (let i = 0; i < positions.count; i++) {
440
+ v3.fromBufferAttribute(positions, i);
441
+ v3.applyMatrix4(cutoutMesh.matrixWorld);
442
+ v3.applyMatrix4(wallMatrixInverse);
443
+ minX = Math.min(minX, v3.x);
444
+ maxX = Math.max(maxX, v3.x);
445
+ minY = Math.min(minY, v3.y);
446
+ maxY = Math.max(maxY, v3.y);
447
+ }
448
+ if (!Number.isFinite(minX))
449
+ continue;
450
+ // Create a box geometry that extends through the wall thickness
451
+ const width = maxX - minX;
452
+ const height = maxY - minY;
453
+ const depth = wallThickness * 2; // Extend beyond wall to ensure clean cut
454
+ const boxGeo = new THREE.BoxGeometry(width, height, depth);
455
+ // Position box at the center of the cutout
456
+ boxGeo.translate(minX + width / 2, minY + height / 2, 0);
457
+ // Pre-compute BVH with new API to avoid deprecation warning
458
+ computeGeometryBoundsTree(boxGeo);
459
+ const brush = new Brush(boxGeo);
460
+ brushes.push(brush);
461
+ }
462
+ return brushes;
463
+ }
464
+ function createShapedOpeningCutoutBrush(opening, wallThickness) {
465
+ const shape = createShapedOpeningCutoutShape(opening);
466
+ const depth = wallThickness * 2;
467
+ const bevelSize = opening.openingShape === 'rounded'
468
+ ? Math.min(Math.max(opening.openingRevealRadius ?? 0.025, 0), Math.max(wallThickness * 0.45, 0.001), Math.max((opening.cornerRadius ?? 0.15) * 0.45, 0.001))
469
+ : 0;
470
+ const geometry = new THREE.ExtrudeGeometry(shape, {
471
+ depth,
472
+ bevelEnabled: bevelSize > 0,
473
+ bevelSegments: bevelSize > 0 ? 8 : 0,
474
+ bevelSize,
475
+ bevelThickness: bevelSize,
476
+ curveSegments: 24,
477
+ });
478
+ geometry.translate(0, 0, -depth / 2);
479
+ computeGeometryBoundsTree(geometry);
480
+ return new Brush(geometry);
481
+ }
482
+ function createShapedOpeningCutoutShape(opening) {
483
+ const halfWidth = opening.width / 2;
484
+ const bottom = opening.position[1] - opening.height / 2;
485
+ const top = opening.position[1] + opening.height / 2;
486
+ const centerX = opening.position[0];
487
+ const left = centerX - halfWidth;
488
+ const right = centerX + halfWidth;
489
+ const width = Math.max(opening.width, 1e-6);
490
+ const height = Math.max(opening.height, 1e-6);
491
+ const shape = new THREE.Shape();
492
+ if (opening.openingShape === 'arch') {
493
+ const archHeight = Math.min(Math.max(opening.archHeight ?? width / 2, 0.01), height);
494
+ const springY = top - archHeight;
495
+ const segments = 32;
496
+ shape.moveTo(left, bottom);
497
+ shape.lineTo(right, bottom);
498
+ shape.lineTo(right, springY);
499
+ for (let index = 1; index <= segments; index += 1) {
500
+ const x = right + (left - right) * (index / segments);
501
+ const normalizedX = Math.min(Math.abs((x - centerX) / halfWidth), 1);
502
+ const y = springY + archHeight * Math.sqrt(Math.max(1 - normalizedX * normalizedX, 0));
503
+ shape.lineTo(x, y);
504
+ }
505
+ shape.lineTo(left, bottom);
506
+ shape.closePath();
507
+ return shape;
508
+ }
509
+ if (opening.openingShape === 'rounded') {
510
+ const radii = getRoundedOpeningRadii(opening, width, height);
511
+ applyRoundedOpeningShape(shape, left, right, bottom, top, radii);
512
+ return shape;
513
+ }
514
+ shape.moveTo(left, bottom);
515
+ shape.lineTo(right, bottom);
516
+ shape.lineTo(right, top);
517
+ shape.lineTo(left, top);
518
+ shape.closePath();
519
+ return shape;
520
+ }
521
+ function getRoundedOpeningRadii(opening, width, height) {
522
+ if (opening.type !== 'window') {
523
+ if (opening.openingRadiusMode === 'individual') {
524
+ const [topLeft = 0, topRight = 0] = opening.openingTopRadii ?? [0.15, 0.15];
525
+ return normalizeCornerRadii({
526
+ topLeft: Math.max(topLeft, 0),
527
+ topRight: Math.max(topRight, 0),
528
+ bottomRight: 0,
529
+ bottomLeft: 0,
530
+ }, width, height);
531
+ }
532
+ const maxRadius = Math.min(width / 2, height);
533
+ const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
534
+ return { topLeft: radius, topRight: radius, bottomRight: 0, bottomLeft: 0 };
535
+ }
536
+ if (opening.openingRadiusMode === 'individual') {
537
+ const [topLeft = 0, topRight = 0, bottomRight = 0, bottomLeft = 0] = opening.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15];
538
+ return normalizeCornerRadii({
539
+ topLeft: Math.max(topLeft, 0),
540
+ topRight: Math.max(topRight, 0),
541
+ bottomRight: Math.max(bottomRight, 0),
542
+ bottomLeft: Math.max(bottomLeft, 0),
543
+ }, width, height);
544
+ }
545
+ const maxRadius = Math.min(width / 2, height / 2);
546
+ const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
547
+ return { topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius };
548
+ }
549
+ function normalizeCornerRadii(radii, width, height) {
550
+ const next = { ...radii };
551
+ const maxScale = Math.min(1, width / Math.max(next.topLeft + next.topRight, 1e-6), width / Math.max(next.bottomLeft + next.bottomRight, 1e-6), height / Math.max(next.topLeft + next.bottomLeft, 1e-6), height / Math.max(next.topRight + next.bottomRight, 1e-6));
552
+ if (maxScale < 1) {
553
+ next.topLeft *= maxScale;
554
+ next.topRight *= maxScale;
555
+ next.bottomRight *= maxScale;
556
+ next.bottomLeft *= maxScale;
557
+ }
558
+ return next;
559
+ }
560
+ function applyRoundedOpeningShape(shape, left, right, bottom, top, radii) {
561
+ const { topLeft, topRight, bottomRight, bottomLeft } = radii;
562
+ shape.moveTo(left + bottomLeft, bottom);
563
+ shape.lineTo(right - bottomRight, bottom);
564
+ if (bottomRight > 1e-6) {
565
+ shape.absarc(right - bottomRight, bottom + bottomRight, bottomRight, -Math.PI / 2, 0, false);
566
+ }
567
+ else {
568
+ shape.lineTo(right, bottom);
569
+ }
570
+ shape.lineTo(right, top - topRight);
571
+ if (topRight > 1e-6) {
572
+ shape.absarc(right - topRight, top - topRight, topRight, 0, Math.PI / 2, false);
573
+ }
574
+ else {
575
+ shape.lineTo(right, top);
576
+ }
577
+ shape.lineTo(left + topLeft, top);
578
+ if (topLeft > 1e-6) {
579
+ shape.absarc(left + topLeft, top - topLeft, topLeft, Math.PI / 2, Math.PI, false);
580
+ }
581
+ else {
582
+ shape.lineTo(left, top);
583
+ }
584
+ shape.lineTo(left, bottom + bottomLeft);
585
+ if (bottomLeft > 1e-6) {
586
+ shape.absarc(left + bottomLeft, bottom + bottomLeft, bottomLeft, Math.PI, Math.PI * 1.5, false);
587
+ }
588
+ else {
589
+ shape.lineTo(left, bottom);
590
+ }
591
+ shape.closePath();
592
+ }
@@ -0,0 +1,2 @@
1
+ export declare const WindowAnimationSystem: () => null;
2
+ //# sourceMappingURL=window-animation-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"window-animation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-animation-system.tsx"],"names":[],"mappings":"AA0HA,eAAO,MAAM,qBAAqB,YA+CjC,CAAA"}