@pascal-app/viewer 1.0.4 → 1.0.5

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 (183) hide show
  1. package/dist/capture/capture-runtime.js +1 -1
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.d.ts +1 -1
  4. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  5. package/dist/components/renderers/node-renderer.js +4 -1
  6. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  7. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -1
  8. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  9. package/dist/components/viewer/glb-interactive.js +1 -1
  10. package/dist/components/viewer/glb-reference-nodes.js +1 -1
  11. package/dist/components/viewer/glb-replace-instances.js +1 -1
  12. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  13. package/dist/components/viewer/glb-scene.js +2 -3
  14. package/dist/components/viewer/glb-walkthrough-controller.js +1 -1
  15. package/dist/components/viewer/index.d.ts +10 -1
  16. package/dist/components/viewer/index.d.ts.map +1 -1
  17. package/dist/components/viewer/index.js +28 -12
  18. package/dist/components/viewer/lights.d.ts.map +1 -1
  19. package/dist/components/viewer/lights.js +107 -21
  20. package/dist/components/viewer/perf-panel.d.ts.map +1 -1
  21. package/dist/components/viewer/perf-panel.js +1 -1
  22. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  23. package/dist/components/viewer/post-processing.js +96 -33
  24. package/dist/components/viewer/registered-systems.js +1 -1
  25. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  26. package/dist/components/viewer/scene-bvh.js +1 -1
  27. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  28. package/dist/components/viewer/scene-environment.js +31 -11
  29. package/dist/components/viewer/selection-manager.js +15 -7
  30. package/dist/components/viewer/underground-ambiance.d.ts +9 -0
  31. package/dist/components/viewer/underground-ambiance.d.ts.map +1 -0
  32. package/dist/components/viewer/underground-ambiance.js +105 -0
  33. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  34. package/dist/components/viewer/viewer-camera.js +1 -1
  35. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -1
  36. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  37. package/dist/hooks/use-gltf-ktx2.js +2 -0
  38. package/dist/hooks/use-node-events.d.ts.map +1 -1
  39. package/dist/index.d.ts +16 -7
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +16 -7
  42. package/dist/lib/backdrop.d.ts +17 -0
  43. package/dist/lib/backdrop.d.ts.map +1 -1
  44. package/dist/lib/backdrop.js +18 -1
  45. package/dist/lib/construction-dust.d.ts +57 -0
  46. package/dist/lib/construction-dust.d.ts.map +1 -0
  47. package/dist/lib/construction-dust.js +187 -0
  48. package/dist/lib/csg-utils.js +1 -1
  49. package/dist/lib/foundation-materials.d.ts +26 -0
  50. package/dist/lib/foundation-materials.d.ts.map +1 -0
  51. package/dist/lib/foundation-materials.js +65 -0
  52. package/dist/lib/gltf-unlit.d.ts +10 -0
  53. package/dist/lib/gltf-unlit.d.ts.map +1 -0
  54. package/dist/lib/gltf-unlit.js +23 -0
  55. package/dist/lib/ground-window.d.ts +33 -0
  56. package/dist/lib/ground-window.d.ts.map +1 -0
  57. package/dist/lib/ground-window.js +114 -0
  58. package/dist/lib/hero-pose.d.ts +1 -1
  59. package/dist/lib/hero-pose.d.ts.map +1 -1
  60. package/dist/lib/ink-edges.d.ts +2 -1
  61. package/dist/lib/ink-edges.d.ts.map +1 -1
  62. package/dist/lib/ktx2-loader.d.ts +7 -0
  63. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  64. package/dist/lib/ktx2-loader.js +35 -0
  65. package/dist/lib/layer-pass.d.ts.map +1 -1
  66. package/dist/lib/layers.d.ts +11 -7
  67. package/dist/lib/layers.d.ts.map +1 -1
  68. package/dist/lib/layers.js +16 -6
  69. package/dist/lib/materials.d.ts +5 -0
  70. package/dist/lib/materials.d.ts.map +1 -1
  71. package/dist/lib/materials.js +34 -4
  72. package/dist/lib/merged-outline-node.d.ts +5 -0
  73. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  74. package/dist/lib/merged-outline-node.js +80 -2
  75. package/dist/lib/pointer-events.d.ts +20 -4
  76. package/dist/lib/pointer-events.d.ts.map +1 -1
  77. package/dist/lib/pointer-events.js +78 -14
  78. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  79. package/dist/lib/reveal-candidates.d.ts +80 -0
  80. package/dist/lib/reveal-candidates.d.ts.map +1 -0
  81. package/dist/lib/reveal-candidates.js +524 -0
  82. package/dist/lib/reveal-ghost.d.ts +9 -0
  83. package/dist/lib/reveal-ghost.d.ts.map +1 -0
  84. package/dist/lib/reveal-ghost.js +62 -0
  85. package/dist/lib/reveal-pending.d.ts +12 -0
  86. package/dist/lib/reveal-pending.d.ts.map +1 -0
  87. package/dist/lib/reveal-pending.js +12 -0
  88. package/dist/lib/reveal-pose.d.ts +70 -0
  89. package/dist/lib/reveal-pose.d.ts.map +1 -0
  90. package/dist/lib/reveal-pose.js +248 -0
  91. package/dist/lib/reveal-schedule.d.ts +208 -0
  92. package/dist/lib/reveal-schedule.d.ts.map +1 -0
  93. package/dist/lib/reveal-schedule.js +410 -0
  94. package/dist/lib/reveal-texture-scale.d.ts +5 -0
  95. package/dist/lib/reveal-texture-scale.d.ts.map +1 -0
  96. package/dist/lib/reveal-texture-scale.js +113 -0
  97. package/dist/lib/reveal-windows.d.ts +23 -0
  98. package/dist/lib/reveal-windows.d.ts.map +1 -0
  99. package/dist/lib/reveal-windows.js +31 -0
  100. package/dist/lib/reveal-wipe.d.ts +71 -0
  101. package/dist/lib/reveal-wipe.d.ts.map +1 -0
  102. package/dist/lib/reveal-wipe.js +105 -0
  103. package/dist/lib/scene-pass-mrt.d.ts +12 -0
  104. package/dist/lib/scene-pass-mrt.d.ts.map +1 -0
  105. package/dist/lib/scene-pass-mrt.js +23 -0
  106. package/dist/lib/scene-themes.d.ts +30 -8
  107. package/dist/lib/scene-themes.d.ts.map +1 -1
  108. package/dist/lib/scene-themes.js +36 -0
  109. package/dist/lib/scene-visibility.d.ts +11 -1
  110. package/dist/lib/scene-visibility.d.ts.map +1 -1
  111. package/dist/lib/scene-visibility.js +38 -0
  112. package/dist/lib/site-surfaces.d.ts +32 -0
  113. package/dist/lib/site-surfaces.d.ts.map +1 -0
  114. package/dist/lib/site-surfaces.js +70 -0
  115. package/dist/lib/snapshot-pipeline.d.ts +2 -1
  116. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  117. package/dist/lib/snapshot-pipeline.js +35 -15
  118. package/dist/lib/tsl-compat.d.ts +2 -2
  119. package/dist/lib/tsl-compat.d.ts.map +1 -1
  120. package/dist/lib/underground-ambiance.d.ts +168 -0
  121. package/dist/lib/underground-ambiance.d.ts.map +1 -0
  122. package/dist/lib/underground-ambiance.js +334 -0
  123. package/dist/store/use-viewer.d.ts +184 -27
  124. package/dist/store/use-viewer.d.ts.map +1 -1
  125. package/dist/store/use-viewer.js +155 -19
  126. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  127. package/dist/systems/ceiling/ceiling-system.js +2 -0
  128. package/dist/systems/construction-reveal/construction-dust.d.ts +16 -0
  129. package/dist/systems/construction-reveal/construction-dust.d.ts.map +1 -0
  130. package/dist/systems/construction-reveal/construction-dust.js +92 -0
  131. package/dist/systems/construction-reveal/construction-reveal.d.ts +276 -0
  132. package/dist/systems/construction-reveal/construction-reveal.d.ts.map +1 -0
  133. package/dist/systems/construction-reveal/construction-reveal.js +2383 -0
  134. package/dist/systems/construction-reveal/dust-material.d.ts +10 -0
  135. package/dist/systems/construction-reveal/dust-material.d.ts.map +1 -0
  136. package/dist/systems/construction-reveal/dust-material.js +17 -0
  137. package/dist/systems/construction-reveal/ghost-dress.d.ts +10 -0
  138. package/dist/systems/construction-reveal/ghost-dress.d.ts.map +1 -0
  139. package/dist/systems/construction-reveal/ghost-dress.js +24 -0
  140. package/dist/systems/elevator/elevator-interaction-system.d.ts +7 -0
  141. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  142. package/dist/systems/elevator/elevator-interaction-system.js +18 -1
  143. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  144. package/dist/systems/interactive/control-widget.d.ts.map +1 -1
  145. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  146. package/dist/systems/interactive/interactive-system.js +16 -6
  147. package/dist/systems/interactive/scripted-clips.js +1 -1
  148. package/dist/systems/item-light/item-light-system.js +1 -1
  149. package/dist/systems/level/level-hold.d.ts +4 -0
  150. package/dist/systems/level/level-hold.d.ts.map +1 -0
  151. package/dist/systems/level/level-hold.js +14 -0
  152. package/dist/systems/level/level-system.d.ts.map +1 -1
  153. package/dist/systems/level/level-system.js +16 -1
  154. package/dist/systems/level/level-utils.d.ts +13 -3
  155. package/dist/systems/level/level-utils.d.ts.map +1 -1
  156. package/dist/systems/level/level-utils.js +56 -8
  157. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts +103 -0
  158. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts.map +1 -0
  159. package/dist/systems/roof/__fixtures__/roof-plane-cases.js +317 -0
  160. package/dist/systems/roof/roof-materials.d.ts +4 -2
  161. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  162. package/dist/systems/roof/roof-materials.js +18 -24
  163. package/dist/systems/roof/roof-system.d.ts +8 -1
  164. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  165. package/dist/systems/roof/roof-system.js +195 -45
  166. package/dist/systems/wall/wall-cutout-cache.d.ts +32 -3
  167. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  168. package/dist/systems/wall/wall-cutout-cache.js +95 -2
  169. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  170. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -1
  171. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  172. package/dist/systems/wall/wall-materials.js +39 -4
  173. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -1
  174. package/dist/systems/wall/wall-system.d.ts +5 -0
  175. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  176. package/dist/systems/wall/wall-system.js +42 -19
  177. package/dist/systems/window/corner-frame.d.ts +10 -0
  178. package/dist/systems/window/corner-frame.d.ts.map +1 -0
  179. package/dist/systems/window/corner-frame.js +17 -0
  180. package/dist/systems/window/window-system.d.ts +1 -1
  181. package/dist/systems/window/window-system.d.ts.map +1 -1
  182. package/dist/systems/window/window-system.js +31 -6
  183. package/package.json +8 -8
@@ -62,11 +62,51 @@ export function wallFacingNegative(dot, previous) {
62
62
  return dot < 0;
63
63
  return previous ? dot < WALL_FACING_HYSTERESIS : dot < -WALL_FACING_HYSTERESIS;
64
64
  }
65
+ const NOT_REVEALING = [];
66
+ /**
67
+ * The walls the cutaway must leave in view because something on them is playing: a wall that is
68
+ * moving in, going out or being edited, a wall that hosts an opening that is, and the walls under a
69
+ * node that is (a facade part). The person's wall mode is not touched: these are stamped visible
70
+ * for as long as they play, and the next frame after hides them again as the view wants.
71
+ */
72
+ export function wallsInPlay(nodes, revealing) {
73
+ const walls = new Set();
74
+ if (revealing.length === 0)
75
+ return walls;
76
+ const down = (id, depth) => {
77
+ const node = nodes[id];
78
+ if (!node)
79
+ return;
80
+ if (node.type === 'wall')
81
+ walls.add(id);
82
+ if (depth >= 4)
83
+ return;
84
+ for (const child of node.children ?? [])
85
+ down(child, depth + 1);
86
+ };
87
+ for (const id of revealing) {
88
+ // The wall itself, or an opening in it: the host stands visible while the opening plays.
89
+ for (let node = nodes[id], guard = 0; node && guard < 16; node = node.parentId ? nodes[node.parentId] : undefined, guard += 1)
90
+ if (node.type === 'wall')
91
+ walls.add(node.id);
92
+ // What stands under a node that plays rides with it.
93
+ down(id, 0);
94
+ }
95
+ return walls;
96
+ }
97
+ /**
98
+ * The visible variant of a mesh's materials, kept on the object by the system that cuts it away, for
99
+ * whoever must draw a copy of the mesh while the cutaway hides it (a deleted wall's ghost). Not in
100
+ * `userData`: `Object3D.clone` copies that through JSON, which would serialize every material.
101
+ */
102
+ export const visibleMaterialsOf = new WeakMap();
65
103
  export class WallCutoutCache {
66
104
  viewerStore;
67
105
  materialResolver;
68
106
  walls = new Map();
69
107
  rebuilt = new Set();
108
+ /** Walls whose palette waits for a preview to give back their groups (see `mayAssign`). */
109
+ deferred = new Set();
70
110
  viewer;
71
111
  nodes;
72
112
  materials;
@@ -83,6 +123,9 @@ export class WallCutoutCache {
83
123
  highlightKey = '';
84
124
  transformed = new Set();
85
125
  overrides = useLiveNodeOverrides.getState().overrides;
126
+ /** The reveal's list the walls in play were read from, and the walls it named. */
127
+ revealing = NOT_REVEALING;
128
+ inPlay = new Set();
86
129
  constructor(viewerStore = useViewer, materialResolver = getMaterialsForWall) {
87
130
  this.viewerStore = viewerStore;
88
131
  this.materialResolver = materialResolver;
@@ -138,13 +181,35 @@ export class WallCutoutCache {
138
181
  : null;
139
182
  const releasedPreview = previewId(previous) === previewId(viewer) ? null : previewId(previous);
140
183
  this.viewer = viewer;
184
+ // A wall whose play starts or ends, or a node that moves under one, changes only its own stamp.
185
+ const revealing = viewer.nodesRevealing ?? NOT_REVEALING;
186
+ const playChanged = new Set();
187
+ if (this.revealing !== revealing || (nodesChanged && revealing.length > 0)) {
188
+ const next = wallsInPlay(scene.nodes, revealing);
189
+ for (const id of next)
190
+ if (!this.inPlay.has(id))
191
+ playChanged.add(id);
192
+ for (const id of this.inPlay)
193
+ if (!next.has(id))
194
+ playChanged.add(id);
195
+ this.inPlay = next;
196
+ this.revealing = revealing;
197
+ }
198
+ // After the stamps are current: a deferred wall is applied with the walls in play it stands among.
199
+ for (const id of this.deferred.size > 0 ? Array.from(this.deferred) : []) {
200
+ this.deferred.delete(id);
201
+ const wall = this.walls.get(id);
202
+ if (wall)
203
+ this.apply(wall, viewer.wallMode, false);
204
+ }
141
205
  const invalidated = overridesChanged ||
142
206
  appearanceChanged ||
143
207
  nodesChanged ||
144
208
  registryChanged ||
145
209
  this.rebuilt.size > 0 ||
146
210
  this.transformed.size > 0 ||
147
- releasedPreview !== null;
211
+ releasedPreview !== null ||
212
+ playChanged.size > 0;
148
213
  const cutaway = viewer.wallMode === 'cutaway';
149
214
  if (!invalidated && !cutaway)
150
215
  return;
@@ -220,6 +285,7 @@ export class WallCutoutCache {
220
285
  changed ||
221
286
  rebuilt ||
222
287
  releasedPreview === id ||
288
+ playChanged.has(id) ||
223
289
  cameraChanged) {
224
290
  this.apply(wall, viewer.wallMode, refresh);
225
291
  }
@@ -285,6 +351,8 @@ export class WallCutoutCache {
285
351
  };
286
352
  wall.visibleVariant = variant(false);
287
353
  wall.hiddenVariant = variant(true);
354
+ // The plain visible palette (no selection or hover tint): what the wall wears when a capture sees it.
355
+ visibleMaterialsOf.set(wall.mesh, materials.visible);
288
356
  }
289
357
  refreshNormal(wall, visited) {
290
358
  const update = (object) => {
@@ -306,7 +374,10 @@ export class WallCutoutCache {
306
374
  if (mode === 'cutaway') {
307
375
  wall.negativeFacing = wallFacingNegative(wall.normal.dot(this.cameraDirection), wall.negativeFacing);
308
376
  }
309
- const hidden = wallHiddenFromFacing(wall.node, mode, wall.negativeFacing ?? false);
377
+ // A wall in play is seen whatever the cutaway says (walls down is the person's own choice, and
378
+ // stays); `mode` itself is the person's, untouched.
379
+ const hidden = wallHiddenFromFacing(wall.node, mode, wall.negativeFacing ?? false) &&
380
+ !(mode === 'cutaway' && this.inPlay.has(wall.node.id));
310
381
  wall.hidden = hidden;
311
382
  // The wall batch and pointer handlers consume this boolean, including
312
383
  // false on stamp lift; translucent walls must continue receiving events.
@@ -330,13 +401,35 @@ export class WallCutoutCache {
330
401
  if (wall.variantKey !== variant.key || refresh) {
331
402
  if (wall.mesh.material !== variant.materials &&
332
403
  !sameMaterialArray(wall.mesh.material, variant.materials)) {
404
+ if (!this.mayAssign(wall, variant.materials)) {
405
+ wall.variantKey = undefined;
406
+ return;
407
+ }
333
408
  wall.mesh.material = variant.materials;
334
409
  }
335
410
  wall.variantKey = variant.key;
336
411
  wall.assignedMaterials = wall.mesh.material;
337
412
  }
338
413
  else if (wall.mesh.material !== wall.assignedMaterials) {
414
+ if (!this.mayAssign(wall, wall.assignedMaterials))
415
+ return;
339
416
  wall.mesh.material = wall.assignedMaterials;
340
417
  }
341
418
  }
419
+ /**
420
+ * A paint preview spread over several walls (a room, all matching) regroups
421
+ * each wall's faces onto its own longer array, but only the hovered wall is
422
+ * known here as a preview's owner. A palette that cannot draw the groups the
423
+ * mesh wears now waits for the preview's restore to put them back: the mesh
424
+ * would otherwise keep groups with no material, and every raycast over it
425
+ * throws on `material.side`.
426
+ */
427
+ mayAssign(wall, materials) {
428
+ if (hasMaterialsForGroups(wall.mesh, materials) || !hasMaterialsForGroups(wall.mesh)) {
429
+ this.deferred.delete(wall.node.id);
430
+ return true;
431
+ }
432
+ this.deferred.add(wall.node.id);
433
+ return false;
434
+ }
342
435
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAGlG,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,OAAO,CAAA;AAC7C,OAAO,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACjD,OAAkB,EAAE,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAEL,eAAe,EACf,KAAK,qBAAqB,EAE3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAA;AAKzB,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAE3C,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,QAGtE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,IAAI,EACd,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,OAAO,GACjB,OAAO,CAKT;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,gBAAgB,EAAE,qBAAqB,EACvC,WAAW,EAAE,qBAAqB,EAClC,UAAU,EAAE,OAAO,GAClB,MAAM,IAAI,CAmCZ;AAED,eAAO,MAAM,UAAU,GAAI,oCAGxB;IACD,WAAW,CAAC,EAAE,qBAAqB,CAAA;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAA;CACzC,SAiCA,CAAA"}
1
+ {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAGlG,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,OAAO,CAAA;AAC7C,OAAO,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACjD,OAAkB,EAAE,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAEL,eAAe,EACf,KAAK,qBAAqB,EAE3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAA;AAKzB,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAE3C,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,QAGtE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,IAAI,EACd,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,OAAO,GACjB,OAAO,CAKT;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,gBAAgB,EAAE,qBAAqB,EACvC,WAAW,EAAE,qBAAqB,EAClC,UAAU,EAAE,OAAO,GAClB,MAAM,IAAI,CAmCZ;AAED,eAAO,MAAM,UAAU,uCAGpB;IACD,WAAW,CAAC,EAAE,qBAAqB,CAAA;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAA;CACzC,SAiCA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"wall-material-variant.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-material-variant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,mBAAmB,GAC3B,SAAS,GACT,WAAW,GACX,aAAa,GACb,gBAAgB,GAChB,kBAAkB,GAClB,oBAAoB,GACpB,mBAAmB,GACnB,qBAAqB,GACrB,uBAAuB,GACvB,iBAAiB,CAAA;AAErB,eAAO,MAAM,0BAA0B,GAAI,yFAMxC;IACD,6DAA6D;IAC7D,eAAe,EAAE,OAAO,CAAA;IACxB,2EAA2E;IAC3E,MAAM,EAAE,OAAO,CAAA;IACf,6DAA6D;IAC7D,iBAAiB,EAAE,OAAO,CAAA;IAC1B,uEAAuE;IACvE,oBAAoB,EAAE,OAAO,CAAA;IAC7B,4DAA4D;IAC5D,gBAAgB,EAAE,OAAO,CAAA;CAC1B,KAAG,mBAMH,CAAA"}
1
+ {"version":3,"file":"wall-material-variant.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-material-variant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,mBAAmB,GAC3B,SAAS,GACT,WAAW,GACX,aAAa,GACb,gBAAgB,GAChB,kBAAkB,GAClB,oBAAoB,GACpB,mBAAmB,GACnB,qBAAqB,GACrB,uBAAuB,GACvB,iBAAiB,CAAA;AAErB,eAAO,MAAM,0BAA0B,4FAMpC;IACD,6DAA6D;IAC7D,eAAe,EAAE,OAAO,CAAA;IACxB,2EAA2E;IAC3E,MAAM,EAAE,OAAO,CAAA;IACf,6DAA6D;IAC7D,iBAAiB,EAAE,OAAO,CAAA;IAC1B,uEAAuE;IACvE,oBAAoB,EAAE,OAAO,CAAA;IAC7B,4DAA4D;IAC5D,gBAAgB,EAAE,OAAO,CAAA;CAC1B,KAAG,mBAMH,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,aAAa,EAClB,KAAK,eAAe,EAGpB,KAAK,QAAQ,EAGd,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAG5C,OAAO,EAEL,KAAK,WAAW,EAMhB,KAAK,aAAa,EAInB,MAAM,qBAAqB,CAAA;AAE5B,KAAK,cAAc,GAAG,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,GAAG,SAAS,CAAA;AAexE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,EAAE,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAA;CAC1B;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM;QAAE,OAAO,EAAE,iBAAiB,CAAC;QAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAA;KAAE,CAAA;CACjE,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,aAAa,EACvB,QAAQ,CAAC,EAAE,OAAO,EAClB,WAAW,CAAC,EAAE,WAAW,EACzB,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,KAC3B,aAAa,CAAA;AA2QlB,wFAAwF;AACxF,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS9F;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS1F;AA4DD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,EAClC,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAQR;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,EAClC,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,aAAa,CAmFf;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,GACjC,iBAAiB,CAUnB"}
1
+ {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,aAAa,EAClB,KAAK,eAAe,EAGpB,KAAK,QAAQ,EAId,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAS5C,OAAO,EAEL,KAAK,WAAW,EAMhB,KAAK,aAAa,EAInB,MAAM,qBAAqB,CAAA;AAE5B,KAAK,cAAc,GAAG,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,GAAG,SAAS,CAAA;AAexE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,EAAE,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAA;CAC1B;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM;QAAE,OAAO,EAAE,iBAAiB,CAAC;QAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAA;KAAE,CAAA;CACjE,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,aAAa,EACvB,QAAQ,CAAC,EAAE,OAAO,EAClB,WAAW,CAAC,EAAE,WAAW,EACzB,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,KAC3B,aAAa,CAAA;AA4SlB,wFAAwF;AACxF,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS9F;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS1F;AA4DD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,EAClC,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAQR;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,EAClC,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,aAAa,CAmFf;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,UAAU,GAAE,SAAS,MAAM,EAAO,GACjC,iBAAiB,CAUnB"}
@@ -1,7 +1,8 @@
1
- import { getEffectiveWallFaceMaterial, getMaterialPresetByRef, getWallSurfaceMaterialSignature, parseMaterialColor, parseMaterialRef, resolveMaterial, resolveWallExteriorSide, resolveWallFaceChain, WALL_SLOT_DEFAULT, wallAssemblyFinishRef, } from '@pascal-app/core';
1
+ import { getEffectiveWallFaceMaterial, getMaterialPresetByRef, getWallSurfaceMaterialSignature, parseMaterialColor, parseMaterialRef, resolveMaterial, resolveWallExteriorSide, resolveWallFaceChain, WALL_SLOT_DEFAULT, wallAssemblyExposedStructure, wallAssemblyFinishRef, } from '@pascal-app/core';
2
2
  import { Color } from 'three';
3
- import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, vec2 } from 'three/tsl';
3
+ import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, uv, vec2 } from 'three/tsl';
4
4
  import { MeshLambertNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
5
+ import { BLOCK_MASONRY, concreteMaterial, masonryMaterial, STRUCTURE_COLORS, } from '../../lib/foundation-materials';
5
6
  import { baseMaterial, createDefaultMaterial, createMaterial, createMaterialFromPresetRef, createSurfaceRoleMaterial, materialPresetRefSignature, resolveMaterialRef, resolveSlotDefaultRef, resolveSurfaceColor, } from '../../lib/materials';
6
7
  const DEFAULT_WALL_COLOR = '#e9e7e3';
7
8
  const WALL_HIGHLIGHT_PROFILES = {
@@ -48,6 +49,29 @@ function wallAssemblyFaceFinishRef(wallNode, face) {
48
49
  const exteriorFace = resolveWallExteriorSide(wallNode) === 1 ? 'a' : 'b';
49
50
  return face === exteriorFace ? wallAssemblyFinishRef(wallNode) : null;
50
51
  }
52
+ // What `face` shows when it is the exterior face of a wall with no cladding over a bare masonry or
53
+ // concrete core: the structure itself (null otherwise, and for any face with a cladding).
54
+ function wallFaceExposedStructure(wallNode, face) {
55
+ if (wallAssemblyFaceFinishRef(wallNode, face))
56
+ return null;
57
+ const exteriorFace = resolveWallExteriorSide(wallNode) === 1 ? 'a' : 'b';
58
+ return face === exteriorFace ? wallAssemblyExposedStructure(wallNode) : null;
59
+ }
60
+ // The structure as the generic foundation draws it (same courses, joints and colours), made once:
61
+ // the block's bond runs along the face's world-planar U, the wall's own metres. Solid shading
62
+ // keeps to a flat colour, like every other wall finish.
63
+ const structureMaterials = new Map();
64
+ function exposedStructureMaterial(structure, shading) {
65
+ if (shading !== 'rendered')
66
+ return createDefaultMaterial(STRUCTURE_COLORS[structure], 0.9, shading);
67
+ let material = structureMaterials.get(structure);
68
+ if (!material) {
69
+ material = structure === 'cmu' ? masonryMaterial(BLOCK_MASONRY, uv().x) : concreteMaterial();
70
+ material.userData.__pascalCachedMaterial = true;
71
+ structureMaterials.set(structure, material);
72
+ }
73
+ return material;
74
+ }
51
75
  // Slot-first resolution for one wall face, matching every other paintable kind:
52
76
  // node.slots[face] ref → legacy inline finish → declared slot default.
53
77
  // A dangling `scene:` ref (material deleted / copied across scenes) falls back
@@ -64,6 +88,9 @@ function resolveWallFaceMaterial(wallNode, face, shading, sceneMaterials) {
64
88
  // exterior — a "2x6 lap siding" wall reads as lap siding without the user
65
89
  // painting it. Painting the face still wins.
66
90
  const assemblyRef = wallAssemblyFaceFinishRef(wallNode, face);
91
+ const exposed = assemblyRef ? null : wallFaceExposedStructure(wallNode, face);
92
+ if (exposed)
93
+ return exposedStructureMaterial(exposed, shading);
67
94
  return resolveWallSlotDefault(assemblyRef ?? finish.ref, shading);
68
95
  }
69
96
  // A region or room finish: its ref, else the face default (same dangling rule).
@@ -91,10 +118,15 @@ function wallFaceMaterialSignature(wallNode, face, sceneMaterials) {
91
118
  const ref = wallNode.slots?.[face];
92
119
  if (ref)
93
120
  return refMaterialSignature(ref, sceneMaterials);
121
+ const spec = getEffectiveWallFaceMaterial(wallNode, face);
122
+ const assemblyRef = wallAssemblyFaceFinishRef(wallNode, face);
94
123
  return JSON.stringify({
95
- legacy: getWallSurfaceMaterialSignature(getEffectiveWallFaceMaterial(wallNode, face)),
124
+ legacy: getWallSurfaceMaterialSignature(spec),
125
+ preset: spec.materialPreset ? materialPresetRefSignature(spec.materialPreset) : null,
96
126
  // Changing the assembly's cladding must re-skin the face.
97
- assemblyFinish: wallAssemblyFaceFinishRef(wallNode, face),
127
+ assemblyFinish: assemblyRef ? materialPresetRefSignature(assemblyRef) : null,
128
+ // So must baring or covering its structure.
129
+ structure: assemblyRef ? null : wallFaceExposedStructure(wallNode, face),
98
130
  });
99
131
  }
100
132
  function resolveRefColor(ref, sceneMaterials, fallback) {
@@ -121,6 +153,9 @@ function resolveWallFaceColor(wallNode, face, sceneMaterials, fallback) {
121
153
  if (assemblyRef && !(spec.materialPreset || spec.material)) {
122
154
  return getMaterialPresetByRef(assemblyRef)?.mapProperties?.color ?? fallback;
123
155
  }
156
+ const exposed = assemblyRef ? null : wallFaceExposedStructure(wallNode, face);
157
+ if (exposed && !(spec.materialPreset || spec.material))
158
+ return STRUCTURE_COLORS[exposed];
124
159
  return getSurfaceColor(spec, fallback);
125
160
  }
126
161
  function getSurfaceColor(spec, fallback = DEFAULT_WALL_COLOR) {
@@ -1 +1 @@
1
- {"version":3,"file":"wall-placeholder-sweep.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-placeholder-sweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,eAAO,MAAM,+BAA+B,KAAK,CAAA;AAEjD,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACrD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;CAC/D,GAAG,IAAI,CAAA;AAER,4EAA4E;AAC5E,eAAO,MAAM,yBAAyB,GAAI,UAAU,YAAY,KAAG,OAMlE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,8CAK/B;IACD,4DAA4D;IAC5D,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzB,qEAAqE;IACrE,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,YAAY,CAAA;IAC5C,iEAAiE;IACjE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IACpC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC,KAAG,MAAM,EAST,CAAA"}
1
+ {"version":3,"file":"wall-placeholder-sweep.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-placeholder-sweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,eAAO,MAAM,+BAA+B,KAAK,CAAA;AAEjD,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACrD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;CAC/D,GAAG,IAAI,CAAA;AAER,4EAA4E;AAC5E,eAAO,MAAM,yBAAyB,aAAc,YAAY,KAAG,OAMlE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,iDAK3B;IACD,4DAA4D;IAC5D,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzB,qEAAqE;IACrE,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,YAAY,CAAA;IAC5C,iEAAiE;IACjE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IACpC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC,KAAG,MAAM,EAST,CAAA"}
@@ -49,6 +49,11 @@ export declare function runWallBuildFrame(geometryAdapter?: WallGeometryAdapter)
49
49
  * never touch their walls, so ordinary room edits cost nothing here.
50
50
  */
51
51
  export declare function markWallsForZoneFinishChanges(): void;
52
+ /**
53
+ * The children a wall builds around, as they stand live: an opening still waiting for its
54
+ * construction reveal is left out, so the wall rises whole and is cut at the opening's turn.
55
+ */
56
+ export declare function wallGeometryChildren(node: WallNode, nodes: Readonly<Record<string, AnyNode>>): AnyNode[];
52
57
  type WallTerrainBottomSampler = (x: number, z: number) => number | null;
53
58
  export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number, baseElevation?: number, baseSegments?: readonly WallSlabSupportSegment[], storeyHeight?: number, terrainBottomAt?: WallTerrainBottomSampler, faceBase?: {
54
59
  a: readonly WallSlabSupportSegment[];
@@ -1 +1 @@
1
- {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { buildWallFinishLayout, containsPoint, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getOpeningWallCut, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallLevelZones, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, isCutterName, pointToKey, resolveCutterHost, resolveLevelId, resolveWallFaceBottom, resolveWallFinish, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, WALL_SURFACE_SLOT_DEFAULTS, wallFinishMaterialIndex, zoneHasWallFinish, } from '@pascal-app/core';
1
+ import { buildWallFinishLayout, containsPoint, cornerPair, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getOpeningWallCut, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallLevelZones, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, isCutterName, pointToKey, resolveCutterHost, resolveLevelId, resolveWallFaceBottom, resolveWallFinish, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, WALL_SURFACE_SLOT_DEFAULTS, wallFinishMaterialIndex, zoneHasWallFinish, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import { useEffect } from 'react';
4
4
  import * as THREE from 'three';
@@ -8,7 +8,9 @@ import { computeBoundsTree } from 'three-mesh-bvh';
8
8
  import { ensureRenderableGeometryAttributes, prepareBrushForCSG } from '../../lib/csg-utils';
9
9
  import { setGroupsSortedByMaterial } from '../../lib/geometry-groups';
10
10
  import { timeSpan } from '../../lib/perf-tracks';
11
+ import { isRevealWaiting } from '../../lib/reveal-pending';
11
12
  import { buildTerrainPerimeterFillGeometry } from '../../lib/terrain-perimeter-fill';
13
+ import { cornerFrameStale } from '../window/corner-frame';
12
14
  import { clearLevelMiterCache, getCachedLevelMiters } from './level-miter-cache';
13
15
  import { buildOpeningCutoutGeometry, getOpeningCutoutBottomPadding, } from './opening-cutout-geometry';
14
16
  import { drainStats, endInitialBuild, initiallyBuiltWalls, isWallInitialBuildActive, pendingAdjacentByLevel, publishWallDrainStats, } from './wall-build-lifecycle';
@@ -27,7 +29,7 @@ const WALL_BAND_SPLIT_EPSILON = 1e-5;
27
29
  function computeGeometryBoundsTree(geometry) {
28
30
  ;
29
31
  geometry.computeBoundsTree = computeBoundsTree;
30
- geometry.computeBoundsTree({ maxLeafSize: 10 });
32
+ geometry.computeBoundsTree({ targetLeafSize: 10 });
31
33
  }
32
34
  function csgGeometry(brush) {
33
35
  return brush.geometry;
@@ -880,21 +882,11 @@ function updateWallGeometry(wallId, miterData, geometryAdapter) {
880
882
  return;
881
883
  const levelId = resolveLevelId(node, nodes);
882
884
  // Covering-clamped plane: a flush/thick slab on the level above shortens
883
- // the plane-bound walls below it (explicit-height walls ignore the value).
885
+ // the plane-bound walls below it.
884
886
  const planeTop = getWallPlaneTop(node, levelId, nodes);
885
887
  const slabSupport = spatialGridManager.getSlabSupportForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness, node.supportSlabId, undefined, node.supportOffset, node.justification);
886
888
  const slabElevation = slabSupport.elevation;
887
- const childrenIds = node.children || [];
888
- const childrenNodes = childrenIds
889
- .map((childId) => nodes[childId])
890
- .filter((n) => n !== undefined)
891
- .map((child) => {
892
- if (child.type !== 'door' && child.type !== 'window')
893
- return child;
894
- const effective = getEffectiveNode(child);
895
- const live = useLiveTransforms.getState().get(child.id);
896
- return live?.position ? { ...effective, position: live.position } : effective;
897
- });
889
+ const childrenNodes = wallGeometryChildren(node, nodes);
898
890
  const prepared = geometryAdapter?.prepareChildren?.(node, childrenNodes, {
899
891
  isLive: (id) => useLiveNodeOverrides.getState().get(id) !== undefined ||
900
892
  useLiveTransforms.getState().get(id) !== undefined,
@@ -948,6 +940,27 @@ function updateWallGeometry(wallId, miterData, geometryAdapter) {
948
940
  useScene.getState().markDirty(child.id);
949
941
  }
950
942
  mesh.userData.wallFrameKey = frameKey;
943
+ // A wall moved apart from the other wall of a corner window, or back, makes or breaks the pair:
944
+ // its windows rebuild their frames once (they read the pair only then).
945
+ for (const child of childrenNodes)
946
+ if (cornerFrameStale(child, nodes))
947
+ useScene.getState().markDirty(child.id);
948
+ }
949
+ /**
950
+ * The children a wall builds around, as they stand live: an opening still waiting for its
951
+ * construction reveal is left out, so the wall rises whole and is cut at the opening's turn.
952
+ */
953
+ export function wallGeometryChildren(node, nodes) {
954
+ return (node.children ?? [])
955
+ .map((childId) => nodes[childId])
956
+ .filter((child) => child !== undefined && !isRevealWaiting(child.id))
957
+ .map((child) => {
958
+ if (child.type !== 'door' && child.type !== 'window')
959
+ return child;
960
+ const effective = getEffectiveNode(child);
961
+ const live = useLiveTransforms.getState().get(child.id);
962
+ return live?.position ? { ...effective, position: live.position } : effective;
963
+ });
951
964
  }
952
965
  const WALL_UV_Y_AXIS = new THREE.Vector3(0, 1, 0);
953
966
  const WALL_UV_UNIT_SCALE = new THREE.Vector3(1, 1, 1);
@@ -1200,7 +1213,10 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1200
1213
  ], storeyHeight = DEFAULT_LEVEL_HEIGHT, terrainBottomAt, faceBase, finishLayout = buildWallFinishLayout(wallNode, []), sceneNodes = useScene.getState().nodes) {
1201
1214
  const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
1202
1215
  const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
1203
- const topElevation = resolveWallTop(wallNode, storeyHeight, slabElevation);
1216
+ const levelId = resolveLevelId(wallNode, sceneNodes);
1217
+ const level = sceneNodes[levelId];
1218
+ const authoredStoreyHeight = level?.type === 'level' ? (level.height ?? null) : null;
1219
+ const topElevation = resolveWallTop(wallNode, storeyHeight, slabElevation, authoredStoreyHeight);
1204
1220
  const faceDatum = faceBase;
1205
1221
  if (faceBase)
1206
1222
  faceBase = {
@@ -1555,7 +1571,7 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1555
1571
  child.position[1] + cut.datum - elevation,
1556
1572
  child.position[2],
1557
1573
  ],
1558
- }, wallThickness, getWallBodyCenterOffset(wallNode), cut.bottom - elevation));
1574
+ }, wallThickness, getWallBodyCenterOffset(wallNode), cut.bottom - elevation, child.type === 'window' ? cornerPair(sceneNodes, child)?.end : undefined));
1559
1575
  continue;
1560
1576
  }
1561
1577
  if (cut.band.length < 3)
@@ -1646,13 +1662,20 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1646
1662
  }
1647
1663
  return brushes;
1648
1664
  }
1649
- function createOpeningCutoutBrush(opening, wallThickness, centerOffset, cutBottom) {
1665
+ /**
1666
+ * How far a corner window's cut runs past its wall's end: past this wall's half of the corner at any
1667
+ * angle (the miter stops at five thicknesses), so the two sides open the corner between them. The
1668
+ * cut only takes this wall's own geometry, so running past it touches nothing else.
1669
+ */
1670
+ const CORNER_CUT_REACH = 6;
1671
+ function createOpeningCutoutBrush(opening, wallThickness, centerOffset, cutBottom, cornerEnd) {
1650
1672
  const halfWidth = opening.width / 2;
1651
1673
  const bottom = cutBottom ?? opening.position[1] - opening.height / 2;
1652
1674
  const bottomPadding = getOpeningCutoutBottomPadding(opening, bottom);
1675
+ const reach = CORNER_CUT_REACH * wallThickness;
1653
1676
  const geometry = buildOpeningCutoutGeometry(opening, {
1654
- left: opening.position[0] - halfWidth,
1655
- right: opening.position[0] + halfWidth,
1677
+ left: opening.position[0] - halfWidth - (cornerEnd === 'start' ? reach : 0),
1678
+ right: opening.position[0] + halfWidth + (cornerEnd === 'end' ? reach : 0),
1656
1679
  bottom: bottom - bottomPadding,
1657
1680
  top: opening.position[1] + opening.height / 2,
1658
1681
  }, wallThickness * 2, wallThickness);
@@ -0,0 +1,10 @@
1
+ import { type AnyNode } from '@pascal-app/core';
2
+ /** Where a window mesh keeps the corner side its frame was built fused on (-1, 0 or 1). */
3
+ export declare const FUSED_CORNER_SIDE = "fusedCornerSide";
4
+ /**
5
+ * Whether a window's frame was built for a corner pair that has since been made or broken without
6
+ * the window changing (its partner deleted or moved, a wall moved apart): a build reads the pair
7
+ * once, so the window rebuilds.
8
+ */
9
+ export declare function cornerFrameStale(node: AnyNode, nodes: Readonly<Record<string, AnyNode>>): boolean;
10
+ //# sourceMappingURL=corner-frame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"corner-frame.d.ts","sourceRoot":"","sources":["../../../src/systems/window/corner-frame.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAA4C,MAAM,kBAAkB,CAAA;AAEzF,2FAA2F;AAC3F,eAAO,MAAM,iBAAiB,oBAAoB,CAAA;AAElD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAMjG"}
@@ -0,0 +1,17 @@
1
+ import { cornerPair, fusesAtCorner, sceneRegistry } from '@pascal-app/core';
2
+ /** Where a window mesh keeps the corner side its frame was built fused on (-1, 0 or 1). */
3
+ export const FUSED_CORNER_SIDE = 'fusedCornerSide';
4
+ /**
5
+ * Whether a window's frame was built for a corner pair that has since been made or broken without
6
+ * the window changing (its partner deleted or moved, a wall moved apart): a build reads the pair
7
+ * once, so the window rebuilds.
8
+ */
9
+ export function cornerFrameStale(node, nodes) {
10
+ if (node.type !== 'window')
11
+ return false;
12
+ const mesh = sceneRegistry.nodes.get(node.id);
13
+ if (!mesh)
14
+ return false;
15
+ const fused = cornerPair(nodes, node)?.post === 'none' && fusesAtCorner(node);
16
+ return Boolean(mesh.userData[FUSED_CORNER_SIDE]) !== fused;
17
+ }
@@ -10,7 +10,7 @@ export declare const DOUBLE_HUNG_BOTTOM_SASH_NAME = "double-hung-bottom-sash";
10
10
  export declare const LOUVERED_WINDOW_SLATS_NAME = "louvered-window-slats";
11
11
  export declare const AWNING_WINDOW_SASH_NAME = "awning-window-sash";
12
12
  export declare const HOPPER_WINDOW_SASH_NAME = "hopper-window-sash";
13
- export declare const pendingWindowAnimationRebuilds: Set<`building_${string}` | `level_${string}` | `elevator_${string}` | `unit_${string}` | `block_${string}` | `ceiling_${string}` | `item_${string}` | `procedural-item_${string}` | `column_${string}` | `construction-dimension_${string}` | `wall_${string}` | `roof_${string}` | `slab_${string}` | `separator_${string}` | `floor-opening_${string}` | `fence_${string}` | `structural-grid_${string}` | `imesh_${string}` | `zone_${string}` | `stair_${string}` | `scan_${string}` | `guide_${string}` | `measurement_${string}` | `spawn_${string}` | `shelf_${string}` | `duct-segment_${string}` | `duct-fitting_${string}` | `duct-terminal_${string}` | `hvac-equipment_${string}` | `lineset_${string}` | `liquid-line_${string}` | `pipe-segment_${string}` | `pipe-fitting_${string}` | `pipe-trap_${string}` | `site_${string}` | `leanto_${string}` | `door_${string}` | `window_${string}` | `fence-gate_${string}` | `fence-opening_${string}` | `cabinet_${string}` | `cabinet-module_${string}` | `rseg_${string}` | `sseg_${string}` | `bvent_${string}` | `rvent_${string}` | `tvent_${string}` | `cupola_${string}` | `eyebrow-vent_${string}` | `gutter_${string}` | `chimney_${string}` | `solarpanel_${string}` | `skylight_${string}` | `dormer_${string}` | `downspout_${string}`>;
13
+ export declare const pendingWindowAnimationRebuilds: Set<`block_${string}` | `building_${string}` | `bvent_${string}` | `cabinet-module_${string}` | `cabinet_${string}` | `ceiling_${string}` | `chimney_${string}` | `column_${string}` | `construction-dimension_${string}` | `cupola_${string}` | `door_${string}` | `dormer_${string}` | `downspout_${string}` | `duct-fitting_${string}` | `duct-segment_${string}` | `duct-terminal_${string}` | `elevator_${string}` | `eyebrow-vent_${string}` | `fence-gate_${string}` | `fence-opening_${string}` | `fence_${string}` | `floor-opening_${string}` | `guide_${string}` | `gutter_${string}` | `hvac-equipment_${string}` | `imesh_${string}` | `item_${string}` | `leanto_${string}` | `level_${string}` | `lineset_${string}` | `liquid-line_${string}` | `measurement_${string}` | `pipe-fitting_${string}` | `pipe-segment_${string}` | `pipe-trap_${string}` | `procedural-item_${string}` | `roof_${string}` | `rseg_${string}` | `rvent_${string}` | `scan_${string}` | `separator_${string}` | `shelf_${string}` | `site_${string}` | `skylight_${string}` | `slab_${string}` | `solarpanel_${string}` | `spawn_${string}` | `sseg_${string}` | `stair_${string}` | `structural-grid_${string}` | `tvent_${string}` | `unit_${string}` | `wall_${string}` | `window_${string}` | `zone_${string}`>;
14
14
  export declare const WindowSystem: () => null;
15
15
  /**
16
16
  * Build a fresh window mesh for preview/ghost rendering.
@@ -1 +1 @@
1
- {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAcL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA2B9B,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,8BAA8B,8BAA8B,CAAA;AACzE,eAAO,MAAM,+BAA+B,+BAA+B,CAAA;AAC3E,eAAO,MAAM,gCAAgC,gCAAgC,CAAA;AAC7E,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AACjE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAS3D,eAAO,MAAM,8BAA8B,gvCAAuB,CAAA;AAElE,eAAO,MAAM,YAAY,YAuHxB,CAAA;AA29GD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC,IAAI,CAInE"}
1
+ {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAeL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA4B9B,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,8BAA8B,8BAA8B,CAAA;AACzE,eAAO,MAAM,+BAA+B,+BAA+B,CAAA;AAC3E,eAAO,MAAM,gCAAgC,gCAAgC,CAAA;AAC7E,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AACjE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAS3D,eAAO,MAAM,8BAA8B,gvCAAuB,CAAA;AAElE,eAAO,MAAM,YAAY,YA2HxB,CAAA;AA8+GD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC,IAAI,CAInE"}
@@ -1,4 +1,4 @@
1
- import { DEFAULT_WALL_THICKNESS, getEffectiveNode, getOpeningWallPlacement, getWallThickness, sceneRegistry, scriptedSize, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
1
+ import { cornerPair, DEFAULT_WALL_THICKNESS, getEffectiveNode, getOpeningWallPlacement, getWallThickness, sceneRegistry, scriptedSize, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import { useEffect, useRef } from 'react';
4
4
  import * as THREE from 'three';
@@ -8,6 +8,7 @@ import { timeSpan } from '../../lib/perf-tracks';
8
8
  import { settleScriptedOpening } from '../../lib/scripted-opening';
9
9
  import useViewer from '../../store/use-viewer';
10
10
  import { getOpeningCutoutProxyDepth } from '../wall/opening-cutout-geometry';
11
+ import { cornerFrameStale, FUSED_CORNER_SIDE } from './corner-frame';
11
12
  // Invisible material for root mesh — used as selection hitbox only
12
13
  const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
13
14
  let baseMaterial = getBaseMaterial();
@@ -137,6 +138,11 @@ export const WindowSystem = () => {
137
138
  continue;
138
139
  clearDirty(id);
139
140
  rebuiltWindowsThisFrame += 1;
141
+ // This window's change can make or break its pair: the partner's frame follows.
142
+ const partnerId = node.corner?.partnerId;
143
+ const partner = partnerId ? nodes[partnerId] : undefined;
144
+ if (partner && cornerFrameStale(partner, nodes))
145
+ useScene.getState().markDirty(partner.id);
140
146
  // Rebuild the parent wall so its cutout reflects the updated window geometry
141
147
  // Avoid triggering expensive wall CSG rebuilds while the window is being interactively moved/duplicated.
142
148
  // The editor tools will request a final wall rebuild on commit.
@@ -1526,8 +1532,21 @@ function addShapedLouveredWindowVisuals(node, mesh) {
1526
1532
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
1527
1533
  }
1528
1534
  }
1535
+ /**
1536
+ * The side of a corner window fused at the corner, in the window's own frame: 1 its +x side, -1 its
1537
+ * -x side, 0 none. A window facing its wall's back is turned half round, so its +x then points to
1538
+ * the wall's start.
1539
+ */
1540
+ function fusedCornerSide(node, rotationY) {
1541
+ const pair = cornerPair(useScene.getState().nodes, node);
1542
+ if (pair?.post !== 'none')
1543
+ return 0;
1544
+ const flipped = Math.abs(rotationY) > Math.PI / 2;
1545
+ return (pair.end === 'end') !== flipped ? 1 : -1;
1546
+ }
1529
1547
  function updateWindowMesh(node, mesh) {
1530
1548
  currentWindowSlot = undefined;
1549
+ mesh.userData[FUSED_CORNER_SIDE] = 0;
1531
1550
  // Root mesh is an invisible hitbox; all visuals live in child meshes
1532
1551
  mesh.geometry.dispose();
1533
1552
  mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
@@ -1618,7 +1637,11 @@ function updateWindowMesh(node, mesh) {
1618
1637
  syncWindowCutout(node, mesh);
1619
1638
  return true;
1620
1639
  }
1621
- const innerW = width - 2 * frameThickness;
1640
+ // A corner window fused at the corner: no jamb on that side, the glass runs to the corner.
1641
+ const fused = fusedCornerSide(node, placement.rotation[1]);
1642
+ mesh.userData[FUSED_CORNER_SIDE] = fused;
1643
+ const innerW = width - (fused ? 1 : 2) * frameThickness;
1644
+ const innerX = (fused * frameThickness) / 2;
1622
1645
  const innerH = height - 2 * frameThickness;
1623
1646
  // ── Frame members ──
1624
1647
  // Top / bottom — full width
@@ -1626,8 +1649,10 @@ function updateWindowMesh(node, mesh) {
1626
1649
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
1627
1650
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
1628
1651
  // Left / right — inner height to avoid corner overlap
1629
- addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
1630
- addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
1652
+ if (fused !== -1)
1653
+ addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
1654
+ if (fused !== 1)
1655
+ addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
1631
1656
  // ── Pane grid ──
1632
1657
  const numCols = columnRatios.length;
1633
1658
  const numRows = rowRatios.length;
@@ -1639,7 +1664,7 @@ function updateWindowMesh(node, mesh) {
1639
1664
  const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
1640
1665
  // Compute column x-centers starting from left edge of inner area
1641
1666
  const colXCenters = [];
1642
- let cx = -innerW / 2;
1667
+ let cx = innerX - innerW / 2;
1643
1668
  for (let c = 0; c < numCols; c++) {
1644
1669
  colXCenters.push(cx + colWidths[c] / 2);
1645
1670
  cx += colWidths[c];
@@ -1656,7 +1681,7 @@ function updateWindowMesh(node, mesh) {
1656
1681
  cy -= rowDividerThickness;
1657
1682
  }
1658
1683
  // Column dividers — full inner height
1659
- cx = -innerW / 2;
1684
+ cx = innerX - innerW / 2;
1660
1685
  currentWindowSlot = 'frame';
1661
1686
  for (let c = 0; c < numCols - 1; c++) {
1662
1687
  cx += colWidths[c];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "prepublishOnly": "npm run build"
34
34
  },
35
35
  "peerDependencies": {
36
- "@pascal-app/core": "^1.0.4",
36
+ "@pascal-app/core": "^1.0.5",
37
37
  "@react-three/drei": "^10",
38
38
  "@react-three/fiber": "^9",
39
39
  "react": "^18 || ^19",
@@ -42,18 +42,18 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "three-bvh-csg": "^0.0.18",
45
- "three-mesh-bvh": "^0.9.8",
45
+ "three-mesh-bvh": "~0.9.16",
46
46
  "zustand": "^5"
47
47
  },
48
48
  "devDependencies": {
49
- "@pascal-app/core": "^1.0.4",
49
+ "@pascal-app/core": "^1.0.5",
50
50
  "@pascal/typescript-config": "*",
51
- "@react-three/test-renderer": "^9.1.0",
52
- "@types/node": "^22",
51
+ "@react-three/test-renderer": "^9.1.1",
52
+ "@types/node": "^26",
53
53
  "@types/react": "19.3.0",
54
54
  "@types/react-dom": "19.3.0",
55
- "@types/three": "^0.184.0",
56
- "typescript": "6.0.3"
55
+ "@types/three": "^0.186.0",
56
+ "typescript": "7.0.2"
57
57
  },
58
58
  "keywords": [
59
59
  "3d",