@pascal-app/viewer 1.0.0-beta.4 → 1.0.0-beta.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 (103) hide show
  1. package/README.md +52 -0
  2. package/dist/components/viewer/frame-limiter.d.ts +11 -0
  3. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  4. package/dist/components/viewer/frame-limiter.js +58 -18
  5. package/dist/components/viewer/index.d.ts +15 -0
  6. package/dist/components/viewer/index.d.ts.map +1 -1
  7. package/dist/components/viewer/index.js +34 -50
  8. package/dist/components/viewer/perf-monitor.js +2 -2
  9. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  10. package/dist/components/viewer/selection-manager.js +7 -2
  11. package/dist/components/viewer/unsupported-gpu-fallback.d.ts +2 -0
  12. package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -0
  13. package/dist/components/viewer/unsupported-gpu-fallback.js +4 -0
  14. package/dist/index.d.ts +8 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -4
  17. package/dist/lib/dispose-object3d.d.ts +4 -0
  18. package/dist/lib/dispose-object3d.d.ts.map +1 -0
  19. package/dist/lib/dispose-object3d.js +27 -0
  20. package/dist/lib/geometry-groups.d.ts +17 -0
  21. package/dist/lib/geometry-groups.d.ts.map +1 -0
  22. package/dist/lib/geometry-groups.js +53 -0
  23. package/dist/lib/isolation.d.ts +8 -8
  24. package/dist/lib/isolation.d.ts.map +1 -1
  25. package/dist/lib/isolation.js +18 -30
  26. package/dist/lib/layers.d.ts +16 -0
  27. package/dist/lib/layers.d.ts.map +1 -1
  28. package/dist/lib/layers.js +18 -0
  29. package/dist/lib/renderer-capability.d.ts +56 -0
  30. package/dist/lib/renderer-capability.d.ts.map +1 -0
  31. package/dist/lib/renderer-capability.js +114 -0
  32. package/dist/lib/scene-visibility.d.ts +14 -0
  33. package/dist/lib/scene-visibility.d.ts.map +1 -0
  34. package/dist/lib/scene-visibility.js +48 -0
  35. package/dist/lib/shadow-only.d.ts +16 -0
  36. package/dist/lib/shadow-only.d.ts.map +1 -1
  37. package/dist/lib/shadow-only.js +8 -17
  38. package/dist/lib/snapshot-pipeline.d.ts +9 -0
  39. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  40. package/dist/lib/snapshot-pipeline.js +33 -9
  41. package/dist/store/use-viewer.d.ts +5 -4
  42. package/dist/store/use-viewer.d.ts.map +1 -1
  43. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  44. package/dist/systems/floor-elevation/floor-elevation-system.js +9 -0
  45. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  46. package/dist/systems/geometry/geometry-system.js +2 -26
  47. package/dist/systems/level/level-system.d.ts.map +1 -1
  48. package/dist/systems/level/level-system.js +7 -1
  49. package/dist/systems/wall/level-miter-cache.d.ts +10 -0
  50. package/dist/systems/wall/level-miter-cache.d.ts.map +1 -0
  51. package/dist/systems/wall/level-miter-cache.js +40 -0
  52. package/dist/systems/wall/wall-cutout.d.ts +12 -0
  53. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  54. package/dist/systems/wall/wall-cutout.js +41 -11
  55. package/dist/systems/wall/wall-system.d.ts +4 -0
  56. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  57. package/dist/systems/wall/wall-system.js +31 -18
  58. package/package.json +4 -2
  59. package/dist/components/viewer/scene-bvh.test.d.ts +0 -2
  60. package/dist/components/viewer/scene-bvh.test.d.ts.map +0 -1
  61. package/dist/components/viewer/scene-bvh.test.js +0 -13
  62. package/dist/lib/csg-utils.test.d.ts +0 -2
  63. package/dist/lib/csg-utils.test.d.ts.map +0 -1
  64. package/dist/lib/csg-utils.test.js +0 -45
  65. package/dist/lib/materials.test.d.ts +0 -2
  66. package/dist/lib/materials.test.d.ts.map +0 -1
  67. package/dist/lib/materials.test.js +0 -25
  68. package/dist/lib/merged-outline-node.test.d.ts +0 -2
  69. package/dist/lib/merged-outline-node.test.d.ts.map +0 -1
  70. package/dist/lib/merged-outline-node.test.js +0 -22
  71. package/dist/lib/polygon-union.test.d.ts +0 -2
  72. package/dist/lib/polygon-union.test.d.ts.map +0 -1
  73. package/dist/lib/polygon-union.test.js +0 -105
  74. package/dist/lib/texture-reference.test.d.ts +0 -2
  75. package/dist/lib/texture-reference.test.d.ts.map +0 -1
  76. package/dist/lib/texture-reference.test.js +0 -86
  77. package/dist/store/use-viewer.test.d.ts +0 -2
  78. package/dist/store/use-viewer.test.d.ts.map +0 -1
  79. package/dist/store/use-viewer.test.js +0 -61
  80. package/dist/systems/door/door-animation.test.d.ts +0 -2
  81. package/dist/systems/door/door-animation.test.d.ts.map +0 -1
  82. package/dist/systems/door/door-animation.test.js +0 -51
  83. package/dist/systems/door/door-floor-alignment.test.d.ts +0 -2
  84. package/dist/systems/door/door-floor-alignment.test.d.ts.map +0 -1
  85. package/dist/systems/door/door-floor-alignment.test.js +0 -50
  86. package/dist/systems/geometry/geometry-system.test.d.ts +0 -2
  87. package/dist/systems/geometry/geometry-system.test.d.ts.map +0 -1
  88. package/dist/systems/geometry/geometry-system.test.js +0 -30
  89. package/dist/systems/slab/slab-system.test.d.ts +0 -2
  90. package/dist/systems/slab/slab-system.test.d.ts.map +0 -1
  91. package/dist/systems/slab/slab-system.test.js +0 -102
  92. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +0 -2
  93. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +0 -1
  94. package/dist/systems/wall/opening-cutout-geometry.test.js +0 -172
  95. package/dist/systems/wall/wall-opening-cutout.test.d.ts +0 -2
  96. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +0 -1
  97. package/dist/systems/wall/wall-opening-cutout.test.js +0 -59
  98. package/dist/systems/wall/wall-support-extension.test.d.ts +0 -2
  99. package/dist/systems/wall/wall-support-extension.test.d.ts.map +0 -1
  100. package/dist/systems/wall/wall-support-extension.test.js +0 -116
  101. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +0 -2
  102. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +0 -1
  103. package/dist/systems/wall/wall-terrain-bottom.test.js +0 -34
@@ -4,6 +4,7 @@ import type { EdgeMode } from '../lib/edge-style';
4
4
  import type { ColorPreset, RenderShading } from '../lib/materials';
5
5
  export type RenderContext = 'editor' | 'viewer';
6
6
  export type MetricNotation = 'meters' | 'millimeters';
7
+ export type WallMode = 'up' | 'cutaway' | 'down' | 'translucent';
7
8
  type SelectionPath = {
8
9
  buildingId: BuildingNode['id'] | null;
9
10
  levelId: LevelNode['id'] | null;
@@ -70,8 +71,8 @@ type ViewerState = {
70
71
  unitExplicit: boolean;
71
72
  levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
72
73
  setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void;
73
- wallMode: 'up' | 'cutaway' | 'down' | 'translucent';
74
- setWallMode: (mode: 'up' | 'cutaway' | 'down' | 'translucent') => void;
74
+ wallMode: WallMode;
75
+ setWallMode: (mode: WallMode) => void;
75
76
  showScans: boolean;
76
77
  setShowScans: (show: boolean) => void;
77
78
  showGuides: boolean;
@@ -133,7 +134,7 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
133
134
  setOptions: (options: Partial<import("zustand/middleware").PersistOptions<ViewerState, {
134
135
  metricNotation: MetricNotation;
135
136
  levelMode: "manual" | "stacked" | "exploded" | "solo";
136
- wallMode: "up" | "down" | "cutaway" | "translucent";
137
+ wallMode: WallMode;
137
138
  projectPreferences: Record<string, {
138
139
  showScans?: boolean;
139
140
  showGuides?: boolean;
@@ -157,7 +158,7 @@ declare const useViewer: import("zustand").UseBoundStore<Omit<import("zustand").
157
158
  getOptions: () => Partial<import("zustand/middleware").PersistOptions<ViewerState, {
158
159
  metricNotation: MetricNotation;
159
160
  levelMode: "manual" | "stacked" | "exploded" | "solo";
160
- wallMode: "up" | "down" | "cutaway" | "translucent";
161
+ wallMode: WallMode;
161
162
  projectPreferences: Record<string, {
162
163
  showScans?: boolean;
163
164
  showGuides?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["../../src/store/use-viewer.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"floor-elevation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/floor-elevation/floor-elevation-system.tsx"],"names":[],"mappings":"AAyCA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,YAoEhC,CAAA"}
1
+ {"version":3,"file":"floor-elevation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/floor-elevation/floor-elevation-system.tsx"],"names":[],"mappings":"AAyCA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,YA6EhC,CAAA"}
@@ -64,6 +64,15 @@ export const FloorElevationSystem = () => {
64
64
  const position = effectiveNode.position;
65
65
  if (!position)
66
66
  return;
67
+ // `applies === false` means the kind opts OUT of floor stacking for this
68
+ // node: its Y belongs to a host frame (a wall/ceiling-mounted item, a
69
+ // cabinet module inside a run, a wall duct terminal). `getFloorPlacedElevation`
70
+ // already returns 0 for them, so the write below would degenerate to
71
+ // copying `position[1]` into the mesh — and tools publish live transforms
72
+ // in WORLD space, so during a drag that lifts the ghost off its host by
73
+ // the host frame's own elevation.
74
+ if (floorPlaced.applies && !floorPlaced.applies(effectiveNode))
75
+ return;
67
76
  // This system is the single drag-time authority for floor-stack mesh Y:
68
77
  // tools publish base positions to live stores, renderers may
69
78
  // reconcile that base Y onto the group, then this presentation system
@@ -1 +1 @@
1
- {"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAa,KAAK,KAAK,EAA4B,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAQtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,YA8L1B,CAAA;AAoKD,eAAe,cAAc,CAAA;AAE7B,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAU7D;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC,EAC3C,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAKT"}
1
+ {"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAa,KAAK,KAAK,EAA4B,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAStF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,YA8L1B,CAAA;AA8ID,eAAe,cAAc,CAAA;AAE7B,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAU7D;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC,EAC3C,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAKT"}
@@ -3,6 +3,7 @@ import { findLevelAncestorId, getEffectiveNode, levelBaseElevationAt, nodeRegist
3
3
  import { useFrame } from '@react-three/fiber';
4
4
  import { useEffect, useRef } from 'react';
5
5
  import { FrontSide } from 'three';
6
+ import { disposeObject3DResources } from '../../lib/dispose-object3d';
6
7
  import { createSurfaceRoleMaterial, } from '../../lib/materials';
7
8
  import useViewer from '../../store/use-viewer';
8
9
  /**
@@ -289,28 +290,7 @@ function disposeChildren(group) {
289
290
  if (!fromGeometry)
290
291
  continue;
291
292
  group.remove(child);
292
- const mesh = child;
293
- if (mesh.geometry?.dispose)
294
- mesh.geometry.dispose();
295
- if ('material' in mesh) {
296
- const m = mesh.material;
297
- if (Array.isArray(m)) {
298
- for (const mat of m) {
299
- if (isCachedMaterial(mat))
300
- continue;
301
- if (mat && typeof mat.dispose === 'function') {
302
- ;
303
- mat.dispose();
304
- }
305
- }
306
- }
307
- else if (isCachedMaterial(m)) {
308
- }
309
- else if (m && typeof m.dispose === 'function') {
310
- ;
311
- m.dispose();
312
- }
313
- }
293
+ disposeObject3DResources(child);
314
294
  }
315
295
  }
316
296
  function applyDefaultSurfaceRole(root, defaultRole, colorPreset, sceneTheme) {
@@ -339,10 +319,6 @@ function getMaterialSide(material) {
339
319
  const source = Array.isArray(material) ? material[0] : material;
340
320
  return source?.side ?? FrontSide;
341
321
  }
342
- function isCachedMaterial(value) {
343
- return Boolean(value?.userData
344
- ?.__pascalCachedMaterial);
345
- }
346
322
  export default GeometrySystem;
347
323
  export function markGeometryBuildOutput(child) {
348
324
  // Tag the builder subtree so paint previews can reach nested meshes (for
@@ -1 +1 @@
1
- {"version":3,"file":"level-system.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-system.tsx"],"names":[],"mappings":"AAeA,eAAO,MAAM,WAAW,YAuDvB,CAAA"}
1
+ {"version":3,"file":"level-system.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-system.tsx"],"names":[],"mappings":"AAeA,eAAO,MAAM,WAAW,YA6DvB,CAAA"}
@@ -35,7 +35,13 @@ export const LevelSystem = () => {
35
35
  const baseY = levelElevations.get(levelId)?.baseY ?? 0;
36
36
  const explodedExtra = levelMode === 'exploded' ? index * EXPLODED_GAP : 0;
37
37
  const targetY = baseY + explodedExtra;
38
- obj.position.y = lerp(obj.position.y, targetY, delta * 12); // Smoothly animate to new Y position
38
+ // Clamped smoothing. The naive `lerp(y, target, delta*12)` multiplies
39
+ // the error by |1 - 12*delta| per frame — DIVERGENT once a frame
40
+ // exceeds ~166 ms (slow machines, headless GL, heavy scenes): levels
41
+ // oscillated kilometers off-screen and the level-fit camera followed
42
+ // (blank viewport). Clamping keeps every step a contraction: identical
43
+ // feel at 60 fps, exact snap instead of overshoot on slow frames.
44
+ obj.position.y = lerp(obj.position.y, targetY, Math.min(1, delta * 12));
39
45
  // Solo: hidden levels ABOVE the soloed one stay in the shadow map
40
46
  // (shadow-caster-only) so the sun still shadows the soloed floor through
41
47
  // them; levels below can't block the sun, so they plain-hide.
@@ -0,0 +1,10 @@
1
+ import { type WallMiterData, type WallNode } from '@pascal-app/core';
2
+ export declare function sameMiterInputs(a: WallNode[], b: WallNode[]): boolean;
3
+ export declare function getCachedLevelMiters(levelId: string, levelWalls: WallNode[]): WallMiterData;
4
+ /**
5
+ * The cache is module-level, so it outlives any single mount. Editor teardown
6
+ * resets the other shared singletons; without the same reset a remount in the
7
+ * same tab keeps every previous level's walls reachable.
8
+ */
9
+ export declare function clearLevelMiterCache(): void;
10
+ //# sourceMappingURL=level-miter-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"level-miter-cache.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/level-miter-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAa1F,wBAAgB,eAAe,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,GAAG,OAAO,CAoBrE;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,aAAa,CAO3F;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
@@ -0,0 +1,40 @@
1
+ import { calculateLevelMiters } from '@pascal-app/core';
2
+ const levelMiterCache = new Map();
3
+ export function sameMiterInputs(a, b) {
4
+ if (a.length !== b.length)
5
+ return false;
6
+ for (let i = 0; i < a.length; i++) {
7
+ const x = a[i];
8
+ const y = b[i];
9
+ if (x === y)
10
+ continue;
11
+ if (!x || !y)
12
+ return false;
13
+ if (x.id !== y.id ||
14
+ x.start[0] !== y.start[0] ||
15
+ x.start[1] !== y.start[1] ||
16
+ x.end[0] !== y.end[0] ||
17
+ x.end[1] !== y.end[1] ||
18
+ x.thickness !== y.thickness ||
19
+ x.curveOffset !== y.curveOffset) {
20
+ return false;
21
+ }
22
+ }
23
+ return true;
24
+ }
25
+ export function getCachedLevelMiters(levelId, levelWalls) {
26
+ const cached = levelMiterCache.get(levelId);
27
+ if (cached && sameMiterInputs(cached.walls, levelWalls))
28
+ return cached.data;
29
+ const data = calculateLevelMiters(levelWalls);
30
+ levelMiterCache.set(levelId, { walls: levelWalls, data });
31
+ return data;
32
+ }
33
+ /**
34
+ * The cache is module-level, so it outlives any single mount. Editor teardown
35
+ * resets the other shared singletons; without the same reset a remount in the
36
+ * same tab keeps every previous level's walls reachable.
37
+ */
38
+ export function clearLevelMiterCache() {
39
+ levelMiterCache.clear();
40
+ }
@@ -1,2 +1,14 @@
1
+ import { type WallNode } from '@pascal-app/core';
2
+ import { type Mesh, Vector3 } from 'three/webgpu';
3
+ import { type WallMode } from '../../store/use-viewer';
4
+ /**
5
+ * Whether a wall should be hidden or see-through for the current camera and
6
+ * wall mode. Pure: reads only its arguments and the mesh's world direction.
7
+ *
8
+ * Exported so hosts rendering their own layers inside `<Viewer>` can match
9
+ * these semantics instead of re-deriving the facing test or inferring state
10
+ * from the assigned material variant.
11
+ */
12
+ export declare function getWallHideState(wallNode: WallNode, wallMesh: Mesh, wallMode: WallMode, cameraDir: Vector3): boolean;
1
13
  export declare const WallCutout: () => null;
2
14
  //# sourceMappingURL=wall-cutout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAyDA,eAAO,MAAM,UAAU,YAuLtB,CAAA"}
1
+ {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAIzB,OAAO,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACjD,OAAkB,EAAE,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAWjE;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,IAAI,EACd,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,OAAO,GACjB,OAAO,CAmBT;AAMD,eAAO,MAAM,UAAU,YA+MtB,CAAA"}
@@ -2,12 +2,20 @@ import { emitter, getWallFaceBandConfig, getWallPlaneTop, resolveLevelId, resolv
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import { useEffect, useRef } from 'react';
4
4
  import { Vector3 } from 'three/webgpu';
5
- import useViewer from '../../store/use-viewer';
5
+ import useViewer, {} from '../../store/use-viewer';
6
6
  import { getMaterialsForWall, getSelectionHighlightMaterials, getWallMaterialHash, } from './wall-materials';
7
7
  const tmpVec = new Vector3();
8
8
  const u = new Vector3();
9
9
  const v = new Vector3();
10
- function getWallHideState(wallNode, wallMesh, wallMode, cameraDir) {
10
+ /**
11
+ * Whether a wall should be hidden or see-through for the current camera and
12
+ * wall mode. Pure: reads only its arguments and the mesh's world direction.
13
+ *
14
+ * Exported so hosts rendering their own layers inside `<Viewer>` can match
15
+ * these semantics instead of re-deriving the facing test or inferring state
16
+ * from the assigned material variant.
17
+ */
18
+ export function getWallHideState(wallNode, wallMesh, wallMode, cameraDir) {
11
19
  let hideWall = wallNode.frontSide === 'interior' && wallNode.backSide === 'interior';
12
20
  if (wallMode === 'up') {
13
21
  hideWall = false;
@@ -40,6 +48,13 @@ export const WallCutout = () => {
40
48
  const lastNumberOfWalls = useRef(0);
41
49
  const lastHighlightKey = useRef('');
42
50
  const lastWallAppearanceKey = useRef('');
51
+ const wallAppearanceKeyRef = useRef('');
52
+ const wallAppearanceInputs = useRef({
53
+ nodes: null,
54
+ materials: null,
55
+ shading: null,
56
+ wallCount: -1,
57
+ });
43
58
  const lastTextures = useRef(useViewer.getState().textures);
44
59
  const lastColorPreset = useRef(useViewer.getState().colorPreset);
45
60
  const lastSceneTheme = useRef(useViewer.getState().sceneTheme);
@@ -65,15 +80,30 @@ export const WallCutout = () => {
65
80
  ? hoveredId
66
81
  : null;
67
82
  const highlightKey = `${Array.from(highlightedWallIds).sort().join('|')}::${deleteHoveredWallId ?? ''}`;
68
- const wallAppearanceKey = Array.from(sceneRegistry.byType.wall)
69
- .sort()
70
- .map((wallId) => {
71
- const wallNode = sceneState.nodes[wallId];
72
- if (wallNode?.type !== 'wall')
73
- return `${wallId}:missing`;
74
- return `${wallId}:${getWallMaterialHash(wallNode, shading, sceneState.materials)}:${JSON.stringify(wallNode.faceBands ?? null)}`;
75
- })
76
- .join('|');
83
+ // Sorting every wall id, hashing each wall's material and JSON-dumping its
84
+ // face bands is a full-scene scan; its inputs are immutable store slices,
85
+ // so identity is enough to know the key cannot have changed.
86
+ const wallCount = sceneRegistry.byType.wall.size;
87
+ const appearanceInputs = wallAppearanceInputs.current;
88
+ if (appearanceInputs.nodes !== sceneState.nodes ||
89
+ appearanceInputs.materials !== sceneState.materials ||
90
+ appearanceInputs.shading !== shading ||
91
+ appearanceInputs.wallCount !== wallCount) {
92
+ appearanceInputs.nodes = sceneState.nodes;
93
+ appearanceInputs.materials = sceneState.materials;
94
+ appearanceInputs.shading = shading;
95
+ appearanceInputs.wallCount = wallCount;
96
+ wallAppearanceKeyRef.current = Array.from(sceneRegistry.byType.wall)
97
+ .sort()
98
+ .map((wallId) => {
99
+ const wallNode = sceneState.nodes[wallId];
100
+ if (wallNode?.type !== 'wall')
101
+ return `${wallId}:missing`;
102
+ return `${wallId}:${getWallMaterialHash(wallNode, shading, sceneState.materials)}:${JSON.stringify(wallNode.faceBands ?? null)}`;
103
+ })
104
+ .join('|');
105
+ }
106
+ const wallAppearanceKey = wallAppearanceKeyRef.current;
77
107
  const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current);
78
108
  const directionChanged = tmpVec.distanceTo(lastCameraTarget.current);
79
109
  const timeSinceUpdate = currentTime - lastUpdateTime.current;
@@ -1,5 +1,9 @@
1
1
  import { type AnyNode, type WallMiterData, type WallNode, type WallSlabSupportSegment } from '@pascal-app/core';
2
2
  import * as THREE from 'three';
3
+ /** Moves every rebuild notice collected so far into `into`. */
4
+ export declare function drainRebuiltWalls(into: Set<string>): void;
5
+ /** Rebuilds this system still owes — neighbours deferred during a drag. */
6
+ export declare function getPendingWallRebuildCount(): number;
3
7
  export declare const WallSystem: () => null;
4
8
  type WallTerrainBottomSampler = (x: number, z: number) => number | null;
5
9
  export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number, baseElevation?: number, baseSegments?: readonly WallSlabSupportSegment[], storeyHeight?: number, terrainBottomAt?: WallTerrainBottomSampler): THREE.BufferGeometry;
@@ -1 +1 @@
1
- {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EA2BZ,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAI5B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqd9B,eAAO,MAAM,UAAU,YAwJtB,CAAA;AAgND,KAAK,wBAAwB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;AAgEvE,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,EACjB,aAAa,SAAgB,EAC7B,YAAY,GAAE,SAAS,sBAAsB,EAE5C,EACD,YAAY,SAAuB,EACnC,eAAe,CAAC,EAAE,wBAAwB,GACzC,KAAK,CAAC,cAAc,CA0OtB"}
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EA0BZ,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAI5B,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAyc9B,+DAA+D;AAC/D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAGzD;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,IAAI,MAAM,CAMnD;AAED,eAAO,MAAM,UAAU,YAiKtB,CAAA;AAgND,KAAK,wBAAwB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;AAgEvE,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,EACjB,aAAa,SAAgB,EAC7B,YAAY,GAAE,SAAS,sBAAsB,EAE5C,EACD,YAAY,SAAuB,EACnC,eAAe,CAAC,EAAE,wBAAwB,GACzC,KAAK,CAAC,cAAc,CA0OtB"}
@@ -1,11 +1,14 @@
1
- import { calculateLevelMiters, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getWallBandSlotId, getWallCurveFrameAt, getWallFaceBandConfig, getWallFaceBandForHeight, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
1
+ import { DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getWallBandSlotId, getWallCurveFrameAt, getWallFaceBandConfig, getWallFaceBandForHeight, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
+ import { useEffect } from 'react';
3
4
  import * as THREE from 'three';
4
5
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
5
6
  import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
6
7
  import { computeBoundsTree } from 'three-mesh-bvh';
7
8
  import { ensureRenderableGeometryAttributes, prepareBrushForCSG } from '../../lib/csg-utils';
9
+ import { setGroupsSortedByMaterial } from '../../lib/geometry-groups';
8
10
  import { buildTerrainPerimeterFillGeometry } from '../../lib/terrain-perimeter-fill';
11
+ import { clearLevelMiterCache, getCachedLevelMiters } from './level-miter-cache';
9
12
  import { buildOpeningCutoutGeometry, getOpeningCutoutBottomPadding, } from './opening-cutout-geometry';
10
13
  // Reusable CSG evaluator for better performance
11
14
  const csgEvaluator = new Evaluator();
@@ -210,18 +213,7 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges, effectiveWallHe
210
213
  }
211
214
  triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag, centroid.y, effectiveWallHeight);
212
215
  }
213
- geometry.clearGroups();
214
- let currentMaterial = triangleMaterials[0] ?? 0;
215
- let groupStart = 0;
216
- for (let triangleIndex = 1; triangleIndex < triangleCount; triangleIndex += 1) {
217
- const materialIndex = triangleMaterials[triangleIndex] ?? 0;
218
- if (materialIndex === currentMaterial)
219
- continue;
220
- geometry.addGroup(groupStart * 3, (triangleIndex - groupStart) * 3, currentMaterial);
221
- groupStart = triangleIndex;
222
- currentMaterial = materialIndex;
223
- }
224
- geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
216
+ setGroupsSortedByMaterial(geometry, triangleMaterials);
225
217
  }
226
218
  function interpolateSplitVertex(a, b, t) {
227
219
  return {
@@ -345,7 +337,21 @@ const WALL_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WALL_REBUILDS_PER_FRAME;
345
337
  const WALL_PROGRESSIVE_TIME_BUDGET_MS = 8;
346
338
  let lastWallDirtyAtMs = 0;
347
339
  const pendingAdjacentByLevel = new Map();
348
- function getPendingAdjacentCount() {
340
+ // Walls whose geometry this system replaced since the last drain.
341
+ //
342
+ // The store's dirty mark is cleared the moment a wall is rebuilt, so anything
343
+ // running later in the same frame would never see it. This is that same
344
+ // signal, held until a consumer picks it up. Neighbours rebuilt by the
345
+ // trailing-edge flush land here too — those never carry a dirty mark at all.
346
+ const rebuiltWalls = new Set();
347
+ /** Moves every rebuild notice collected so far into `into`. */
348
+ export function drainRebuiltWalls(into) {
349
+ for (const wallId of rebuiltWalls)
350
+ into.add(wallId);
351
+ rebuiltWalls.clear();
352
+ }
353
+ /** Rebuilds this system still owes — neighbours deferred during a drag. */
354
+ export function getPendingWallRebuildCount() {
349
355
  let count = 0;
350
356
  for (const ids of pendingAdjacentByLevel.values()) {
351
357
  count += ids.size;
@@ -362,6 +368,10 @@ export const WallSystem = () => {
362
368
  // drags could land an override and a markDirty in the same React
363
369
  // tick and the next `useFrame` would still see the stale closure.
364
370
  useLiveNodeOverrides((s) => s.overrides);
371
+ // The miter cache is module-level, so it outlives this mount. Editor
372
+ // teardown resets the other shared singletons; without the same reset here a
373
+ // remount in the same tab keeps every previous level's walls reachable.
374
+ useEffect(() => () => clearLevelMiterCache(), []);
365
375
  useFrame(() => {
366
376
  const hasDirty = dirtyNodes.size > 0;
367
377
  const hasPending = pendingAdjacentByLevel.size > 0;
@@ -401,7 +411,7 @@ export const WallSystem = () => {
401
411
  break;
402
412
  }
403
413
  const levelWalls = getLevelWalls(levelId);
404
- const miterData = calculateLevelMiters(levelWalls);
414
+ const miterData = getCachedLevelMiters(levelId, levelWalls);
405
415
  const rebuiltWallIds = new Set();
406
416
  // Update dirty walls — always, no throttling. The dragged wall must
407
417
  // follow the cursor with full fidelity (cutouts and all). Large imports
@@ -420,6 +430,7 @@ export const WallSystem = () => {
420
430
  if (mesh) {
421
431
  updateWallGeometry(wallId, miterData);
422
432
  clearDirty(wallId);
433
+ rebuiltWalls.add(wallId);
423
434
  rebuiltWallIds.add(wallId);
424
435
  rebuiltWallsThisFrame += 1;
425
436
  }
@@ -447,7 +458,7 @@ export const WallSystem = () => {
447
458
  // their correct miter joins.
448
459
  const quiet = !hasDirtyWalls && now - lastWallDirtyAtMs >= DRAG_FLUSH_MS;
449
460
  if (quiet && pendingAdjacentByLevel.size > 0) {
450
- const pendingCount = getPendingAdjacentCount();
461
+ const pendingCount = getPendingWallRebuildCount();
451
462
  const useProgressiveAdjacentRebuilds = pendingCount > WALL_PROGRESSIVE_DIRTY_THRESHOLD;
452
463
  let rebuiltAdjacentThisFrame = 0;
453
464
  const adjacentFrameStartedAt = performance.now();
@@ -455,7 +466,7 @@ export const WallSystem = () => {
455
466
  if (pendingIds.size === 0)
456
467
  continue;
457
468
  const levelWalls = getLevelWalls(levelId);
458
- const miterData = calculateLevelMiters(levelWalls);
469
+ const miterData = getCachedLevelMiters(levelId, levelWalls);
459
470
  for (const wallId of Array.from(pendingIds)) {
460
471
  if (useProgressiveAdjacentRebuilds) {
461
472
  if (rebuiltAdjacentThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
@@ -467,8 +478,10 @@ export const WallSystem = () => {
467
478
  }
468
479
  }
469
480
  const mesh = sceneRegistry.nodes.get(wallId);
470
- if (mesh)
481
+ if (mesh) {
471
482
  updateWallGeometry(wallId, miterData);
483
+ rebuiltWalls.add(wallId);
484
+ }
472
485
  pendingIds.delete(wallId);
473
486
  rebuiltAdjacentThisFrame += 1;
474
487
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,10 +19,11 @@
19
19
  "scripts": {
20
20
  "build": "tsc --build",
21
21
  "dev": "tsgo --build --watch",
22
+ "test": "bun test src",
22
23
  "prepublishOnly": "npm run build"
23
24
  },
24
25
  "peerDependencies": {
25
- "@pascal-app/core": "^1.0.0-beta.4",
26
+ "@pascal-app/core": "^1.0.0-beta.5",
26
27
  "@react-three/drei": "^10",
27
28
  "@react-three/fiber": "^9",
28
29
  "react": "^18 || ^19",
@@ -34,6 +35,7 @@
34
35
  "zustand": "^5"
35
36
  },
36
37
  "devDependencies": {
38
+ "@pascal-app/core": "^1.0.0-beta.5",
37
39
  "@pascal/typescript-config": "*",
38
40
  "@types/node": "^22",
39
41
  "@types/react": "^19.2.2",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=scene-bvh.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"scene-bvh.test.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/scene-bvh.test.ts"],"names":[],"mappings":""}
@@ -1,13 +0,0 @@
1
- // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
- // include Bun ambient types in its production declaration build.
3
- import { describe, expect, test } from 'bun:test';
4
- import { Mesh } from 'three';
5
- import { isSceneBvhExcluded } from './scene-bvh';
6
- describe('scene BVH exclusions', () => {
7
- test('keeps annotation meshes on their normal raycast path', () => {
8
- expect(isSceneBvhExcluded(new Mesh())).toBe(false);
9
- const annotation = new Mesh();
10
- annotation.userData.excludeFromBvh = true;
11
- expect(isSceneBvhExcluded(annotation)).toBe(true);
12
- });
13
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=csg-utils.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"csg-utils.test.d.ts","sourceRoot":"","sources":["../../src/lib/csg-utils.test.ts"],"names":[],"mappings":""}
@@ -1,45 +0,0 @@
1
- // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
- // depend on @types/bun so the import type is unresolved at compile time.
3
- import { describe, expect, test } from 'bun:test';
4
- import * as THREE from 'three';
5
- import { ensureRenderableGeometryAttributes } from './csg-utils';
6
- describe('ensureRenderableGeometryAttributes', () => {
7
- test('fills missing render attributes to match position count', () => {
8
- const geometry = new THREE.BufferGeometry();
9
- geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3));
10
- ensureRenderableGeometryAttributes(geometry);
11
- expect(geometry.getAttribute('position')?.count).toBe(3);
12
- expect(geometry.getAttribute('normal')?.count).toBe(3);
13
- expect(geometry.getAttribute('uv')?.count).toBe(3);
14
- expect(geometry.getAttribute('uv2')?.count).toBe(3);
15
- });
16
- test('replaces render attributes with the wrong item size', () => {
17
- const geometry = new THREE.BufferGeometry();
18
- geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3));
19
- geometry.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0]), 1));
20
- geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0]), 1));
21
- ensureRenderableGeometryAttributes(geometry);
22
- expect(geometry.getAttribute('uv')?.itemSize).toBe(2);
23
- expect(geometry.getAttribute('uv2')?.itemSize).toBe(2);
24
- expect(geometry.getAttribute('uv')?.count).toBe(3);
25
- expect(geometry.getAttribute('uv2')?.count).toBe(3);
26
- });
27
- test('copies uv into uv2 without depending on backing array layout', () => {
28
- const geometry = new THREE.BufferGeometry();
29
- geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3));
30
- geometry.setAttribute('uv', new THREE.InterleavedBufferAttribute(new THREE.InterleavedBuffer(new Float32Array([0, 0, 7, 1, 0, 8, 0, 1, 9]), 3), 2, 0));
31
- ensureRenderableGeometryAttributes(geometry);
32
- const uv2 = geometry.getAttribute('uv2');
33
- expect(Array.from(uv2.array)).toEqual([0, 0, 1, 0, 0, 1]);
34
- });
35
- test('replaces empty geometries with a degenerate renderable triangle', () => {
36
- const geometry = new THREE.BufferGeometry();
37
- ensureRenderableGeometryAttributes(geometry);
38
- expect(geometry.getIndex()).toBeNull();
39
- expect(geometry.groups).toHaveLength(0);
40
- expect(geometry.getAttribute('position')?.count).toBe(3);
41
- expect(geometry.getAttribute('normal')?.count).toBe(3);
42
- expect(geometry.getAttribute('uv')?.count).toBe(3);
43
- expect(geometry.getAttribute('uv2')?.count).toBe(3);
44
- });
45
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=materials.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"materials.test.d.ts","sourceRoot":"","sources":["../../src/lib/materials.test.ts"],"names":[],"mappings":""}
@@ -1,25 +0,0 @@
1
- // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
- // depend on @types/bun so the import type is unresolved at compile time.
3
- import { describe, expect, test } from 'bun:test';
4
- import { getTextureKey, resolveTextureRepeat } from './materials';
5
- function materialWithRepeat(repeat) {
6
- return {
7
- texture: {
8
- url: 'https://example.com/texture.png',
9
- repeat,
10
- },
11
- };
12
- }
13
- describe('legacy texture repeat values', () => {
14
- test('normalizes tuple, scalar, and Vector2-shaped repeats', () => {
15
- expect(resolveTextureRepeat([2, 3], undefined)).toEqual([2, 3]);
16
- expect(resolveTextureRepeat(2, undefined)).toEqual([2, 2]);
17
- expect(resolveTextureRepeat({ x: 2, y: 3 }, undefined)).toEqual([2, 3]);
18
- });
19
- test('falls back to scale for malformed repeats', () => {
20
- expect(resolveTextureRepeat({ width: 2 }, 4)).toEqual([4, 4]);
21
- });
22
- test('keeps distinct Vector2-shaped repeats in distinct cache entries', () => {
23
- expect(getTextureKey(materialWithRepeat({ x: 2, y: 3 }))).not.toBe(getTextureKey(materialWithRepeat({ x: 4, y: 5 })));
24
- });
25
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=merged-outline-node.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"merged-outline-node.test.d.ts","sourceRoot":"","sources":["../../src/lib/merged-outline-node.test.ts"],"names":[],"mappings":""}
@@ -1,22 +0,0 @@
1
- // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
- // depend on @types/bun so the import type is unresolved at compile time.
3
- import { describe, expect, test } from 'bun:test';
4
- import { Object3D, PerspectiveCamera, Scene } from 'three';
5
- import { mergedOutline } from './merged-outline-node';
6
- describe('merged outline rendering', () => {
7
- test('keeps selected outlines active during camera interaction', () => {
8
- const cameraInteractionActive = true;
9
- const params = {
10
- enabled: () => !cameraInteractionActive,
11
- primaryObjects: [new Object3D()],
12
- };
13
- const outline = mergedOutline(new Scene(), new PerspectiveCamera(), params);
14
- const frame = {
15
- get renderer() {
16
- throw new Error('outline renderer was reached');
17
- },
18
- };
19
- expect(() => outline.updateBefore(frame)).toThrow('outline renderer was reached');
20
- outline.dispose();
21
- });
22
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=polygon-union.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"polygon-union.test.d.ts","sourceRoot":"","sources":["../../src/lib/polygon-union.test.ts"],"names":[],"mappings":""}