@pascal-app/viewer 0.9.1 → 0.9.2

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 (176) hide show
  1. package/dist/components/error-boundary.d.ts +6 -0
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/error-boundary.js +6 -0
  4. package/dist/components/renderers/node-renderer.d.ts +5 -1
  5. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  6. package/dist/components/renderers/node-renderer.js +5 -2
  7. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  8. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  9. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  10. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  11. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  12. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  13. package/dist/components/viewer/frame-limiter.js +39 -4
  14. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  15. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-interactive.js +394 -0
  17. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  19. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  20. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  22. package/dist/components/viewer/glb-replace-instances.js +61 -0
  23. package/dist/components/viewer/glb-scene.d.ts +54 -0
  24. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  25. package/dist/components/viewer/glb-scene.js +920 -0
  26. package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
  29. package/dist/components/viewer/index.d.ts +19 -2
  30. package/dist/components/viewer/index.d.ts.map +1 -1
  31. package/dist/components/viewer/index.js +104 -12
  32. package/dist/components/viewer/lights.d.ts.map +1 -1
  33. package/dist/components/viewer/lights.js +36 -5
  34. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  35. package/dist/components/viewer/perf-monitor.js +17 -2
  36. package/dist/components/viewer/post-processing.d.ts +7 -1
  37. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  38. package/dist/components/viewer/post-processing.js +179 -24
  39. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  40. package/dist/components/viewer/registered-systems.js +6 -4
  41. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  42. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  43. package/dist/components/viewer/scene-bvh.js +3 -0
  44. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  46. package/dist/components/viewer/scene-bvh.test.js +13 -0
  47. package/dist/components/viewer/scene-environment.d.ts +2 -0
  48. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  49. package/dist/components/viewer/scene-environment.js +79 -0
  50. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  51. package/dist/components/viewer/selection-manager.js +18 -10
  52. package/dist/components/viewer/walkthrough-controls.d.ts +1 -0
  53. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  54. package/dist/components/viewer/walkthrough-controls.js +20 -0
  55. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  56. package/dist/hooks/use-gltf-ktx2.js +3 -22
  57. package/dist/hooks/use-node-events.d.ts +1 -1
  58. package/dist/hooks/use-node-events.d.ts.map +1 -1
  59. package/dist/hooks/use-node-events.js +22 -14
  60. package/dist/index.d.ts +19 -8
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +22 -8
  63. package/dist/lib/backdrop.d.ts +33 -0
  64. package/dist/lib/backdrop.d.ts.map +1 -0
  65. package/dist/lib/backdrop.js +92 -0
  66. package/dist/lib/box-uv.d.ts +13 -0
  67. package/dist/lib/box-uv.d.ts.map +1 -0
  68. package/dist/lib/box-uv.js +32 -0
  69. package/dist/lib/csg-utils.d.ts +3 -9
  70. package/dist/lib/csg-utils.d.ts.map +1 -1
  71. package/dist/lib/csg-utils.js +65 -5
  72. package/dist/lib/csg-utils.test.d.ts +2 -0
  73. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  74. package/dist/lib/csg-utils.test.js +45 -0
  75. package/dist/lib/edge-style.d.ts +1 -0
  76. package/dist/lib/edge-style.d.ts.map +1 -1
  77. package/dist/lib/edge-style.js +26 -6
  78. package/dist/lib/ink-edges.d.ts +1 -1
  79. package/dist/lib/ink-edges.d.ts.map +1 -1
  80. package/dist/lib/ink-edges.js +13 -7
  81. package/dist/lib/ktx2-loader.d.ts +43 -0
  82. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  83. package/dist/lib/ktx2-loader.js +136 -0
  84. package/dist/lib/layers.d.ts +10 -0
  85. package/dist/lib/layers.d.ts.map +1 -1
  86. package/dist/lib/layers.js +10 -0
  87. package/dist/lib/materials.d.ts +14 -1
  88. package/dist/lib/materials.d.ts.map +1 -1
  89. package/dist/lib/materials.js +103 -26
  90. package/dist/lib/scene-themes.d.ts +1 -0
  91. package/dist/lib/scene-themes.d.ts.map +1 -1
  92. package/dist/lib/scene-themes.js +25 -16
  93. package/dist/lib/shadow-only.d.ts +4 -0
  94. package/dist/lib/shadow-only.d.ts.map +1 -0
  95. package/dist/lib/shadow-only.js +36 -0
  96. package/dist/lib/tsl-compat.d.ts +12 -0
  97. package/dist/lib/tsl-compat.d.ts.map +1 -0
  98. package/dist/lib/tsl-compat.js +13 -0
  99. package/dist/store/use-viewer.d.ts +60 -18
  100. package/dist/store/use-viewer.d.ts.map +1 -1
  101. package/dist/store/use-viewer.js +175 -3
  102. package/dist/store/use-viewer.test.d.ts +2 -0
  103. package/dist/store/use-viewer.test.d.ts.map +1 -0
  104. package/dist/store/use-viewer.test.js +42 -0
  105. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  106. package/dist/systems/ceiling/ceiling-system.js +4 -1
  107. package/dist/systems/door/door-animation.test.d.ts +2 -0
  108. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  109. package/dist/systems/door/door-animation.test.js +51 -0
  110. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  111. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  112. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  113. package/dist/systems/door/door-system.d.ts +24 -0
  114. package/dist/systems/door/door-system.d.ts.map +1 -1
  115. package/dist/systems/door/door-system.js +494 -161
  116. package/dist/systems/fence/fence-system.d.ts +17 -0
  117. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  118. package/dist/systems/fence/fence-system.js +221 -53
  119. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  120. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  121. package/dist/systems/geometry/geometry-system.js +95 -26
  122. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  123. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  124. package/dist/systems/geometry/geometry-system.test.js +30 -0
  125. package/dist/systems/interactive/control-widget.d.ts +10 -0
  126. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  127. package/dist/systems/interactive/control-widget.js +35 -0
  128. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  129. package/dist/systems/interactive/interactive-system.js +55 -56
  130. package/dist/systems/item/item-system.d.ts.map +1 -1
  131. package/dist/systems/item/item-system.js +20 -6
  132. package/dist/systems/level/level-system.d.ts.map +1 -1
  133. package/dist/systems/level/level-system.js +28 -4
  134. package/dist/systems/level/level-utils.d.ts +0 -3
  135. package/dist/systems/level/level-utils.d.ts.map +1 -1
  136. package/dist/systems/level/level-utils.js +2 -41
  137. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  138. package/dist/systems/roof/roof-materials.js +29 -8
  139. package/dist/systems/roof/roof-system.d.ts +21 -8
  140. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  141. package/dist/systems/roof/roof-system.js +1079 -221
  142. package/dist/systems/slab/slab-system.d.ts +5 -3
  143. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  144. package/dist/systems/slab/slab-system.js +41 -23
  145. package/dist/systems/slab/slab-system.test.js +3 -2
  146. package/dist/systems/stair/stair-system.js +6 -3
  147. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  148. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  149. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  150. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  151. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  152. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  153. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-cutout.js +46 -17
  155. package/dist/systems/wall/wall-materials.d.ts +11 -7
  156. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  157. package/dist/systems/wall/wall-materials.js +244 -35
  158. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  159. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  160. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  161. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  162. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  163. package/dist/systems/wall/wall-support-extension.test.js +48 -0
  164. package/dist/systems/wall/wall-system.d.ts +2 -2
  165. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  166. package/dist/systems/wall/wall-system.js +329 -174
  167. package/dist/systems/window/window-animation-system.d.ts +12 -0
  168. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  169. package/dist/systems/window/window-animation-system.js +16 -5
  170. package/dist/systems/window/window-system.d.ts +7 -0
  171. package/dist/systems/window/window-system.d.ts.map +1 -1
  172. package/dist/systems/window/window-system.js +240 -14
  173. package/package.json +4 -4
  174. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  175. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  176. package/dist/components/viewer/ground-occluder.js +0 -67
@@ -1,13 +1,35 @@
1
- import { calculateLevelMiters, DEFAULT_WALL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getWallCurveFrameAt, getWallMiterBoundaryPoints, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, sceneRegistry, spatialGridManager, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
1
+ import { calculateLevelMiters, DEFAULT_WALL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getWallBandSlotId, getWallCurveFrameAt, getWallFaceBandConfig, getWallFaceBandForHeight, getWallMiterBoundaryPoints, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, sceneRegistry, spatialGridManager, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
4
  import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
5
5
  import { computeBoundsTree } from 'three-mesh-bvh';
6
+ import { ensureRenderableGeometryAttributes, prepareBrushForCSG } from '../../lib/csg-utils';
7
+ import { buildOpeningCutoutGeometry, getOpeningCutoutBottomPadding, } from './opening-cutout-geometry';
6
8
  // Reusable CSG evaluator for better performance
7
9
  const csgEvaluator = new Evaluator();
10
+ csgEvaluator.attributes = ['position', 'normal', 'uv', 'uv2'];
8
11
  const CURVED_WALL_3D_ENDPOINT_INSET = 0.0015;
9
12
  const WALL_FACE_NORMAL_Y_EPSILON = 0.6;
10
13
  const WALL_FACE_EDGE_DISTANCE_EPSILON = 0.003;
14
+ const WALL_BAND_SPLIT_EPSILON = 1e-5;
15
+ const WALL_BAND_SLOT_MATERIAL_INDEX = {
16
+ interior: 1,
17
+ exterior: 2,
18
+ lowerInterior: 3,
19
+ middleInterior: 4,
20
+ upperInterior: 5,
21
+ topInterior: 6,
22
+ lowerExterior: 7,
23
+ middleExterior: 8,
24
+ upperExterior: 9,
25
+ topExterior: 10,
26
+ skirtingInterior: 0,
27
+ skirtingExterior: 0,
28
+ crownInterior: 0,
29
+ crownExterior: 0,
30
+ chairRailInterior: 0,
31
+ chairRailExterior: 0,
32
+ };
11
33
  function computeGeometryBoundsTree(geometry) {
12
34
  ;
13
35
  geometry.computeBoundsTree = computeBoundsTree;
@@ -16,12 +38,6 @@ function computeGeometryBoundsTree(geometry) {
16
38
  function csgGeometry(brush) {
17
39
  return brush.geometry;
18
40
  }
19
- function ensureUv2Attribute(geometry) {
20
- const uv = geometry.getAttribute('uv');
21
- if (!uv)
22
- return;
23
- geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
24
- }
25
41
  function insetCurvedWallBoundaryPointsFor3D(wall, boundaryPoints, miterData) {
26
42
  if (!(boundaryPoints && isCurvedWall(wall))) {
27
43
  return boundaryPoints;
@@ -117,14 +133,15 @@ function distanceToWallBoundaryEdge(point, edge) {
117
133
  const closestZ = edge.start.y + edgeDz * t;
118
134
  return Math.hypot(point.x - closestX, point.y - closestZ);
119
135
  }
120
- function getWallFaceMaterialIndex(wall, face) {
136
+ function getWallFaceMaterialIndex(wall, face, y) {
121
137
  const semantic = face === 'front' ? wall.frontSide : wall.backSide;
122
- const fallback = face === 'front' ? 1 : 2;
123
- if (semantic === 'interior')
124
- return 1;
125
- if (semantic === 'exterior')
126
- return 2;
127
- return fallback;
138
+ const fallback = face === 'front' ? 'interior' : 'exterior';
139
+ const side = semantic === 'interior' || semantic === 'exterior' ? semantic : fallback;
140
+ const bands = getWallFaceBandConfig(wall);
141
+ if (!bands.enabled)
142
+ return WALL_BAND_SLOT_MATERIAL_INDEX[side];
143
+ const band = getWallFaceBandForHeight(wall, y);
144
+ return WALL_BAND_SLOT_MATERIAL_INDEX[getWallBandSlotId(side, band)];
128
145
  }
129
146
  function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
130
147
  const position = geometry.getAttribute('position');
@@ -189,7 +206,7 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
189
206
  triangleMaterials[triangleIndex] = 0;
190
207
  continue;
191
208
  }
192
- triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag);
209
+ triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag, centroid.y);
193
210
  }
194
211
  geometry.clearGroups();
195
212
  let currentMaterial = triangleMaterials[0] ?? 0;
@@ -204,6 +221,105 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
204
221
  }
205
222
  geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
206
223
  }
224
+ function interpolateSplitVertex(a, b, t) {
225
+ return {
226
+ x: a.x + (b.x - a.x) * t,
227
+ y: a.y + (b.y - a.y) * t,
228
+ z: a.z + (b.z - a.z) * t,
229
+ };
230
+ }
231
+ function clipPolygonByY(polygon, planeY, keepBelow) {
232
+ const out = [];
233
+ if (polygon.length === 0)
234
+ return out;
235
+ const isInside = (vertex) => keepBelow
236
+ ? vertex.y <= planeY + WALL_BAND_SPLIT_EPSILON
237
+ : vertex.y >= planeY - WALL_BAND_SPLIT_EPSILON;
238
+ for (let index = 0; index < polygon.length; index += 1) {
239
+ const current = polygon[index];
240
+ const previous = polygon[(index + polygon.length - 1) % polygon.length];
241
+ const currentInside = isInside(current);
242
+ const previousInside = isInside(previous);
243
+ if (currentInside !== previousInside) {
244
+ const denom = current.y - previous.y;
245
+ if (Math.abs(denom) > WALL_BAND_SPLIT_EPSILON) {
246
+ out.push(interpolateSplitVertex(previous, current, (planeY - previous.y) / denom));
247
+ }
248
+ }
249
+ if (currentInside)
250
+ out.push(current);
251
+ }
252
+ return out;
253
+ }
254
+ function triangulateSplitPolygon(polygon, positions) {
255
+ if (polygon.length < 3)
256
+ return;
257
+ const first = polygon[0];
258
+ for (let index = 1; index < polygon.length - 1; index += 1) {
259
+ const b = polygon[index];
260
+ const c = polygon[index + 1];
261
+ positions.push(first.x, first.y, first.z, b.x, b.y, b.z, c.x, c.y, c.z);
262
+ }
263
+ }
264
+ function splitGeometryAtHorizontalPlanes(geometry, planes) {
265
+ const splitPlanes = Array.from(new Set(planes
266
+ .filter((plane) => Number.isFinite(plane) && plane > WALL_BAND_SPLIT_EPSILON)
267
+ .map((plane) => Math.round(plane / WALL_BAND_SPLIT_EPSILON) * WALL_BAND_SPLIT_EPSILON))).sort((a, b) => a - b);
268
+ if (splitPlanes.length === 0)
269
+ return geometry;
270
+ const source = geometry.index ? geometry.toNonIndexed() : geometry;
271
+ const position = source.getAttribute('position');
272
+ if (!position || position.count === 0)
273
+ return source;
274
+ const positions = [];
275
+ for (let index = 0; index < position.count; index += 3) {
276
+ let polygons = [
277
+ [
278
+ { x: position.getX(index), y: position.getY(index), z: position.getZ(index) },
279
+ { x: position.getX(index + 1), y: position.getY(index + 1), z: position.getZ(index + 1) },
280
+ { x: position.getX(index + 2), y: position.getY(index + 2), z: position.getZ(index + 2) },
281
+ ],
282
+ ];
283
+ for (const plane of splitPlanes) {
284
+ const next = [];
285
+ for (const polygon of polygons) {
286
+ const minY = Math.min(...polygon.map((vertex) => vertex.y));
287
+ const maxY = Math.max(...polygon.map((vertex) => vertex.y));
288
+ if (plane <= minY + WALL_BAND_SPLIT_EPSILON || plane >= maxY - WALL_BAND_SPLIT_EPSILON) {
289
+ next.push(polygon);
290
+ continue;
291
+ }
292
+ const below = clipPolygonByY(polygon, plane, true);
293
+ const above = clipPolygonByY(polygon, plane, false);
294
+ if (below.length >= 3)
295
+ next.push(below);
296
+ if (above.length >= 3)
297
+ next.push(above);
298
+ }
299
+ polygons = next;
300
+ }
301
+ for (const polygon of polygons)
302
+ triangulateSplitPolygon(polygon, positions);
303
+ }
304
+ if (source !== geometry)
305
+ geometry.dispose();
306
+ source.dispose();
307
+ const split = new THREE.BufferGeometry();
308
+ split.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
309
+ split.computeVertexNormals();
310
+ return split;
311
+ }
312
+ function getWallBandSplitPlanes(wall) {
313
+ const bands = getWallFaceBandConfig(wall);
314
+ if (!bands.enabled)
315
+ return [];
316
+ const planes = [bands.lowerTop];
317
+ if (bands.count >= 3)
318
+ planes.push(bands.middleTop);
319
+ if (bands.count >= 4)
320
+ planes.push(bands.upperTop);
321
+ return planes.filter((plane) => plane > WALL_BAND_SPLIT_EPSILON && plane < (wall.height ?? 2.5) - WALL_BAND_SPLIT_EPSILON);
322
+ }
207
323
  // ============================================================================
208
324
  // WALL SYSTEM
209
325
  // ============================================================================
@@ -258,7 +374,7 @@ export const WallSystem = () => {
258
374
  if (hasDirty) {
259
375
  dirtyNodes.forEach((id) => {
260
376
  const node = nodes[id];
261
- if (!node || node.type !== 'wall')
377
+ if (node?.type !== 'wall')
262
378
  return;
263
379
  const levelId = node.parentId;
264
380
  if (!levelId)
@@ -387,7 +503,7 @@ function getEffectiveWall(wall) {
387
503
  function getLevelWalls(levelId) {
388
504
  const { nodes } = useScene.getState();
389
505
  const level = nodes[levelId];
390
- if (!level || level.type !== 'level')
506
+ if (level?.type !== 'level')
391
507
  return [];
392
508
  const walls = [];
393
509
  for (const childId of level.children) {
@@ -406,14 +522,15 @@ function getLevelWalls(levelId) {
406
522
  function updateWallGeometry(wallId, miterData) {
407
523
  const nodes = useScene.getState().nodes;
408
524
  const sceneNode = nodes[wallId];
409
- if (!sceneNode || sceneNode.type !== 'wall')
525
+ if (sceneNode?.type !== 'wall')
410
526
  return;
411
527
  const node = getEffectiveWall(sceneNode);
412
528
  const mesh = sceneRegistry.nodes.get(wallId);
413
529
  if (!mesh)
414
530
  return;
415
531
  const levelId = resolveLevelId(node, nodes);
416
- const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness);
532
+ const slabSupport = spatialGridManager.getSlabSupportForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness);
533
+ const slabElevation = slabSupport.elevation;
417
534
  const childrenIds = node.children || [];
418
535
  // Merge live overrides into door / window children so cutouts track an
419
536
  // in-flight resize drag (door width arrow, window height arrow, etc.)
@@ -427,22 +544,27 @@ function updateWallGeometry(wallId, miterData) {
427
544
  return child;
428
545
  // `getEffectiveNode` folds in resize overrides (width/height arrows).
429
546
  // Position moves publish to `useLiveTransforms` instead, so fold that
430
- // in too — otherwise shaped openings (arch/rounded/`opening`), whose
431
- // cutout brush is rebuilt from `node.position`, lag the live move
432
- // (rectangular cutouts already track via the live mesh matrixWorld).
547
+ // in too — opening cutout brushes are rebuilt directly from the
548
+ // effective node position rather than from the rendered proxy mesh.
433
549
  const effective = getEffectiveNode(child);
434
550
  const live = useLiveTransforms.getState().get(child.id);
435
551
  if (!live?.position)
436
552
  return effective;
437
553
  return { ...effective, position: live.position };
438
554
  });
439
- const newGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation);
555
+ const builtGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments);
556
+ const wallAngle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
557
+ // World transform the render mesh will apply (position + Y-rotation below).
558
+ // Reproduce it here so the UVs can be projected in WORLD space — see
559
+ // `applyWorldPlanarWallUVs`.
560
+ const wallWorldMatrix = new THREE.Matrix4().compose(new THREE.Vector3(node.start[0], slabElevation, node.start[1]), new THREE.Quaternion().setFromAxisAngle(WALL_UV_Y_AXIS, -wallAngle), WALL_UV_UNIT_SCALE);
561
+ const newGeo = applyWorldPlanarWallUVs(builtGeo, wallWorldMatrix);
440
562
  mesh.geometry.dispose();
441
563
  mesh.geometry = newGeo;
442
564
  // Update collision mesh
443
565
  const collisionMesh = mesh.getObjectByName('collision-mesh');
444
566
  if (collisionMesh) {
445
- const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation);
567
+ const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments);
446
568
  collisionMesh.geometry.dispose();
447
569
  collisionMesh.geometry = collisionGeo;
448
570
  }
@@ -450,19 +572,79 @@ function updateWallGeometry(wallId, miterData) {
450
572
  const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
451
573
  mesh.rotation.y = -angle;
452
574
  }
575
+ const WALL_UV_Y_AXIS = new THREE.Vector3(0, 1, 0);
576
+ const WALL_UV_UNIT_SCALE = new THREE.Vector3(1, 1, 1);
577
+ /**
578
+ * Re-project a wall's UVs in WORLD space (1 UV unit = 1 m) so the finish tiles
579
+ * continuously across adjacent walls and lines up with the roof gable above —
580
+ * instead of THREE's `ExtrudeGeometry` UVs, which restart at each wall's own
581
+ * start/end. Matches `roof-system`'s `pushRoofUv` projection exactly: vertical
582
+ * faces use `U = ±worldX/Z` (the axis across the face normal) and `V = 1 -
583
+ * worldY`; the thin top/bottom caps use `(worldX, worldZ)`. De-indexes first so
584
+ * every triangle projects by its own face normal (no shared-vertex seams at
585
+ * edges). Applied only to the render mesh; collision/floorplan geometry is
586
+ * untouched.
587
+ */
588
+ function applyWorldPlanarWallUVs(geometry, worldMatrix) {
589
+ const target = geometry.index ? geometry.toNonIndexed() : geometry;
590
+ if (target !== geometry)
591
+ geometry.dispose();
592
+ const position = target.getAttribute('position');
593
+ if (!position || position.count === 0)
594
+ return target;
595
+ const a = new THREE.Vector3();
596
+ const b = new THREE.Vector3();
597
+ const c = new THREE.Vector3();
598
+ const normal = new THREE.Vector3();
599
+ const edgeAB = new THREE.Vector3();
600
+ const edgeAC = new THREE.Vector3();
601
+ const uvs = new Float32Array(position.count * 2);
602
+ for (let i = 0; i < position.count; i += 3) {
603
+ a.fromBufferAttribute(position, i).applyMatrix4(worldMatrix);
604
+ b.fromBufferAttribute(position, i + 1).applyMatrix4(worldMatrix);
605
+ c.fromBufferAttribute(position, i + 2).applyMatrix4(worldMatrix);
606
+ edgeAB.subVectors(b, a);
607
+ edgeAC.subVectors(c, a);
608
+ normal.crossVectors(edgeAB, edgeAC).normalize();
609
+ const absX = Math.abs(normal.x);
610
+ const absY = Math.abs(normal.y);
611
+ const absZ = Math.abs(normal.z);
612
+ for (let k = 0; k < 3; k += 1) {
613
+ const p = k === 0 ? a : k === 1 ? b : c;
614
+ let u;
615
+ let v;
616
+ if (absY >= absX && absY >= absZ) {
617
+ u = p.x;
618
+ v = p.z;
619
+ }
620
+ else {
621
+ v = 1 - p.y;
622
+ u = absX >= absZ ? (normal.x >= 0 ? p.z : -p.z) : normal.z >= 0 ? p.x : -p.x;
623
+ }
624
+ uvs[(i + k) * 2] = u;
625
+ uvs[(i + k) * 2 + 1] = v;
626
+ }
627
+ }
628
+ target.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
629
+ target.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2));
630
+ return target;
631
+ }
453
632
  /**
454
633
  * Generates extruded wall geometry with mitering and cutouts
455
634
  *
456
635
  * Key insight from demo: polygon is built in WORLD coordinates first,
457
636
  * then we transform to wall-local for the 3D mesh.
458
637
  */
459
- export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0) {
638
+ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0, baseElevation = slabElevation, baseSegments = [
639
+ { start: 0, end: 1, elevation: baseElevation },
640
+ ]) {
460
641
  const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
461
642
  const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
462
- // Positive slab: shift the whole wall up (full height preserved)
463
- // Negative slab: extend wall downward so top stays fixed at wallNode.height
464
643
  const wallHeight = wallNode.height ?? DEFAULT_WALL_HEIGHT;
465
- const height = slabElevation > 0 ? wallHeight : wallHeight - slabElevation;
644
+ const topElevation = slabElevation > 0 ? slabElevation + wallHeight : wallHeight;
645
+ const effectiveBaseElevation = Math.min(baseElevation, slabElevation);
646
+ const localBottom = effectiveBaseElevation - slabElevation;
647
+ const height = topElevation - effectiveBaseElevation;
466
648
  const thickness = getWallThickness(wallNode);
467
649
  // Wall direction and normal (exactly like demo)
468
650
  const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y };
@@ -509,24 +691,118 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
509
691
  });
510
692
  // Rotate so extrusion direction (Z) becomes height direction (Y)
511
693
  geometry.rotateX(-Math.PI / 2);
694
+ if (Math.abs(localBottom) > 1e-9)
695
+ geometry.translate(0, localBottom, 0);
512
696
  geometry.computeVertexNormals();
513
697
  assignWallMaterialGroups(geometry, wallNode, boundaryEdges);
514
- ensureUv2Attribute(geometry);
515
- // Apply CSG subtraction for cutouts (doors/windows)
516
- const cutoutBrushes = collectCutoutBrushes(wallNode, childrenNodes, thickness);
698
+ ensureRenderableGeometryAttributes(geometry);
699
+ // Start with the lowest required wall prism, then remove the volume below
700
+ // each higher-supported run. This keeps the existing mitered footprint and
701
+ // opening CSG while giving one wall a stepped longitudinal base.
702
+ const baseProfileCutouts = [];
703
+ for (const segment of baseSegments) {
704
+ const segmentElevation = Math.min(segment.elevation, slabElevation);
705
+ const cutHeight = segmentElevation - effectiveBaseElevation;
706
+ if (cutHeight <= 1e-6 || segment.end - segment.start <= 1e-7)
707
+ continue;
708
+ const segmentStart = THREE.MathUtils.clamp(segment.start, 0, 1);
709
+ const segmentEnd = THREE.MathUtils.clamp(segment.end, 0, 1);
710
+ const cutHalfWidth = Math.max(thickness * 2, 0.2);
711
+ const worldCutoutPoints = [];
712
+ if (isCurvedWall(wallNode)) {
713
+ const sampleCount = Math.max(2, Math.ceil((segmentEnd - segmentStart) * 24));
714
+ const left = [];
715
+ const right = [];
716
+ for (let index = 0; index <= sampleCount; index++) {
717
+ const t = segmentStart + ((segmentEnd - segmentStart) * index) / sampleCount;
718
+ const frame = getWallCurveFrameAt(wallNode, t);
719
+ const endpointExtension = index === 0 && segmentStart <= 1e-7
720
+ ? -cutHalfWidth
721
+ : index === sampleCount && segmentEnd >= 1 - 1e-7
722
+ ? cutHalfWidth
723
+ : 0;
724
+ const center = {
725
+ x: frame.point.x + frame.tangent.x * endpointExtension,
726
+ y: frame.point.y + frame.tangent.y * endpointExtension,
727
+ };
728
+ left.push({
729
+ x: center.x + frame.normal.x * cutHalfWidth,
730
+ y: center.y + frame.normal.y * cutHalfWidth,
731
+ });
732
+ right.push({
733
+ x: center.x - frame.normal.x * cutHalfWidth,
734
+ y: center.y - frame.normal.y * cutHalfWidth,
735
+ });
736
+ }
737
+ worldCutoutPoints.push(...left, ...right.reverse());
738
+ }
739
+ else {
740
+ const tangentX = v.x / L;
741
+ const tangentY = v.y / L;
742
+ const normalX = -tangentY;
743
+ const normalY = tangentX;
744
+ const startExtension = segmentStart <= 1e-7 ? cutHalfWidth : 0;
745
+ const endExtension = segmentEnd >= 1 - 1e-7 ? cutHalfWidth : 0;
746
+ const startPoint = {
747
+ x: wallStart.x + tangentX * (segmentStart * L - startExtension),
748
+ y: wallStart.y + tangentY * (segmentStart * L - startExtension),
749
+ };
750
+ const endPoint = {
751
+ x: wallStart.x + tangentX * (segmentEnd * L + endExtension),
752
+ y: wallStart.y + tangentY * (segmentEnd * L + endExtension),
753
+ };
754
+ worldCutoutPoints.push({
755
+ x: startPoint.x + normalX * cutHalfWidth,
756
+ y: startPoint.y + normalY * cutHalfWidth,
757
+ }, { x: endPoint.x + normalX * cutHalfWidth, y: endPoint.y + normalY * cutHalfWidth }, { x: endPoint.x - normalX * cutHalfWidth, y: endPoint.y - normalY * cutHalfWidth }, {
758
+ x: startPoint.x - normalX * cutHalfWidth,
759
+ y: startPoint.y - normalY * cutHalfWidth,
760
+ });
761
+ }
762
+ const localCutoutPoints = worldCutoutPoints.map(worldToLocal);
763
+ if (localCutoutPoints.length < 3)
764
+ continue;
765
+ const cutoutShape = new THREE.Shape();
766
+ cutoutShape.moveTo(localCutoutPoints[0].x, -localCutoutPoints[0].z);
767
+ for (let index = 1; index < localCutoutPoints.length; index++) {
768
+ cutoutShape.lineTo(localCutoutPoints[index].x, -localCutoutPoints[index].z);
769
+ }
770
+ cutoutShape.closePath();
771
+ const cutoutBottom = localBottom - 0.01;
772
+ const cutoutTop = segmentElevation - slabElevation;
773
+ const cutoutGeometry = new THREE.ExtrudeGeometry(cutoutShape, {
774
+ depth: cutoutTop - cutoutBottom,
775
+ bevelEnabled: false,
776
+ });
777
+ cutoutGeometry.rotateX(-Math.PI / 2);
778
+ cutoutGeometry.translate(0, cutoutBottom, 0);
779
+ computeGeometryBoundsTree(cutoutGeometry);
780
+ baseProfileCutouts.push(new Brush(cutoutGeometry));
781
+ }
782
+ // Apply base-profile and opening cutouts in one CSG pass.
783
+ const cutoutBrushes = [
784
+ ...baseProfileCutouts,
785
+ ...collectCutoutBrushes(wallNode, childrenNodes, thickness),
786
+ ];
517
787
  if (cutoutBrushes.length === 0) {
518
- return geometry;
788
+ const splitGeometry = splitGeometryAtHorizontalPlanes(geometry, getWallBandSplitPlanes(wallNode));
789
+ splitGeometry.computeVertexNormals();
790
+ assignWallMaterialGroups(splitGeometry, wallNode, boundaryEdges);
791
+ ensureRenderableGeometryAttributes(splitGeometry);
792
+ return splitGeometry;
519
793
  }
520
794
  // Create wall brush from geometry
521
795
  // Pre-compute BVH with new API to avoid deprecation warning
796
+ ensureRenderableGeometryAttributes(geometry);
522
797
  computeGeometryBoundsTree(geometry);
523
798
  const wallBrush = new Brush(geometry);
524
799
  wallBrush.updateMatrixWorld();
525
800
  // Subtract each cutout from the wall
526
801
  let resultBrush = wallBrush;
527
802
  for (const cutoutBrush of cutoutBrushes) {
528
- cutoutBrush.updateMatrixWorld();
803
+ prepareBrushForCSG(cutoutBrush);
529
804
  const newResult = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
805
+ prepareBrushForCSG(newResult);
530
806
  if (resultBrush !== wallBrush) {
531
807
  csgGeometry(resultBrush).dispose();
532
808
  }
@@ -538,14 +814,16 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
538
814
  csgGeometry(brush).dispose();
539
815
  }
540
816
  const resultGeometry = csgGeometry(resultBrush);
541
- resultGeometry.computeVertexNormals();
542
- assignWallMaterialGroups(resultGeometry, wallNode, boundaryEdges);
543
- ensureUv2Attribute(resultGeometry);
544
- return resultGeometry;
817
+ const splitResultGeometry = splitGeometryAtHorizontalPlanes(resultGeometry, getWallBandSplitPlanes(wallNode));
818
+ splitResultGeometry.computeVertexNormals();
819
+ assignWallMaterialGroups(splitResultGeometry, wallNode, boundaryEdges);
820
+ ensureRenderableGeometryAttributes(splitResultGeometry);
821
+ return splitResultGeometry;
545
822
  }
546
823
  /**
547
- * Collects cutout brushes from child items for CSG subtraction
548
- * The cutout mesh is a plane, so we extrude it into a box that goes through the wall
824
+ * Collects opening and item cutout brushes for CSG subtraction. Door/window
825
+ * cuts come directly from node geometry; item proxy meshes are transformed
826
+ * into wall-local boxes that pass through the wall.
549
827
  */
550
828
  function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
551
829
  const brushes = [];
@@ -558,15 +836,8 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
558
836
  for (const child of childrenNodes) {
559
837
  if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door')
560
838
  continue;
561
- if ((child.type === 'door' && child.openingKind === 'opening') ||
562
- (child.type === 'door' &&
563
- child.openingKind === 'door' &&
564
- (child.openingShape === 'arch' || child.openingShape === 'rounded')) ||
565
- (child.type === 'window' && child.openingKind === 'opening') ||
566
- (child.type === 'window' &&
567
- child.openingKind === 'window' &&
568
- (child.openingShape === 'arch' || child.openingShape === 'rounded'))) {
569
- brushes.push(createShapedOpeningCutoutBrush(child, wallThickness));
839
+ if (child.type === 'door' || child.type === 'window') {
840
+ brushes.push(createOpeningCutoutBrush(child, wallThickness));
570
841
  continue;
571
842
  }
572
843
  const childMesh = sceneRegistry.nodes.get(child.id);
@@ -609,132 +880,16 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
609
880
  }
610
881
  return brushes;
611
882
  }
612
- function createShapedOpeningCutoutBrush(opening, wallThickness) {
613
- const shape = createShapedOpeningCutoutShape(opening);
614
- const depth = wallThickness * 2;
615
- const bevelSize = opening.openingShape === 'rounded'
616
- ? 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))
617
- : 0;
618
- const geometry = new THREE.ExtrudeGeometry(shape, {
619
- depth,
620
- bevelEnabled: bevelSize > 0,
621
- bevelSegments: bevelSize > 0 ? 8 : 0,
622
- bevelSize,
623
- bevelThickness: bevelSize,
624
- curveSegments: 24,
625
- });
626
- geometry.translate(0, 0, -depth / 2);
627
- computeGeometryBoundsTree(geometry);
628
- return new Brush(geometry);
629
- }
630
- function createShapedOpeningCutoutShape(opening) {
883
+ function createOpeningCutoutBrush(opening, wallThickness) {
631
884
  const halfWidth = opening.width / 2;
632
885
  const bottom = opening.position[1] - opening.height / 2;
633
- const top = opening.position[1] + opening.height / 2;
634
- const centerX = opening.position[0];
635
- const left = centerX - halfWidth;
636
- const right = centerX + halfWidth;
637
- const width = Math.max(opening.width, 1e-6);
638
- const height = Math.max(opening.height, 1e-6);
639
- const shape = new THREE.Shape();
640
- if (opening.openingShape === 'arch') {
641
- const archHeight = Math.min(Math.max(opening.archHeight ?? width / 2, 0.01), height);
642
- const springY = top - archHeight;
643
- const segments = 32;
644
- shape.moveTo(left, bottom);
645
- shape.lineTo(right, bottom);
646
- shape.lineTo(right, springY);
647
- for (let index = 1; index <= segments; index += 1) {
648
- const x = right + (left - right) * (index / segments);
649
- const normalizedX = Math.min(Math.abs((x - centerX) / halfWidth), 1);
650
- const y = springY + archHeight * Math.sqrt(Math.max(1 - normalizedX * normalizedX, 0));
651
- shape.lineTo(x, y);
652
- }
653
- shape.lineTo(left, bottom);
654
- shape.closePath();
655
- return shape;
656
- }
657
- if (opening.openingShape === 'rounded') {
658
- const radii = getRoundedOpeningRadii(opening, width, height);
659
- applyRoundedOpeningShape(shape, left, right, bottom, top, radii);
660
- return shape;
661
- }
662
- shape.moveTo(left, bottom);
663
- shape.lineTo(right, bottom);
664
- shape.lineTo(right, top);
665
- shape.lineTo(left, top);
666
- shape.closePath();
667
- return shape;
668
- }
669
- function getRoundedOpeningRadii(opening, width, height) {
670
- if (opening.type !== 'window') {
671
- if (opening.openingRadiusMode === 'individual') {
672
- const [topLeft = 0, topRight = 0] = opening.openingTopRadii ?? [0.15, 0.15];
673
- return normalizeCornerRadii({
674
- topLeft: Math.max(topLeft, 0),
675
- topRight: Math.max(topRight, 0),
676
- bottomRight: 0,
677
- bottomLeft: 0,
678
- }, width, height);
679
- }
680
- const maxRadius = Math.min(width / 2, height);
681
- const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
682
- return { topLeft: radius, topRight: radius, bottomRight: 0, bottomLeft: 0 };
683
- }
684
- if (opening.openingRadiusMode === 'individual') {
685
- const [topLeft = 0, topRight = 0, bottomRight = 0, bottomLeft = 0] = opening.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15];
686
- return normalizeCornerRadii({
687
- topLeft: Math.max(topLeft, 0),
688
- topRight: Math.max(topRight, 0),
689
- bottomRight: Math.max(bottomRight, 0),
690
- bottomLeft: Math.max(bottomLeft, 0),
691
- }, width, height);
692
- }
693
- const maxRadius = Math.min(width / 2, height / 2);
694
- const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
695
- return { topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius };
696
- }
697
- function normalizeCornerRadii(radii, width, height) {
698
- const next = { ...radii };
699
- 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));
700
- if (maxScale < 1) {
701
- next.topLeft *= maxScale;
702
- next.topRight *= maxScale;
703
- next.bottomRight *= maxScale;
704
- next.bottomLeft *= maxScale;
705
- }
706
- return next;
707
- }
708
- function applyRoundedOpeningShape(shape, left, right, bottom, top, radii) {
709
- const { topLeft, topRight, bottomRight, bottomLeft } = radii;
710
- shape.moveTo(left + bottomLeft, bottom);
711
- shape.lineTo(right - bottomRight, bottom);
712
- if (bottomRight > 1e-6) {
713
- shape.absarc(right - bottomRight, bottom + bottomRight, bottomRight, -Math.PI / 2, 0, false);
714
- }
715
- else {
716
- shape.lineTo(right, bottom);
717
- }
718
- shape.lineTo(right, top - topRight);
719
- if (topRight > 1e-6) {
720
- shape.absarc(right - topRight, top - topRight, topRight, 0, Math.PI / 2, false);
721
- }
722
- else {
723
- shape.lineTo(right, top);
724
- }
725
- shape.lineTo(left + topLeft, top);
726
- if (topLeft > 1e-6) {
727
- shape.absarc(left + topLeft, top - topLeft, topLeft, Math.PI / 2, Math.PI, false);
728
- }
729
- else {
730
- shape.lineTo(left, top);
731
- }
732
- shape.lineTo(left, bottom + bottomLeft);
733
- if (bottomLeft > 1e-6) {
734
- shape.absarc(left + bottomLeft, bottom + bottomLeft, bottomLeft, Math.PI, Math.PI * 1.5, false);
735
- }
736
- else {
737
- shape.lineTo(left, bottom);
738
- }
739
- shape.closePath();
886
+ const bottomPadding = getOpeningCutoutBottomPadding(opening, bottom);
887
+ const geometry = buildOpeningCutoutGeometry(opening, {
888
+ left: opening.position[0] - halfWidth,
889
+ right: opening.position[0] + halfWidth,
890
+ bottom: bottom - bottomPadding,
891
+ top: opening.position[1] + opening.height / 2,
892
+ }, wallThickness * 2, wallThickness);
893
+ computeGeometryBoundsTree(geometry);
894
+ return new Brush(geometry);
740
895
  }
@@ -1,2 +1,14 @@
1
+ import { type WindowNode } from '@pascal-app/core';
2
+ import type { Object3D } from 'three';
3
+ /**
4
+ * Pose a window's moving parts (sash/panel/slats) at `value` (0 = closed,
5
+ * 1 = open) by mutating the named child groups under `mesh`. Returns true when
6
+ * the window type has a direct pose path and the named parts were found.
7
+ *
8
+ * This is the single source of truth for window kinematics: the live animation
9
+ * system poses the registered scene mesh, and the GLB exporter poses an export
10
+ * clone to sample the open/close keyframes for a baked animation clip.
11
+ */
12
+ export declare function poseWindowMovingParts(node: WindowNode, mesh: Object3D | undefined, value: number): boolean;
1
13
  export declare const WindowAnimationSystem: () => null;
2
14
  //# sourceMappingURL=window-animation-system.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"window-animation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-animation-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAsBrC;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,GACZ,OAAO,CAqFT;AAQD,eAAO,MAAM,qBAAqB,YA+CjC,CAAA"}