@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.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/capture/asset-url.d.ts +2 -0
- package/dist/capture/asset-url.d.ts.map +1 -0
- package/dist/capture/asset-url.js +13 -0
- package/dist/capture/capture-runtime.d.ts +43 -0
- package/dist/capture/capture-runtime.d.ts.map +1 -0
- package/dist/capture/capture-runtime.js +228 -0
- package/dist/capture/frame.d.ts +4 -0
- package/dist/capture/frame.d.ts.map +1 -0
- package/dist/capture/frame.js +21 -0
- package/dist/capture/index.d.ts +12 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +11 -0
- package/dist/capture/layer-visibility.d.ts +5 -0
- package/dist/capture/layer-visibility.d.ts.map +1 -0
- package/dist/capture/layer-visibility.js +11 -0
- package/dist/capture/layers/clay-matcap.d.ts +3 -0
- package/dist/capture/layers/clay-matcap.d.ts.map +1 -0
- package/dist/capture/layers/clay-matcap.js +26 -0
- package/dist/capture/layers/device-motion-layer.d.ts +7 -0
- package/dist/capture/layers/device-motion-layer.d.ts.map +1 -0
- package/dist/capture/layers/device-motion-layer.js +78 -0
- package/dist/capture/layers/point-cloud-layer.d.ts +15 -0
- package/dist/capture/layers/point-cloud-layer.d.ts.map +1 -0
- package/dist/capture/layers/point-cloud-layer.js +106 -0
- package/dist/capture/layers/room-model-layer.d.ts +9 -0
- package/dist/capture/layers/room-model-layer.d.ts.map +1 -0
- package/dist/capture/layers/room-model-layer.js +71 -0
- package/dist/capture/layers/surface-mesh-data.d.ts +9 -0
- package/dist/capture/layers/surface-mesh-data.d.ts.map +1 -0
- package/dist/capture/layers/surface-mesh-data.js +74 -0
- package/dist/capture/layers/surface-mesh-layer.d.ts +7 -0
- package/dist/capture/layers/surface-mesh-layer.d.ts.map +1 -0
- package/dist/capture/layers/surface-mesh-layer.js +16 -0
- package/dist/capture/preview.d.ts +3 -0
- package/dist/capture/preview.d.ts.map +1 -0
- package/dist/capture/preview.js +2 -0
- package/dist/capture/source-state.d.ts +22 -0
- package/dist/capture/source-state.d.ts.map +1 -0
- package/dist/capture/source-state.js +182 -0
- package/dist/capture/stream-rendering.d.ts +14 -0
- package/dist/capture/stream-rendering.d.ts.map +1 -0
- package/dist/capture/stream-rendering.js +69 -0
- package/dist/capture/trajectory.d.ts +20 -0
- package/dist/capture/trajectory.d.ts.map +1 -0
- package/dist/capture/trajectory.js +77 -0
- package/dist/components/viewer/batched-mesh-spike.d.ts +14 -0
- package/dist/components/viewer/batched-mesh-spike.d.ts.map +1 -0
- package/dist/components/viewer/batched-mesh-spike.js +64 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +4 -3
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +70 -62
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +123 -89
- package/dist/components/viewer/perf-monitor.d.ts +6 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +307 -116
- package/dist/components/viewer/perf-panel.d.ts +3 -0
- package/dist/components/viewer/perf-panel.d.ts.map +1 -0
- package/dist/components/viewer/perf-panel.js +178 -0
- package/dist/components/viewer/pointer-raycast-layers.d.ts +15 -0
- package/dist/components/viewer/pointer-raycast-layers.d.ts.map +1 -0
- package/dist/components/viewer/pointer-raycast-layers.js +28 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +124 -83
- package/dist/components/viewer/registered-systems.d.ts +14 -5
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +22 -10
- package/dist/components/viewer/scene-atmosphere.d.ts +30 -0
- package/dist/components/viewer/scene-atmosphere.d.ts.map +1 -0
- package/dist/components/viewer/scene-atmosphere.js +89 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -1
- package/dist/components/viewer/scene-environment.js +19 -5
- package/dist/components/viewer/scene-ground-replacement.d.ts +5 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -0
- package/dist/components/viewer/scene-ground-replacement.js +35 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +4 -1
- package/dist/components/viewer/viewer-presentations.d.ts +67 -0
- package/dist/components/viewer/viewer-presentations.d.ts.map +1 -0
- package/dist/components/viewer/viewer-presentations.js +120 -0
- package/dist/hooks/use-library-materials-version.d.ts +7 -0
- package/dist/hooks/use-library-materials-version.d.ts.map +1 -0
- package/dist/hooks/use-library-materials-version.js +10 -0
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +1 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -6
- package/dist/lib/csg-utils.d.ts +1 -0
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +6 -1
- package/dist/lib/gpu-perf.d.ts +0 -2
- package/dist/lib/gpu-perf.d.ts.map +1 -1
- package/dist/lib/gpu-perf.js +7 -21
- package/dist/lib/layer-pass.d.ts +28 -0
- package/dist/lib/layer-pass.d.ts.map +1 -0
- package/dist/lib/layer-pass.js +254 -0
- package/dist/lib/materials.d.ts +12 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +51 -17
- package/dist/lib/merged-outline-node.d.ts +17 -8
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +194 -52
- package/dist/lib/perf-actions.d.ts +55 -0
- package/dist/lib/perf-actions.d.ts.map +1 -0
- package/dist/lib/perf-actions.js +211 -0
- package/dist/lib/perf-observers.d.ts +2 -0
- package/dist/lib/perf-observers.d.ts.map +1 -0
- package/dist/lib/perf-observers.js +17 -0
- package/dist/lib/perf-panel-store.d.ts +71 -0
- package/dist/lib/perf-panel-store.d.ts.map +1 -0
- package/dist/lib/perf-panel-store.js +29 -0
- package/dist/lib/perf-tracks.d.ts +43 -0
- package/dist/lib/perf-tracks.d.ts.map +1 -0
- package/dist/lib/perf-tracks.js +113 -0
- package/dist/lib/pointer-events.d.ts +7 -0
- package/dist/lib/pointer-events.d.ts.map +1 -0
- package/dist/lib/pointer-events.js +680 -0
- package/dist/lib/post-processing-resources.d.ts +12 -0
- package/dist/lib/post-processing-resources.d.ts.map +1 -0
- package/dist/lib/post-processing-resources.js +19 -0
- package/dist/lib/renderer-capability.d.ts.map +1 -1
- package/dist/lib/renderer-capability.js +4 -2
- package/dist/lib/scene-visibility.d.ts +2 -1
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +22 -1
- package/dist/store/use-viewer.d.ts +2 -2
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +1 -1
- package/dist/systems/door/door-animation-system.d.ts.map +1 -1
- package/dist/systems/door/door-animation-system.js +6 -9
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +13 -2
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +3 -2
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +1 -1
- package/dist/systems/level/level-utils.d.ts +9 -0
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +14 -0
- package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts +8 -0
- package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts.map +1 -0
- package/dist/systems/perf-action-settle/perf-action-settle-system.js +32 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1196 -58
- package/dist/systems/scan/scan-system.js +6 -5
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +1 -2
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +9 -8
- package/dist/systems/surface-hole-geometry.d.ts +1 -1
- package/dist/systems/surface-hole-geometry.d.ts.map +1 -1
- package/dist/systems/surface-hole-geometry.js +1 -1
- package/dist/systems/wall/level-miter-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-build-lifecycle.d.ts +9 -0
- package/dist/systems/wall/wall-build-lifecycle.d.ts.map +1 -0
- package/dist/systems/wall/wall-build-lifecycle.js +74 -0
- package/dist/systems/wall/wall-cutout-cache.d.ts +57 -0
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -0
- package/dist/systems/wall/wall-cutout-cache.js +304 -0
- package/dist/systems/wall/wall-cutout.d.ts +12 -1
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +24 -161
- package/dist/systems/wall/wall-material-variant.d.ts +31 -0
- package/dist/systems/wall/wall-material-variant.d.ts.map +1 -0
- package/dist/systems/wall/wall-material-variant.js +10 -0
- package/dist/systems/wall/wall-materials.d.ts +6 -0
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +29 -13
- package/dist/systems/wall/wall-placeholder-sweep.d.ts +50 -0
- package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -0
- package/dist/systems/wall/wall-placeholder-sweep.js +51 -0
- package/dist/systems/wall/wall-rebuild-notifications.d.ts +5 -0
- package/dist/systems/wall/wall-rebuild-notifications.d.ts.map +1 -0
- package/dist/systems/wall/wall-rebuild-notifications.js +23 -0
- package/dist/systems/wall/wall-system.d.ts +11 -4
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +405 -153
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +9 -8
- package/dist/systems/window/window-system.d.ts +1 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +16 -2
- package/package.json +17 -4
- package/dist/lib/polygon-union.d.ts +0 -4
- package/dist/lib/polygon-union.d.ts.map +0 -1
- 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)
|
|
6
|
-
*
|
|
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
|
-
|
|
264
|
-
|
|
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.
|
|
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 [
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
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
|
+
}
|