@uptimizr/playcanvas 0.1.2 → 0.2.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/dist/collector.d.ts +8 -0
- package/dist/collector.d.ts.map +1 -1
- package/dist/collector.js +152 -174
- package/dist/collector.js.map +1 -1
- package/dist/uptimizr-playcanvas.global.js +33 -33
- package/dist/uptimizr-playcanvas.js +33 -33
- package/package.json +4 -4
package/dist/collector.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ export interface PlayCanvasCaptureOptions {
|
|
|
18
18
|
perf?: boolean;
|
|
19
19
|
/** WebGL `webglcontextlost` / `webglcontextrestored` transitions on the canvas. */
|
|
20
20
|
contextLoss?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Asset / resource load timing (`asset_load`, #14). **On by default**: it hooks
|
|
23
|
+
* the PlayCanvas `app.assets` registry load lifecycle (`load:start` → `load` /
|
|
24
|
+
* `error`) and emits one `asset_load` per observed load with its duration and
|
|
25
|
+
* (when known) byte size. Privacy (ADR 0003): only the asset's app-defined
|
|
26
|
+
* **name** is recorded — never the file URL.
|
|
27
|
+
*/
|
|
28
|
+
assetLoad?: boolean;
|
|
21
29
|
/**
|
|
22
30
|
* Per-object dwell / attention capture (`mesh_visibility`, #37). **Opt-in, off
|
|
23
31
|
* by default** (privacy, ADR 0003). Each window the connector emits one
|
package/dist/collector.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"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"}
|
package/dist/collector.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { poseUnchanged, resolveCadence, toCanonicalDirection, toCanonicalPosition, toCanonicalQuat, } from "@uptimizr/sdk-core";
|
|
2
|
+
import { LIMITS } from "@uptimizr/schema";
|
|
2
3
|
import { clamp01, toVec3 } from "./vec.js";
|
|
3
4
|
import { createGazeRaycaster, createSceneRaycaster } from "./raycast.js";
|
|
4
5
|
/**
|
|
@@ -24,30 +25,14 @@ function isPointerLocked(getCanvas) {
|
|
|
24
25
|
const locked = document.pointerLockElement;
|
|
25
26
|
return locked != null && locked === getCanvas();
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
/** True when two poses are equal within `eps`. */
|
|
31
|
-
function poseUnchanged(a, b, eps) {
|
|
32
|
-
if (!vec3Close(a.position, b.position, eps))
|
|
33
|
-
return false;
|
|
34
|
-
if (!vec3Close(a.direction, b.direction, eps))
|
|
35
|
-
return false;
|
|
36
|
-
if ((a.fov === undefined) !== (b.fov === undefined))
|
|
37
|
-
return false;
|
|
38
|
-
if (a.fov !== undefined && b.fov !== undefined && Math.abs(a.fov - b.fov) > eps)
|
|
39
|
-
return false;
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
28
|
+
/** Max stored `asset_load.name` length (schema bound, ADR 0003). */
|
|
29
|
+
const ASSET_NAME_MAX = LIMITS.maxAssetNameLength;
|
|
42
30
|
function sub3(a, b) {
|
|
43
31
|
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
44
32
|
}
|
|
45
33
|
function dot3(a, b) {
|
|
46
34
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
47
35
|
}
|
|
48
|
-
function len3(a) {
|
|
49
|
-
return Math.hypot(a[0], a[1], a[2]);
|
|
50
|
-
}
|
|
51
36
|
/**
|
|
52
37
|
* Derive the per-tick capture rate from a `sampling.nodes` entry: either a bare
|
|
53
38
|
* {@link SampleRate} (root-only, ADR 0027) or a {@link NodeSamplingConfig} whose
|
|
@@ -138,22 +123,6 @@ function readNodeTransform(node, scaleEps) {
|
|
|
138
123
|
}
|
|
139
124
|
return sample;
|
|
140
125
|
}
|
|
141
|
-
/** True when two node samples are equal within `eps` (scale presence must also match). */
|
|
142
|
-
function nodeSampleUnchanged(a, b, eps) {
|
|
143
|
-
if (!vec3Close(a.position, b.position, eps))
|
|
144
|
-
return false;
|
|
145
|
-
if (Math.abs(a.rotation[0] - b.rotation[0]) > eps ||
|
|
146
|
-
Math.abs(a.rotation[1] - b.rotation[1]) > eps ||
|
|
147
|
-
Math.abs(a.rotation[2] - b.rotation[2]) > eps ||
|
|
148
|
-
Math.abs(a.rotation[3] - b.rotation[3]) > eps) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
if ((a.scale === undefined) !== (b.scale === undefined))
|
|
152
|
-
return false;
|
|
153
|
-
if (a.scale && b.scale && !vec3Close(a.scale, b.scale, eps))
|
|
154
|
-
return false;
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
126
|
/**
|
|
158
127
|
* Resolve a declared {@link PlayCanvasActor} to a live node, or `null` when it is
|
|
159
128
|
* not (yet) in the scene. A function is called each time (robust to load order
|
|
@@ -240,17 +209,6 @@ function readPlayCanvasBoneTransform(bone, scaleEps) {
|
|
|
240
209
|
}
|
|
241
210
|
return sample;
|
|
242
211
|
}
|
|
243
|
-
/** Round an AABB to mm precision so tiny float jitter doesn't re-send the box. */
|
|
244
|
-
function roundAabb(b) {
|
|
245
|
-
return b.map((v) => Math.round(v * 1000) / 1000);
|
|
246
|
-
}
|
|
247
|
-
/** True when two AABBs match within `eps` on every axis. */
|
|
248
|
-
function aabbClose(a, b, eps) {
|
|
249
|
-
for (let i = 0; i < 6; i++)
|
|
250
|
-
if (Math.abs((a[i] ?? 0) - (b[i] ?? 0)) > eps)
|
|
251
|
-
return false;
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
212
|
function meshInstancesOf(node) {
|
|
255
213
|
return node.render?.meshInstances ?? node.model?.meshInstances ?? [];
|
|
256
214
|
}
|
|
@@ -344,6 +302,8 @@ export function playcanvasCollector(options) {
|
|
|
344
302
|
meshPicks: capture.meshPicks ?? true,
|
|
345
303
|
perf: (capture.perf ?? true) && perfCadence.mode !== "off",
|
|
346
304
|
contextLoss: capture.contextLoss ?? true,
|
|
305
|
+
// Asset load timing is on by default (#14): startup perf, name-only (no URL).
|
|
306
|
+
assetLoad: capture.assetLoad ?? true,
|
|
347
307
|
// Opt-in, off by default (privacy, ADR 0003).
|
|
348
308
|
meshVisibility: capture.meshVisibility ?? false,
|
|
349
309
|
hoverDwell: capture.hoverDwell ?? false,
|
|
@@ -417,6 +377,17 @@ export function playcanvasCollector(options) {
|
|
|
417
377
|
name: "playcanvas",
|
|
418
378
|
start(ctx) {
|
|
419
379
|
const appView = app;
|
|
380
|
+
// The engine-agnostic aggregator (#10): the connector reads live engine
|
|
381
|
+
// state into plain-number snapshot DTOs and hands them here; the
|
|
382
|
+
// offload-eligible math (matrix-free decompose idle-diffs, visibility
|
|
383
|
+
// bucketing, gesture classification) runs main-thread by default or inside
|
|
384
|
+
// the offload worker, behind the client `offload` flag.
|
|
385
|
+
const aggregatorConfig = {
|
|
386
|
+
perf: { suppressIdle: suppressIdlePerfSamples, fpsThreshold: perfFpsThreshold },
|
|
387
|
+
node: { suppressIdle: suppressIdleSamples },
|
|
388
|
+
visibility: { centeredCos: visCenteredCos, boundingBox: visBoundingBox, boundsEps: 1e-3 },
|
|
389
|
+
};
|
|
390
|
+
const snapshot = ctx.createAggregation(aggregatorConfig);
|
|
420
391
|
const timers = [];
|
|
421
392
|
const domListeners = [];
|
|
422
393
|
const frameCallbacks = [];
|
|
@@ -426,7 +397,6 @@ export function playcanvasCollector(options) {
|
|
|
426
397
|
let disposed = false;
|
|
427
398
|
let lastPointerMove = 0;
|
|
428
399
|
let lastPose;
|
|
429
|
-
let lastFps;
|
|
430
400
|
// camera_gesture (ADR 0025) bracket state: the camera snapshot at
|
|
431
401
|
// pointer-down, diffed against pointer-up to classify the navigation.
|
|
432
402
|
let gestureStart = null;
|
|
@@ -476,8 +446,8 @@ export function playcanvasCollector(options) {
|
|
|
476
446
|
? toCanonicalPosition(gazeHit.point, "right")
|
|
477
447
|
: undefined;
|
|
478
448
|
const hitMesh = gazeHit && gazeHit.name ? gazeHit.name : undefined;
|
|
479
|
-
|
|
480
|
-
|
|
449
|
+
snapshot({
|
|
450
|
+
channel: "camera",
|
|
481
451
|
position: pose.position,
|
|
482
452
|
direction: pose.direction,
|
|
483
453
|
...(pose.fov !== undefined ? { fov: pose.fov } : {}),
|
|
@@ -508,13 +478,11 @@ export function playcanvasCollector(options) {
|
|
|
508
478
|
const fps = readFps();
|
|
509
479
|
if (fps <= 0)
|
|
510
480
|
return;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
lastFps = fps;
|
|
517
|
-
ctx.emit({ type: "frame_perf", fps });
|
|
481
|
+
// Hand the engine FPS read to the aggregator; it owns the idle-diff (and,
|
|
482
|
+
// for engines that supply a frame-time window, the percentiles/longFrames).
|
|
483
|
+
// PlayCanvas exposes only an averaged FPS, so the window stays empty and the
|
|
484
|
+
// aggregator emits `frame_perf` with `fps` alone — byte-for-byte unchanged.
|
|
485
|
+
snapshot({ channel: "perf", frameTimes: new Float32Array(0), fps, jankFrameMs: 0 });
|
|
518
486
|
};
|
|
519
487
|
// Drive a continuous channel either on a timer (fixed interval) or once per
|
|
520
488
|
// engine frame ("frame"). PlayCanvas owns its render loop, so the connector
|
|
@@ -553,10 +521,12 @@ export function playcanvasCollector(options) {
|
|
|
553
521
|
// actor gets its own cadence-driven sampler: resolve the node (lazily —
|
|
554
522
|
// resolvers handle load order), refuse cameras (the visitor camera is
|
|
555
523
|
// already `camera_sample`; "events live once"), read the WORLD transform,
|
|
556
|
-
// convert it to the canonical frame, and
|
|
557
|
-
//
|
|
524
|
+
// convert it to the canonical frame, and hand the decomposed sample to the
|
|
525
|
+
// aggregator. PlayCanvas applies a right-handed→canonical conversion (ADR
|
|
526
|
+
// 0018), so we pass the already-decomposed sample (never the raw matrix
|
|
527
|
+
// path); the aggregator owns the idle-diff so a static actor stays off the
|
|
528
|
+
// wire (#10).
|
|
558
529
|
const root = appView.root;
|
|
559
|
-
const lastNodeSample = new Map();
|
|
560
530
|
const refusedCamera = new Set();
|
|
561
531
|
for (const id of actorIds) {
|
|
562
532
|
const actor = actorMap[id];
|
|
@@ -580,40 +550,24 @@ export function playcanvasCollector(options) {
|
|
|
580
550
|
}
|
|
581
551
|
return;
|
|
582
552
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
// Subtree descendants (ADR 0033): walk the bounded hierarchy and emit
|
|
596
|
-
// each kept node's WORLD transform with its `childPath`. Idle
|
|
597
|
-
// suppression is keyed per (actor, childPath) so a static part costs
|
|
598
|
-
// nothing on the wire.
|
|
553
|
+
snapshot({
|
|
554
|
+
channel: "node",
|
|
555
|
+
nodeId: id,
|
|
556
|
+
decomposed: readNodeTransform(node, cameraEpsilon),
|
|
557
|
+
scaleEps: cameraEpsilon,
|
|
558
|
+
});
|
|
559
|
+
// Subtree descendants (ADR 0033): walk the bounded hierarchy and hand
|
|
560
|
+
// each kept node's WORLD transform with its `childPath` to the
|
|
561
|
+
// aggregator, which idle-diffs per (actor, childPath) so a static part
|
|
562
|
+
// costs nothing on the wire.
|
|
599
563
|
if (subtree) {
|
|
600
564
|
for (const { childPath, node: child } of collectSubtree(node, subtree)) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const childPrev = lastNodeSample.get(key);
|
|
604
|
-
if (suppressIdleSamples &&
|
|
605
|
-
childPrev &&
|
|
606
|
-
nodeSampleUnchanged(childPrev, childSample, cameraEpsilon)) {
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
lastNodeSample.set(key, childSample);
|
|
610
|
-
ctx.emit({
|
|
611
|
-
type: "node_transform",
|
|
565
|
+
snapshot({
|
|
566
|
+
channel: "node",
|
|
612
567
|
nodeId: id,
|
|
613
568
|
childPath,
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
...(childSample.scale ? { scale: childSample.scale } : {}),
|
|
569
|
+
decomposed: readNodeTransform(child, cameraEpsilon),
|
|
570
|
+
scaleEps: cameraEpsilon,
|
|
617
571
|
});
|
|
618
572
|
}
|
|
619
573
|
}
|
|
@@ -626,11 +580,13 @@ export function playcanvasCollector(options) {
|
|
|
626
580
|
// Skeleton bone capture (`node_transform` + `boneId`, ADR 0027 Tier 2).
|
|
627
581
|
// For each declared skinned actor, resolve its skeleton bones (by the
|
|
628
582
|
// configured allowlist or "*"), then sample each bone's skeleton-LOCAL
|
|
629
|
-
// pose at the actor's cadence
|
|
630
|
-
//
|
|
631
|
-
//
|
|
583
|
+
// pose at the actor's cadence and hand the decomposed sample to the
|
|
584
|
+
// aggregator (which idle-diffs per (actor, bone), #10). PlayCanvas reads
|
|
585
|
+
// the local TRS via `getLocal*` and applies the canonical conversion (ADR
|
|
586
|
+
// 0018), so — like nodes — we pass the decomposed sample, never the raw
|
|
587
|
+
// matrix path. The local frame is parent-relative, so motion replays onto
|
|
588
|
+
// a differently-placed instance of the same rig (ADR 0027).
|
|
632
589
|
const root = appView.root;
|
|
633
|
-
const lastBoneSample = new Map();
|
|
634
590
|
const warnedNoSkeleton = new Set();
|
|
635
591
|
for (const id of boneActorIds) {
|
|
636
592
|
const actor = actorMap[id];
|
|
@@ -658,19 +614,12 @@ export function playcanvasCollector(options) {
|
|
|
658
614
|
const sample = readPlayCanvasBoneTransform(bone, cameraEpsilon);
|
|
659
615
|
if (!sample)
|
|
660
616
|
continue;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (suppressIdleSamples && prev && nodeSampleUnchanged(prev, sample, cameraEpsilon)) {
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
lastBoneSample.set(key, sample);
|
|
667
|
-
ctx.emit({
|
|
668
|
-
type: "node_transform",
|
|
617
|
+
snapshot({
|
|
618
|
+
channel: "node",
|
|
669
619
|
nodeId: id,
|
|
670
620
|
boneId: boneName,
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
...(sample.scale ? { scale: sample.scale } : {}),
|
|
621
|
+
decomposed: sample,
|
|
622
|
+
scaleEps: cameraEpsilon,
|
|
674
623
|
});
|
|
675
624
|
}
|
|
676
625
|
};
|
|
@@ -679,14 +628,12 @@ export function playcanvasCollector(options) {
|
|
|
679
628
|
}
|
|
680
629
|
}
|
|
681
630
|
// --- Per-object dwell (`mesh_visibility`, #37) ---
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
631
|
+
// Each engine frame we read which tracked objects are on-screen (the engine
|
|
632
|
+
// bounds/frustum reads must stay main-thread) and hand the raw per-tick
|
|
633
|
+
// observations to the aggregator; it owns the dwell/centred/screen-fraction
|
|
634
|
+
// bucketing and the per-window flush (ADR 0012, #10). `frameend` pauses when
|
|
635
|
+
// the tab is hidden — like Babylon's onBeforeRender.
|
|
685
636
|
if (want.meshVisibility) {
|
|
686
|
-
const accum = new Map();
|
|
687
|
-
// Last AABB sent per mesh, so a static box is sent once then suppressed.
|
|
688
|
-
const sentBounds = new Map();
|
|
689
|
-
const boundsEps = 1e-3;
|
|
690
637
|
let lastVisTime = ctx.now();
|
|
691
638
|
const sampleVisibility = () => {
|
|
692
639
|
const now = ctx.now();
|
|
@@ -699,11 +646,12 @@ export function playcanvasCollector(options) {
|
|
|
699
646
|
const cf = camView.forward;
|
|
700
647
|
const camPos = [cp.x, cp.y, cp.z];
|
|
701
648
|
const forward = [cf.x, cf.y, cf.z];
|
|
702
|
-
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
const
|
|
649
|
+
// Vertical FOV in radians; `0` lets the aggregator apply its `0.8`
|
|
650
|
+
// default (→ half-FOV `0.4`), matching the previous ortho/missing-fov
|
|
651
|
+
// fallback.
|
|
652
|
+
const fov = verticalFovRad(camView.camera) ?? 0;
|
|
706
653
|
let tracked = 0;
|
|
654
|
+
const observations = [];
|
|
707
655
|
appView.root?.forEach?.((raw) => {
|
|
708
656
|
const node = raw;
|
|
709
657
|
if (meshInstancesOf(node).length === 0)
|
|
@@ -728,54 +676,41 @@ export function playcanvasCollector(options) {
|
|
|
728
676
|
if (!sphereInFront(bounds.center, bounds.radius, camPos, forward)) {
|
|
729
677
|
return;
|
|
730
678
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
};
|
|
753
|
-
const flushVisibility = () => {
|
|
754
|
-
for (const [mesh, entry] of accum) {
|
|
755
|
-
if (entry.visibleMs <= 0)
|
|
756
|
-
continue;
|
|
757
|
-
let bounds;
|
|
758
|
-
if (entry.bounds) {
|
|
759
|
-
const rounded = roundAabb(entry.bounds);
|
|
760
|
-
const prev = sentBounds.get(mesh);
|
|
761
|
-
if (!prev || !aabbClose(prev, rounded, boundsEps)) {
|
|
762
|
-
bounds = rounded;
|
|
763
|
-
sentBounds.set(mesh, rounded);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
ctx.emit({
|
|
767
|
-
type: "mesh_visibility",
|
|
768
|
-
mesh,
|
|
769
|
-
visibleMs: Math.round(entry.visibleMs),
|
|
770
|
-
...(entry.centeredMs > 0 ? { centeredMs: Math.round(entry.centeredMs) } : {}),
|
|
771
|
-
...(entry.maxScreenFraction > 0
|
|
772
|
-
? { maxScreenFraction: entry.maxScreenFraction }
|
|
679
|
+
observations.push({
|
|
680
|
+
mesh: name,
|
|
681
|
+
center: bounds.center,
|
|
682
|
+
radius: bounds.radius,
|
|
683
|
+
// Ride the world AABB along only when bounds capture is on (#53). The
|
|
684
|
+
// canonical frame negates Z (right-handed PlayCanvas → canonical),
|
|
685
|
+
// swapping the Z min/max — match hitPoint / camera_sample (ADR 0018);
|
|
686
|
+
// the aggregator dedupes/rounds it across the window.
|
|
687
|
+
...(visBoundingBox
|
|
688
|
+
? {
|
|
689
|
+
aabb: [
|
|
690
|
+
bounds.aabb[0],
|
|
691
|
+
bounds.aabb[1],
|
|
692
|
+
-bounds.aabb[5],
|
|
693
|
+
bounds.aabb[3],
|
|
694
|
+
bounds.aabb[4],
|
|
695
|
+
-bounds.aabb[2],
|
|
696
|
+
],
|
|
697
|
+
}
|
|
773
698
|
: {}),
|
|
774
|
-
...(bounds ? { bounds } : {}),
|
|
775
699
|
});
|
|
776
|
-
}
|
|
777
|
-
|
|
700
|
+
});
|
|
701
|
+
if (observations.length === 0)
|
|
702
|
+
return;
|
|
703
|
+
snapshot({
|
|
704
|
+
channel: "visibilityTick",
|
|
705
|
+
stepMs,
|
|
706
|
+
camPos,
|
|
707
|
+
// Pass the raw (un-normalized) forward; the aggregator normalizes.
|
|
708
|
+
forward,
|
|
709
|
+
fov,
|
|
710
|
+
meshes: observations,
|
|
711
|
+
});
|
|
778
712
|
};
|
|
713
|
+
const flushVisibility = () => snapshot({ channel: "visibilityFlush" });
|
|
779
714
|
// Sample every frame; flush on the window timer + once on stop (trailing).
|
|
780
715
|
frameCallbacks.push(sampleVisibility);
|
|
781
716
|
ensureFrameendBound();
|
|
@@ -840,8 +775,8 @@ export function playcanvasCollector(options) {
|
|
|
840
775
|
if (hoverMesh !== undefined && !hoverActed) {
|
|
841
776
|
const dwellMs = now - hoverStartMs;
|
|
842
777
|
if (dwellMs >= hoverMinDwellMs) {
|
|
843
|
-
|
|
844
|
-
|
|
778
|
+
snapshot({
|
|
779
|
+
channel: "hover",
|
|
845
780
|
mesh: hoverMesh,
|
|
846
781
|
dwellMs,
|
|
847
782
|
...(hoverSource ? { source: hoverSource } : {}),
|
|
@@ -977,19 +912,14 @@ export function playcanvasCollector(options) {
|
|
|
977
912
|
gestureStart = null;
|
|
978
913
|
if (!opened)
|
|
979
914
|
return;
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
type: "camera_gesture",
|
|
987
|
-
kind: classified.kind,
|
|
915
|
+
// Hand the start→end bracket to the aggregator; the (pure) classification
|
|
916
|
+
// math runs there, main-thread or in the worker (#10).
|
|
917
|
+
snapshot({
|
|
918
|
+
channel: "gesture",
|
|
919
|
+
start: opened.sample,
|
|
920
|
+
end: readGestureSample(),
|
|
988
921
|
durationMs: Math.max(0, Math.round(ctx.now() - opened.ts)),
|
|
989
|
-
|
|
990
|
-
...(classified.rollDeg !== undefined ? { rollDeg: classified.rollDeg } : {}),
|
|
991
|
-
...(classified.zoomRatio !== undefined ? { zoomRatio: classified.zoomRatio } : {}),
|
|
992
|
-
...(classified.panDist !== undefined ? { panDist: classified.panDist } : {}),
|
|
922
|
+
options: { sensitivity: cameraGestureSensitivity },
|
|
993
923
|
...(opened.source ? { source: opened.source } : {}),
|
|
994
924
|
});
|
|
995
925
|
};
|
|
@@ -1006,6 +936,54 @@ export function playcanvasCollector(options) {
|
|
|
1006
936
|
addListener("webglcontextlost", () => ctx.emit({ type: "context_lost" }));
|
|
1007
937
|
addListener("webglcontextrestored", () => ctx.emit({ type: "context_restored" }));
|
|
1008
938
|
}
|
|
939
|
+
// Asset / resource load timing (`asset_load`, #14). PlayCanvas' `app.assets`
|
|
940
|
+
// registry (an EventHandler) exposes the load lifecycle: `load:start` fires
|
|
941
|
+
// when an asset begins network loading, `load` on success, `error` on
|
|
942
|
+
// failure. We bracket start→load to report a duration; assets whose load we
|
|
943
|
+
// never observed from the start (already-loaded, or added before tracking)
|
|
944
|
+
// are skipped so `loadMs` is never fabricated. Privacy (ADR 0003): only the
|
|
945
|
+
// asset's app-defined `name` is recorded — never the file URL.
|
|
946
|
+
const assets = appView.assets;
|
|
947
|
+
if (want.assetLoad && assets) {
|
|
948
|
+
const startTimes = new Map();
|
|
949
|
+
const keyOf = (asset) => asset.id ?? asset;
|
|
950
|
+
const onLoadStart = (raw) => {
|
|
951
|
+
startTimes.set(keyOf(raw), ctx.now());
|
|
952
|
+
};
|
|
953
|
+
const onLoad = (raw) => {
|
|
954
|
+
const asset = raw;
|
|
955
|
+
const key = keyOf(asset);
|
|
956
|
+
const start = startTimes.get(key);
|
|
957
|
+
if (start === undefined)
|
|
958
|
+
return; // load not observed from the start
|
|
959
|
+
startTimes.delete(key);
|
|
960
|
+
const event = {
|
|
961
|
+
type: "asset_load",
|
|
962
|
+
name: (asset.name ?? "asset").slice(0, ASSET_NAME_MAX),
|
|
963
|
+
loadMs: Math.max(0, Math.round(ctx.now() - start)),
|
|
964
|
+
};
|
|
965
|
+
const bytes = asset.file?.size;
|
|
966
|
+
if (typeof bytes === "number" && bytes > 0)
|
|
967
|
+
event.bytes = bytes;
|
|
968
|
+
ctx.emit(event);
|
|
969
|
+
};
|
|
970
|
+
const onError = (...args) => {
|
|
971
|
+
// Registry fires `error(err, asset)`; drop the pending start (no emit —
|
|
972
|
+
// `asset_load` is a success-timing signal with no error field).
|
|
973
|
+
const asset = args[1];
|
|
974
|
+
if (asset)
|
|
975
|
+
startTimes.delete(keyOf(asset));
|
|
976
|
+
};
|
|
977
|
+
assets.on("load:start", onLoadStart);
|
|
978
|
+
assets.on("load", onLoad);
|
|
979
|
+
assets.on("error", onError);
|
|
980
|
+
stopCallbacks.push(() => {
|
|
981
|
+
assets.off("load:start", onLoadStart);
|
|
982
|
+
assets.off("load", onLoad);
|
|
983
|
+
assets.off("error", onError);
|
|
984
|
+
startTimes.clear();
|
|
985
|
+
});
|
|
986
|
+
}
|
|
1009
987
|
// GPU / memory footprint (`resource_sample`, #44). A low-rate timer samples
|
|
1010
988
|
// the triangles PlayCanvas submitted last frame (`app.stats.frame.triangles`)
|
|
1011
989
|
// and the JS heap. PlayCanvas exposes no per-frame vertex count or resident
|