@heojeongbo/fluxion-render 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/chunk-3PK3KDO5.js +36 -0
- package/dist/chunk-3PK3KDO5.js.map +1 -0
- package/dist/chunk-56NZ4OEO.js +247 -0
- package/dist/chunk-56NZ4OEO.js.map +1 -0
- package/dist/chunk-R7FLS7BG.js +15 -0
- package/dist/chunk-R7FLS7BG.js.map +1 -0
- package/dist/fluxion-host-C_n0cGQO.d.ts +319 -0
- package/dist/fluxion-worker.d.ts +2 -0
- package/dist/fluxion-worker.js +816 -0
- package/dist/fluxion-worker.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +180 -0
- package/dist/react.js +195 -0
- package/dist/react.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/shared/lib/time-format.ts
|
|
2
|
+
var TOKEN_RE = /HH|SSS|mm|ss|H|m|s|S/g;
|
|
3
|
+
function makeClockFormatter(pattern) {
|
|
4
|
+
return (epochMs) => formatClock(epochMs, pattern);
|
|
5
|
+
}
|
|
6
|
+
function formatClock(epochMs, pattern) {
|
|
7
|
+
const d = new Date(epochMs);
|
|
8
|
+
return pattern.replace(TOKEN_RE, (tok) => {
|
|
9
|
+
switch (tok) {
|
|
10
|
+
case "HH":
|
|
11
|
+
return String(d.getHours()).padStart(2, "0");
|
|
12
|
+
case "H":
|
|
13
|
+
return String(d.getHours());
|
|
14
|
+
case "mm":
|
|
15
|
+
return String(d.getMinutes()).padStart(2, "0");
|
|
16
|
+
case "m":
|
|
17
|
+
return String(d.getMinutes());
|
|
18
|
+
case "ss":
|
|
19
|
+
return String(d.getSeconds()).padStart(2, "0");
|
|
20
|
+
case "s":
|
|
21
|
+
return String(d.getSeconds());
|
|
22
|
+
case "SSS":
|
|
23
|
+
return String(d.getMilliseconds()).padStart(3, "0");
|
|
24
|
+
case "S":
|
|
25
|
+
return String(Math.floor(d.getMilliseconds() / 100));
|
|
26
|
+
default:
|
|
27
|
+
return tok;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
makeClockFormatter,
|
|
34
|
+
formatClock
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=chunk-3PK3KDO5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/lib/time-format.ts"],"sourcesContent":["/**\n * Tiny clock pattern formatter. Supports the following tokens (leftmost\n * longest wins):\n *\n * HH - hours, zero-padded (00..23)\n * H - hours (0..23)\n * mm - minutes, zero-padded (00..59)\n * m - minutes (0..59)\n * ss - seconds, zero-padded (00..59)\n * s - seconds (0..59)\n * SSS - milliseconds, zero-padded (000..999)\n * S - tenths of a second (0..9)\n *\n * Anything else is treated as a literal (e.g. \":\", \".\", \" \", \"T\"). The parser\n * is intentionally small: no locale support, no escape syntax. Feed it a\n * wall-clock epoch in ms.\n *\n * Example:\n * formatClock(Date.now(), \"HH:mm:ss\") -> \"14:07:32\"\n * formatClock(Date.now(), \"HH:mm:ss.SSS\") -> \"14:07:32.481\"\n * formatClock(Date.now(), \"H:m:s\") -> \"14:7:32\"\n */\nconst TOKEN_RE = /HH|SSS|mm|ss|H|m|s|S/g;\n\nexport type TickFormatter = (epochMs: number) => string;\n\n/** Build a memoized formatter for a pattern. */\nexport function makeClockFormatter(pattern: string): TickFormatter {\n return (epochMs: number) => formatClock(epochMs, pattern);\n}\n\nexport function formatClock(epochMs: number, pattern: string): string {\n const d = new Date(epochMs);\n return pattern.replace(TOKEN_RE, (tok) => {\n switch (tok) {\n case \"HH\":\n return String(d.getHours()).padStart(2, \"0\");\n case \"H\":\n return String(d.getHours());\n case \"mm\":\n return String(d.getMinutes()).padStart(2, \"0\");\n case \"m\":\n return String(d.getMinutes());\n case \"ss\":\n return String(d.getSeconds()).padStart(2, \"0\");\n case \"s\":\n return String(d.getSeconds());\n case \"SSS\":\n return String(d.getMilliseconds()).padStart(3, \"0\");\n case \"S\":\n return String(Math.floor(d.getMilliseconds() / 100));\n default:\n return tok;\n }\n });\n}\n"],"mappings":";AAsBA,IAAM,WAAW;AAKV,SAAS,mBAAmB,SAAgC;AACjE,SAAO,CAAC,YAAoB,YAAY,SAAS,OAAO;AAC1D;AAEO,SAAS,YAAY,SAAiB,SAAyB;AACpE,QAAM,IAAI,IAAI,KAAK,OAAO;AAC1B,SAAO,QAAQ,QAAQ,UAAU,CAAC,QAAQ;AACxC,YAAQ,KAAK;AAAA,MACX,KAAK;AACH,eAAO,OAAO,EAAE,SAAS,CAAC,EAAE,SAAS,GAAG,GAAG;AAAA,MAC7C,KAAK;AACH,eAAO,OAAO,EAAE,SAAS,CAAC;AAAA,MAC5B,KAAK;AACH,eAAO,OAAO,EAAE,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AAAA,MAC/C,KAAK;AACH,eAAO,OAAO,EAAE,WAAW,CAAC;AAAA,MAC9B,KAAK;AACH,eAAO,OAAO,EAAE,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AAAA,MAC/C,KAAK;AACH,eAAO,OAAO,EAAE,WAAW,CAAC;AAAA,MAC9B,KAAK;AACH,eAAO,OAAO,EAAE,gBAAgB,CAAC,EAAE,SAAS,GAAG,GAAG;AAAA,MACpD,KAAK;AACH,eAAO,OAAO,KAAK,MAAM,EAAE,gBAAgB,IAAI,GAAG,CAAC;AAAA,MACrD;AACE,eAAO;AAAA,IACX;AAAA,EACF,CAAC;AACH;","names":[]}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Op
|
|
3
|
+
} from "./chunk-R7FLS7BG.js";
|
|
4
|
+
|
|
5
|
+
// src/features/host/model/layer-handles.ts
|
|
6
|
+
var LineLayerHandle = class {
|
|
7
|
+
constructor(sink, id) {
|
|
8
|
+
this.sink = sink;
|
|
9
|
+
this.id = id;
|
|
10
|
+
}
|
|
11
|
+
sink;
|
|
12
|
+
id;
|
|
13
|
+
/** Push a single `[t, y]` sample. Allocates a 2-element Float32Array. */
|
|
14
|
+
push(sample) {
|
|
15
|
+
const buf = new Float32Array(2);
|
|
16
|
+
buf[0] = sample.t;
|
|
17
|
+
buf[1] = sample.y;
|
|
18
|
+
this.sink.pushData(this.id, buf);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Push an array of samples in one postMessage. Encodes into a single
|
|
22
|
+
* contiguous Float32Array and transfers ownership. Prefer this over a loop
|
|
23
|
+
* of `push()` when you already have a batch in hand.
|
|
24
|
+
*/
|
|
25
|
+
pushBatch(samples) {
|
|
26
|
+
const n = samples.length;
|
|
27
|
+
if (n === 0) return;
|
|
28
|
+
const buf = new Float32Array(n * 2);
|
|
29
|
+
for (let i = 0; i < n; i++) {
|
|
30
|
+
const s = samples[i];
|
|
31
|
+
buf[i * 2] = s.t;
|
|
32
|
+
buf[i * 2 + 1] = s.y;
|
|
33
|
+
}
|
|
34
|
+
this.sink.pushData(this.id, buf);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Escape hatch: push a pre-built `[t, y, t, y, ...]` Float32Array directly.
|
|
38
|
+
* Use for hot paths where you can avoid the object-to-array encode step.
|
|
39
|
+
* The TypedArray's byteOffset must be 0 (same rule as `pushData`).
|
|
40
|
+
*/
|
|
41
|
+
pushRaw(data) {
|
|
42
|
+
this.sink.pushData(this.id, data);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var LineStaticLayerHandle = class {
|
|
46
|
+
constructor(sink, id) {
|
|
47
|
+
this.sink = sink;
|
|
48
|
+
this.id = id;
|
|
49
|
+
}
|
|
50
|
+
sink;
|
|
51
|
+
id;
|
|
52
|
+
setXY(points) {
|
|
53
|
+
const n = points.length;
|
|
54
|
+
const buf = new Float32Array(n * 2);
|
|
55
|
+
for (let i = 0; i < n; i++) {
|
|
56
|
+
const p = points[i];
|
|
57
|
+
buf[i * 2] = p.x;
|
|
58
|
+
buf[i * 2 + 1] = p.y;
|
|
59
|
+
}
|
|
60
|
+
this.sink.pushData(this.id, buf);
|
|
61
|
+
}
|
|
62
|
+
setY(values) {
|
|
63
|
+
const buf = new Float32Array(values.length);
|
|
64
|
+
for (let i = 0; i < values.length; i++) buf[i] = values[i];
|
|
65
|
+
this.sink.pushData(this.id, buf);
|
|
66
|
+
}
|
|
67
|
+
pushRaw(data) {
|
|
68
|
+
this.sink.pushData(this.id, data);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var LidarLayerHandle = class {
|
|
72
|
+
constructor(sink, id, stride = 4) {
|
|
73
|
+
this.sink = sink;
|
|
74
|
+
this.id = id;
|
|
75
|
+
this.stride = stride;
|
|
76
|
+
}
|
|
77
|
+
sink;
|
|
78
|
+
id;
|
|
79
|
+
stride;
|
|
80
|
+
push(points) {
|
|
81
|
+
const stride = this.stride;
|
|
82
|
+
const n = points.length;
|
|
83
|
+
const buf = new Float32Array(n * stride);
|
|
84
|
+
for (let i = 0; i < n; i++) {
|
|
85
|
+
const p = points[i];
|
|
86
|
+
const o = i * stride;
|
|
87
|
+
buf[o] = p.x;
|
|
88
|
+
buf[o + 1] = p.y;
|
|
89
|
+
if (stride >= 3) buf[o + 2] = p.z ?? 0;
|
|
90
|
+
if (stride >= 4) buf[o + 3] = p.intensity ?? 0;
|
|
91
|
+
}
|
|
92
|
+
this.sink.pushData(this.id, buf);
|
|
93
|
+
}
|
|
94
|
+
pushRaw(data) {
|
|
95
|
+
this.sink.pushData(this.id, data);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/features/host/model/fluxion-host.ts
|
|
100
|
+
function defaultWorkerFactory() {
|
|
101
|
+
return new Worker(new URL("./fluxion-worker.js", import.meta.url), {
|
|
102
|
+
type: "module"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function dtypeOf(arr) {
|
|
106
|
+
if (arr instanceof Float32Array) return "f32";
|
|
107
|
+
if (arr instanceof Uint8Array) return "u8";
|
|
108
|
+
if (arr instanceof Int16Array) return "i16";
|
|
109
|
+
if (arr instanceof Uint16Array) return "u16";
|
|
110
|
+
if (arr instanceof Int32Array) return "i32";
|
|
111
|
+
throw new Error("fluxion-render: unsupported TypedArray");
|
|
112
|
+
}
|
|
113
|
+
var FluxionHost = class {
|
|
114
|
+
worker;
|
|
115
|
+
disposed = false;
|
|
116
|
+
constructor(canvas, opts = {}) {
|
|
117
|
+
this.worker = (opts.workerFactory ?? defaultWorkerFactory)();
|
|
118
|
+
const offscreen = canvas.transferControlToOffscreen();
|
|
119
|
+
const dpr = typeof devicePixelRatio === "number" ? devicePixelRatio : 1;
|
|
120
|
+
const rect = canvas.getBoundingClientRect();
|
|
121
|
+
const width = rect.width || canvas.width || 300;
|
|
122
|
+
const height = rect.height || canvas.height || 150;
|
|
123
|
+
this.post(
|
|
124
|
+
{
|
|
125
|
+
op: Op.INIT,
|
|
126
|
+
canvas: offscreen,
|
|
127
|
+
width,
|
|
128
|
+
height,
|
|
129
|
+
dpr
|
|
130
|
+
},
|
|
131
|
+
[offscreen]
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
addLayer(id, kind, config) {
|
|
135
|
+
this.post({ op: Op.ADD_LAYER, id, kind, config });
|
|
136
|
+
}
|
|
137
|
+
removeLayer(id) {
|
|
138
|
+
this.post({ op: Op.REMOVE_LAYER, id });
|
|
139
|
+
}
|
|
140
|
+
configLayer(id, config) {
|
|
141
|
+
this.post({ op: Op.CONFIG, id, config });
|
|
142
|
+
}
|
|
143
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
144
|
+
// Typed add-layer helpers: construct + return a typed handle in one call.
|
|
145
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
146
|
+
/**
|
|
147
|
+
* Add a streaming line layer and return a handle that accepts structured
|
|
148
|
+
* `{ t, y }` samples instead of raw Float32Array interleaved layout.
|
|
149
|
+
*/
|
|
150
|
+
addLineLayer(id, config) {
|
|
151
|
+
this.addLayer(id, "line", config);
|
|
152
|
+
return new LineLayerHandle(this, id);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Add a static xy line layer and return a handle that accepts
|
|
156
|
+
* `{ x, y }[]` or plain y-only arrays.
|
|
157
|
+
*/
|
|
158
|
+
addLineStaticLayer(id, config) {
|
|
159
|
+
this.addLayer(id, "line-static", config);
|
|
160
|
+
return new LineStaticLayerHandle(this, id);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Add a LiDAR scatter layer and return a handle that accepts
|
|
164
|
+
* `{ x, y, z?, intensity? }[]`. The handle's stride must match
|
|
165
|
+
* `config.stride` (default 4).
|
|
166
|
+
*/
|
|
167
|
+
addLidarLayer(id, config) {
|
|
168
|
+
this.addLayer(id, "lidar", config);
|
|
169
|
+
const stride = config?.stride ?? 4;
|
|
170
|
+
return new LidarLayerHandle(this, id, stride);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Add an axis/grid layer. Axis layers don't take data, so this returns
|
|
174
|
+
* void — use `configLayer` to retune bounds / time window later.
|
|
175
|
+
*/
|
|
176
|
+
addAxisLayer(id, config) {
|
|
177
|
+
this.addLayer(id, "axis-grid", config);
|
|
178
|
+
}
|
|
179
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
180
|
+
// Attach a typed handle to a layer that was added via another API path
|
|
181
|
+
// (e.g. declaratively through `<FluxionCanvas layers={...}>` or
|
|
182
|
+
// `useFluxionCanvas({ layers: [...] })`).
|
|
183
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
184
|
+
line(id) {
|
|
185
|
+
return new LineLayerHandle(this, id);
|
|
186
|
+
}
|
|
187
|
+
lineStatic(id) {
|
|
188
|
+
return new LineStaticLayerHandle(this, id);
|
|
189
|
+
}
|
|
190
|
+
lidar(id, stride = 4) {
|
|
191
|
+
return new LidarLayerHandle(this, id, stride);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Push TypedArray data to a layer. Transfers the underlying ArrayBuffer —
|
|
195
|
+
* the caller MUST NOT use `data` again afterwards.
|
|
196
|
+
*
|
|
197
|
+
* The TypedArray must start at byteOffset 0 because the worker reconstructs
|
|
198
|
+
* the view at offset 0. Subviews would silently read from the wrong offset,
|
|
199
|
+
* so they're rejected up-front. Use `data.slice()` to get a fresh buffer.
|
|
200
|
+
*/
|
|
201
|
+
pushData(id, data) {
|
|
202
|
+
if (data.byteOffset !== 0) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`fluxion-render: TypedArray must start at byteOffset 0 (got ${data.byteOffset}). Call .slice() to copy into a fresh buffer before pushing.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const buffer = data.buffer;
|
|
208
|
+
this.post(
|
|
209
|
+
{
|
|
210
|
+
op: Op.DATA,
|
|
211
|
+
id,
|
|
212
|
+
buffer,
|
|
213
|
+
dtype: dtypeOf(data),
|
|
214
|
+
length: data.length
|
|
215
|
+
},
|
|
216
|
+
[buffer]
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
resize(width, height, dpr) {
|
|
220
|
+
this.post({ op: Op.RESIZE, width, height, dpr });
|
|
221
|
+
}
|
|
222
|
+
dispose() {
|
|
223
|
+
if (this.disposed) return;
|
|
224
|
+
this.disposed = true;
|
|
225
|
+
try {
|
|
226
|
+
this.post({ op: Op.DISPOSE });
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
this.worker.terminate();
|
|
230
|
+
}
|
|
231
|
+
post(msg, transfer) {
|
|
232
|
+
if (this.disposed) return;
|
|
233
|
+
if (transfer && transfer.length) {
|
|
234
|
+
this.worker.postMessage(msg, transfer);
|
|
235
|
+
} else {
|
|
236
|
+
this.worker.postMessage(msg);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export {
|
|
242
|
+
LineLayerHandle,
|
|
243
|
+
LineStaticLayerHandle,
|
|
244
|
+
LidarLayerHandle,
|
|
245
|
+
FluxionHost
|
|
246
|
+
};
|
|
247
|
+
//# sourceMappingURL=chunk-56NZ4OEO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/features/host/model/layer-handles.ts","../src/features/host/model/fluxion-host.ts"],"sourcesContent":["/**\n * Type-safe layer handles.\n *\n * These wrap the raw `FluxionHost.pushData(id, Float32Array)` API so callers\n * can work with structured records on the main thread (e.g. after receiving\n * ROS messages or processing sensor frames) without hand-rolling the\n * interleaved Float32Array layout every time.\n *\n * Each handle is a thin encoder: the input shape is type-checked at the call\n * site, encoding to the worker-side layout happens once, and the underlying\n * ArrayBuffer is still transferred zero-copy.\n */\n\n// A minimal surface of `FluxionHost` that handles depend on. Declared here\n// so this module has no circular import with fluxion-host.ts.\nexport interface FluxionDataSink {\n pushData(id: string, data: Float32Array): void;\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Line (streaming) — data layout: [t, y, t, y, ...]\n// ────────────────────────────────────────────────────────────────────────────\n\n/** One streaming sample. `t` is host-relative ms (what `axis-grid` time mode expects). */\nexport interface LineSample {\n t: number;\n y: number;\n}\n\nexport class LineLayerHandle {\n constructor(\n private readonly sink: FluxionDataSink,\n readonly id: string,\n ) {}\n\n /** Push a single `[t, y]` sample. Allocates a 2-element Float32Array. */\n push(sample: LineSample): void {\n const buf = new Float32Array(2);\n buf[0] = sample.t;\n buf[1] = sample.y;\n this.sink.pushData(this.id, buf);\n }\n\n /**\n * Push an array of samples in one postMessage. Encodes into a single\n * contiguous Float32Array and transfers ownership. Prefer this over a loop\n * of `push()` when you already have a batch in hand.\n */\n pushBatch(samples: readonly LineSample[]): void {\n const n = samples.length;\n if (n === 0) return;\n const buf = new Float32Array(n * 2);\n for (let i = 0; i < n; i++) {\n const s = samples[i];\n buf[i * 2] = s.t;\n buf[i * 2 + 1] = s.y;\n }\n this.sink.pushData(this.id, buf);\n }\n\n /**\n * Escape hatch: push a pre-built `[t, y, t, y, ...]` Float32Array directly.\n * Use for hot paths where you can avoid the object-to-array encode step.\n * The TypedArray's byteOffset must be 0 (same rule as `pushData`).\n */\n pushRaw(data: Float32Array): void {\n this.sink.pushData(this.id, data);\n }\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Line-static — data layout: [x, y, x, y, ...] or [y0, y1, ...]\n// ────────────────────────────────────────────────────────────────────────────\n\nexport interface XyPoint {\n x: number;\n y: number;\n}\n\n/**\n * Handle for `kind: \"line-static\"` layers. `setXY` replaces the entire series\n * with a new xy array; `setY` does the same with y-only data (x is computed\n * from the layer's configured x range). Use the variant that matches the\n * layer's `layout` config — this is not enforced at the type level since the\n * worker-side layout is a runtime config.\n */\nexport class LineStaticLayerHandle {\n constructor(\n private readonly sink: FluxionDataSink,\n readonly id: string,\n ) {}\n\n setXY(points: readonly XyPoint[]): void {\n const n = points.length;\n const buf = new Float32Array(n * 2);\n for (let i = 0; i < n; i++) {\n const p = points[i];\n buf[i * 2] = p.x;\n buf[i * 2 + 1] = p.y;\n }\n this.sink.pushData(this.id, buf);\n }\n\n setY(values: readonly number[]): void {\n const buf = new Float32Array(values.length);\n for (let i = 0; i < values.length; i++) buf[i] = values[i];\n this.sink.pushData(this.id, buf);\n }\n\n pushRaw(data: Float32Array): void {\n this.sink.pushData(this.id, data);\n }\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Lidar — data layout: [x, y, (z), (intensity), ...] with configurable stride\n// ────────────────────────────────────────────────────────────────────────────\n\nexport interface LidarPoint {\n x: number;\n y: number;\n /** Optional when the layer stride is 2. Defaults to 0 otherwise. */\n z?: number;\n /** Optional when the layer stride is <4. Defaults to 0 otherwise. */\n intensity?: number;\n}\n\nexport type LidarStride = 2 | 3 | 4;\n\n/**\n * Handle for `kind: \"lidar\"` scatter layers. The stride (2 = xy, 3 = xyz,\n * 4 = xyz+intensity) must match the layer's stride config. Passing a\n * mismatched stride will succeed but the worker will read the wrong fields.\n */\nexport class LidarLayerHandle {\n constructor(\n private readonly sink: FluxionDataSink,\n readonly id: string,\n readonly stride: LidarStride = 4,\n ) {}\n\n push(points: readonly LidarPoint[]): void {\n const stride = this.stride;\n const n = points.length;\n const buf = new Float32Array(n * stride);\n for (let i = 0; i < n; i++) {\n const p = points[i];\n const o = i * stride;\n buf[o] = p.x;\n buf[o + 1] = p.y;\n if (stride >= 3) buf[o + 2] = p.z ?? 0;\n if (stride >= 4) buf[o + 3] = p.intensity ?? 0;\n }\n this.sink.pushData(this.id, buf);\n }\n\n pushRaw(data: Float32Array): void {\n this.sink.pushData(this.id, data);\n }\n}\n","import type { AxisGridConfig } from \"../../../entities/axis-grid-layer\";\nimport type { LidarScatterConfig } from \"../../../entities/lidar-scatter-layer\";\nimport type { LineChartConfig } from \"../../../entities/line-chart-layer\";\nimport type { LineChartStaticConfig } from \"../../../entities/line-chart-static-layer\";\nimport { Op, type DType, type HostMsg, type LayerKind } from \"../../../shared/protocol\";\nimport {\n LidarLayerHandle,\n type LidarStride,\n LineLayerHandle,\n LineStaticLayerHandle,\n} from \"./layer-handles\";\n\n/**\n * TypedArray flavors that FluxionRender accepts. `ArrayBufferView` is too\n * permissive (includes DataView), so we narrow to the specific types whose\n * layout matches the worker-side `wrapTypedArray` contract.\n */\nexport type FluxionTypedArray =\n | Float32Array\n | Uint8Array\n | Int16Array\n | Uint16Array\n | Int32Array;\n\nexport interface FluxionHostOptions {\n /**\n * Override the worker URL. Useful when bundlers don't support\n * `new Worker(new URL('./fluxion-worker.js', import.meta.url))`.\n * Pass a factory that returns a constructed Worker.\n */\n workerFactory?: () => Worker;\n}\n\nfunction defaultWorkerFactory(): Worker {\n // Vite / modern bundlers resolve this to a separate worker chunk.\n return new Worker(new URL(\"./fluxion-worker.js\", import.meta.url), {\n type: \"module\",\n });\n}\n\nfunction dtypeOf(arr: FluxionTypedArray): DType {\n if (arr instanceof Float32Array) return \"f32\";\n if (arr instanceof Uint8Array) return \"u8\";\n if (arr instanceof Int16Array) return \"i16\";\n if (arr instanceof Uint16Array) return \"u16\";\n if (arr instanceof Int32Array) return \"i32\";\n throw new Error(\"fluxion-render: unsupported TypedArray\");\n}\n\n/**\n * Main-thread handle to a worker-hosted rendering engine.\n *\n * Lifecycle:\n * const host = new FluxionHost(canvas);\n * host.addLayer('chart', 'line', { color: '#0ff' });\n * host.pushData('chart', float32); // transfers ownership\n * host.resize(w, h, dpr);\n * host.dispose();\n */\nexport class FluxionHost {\n private worker: Worker;\n private disposed = false;\n\n constructor(canvas: HTMLCanvasElement, opts: FluxionHostOptions = {}) {\n this.worker = (opts.workerFactory ?? defaultWorkerFactory)();\n\n const offscreen = canvas.transferControlToOffscreen();\n const dpr = typeof devicePixelRatio === \"number\" ? devicePixelRatio : 1;\n const rect = canvas.getBoundingClientRect();\n const width = rect.width || canvas.width || 300;\n const height = rect.height || canvas.height || 150;\n\n this.post(\n {\n op: Op.INIT,\n canvas: offscreen,\n width,\n height,\n dpr,\n },\n [offscreen],\n );\n }\n\n /**\n * Typed `addLayer` overloads.\n *\n * Prefer the kind-specific helpers below (`addLineLayer`, `addAxisLayer`,\n * etc.) — they both type-check the config AND return a typed handle where\n * applicable. This overload is retained for cases where the kind is chosen\n * dynamically.\n */\n addLayer(id: string, kind: \"line\", config?: LineChartConfig): void;\n addLayer(id: string, kind: \"line-static\", config?: LineChartStaticConfig): void;\n addLayer(id: string, kind: \"lidar\", config?: LidarScatterConfig): void;\n addLayer(id: string, kind: \"axis-grid\", config?: AxisGridConfig): void;\n // Dynamic fallback for code paths that pass a runtime `LayerKind` (e.g.\n // `useFluxionCanvas({ layers: FluxionLayerSpec[] })`).\n addLayer(id: string, kind: LayerKind, config?: unknown): void;\n addLayer(id: string, kind: LayerKind, config?: unknown): void {\n this.post({ op: Op.ADD_LAYER, id, kind, config });\n }\n\n removeLayer(id: string): void {\n this.post({ op: Op.REMOVE_LAYER, id });\n }\n\n /**\n * Typed `configLayer` overloads — pick the config shape from the kind used\n * when the layer was created. There's no runtime tag check; the caller is\n * trusted to pass the right config for the right id.\n */\n configLayer(id: string, config: LineChartConfig): void;\n configLayer(id: string, config: LineChartStaticConfig): void;\n configLayer(id: string, config: LidarScatterConfig): void;\n configLayer(id: string, config: AxisGridConfig): void;\n // Dynamic fallback for helpers like `useLayerConfig` that carry an opaque\n // config alongside the layer id.\n configLayer(id: string, config: unknown): void;\n configLayer(id: string, config: unknown): void {\n this.post({ op: Op.CONFIG, id, config });\n }\n\n // ──────────────────────────────────────────────────────────────────────\n // Typed add-layer helpers: construct + return a typed handle in one call.\n // ──────────────────────────────────────────────────────────────────────\n\n /**\n * Add a streaming line layer and return a handle that accepts structured\n * `{ t, y }` samples instead of raw Float32Array interleaved layout.\n */\n addLineLayer(id: string, config?: LineChartConfig): LineLayerHandle {\n this.addLayer(id, \"line\", config);\n return new LineLayerHandle(this, id);\n }\n\n /**\n * Add a static xy line layer and return a handle that accepts\n * `{ x, y }[]` or plain y-only arrays.\n */\n addLineStaticLayer(id: string, config?: LineChartStaticConfig): LineStaticLayerHandle {\n this.addLayer(id, \"line-static\", config);\n return new LineStaticLayerHandle(this, id);\n }\n\n /**\n * Add a LiDAR scatter layer and return a handle that accepts\n * `{ x, y, z?, intensity? }[]`. The handle's stride must match\n * `config.stride` (default 4).\n */\n addLidarLayer(id: string, config?: LidarScatterConfig): LidarLayerHandle {\n this.addLayer(id, \"lidar\", config);\n const stride = (config?.stride as LidarStride | undefined) ?? 4;\n return new LidarLayerHandle(this, id, stride);\n }\n\n /**\n * Add an axis/grid layer. Axis layers don't take data, so this returns\n * void — use `configLayer` to retune bounds / time window later.\n */\n addAxisLayer(id: string, config?: AxisGridConfig): void {\n this.addLayer(id, \"axis-grid\", config);\n }\n\n // ──────────────────────────────────────────────────────────────────────\n // Attach a typed handle to a layer that was added via another API path\n // (e.g. declaratively through `<FluxionCanvas layers={...}>` or\n // `useFluxionCanvas({ layers: [...] })`).\n // ──────────────────────────────────────────────────────────────────────\n\n line(id: string): LineLayerHandle {\n return new LineLayerHandle(this, id);\n }\n\n lineStatic(id: string): LineStaticLayerHandle {\n return new LineStaticLayerHandle(this, id);\n }\n\n lidar(id: string, stride: LidarStride = 4): LidarLayerHandle {\n return new LidarLayerHandle(this, id, stride);\n }\n\n /**\n * Push TypedArray data to a layer. Transfers the underlying ArrayBuffer —\n * the caller MUST NOT use `data` again afterwards.\n *\n * The TypedArray must start at byteOffset 0 because the worker reconstructs\n * the view at offset 0. Subviews would silently read from the wrong offset,\n * so they're rejected up-front. Use `data.slice()` to get a fresh buffer.\n */\n pushData(id: string, data: FluxionTypedArray): void {\n if (data.byteOffset !== 0) {\n throw new Error(\n `fluxion-render: TypedArray must start at byteOffset 0 (got ${data.byteOffset}). ` +\n `Call .slice() to copy into a fresh buffer before pushing.`,\n );\n }\n const buffer = data.buffer as ArrayBuffer;\n this.post(\n {\n op: Op.DATA,\n id,\n buffer,\n dtype: dtypeOf(data),\n length: data.length,\n },\n [buffer],\n );\n }\n\n resize(width: number, height: number, dpr: number): void {\n this.post({ op: Op.RESIZE, width, height, dpr });\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n try {\n this.post({ op: Op.DISPOSE });\n } catch {\n // worker may already be gone\n }\n this.worker.terminate();\n }\n\n private post(msg: HostMsg, transfer?: Transferable[]): void {\n if (this.disposed) return;\n if (transfer && transfer.length) {\n this.worker.postMessage(msg, transfer);\n } else {\n this.worker.postMessage(msg);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/protocol/protocol.ts"],"sourcesContent":["/**\n * Binary message protocol between main-thread `FluxionHost` and worker `Engine`.\n * Uses a plain const object (not const enum) so consumers with\n * `isolatedModules` can import types safely from the published package.\n */\nexport const Op = {\n INIT: 1,\n RESIZE: 2,\n ADD_LAYER: 3,\n REMOVE_LAYER: 4,\n CONFIG: 5,\n DATA: 6,\n DISPOSE: 7,\n} as const;\nexport type Op = (typeof Op)[keyof typeof Op];\n\nexport type LayerKind = \"line\" | \"line-static\" | \"lidar\" | \"axis-grid\";\n\nexport type DType = \"f32\" | \"u8\" | \"i16\" | \"u16\" | \"i32\";\n\nexport interface InitMsg {\n op: typeof Op.INIT;\n canvas: OffscreenCanvas;\n width: number;\n height: number;\n dpr: number;\n}\n\nexport interface ResizeMsg {\n op: typeof Op.RESIZE;\n width: number;\n height: number;\n dpr: number;\n}\n\nexport interface AddLayerMsg {\n op: typeof Op.ADD_LAYER;\n id: string;\n kind: LayerKind;\n config?: unknown;\n}\n\nexport interface RemoveLayerMsg {\n op: typeof Op.REMOVE_LAYER;\n id: string;\n}\n\nexport interface ConfigMsg {\n op: typeof Op.CONFIG;\n id: string;\n config: unknown;\n}\n\nexport interface DataMsg {\n op: typeof Op.DATA;\n id: string;\n buffer: ArrayBuffer;\n dtype: DType;\n length: number;\n}\n\nexport interface DisposeMsg {\n op: typeof Op.DISPOSE;\n}\n\nexport type HostMsg =\n | InitMsg\n | ResizeMsg\n | AddLayerMsg\n | RemoveLayerMsg\n | ConfigMsg\n | DataMsg\n | DisposeMsg;\n"],"mappings":";AAKO,IAAM,KAAK;AAAA,EAChB,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,SAAS;AACX;","names":[]}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
interface AxisGridConfig {
|
|
2
|
+
/** Fixed x-range. Used when `xMode` is "fixed" (default). */
|
|
3
|
+
xRange?: [number, number];
|
|
4
|
+
yRange?: [number, number];
|
|
5
|
+
gridColor?: string;
|
|
6
|
+
axisColor?: string;
|
|
7
|
+
labelColor?: string;
|
|
8
|
+
font?: string;
|
|
9
|
+
targetTicks?: number;
|
|
10
|
+
/** If true (default), writes this layer's bounds into `viewport` so data layers share them. */
|
|
11
|
+
applyToViewport?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* "fixed": xRange is literal world units (default).
|
|
14
|
+
* "time": bounds follow the streaming `viewport.latestT` as a trailing
|
|
15
|
+
* sliding window `[latestT - timeWindowMs, latestT]`. yRange is still fixed.
|
|
16
|
+
*/
|
|
17
|
+
xMode?: "fixed" | "time";
|
|
18
|
+
/** Width of the sliding window in ms when xMode="time". Default 5000. */
|
|
19
|
+
timeWindowMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Absolute wall-clock epoch (ms) corresponding to data timestamp `0`. When
|
|
22
|
+
* set together with `xMode: "time"`, tick labels render as wall clock
|
|
23
|
+
* instead of elapsed seconds. Typically set once at host creation:
|
|
24
|
+
* `timeOrigin: Date.now()` on the main thread.
|
|
25
|
+
*/
|
|
26
|
+
timeOrigin?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Clock-pattern string used to render x tick labels when `xMode: "time"`
|
|
29
|
+
* AND `timeOrigin` is set. Default `"HH:mm:ss"`. Supported tokens:
|
|
30
|
+
* `HH / H / mm / m / ss / s / SSS / S`. Anything else is a literal.
|
|
31
|
+
*
|
|
32
|
+
* Ignored when `timeOrigin` is not provided — elapsed-seconds fallback
|
|
33
|
+
* (`"X.Xs"`) is used instead.
|
|
34
|
+
*/
|
|
35
|
+
xTickFormat?: string;
|
|
36
|
+
/**
|
|
37
|
+
* "fixed" (default): use configured `yRange`.
|
|
38
|
+
* "auto": data-driven. Reads `viewport.observedYMin/Max` during draw,
|
|
39
|
+
* applies padding and clamps, updates `bounds.yMin/yMax`. Requires at
|
|
40
|
+
* least one data layer (e.g. `LineChartLayer`) in the stack to publish
|
|
41
|
+
* observations via its `scan()` pass.
|
|
42
|
+
*/
|
|
43
|
+
yMode?: "fixed" | "auto";
|
|
44
|
+
/** Padding ratio applied above/below the observed range. Default 0.1 (10%). */
|
|
45
|
+
yAutoPadding?: number;
|
|
46
|
+
/** Absolute lower clamp after padding. */
|
|
47
|
+
yAutoMin?: number;
|
|
48
|
+
/** Absolute upper clamp after padding. */
|
|
49
|
+
yAutoMax?: number;
|
|
50
|
+
/** Show vertical grid lines at x ticks. */
|
|
51
|
+
showXGrid?: boolean;
|
|
52
|
+
/** Show horizontal grid lines at y ticks. */
|
|
53
|
+
showYGrid?: boolean;
|
|
54
|
+
/** Show the x=0 / y=0 axis lines when 0 is inside the range. */
|
|
55
|
+
showAxes?: boolean;
|
|
56
|
+
/** Show tick labels along the x axis. */
|
|
57
|
+
showXLabels?: boolean;
|
|
58
|
+
/** Show tick labels along the y axis. */
|
|
59
|
+
showYLabels?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface LidarScatterConfig {
|
|
63
|
+
/** Floats per point. Default 4 (x,y,z,intensity). Minimum 2 (x,y). */
|
|
64
|
+
stride?: number;
|
|
65
|
+
/** Point size in pixels. */
|
|
66
|
+
pointSize?: number;
|
|
67
|
+
/** Max intensity value for normalization (0..1). Default 1.0. */
|
|
68
|
+
intensityMax?: number;
|
|
69
|
+
/** Solid color override. When set, intensity LUT is ignored. */
|
|
70
|
+
color?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface LineChartConfig {
|
|
74
|
+
color?: string;
|
|
75
|
+
lineWidth?: number;
|
|
76
|
+
/** Ring buffer capacity (number of [t,y] samples retained). Default 2048. */
|
|
77
|
+
capacity?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface LineChartStaticConfig {
|
|
81
|
+
color?: string;
|
|
82
|
+
lineWidth?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Data layout:
|
|
85
|
+
* - "xy" (default): interleaved [x,y,x,y,...] stride=2
|
|
86
|
+
* - "y": [y0,y1,...] with implicit x = linear sweep across `viewport.bounds.x`
|
|
87
|
+
*/
|
|
88
|
+
layout?: "xy" | "y";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Binary message protocol between main-thread `FluxionHost` and worker `Engine`.
|
|
93
|
+
* Uses a plain const object (not const enum) so consumers with
|
|
94
|
+
* `isolatedModules` can import types safely from the published package.
|
|
95
|
+
*/
|
|
96
|
+
declare const Op: {
|
|
97
|
+
readonly INIT: 1;
|
|
98
|
+
readonly RESIZE: 2;
|
|
99
|
+
readonly ADD_LAYER: 3;
|
|
100
|
+
readonly REMOVE_LAYER: 4;
|
|
101
|
+
readonly CONFIG: 5;
|
|
102
|
+
readonly DATA: 6;
|
|
103
|
+
readonly DISPOSE: 7;
|
|
104
|
+
};
|
|
105
|
+
type Op = (typeof Op)[keyof typeof Op];
|
|
106
|
+
type LayerKind = "line" | "line-static" | "lidar" | "axis-grid";
|
|
107
|
+
type DType = "f32" | "u8" | "i16" | "u16" | "i32";
|
|
108
|
+
interface InitMsg {
|
|
109
|
+
op: typeof Op.INIT;
|
|
110
|
+
canvas: OffscreenCanvas;
|
|
111
|
+
width: number;
|
|
112
|
+
height: number;
|
|
113
|
+
dpr: number;
|
|
114
|
+
}
|
|
115
|
+
interface ResizeMsg {
|
|
116
|
+
op: typeof Op.RESIZE;
|
|
117
|
+
width: number;
|
|
118
|
+
height: number;
|
|
119
|
+
dpr: number;
|
|
120
|
+
}
|
|
121
|
+
interface AddLayerMsg {
|
|
122
|
+
op: typeof Op.ADD_LAYER;
|
|
123
|
+
id: string;
|
|
124
|
+
kind: LayerKind;
|
|
125
|
+
config?: unknown;
|
|
126
|
+
}
|
|
127
|
+
interface RemoveLayerMsg {
|
|
128
|
+
op: typeof Op.REMOVE_LAYER;
|
|
129
|
+
id: string;
|
|
130
|
+
}
|
|
131
|
+
interface ConfigMsg {
|
|
132
|
+
op: typeof Op.CONFIG;
|
|
133
|
+
id: string;
|
|
134
|
+
config: unknown;
|
|
135
|
+
}
|
|
136
|
+
interface DataMsg {
|
|
137
|
+
op: typeof Op.DATA;
|
|
138
|
+
id: string;
|
|
139
|
+
buffer: ArrayBuffer;
|
|
140
|
+
dtype: DType;
|
|
141
|
+
length: number;
|
|
142
|
+
}
|
|
143
|
+
interface DisposeMsg {
|
|
144
|
+
op: typeof Op.DISPOSE;
|
|
145
|
+
}
|
|
146
|
+
type HostMsg = InitMsg | ResizeMsg | AddLayerMsg | RemoveLayerMsg | ConfigMsg | DataMsg | DisposeMsg;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Type-safe layer handles.
|
|
150
|
+
*
|
|
151
|
+
* These wrap the raw `FluxionHost.pushData(id, Float32Array)` API so callers
|
|
152
|
+
* can work with structured records on the main thread (e.g. after receiving
|
|
153
|
+
* ROS messages or processing sensor frames) without hand-rolling the
|
|
154
|
+
* interleaved Float32Array layout every time.
|
|
155
|
+
*
|
|
156
|
+
* Each handle is a thin encoder: the input shape is type-checked at the call
|
|
157
|
+
* site, encoding to the worker-side layout happens once, and the underlying
|
|
158
|
+
* ArrayBuffer is still transferred zero-copy.
|
|
159
|
+
*/
|
|
160
|
+
interface FluxionDataSink {
|
|
161
|
+
pushData(id: string, data: Float32Array): void;
|
|
162
|
+
}
|
|
163
|
+
/** One streaming sample. `t` is host-relative ms (what `axis-grid` time mode expects). */
|
|
164
|
+
interface LineSample {
|
|
165
|
+
t: number;
|
|
166
|
+
y: number;
|
|
167
|
+
}
|
|
168
|
+
declare class LineLayerHandle {
|
|
169
|
+
private readonly sink;
|
|
170
|
+
readonly id: string;
|
|
171
|
+
constructor(sink: FluxionDataSink, id: string);
|
|
172
|
+
/** Push a single `[t, y]` sample. Allocates a 2-element Float32Array. */
|
|
173
|
+
push(sample: LineSample): void;
|
|
174
|
+
/**
|
|
175
|
+
* Push an array of samples in one postMessage. Encodes into a single
|
|
176
|
+
* contiguous Float32Array and transfers ownership. Prefer this over a loop
|
|
177
|
+
* of `push()` when you already have a batch in hand.
|
|
178
|
+
*/
|
|
179
|
+
pushBatch(samples: readonly LineSample[]): void;
|
|
180
|
+
/**
|
|
181
|
+
* Escape hatch: push a pre-built `[t, y, t, y, ...]` Float32Array directly.
|
|
182
|
+
* Use for hot paths where you can avoid the object-to-array encode step.
|
|
183
|
+
* The TypedArray's byteOffset must be 0 (same rule as `pushData`).
|
|
184
|
+
*/
|
|
185
|
+
pushRaw(data: Float32Array): void;
|
|
186
|
+
}
|
|
187
|
+
interface XyPoint {
|
|
188
|
+
x: number;
|
|
189
|
+
y: number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Handle for `kind: "line-static"` layers. `setXY` replaces the entire series
|
|
193
|
+
* with a new xy array; `setY` does the same with y-only data (x is computed
|
|
194
|
+
* from the layer's configured x range). Use the variant that matches the
|
|
195
|
+
* layer's `layout` config — this is not enforced at the type level since the
|
|
196
|
+
* worker-side layout is a runtime config.
|
|
197
|
+
*/
|
|
198
|
+
declare class LineStaticLayerHandle {
|
|
199
|
+
private readonly sink;
|
|
200
|
+
readonly id: string;
|
|
201
|
+
constructor(sink: FluxionDataSink, id: string);
|
|
202
|
+
setXY(points: readonly XyPoint[]): void;
|
|
203
|
+
setY(values: readonly number[]): void;
|
|
204
|
+
pushRaw(data: Float32Array): void;
|
|
205
|
+
}
|
|
206
|
+
interface LidarPoint {
|
|
207
|
+
x: number;
|
|
208
|
+
y: number;
|
|
209
|
+
/** Optional when the layer stride is 2. Defaults to 0 otherwise. */
|
|
210
|
+
z?: number;
|
|
211
|
+
/** Optional when the layer stride is <4. Defaults to 0 otherwise. */
|
|
212
|
+
intensity?: number;
|
|
213
|
+
}
|
|
214
|
+
type LidarStride = 2 | 3 | 4;
|
|
215
|
+
/**
|
|
216
|
+
* Handle for `kind: "lidar"` scatter layers. The stride (2 = xy, 3 = xyz,
|
|
217
|
+
* 4 = xyz+intensity) must match the layer's stride config. Passing a
|
|
218
|
+
* mismatched stride will succeed but the worker will read the wrong fields.
|
|
219
|
+
*/
|
|
220
|
+
declare class LidarLayerHandle {
|
|
221
|
+
private readonly sink;
|
|
222
|
+
readonly id: string;
|
|
223
|
+
readonly stride: LidarStride;
|
|
224
|
+
constructor(sink: FluxionDataSink, id: string, stride?: LidarStride);
|
|
225
|
+
push(points: readonly LidarPoint[]): void;
|
|
226
|
+
pushRaw(data: Float32Array): void;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* TypedArray flavors that FluxionRender accepts. `ArrayBufferView` is too
|
|
231
|
+
* permissive (includes DataView), so we narrow to the specific types whose
|
|
232
|
+
* layout matches the worker-side `wrapTypedArray` contract.
|
|
233
|
+
*/
|
|
234
|
+
type FluxionTypedArray = Float32Array | Uint8Array | Int16Array | Uint16Array | Int32Array;
|
|
235
|
+
interface FluxionHostOptions {
|
|
236
|
+
/**
|
|
237
|
+
* Override the worker URL. Useful when bundlers don't support
|
|
238
|
+
* `new Worker(new URL('./fluxion-worker.js', import.meta.url))`.
|
|
239
|
+
* Pass a factory that returns a constructed Worker.
|
|
240
|
+
*/
|
|
241
|
+
workerFactory?: () => Worker;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Main-thread handle to a worker-hosted rendering engine.
|
|
245
|
+
*
|
|
246
|
+
* Lifecycle:
|
|
247
|
+
* const host = new FluxionHost(canvas);
|
|
248
|
+
* host.addLayer('chart', 'line', { color: '#0ff' });
|
|
249
|
+
* host.pushData('chart', float32); // transfers ownership
|
|
250
|
+
* host.resize(w, h, dpr);
|
|
251
|
+
* host.dispose();
|
|
252
|
+
*/
|
|
253
|
+
declare class FluxionHost {
|
|
254
|
+
private worker;
|
|
255
|
+
private disposed;
|
|
256
|
+
constructor(canvas: HTMLCanvasElement, opts?: FluxionHostOptions);
|
|
257
|
+
/**
|
|
258
|
+
* Typed `addLayer` overloads.
|
|
259
|
+
*
|
|
260
|
+
* Prefer the kind-specific helpers below (`addLineLayer`, `addAxisLayer`,
|
|
261
|
+
* etc.) — they both type-check the config AND return a typed handle where
|
|
262
|
+
* applicable. This overload is retained for cases where the kind is chosen
|
|
263
|
+
* dynamically.
|
|
264
|
+
*/
|
|
265
|
+
addLayer(id: string, kind: "line", config?: LineChartConfig): void;
|
|
266
|
+
addLayer(id: string, kind: "line-static", config?: LineChartStaticConfig): void;
|
|
267
|
+
addLayer(id: string, kind: "lidar", config?: LidarScatterConfig): void;
|
|
268
|
+
addLayer(id: string, kind: "axis-grid", config?: AxisGridConfig): void;
|
|
269
|
+
addLayer(id: string, kind: LayerKind, config?: unknown): void;
|
|
270
|
+
removeLayer(id: string): void;
|
|
271
|
+
/**
|
|
272
|
+
* Typed `configLayer` overloads — pick the config shape from the kind used
|
|
273
|
+
* when the layer was created. There's no runtime tag check; the caller is
|
|
274
|
+
* trusted to pass the right config for the right id.
|
|
275
|
+
*/
|
|
276
|
+
configLayer(id: string, config: LineChartConfig): void;
|
|
277
|
+
configLayer(id: string, config: LineChartStaticConfig): void;
|
|
278
|
+
configLayer(id: string, config: LidarScatterConfig): void;
|
|
279
|
+
configLayer(id: string, config: AxisGridConfig): void;
|
|
280
|
+
configLayer(id: string, config: unknown): void;
|
|
281
|
+
/**
|
|
282
|
+
* Add a streaming line layer and return a handle that accepts structured
|
|
283
|
+
* `{ t, y }` samples instead of raw Float32Array interleaved layout.
|
|
284
|
+
*/
|
|
285
|
+
addLineLayer(id: string, config?: LineChartConfig): LineLayerHandle;
|
|
286
|
+
/**
|
|
287
|
+
* Add a static xy line layer and return a handle that accepts
|
|
288
|
+
* `{ x, y }[]` or plain y-only arrays.
|
|
289
|
+
*/
|
|
290
|
+
addLineStaticLayer(id: string, config?: LineChartStaticConfig): LineStaticLayerHandle;
|
|
291
|
+
/**
|
|
292
|
+
* Add a LiDAR scatter layer and return a handle that accepts
|
|
293
|
+
* `{ x, y, z?, intensity? }[]`. The handle's stride must match
|
|
294
|
+
* `config.stride` (default 4).
|
|
295
|
+
*/
|
|
296
|
+
addLidarLayer(id: string, config?: LidarScatterConfig): LidarLayerHandle;
|
|
297
|
+
/**
|
|
298
|
+
* Add an axis/grid layer. Axis layers don't take data, so this returns
|
|
299
|
+
* void — use `configLayer` to retune bounds / time window later.
|
|
300
|
+
*/
|
|
301
|
+
addAxisLayer(id: string, config?: AxisGridConfig): void;
|
|
302
|
+
line(id: string): LineLayerHandle;
|
|
303
|
+
lineStatic(id: string): LineStaticLayerHandle;
|
|
304
|
+
lidar(id: string, stride?: LidarStride): LidarLayerHandle;
|
|
305
|
+
/**
|
|
306
|
+
* Push TypedArray data to a layer. Transfers the underlying ArrayBuffer —
|
|
307
|
+
* the caller MUST NOT use `data` again afterwards.
|
|
308
|
+
*
|
|
309
|
+
* The TypedArray must start at byteOffset 0 because the worker reconstructs
|
|
310
|
+
* the view at offset 0. Subviews would silently read from the wrong offset,
|
|
311
|
+
* so they're rejected up-front. Use `data.slice()` to get a fresh buffer.
|
|
312
|
+
*/
|
|
313
|
+
pushData(id: string, data: FluxionTypedArray): void;
|
|
314
|
+
resize(width: number, height: number, dpr: number): void;
|
|
315
|
+
dispose(): void;
|
|
316
|
+
private post;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export { type AxisGridConfig as A, type DType as D, type FluxionDataSink as F, type HostMsg as H, type LayerKind as L, type XyPoint as X, FluxionHost as a, type FluxionHostOptions as b, type FluxionTypedArray as c, LidarLayerHandle as d, type LidarPoint as e, type LidarScatterConfig as f, type LidarStride as g, type LineChartConfig as h, type LineChartStaticConfig as i, LineLayerHandle as j, type LineSample as k, LineStaticLayerHandle as l };
|