@triscope/core 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/dist/compose.d.ts +11 -0
  4. package/dist/compose.d.ts.map +1 -0
  5. package/dist/compose.js +152 -0
  6. package/dist/compose.js.map +1 -0
  7. package/dist/editor.d.ts +14 -0
  8. package/dist/editor.d.ts.map +1 -0
  9. package/dist/editor.js +131 -0
  10. package/dist/editor.js.map +1 -0
  11. package/dist/harness.d.ts +199 -0
  12. package/dist/harness.d.ts.map +1 -0
  13. package/dist/harness.js +1027 -0
  14. package/dist/harness.js.map +1 -0
  15. package/dist/index.d.ts +32 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +20 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/inspect.d.ts +94 -0
  20. package/dist/inspect.d.ts.map +1 -0
  21. package/dist/inspect.js +434 -0
  22. package/dist/inspect.js.map +1 -0
  23. package/dist/knob-utils.d.ts +22 -0
  24. package/dist/knob-utils.d.ts.map +1 -0
  25. package/dist/knob-utils.js +51 -0
  26. package/dist/knob-utils.js.map +1 -0
  27. package/dist/lab/css.d.ts +11 -0
  28. package/dist/lab/css.d.ts.map +1 -0
  29. package/dist/lab/css.js +57 -0
  30. package/dist/lab/css.js.map +1 -0
  31. package/dist/lab/dom.d.ts +13 -0
  32. package/dist/lab/dom.d.ts.map +1 -0
  33. package/dist/lab/dom.js +76 -0
  34. package/dist/lab/dom.js.map +1 -0
  35. package/dist/motion-probe.d.ts +47 -0
  36. package/dist/motion-probe.d.ts.map +1 -0
  37. package/dist/motion-probe.js +122 -0
  38. package/dist/motion-probe.js.map +1 -0
  39. package/dist/probe-utils.d.ts +14 -0
  40. package/dist/probe-utils.d.ts.map +1 -0
  41. package/dist/probe-utils.js +18 -0
  42. package/dist/probe-utils.js.map +1 -0
  43. package/dist/scene-cameras.d.ts +6 -0
  44. package/dist/scene-cameras.d.ts.map +1 -0
  45. package/dist/scene-cameras.js +20 -0
  46. package/dist/scene-cameras.js.map +1 -0
  47. package/dist/scene-delta.d.ts +21 -0
  48. package/dist/scene-delta.d.ts.map +1 -0
  49. package/dist/scene-delta.js +57 -0
  50. package/dist/scene-delta.js.map +1 -0
  51. package/dist/scene-introspect.d.ts +78 -0
  52. package/dist/scene-introspect.d.ts.map +1 -0
  53. package/dist/scene-introspect.js +164 -0
  54. package/dist/scene-introspect.js.map +1 -0
  55. package/dist/scene-registry.d.ts +36 -0
  56. package/dist/scene-registry.d.ts.map +1 -0
  57. package/dist/scene-registry.js +64 -0
  58. package/dist/scene-registry.js.map +1 -0
  59. package/dist/scene-view.d.ts +52 -0
  60. package/dist/scene-view.d.ts.map +1 -0
  61. package/dist/scene-view.js +171 -0
  62. package/dist/scene-view.js.map +1 -0
  63. package/dist/source-tag.d.ts +34 -0
  64. package/dist/source-tag.d.ts.map +1 -0
  65. package/dist/source-tag.js +120 -0
  66. package/dist/source-tag.js.map +1 -0
  67. package/dist/telemetry.d.ts +53 -0
  68. package/dist/telemetry.d.ts.map +1 -0
  69. package/dist/telemetry.js +302 -0
  70. package/dist/telemetry.js.map +1 -0
  71. package/dist/types.d.ts +142 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/types.js +9 -0
  74. package/dist/types.js.map +1 -0
  75. package/dist/uniform-access.d.ts +32 -0
  76. package/dist/uniform-access.d.ts.map +1 -0
  77. package/dist/uniform-access.js +144 -0
  78. package/dist/uniform-access.js.map +1 -0
  79. package/dist/validate.d.ts +2 -0
  80. package/dist/validate.d.ts.map +1 -0
  81. package/dist/validate.js +81 -0
  82. package/dist/validate.js.map +1 -0
  83. package/dist/vite.d.ts +3 -0
  84. package/dist/vite.d.ts.map +1 -0
  85. package/dist/vite.js +4 -0
  86. package/dist/vite.js.map +1 -0
  87. package/dist/warnings.d.ts +24 -0
  88. package/dist/warnings.d.ts.map +1 -0
  89. package/dist/warnings.js +26 -0
  90. package/dist/warnings.js.map +1 -0
  91. package/package.json +60 -0
  92. package/src/compose.ts +164 -0
  93. package/src/editor.ts +138 -0
  94. package/src/harness.ts +1263 -0
  95. package/src/index.ts +58 -0
  96. package/src/inspect.ts +498 -0
  97. package/src/knob-utils.ts +60 -0
  98. package/src/lab/css.ts +56 -0
  99. package/src/lab/dom.ts +88 -0
  100. package/src/motion-probe.ts +135 -0
  101. package/src/probe-utils.ts +17 -0
  102. package/src/scene-cameras.ts +33 -0
  103. package/src/scene-delta.ts +69 -0
  104. package/src/scene-introspect.ts +230 -0
  105. package/src/scene-registry.ts +103 -0
  106. package/src/scene-view.ts +204 -0
  107. package/src/source-tag.ts +139 -0
  108. package/src/telemetry.ts +337 -0
  109. package/src/three-webgpu-shim.d.ts +130 -0
  110. package/src/types.ts +121 -0
  111. package/src/uniform-access.ts +152 -0
  112. package/src/validate.ts +82 -0
  113. package/src/vite.ts +5 -0
  114. package/src/warnings.ts +41 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Auto source-tag for the scene graph.
3
+ *
4
+ * Monkey-patches THREE.Object3D.prototype.add exactly once so every object
5
+ * added to a scene gets a userData.__tris record containing the user
6
+ * source frame (file:line + fn name from V8 stack), the object class, the
7
+ * geometry class, and a material hint.
8
+ *
9
+ * Element authors do not call anything. The tag appears on every mesh
10
+ * added via .add(), Three's standard pattern. The picker in inspect.ts
11
+ * reads this back when the user clicks, so we map "this pixel on screen"
12
+ * to "this exact line in your code" without grep.
13
+ *
14
+ * Stack parsing skips any frame inside three/, @triscope/, or
15
+ * node_modules/. In vite dev mode the stack is already source-mapped,
16
+ * resolving to original .ts source files. Production minified builds
17
+ * lose the precision but triscope is dev-only.
18
+ */
19
+ import * as THREE from 'three/webgpu';
20
+ /**
21
+ * Note on line accuracy: in browser dev mode, `new Error().stack` returns
22
+ * positions in the file as vite served it (after esbuild's TS-to-JS
23
+ * transform). Vite tries to preserve line counts but TSL `Fn(([uv]) => …)`
24
+ * blocks and other complex constructions can drift by tens of lines.
25
+ * The captured line is therefore "approximate" — close enough for
26
+ * `code --goto` to land in the right neighborhood, but verify visually
27
+ * (or use `parentChain` + `geometry` + `material.color` as cross-checks)
28
+ * before assuming it's exact.
29
+ */
30
+ let patched = false;
31
+ export function installSourceTagPatch() {
32
+ if (patched)
33
+ return false;
34
+ patched = true;
35
+ const origAdd = THREE.Object3D.prototype.add;
36
+ THREE.Object3D.prototype.add = function (...children) {
37
+ let stack = [];
38
+ try {
39
+ const raw = new Error().stack ?? '';
40
+ stack = parseUserStack(raw);
41
+ }
42
+ catch {
43
+ /* stack capture is best-effort */
44
+ }
45
+ const source = stack[0] ?? null;
46
+ for (const child of children) {
47
+ if (!child)
48
+ continue;
49
+ const prior = child.userData?.__tris;
50
+ if (prior && prior.source)
51
+ continue;
52
+ const tag = {
53
+ source,
54
+ stack,
55
+ type: child.constructor.name,
56
+ };
57
+ const asMesh = child;
58
+ if (asMesh.geometry)
59
+ tag.geometry = asMesh.geometry.type;
60
+ if (asMesh.material)
61
+ tag.material = extractMaterialHint(asMesh.material);
62
+ if (child.name)
63
+ tag.name = child.name;
64
+ child.userData = child.userData ?? {};
65
+ child.userData.__tris = tag;
66
+ }
67
+ return origAdd.apply(this, children);
68
+ };
69
+ return true;
70
+ }
71
+ export const FRAME_RE = /at (?:(?<fn>[^(]+?) \()?(?<url>[^()]+?):(?<line>\d+):(?<col>\d+)\)?$/;
72
+ export const SKIP_PATTERNS = [
73
+ /\/node_modules\//,
74
+ /\/three\//,
75
+ /\/@triscope\//,
76
+ /\/triscope\/packages\//,
77
+ /\/(harness|source-tag|inspect|editor|telemetry)\.[jt]sx?/,
78
+ /^node:/,
79
+ /^(?:webpack|vite):/,
80
+ ];
81
+ export function parseUserStack(raw, max = 8) {
82
+ const out = [];
83
+ for (const lineStr of raw.split('\n')) {
84
+ const m = FRAME_RE.exec(lineStr.trim());
85
+ if (!m?.groups)
86
+ continue;
87
+ const url = stripUrl(m.groups.url);
88
+ if (SKIP_PATTERNS.some((p) => p.test(url)))
89
+ continue;
90
+ out.push({
91
+ file: url,
92
+ line: Number(m.groups.line),
93
+ col: Number(m.groups.col),
94
+ fn: m.groups.fn || undefined,
95
+ });
96
+ if (out.length >= max)
97
+ break;
98
+ }
99
+ return out;
100
+ }
101
+ export function stripUrl(u) {
102
+ return u.replace(/[?#].*$/, '');
103
+ }
104
+ export function extractMaterialHint(material) {
105
+ const m = material;
106
+ const hint = {};
107
+ try {
108
+ if (m?.color?.getHexString)
109
+ hint.color = '#' + m.color.getHexString();
110
+ }
111
+ catch { }
112
+ try {
113
+ const tex = m?.map;
114
+ if (tex)
115
+ hint.map = tex.name || tex.source?.data?.src || null;
116
+ }
117
+ catch { }
118
+ return hint;
119
+ }
120
+ //# sourceMappingURL=source-tag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-tag.js","sourceRoot":"","sources":["../src/source-tag.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import type { Plugin } from 'vite';
2
+ interface TelemetryOptions {
3
+ /**
4
+ * Project name used to namespace state files in /tmp.
5
+ * Defaults to the name in package.json (sanitized).
6
+ */
7
+ project?: string;
8
+ /** Override state file path. */
9
+ statePath?: string;
10
+ /** Override log file path. */
11
+ logPath?: string;
12
+ /**
13
+ * Regex matching files that need a full-reload (instead of HMR) when they
14
+ * change. Default catches TSL material / mesh / element / shader sources,
15
+ * because vite HMR cannot remount a THREE.Material already in the scene.
16
+ * Pass `null` to disable.
17
+ */
18
+ forceReloadOn?: RegExp | null;
19
+ }
20
+ /**
21
+ * Read a request body to a string with a hard timeout. A broken/slow client
22
+ * could otherwise hold a Vite middleware slot open indefinitely (the original
23
+ * readBody had data/end/error listeners but no timeout). On timeout we destroy
24
+ * the socket so the slot is freed, and reject so the route returns 400.
25
+ * Override the budget with TRISCOPE_READBODY_TIMEOUT_MS.
26
+ */
27
+ export declare function readRequestBody(req: {
28
+ on: (event: string, cb: (arg: never) => void) => void;
29
+ socket?: {
30
+ destroy?: () => void;
31
+ };
32
+ }, timeoutMs?: number): Promise<string>;
33
+ /**
34
+ * Vite plugin that wires the telemetry sink:
35
+ *
36
+ * POST /__state → writes /tmp/<project>-state.json + appends to /tmp/<project>-state.log
37
+ * GET /__state → returns the latest snapshot
38
+ * POST /__knob → stores a pending knob change for the harness to consume
39
+ * GET /__knob → returns and CLEARS the pending knob queue (polled by harness)
40
+ * GET /__manifest → returns the registered element manifest (POSTed by harness on boot)
41
+ * POST /__manifest → harness pushes the live manifest (elements/cameras/knobs)
42
+ *
43
+ * The names start with __ so they cannot collide with user routes.
44
+ */
45
+ export declare function triscopeTelemetryPlugin(opts?: TelemetryOptions): Plugin;
46
+ export interface TelemetryPaths {
47
+ project: string;
48
+ statePath: string;
49
+ logPath: string;
50
+ }
51
+ export declare function resolveTelemetryPaths(cwd?: string): TelemetryPaths;
52
+ export {};
53
+ //# sourceMappingURL=telemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,UAAU,gBAAgB;IACxB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE;IACH,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,KAAK,IAAI,CAAC;IACtD,MAAM,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CACnC,EACD,SAAS,GAAE,MAAkE,GAC5E,OAAO,CAAC,MAAM,CAAC,CAgCjB;AAgCD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,gBAAqB,GAAG,MAAM,CAyM3E;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAsB,GAAG,cAAc,CAOjF"}
@@ -0,0 +1,302 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ /**
5
+ * Read a request body to a string with a hard timeout. A broken/slow client
6
+ * could otherwise hold a Vite middleware slot open indefinitely (the original
7
+ * readBody had data/end/error listeners but no timeout). On timeout we destroy
8
+ * the socket so the slot is freed, and reject so the route returns 400.
9
+ * Override the budget with TRISCOPE_READBODY_TIMEOUT_MS.
10
+ */
11
+ export function readRequestBody(req, timeoutMs = Number(process.env.TRISCOPE_READBODY_TIMEOUT_MS ?? 10000)) {
12
+ return new Promise((resolve, reject) => {
13
+ let body = '';
14
+ let done = false;
15
+ const timer = setTimeout(() => {
16
+ if (done)
17
+ return;
18
+ done = true;
19
+ try {
20
+ req.socket?.destroy?.();
21
+ }
22
+ catch {
23
+ /* best-effort */
24
+ }
25
+ reject(new Error(`request body read timed out after ${timeoutMs}ms`));
26
+ }, timeoutMs);
27
+ // Don't let a pending read keep the process alive.
28
+ timer.unref?.();
29
+ req.on('data', (c) => {
30
+ body += c;
31
+ });
32
+ req.on('end', () => {
33
+ if (done)
34
+ return;
35
+ done = true;
36
+ clearTimeout(timer);
37
+ resolve(body);
38
+ });
39
+ req.on('error', (e) => {
40
+ if (done)
41
+ return;
42
+ done = true;
43
+ clearTimeout(timer);
44
+ reject(e);
45
+ });
46
+ });
47
+ }
48
+ function readProjectLabs(cwd) {
49
+ try {
50
+ const p = join(cwd, 'package.json');
51
+ if (!existsSync(p))
52
+ return {};
53
+ const pkg = JSON.parse(readFileSync(p, 'utf8'));
54
+ const labs = pkg?.triscope?.labs;
55
+ if (labs && typeof labs === 'object') {
56
+ const out = {};
57
+ for (const [k, v] of Object.entries(labs)) {
58
+ if (typeof v === 'string')
59
+ out[k] = v;
60
+ }
61
+ return out;
62
+ }
63
+ return {};
64
+ }
65
+ catch {
66
+ return {};
67
+ }
68
+ }
69
+ function readPackageName(cwd) {
70
+ try {
71
+ const p = join(cwd, 'package.json');
72
+ if (!existsSync(p))
73
+ return 'triscope-project';
74
+ const pkg = JSON.parse(readFileSync(p, 'utf8'));
75
+ return String(pkg.name ?? 'triscope-project').replace(/[^A-Za-z0-9._-]/g, '-');
76
+ }
77
+ catch {
78
+ return 'triscope-project';
79
+ }
80
+ }
81
+ /**
82
+ * Vite plugin that wires the telemetry sink:
83
+ *
84
+ * POST /__state → writes /tmp/<project>-state.json + appends to /tmp/<project>-state.log
85
+ * GET /__state → returns the latest snapshot
86
+ * POST /__knob → stores a pending knob change for the harness to consume
87
+ * GET /__knob → returns and CLEARS the pending knob queue (polled by harness)
88
+ * GET /__manifest → returns the registered element manifest (POSTed by harness on boot)
89
+ * POST /__manifest → harness pushes the live manifest (elements/cameras/knobs)
90
+ *
91
+ * The names start with __ so they cannot collide with user routes.
92
+ */
93
+ export function triscopeTelemetryPlugin(opts = {}) {
94
+ const project = opts.project ?? readPackageName(process.cwd());
95
+ const statePath = opts.statePath ?? join(tmpdir(), `${project}-state.json`);
96
+ const logPath = opts.logPath ?? join(tmpdir(), `${project}-state.log`);
97
+ // In-memory pending knob queue. Harness polls and drains.
98
+ const pendingKnobs = [];
99
+ // Persisted knob state per element. Survives the harness's full-reload so
100
+ // the harness can re-hydrate to the last user-applied values instead of
101
+ // snapping back to spec defaults. Updated on every POST /__knob, read by
102
+ // the harness via GET /__knob/current on boot.
103
+ const lastKnobValues = {};
104
+ // SDL (set_scene_param): pending camera/knob deltas the harness drains via
105
+ // GET /__scene, plus the merged persisted scene state for re-hydration on
106
+ // reload (GET /__scene/current). Mirrors the /__knob pattern.
107
+ const pendingScene = [];
108
+ const lastSceneState = {
109
+ cameras: {},
110
+ knobs: {},
111
+ };
112
+ // Manifest is a map keyed by element name so multiple labs can co-exist —
113
+ // each harness POSTs its own entry on boot.
114
+ const manifestByElement = {};
115
+ // Pre-seed with package.json#triscope.labs so MCP capture_views works on
116
+ // the very first call (before any browser tab loads a lab).
117
+ for (const [name, labUrl] of Object.entries(readProjectLabs(process.cwd()))) {
118
+ manifestByElement[name] = { element: name, labUrl };
119
+ }
120
+ const forceReloadOn = opts.forceReloadOn === null
121
+ ? null
122
+ : (opts.forceReloadOn ?? /(\.tsl|Element|Mesh|Material|Shader)\.(ts|tsx|js|mjs)$/i);
123
+ return {
124
+ name: 'triscope-telemetry',
125
+ configureServer(server) {
126
+ mkdirSync(dirname(statePath), { recursive: true });
127
+ // Clear last session's telemetry on dev-server boot so a previously-open
128
+ // lab's elements don't linger as phantoms (the POST /__state merge is for
129
+ // multiple tabs in the SAME session — it has no cross-session expiry).
130
+ // The harness re-POSTs on mount, so a live tab repopulates immediately.
131
+ try {
132
+ rmSync(statePath, { force: true });
133
+ }
134
+ catch {
135
+ /* best-effort — a stale file just merges as before */
136
+ }
137
+ const readBody = (req) => readRequestBody(req);
138
+ server.middlewares.use('/__state', async (req, res, next) => {
139
+ if (!req.method)
140
+ return next();
141
+ try {
142
+ if (req.method === 'POST') {
143
+ const body = await readBody(req);
144
+ const payload = JSON.parse(body);
145
+ // Merge the elements map across labs so two tabs on different
146
+ // lab pages don't clobber each other's telemetry. Top-level
147
+ // fields (perf/time/cameras) still reflect the last writer
148
+ // since they're per-tab — that's expected when read_telemetry
149
+ // is project-scoped, not lab-scoped.
150
+ let merged = payload;
151
+ try {
152
+ if (existsSync(statePath)) {
153
+ const existing = JSON.parse(readFileSync(statePath, 'utf8'));
154
+ merged = {
155
+ ...payload,
156
+ elements: { ...(existing?.elements ?? {}), ...(payload?.elements ?? {}) },
157
+ };
158
+ }
159
+ }
160
+ catch {
161
+ /* corrupt file — overwrite with the new payload */
162
+ }
163
+ writeFileSync(statePath, JSON.stringify(merged, null, 2));
164
+ const ts = new Date().toISOString();
165
+ const fps = payload?.perf?.fps?.toFixed?.(0) ?? '?';
166
+ const cam = payload?.activeCamera ?? '?';
167
+ appendFileSync(logPath, `${ts} fps=${fps} cam=${cam}\n`);
168
+ res.statusCode = 200;
169
+ return res.end('ok');
170
+ }
171
+ if (req.method === 'GET') {
172
+ res.setHeader('content-type', 'application/json');
173
+ return res.end(existsSync(statePath) ? readFileSync(statePath) : '{}');
174
+ }
175
+ }
176
+ catch (err) {
177
+ res.statusCode = 400;
178
+ return res.end(String(err));
179
+ }
180
+ return next();
181
+ });
182
+ server.middlewares.use('/__knob', async (req, res, next) => {
183
+ if (!req.method)
184
+ return next();
185
+ try {
186
+ // GET /__knob/current → persisted state (sub-path on the same prefix).
187
+ if (req.method === 'GET' && (req.url ?? '').startsWith('/current')) {
188
+ res.setHeader('content-type', 'application/json');
189
+ return res.end(JSON.stringify(lastKnobValues));
190
+ }
191
+ if (req.method === 'POST') {
192
+ const body = await readBody(req);
193
+ const payload = JSON.parse(body);
194
+ const updates = Array.isArray(payload) ? payload : [payload];
195
+ for (const u of updates) {
196
+ if (typeof u?.element === 'string' && typeof u?.key === 'string') {
197
+ lastKnobValues[u.element] ??= {};
198
+ lastKnobValues[u.element][u.key] = u.value;
199
+ }
200
+ }
201
+ pendingKnobs.push(...updates);
202
+ res.statusCode = 200;
203
+ return res.end('ok');
204
+ }
205
+ if (req.method === 'GET') {
206
+ res.setHeader('content-type', 'application/json');
207
+ const drained = pendingKnobs.splice(0, pendingKnobs.length);
208
+ return res.end(JSON.stringify(drained));
209
+ }
210
+ }
211
+ catch (err) {
212
+ res.statusCode = 400;
213
+ return res.end(String(err));
214
+ }
215
+ return next();
216
+ });
217
+ server.middlewares.use('/__scene', async (req, res, next) => {
218
+ if (!req.method)
219
+ return next();
220
+ try {
221
+ // GET /__scene/current → merged persisted scene state (re-hydration).
222
+ if (req.method === 'GET' && (req.url ?? '').startsWith('/current')) {
223
+ res.setHeader('content-type', 'application/json');
224
+ return res.end(JSON.stringify(lastSceneState));
225
+ }
226
+ if (req.method === 'POST') {
227
+ const body = await readBody(req);
228
+ const delta = JSON.parse(body);
229
+ // Merge into the persisted state so a reload re-applies it.
230
+ if (delta?.cameras && typeof delta.cameras === 'object') {
231
+ for (const [n, c] of Object.entries(delta.cameras)) {
232
+ lastSceneState.cameras[n] = { ...lastSceneState.cameras[n], ...c };
233
+ }
234
+ }
235
+ if (delta?.knobs && typeof delta.knobs === 'object') {
236
+ for (const [k, v] of Object.entries(delta.knobs))
237
+ lastSceneState.knobs[k] = v;
238
+ }
239
+ pendingScene.push(delta);
240
+ res.statusCode = 200;
241
+ return res.end('ok');
242
+ }
243
+ if (req.method === 'GET') {
244
+ res.setHeader('content-type', 'application/json');
245
+ const drained = pendingScene.splice(0, pendingScene.length);
246
+ return res.end(JSON.stringify(drained));
247
+ }
248
+ }
249
+ catch (err) {
250
+ res.statusCode = 400;
251
+ return res.end(String(err));
252
+ }
253
+ return next();
254
+ });
255
+ server.middlewares.use('/__manifest', async (req, res, next) => {
256
+ if (!req.method)
257
+ return next();
258
+ try {
259
+ if (req.method === 'POST') {
260
+ const body = await readBody(req);
261
+ const payload = JSON.parse(body);
262
+ if (payload?.element && typeof payload.element === 'string') {
263
+ manifestByElement[payload.element] = payload;
264
+ }
265
+ res.statusCode = 200;
266
+ return res.end('ok');
267
+ }
268
+ if (req.method === 'GET') {
269
+ res.setHeader('content-type', 'application/json');
270
+ return res.end(JSON.stringify({ elements: manifestByElement }));
271
+ }
272
+ }
273
+ catch (err) {
274
+ res.statusCode = 400;
275
+ return res.end(String(err));
276
+ }
277
+ return next();
278
+ });
279
+ },
280
+ handleHotUpdate({ file, server }) {
281
+ // TSL materials (and any code that ends up baked into the renderer
282
+ // node graph) cannot be remounted via vite HMR because the THREE
283
+ // Material instance is already in the scene. Force full-reload so
284
+ // edits to shader/element/mesh files reach the renderer. All other
285
+ // files (plain ts/css/etc.) continue to HMR normally.
286
+ if (forceReloadOn && forceReloadOn.test(file)) {
287
+ server.ws.send({ type: 'full-reload' });
288
+ return [];
289
+ }
290
+ return undefined;
291
+ },
292
+ };
293
+ }
294
+ export function resolveTelemetryPaths(cwd = process.cwd()) {
295
+ const project = readPackageName(cwd);
296
+ return {
297
+ project,
298
+ statePath: join(tmpdir(), `${project}-state.json`),
299
+ logPath: join(tmpdir(), `${project}-state.log`),
300
+ };
301
+ }
302
+ //# sourceMappingURL=telemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,142 @@
1
+ import type * as THREE from 'three/webgpu';
2
+ /** A position+target camera preset for one pane of the lab grid. */
3
+ export interface CameraSpec {
4
+ position: [number, number, number];
5
+ target: [number, number, number];
6
+ /** Vertical FOV in degrees. Default 45. */
7
+ fov?: number;
8
+ /** If true, auto-fit element bounds into the camera frame. */
9
+ fit?: boolean;
10
+ /** Near clip. Default 0.1. */
11
+ near?: number;
12
+ /** Far clip. Default 2000. */
13
+ far?: number;
14
+ }
15
+ /** A tunable knob exposed to the slider editor and the MCP `set_knob` tool. */
16
+ export type Knob = {
17
+ type: 'number';
18
+ min: number;
19
+ max: number;
20
+ step?: number;
21
+ default: number;
22
+ label?: string;
23
+ } | {
24
+ type: 'int';
25
+ min: number;
26
+ max: number;
27
+ default: number;
28
+ label?: string;
29
+ } | {
30
+ type: 'color';
31
+ default: string;
32
+ label?: string;
33
+ } | {
34
+ type: 'boolean';
35
+ default: boolean;
36
+ label?: string;
37
+ }
38
+ /**
39
+ * Action knob — fires `onKnob(handle, key, true)` each time it's set, but
40
+ * does NOT persist a value across reloads and does NOT auto-fire on mount.
41
+ * Use for one-shot triggers (fire cannon, load weapon, request screenshot)
42
+ * where the act of setting is the signal and there is no "current value".
43
+ */
44
+ | {
45
+ type: 'trigger';
46
+ label?: string;
47
+ };
48
+ /** Context passed to `Element.mount`. */
49
+ export interface MountContext {
50
+ renderer: THREE.WebGPURenderer;
51
+ scene: THREE.Scene;
52
+ /** Shared time uniform (seconds). Updated each frame by the harness. */
53
+ time: {
54
+ value: number;
55
+ };
56
+ /** dt in seconds, last frame. */
57
+ dt: {
58
+ value: number;
59
+ };
60
+ }
61
+ /** The handle returned by `Element.mount`. */
62
+ export interface MountHandle {
63
+ /** The root Object3D the element added to the scene. */
64
+ root: THREE.Object3D;
65
+ /** Tear-down hook. Must remove the element from the scene and free GPU resources. */
66
+ dispose: () => void;
67
+ /** Element-specific data the telemetry/onKnob callbacks may need. */
68
+ userData?: Record<string, unknown>;
69
+ }
70
+ /**
71
+ * A self-describing 3D element. The triscope lab harness consumes this
72
+ * to render a multi-camera grid, drive a tunables UI, post telemetry,
73
+ * and run the smoke test. Composition is just an element whose `mount`
74
+ * mounts other elements.
75
+ */
76
+ export interface Element {
77
+ name: string;
78
+ mount: (args: {
79
+ parent: THREE.Object3D;
80
+ ctx: MountContext;
81
+ }) => MountHandle;
82
+ /**
83
+ * Optional override for the lab page URL. Either a path relative to the
84
+ * dev server (`/triscope-ship.html`) or a full URL. When set, the harness
85
+ * publishes it on the manifest so the MCP/CLI can route `capture_views`
86
+ * and `run_smoke` to the right page without hardcoding `/labs/<name>.html`.
87
+ */
88
+ labUrl?: string;
89
+ /** Local-space bounding box. Used for auto-fitting cameras and the scene framing. */
90
+ bounds?: {
91
+ min: [number, number, number];
92
+ max: [number, number, number];
93
+ };
94
+ /**
95
+ * Named camera presets — each becomes one pane in the lab grid. Pass the
96
+ * literal `'auto'` to have the harness synthesize 4 fitted presets
97
+ * (front/side/top/three-quarter) from `bounds` (see scene-cameras.ts), so an
98
+ * element doesn't have to hand-author cameras.
99
+ */
100
+ cameras: Record<string, CameraSpec> | 'auto';
101
+ /** Tunable knobs. Rendered as sliders + exposed to MCP `set_knob`. */
102
+ knobs?: Record<string, Knob>;
103
+ /** Live-update hook. Called when a knob changes; must apply the change without rebuilding pipelines. */
104
+ onKnob?: (handle: MountHandle, key: string, value: number | string | boolean) => void;
105
+ /** Per-frame state to publish via the telemetry sink. Return JSON-serializable values. */
106
+ telemetry?: (handle: MountHandle, ctx: MountContext) => Record<string, unknown>;
107
+ /**
108
+ * Named per-frame numeric probes for animated state. The harness samples each
109
+ * every frame, keeps a ring buffer (~2 s at 60 fps), and exposes summary stats
110
+ * under `telemetry.elements.<name>.motion.<probeKey>`:
111
+ * { latest, mean, min, max, peakToPeak, samples: lastN }
112
+ * Use for amplitude (vertex displacement), oscillation rate, particle counts —
113
+ * anything dynamic the Element wants to quantify.
114
+ */
115
+ motionProbes?: Record<string, (handle: MountHandle, ctx: MountContext) => number>;
116
+ /**
117
+ * Per-frame discrete-event drain. The harness calls this every frame; the
118
+ * element returns events that occurred since the last call (typically by
119
+ * draining an internal queue). The harness appends them to a ring buffer
120
+ * (cap 128) and exposes them via `telemetry.events`. Use for one-shot
121
+ * signals like collisions, weapon fires, state transitions — anything the
122
+ * test script needs to verify a posteriori with `read_telemetry .events`.
123
+ *
124
+ * Implementation MUST drain (return + clear) each call: events returned
125
+ * twice will appear twice in the buffer.
126
+ */
127
+ events?: (handle: MountHandle, ctx: MountContext) => TriscopeEvent[];
128
+ }
129
+ /** Discrete event emitted by an Element. */
130
+ export interface TriscopeEvent {
131
+ /** Timestamp in seconds. Should reuse `ctx.time.value` for sim-consistent ordering. */
132
+ timestamp: number;
133
+ /** Discriminator — caller-defined (e.g. 'fire' | 'splash' | 'impact'). */
134
+ type: string;
135
+ /** Optional opaque payload — anything JSON-serializable. */
136
+ payload?: Record<string, unknown>;
137
+ }
138
+ /** Default value extracted from a knob spec. Trigger knobs have no
139
+ * default — they are pure action signals; this returns `false` only as a
140
+ * placeholder so callers iterating knob values get a defined entry. */
141
+ export declare function knobDefault(k: Knob): number | string | boolean;
142
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Default value extracted from a knob spec. Trigger knobs have no
2
+ * default — they are pure action signals; this returns `false` only as a
3
+ * placeholder so callers iterating knob values get a defined entry. */
4
+ export function knobDefault(k) {
5
+ if (k.type === 'trigger')
6
+ return false;
7
+ return k.default;
8
+ }
9
+ //# sourceMappingURL=types.js.map