@pascal-app/viewer 0.7.0 → 0.9.1

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 (216) hide show
  1. package/dist/components/renderers/node-renderer.d.ts +1 -1
  2. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.js +38 -22
  4. package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
  5. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
  6. package/dist/components/renderers/parametric-node-renderer.js +34 -0
  7. package/dist/components/renderers/scene-renderer.d.ts +1 -1
  8. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  9. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  10. package/dist/components/viewer/frame-limiter.js +5 -1
  11. package/dist/components/viewer/ground-occluder.d.ts +1 -1
  12. package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
  13. package/dist/components/viewer/ground-occluder.js +12 -24
  14. package/dist/components/viewer/index.d.ts +38 -1
  15. package/dist/components/viewer/index.d.ts.map +1 -1
  16. package/dist/components/viewer/index.js +183 -60
  17. package/dist/components/viewer/lights.d.ts +1 -1
  18. package/dist/components/viewer/lights.d.ts.map +1 -1
  19. package/dist/components/viewer/lights.js +170 -43
  20. package/dist/components/viewer/perf-monitor.d.ts +1 -1
  21. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  22. package/dist/components/viewer/perf-monitor.js +99 -11
  23. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  24. package/dist/components/viewer/post-processing.js +217 -61
  25. package/dist/components/viewer/registered-systems.d.ts +12 -0
  26. package/dist/components/viewer/registered-systems.d.ts.map +1 -0
  27. package/dist/components/viewer/registered-systems.js +47 -0
  28. package/dist/components/viewer/scene-bvh.d.ts +17 -0
  29. package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
  30. package/dist/components/viewer/scene-bvh.js +94 -0
  31. package/dist/components/viewer/selection-manager.d.ts +1 -1
  32. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  33. package/dist/components/viewer/selection-manager.js +20 -5
  34. package/dist/components/viewer/viewer-camera.d.ts +1 -1
  35. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  36. package/dist/components/viewer/walkthrough-controls.d.ts +1 -1
  37. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  38. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  39. package/dist/hooks/use-node-events.d.ts +5 -73
  40. package/dist/hooks/use-node-events.d.ts.map +1 -1
  41. package/dist/hooks/use-node-events.js +19 -7
  42. package/dist/index.d.ts +36 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +67 -3
  45. package/dist/lib/csg-utils.d.ts +16 -0
  46. package/dist/lib/csg-utils.d.ts.map +1 -0
  47. package/dist/lib/csg-utils.js +33 -0
  48. package/dist/lib/drawable-geometry.d.ts +21 -0
  49. package/dist/lib/drawable-geometry.d.ts.map +1 -0
  50. package/dist/lib/drawable-geometry.js +22 -0
  51. package/dist/lib/edge-style.d.ts +3 -0
  52. package/dist/lib/edge-style.d.ts.map +1 -0
  53. package/dist/lib/edge-style.js +11 -0
  54. package/dist/lib/gpu-perf.d.ts +4 -0
  55. package/dist/lib/gpu-perf.d.ts.map +1 -0
  56. package/dist/lib/gpu-perf.js +23 -0
  57. package/dist/lib/ink-edges.d.ts +9 -0
  58. package/dist/lib/ink-edges.d.ts.map +1 -0
  59. package/dist/lib/ink-edges.js +43 -0
  60. package/dist/lib/isolation.d.ts +39 -0
  61. package/dist/lib/isolation.d.ts.map +1 -0
  62. package/dist/lib/isolation.js +103 -0
  63. package/dist/lib/layers.d.ts +18 -0
  64. package/dist/lib/layers.d.ts.map +1 -1
  65. package/dist/lib/layers.js +18 -0
  66. package/dist/lib/materials.d.ts +25 -15
  67. package/dist/lib/materials.d.ts.map +1 -1
  68. package/dist/lib/materials.js +277 -63
  69. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  70. package/dist/lib/merged-outline-node.js +11 -11
  71. package/dist/lib/polygon-union.d.ts +4 -0
  72. package/dist/lib/polygon-union.d.ts.map +1 -0
  73. package/dist/lib/polygon-union.js +289 -0
  74. package/dist/lib/polygon-union.test.d.ts +2 -0
  75. package/dist/lib/polygon-union.test.d.ts.map +1 -0
  76. package/dist/lib/polygon-union.test.js +105 -0
  77. package/dist/lib/scene-themes.d.ts +29 -0
  78. package/dist/lib/scene-themes.d.ts.map +1 -0
  79. package/dist/lib/scene-themes.js +193 -0
  80. package/dist/store/use-viewer.d.ts +42 -4
  81. package/dist/store/use-viewer.d.ts.map +1 -1
  82. package/dist/store/use-viewer.js +26 -3
  83. package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
  84. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  85. package/dist/systems/ceiling/ceiling-system.js +62 -12
  86. package/dist/systems/door/door-system.d.ts.map +1 -1
  87. package/dist/systems/door/door-system.js +373 -22
  88. package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
  89. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
  90. package/dist/systems/elevator/elevator-interaction-system.js +62 -0
  91. package/dist/systems/fence/fence-system.d.ts +3 -0
  92. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  93. package/dist/systems/fence/fence-system.js +10 -6
  94. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
  95. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
  96. package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
  97. package/dist/systems/geometry/geometry-system.d.ts +34 -0
  98. package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
  99. package/dist/systems/geometry/geometry-system.js +279 -0
  100. package/dist/systems/guide/guide-system.d.ts.map +1 -1
  101. package/dist/systems/guide/guide-system.js +1 -26
  102. package/dist/systems/interactive/interactive-system.d.ts +1 -1
  103. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  104. package/dist/systems/item/item-system.d.ts +7 -0
  105. package/dist/systems/item/item-system.d.ts.map +1 -1
  106. package/dist/systems/item/item-system.js +8 -11
  107. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  108. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  109. package/dist/systems/item-light/item-light-system.js +18 -5
  110. package/dist/systems/roof/roof-materials.d.ts +3 -2
  111. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  112. package/dist/systems/roof/roof-materials.js +38 -14
  113. package/dist/systems/roof/roof-system.d.ts +27 -0
  114. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  115. package/dist/systems/roof/roof-system.js +411 -71
  116. package/dist/systems/scan/scan-system.d.ts.map +1 -1
  117. package/dist/systems/scan/scan-system.js +1 -26
  118. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  119. package/dist/systems/slab/slab-system.js +167 -49
  120. package/dist/systems/slab/slab-system.test.d.ts +2 -0
  121. package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
  122. package/dist/systems/slab/slab-system.test.js +62 -0
  123. package/dist/systems/stair/stair-materials.d.ts +4 -3
  124. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  125. package/dist/systems/stair/stair-materials.js +30 -15
  126. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  127. package/dist/systems/stair/stair-system.js +58 -71
  128. package/dist/systems/surface-hole-geometry.d.ts +3 -0
  129. package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
  130. package/dist/systems/surface-hole-geometry.js +4 -0
  131. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  132. package/dist/systems/wall/wall-cutout.js +18 -2
  133. package/dist/systems/wall/wall-materials.d.ts +4 -3
  134. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  135. package/dist/systems/wall/wall-materials.js +52 -28
  136. package/dist/systems/wall/wall-system.d.ts +1 -1
  137. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  138. package/dist/systems/wall/wall-system.js +179 -31
  139. package/dist/systems/window/window-system.d.ts.map +1 -1
  140. package/dist/systems/window/window-system.js +71 -7
  141. package/package.json +4 -5
  142. package/dist/components/renderers/building/building-renderer.d.ts +0 -5
  143. package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
  144. package/dist/components/renderers/building/building-renderer.js +0 -11
  145. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
  146. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
  147. package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
  148. package/dist/components/renderers/column/column-renderer.d.ts +0 -5
  149. package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
  150. package/dist/components/renderers/column/column-renderer.js +0 -594
  151. package/dist/components/renderers/door/door-renderer.d.ts +0 -5
  152. package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
  153. package/dist/components/renderers/door/door-renderer.js +0 -16
  154. package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
  155. package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
  156. package/dist/components/renderers/fence/fence-renderer.js +0 -29
  157. package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
  158. package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
  159. package/dist/components/renderers/guide/guide-renderer.js +0 -38
  160. package/dist/components/renderers/item/item-renderer.d.ts +0 -5
  161. package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
  162. package/dist/components/renderers/item/item-renderer.js +0 -172
  163. package/dist/components/renderers/level/level-renderer.d.ts +0 -5
  164. package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
  165. package/dist/components/renderers/level/level-renderer.js +0 -11
  166. package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
  167. package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
  168. package/dist/components/renderers/roof/roof-materials.js +0 -16
  169. package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
  170. package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
  171. package/dist/components/renderers/roof/roof-renderer.js +0 -32
  172. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
  173. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
  174. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
  175. package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
  176. package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
  177. package/dist/components/renderers/scan/scan-renderer.js +0 -54
  178. package/dist/components/renderers/site/site-renderer.d.ts +0 -5
  179. package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
  180. package/dist/components/renderers/site/site-renderer.js +0 -106
  181. package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
  182. package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
  183. package/dist/components/renderers/slab/slab-renderer.js +0 -58
  184. package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
  185. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
  186. package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
  187. package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
  188. package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
  189. package/dist/components/renderers/stair/stair-renderer.js +0 -603
  190. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
  191. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
  192. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -32
  193. package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
  194. package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
  195. package/dist/components/renderers/wall/wall-renderer.js +0 -16
  196. package/dist/components/renderers/window/window-renderer.d.ts +0 -5
  197. package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
  198. package/dist/components/renderers/window/window-renderer.js +0 -21
  199. package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
  200. package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
  201. package/dist/components/renderers/zone/zone-renderer.js +0 -181
  202. package/dist/lib/suppress-three-clock-warning.d.ts +0 -25
  203. package/dist/lib/suppress-three-clock-warning.d.ts.map +0 -1
  204. package/dist/lib/suppress-three-clock-warning.js +0 -51
  205. package/dist/systems/export/export-system.d.ts +0 -2
  206. package/dist/systems/export/export-system.d.ts.map +0 -1
  207. package/dist/systems/export/export-system.js +0 -60
  208. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts +0 -14
  209. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts.map +0 -1
  210. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.js +0 -155
  211. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts +0 -6
  212. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts.map +0 -1
  213. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.js +0 -75
  214. package/dist/systems/item-mesh-metadata/sync-request.d.ts +0 -7
  215. package/dist/systems/item-mesh-metadata/sync-request.d.ts.map +0 -1
  216. package/dist/systems/item-mesh-metadata/sync-request.js +0 -25
@@ -1,4 +1,4 @@
1
- import { calculateLevelMiters, DEFAULT_WALL_HEIGHT, getAdjacentWallIds, getWallCurveFrameAt, getWallMiterBoundaryPoints, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, sceneRegistry, spatialGridManager, useScene, } from '@pascal-app/core';
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';
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';
@@ -13,6 +13,9 @@ function computeGeometryBoundsTree(geometry) {
13
13
  geometry.computeBoundsTree = computeBoundsTree;
14
14
  geometry.computeBoundsTree({ maxLeafSize: 10 });
15
15
  }
16
+ function csgGeometry(brush) {
17
+ return brush.geometry;
18
+ }
16
19
  function ensureUv2Attribute(geometry) {
17
20
  const uv = geometry.getAttribute('uv');
18
21
  if (!uv)
@@ -20,7 +23,7 @@ function ensureUv2Attribute(geometry) {
20
23
  geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
21
24
  }
22
25
  function insetCurvedWallBoundaryPointsFor3D(wall, boundaryPoints, miterData) {
23
- if (!boundaryPoints || !isCurvedWall(wall)) {
26
+ if (!(boundaryPoints && isCurvedWall(wall))) {
24
27
  return boundaryPoints;
25
28
  }
26
29
  const insetDistance = Math.min(CURVED_WALL_3D_ENDPOINT_INSET, Math.max((wall.thickness ?? 0.1) * 0.01, 0.0005));
@@ -205,49 +208,158 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
205
208
  // WALL SYSTEM
206
209
  // ============================================================================
207
210
  let useFrameNb = 0;
211
+ // ─── Drag-throttle state (singleton — one WallSystem mounted globally) ──
212
+ //
213
+ // Endpoint drags fire `markDirty(wallId)` on every pointermove tick. Without
214
+ // throttling, each tick rebuilds the dragged wall (~1 CSG + miter pass) AND
215
+ // every adjacent wall sharing a corner (3–4× in a t-junction or room).
216
+ // Visible as drag lag, especially on walls with door/window cutouts.
217
+ //
218
+ // Strategy: rebuild the dragged wall every tick (so the drag follows the
219
+ // cursor with full fidelity), but defer adjacent rebuilds to a trailing-
220
+ // edge flush DRAG_FLUSH_MS after the dirty stream stops. Visually, neighbor
221
+ // corners stay at their pre-drag miter until release, then snap into place
222
+ // within ~80ms. Standard CAD-app behavior. Speeds up t-junction drags ~3×,
223
+ // 4-corner-room drags ~4×.
224
+ const DRAG_FLUSH_MS = 80;
225
+ const MAX_WALL_REBUILDS_PER_FRAME = 8;
226
+ const WALL_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WALL_REBUILDS_PER_FRAME;
227
+ const WALL_PROGRESSIVE_TIME_BUDGET_MS = 8;
228
+ let lastWallDirtyAtMs = 0;
229
+ const pendingAdjacentByLevel = new Map();
230
+ function getPendingAdjacentCount() {
231
+ let count = 0;
232
+ for (const ids of pendingAdjacentByLevel.values()) {
233
+ count += ids.size;
234
+ }
235
+ return count;
236
+ }
208
237
  export const WallSystem = () => {
209
238
  const dirtyNodes = useScene((state) => state.dirtyNodes);
210
239
  const clearDirty = useScene((state) => state.clearDirty);
240
+ // Subscribe so override-only changes (no scene write) still re-run
241
+ // this component, which lets the gate below pick up the latest
242
+ // `dirtyNodes` set from the same render pass that received the
243
+ // override-publishing `markDirty` call. Without this, very fast
244
+ // drags could land an override and a markDirty in the same React
245
+ // tick and the next `useFrame` would still see the stale closure.
246
+ useLiveNodeOverrides((s) => s.overrides);
211
247
  useFrame(() => {
212
- if (dirtyNodes.size === 0)
248
+ const hasDirty = dirtyNodes.size > 0;
249
+ const hasPending = pendingAdjacentByLevel.size > 0;
250
+ if (!hasDirty && !hasPending)
213
251
  return;
214
252
  const nodes = useScene.getState().nodes;
253
+ const now = performance.now();
215
254
  // Collect dirty walls and their levels
216
255
  const dirtyWallsByLevel = new Map();
256
+ let dirtyWallCount = 0;
217
257
  useFrameNb += 1;
218
- dirtyNodes.forEach((id) => {
219
- const node = nodes[id];
220
- if (!node || node.type !== 'wall')
221
- return;
222
- const levelId = node.parentId;
223
- if (!levelId)
224
- return;
225
- if (!dirtyWallsByLevel.has(levelId)) {
226
- dirtyWallsByLevel.set(levelId, new Set());
227
- }
228
- dirtyWallsByLevel.get(levelId)?.add(id);
229
- });
258
+ if (hasDirty) {
259
+ dirtyNodes.forEach((id) => {
260
+ const node = nodes[id];
261
+ if (!node || node.type !== 'wall')
262
+ return;
263
+ const levelId = node.parentId;
264
+ if (!levelId)
265
+ return;
266
+ if (!dirtyWallsByLevel.has(levelId)) {
267
+ dirtyWallsByLevel.set(levelId, new Set());
268
+ }
269
+ dirtyWallsByLevel.get(levelId)?.add(id);
270
+ dirtyWallCount += 1;
271
+ });
272
+ }
273
+ const hasDirtyWalls = dirtyWallsByLevel.size > 0;
274
+ if (hasDirtyWalls) {
275
+ lastWallDirtyAtMs = now;
276
+ }
277
+ const useProgressiveWallRebuilds = dirtyWallCount > WALL_PROGRESSIVE_DIRTY_THRESHOLD;
278
+ let rebuiltWallsThisFrame = 0;
279
+ const rebuildFrameStartedAt = now;
230
280
  // Process each level that has dirty walls
231
281
  for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
282
+ if (useProgressiveWallRebuilds && rebuiltWallsThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
283
+ break;
284
+ }
232
285
  const levelWalls = getLevelWalls(levelId);
233
286
  const miterData = calculateLevelMiters(levelWalls);
234
- // Update dirty walls
287
+ const rebuiltWallIds = new Set();
288
+ // Update dirty walls — always, no throttling. The dragged wall must
289
+ // follow the cursor with full fidelity (cutouts and all). Large imports
290
+ // enter the progressive path so initial load can't lock the tab.
235
291
  for (const wallId of dirtyWallIds) {
292
+ if (useProgressiveWallRebuilds) {
293
+ if (rebuiltWallsThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
294
+ break;
295
+ }
296
+ if (rebuiltWallsThisFrame > 0 &&
297
+ performance.now() - rebuildFrameStartedAt >= WALL_PROGRESSIVE_TIME_BUDGET_MS) {
298
+ break;
299
+ }
300
+ }
236
301
  const mesh = sceneRegistry.nodes.get(wallId);
237
302
  if (mesh) {
238
303
  updateWallGeometry(wallId, miterData);
239
304
  clearDirty(wallId);
305
+ rebuiltWallIds.add(wallId);
306
+ rebuiltWallsThisFrame += 1;
240
307
  }
241
308
  // If mesh not found, keep it dirty for next frame
242
309
  }
243
- // Update adjacent walls that share junctions
244
- const adjacentWallIds = getAdjacentWallIds(levelWalls, dirtyWallIds);
310
+ if (rebuiltWallIds.size === 0) {
311
+ continue;
312
+ }
313
+ // Adjacent walls sharing junctions — *defer* during active drag
314
+ // (dirty arrived this frame), flush on the trailing edge.
315
+ const adjacentWallIds = getAdjacentWallIds(levelWalls, rebuiltWallIds);
316
+ let pending = pendingAdjacentByLevel.get(levelId);
317
+ if (!pending) {
318
+ pending = new Set();
319
+ pendingAdjacentByLevel.set(levelId, pending);
320
+ }
245
321
  for (const wallId of adjacentWallIds) {
246
322
  if (!dirtyWallIds.has(wallId)) {
323
+ pending.add(wallId);
324
+ }
325
+ }
326
+ }
327
+ // Trailing-edge flush: if no new dirty marks for DRAG_FLUSH_MS, the
328
+ // drag has ended — rebuild the queued neighbors so corners snap into
329
+ // their correct miter joins.
330
+ const quiet = !hasDirtyWalls && now - lastWallDirtyAtMs >= DRAG_FLUSH_MS;
331
+ if (quiet && pendingAdjacentByLevel.size > 0) {
332
+ const pendingCount = getPendingAdjacentCount();
333
+ const useProgressiveAdjacentRebuilds = pendingCount > WALL_PROGRESSIVE_DIRTY_THRESHOLD;
334
+ let rebuiltAdjacentThisFrame = 0;
335
+ const adjacentFrameStartedAt = performance.now();
336
+ for (const [levelId, pendingIds] of pendingAdjacentByLevel) {
337
+ if (pendingIds.size === 0)
338
+ continue;
339
+ const levelWalls = getLevelWalls(levelId);
340
+ const miterData = calculateLevelMiters(levelWalls);
341
+ for (const wallId of Array.from(pendingIds)) {
342
+ if (useProgressiveAdjacentRebuilds) {
343
+ if (rebuiltAdjacentThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
344
+ break;
345
+ }
346
+ if (rebuiltAdjacentThisFrame > 0 &&
347
+ performance.now() - adjacentFrameStartedAt >= WALL_PROGRESSIVE_TIME_BUDGET_MS) {
348
+ break;
349
+ }
350
+ }
247
351
  const mesh = sceneRegistry.nodes.get(wallId);
248
- if (mesh) {
352
+ if (mesh)
249
353
  updateWallGeometry(wallId, miterData);
250
- }
354
+ pendingIds.delete(wallId);
355
+ rebuiltAdjacentThisFrame += 1;
356
+ }
357
+ if (pendingIds.size === 0) {
358
+ pendingAdjacentByLevel.delete(levelId);
359
+ }
360
+ if (useProgressiveAdjacentRebuilds &&
361
+ rebuiltAdjacentThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
362
+ break;
251
363
  }
252
364
  }
253
365
  }
@@ -255,7 +367,22 @@ export const WallSystem = () => {
255
367
  return null;
256
368
  };
257
369
  /**
258
- * Gets all walls that belong to a level
370
+ * Merge any live override for a wall into the scene record. Lets the
371
+ * 2D move handler publish `{ start, end, curveOffset }` to
372
+ * `useLiveNodeOverrides` and have the geometry / miter pipeline use
373
+ * those values without zustand churn during the drag. When no
374
+ * override is set, the wall is returned unchanged.
375
+ */
376
+ function getEffectiveWall(wall) {
377
+ const override = useLiveNodeOverrides.getState().get(wall.id);
378
+ if (!override || Object.keys(override).length === 0)
379
+ return wall;
380
+ return { ...wall, ...override };
381
+ }
382
+ /**
383
+ * Gets all walls that belong to a level, with any live overrides
384
+ * merged in so miters compute against the cursor-driven positions
385
+ * (not the pre-drag scene state).
259
386
  */
260
387
  function getLevelWalls(levelId) {
261
388
  const { nodes } = useScene.getState();
@@ -266,28 +393,49 @@ function getLevelWalls(levelId) {
266
393
  for (const childId of level.children) {
267
394
  const child = nodes[childId];
268
395
  if (child?.type === 'wall') {
269
- walls.push(child);
396
+ walls.push(getEffectiveWall(child));
270
397
  }
271
398
  }
272
399
  return walls;
273
400
  }
274
401
  /**
275
- * Updates the geometry for a single wall
402
+ * Updates the geometry for a single wall. Reads the effective node
403
+ * (override-merged) so a 2D drag visibly moves the 3D mesh without
404
+ * having touched `useScene` mid-drag.
276
405
  */
277
406
  function updateWallGeometry(wallId, miterData) {
278
407
  const nodes = useScene.getState().nodes;
279
- const node = nodes[wallId];
280
- if (!node || node.type !== 'wall')
408
+ const sceneNode = nodes[wallId];
409
+ if (!sceneNode || sceneNode.type !== 'wall')
281
410
  return;
411
+ const node = getEffectiveWall(sceneNode);
282
412
  const mesh = sceneRegistry.nodes.get(wallId);
283
413
  if (!mesh)
284
414
  return;
285
415
  const levelId = resolveLevelId(node, nodes);
286
- const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end);
416
+ const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness);
287
417
  const childrenIds = node.children || [];
418
+ // Merge live overrides into door / window children so cutouts track an
419
+ // in-flight resize drag (door width arrow, window height arrow, etc.)
420
+ // without waiting on the scene store. Non-cutout children pass through
421
+ // unchanged.
288
422
  const childrenNodes = childrenIds
289
423
  .map((childId) => nodes[childId])
290
- .filter((n) => n !== undefined);
424
+ .filter((n) => n !== undefined)
425
+ .map((child) => {
426
+ if (child.type !== 'door' && child.type !== 'window')
427
+ return child;
428
+ // `getEffectiveNode` folds in resize overrides (width/height arrows).
429
+ // 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).
433
+ const effective = getEffectiveNode(child);
434
+ const live = useLiveTransforms.getState().get(child.id);
435
+ if (!live?.position)
436
+ return effective;
437
+ return { ...effective, position: live.position };
438
+ });
291
439
  const newGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation);
292
440
  mesh.geometry.dispose();
293
441
  mesh.geometry = newGeo;
@@ -380,16 +528,16 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
380
528
  cutoutBrush.updateMatrixWorld();
381
529
  const newResult = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
382
530
  if (resultBrush !== wallBrush) {
383
- resultBrush.geometry.dispose();
531
+ csgGeometry(resultBrush).dispose();
384
532
  }
385
533
  resultBrush = newResult;
386
534
  }
387
535
  // Clean up
388
- wallBrush.geometry.dispose();
536
+ csgGeometry(wallBrush).dispose();
389
537
  for (const brush of cutoutBrushes) {
390
- brush.geometry.dispose();
538
+ csgGeometry(brush).dispose();
391
539
  }
392
- const resultGeometry = resultBrush.geometry;
540
+ const resultGeometry = csgGeometry(resultBrush);
393
541
  resultGeometry.computeVertexNormals();
394
542
  assignWallMaterialGroups(resultGeometry, wallNode, boundaryEdges);
395
543
  ensureUv2Attribute(resultGeometry);
@@ -1 +1 @@
1
- {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAaA,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;AAE3D,eAAO,MAAM,YAAY,YA2BxB,CAAA"}
1
+ {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAuBA,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;AAM3D,eAAO,MAAM,YAAY,YA0FxB,CAAA"}
@@ -1,9 +1,13 @@
1
- import { sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
1
+ import { getEffectiveNode, sceneRegistry, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
+ import { useEffect, useRef } from 'react';
3
4
  import * as THREE from 'three';
4
- import { baseMaterial, glassMaterial } from '../../lib/materials';
5
+ import { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, } from '../../lib/materials';
6
+ import useViewer from '../../store/use-viewer';
5
7
  // Invisible material for root mesh — used as selection hitbox only
6
8
  const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
9
+ let baseMaterial = getBaseMaterial();
10
+ let glassMaterial = defaultGlassMaterial;
7
11
  export const CASEMENT_WINDOW_SASH_NAME = 'casement-window-sash';
8
12
  export const FRENCH_CASEMENT_LEFT_SASH_NAME = 'french-casement-left-sash';
9
13
  export const FRENCH_CASEMENT_RIGHT_SASH_NAME = 'french-casement-right-sash';
@@ -14,27 +18,87 @@ export const DOUBLE_HUNG_BOTTOM_SASH_NAME = 'double-hung-bottom-sash';
14
18
  export const LOUVERED_WINDOW_SLATS_NAME = 'louvered-window-slats';
15
19
  export const AWNING_WINDOW_SASH_NAME = 'awning-window-sash';
16
20
  export const HOPPER_WINDOW_SASH_NAME = 'hopper-window-sash';
21
+ const MAX_WINDOW_REBUILDS_PER_FRAME = 16;
22
+ const WINDOW_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WINDOW_REBUILDS_PER_FRAME;
23
+ const WINDOW_PROGRESSIVE_TIME_BUDGET_MS = 8;
17
24
  export const WindowSystem = () => {
18
25
  const dirtyNodes = useScene((state) => state.dirtyNodes);
19
26
  const clearDirty = useScene((state) => state.clearDirty);
27
+ const shading = useViewer((state) => state.shading);
28
+ const textures = useViewer((state) => state.textures);
29
+ const colorPreset = useViewer((state) => state.colorPreset);
30
+ const materialRevisionRef = useRef(null);
31
+ // Subscribe so override-only updates re-run this component. Mirrors
32
+ // WallSystem + DoorSystem.
33
+ useLiveNodeOverrides((s) => s.overrides);
34
+ baseMaterial = textures
35
+ ? getBaseMaterial(shading)
36
+ : createSurfaceRoleMaterial('joinery', colorPreset);
37
+ glassMaterial = textures
38
+ ? defaultGlassMaterial
39
+ : createSurfaceRoleMaterial('glazing', colorPreset);
40
+ useEffect(() => {
41
+ const materialRevision = `${shading}:${textures ? 'textures' : 'solid'}:${colorPreset}`;
42
+ if (materialRevisionRef.current === materialRevision)
43
+ return;
44
+ materialRevisionRef.current = materialRevision;
45
+ const nodes = useScene.getState().nodes;
46
+ for (const node of Object.values(nodes)) {
47
+ if (node?.type === 'window') {
48
+ useScene.getState().dirtyNodes.add(node.id);
49
+ }
50
+ }
51
+ });
20
52
  useFrame(() => {
21
53
  if (dirtyNodes.size === 0)
22
54
  return;
55
+ baseMaterial = textures
56
+ ? getBaseMaterial(shading)
57
+ : createSurfaceRoleMaterial('joinery', colorPreset);
58
+ glassMaterial = textures
59
+ ? defaultGlassMaterial
60
+ : createSurfaceRoleMaterial('glazing', colorPreset);
23
61
  const nodes = useScene.getState().nodes;
62
+ const dirtyWindowIds = [];
24
63
  dirtyNodes.forEach((id) => {
25
64
  const node = nodes[id];
26
65
  if (!node || node.type !== 'window')
27
66
  return;
67
+ dirtyWindowIds.push(id);
68
+ });
69
+ const useProgressiveWindowRebuilds = dirtyWindowIds.length > WINDOW_PROGRESSIVE_DIRTY_THRESHOLD;
70
+ const frameStartedAt = performance.now();
71
+ let rebuiltWindowsThisFrame = 0;
72
+ for (const id of dirtyWindowIds) {
73
+ if (useProgressiveWindowRebuilds) {
74
+ if (rebuiltWindowsThisFrame >= MAX_WINDOW_REBUILDS_PER_FRAME) {
75
+ break;
76
+ }
77
+ if (rebuiltWindowsThisFrame > 0 &&
78
+ performance.now() - frameStartedAt >= WINDOW_PROGRESSIVE_TIME_BUDGET_MS) {
79
+ break;
80
+ }
81
+ }
82
+ const node = nodes[id];
83
+ if (!node || node.type !== 'window')
84
+ continue;
28
85
  const mesh = sceneRegistry.nodes.get(id);
29
86
  if (!mesh)
30
- return; // Keep dirty until mesh mounts
31
- updateWindowMesh(node, mesh);
87
+ continue; // Keep dirty until mesh mounts
88
+ // Merge any live override (width / height / position) so the mesh
89
+ // rebuild reflects the in-flight drag without zustand churn.
90
+ const effectiveNode = getEffectiveNode(node);
91
+ updateWindowMesh(effectiveNode, mesh);
32
92
  clearDirty(id);
93
+ rebuiltWindowsThisFrame += 1;
33
94
  // Rebuild the parent wall so its cutout reflects the updated window geometry
34
- if (node.parentId) {
35
- useScene.getState().dirtyNodes.add(node.parentId);
95
+ // Avoid triggering expensive wall CSG rebuilds while the window is being interactively moved/duplicated.
96
+ // The editor tools will request a final wall rebuild on commit.
97
+ const isTransient = !!node.metadata?.isTransient;
98
+ if (!isTransient && effectiveNode.parentId) {
99
+ useScene.getState().dirtyNodes.add(effectiveNode.parentId);
36
100
  }
37
- });
101
+ }
38
102
  }, 3);
39
103
  return null;
40
104
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "0.7.0",
3
+ "version": "0.9.1",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,24 +22,23 @@
22
22
  "prepublishOnly": "npm run build"
23
23
  },
24
24
  "peerDependencies": {
25
- "@pascal-app/core": "^0.7.0",
25
+ "@pascal-app/core": "^0.9.1",
26
26
  "@react-three/drei": "^10",
27
27
  "@react-three/fiber": "^9",
28
28
  "react": "^18 || ^19",
29
29
  "three": "^0.184"
30
30
  },
31
31
  "dependencies": {
32
- "polygon-clipping": "^0.15.7",
33
32
  "three-bvh-csg": "^0.0.18",
34
33
  "three-mesh-bvh": "^0.9.8",
35
34
  "zustand": "^5"
36
35
  },
37
36
  "devDependencies": {
38
37
  "@pascal/typescript-config": "*",
39
- "@types/node": "^25.5.0",
38
+ "@types/node": "^22",
40
39
  "@types/react": "^19.2.2",
41
40
  "@types/three": "^0.184.0",
42
- "typescript": "5.9.3"
41
+ "typescript": "6.0.3"
43
42
  },
44
43
  "keywords": [
45
44
  "3d",
@@ -1,5 +0,0 @@
1
- import { type BuildingNode } from '@pascal-app/core';
2
- export declare const BuildingRenderer: ({ node }: {
3
- node: BuildingNode;
4
- }) => import("react/jsx-runtime").JSX.Element;
5
- //# sourceMappingURL=building-renderer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"building-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/building/building-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,kBAAkB,CAAA;AAMjE,eAAO,MAAM,gBAAgB,GAAI,UAAU;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,4CAkBhE,CAAA"}
@@ -1,11 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useRegistry } from '@pascal-app/core';
3
- import { useRef } from 'react';
4
- import { useNodeEvents } from '../../../hooks/use-node-events';
5
- import { NodeRenderer } from '../node-renderer';
6
- export const BuildingRenderer = ({ node }) => {
7
- const ref = useRef(null);
8
- useRegistry(node.id, node.type, ref);
9
- const handlers = useNodeEvents(node, 'building');
10
- return (_jsx("group", { position: node.position, ref: ref, rotation: [node.rotation[0], node.rotation[1], node.rotation[2]], ...handlers, children: node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) }));
11
- };
@@ -1,5 +0,0 @@
1
- import { type CeilingNode } from '@pascal-app/core';
2
- export declare const CeilingRenderer: ({ node }: {
3
- node: CeilingNode;
4
- }) => import("react/jsx-runtime").JSX.Element;
5
- //# sourceMappingURL=ceiling-renderer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ceiling-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/ceiling/ceiling-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAIjB,MAAM,kBAAkB,CAAA;AA8CzB,eAAO,MAAM,eAAe,GAAI,UAAU;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,4CAoC9D,CAAA"}
@@ -1,58 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { getMaterialPresetByRef, resolveMaterial, useRegistry, } from '@pascal-app/core';
3
- import { useMemo, useRef } from 'react';
4
- import { float, mix, positionWorld, smoothstep } from 'three/tsl';
5
- import { BackSide, FrontSide, MeshBasicNodeMaterial } from 'three/webgpu';
6
- import { useNodeEvents } from '../../../hooks/use-node-events';
7
- import { NodeRenderer } from '../node-renderer';
8
- const gridScale = 5;
9
- const gridX = positionWorld.x.mul(gridScale).fract();
10
- const gridY = positionWorld.z.mul(gridScale).fract();
11
- const lineWidth = 0.05;
12
- const lineX = smoothstep(lineWidth, 0, gridX).add(smoothstep(1.0 - lineWidth, 1.0, gridX));
13
- const lineY = smoothstep(lineWidth, 0, gridY).add(smoothstep(1.0 - lineWidth, 1.0, gridY));
14
- const gridPattern = lineX.max(lineY);
15
- const gridOpacity = mix(float(0.2), float(0.6), gridPattern);
16
- function createCeilingMaterials(color = '#999999') {
17
- const topMaterial = new MeshBasicNodeMaterial({
18
- color,
19
- transparent: true,
20
- depthWrite: false,
21
- side: FrontSide,
22
- });
23
- topMaterial.opacityNode = gridOpacity;
24
- const bottomMaterial = new MeshBasicNodeMaterial({
25
- color,
26
- transparent: true,
27
- side: BackSide,
28
- });
29
- return { topMaterial, bottomMaterial };
30
- }
31
- const ceilingMaterialCache = new Map();
32
- function getCeilingMaterials(color = '#999999') {
33
- const cacheKey = color;
34
- const cached = ceilingMaterialCache.get(cacheKey);
35
- if (cached)
36
- return cached;
37
- const materials = createCeilingMaterials(color);
38
- ceilingMaterialCache.set(cacheKey, materials);
39
- return materials;
40
- }
41
- export const CeilingRenderer = ({ node }) => {
42
- const ref = useRef(null);
43
- useRegistry(node.id, 'ceiling', ref);
44
- const handlers = useNodeEvents(node, 'ceiling');
45
- const materials = useMemo(() => {
46
- const preset = getMaterialPresetByRef(node.materialPreset);
47
- const props = preset?.mapProperties ?? resolveMaterial(node.material);
48
- const color = props.color || '#999999';
49
- return getCeilingMaterials(color);
50
- }, [
51
- node.materialPreset,
52
- node.material,
53
- node.material?.preset,
54
- node.material?.properties,
55
- node.material?.texture,
56
- ]);
57
- return (_jsxs("mesh", { material: materials.bottomMaterial, ref: ref, children: [_jsx("boxGeometry", { args: [0, 0, 0] }), _jsx("mesh", { material: materials.topMaterial, name: "ceiling-grid", ...handlers, scale: 0, visible: false, children: _jsx("boxGeometry", { args: [0, 0, 0] }) }), node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId)))] }));
58
- };
@@ -1,5 +0,0 @@
1
- import { type ColumnNode } from '@pascal-app/core';
2
- export declare const ColumnRenderer: ({ node }: {
3
- node: ColumnNode;
4
- }) => import("react/jsx-runtime").JSX.Element;
5
- //# sourceMappingURL=column-renderer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"column-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/column/column-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAkC,MAAM,kBAAkB,CAAA;AA64ClF,eAAO,MAAM,cAAc,GAAI,UAAU;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,4CA0E5D,CAAA"}