@sailfish-ai/recorder 1.12.4 → 1.12.6
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/README.md +8 -0
- package/dist/canvas/adapters.js +382 -0
- package/dist/canvas/adapters.js.br +0 -0
- package/dist/canvas/adapters.js.gz +0 -0
- package/dist/canvas/canvasManager.js +1949 -0
- package/dist/canvas/canvasManager.js.br +0 -0
- package/dist/canvas/canvasManager.js.gz +0 -0
- package/dist/canvas/canvasPrivacy.js +233 -0
- package/dist/canvas/canvasPrivacy.js.br +0 -0
- package/dist/canvas/canvasPrivacy.js.gz +0 -0
- package/dist/canvas/codec.js +314 -0
- package/dist/canvas/codec.js.br +0 -0
- package/dist/canvas/codec.js.gz +0 -0
- package/dist/canvas/deviceClass.js +67 -0
- package/dist/canvas/deviceClass.js.br +0 -0
- package/dist/canvas/deviceClass.js.gz +0 -0
- package/dist/canvas/encodeWorker.js +284 -0
- package/dist/canvas/encodeWorker.js.br +0 -0
- package/dist/canvas/encodeWorker.js.gz +0 -0
- package/dist/canvas/governor.js +79 -0
- package/dist/canvas/governor.js.br +0 -0
- package/dist/canvas/governor.js.gz +0 -0
- package/dist/canvas/keyframes.js +183 -0
- package/dist/canvas/keyframes.js.br +0 -0
- package/dist/canvas/keyframes.js.gz +0 -0
- package/dist/canvas/mediaRecorderCapture.js +257 -0
- package/dist/canvas/mediaRecorderCapture.js.br +0 -0
- package/dist/canvas/mediaRecorderCapture.js.gz +0 -0
- package/dist/canvas/objectLog.js +150 -0
- package/dist/canvas/objectLog.js.br +0 -0
- package/dist/canvas/objectLog.js.gz +0 -0
- package/dist/canvas/patch2d.js +1302 -0
- package/dist/canvas/patch2d.js.br +0 -0
- package/dist/canvas/patch2d.js.gz +0 -0
- package/dist/canvas/patchGL.js +371 -0
- package/dist/canvas/patchGL.js.br +0 -0
- package/dist/canvas/patchGL.js.gz +0 -0
- package/dist/canvas/patchWebGPU.js +361 -0
- package/dist/canvas/patchWebGPU.js.br +0 -0
- package/dist/canvas/patchWebGPU.js.gz +0 -0
- package/dist/canvas/piiDetect.js +155 -0
- package/dist/canvas/piiDetect.js.br +0 -0
- package/dist/canvas/piiDetect.js.gz +0 -0
- package/dist/canvas/protocol.js +122 -0
- package/dist/canvas/protocol.js.br +0 -0
- package/dist/canvas/protocol.js.gz +0 -0
- package/dist/canvas/videoCapture.js +252 -0
- package/dist/canvas/videoCapture.js.br +0 -0
- package/dist/canvas/videoCapture.js.gz +0 -0
- package/dist/canvas/workerRelay.js +99 -0
- package/dist/canvas/workerRelay.js.br +0 -0
- package/dist/canvas/workerRelay.js.gz +0 -0
- package/dist/canvas/workerShim.js +227 -0
- package/dist/canvas/workerShim.js.br +0 -0
- package/dist/canvas/workerShim.js.gz +0 -0
- package/dist/canvas-worker.cjs +74 -0
- package/dist/canvas-worker.cjs.br +0 -0
- package/dist/canvas-worker.cjs.gz +0 -0
- package/dist/canvas-worker.js +78 -0
- package/dist/canvas-worker.js.br +0 -0
- package/dist/canvas-worker.js.gz +0 -0
- package/dist/canvasFrameStore.js +198 -0
- package/dist/canvasFrameStore.js.br +0 -0
- package/dist/canvasFrameStore.js.gz +0 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js +1440 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js.br +0 -0
- package/dist/chunks/canvasManager-BYn3_oyo.js.gz +0 -0
- package/dist/chunks/canvasManager-CoX5072G.js +1456 -0
- package/dist/chunks/canvasManager-CoX5072G.js.br +0 -0
- package/dist/chunks/canvasManager-CoX5072G.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-FQtY90Av.js → chunkSerializer-C_K0Wyk0.js} +1 -1
- package/dist/chunks/chunkSerializer-C_K0Wyk0.js.br +0 -0
- package/dist/chunks/chunkSerializer-C_K0Wyk0.js.gz +0 -0
- package/dist/chunks/{chunkSerializer-DDukZpgl.js → chunkSerializer-CghPitF4.js} +1 -1
- package/dist/chunks/chunkSerializer-CghPitF4.js.br +0 -0
- package/dist/chunks/chunkSerializer-CghPitF4.js.gz +0 -0
- package/dist/chunks/{index-C-qbsfKe.js → index-BIj4l3CV.js} +1028 -650
- package/dist/chunks/index-BIj4l3CV.js.br +0 -0
- package/dist/chunks/index-BIj4l3CV.js.gz +0 -0
- package/dist/chunks/{index-D6axlCRu.js → index-DIYomvdD.js} +1119 -720
- package/dist/chunks/index-DIYomvdD.js.br +0 -0
- package/dist/chunks/index-DIYomvdD.js.gz +0 -0
- package/dist/chunks/patch2d-9voowhJ_.js +816 -0
- package/dist/chunks/patch2d-9voowhJ_.js.br +0 -0
- package/dist/chunks/patch2d-9voowhJ_.js.gz +0 -0
- package/dist/chunks/patch2d-PQxcaE6r.js +886 -0
- package/dist/chunks/patch2d-PQxcaE6r.js.br +0 -0
- package/dist/chunks/patch2d-PQxcaE6r.js.gz +0 -0
- package/dist/graphql.js +12 -1
- package/dist/graphql.js.br +0 -0
- package/dist/graphql.js.gz +0 -0
- package/dist/inAppReportIssueModal/index.js +48 -0
- package/dist/inAppReportIssueModal/index.js.br +0 -0
- package/dist/inAppReportIssueModal/index.js.gz +0 -0
- package/dist/index.js +144 -2
- package/dist/index.js.br +0 -0
- package/dist/index.js.gz +0 -0
- package/dist/recorder.cjs +2 -2
- package/dist/recorder.cjs.br +0 -0
- package/dist/recorder.cjs.gz +0 -0
- package/dist/recorder.js +52 -41
- package/dist/recorder.js.br +0 -0
- package/dist/recorder.js.gz +0 -0
- package/dist/recorder.umd.cjs +4847 -2212
- package/dist/recorder.umd.cjs.br +0 -0
- package/dist/recorder.umd.cjs.gz +0 -0
- package/dist/recording.js +119 -10
- package/dist/recording.js.br +0 -0
- package/dist/recording.js.gz +0 -0
- package/dist/types/canvas/adapters.d.ts +71 -0
- package/dist/types/canvas/canvasManager.d.ts +273 -0
- package/dist/types/canvas/canvasPrivacy.d.ts +184 -0
- package/dist/types/canvas/codec.d.ts +27 -0
- package/dist/types/canvas/deviceClass.d.ts +40 -0
- package/dist/types/canvas/encodeWorker.d.ts +54 -0
- package/dist/types/canvas/governor.d.ts +44 -0
- package/dist/types/canvas/keyframes.d.ts +81 -0
- package/dist/types/canvas/mediaRecorderCapture.d.ts +91 -0
- package/dist/types/canvas/objectLog.d.ts +75 -0
- package/dist/types/canvas/patch2d.d.ts +339 -0
- package/dist/types/canvas/patchGL.d.ts +74 -0
- package/dist/types/canvas/patchWebGPU.d.ts +37 -0
- package/dist/types/canvas/piiDetect.d.ts +60 -0
- package/dist/types/canvas/protocol.d.ts +176 -0
- package/dist/types/canvas/videoCapture.d.ts +67 -0
- package/dist/types/canvas/workerRelay.d.ts +29 -0
- package/dist/types/canvas/workerShim.d.ts +10 -0
- package/dist/types/canvasFrameStore.d.ts +23 -0
- package/dist/types/graphql.d.ts +1 -1
- package/dist/types/index.d.ts +99 -1
- package/dist/types/types.d.ts +40 -0
- package/dist/types/websocket.d.ts +19 -0
- package/dist/websocket.js +210 -0
- package/dist/websocket.js.br +0 -0
- package/dist/websocket.js.gz +0 -0
- package/package.json +7 -1
- package/dist/chunks/chunkSerializer-DDukZpgl.js.br +0 -0
- package/dist/chunks/chunkSerializer-DDukZpgl.js.gz +0 -0
- package/dist/chunks/chunkSerializer-FQtY90Av.js.br +0 -0
- package/dist/chunks/chunkSerializer-FQtY90Av.js.gz +0 -0
- package/dist/chunks/index-C-qbsfKe.js.br +0 -0
- package/dist/chunks/index-C-qbsfKe.js.gz +0 -0
- package/dist/chunks/index-D6axlCRu.js.br +0 -0
- package/dist/chunks/index-D6axlCRu.js.gz +0 -0
package/README.md
CHANGED
|
@@ -239,6 +239,14 @@ initRecorder({
|
|
|
239
239
|
|
|
240
240
|
`scripts/bench-perf.js` loads the same page twice — once with capture on, once with capture off — under headless Chrome, and reports CPU, JS heap, FCP/LCP, long-task time, and total load time side-by-side.
|
|
241
241
|
|
|
242
|
+
> **Canvas recording** has its own full, customer-facing cost/fidelity report —
|
|
243
|
+
> per-frame slowdown, CPU, RAM, recorded size and page weight for **2D, WebGL and
|
|
244
|
+
> WebGPU** canvases, on **desktop and (CPU-throttled) mobile**, plus replay SSIM,
|
|
245
|
+
> Lighthouse page-level deltas, and a glossary explaining every metric. See
|
|
246
|
+
> **[PERFORMANCE.md](./PERFORMANCE.md)**. Headline: lossless replay (SSIM 1.0),
|
|
247
|
+
> **~0.5 µs per draw** (~1% of a 60 fps frame for typical scenes), **0 ms** added
|
|
248
|
+
> Total Blocking Time, no memory leak, **~10 KB gzipped**.
|
|
249
|
+
|
|
242
250
|
#### One-time app change (required before running the bench)
|
|
243
251
|
|
|
244
252
|
The bench needs a way to flip capture on/off without rebuilding your app between variants. Wire `?sf_perf=off` in your `initRecorder` call so the URL controls the option — paste this where you call `initRecorder`:
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 — framework semantic adapters (contract §9).
|
|
3
|
+
*
|
|
4
|
+
* Canvas pixels know WHERE a click landed (the pick buffer resolves that to a
|
|
5
|
+
* draw command / source line). Frameworks additionally know WHAT logical
|
|
6
|
+
* object the user clicked: a Konva "Upgrade CTA" shape, a Chart.js datapoint,
|
|
7
|
+
* a Three.js mesh. These adapters capture that semantic target on pointerdown
|
|
8
|
+
* and emit it as a `sf-canvas` op:"hit" index event:
|
|
9
|
+
*
|
|
10
|
+
* {tag:"sf-canvas", op:"hit", canvasId, t, x, y,
|
|
11
|
+
* target:{framework, label, objectType?, path?}}
|
|
12
|
+
*
|
|
13
|
+
* The SDK cannot import the customer's framework (three/konva/fabric/pixi/
|
|
14
|
+
* chart.js), so the customer (or a future build plugin) registers their live
|
|
15
|
+
* instance via the exported register* helpers below — each returns an
|
|
16
|
+
* unregister fn for component teardown. A window-global fallback covers
|
|
17
|
+
* script-tag (UMD) usage where the framework publishes a global.
|
|
18
|
+
*
|
|
19
|
+
* Every adapter callback is try/catch'd: a framework hiccup never breaks
|
|
20
|
+
* recording. SSR-safe: no window/document access at module scope.
|
|
21
|
+
*/
|
|
22
|
+
// [creation-site stamping] object -> build-time creation id. Set by:
|
|
23
|
+
// - Layer A: the build plugin's `__sfnew` helper via globalThis.__sfCreateSite
|
|
24
|
+
// (marks a `new X()` result directly), and
|
|
25
|
+
// - Layer B: the build plugin's `__sfcreate` helper via globalThis.__sfCreateFrom
|
|
26
|
+
// (marks a loader/clone/factory RESULT subtree — see markSubtree).
|
|
27
|
+
// WeakMap -> no retention.
|
|
28
|
+
const createIdOf = new WeakMap();
|
|
29
|
+
/** Creation provenance for the clicked object: nearest stamped createId (self or
|
|
30
|
+
* up the `.parent` chain). Never throws. */
|
|
31
|
+
function creationOf(obj) {
|
|
32
|
+
const out = {};
|
|
33
|
+
try {
|
|
34
|
+
let o = obj;
|
|
35
|
+
for (let i = 0; i < 8 && o && typeof o === "object"; i++) {
|
|
36
|
+
const id = createIdOf.get(o);
|
|
37
|
+
if (id) {
|
|
38
|
+
out.createId = id;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
o = o.parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch { /* best effort */ }
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
// ─── Layer B: produced-object subtree marking (globalThis.__sfCreateFrom) ─────
|
|
48
|
+
/** Depth and total-node caps for markSubtree — bound the walk so a pathological
|
|
49
|
+
* scene graph can never stall the click path or hold work proportional to N. */
|
|
50
|
+
const MARK_MAX_DEPTH = 6;
|
|
51
|
+
const MARK_MAX_NODES = 2000;
|
|
52
|
+
/** Framework child collections for an object: three.js `.children` (+ GLTF result
|
|
53
|
+
* `.scene`/`.scenes`), pixi `.children`, fabric/konva `getObjects?.()`. Returns the
|
|
54
|
+
* direct child objects to descend into. Never throws. */
|
|
55
|
+
function frameworkChildren(node) {
|
|
56
|
+
const out = [];
|
|
57
|
+
const push = (v) => { if (v && typeof v === "object")
|
|
58
|
+
out.push(v); };
|
|
59
|
+
try {
|
|
60
|
+
const n = node;
|
|
61
|
+
// three.js Object3D / pixi Container: `.children` array
|
|
62
|
+
if (Array.isArray(n.children))
|
|
63
|
+
for (const c of n.children)
|
|
64
|
+
push(c);
|
|
65
|
+
// GLTF loader result: `.scene` (a Group) and/or `.scenes` (Group[])
|
|
66
|
+
push(n.scene);
|
|
67
|
+
if (Array.isArray(n.scenes))
|
|
68
|
+
for (const s of n.scenes)
|
|
69
|
+
push(s);
|
|
70
|
+
// fabric.Group / konva.Container: `getObjects()` returns the child array
|
|
71
|
+
if (typeof n.getObjects === "function") {
|
|
72
|
+
const objs = n.getObjects();
|
|
73
|
+
if (Array.isArray(objs))
|
|
74
|
+
for (const o of objs)
|
|
75
|
+
push(o);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* best effort — a hostile getter must never break marking */ }
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/** Mark a produced object and its bounded framework subtree with the creation-site
|
|
82
|
+
* id. Sets createId ONLY where none exists (Layer A / a nearer site wins). Walks
|
|
83
|
+
* breadth-first to MARK_MAX_DEPTH and MARK_MAX_NODES; WeakMap only (no retention);
|
|
84
|
+
* never throws. */
|
|
85
|
+
function markSubtree(root, id) {
|
|
86
|
+
try {
|
|
87
|
+
if (!root || typeof root !== "object")
|
|
88
|
+
return;
|
|
89
|
+
// BFS with a visited set so cyclic graphs (a child re-added to an ancestor)
|
|
90
|
+
// cannot loop, and the node cap bounds total work.
|
|
91
|
+
const seen = new WeakSet();
|
|
92
|
+
let frontier = [root];
|
|
93
|
+
seen.add(root);
|
|
94
|
+
let nodes = 0;
|
|
95
|
+
for (let depth = 0; depth <= MARK_MAX_DEPTH && frontier.length; depth++) {
|
|
96
|
+
const next = [];
|
|
97
|
+
for (const node of frontier) {
|
|
98
|
+
if (nodes >= MARK_MAX_NODES)
|
|
99
|
+
return;
|
|
100
|
+
nodes++;
|
|
101
|
+
try {
|
|
102
|
+
if (!createIdOf.has(node))
|
|
103
|
+
createIdOf.set(node, id);
|
|
104
|
+
}
|
|
105
|
+
catch { /* best effort */ }
|
|
106
|
+
if (depth < MARK_MAX_DEPTH) {
|
|
107
|
+
for (const child of frameworkChildren(node)) {
|
|
108
|
+
if (!seen.has(child)) {
|
|
109
|
+
seen.add(child);
|
|
110
|
+
next.push(child);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
frontier = next;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch { /* never throw out of the marker */ }
|
|
119
|
+
}
|
|
120
|
+
/** True for a thenable (Promise / promise-like). Guarded `.then` read. */
|
|
121
|
+
function isThenable(v) {
|
|
122
|
+
try {
|
|
123
|
+
return !!v && (typeof v === "object" || typeof v === "function") &&
|
|
124
|
+
typeof v.then === "function";
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Runtime hook for Layer B build-time `__sfcreate(() => EXPR.method(args), id)`.
|
|
131
|
+
* Marks the producer's RESULT subtree with the call-site id so a later click on a
|
|
132
|
+
* loaded/cloned object resolves to the producer's source line. Async producers
|
|
133
|
+
* (loadAsync / promise loaders) resolve later — attach `.then` and mark the
|
|
134
|
+
* resolved value; rejection behavior is untouched. Fully guarded, SSR-safe. */
|
|
135
|
+
function createFrom(result, siteId) {
|
|
136
|
+
try {
|
|
137
|
+
if (!siteId)
|
|
138
|
+
return;
|
|
139
|
+
if (isThenable(result)) {
|
|
140
|
+
// Mark on fulfillment; swallow rejection so we never alter the promise's
|
|
141
|
+
// own rejection handling (the returned promise is the original — see helper).
|
|
142
|
+
result.then((v) => { markSubtree(v, siteId); }, () => { });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
markSubtree(result, siteId);
|
|
146
|
+
}
|
|
147
|
+
catch { /* best effort — never throw into the producer's call path */ }
|
|
148
|
+
}
|
|
149
|
+
/** canvas element -> its framework resolver (explicit registrations) */
|
|
150
|
+
const resolvers = new Map();
|
|
151
|
+
function isCanvas(v) {
|
|
152
|
+
return (typeof HTMLCanvasElement !== "undefined" &&
|
|
153
|
+
v instanceof HTMLCanvasElement);
|
|
154
|
+
}
|
|
155
|
+
function bind(canvas, resolver) {
|
|
156
|
+
if (!isCanvas(canvas))
|
|
157
|
+
return () => { };
|
|
158
|
+
resolvers.set(canvas, resolver);
|
|
159
|
+
return () => {
|
|
160
|
+
if (resolvers.get(canvas) === resolver)
|
|
161
|
+
resolvers.delete(canvas);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ─── Register helpers (public API; re-exported from index.tsx) ────────────────
|
|
165
|
+
/** Konva: hit-test the stage's hit graph; label from node name()/id()/class. */
|
|
166
|
+
export function registerKonvaStage(stage) {
|
|
167
|
+
try {
|
|
168
|
+
const s = stage;
|
|
169
|
+
// Konva layer canvases are all stage-sized and stacked at (0,0), so binding
|
|
170
|
+
// any one of them covers the whole stage, and getIntersection hit-tests across
|
|
171
|
+
// every layer regardless of which canvas element received the pointer. Bind
|
|
172
|
+
// the first layer's canvas (binding all would just emit N redundant hits).
|
|
173
|
+
const layers = s.getLayers?.() ?? [];
|
|
174
|
+
const canvas = layers[0]?.getCanvas?.()?._canvas;
|
|
175
|
+
return bind(canvas, (cv, ev) => {
|
|
176
|
+
const rect = cv.getBoundingClientRect();
|
|
177
|
+
const node = s.getIntersection?.({ x: ev.clientX - rect.left, y: ev.clientY - rect.top });
|
|
178
|
+
if (!node)
|
|
179
|
+
return null;
|
|
180
|
+
const cls = node.getClassName?.();
|
|
181
|
+
const label = node.name?.() || node.id?.() || cls || "shape";
|
|
182
|
+
return { framework: "konva", label, objectType: cls, ...creationOf(node) };
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return () => { };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Fabric: findTarget(ev) on the canvas; label from object name/id/type. */
|
|
190
|
+
export function registerFabricCanvas(fabricCanvas) {
|
|
191
|
+
try {
|
|
192
|
+
const fc = fabricCanvas;
|
|
193
|
+
return bind(fc.lowerCanvasEl, (_cv, ev) => {
|
|
194
|
+
const t = fc.findTarget?.(ev);
|
|
195
|
+
if (!t)
|
|
196
|
+
return null;
|
|
197
|
+
const label = t.name || t.id || t.type || "object";
|
|
198
|
+
return { framework: "fabric", label, objectType: t.type, ...creationOf(t) };
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return () => { };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Chart.js: getElementsAtEventForMode -> "datasetLabel[index]". */
|
|
206
|
+
export function registerChart(chart) {
|
|
207
|
+
try {
|
|
208
|
+
const c = chart;
|
|
209
|
+
return bind(c.canvas, (_cv, ev) => {
|
|
210
|
+
const els = c.getElementsAtEventForMode?.(ev, "nearest", { intersect: true }, false);
|
|
211
|
+
if (!els || !els.length)
|
|
212
|
+
return null;
|
|
213
|
+
const { datasetIndex, index } = els[0];
|
|
214
|
+
const dsLabel = c.data?.datasets?.[datasetIndex]?.label ?? `dataset${datasetIndex}`;
|
|
215
|
+
// Chart.js has no per-datum object; Layer A stamps the `new Chart(...)` site
|
|
216
|
+
// (createId of the chart instance), and configRef pins which datum. [Layer D]
|
|
217
|
+
return {
|
|
218
|
+
framework: "chartjs", label: `${dsLabel}[${index}]`, objectType: c.config?.type,
|
|
219
|
+
...creationOf(c), configRef: { datasetIndex, index },
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return () => { };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Three.js: raycast on pointerdown. The customer passes their THREE namespace
|
|
229
|
+
* (the SDK must not bundle three) plus the scene/camera/renderer.
|
|
230
|
+
*/
|
|
231
|
+
export function registerThreeScene(scene, camera, renderer, three) {
|
|
232
|
+
try {
|
|
233
|
+
const THREE = three;
|
|
234
|
+
const r = renderer;
|
|
235
|
+
const sc = scene;
|
|
236
|
+
const raycaster = new THREE.Raycaster();
|
|
237
|
+
const v2 = new THREE.Vector2();
|
|
238
|
+
return bind(r.domElement, (cv, ev) => {
|
|
239
|
+
const rect = cv.getBoundingClientRect();
|
|
240
|
+
v2.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
|
|
241
|
+
v2.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
|
|
242
|
+
raycaster.setFromCamera(v2, camera);
|
|
243
|
+
const hits = raycaster.intersectObjects(sc.children ?? [], true);
|
|
244
|
+
if (!hits.length)
|
|
245
|
+
return null;
|
|
246
|
+
const obj = hits[0].object;
|
|
247
|
+
let named = obj;
|
|
248
|
+
while (named && !named.name && named.parent)
|
|
249
|
+
named = named.parent;
|
|
250
|
+
const label = named?.name || obj.name || obj.uuid || "mesh";
|
|
251
|
+
// Read creation provenance from the LEAF `obj`. Layer B's markSubtree stamps
|
|
252
|
+
// every node of a loaded/cloned subtree, and creationOf also walks `.parent`
|
|
253
|
+
// for a stamped ancestor — so loader-produced meshes resolve to their `load`
|
|
254
|
+
// call site and `new`-stamped ancestors are still found. [audit]
|
|
255
|
+
return { framework: "three", label, objectType: obj.type, path: obj.uuid, ...creationOf(obj) };
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return () => { };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** PixiJS (v8): hit-test the event boundary; label from object label/name. */
|
|
263
|
+
export function registerPixiApp(app) {
|
|
264
|
+
try {
|
|
265
|
+
const a = app;
|
|
266
|
+
const canvas = a.canvas ?? a.view;
|
|
267
|
+
return bind(canvas, (cv, ev) => {
|
|
268
|
+
const rect = cv.getBoundingClientRect();
|
|
269
|
+
const x = ev.clientX - rect.left;
|
|
270
|
+
const y = ev.clientY - rect.top;
|
|
271
|
+
const obj = a.renderer?.events?.rootBoundary?.hitTest?.(x, y);
|
|
272
|
+
if (!obj)
|
|
273
|
+
return null;
|
|
274
|
+
const objectType = pixiObjectType(obj);
|
|
275
|
+
const label = obj.label || obj.name || objectType || "displayObject";
|
|
276
|
+
return { framework: "pixi", label, objectType, ...creationOf(obj) };
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return () => { };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** Best-effort stable Pixi (v8) type name. Production builds minify
|
|
284
|
+
* `constructor.name` (→ "t", "e"), so duck-type the common display objects by
|
|
285
|
+
* their distinguishing fields first; fall back to the ctor name only when it
|
|
286
|
+
* survived minification (length > 1). [audit MEDIUM] */
|
|
287
|
+
function pixiObjectType(obj) {
|
|
288
|
+
// Order matters: `texture` is NOT unique to Sprite in Pixi v8 — a Mesh exposes
|
|
289
|
+
// it as a data getter and a Graphics as a METHOD — so test the distinctive
|
|
290
|
+
// fields first, and treat `texture` as a Sprite signal only when it's a real
|
|
291
|
+
// object (not a function). [audit MEDIUM]
|
|
292
|
+
if (typeof obj.text === "string")
|
|
293
|
+
return "Text";
|
|
294
|
+
const hasTextureObj = !!obj.texture && typeof obj.texture !== "function";
|
|
295
|
+
if (hasTextureObj && (obj.tilePosition || obj.tileScale))
|
|
296
|
+
return "TilingSprite";
|
|
297
|
+
if (obj.geometry && obj.shader)
|
|
298
|
+
return "Mesh";
|
|
299
|
+
if (obj.context)
|
|
300
|
+
return "Graphics";
|
|
301
|
+
if (hasTextureObj)
|
|
302
|
+
return "Sprite";
|
|
303
|
+
if (Array.isArray(obj.children))
|
|
304
|
+
return "Container";
|
|
305
|
+
const ctor = obj.constructor?.name;
|
|
306
|
+
return ctor && ctor.length > 1 ? ctor : undefined;
|
|
307
|
+
}
|
|
308
|
+
let deps = null;
|
|
309
|
+
let listening = false;
|
|
310
|
+
function onPointerDown(ev) {
|
|
311
|
+
const d = deps;
|
|
312
|
+
if (!d)
|
|
313
|
+
return;
|
|
314
|
+
for (const [canvas, resolver] of resolvers) {
|
|
315
|
+
try {
|
|
316
|
+
// Prune detached canvases (SPA route changes that re-register without an
|
|
317
|
+
// intervening uninstall leave stale entries holding detached elements). [audit LOW]
|
|
318
|
+
if (!canvas.isConnected) {
|
|
319
|
+
resolvers.delete(canvas);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const rect = canvas.getBoundingClientRect();
|
|
323
|
+
if (ev.clientX < rect.left || ev.clientX > rect.right ||
|
|
324
|
+
ev.clientY < rect.top || ev.clientY > rect.bottom)
|
|
325
|
+
continue;
|
|
326
|
+
const canvasId = d.canvasIdFor(canvas);
|
|
327
|
+
if (!canvasId)
|
|
328
|
+
continue;
|
|
329
|
+
const target = resolver(canvas, ev);
|
|
330
|
+
if (!target)
|
|
331
|
+
continue;
|
|
332
|
+
const w = canvas.width, h = canvas.height;
|
|
333
|
+
const x = rect.width ? Math.round((ev.clientX - rect.left) * w / rect.width) : 0;
|
|
334
|
+
const y = rect.height ? Math.round((ev.clientY - rect.top) * h / rect.height) : 0;
|
|
335
|
+
d.emitHit({ op: "hit", canvasId, t: d.now(), x, y, target });
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
/* a framework hiccup must never break recording */
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export function installAdapters(d) {
|
|
343
|
+
deps = d;
|
|
344
|
+
// [creation-site stamping — Layer A] The build plugin rewrites `new X(...)` to
|
|
345
|
+
// `__sfnew(new X(...), id)`, whose helper invokes globalThis.__sfCreateSite(obj,
|
|
346
|
+
// id). Mark the object with its construction-site id so a later click resolves
|
|
347
|
+
// to the exact `new X()` line. Always safe to define (SSR: globalThis exists).
|
|
348
|
+
const g = globalThis;
|
|
349
|
+
g.__sfCreateSite = (o, id) => {
|
|
350
|
+
try {
|
|
351
|
+
if (o && typeof o === "object")
|
|
352
|
+
createIdOf.set(o, id);
|
|
353
|
+
}
|
|
354
|
+
catch { /* best effort */ }
|
|
355
|
+
};
|
|
356
|
+
// [creation-site stamping — Layer B] The build plugin rewrites object-PRODUCING
|
|
357
|
+
// calls `EXPR.method(args)` (load/loadAsync/parse/parseAsync/clone/copy) to
|
|
358
|
+
// `__sfcreate(() => EXPR.method(args), id)`, whose helper invokes
|
|
359
|
+
// globalThis.__sfCreateFrom(result, id). Mark the produced object subtree with
|
|
360
|
+
// the producer's call-site id so a later click on a loaded/cloned object
|
|
361
|
+
// resolves to that line. Async results (loadAsync/promise loaders) mark on
|
|
362
|
+
// resolution. Always safe to define (SSR: globalThis exists).
|
|
363
|
+
g.__sfCreateFrom = (result, id) => { createFrom(result, id); };
|
|
364
|
+
if (listening || typeof window === "undefined")
|
|
365
|
+
return;
|
|
366
|
+
window.addEventListener("pointerdown", onPointerDown, { capture: true, passive: true });
|
|
367
|
+
listening = true;
|
|
368
|
+
}
|
|
369
|
+
export function uninstallAdapters() {
|
|
370
|
+
deps = null;
|
|
371
|
+
if (listening && typeof window !== "undefined") {
|
|
372
|
+
window.removeEventListener("pointerdown", onPointerDown, { capture: true });
|
|
373
|
+
}
|
|
374
|
+
listening = false;
|
|
375
|
+
resolvers.clear();
|
|
376
|
+
try {
|
|
377
|
+
const g = globalThis;
|
|
378
|
+
delete g.__sfCreateSite;
|
|
379
|
+
delete g.__sfCreateFrom;
|
|
380
|
+
}
|
|
381
|
+
catch { /* ignore */ }
|
|
382
|
+
}
|
|
Binary file
|
|
Binary file
|