@pascal-app/viewer 1.0.0-beta.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/dist/capture/asset-url.d.ts +2 -0
  4. package/dist/capture/asset-url.d.ts.map +1 -0
  5. package/dist/capture/asset-url.js +13 -0
  6. package/dist/capture/capture-runtime.d.ts +43 -0
  7. package/dist/capture/capture-runtime.d.ts.map +1 -0
  8. package/dist/capture/capture-runtime.js +228 -0
  9. package/dist/capture/frame.d.ts +4 -0
  10. package/dist/capture/frame.d.ts.map +1 -0
  11. package/dist/capture/frame.js +21 -0
  12. package/dist/capture/index.d.ts +12 -0
  13. package/dist/capture/index.d.ts.map +1 -0
  14. package/dist/capture/index.js +11 -0
  15. package/dist/capture/layer-visibility.d.ts +5 -0
  16. package/dist/capture/layer-visibility.d.ts.map +1 -0
  17. package/dist/capture/layer-visibility.js +11 -0
  18. package/dist/capture/layers/clay-matcap.d.ts +3 -0
  19. package/dist/capture/layers/clay-matcap.d.ts.map +1 -0
  20. package/dist/capture/layers/clay-matcap.js +26 -0
  21. package/dist/capture/layers/device-motion-layer.d.ts +7 -0
  22. package/dist/capture/layers/device-motion-layer.d.ts.map +1 -0
  23. package/dist/capture/layers/device-motion-layer.js +78 -0
  24. package/dist/capture/layers/point-cloud-layer.d.ts +15 -0
  25. package/dist/capture/layers/point-cloud-layer.d.ts.map +1 -0
  26. package/dist/capture/layers/point-cloud-layer.js +106 -0
  27. package/dist/capture/layers/room-model-layer.d.ts +9 -0
  28. package/dist/capture/layers/room-model-layer.d.ts.map +1 -0
  29. package/dist/capture/layers/room-model-layer.js +71 -0
  30. package/dist/capture/layers/surface-mesh-data.d.ts +9 -0
  31. package/dist/capture/layers/surface-mesh-data.d.ts.map +1 -0
  32. package/dist/capture/layers/surface-mesh-data.js +74 -0
  33. package/dist/capture/layers/surface-mesh-layer.d.ts +7 -0
  34. package/dist/capture/layers/surface-mesh-layer.d.ts.map +1 -0
  35. package/dist/capture/layers/surface-mesh-layer.js +16 -0
  36. package/dist/capture/preview.d.ts +3 -0
  37. package/dist/capture/preview.d.ts.map +1 -0
  38. package/dist/capture/preview.js +2 -0
  39. package/dist/capture/source-state.d.ts +22 -0
  40. package/dist/capture/source-state.d.ts.map +1 -0
  41. package/dist/capture/source-state.js +182 -0
  42. package/dist/capture/stream-rendering.d.ts +14 -0
  43. package/dist/capture/stream-rendering.d.ts.map +1 -0
  44. package/dist/capture/stream-rendering.js +69 -0
  45. package/dist/capture/trajectory.d.ts +20 -0
  46. package/dist/capture/trajectory.d.ts.map +1 -0
  47. package/dist/capture/trajectory.js +77 -0
  48. package/dist/components/viewer/batched-mesh-spike.d.ts +14 -0
  49. package/dist/components/viewer/batched-mesh-spike.d.ts.map +1 -0
  50. package/dist/components/viewer/batched-mesh-spike.js +64 -0
  51. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  52. package/dist/components/viewer/frame-limiter.js +4 -3
  53. package/dist/components/viewer/index.d.ts.map +1 -1
  54. package/dist/components/viewer/index.js +70 -62
  55. package/dist/components/viewer/lights.d.ts.map +1 -1
  56. package/dist/components/viewer/lights.js +123 -89
  57. package/dist/components/viewer/perf-monitor.d.ts +6 -1
  58. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  59. package/dist/components/viewer/perf-monitor.js +307 -116
  60. package/dist/components/viewer/perf-panel.d.ts +3 -0
  61. package/dist/components/viewer/perf-panel.d.ts.map +1 -0
  62. package/dist/components/viewer/perf-panel.js +178 -0
  63. package/dist/components/viewer/pointer-raycast-layers.d.ts +15 -0
  64. package/dist/components/viewer/pointer-raycast-layers.d.ts.map +1 -0
  65. package/dist/components/viewer/pointer-raycast-layers.js +28 -0
  66. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  67. package/dist/components/viewer/post-processing.js +124 -83
  68. package/dist/components/viewer/registered-systems.d.ts +14 -5
  69. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  70. package/dist/components/viewer/registered-systems.js +22 -10
  71. package/dist/components/viewer/scene-atmosphere.d.ts +30 -0
  72. package/dist/components/viewer/scene-atmosphere.d.ts.map +1 -0
  73. package/dist/components/viewer/scene-atmosphere.js +89 -0
  74. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  75. package/dist/components/viewer/scene-environment.js +19 -5
  76. package/dist/components/viewer/scene-ground-replacement.d.ts +5 -0
  77. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -0
  78. package/dist/components/viewer/scene-ground-replacement.js +35 -0
  79. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  80. package/dist/components/viewer/viewer-camera.js +4 -1
  81. package/dist/components/viewer/viewer-presentations.d.ts +67 -0
  82. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -0
  83. package/dist/components/viewer/viewer-presentations.js +120 -0
  84. package/dist/hooks/use-library-materials-version.d.ts +7 -0
  85. package/dist/hooks/use-library-materials-version.d.ts.map +1 -0
  86. package/dist/hooks/use-library-materials-version.js +10 -0
  87. package/dist/hooks/use-node-events.d.ts.map +1 -1
  88. package/dist/hooks/use-node-events.js +1 -0
  89. package/dist/index.d.ts +15 -6
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +15 -6
  92. package/dist/lib/csg-utils.d.ts +1 -0
  93. package/dist/lib/csg-utils.d.ts.map +1 -1
  94. package/dist/lib/csg-utils.js +6 -1
  95. package/dist/lib/gpu-perf.d.ts +0 -2
  96. package/dist/lib/gpu-perf.d.ts.map +1 -1
  97. package/dist/lib/gpu-perf.js +7 -21
  98. package/dist/lib/layer-pass.d.ts +28 -0
  99. package/dist/lib/layer-pass.d.ts.map +1 -0
  100. package/dist/lib/layer-pass.js +254 -0
  101. package/dist/lib/materials.d.ts +12 -0
  102. package/dist/lib/materials.d.ts.map +1 -1
  103. package/dist/lib/materials.js +51 -17
  104. package/dist/lib/merged-outline-node.d.ts +17 -8
  105. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  106. package/dist/lib/merged-outline-node.js +194 -52
  107. package/dist/lib/perf-actions.d.ts +55 -0
  108. package/dist/lib/perf-actions.d.ts.map +1 -0
  109. package/dist/lib/perf-actions.js +211 -0
  110. package/dist/lib/perf-observers.d.ts +2 -0
  111. package/dist/lib/perf-observers.d.ts.map +1 -0
  112. package/dist/lib/perf-observers.js +17 -0
  113. package/dist/lib/perf-panel-store.d.ts +71 -0
  114. package/dist/lib/perf-panel-store.d.ts.map +1 -0
  115. package/dist/lib/perf-panel-store.js +29 -0
  116. package/dist/lib/perf-tracks.d.ts +43 -0
  117. package/dist/lib/perf-tracks.d.ts.map +1 -0
  118. package/dist/lib/perf-tracks.js +113 -0
  119. package/dist/lib/pointer-events.d.ts +7 -0
  120. package/dist/lib/pointer-events.d.ts.map +1 -0
  121. package/dist/lib/pointer-events.js +680 -0
  122. package/dist/lib/post-processing-resources.d.ts +12 -0
  123. package/dist/lib/post-processing-resources.d.ts.map +1 -0
  124. package/dist/lib/post-processing-resources.js +19 -0
  125. package/dist/lib/renderer-capability.d.ts.map +1 -1
  126. package/dist/lib/renderer-capability.js +4 -2
  127. package/dist/lib/scene-visibility.d.ts +2 -1
  128. package/dist/lib/scene-visibility.d.ts.map +1 -1
  129. package/dist/lib/scene-visibility.js +22 -1
  130. package/dist/store/use-viewer.d.ts +2 -2
  131. package/dist/store/use-viewer.d.ts.map +1 -1
  132. package/dist/systems/ceiling/ceiling-system.js +1 -1
  133. package/dist/systems/door/door-animation-system.d.ts.map +1 -1
  134. package/dist/systems/door/door-animation-system.js +6 -9
  135. package/dist/systems/door/door-system.d.ts.map +1 -1
  136. package/dist/systems/door/door-system.js +13 -2
  137. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  138. package/dist/systems/geometry/geometry-system.js +3 -2
  139. package/dist/systems/level/level-system.d.ts.map +1 -1
  140. package/dist/systems/level/level-system.js +1 -1
  141. package/dist/systems/level/level-utils.d.ts +9 -0
  142. package/dist/systems/level/level-utils.d.ts.map +1 -1
  143. package/dist/systems/level/level-utils.js +14 -0
  144. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts +8 -0
  145. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts.map +1 -0
  146. package/dist/systems/perf-action-settle/perf-action-settle-system.js +32 -0
  147. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  148. package/dist/systems/roof/roof-system.js +1196 -58
  149. package/dist/systems/scan/scan-system.js +6 -5
  150. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  151. package/dist/systems/slab/slab-system.js +1 -2
  152. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  153. package/dist/systems/stair/stair-system.js +9 -8
  154. package/dist/systems/surface-hole-geometry.d.ts +1 -1
  155. package/dist/systems/surface-hole-geometry.d.ts.map +1 -1
  156. package/dist/systems/surface-hole-geometry.js +1 -1
  157. package/dist/systems/wall/level-miter-cache.d.ts.map +1 -1
  158. package/dist/systems/wall/wall-build-lifecycle.d.ts +9 -0
  159. package/dist/systems/wall/wall-build-lifecycle.d.ts.map +1 -0
  160. package/dist/systems/wall/wall-build-lifecycle.js +74 -0
  161. package/dist/systems/wall/wall-cutout-cache.d.ts +57 -0
  162. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -0
  163. package/dist/systems/wall/wall-cutout-cache.js +304 -0
  164. package/dist/systems/wall/wall-cutout.d.ts +12 -1
  165. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  166. package/dist/systems/wall/wall-cutout.js +24 -161
  167. package/dist/systems/wall/wall-material-variant.d.ts +31 -0
  168. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -0
  169. package/dist/systems/wall/wall-material-variant.js +10 -0
  170. package/dist/systems/wall/wall-materials.d.ts +6 -0
  171. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  172. package/dist/systems/wall/wall-materials.js +29 -13
  173. package/dist/systems/wall/wall-placeholder-sweep.d.ts +50 -0
  174. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -0
  175. package/dist/systems/wall/wall-placeholder-sweep.js +51 -0
  176. package/dist/systems/wall/wall-rebuild-notifications.d.ts +5 -0
  177. package/dist/systems/wall/wall-rebuild-notifications.d.ts.map +1 -0
  178. package/dist/systems/wall/wall-rebuild-notifications.js +23 -0
  179. package/dist/systems/wall/wall-system.d.ts +11 -4
  180. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  181. package/dist/systems/wall/wall-system.js +405 -153
  182. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  183. package/dist/systems/window/window-animation-system.js +9 -8
  184. package/dist/systems/window/window-system.d.ts +1 -0
  185. package/dist/systems/window/window-system.d.ts.map +1 -1
  186. package/dist/systems/window/window-system.js +16 -2
  187. package/package.json +17 -4
  188. package/dist/lib/polygon-union.d.ts +0 -4
  189. package/dist/lib/polygon-union.d.ts.map +0 -1
  190. package/dist/lib/polygon-union.js +0 -289
@@ -2,20 +2,17 @@
2
2
  // this file is a fork of OutlineNode and is intentionally exempt from strict TS checking.
3
3
  /**
4
4
  * MergedOutlineNode — a fork of Three.js OutlineNode that processes two object
5
- * groups (primary = selected, secondary = hovered) in a single pass, sharing the
6
- * expensive non-selected depth pre-render between both groups.
7
- *
8
- * Cost comparison vs two separate OutlineNode instances:
9
- * Before: depth_A + mask_A + edge_A×6 + depth_B + mask_B + edge_B×6 = 2 depth passes
10
- * After: depth_AB (shared) + mask_A + edge_A×6 + mask_B + edge_B×6 = 1 depth pass
5
+ * groups (primary = selected, secondary = hovered), reusing scene depth when
6
+ * supplied and rendering masks from small private scenes. Callers without scene
7
+ * depth retain one shared non-selected depth pass for both groups.
11
8
  *
12
9
  * Additional early-outs:
13
10
  * - Both empty → skip everything (0 passes)
14
11
  * - Only primary → skip secondary mask/edge/blur
15
12
  * - Only secondary → skip primary mask/edge/blur
16
13
  */
17
- import { DepthTexture, FloatType, RenderTarget, Vector2 } from 'three';
18
- import { color, exp, Fn, float, int, Loop, min, mul, nodeObject, orthographicDepthToViewZ, passTexture, perspectiveDepthToViewZ, positionView, reference, screenUV, texture, textureSize, uniform, uv, vec2, vec3, vec4, } from 'three/tsl';
14
+ import { DepthTexture, FloatType, Mesh, Object3D, RenderTarget, Scene, Sprite, Vector2, } from 'three';
15
+ import { builtin, color, exp, Fn, float, floatBitsToUint, int, Loop, min, mul, nodeObject, orthographicDepthToViewZ, passTexture, perspectiveDepthToViewZ, positionView, reference, screenUV, texture, textureSize, uniform, uv, vec2, vec3, vec4, } from 'three/tsl';
19
16
  import { NodeMaterial, NodeUpdateType, QuadMesh, RendererUtils, SpriteNodeMaterial, TempNode, } from 'three/webgpu';
20
17
  import { hasDrawableGeometry } from './drawable-geometry';
21
18
  const _quadMesh = new QuadMesh();
@@ -27,6 +24,8 @@ let _rendererState; // eslint-disable-line @typescript-eslint/no-explicit-any
27
24
  // Helper: render targets for one outline group
28
25
  // ---------------------------------------------------------------------------
29
26
  function makeGroupTargets(downSampleRatio) {
27
+ // Preserve the original pixel-center mask coverage. MSAA-resolved masks mix
28
+ // visible/background samples into hidden edges before edge detection.
30
29
  const maskBuffer = new RenderTarget();
31
30
  const maskDownSample = new RenderTarget(1, 1, { depthBuffer: false });
32
31
  const edgeBuffer1 = new RenderTarget(1, 1, { depthBuffer: false });
@@ -85,6 +84,7 @@ export class MergedOutlineNode extends TempNode {
85
84
  secondaryEdgeGlowNode;
86
85
  downSampleRatio;
87
86
  updateBeforeType;
87
+ _sceneDepthNode;
88
88
  _depthRT;
89
89
  _depthTexUniform;
90
90
  _groupA;
@@ -121,6 +121,11 @@ export class MergedOutlineNode extends TempNode {
121
121
  _compositeMatB;
122
122
  _cacheA = new Set();
123
123
  _cacheB = new Set();
124
+ _proxiesA = new Map();
125
+ _proxiesB = new Map();
126
+ _proxyMaskMaterials = new WeakMap();
127
+ _maskSceneA = new Scene();
128
+ _maskSceneB = new Scene();
124
129
  // Tracks whether either group rendered last frame. We use this to decide
125
130
  // when it's safe to skip renderer state manipulation entirely — touching
126
131
  // the renderer (resetRendererAndSceneState + setRenderTarget + clearColor)
@@ -132,7 +137,12 @@ export class MergedOutlineNode extends TempNode {
132
137
  _textureNodeB;
133
138
  constructor(scene, camera, params = {}) {
134
139
  super('vec4');
135
- const { primaryObjects = [], secondaryObjects = [], primaryEdgeThickness = float(1), secondaryEdgeThickness = float(1), primaryEdgeGlow = float(0), secondaryEdgeGlow = float(0), downSampleRatio = 2, } = params;
140
+ const { primaryObjects = [], secondaryObjects = [], primaryEdgeThickness = float(1), secondaryEdgeThickness = float(1), primaryEdgeGlow = float(0), secondaryEdgeGlow = float(0), downSampleRatio = 2, sceneDepthNode = null, } = params;
141
+ this._sceneDepthNode = sceneDepthNode;
142
+ this._maskSceneA.matrixWorldAutoUpdate = false;
143
+ this._maskSceneB.matrixWorldAutoUpdate = false;
144
+ this._maskSceneA.name = 'MergedOutline [ Mask A ]';
145
+ this._maskSceneB.name = 'MergedOutline [ Mask B ]';
136
146
  this.scene = scene;
137
147
  this.camera = camera;
138
148
  this.primaryObjects = primaryObjects;
@@ -233,6 +243,11 @@ export class MergedOutlineNode extends TempNode {
233
243
  }
234
244
  const { renderer } = frame;
235
245
  const { camera, scene } = this;
246
+ // Update the producer before resetting renderer state, even if this outline
247
+ // is the first consumer in the graph. NodeFrame deduplicates FRAME updates.
248
+ if (hasAny && this._sceneDepthNode?.passNode) {
249
+ frame.updateBeforeNode(this._sceneDepthNode.passNode);
250
+ }
236
251
  _rendererState = RendererUtils.resetRendererAndSceneState(renderer, scene, _rendererState);
237
252
  const size = renderer.getDrawingBufferSize(_size);
238
253
  this.setSize(size.width, size.height);
@@ -248,6 +263,10 @@ export class MergedOutlineNode extends TempNode {
248
263
  renderer.clearColor();
249
264
  this._wroteGroupBLastFrame = false;
250
265
  }
266
+ this._buildCache(this.primaryObjects, this._cacheA);
267
+ this._buildCache(this.secondaryObjects, this._cacheB);
268
+ const useProxiesA = this._syncProxies(this._cacheA, this._proxiesA, this._maskSceneA);
269
+ const useProxiesB = this._syncProxies(this._cacheB, this._proxiesB, this._maskSceneB);
251
270
  if (!hasAny) {
252
271
  RendererUtils.restoreRendererAndSceneState(renderer, scene, _rendererState);
253
272
  return;
@@ -255,55 +274,26 @@ export class MergedOutlineNode extends TempNode {
255
274
  renderer.setClearColor(0xff_ff_ff, 1);
256
275
  this._wroteGroupALastFrame = hasPrimary;
257
276
  this._wroteGroupBLastFrame = hasSecondary;
258
- if (hasPrimary)
259
- this._buildCache(this.primaryObjects, this._cacheA);
260
- if (hasSecondary)
261
- this._buildCache(this.secondaryObjects, this._cacheB);
262
277
  const savedName = scene.name;
263
- // ── 1. Shared depth pass: all objects NOT in either group ─────────────────
264
- renderer.setRenderTarget(this._depthRT);
265
- renderer.setRenderObjectFunction((obj, sc, cam, geo, _mat, grp, lights, clip) => {
266
- if (!hasDrawableGeometry(geo))
267
- return;
268
- const inCache = this._cacheA.has(obj) || this._cacheB.has(obj);
269
- if (!inCache) {
270
- const m = obj.isSprite ? this._depthSpriteMaterial : this._depthMaterial;
271
- renderer.renderObject(obj, sc, cam, geo, m, grp, lights, clip);
272
- }
273
- });
274
- scene.name = 'MergedOutline [ Depth ]';
275
- renderer.render(scene, camera);
276
- // ── 2a. Primary mask pass ─────────────────────────────────────────────────
277
- if (hasPrimary) {
278
- renderer.setRenderTarget(this._groupA.maskBuffer);
279
- renderer.setRenderObjectFunction((obj, sc, cam, geo, _mat, grp, lights, clip) => {
280
- if (!hasDrawableGeometry(geo))
281
- return;
282
- if (this._cacheA.has(obj)) {
283
- const m = obj.isSprite ? this._prepareMaskSpriteMatA : this._prepareMaskMatA;
284
- renderer.renderObject(obj, sc, cam, geo, m, grp, lights, clip);
285
- }
286
- });
287
- scene.name = 'MergedOutline [ Mask A ]';
288
- renderer.render(scene, camera);
289
- }
290
- // ── 2b. Secondary mask pass ───────────────────────────────────────────────
291
- if (hasSecondary) {
292
- renderer.setRenderTarget(this._groupB.maskBuffer);
278
+ if (!this._sceneDepthNode) {
279
+ renderer.setRenderTarget(this._depthRT);
293
280
  renderer.setRenderObjectFunction((obj, sc, cam, geo, _mat, grp, lights, clip) => {
294
281
  if (!hasDrawableGeometry(geo))
295
282
  return;
296
- if (this._cacheB.has(obj)) {
297
- const m = obj.isSprite ? this._prepareMaskSpriteMatB : this._prepareMaskMatB;
283
+ if (!(this._cacheA.has(obj) || this._cacheB.has(obj))) {
284
+ const m = obj.isSprite ? this._depthSpriteMaterial : this._depthMaterial;
298
285
  renderer.renderObject(obj, sc, cam, geo, m, grp, lights, clip);
299
286
  }
300
287
  });
301
- scene.name = 'MergedOutline [ Mask B ]';
288
+ scene.name = 'MergedOutline [ Depth ]';
302
289
  renderer.render(scene, camera);
303
290
  }
291
+ // The fallback depth render may have updated source matrices since sync.
292
+ if (hasPrimary)
293
+ this._renderMask(renderer, 'A', useProxiesA);
294
+ if (hasSecondary)
295
+ this._renderMask(renderer, 'B', useProxiesB);
304
296
  renderer.setRenderObjectFunction(_rendererState.renderObjectFunction);
305
- this._cacheA.clear();
306
- this._cacheB.clear();
307
297
  scene.name = savedName;
308
298
  // ── 3–7. Edge detect + blur + composite per active group ──────────────────
309
299
  if (hasPrimary)
@@ -312,6 +302,117 @@ export class MergedOutlineNode extends TempNode {
312
302
  this._runEdgePipeline(renderer, 'B');
313
303
  RendererUtils.restoreRendererAndSceneState(renderer, scene, _rendererState);
314
304
  }
305
+ _renderMask(renderer, group, useProxies) {
306
+ const isA = group === 'A';
307
+ const cache = isA ? this._cacheA : this._cacheB;
308
+ const proxies = isA ? this._proxiesA : this._proxiesB;
309
+ const maskScene = isA ? this._maskSceneA : this._maskSceneB;
310
+ const material = isA ? this._prepareMaskMatA : this._prepareMaskMatB;
311
+ const spriteMaterial = isA ? this._prepareMaskSpriteMatA : this._prepareMaskSpriteMatB;
312
+ if (useProxies) {
313
+ for (const [source, proxy] of proxies)
314
+ proxy.matrixWorld.copy(source.matrixWorld);
315
+ }
316
+ renderer.setRenderTarget((isA ? this._groupA : this._groupB).maskBuffer);
317
+ // Keep source materials on proxies for material visibility and geometry
318
+ // groups; substitute only at submission, just like the full-scene path.
319
+ renderer.setRenderObjectFunction((obj, sc, cam, geo, _mat, grp, lights, clip) => {
320
+ if (!hasDrawableGeometry(geo))
321
+ return;
322
+ if (useProxies || cache.has(obj)) {
323
+ let maskMaterial = obj.isSprite ? spriteMaterial : material;
324
+ if (useProxies) {
325
+ let ownedMaterial = this._proxyMaskMaterials.get(obj);
326
+ if (!ownedMaterial) {
327
+ // Disposing this private material releases the proxy's RenderObjects
328
+ // without disposing geometry or materials owned by the source scene.
329
+ ownedMaterial = maskMaterial.clone();
330
+ this._proxyMaskMaterials.set(obj, ownedMaterial);
331
+ }
332
+ if (ownedMaterial.colorNode !== maskMaterial.colorNode) {
333
+ ownedMaterial.colorNode = maskMaterial.colorNode;
334
+ ownedMaterial.needsUpdate = true;
335
+ }
336
+ maskMaterial = ownedMaterial;
337
+ }
338
+ renderer.renderObject(obj, sc, cam, geo, maskMaterial, grp, lights, clip);
339
+ }
340
+ });
341
+ renderer.render(useProxies ? maskScene : this.scene, this.camera);
342
+ }
343
+ _syncProxies(cache, proxies, scene) {
344
+ const supported = this._supportsProxies(cache);
345
+ for (const [source, proxy] of proxies) {
346
+ if (!supported || !cache.has(source) || !hasDrawableGeometry(source.geometry)) {
347
+ this._disposeProxyMaterial(proxy);
348
+ scene.remove(proxy);
349
+ proxies.delete(source);
350
+ }
351
+ }
352
+ if (!supported)
353
+ return false;
354
+ for (const source of cache) {
355
+ if (!hasDrawableGeometry(source.geometry))
356
+ continue;
357
+ let proxy = proxies.get(source);
358
+ if (!proxy) {
359
+ proxy = source.isSprite
360
+ ? new Sprite(source.material)
361
+ : new Mesh(source.geometry, source.material);
362
+ proxy.matrixAutoUpdate = false;
363
+ proxy.matrixWorldAutoUpdate = false;
364
+ proxy.frustumCulled = false;
365
+ proxies.set(source, proxy);
366
+ scene.add(proxy);
367
+ }
368
+ else if (proxy.geometry !== source.geometry) {
369
+ // Three's RenderObject.setGeometry does not move its dispose listener.
370
+ this._disposeProxyMaterial(proxy);
371
+ }
372
+ let visible = true;
373
+ let attached = false;
374
+ for (let ancestor = source; ancestor; ancestor = ancestor.parent) {
375
+ if (!ancestor.visible)
376
+ visible = false;
377
+ if (ancestor === this.scene)
378
+ attached = true;
379
+ }
380
+ proxy.geometry = source.geometry;
381
+ proxy.material = source.material;
382
+ proxy.matrixWorld.copy(source.matrixWorld);
383
+ proxy.layers.mask = source.layers.mask;
384
+ proxy.visible = visible && attached;
385
+ proxy.renderOrder = source.renderOrder;
386
+ proxy.morphTargetInfluences = source.morphTargetInfluences;
387
+ proxy.morphTargetDictionary = source.morphTargetDictionary;
388
+ proxy.count = source.count;
389
+ if (source.isSprite)
390
+ proxy.center.copy(source.center);
391
+ }
392
+ return true;
393
+ }
394
+ _supportsProxies(cache) {
395
+ for (const source of cache) {
396
+ const prototype = Object.getPrototypeOf(source);
397
+ if ((prototype !== Mesh.prototype && prototype !== Sprite.prototype) ||
398
+ source.onBeforeRender !== Object3D.prototype.onBeforeRender ||
399
+ source.onAfterRender !== Object3D.prototype.onAfterRender) {
400
+ return false;
401
+ }
402
+ for (let ancestor = source; ancestor; ancestor = ancestor.parent) {
403
+ if (ancestor.isLOD ||
404
+ ancestor.isClippingGroup ||
405
+ (ancestor.isGroup && ancestor.renderOrder !== 0)) {
406
+ return false;
407
+ }
408
+ }
409
+ }
410
+ return true;
411
+ }
412
+ _disposeProxyMaterial(proxy) {
413
+ this._proxyMaskMaterials.get(proxy)?.dispose();
414
+ this._proxyMaskMaterials.delete(proxy);
415
+ }
315
416
  _runEdgePipeline(renderer, group) {
316
417
  const isA = group === 'A';
317
418
  const g = isA ? this._groupA : this._groupB;
@@ -350,15 +451,45 @@ export class MergedOutlineNode extends TempNode {
350
451
  renderer.setRenderTarget(g.composite);
351
452
  _quadMesh.render(renderer);
352
453
  }
353
- setup(_builder) {
454
+ setup(builder) {
455
+ if (this._sceneDepthNode) {
456
+ builder.getNodeProperties(this).sceneDepthNode = this._sceneDepthNode;
457
+ }
458
+ const reversed = builder.renderer.reversedDepthBuffer;
459
+ const depthTexture = this._sceneDepthNode?.value;
460
+ const floatDepth = reversed || depthTexture?.type === FloatType;
461
+ const depthSamples = this._sceneDepthNode && builder.renderer.backend.isWebGPUBackend
462
+ ? (this._sceneDepthNode.passNode?.options.samples ?? builder.renderer.samples)
463
+ : 0;
354
464
  // ── prepareMask ───────────────────────────────────────────────────────────
355
465
  const buildPrepareMask = () => {
356
- const depth = this._depthTexUniform.sample(screenUV);
466
+ let depth = (this._sceneDepthNode ?? this._depthTexUniform).sample(screenUV).r;
467
+ if (this._sceneDepthNode) {
468
+ // The symmetric MSAA sample positions average to the pixel center where
469
+ // the mask rasterizes. Average differences to avoid rounding a sum of
470
+ // nearly-one depths and consuming the one-ULP allowance below.
471
+ let depthOffset = float(0);
472
+ for (let sampleIndex = 1; sampleIndex < depthSamples; sampleIndex++) {
473
+ const sampleDepth = this._sceneDepthNode.sample(screenUV).level(sampleIndex).r;
474
+ depthOffset = depthOffset.add(sampleDepth.sub(depth));
475
+ }
476
+ if (depthSamples > 1)
477
+ depth = depth.add(depthOffset.div(depthSamples));
478
+ // Extract the float exponent for one ULP; a relative epsilon can hide
479
+ // centimetre-scale occlusion at long distances with conventional depth.
480
+ const bias = floatDepth
481
+ ? float(floatBitsToUint(depth.abs()).shiftRight(23).bitAnd(255).max(1)).sub(150).exp2()
482
+ : float(2 ** -24);
483
+ // Both Three builders expose fragCoord.xy; read rasterized z directly
484
+ // so view-Z reconstruction cannot consume the small depth allowance.
485
+ const fragmentDepth = Fn((_, shaderBuilder) => builtin(shaderBuilder.getFragCoord().replace(/\.xy$/, '.z')))();
486
+ const separation = reversed ? depth.sub(fragmentDepth) : fragmentDepth.sub(depth);
487
+ return vec3(0.0, separation.greaterThan(bias).select(1, 0), 1.0);
488
+ }
357
489
  const viewZ = this.camera.isPerspectiveCamera
358
490
  ? perspectiveDepthToViewZ(depth, this._cameraNear, this._cameraFar)
359
491
  : orthographicDepthToViewZ(depth, this._cameraNear, this._cameraFar);
360
- const depthTest = positionView.z.lessThanEqual(viewZ).select(1, 0);
361
- return vec3(0.0, depthTest, 1.0);
492
+ return vec3(0.0, positionView.z.lessThanEqual(viewZ).select(1, 0), 1.0);
362
493
  };
363
494
  const maskColorA = buildPrepareMask();
364
495
  this._prepareMaskMatA.colorNode = maskColorA;
@@ -445,6 +576,16 @@ export class MergedOutlineNode extends TempNode {
445
576
  dispose() {
446
577
  this.primaryObjects.length = 0;
447
578
  this.secondaryObjects.length = 0;
579
+ for (const proxy of this._proxiesA.values())
580
+ this._disposeProxyMaterial(proxy);
581
+ for (const proxy of this._proxiesB.values())
582
+ this._disposeProxyMaterial(proxy);
583
+ this._maskSceneA.clear();
584
+ this._maskSceneB.clear();
585
+ this._proxiesA.clear();
586
+ this._proxiesB.clear();
587
+ this._cacheA.clear();
588
+ this._cacheB.clear();
448
589
  this._depthRT.dispose();
449
590
  this._groupA.dispose();
450
591
  this._groupB.dispose();
@@ -466,6 +607,7 @@ export class MergedOutlineNode extends TempNode {
466
607
  this._compositeMatB.dispose();
467
608
  }
468
609
  _buildCache(objects, cache) {
610
+ cache.clear();
469
611
  for (const obj of objects) {
470
612
  obj.traverse((child) => {
471
613
  if (child.isMesh || child.isSprite)
@@ -0,0 +1,55 @@
1
+ export type PerfActionReceipt = {
2
+ name: string;
3
+ /** Free-form context, e.g. the node id or kind. */
4
+ detail: string;
5
+ /** begin → commit (the human gesture; 0 for instant actions like undo). */
6
+ dragMs: number;
7
+ /** commit → fully settled (rebuilds + one GPU sample after quiet). */
8
+ settleMs: number;
9
+ /** begin → settled. */
10
+ totalMs: number;
11
+ /** Frames observed between commit and settled. */
12
+ settleFrames: number;
13
+ /** Per-track attribution over begin → settled, sorted by totalMs desc. */
14
+ tracks: Array<{
15
+ name: string;
16
+ totalMs: number;
17
+ count: number;
18
+ }>;
19
+ outcome: 'settled' | 'interrupted' | 'timeout';
20
+ endedAt: number;
21
+ };
22
+ /**
23
+ * Start attributing samples to a named action. Interrupts any active one.
24
+ * Returns an id the caller can compare against `getActivePerfActionId()` to
25
+ * commit only the action it actually began.
26
+ */
27
+ export declare function beginPerfAction(name: string, detail?: string): number | null;
28
+ /** The gesture ended (pointer up / operation dispatched); settling begins. */
29
+ export declare function commitPerfAction(): void;
30
+ /** The gesture was aborted (Escape mid-drag); discard without a settle wait. */
31
+ export declare function cancelPerfAction(): void;
32
+ /** Convenience for instant actions (undo, level switch): begin + commit. */
33
+ export declare function markPerfAction(name: string, detail?: string): void;
34
+ /**
35
+ * Whether an action is currently being attributed. Lets a generic call site
36
+ * (the interaction scope) yield to a more specific one that began first.
37
+ */
38
+ export declare function hasActivePerfAction(): boolean;
39
+ /**
40
+ * Like `hasActivePerfAction`, but false once the active action has committed.
41
+ * A generic bracket yields to an UNCOMMITTED action (a gesture in flight) but
42
+ * must be free to start a new receipt while the previous one is merely
43
+ * settling — beginPerfAction then finalizes the settling one as interrupted.
44
+ */
45
+ export declare function hasUncommittedPerfAction(): boolean;
46
+ /** Id of the action currently attributing samples, if any. */
47
+ export declare function getActivePerfActionId(): number | null;
48
+ /**
49
+ * Called once per frame by the viewer settle system with the current dirty
50
+ * count and the wall system's deferred-neighbour backlog. Also the ledger's
51
+ * only heartbeat, so it is where a stuck action gets released.
52
+ */
53
+ export declare function notifyPerfActionFrame(dirtyCount: number, pendingRebuilds: number): void;
54
+ export declare function usePerfActionReceipts(): PerfActionReceipt[];
55
+ //# sourceMappingURL=perf-actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perf-actions.d.ts","sourceRoot":"","sources":["../../src/lib/perf-actions.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAA;IAChB,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAA;IACpB,0EAA0E;IAC1E,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC/D,OAAO,EAAE,SAAS,GAAG,aAAa,GAAG,SAAS,CAAA;IAC9C,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AA8FD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,MAAM,GAAG,IAAI,CAmCxE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,IAAI,CAG9D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAED,8DAA8D;AAC9D,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAErD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,CAgBvF;AAgBD,wBAAgB,qBAAqB,IAAI,iBAAiB,EAAE,CAE3D"}
@@ -0,0 +1,211 @@
1
+ // Action-cost ledger for `?perf`.
2
+ //
3
+ // An "action" is one user edit gesture: a wall-endpoint drag, a door move, an
4
+ // undo, a level switch. The editor package brackets the gesture with
5
+ // `beginPerfAction` / `commitPerfAction`; every perf-tracks sample recorded in
6
+ // between (wall-csg, geometry, react-render, gpu-render, …) is attributed to
7
+ // it. The action is "settled" only when the scene has finished digesting the
8
+ // edit: dirty queue empty, deferred wall neighbour rebuilds flushed, and one
9
+ // more GPU sample resolved after that — i.e. the user actually sees the final
10
+ // result. The settle system in the viewer feeds that state per frame via
11
+ // `notifyPerfActionFrame`.
12
+ //
13
+ // Everything is a no-op without `?perf`.
14
+ import { useSyncExternalStore } from 'react';
15
+ import { PERF_OVERLAY_ENABLED } from './gpu-perf';
16
+ import { subscribePerfSamples } from './perf-tracks';
17
+ const SETTLE_TIMEOUT_MS = 5000;
18
+ // A gesture that begins and never commits (a hover preview, a drag whose
19
+ // pointerup never reached the caller) is never settle-checked, so without an
20
+ // absolute cap it would hold its sample subscription — and keep growing its
21
+ // buckets — for the rest of the session.
22
+ const UNCOMMITTED_TIMEOUT_MS = 60_000;
23
+ const MAX_RECEIPTS = 5;
24
+ let active = null;
25
+ let actionSeq = 0;
26
+ // Whether this device has ever produced a real timestamp-query sample. Without
27
+ // `timestamp-query` support no 'gpu-render' sample can ever arrive, so settle
28
+ // falls back to the queue fence — see the sample listener below.
29
+ let gpuTimestampsSeen = false;
30
+ let receipts = [];
31
+ const listeners = new Set();
32
+ function emitReceipts() {
33
+ for (const listener of listeners)
34
+ listener();
35
+ }
36
+ function finalize(outcome) {
37
+ const action = active;
38
+ if (!action)
39
+ return;
40
+ active = null;
41
+ action.unsubscribe();
42
+ const now = performance.now();
43
+ const committedAt = action.committedAt ?? now;
44
+ const receipt = {
45
+ name: action.name,
46
+ detail: action.detail,
47
+ dragMs: committedAt - action.startedAt,
48
+ settleMs: now - committedAt,
49
+ totalMs: now - action.startedAt,
50
+ settleFrames: action.settleFrames,
51
+ tracks: [...action.buckets.entries()]
52
+ .map(([name, b]) => ({ name, totalMs: b.totalMs, count: b.count }))
53
+ .sort((a, b) => b.totalMs - a.totalMs),
54
+ outcome,
55
+ endedAt: now,
56
+ };
57
+ receipts = [receipt, ...receipts].slice(0, MAX_RECEIPTS);
58
+ emitReceipts();
59
+ // One timeline entry per action so recordings show the full span with its
60
+ // breakdown attached.
61
+ try {
62
+ performance.measure(`${action.name}${action.detail ? ` ${action.detail}` : ''}`, {
63
+ start: action.startedAt,
64
+ end: now,
65
+ detail: {
66
+ devtools: {
67
+ dataType: 'track-entry',
68
+ track: 'Actions',
69
+ trackGroup: 'Pascal',
70
+ color: outcome === 'settled' ? 'secondary' : 'error',
71
+ properties: [
72
+ ['outcome', outcome],
73
+ ['drag ms', receipt.dragMs.toFixed(1)],
74
+ ['settle ms', receipt.settleMs.toFixed(1)],
75
+ ...receipt.tracks
76
+ .slice(0, 6)
77
+ .map((t) => [t.name, `${t.totalMs.toFixed(1)}ms (${t.count}×)`]),
78
+ ],
79
+ },
80
+ },
81
+ });
82
+ }
83
+ catch { }
84
+ // eslint-disable-next-line no-console
85
+ console.log(`[perf] ${action.name}${action.detail ? ` (${action.detail})` : ''}: ` +
86
+ `${receipt.totalMs.toFixed(0)}ms total — drag ${receipt.dragMs.toFixed(0)}, ` +
87
+ `settle ${receipt.settleMs.toFixed(0)} over ${receipt.settleFrames} frames [${outcome}] — ` +
88
+ receipt.tracks
89
+ .slice(0, 6)
90
+ .map((t) => `${t.name} ${t.totalMs.toFixed(1)}ms`)
91
+ .join(', '));
92
+ }
93
+ /**
94
+ * Start attributing samples to a named action. Interrupts any active one.
95
+ * Returns an id the caller can compare against `getActivePerfActionId()` to
96
+ * commit only the action it actually began.
97
+ */
98
+ export function beginPerfAction(name, detail = '') {
99
+ if (!PERF_OVERLAY_ENABLED)
100
+ return null;
101
+ if (active)
102
+ finalize('interrupted');
103
+ const id = ++actionSeq;
104
+ const buckets = new Map();
105
+ active = {
106
+ id,
107
+ name,
108
+ detail,
109
+ startedAt: performance.now(),
110
+ committedAt: null,
111
+ settleFrames: 0,
112
+ awaitingGpu: false,
113
+ buckets,
114
+ unsubscribe: subscribePerfSamples((track, ms) => {
115
+ const bucket = buckets.get(track);
116
+ if (bucket) {
117
+ bucket.totalMs += ms;
118
+ bucket.count += 1;
119
+ }
120
+ else {
121
+ buckets.set(track, { totalMs: ms, count: 1 });
122
+ }
123
+ if (track === 'gpu-render')
124
+ gpuTimestampsSeen = true;
125
+ // A GPU sample landing after the quiet point is the settle signal. On
126
+ // devices without timestamp-query no 'gpu-render' sample ever arrives —
127
+ // the queue fence is the closest "the user saw it" stand-in there.
128
+ if (active?.awaitingGpu &&
129
+ (track === 'gpu-render' || (!gpuTimestampsSeen && track === 'gpu-queue'))) {
130
+ finalize('settled');
131
+ }
132
+ }),
133
+ };
134
+ return id;
135
+ }
136
+ /** The gesture ended (pointer up / operation dispatched); settling begins. */
137
+ export function commitPerfAction() {
138
+ if (!active || active.committedAt !== null)
139
+ return;
140
+ active.committedAt = performance.now();
141
+ }
142
+ /** The gesture was aborted (Escape mid-drag); discard without a settle wait. */
143
+ export function cancelPerfAction() {
144
+ if (!active)
145
+ return;
146
+ finalize('interrupted');
147
+ }
148
+ /** Convenience for instant actions (undo, level switch): begin + commit. */
149
+ export function markPerfAction(name, detail = '') {
150
+ beginPerfAction(name, detail);
151
+ commitPerfAction();
152
+ }
153
+ /**
154
+ * Whether an action is currently being attributed. Lets a generic call site
155
+ * (the interaction scope) yield to a more specific one that began first.
156
+ */
157
+ export function hasActivePerfAction() {
158
+ return active !== null;
159
+ }
160
+ /**
161
+ * Like `hasActivePerfAction`, but false once the active action has committed.
162
+ * A generic bracket yields to an UNCOMMITTED action (a gesture in flight) but
163
+ * must be free to start a new receipt while the previous one is merely
164
+ * settling — beginPerfAction then finalizes the settling one as interrupted.
165
+ */
166
+ export function hasUncommittedPerfAction() {
167
+ return active !== null && active.committedAt === null;
168
+ }
169
+ /** Id of the action currently attributing samples, if any. */
170
+ export function getActivePerfActionId() {
171
+ return active?.id ?? null;
172
+ }
173
+ /**
174
+ * Called once per frame by the viewer settle system with the current dirty
175
+ * count and the wall system's deferred-neighbour backlog. Also the ledger's
176
+ * only heartbeat, so it is where a stuck action gets released.
177
+ */
178
+ export function notifyPerfActionFrame(dirtyCount, pendingRebuilds) {
179
+ const action = active;
180
+ if (!action)
181
+ return;
182
+ const now = performance.now();
183
+ if (action.committedAt === null) {
184
+ if (now - action.startedAt > UNCOMMITTED_TIMEOUT_MS)
185
+ finalize('interrupted');
186
+ return;
187
+ }
188
+ action.settleFrames += 1;
189
+ if (now - action.committedAt > SETTLE_TIMEOUT_MS) {
190
+ finalize('timeout');
191
+ return;
192
+ }
193
+ if (!action.awaitingGpu && dirtyCount === 0 && pendingRebuilds === 0) {
194
+ action.awaitingGpu = true;
195
+ }
196
+ }
197
+ // Module-level so the panel's twice-a-second re-render doesn't tear the
198
+ // subscription down and rebuild it; `receipts` is replaced, never mutated, so
199
+ // the snapshot is stable between finalizes.
200
+ function subscribeReceipts(listener) {
201
+ listeners.add(listener);
202
+ return () => {
203
+ listeners.delete(listener);
204
+ };
205
+ }
206
+ function getReceipts() {
207
+ return receipts;
208
+ }
209
+ export function usePerfActionReceipts() {
210
+ return useSyncExternalStore(subscribeReceipts, getReceipts, getReceipts);
211
+ }
@@ -0,0 +1,2 @@
1
+ export declare function initPerfObservers(): void;
2
+ //# sourceMappingURL=perf-observers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perf-observers.d.ts","sourceRoot":"","sources":["../../src/lib/perf-observers.ts"],"names":[],"mappings":"AAKA,wBAAgB,iBAAiB,IAAI,IAAI,CAYxC"}
@@ -0,0 +1,17 @@
1
+ import { PERF_OVERLAY_ENABLED } from './gpu-perf';
2
+ import { recordPerfSample } from './perf-tracks';
3
+ let initialized = false;
4
+ export function initPerfObservers() {
5
+ if (!PERF_OVERLAY_ENABLED || initialized)
6
+ return;
7
+ initialized = true;
8
+ try {
9
+ const observer = new PerformanceObserver((list) => {
10
+ for (const entry of list.getEntries()) {
11
+ recordPerfSample('long-task', entry.duration);
12
+ }
13
+ });
14
+ observer.observe({ type: 'longtask', buffered: true });
15
+ }
16
+ catch { }
17
+ }