@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,816 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatClock
|
|
3
|
+
} from "./chunk-3PK3KDO5.js";
|
|
4
|
+
import {
|
|
5
|
+
Op
|
|
6
|
+
} from "./chunk-R7FLS7BG.js";
|
|
7
|
+
|
|
8
|
+
// src/shared/lib/math.ts
|
|
9
|
+
function niceStep(range, targetTicks) {
|
|
10
|
+
const rough = range / Math.max(1, targetTicks);
|
|
11
|
+
const pow10 = Math.pow(10, Math.floor(Math.log10(rough)));
|
|
12
|
+
const norm = rough / pow10;
|
|
13
|
+
let nice;
|
|
14
|
+
if (norm < 1.5) nice = 1;
|
|
15
|
+
else if (norm < 3) nice = 2;
|
|
16
|
+
else if (norm < 7) nice = 5;
|
|
17
|
+
else nice = 10;
|
|
18
|
+
return nice * pow10;
|
|
19
|
+
}
|
|
20
|
+
function niceTicks(min, max, targetTicks = 6) {
|
|
21
|
+
if (!isFinite(min) || !isFinite(max) || max <= min) return [];
|
|
22
|
+
const step = niceStep(max - min, targetTicks);
|
|
23
|
+
const start = Math.ceil(min / step) * step;
|
|
24
|
+
const out = [];
|
|
25
|
+
for (let v = start; v <= max + step * 1e-6; v += step) {
|
|
26
|
+
out.push(Number(v.toFixed(12)));
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/entities/axis-grid-layer/model/axis-grid-layer.ts
|
|
32
|
+
var AxisGridLayer = class {
|
|
33
|
+
id;
|
|
34
|
+
gridColor = "rgba(255,255,255,0.08)";
|
|
35
|
+
axisColor = "rgba(255,255,255,0.4)";
|
|
36
|
+
labelColor = "rgba(255,255,255,0.7)";
|
|
37
|
+
font = "10px sans-serif";
|
|
38
|
+
targetTicks = 6;
|
|
39
|
+
bounds = { xMin: -1, xMax: 1, yMin: -1, yMax: 1 };
|
|
40
|
+
applyToViewport = true;
|
|
41
|
+
xMode = "fixed";
|
|
42
|
+
timeWindowMs = 5e3;
|
|
43
|
+
timeOrigin = null;
|
|
44
|
+
xTickFormat = "HH:mm:ss";
|
|
45
|
+
yMode = "fixed";
|
|
46
|
+
yAutoPadding = 0.1;
|
|
47
|
+
yAutoMin;
|
|
48
|
+
yAutoMax;
|
|
49
|
+
showXGrid = true;
|
|
50
|
+
showYGrid = true;
|
|
51
|
+
showAxes = true;
|
|
52
|
+
showXLabels = true;
|
|
53
|
+
showYLabels = true;
|
|
54
|
+
constructor(id) {
|
|
55
|
+
this.id = id;
|
|
56
|
+
}
|
|
57
|
+
setConfig(config) {
|
|
58
|
+
const c = config;
|
|
59
|
+
if (c.xRange) {
|
|
60
|
+
this.bounds.xMin = c.xRange[0];
|
|
61
|
+
this.bounds.xMax = c.xRange[1];
|
|
62
|
+
}
|
|
63
|
+
if (c.yRange) {
|
|
64
|
+
this.bounds.yMin = c.yRange[0];
|
|
65
|
+
this.bounds.yMax = c.yRange[1];
|
|
66
|
+
}
|
|
67
|
+
if (c.gridColor) this.gridColor = c.gridColor;
|
|
68
|
+
if (c.axisColor) this.axisColor = c.axisColor;
|
|
69
|
+
if (c.labelColor) this.labelColor = c.labelColor;
|
|
70
|
+
if (c.font) this.font = c.font;
|
|
71
|
+
if (c.targetTicks) this.targetTicks = c.targetTicks;
|
|
72
|
+
if (c.applyToViewport !== void 0) this.applyToViewport = c.applyToViewport;
|
|
73
|
+
if (c.xMode !== void 0) this.xMode = c.xMode;
|
|
74
|
+
if (c.timeWindowMs !== void 0) this.timeWindowMs = c.timeWindowMs;
|
|
75
|
+
if (c.timeOrigin !== void 0) this.timeOrigin = c.timeOrigin;
|
|
76
|
+
if (c.xTickFormat !== void 0) this.xTickFormat = c.xTickFormat;
|
|
77
|
+
if (c.yMode !== void 0) this.yMode = c.yMode;
|
|
78
|
+
if (c.yAutoPadding !== void 0) this.yAutoPadding = c.yAutoPadding;
|
|
79
|
+
if (c.yAutoMin !== void 0) this.yAutoMin = c.yAutoMin;
|
|
80
|
+
if (c.yAutoMax !== void 0) this.yAutoMax = c.yAutoMax;
|
|
81
|
+
if (c.showXGrid !== void 0) this.showXGrid = c.showXGrid;
|
|
82
|
+
if (c.showYGrid !== void 0) this.showYGrid = c.showYGrid;
|
|
83
|
+
if (c.showAxes !== void 0) this.showAxes = c.showAxes;
|
|
84
|
+
if (c.showXLabels !== void 0) this.showXLabels = c.showXLabels;
|
|
85
|
+
if (c.showYLabels !== void 0) this.showYLabels = c.showYLabels;
|
|
86
|
+
}
|
|
87
|
+
setData(_buffer, _length, _viewport) {
|
|
88
|
+
}
|
|
89
|
+
resize(_viewport) {
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Orchestration pass: establish x bounds so data layers' `scan` can filter
|
|
93
|
+
* visible samples correctly. yMode:"auto" is finalized in `draw` after all
|
|
94
|
+
* line layers have published their observations.
|
|
95
|
+
*/
|
|
96
|
+
scan(viewport) {
|
|
97
|
+
if (this.xMode === "time") {
|
|
98
|
+
const latestT = viewport.latestT;
|
|
99
|
+
this.bounds.xMin = latestT - this.timeWindowMs;
|
|
100
|
+
this.bounds.xMax = latestT;
|
|
101
|
+
}
|
|
102
|
+
if (this.applyToViewport) {
|
|
103
|
+
viewport.setBounds(this.bounds);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
draw(ctx, viewport) {
|
|
107
|
+
if (this.yMode === "auto") {
|
|
108
|
+
let yMin = viewport.observedYMin;
|
|
109
|
+
let yMax = viewport.observedYMax;
|
|
110
|
+
if (!Number.isFinite(yMin) || !Number.isFinite(yMax)) {
|
|
111
|
+
yMin = this.bounds.yMin;
|
|
112
|
+
yMax = this.bounds.yMax;
|
|
113
|
+
if (yMin === yMax) {
|
|
114
|
+
yMin = -1;
|
|
115
|
+
yMax = 1;
|
|
116
|
+
}
|
|
117
|
+
} else if (yMin === yMax) {
|
|
118
|
+
yMin -= 0.5;
|
|
119
|
+
yMax += 0.5;
|
|
120
|
+
} else {
|
|
121
|
+
const pad = (yMax - yMin) * this.yAutoPadding;
|
|
122
|
+
yMin -= pad;
|
|
123
|
+
yMax += pad;
|
|
124
|
+
}
|
|
125
|
+
if (this.yAutoMin !== void 0 && yMin < this.yAutoMin) yMin = this.yAutoMin;
|
|
126
|
+
if (this.yAutoMax !== void 0 && yMax > this.yAutoMax) yMax = this.yAutoMax;
|
|
127
|
+
this.bounds.yMin = yMin;
|
|
128
|
+
this.bounds.yMax = yMax;
|
|
129
|
+
if (this.applyToViewport) viewport.setBounds(this.bounds);
|
|
130
|
+
}
|
|
131
|
+
const { widthPx: w, heightPx: h } = viewport;
|
|
132
|
+
const xTicks = niceTicks(this.bounds.xMin, this.bounds.xMax, this.targetTicks);
|
|
133
|
+
const yTicks = niceTicks(this.bounds.yMin, this.bounds.yMax, this.targetTicks);
|
|
134
|
+
if (this.showXGrid || this.showYGrid) {
|
|
135
|
+
ctx.strokeStyle = this.gridColor;
|
|
136
|
+
ctx.lineWidth = 1;
|
|
137
|
+
ctx.beginPath();
|
|
138
|
+
if (this.showXGrid) {
|
|
139
|
+
for (let i = 0; i < xTicks.length; i++) {
|
|
140
|
+
const x = Math.round(viewport.xToPx(xTicks[i])) + 0.5;
|
|
141
|
+
ctx.moveTo(x, 0);
|
|
142
|
+
ctx.lineTo(x, h);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (this.showYGrid) {
|
|
146
|
+
for (let i = 0; i < yTicks.length; i++) {
|
|
147
|
+
const y = Math.round(viewport.yToPx(yTicks[i])) + 0.5;
|
|
148
|
+
ctx.moveTo(0, y);
|
|
149
|
+
ctx.lineTo(w, y);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
ctx.stroke();
|
|
153
|
+
}
|
|
154
|
+
if (this.showAxes) {
|
|
155
|
+
ctx.strokeStyle = this.axisColor;
|
|
156
|
+
ctx.beginPath();
|
|
157
|
+
if (this.bounds.xMin < 0 && this.bounds.xMax > 0) {
|
|
158
|
+
const x0 = Math.round(viewport.xToPx(0)) + 0.5;
|
|
159
|
+
ctx.moveTo(x0, 0);
|
|
160
|
+
ctx.lineTo(x0, h);
|
|
161
|
+
}
|
|
162
|
+
if (this.bounds.yMin < 0 && this.bounds.yMax > 0) {
|
|
163
|
+
const y0 = Math.round(viewport.yToPx(0)) + 0.5;
|
|
164
|
+
ctx.moveTo(0, y0);
|
|
165
|
+
ctx.lineTo(w, y0);
|
|
166
|
+
}
|
|
167
|
+
ctx.stroke();
|
|
168
|
+
}
|
|
169
|
+
if (this.showXLabels || this.showYLabels) {
|
|
170
|
+
ctx.fillStyle = this.labelColor;
|
|
171
|
+
ctx.font = this.font;
|
|
172
|
+
if (this.showXLabels) {
|
|
173
|
+
ctx.textBaseline = "top";
|
|
174
|
+
for (let i = 0; i < xTicks.length; i++) {
|
|
175
|
+
const x = viewport.xToPx(xTicks[i]);
|
|
176
|
+
ctx.fillText(
|
|
177
|
+
formatTick(xTicks[i], this.xMode, this.timeOrigin, this.xTickFormat),
|
|
178
|
+
x + 2,
|
|
179
|
+
h - 12
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (this.showYLabels) {
|
|
184
|
+
ctx.textBaseline = "middle";
|
|
185
|
+
for (let i = 0; i < yTicks.length; i++) {
|
|
186
|
+
const y = viewport.yToPx(yTicks[i]);
|
|
187
|
+
ctx.fillText(String(yTicks[i]), 2, y - 6);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
dispose() {
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
function formatTick(value, mode, timeOrigin, pattern) {
|
|
196
|
+
if (mode === "time") {
|
|
197
|
+
if (timeOrigin != null) {
|
|
198
|
+
return formatClock(timeOrigin + value, pattern);
|
|
199
|
+
}
|
|
200
|
+
const s = value / 1e3;
|
|
201
|
+
return `${s.toFixed(1)}s`;
|
|
202
|
+
}
|
|
203
|
+
return String(value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/entities/layer-stack/model/layer-stack.ts
|
|
207
|
+
var LayerStack = class {
|
|
208
|
+
layers = [];
|
|
209
|
+
byId = /* @__PURE__ */ new Map();
|
|
210
|
+
add(layer) {
|
|
211
|
+
this.layers.push(layer);
|
|
212
|
+
this.byId.set(layer.id, layer);
|
|
213
|
+
}
|
|
214
|
+
remove(id) {
|
|
215
|
+
const layer = this.byId.get(id);
|
|
216
|
+
if (!layer) return;
|
|
217
|
+
this.byId.delete(id);
|
|
218
|
+
const i = this.layers.indexOf(layer);
|
|
219
|
+
if (i >= 0) this.layers.splice(i, 1);
|
|
220
|
+
layer.dispose();
|
|
221
|
+
}
|
|
222
|
+
get(id) {
|
|
223
|
+
return this.byId.get(id);
|
|
224
|
+
}
|
|
225
|
+
resizeAll(viewport) {
|
|
226
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
227
|
+
this.layers[i].resize(viewport);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Pre-draw pass. Layers that implement `scan` update shared viewport state
|
|
232
|
+
* (bounds, observed extents) here so downstream layers' `draw` sees the
|
|
233
|
+
* correct values. Iterates in insertion order so axis-grid (added first)
|
|
234
|
+
* writes bounds before data layers read them.
|
|
235
|
+
*/
|
|
236
|
+
scanAll(viewport) {
|
|
237
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
238
|
+
this.layers[i].scan?.(viewport);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
drawAll(ctx, viewport) {
|
|
242
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
243
|
+
this.layers[i].draw(ctx, viewport);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
disposeAll() {
|
|
247
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
248
|
+
this.layers[i].dispose();
|
|
249
|
+
}
|
|
250
|
+
this.layers.length = 0;
|
|
251
|
+
this.byId.clear();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// src/shared/lib/color.ts
|
|
256
|
+
var LUT_SIZE = 256;
|
|
257
|
+
var lutR = new Uint8ClampedArray(LUT_SIZE);
|
|
258
|
+
var lutG = new Uint8ClampedArray(LUT_SIZE);
|
|
259
|
+
var lutB = new Uint8ClampedArray(LUT_SIZE);
|
|
260
|
+
function ramp(t) {
|
|
261
|
+
const stops = [
|
|
262
|
+
[0, [13, 8, 135]],
|
|
263
|
+
[0.25, [84, 2, 163]],
|
|
264
|
+
[0.5, [156, 23, 158]],
|
|
265
|
+
[0.75, [225, 100, 98]],
|
|
266
|
+
[1, [240, 249, 33]]
|
|
267
|
+
];
|
|
268
|
+
for (let i = 0; i < stops.length - 1; i++) {
|
|
269
|
+
const [t0, c0] = stops[i];
|
|
270
|
+
const [t1, c1] = stops[i + 1];
|
|
271
|
+
if (t <= t1) {
|
|
272
|
+
const k = (t - t0) / (t1 - t0);
|
|
273
|
+
return [
|
|
274
|
+
c0[0] + (c1[0] - c0[0]) * k,
|
|
275
|
+
c0[1] + (c1[1] - c0[1]) * k,
|
|
276
|
+
c0[2] + (c1[2] - c0[2]) * k
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return stops[stops.length - 1][1];
|
|
281
|
+
}
|
|
282
|
+
for (let i = 0; i < LUT_SIZE; i++) {
|
|
283
|
+
const [r, g, b] = ramp(i / (LUT_SIZE - 1));
|
|
284
|
+
lutR[i] = r;
|
|
285
|
+
lutG[i] = g;
|
|
286
|
+
lutB[i] = b;
|
|
287
|
+
}
|
|
288
|
+
var SINGLETON = Object.freeze({
|
|
289
|
+
r: lutR,
|
|
290
|
+
g: lutG,
|
|
291
|
+
b: lutB,
|
|
292
|
+
size: LUT_SIZE
|
|
293
|
+
});
|
|
294
|
+
function intensityLUT() {
|
|
295
|
+
return SINGLETON;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/entities/lidar-scatter-layer/model/lidar-scatter-layer.ts
|
|
299
|
+
var LUT_BUCKETS = 256;
|
|
300
|
+
var LidarScatterLayer = class {
|
|
301
|
+
id;
|
|
302
|
+
stride = 4;
|
|
303
|
+
pointSize = 2;
|
|
304
|
+
intensityMax = 1;
|
|
305
|
+
solidColor = null;
|
|
306
|
+
data = null;
|
|
307
|
+
length = 0;
|
|
308
|
+
// Scratch buffers for counting-sort batching.
|
|
309
|
+
sortedX = new Float32Array(0);
|
|
310
|
+
sortedY = new Float32Array(0);
|
|
311
|
+
bucketCount = new Uint32Array(LUT_BUCKETS);
|
|
312
|
+
bucketOffset = new Uint32Array(LUT_BUCKETS);
|
|
313
|
+
scratchCapacity = 0;
|
|
314
|
+
constructor(id) {
|
|
315
|
+
this.id = id;
|
|
316
|
+
}
|
|
317
|
+
setConfig(config) {
|
|
318
|
+
const c = config;
|
|
319
|
+
if (c.stride !== void 0) this.stride = Math.max(2, c.stride | 0);
|
|
320
|
+
if (c.pointSize !== void 0) this.pointSize = Math.max(1, c.pointSize);
|
|
321
|
+
if (c.intensityMax !== void 0) this.intensityMax = c.intensityMax;
|
|
322
|
+
if (c.color !== void 0) this.solidColor = parseColor(c.color);
|
|
323
|
+
}
|
|
324
|
+
setData(buffer, length, _viewport) {
|
|
325
|
+
this.data = new Float32Array(buffer, 0, length);
|
|
326
|
+
this.length = length;
|
|
327
|
+
}
|
|
328
|
+
resize(_viewport) {
|
|
329
|
+
}
|
|
330
|
+
draw(ctx, viewport) {
|
|
331
|
+
const data = this.data;
|
|
332
|
+
if (!data || this.length < this.stride) return;
|
|
333
|
+
const stride = this.stride;
|
|
334
|
+
const size = this.pointSize;
|
|
335
|
+
const half = size / 2;
|
|
336
|
+
const count = this.length / stride | 0;
|
|
337
|
+
if (this.solidColor) {
|
|
338
|
+
this.drawSolid(ctx, viewport, data, stride, count, size, half);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.ensureScratch(count);
|
|
342
|
+
this.drawBucketed(ctx, viewport, data, stride, count, size, half);
|
|
343
|
+
}
|
|
344
|
+
drawSolid(ctx, viewport, data, stride, count, size, half) {
|
|
345
|
+
const [r, g, b] = this.solidColor;
|
|
346
|
+
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
347
|
+
ctx.beginPath();
|
|
348
|
+
for (let i = 0; i < count; i++) {
|
|
349
|
+
const o = i * stride;
|
|
350
|
+
const px = viewport.xToPx(data[o]);
|
|
351
|
+
const py = viewport.yToPx(data[o + 1]);
|
|
352
|
+
ctx.rect(px - half, py - half, size, size);
|
|
353
|
+
}
|
|
354
|
+
ctx.fill();
|
|
355
|
+
}
|
|
356
|
+
drawBucketed(ctx, viewport, data, stride, count, size, half) {
|
|
357
|
+
const lut = intensityLUT();
|
|
358
|
+
const invMax = 1 / (this.intensityMax || 1);
|
|
359
|
+
const sortedX = this.sortedX;
|
|
360
|
+
const sortedY = this.sortedY;
|
|
361
|
+
const bucketCount = this.bucketCount;
|
|
362
|
+
const bucketOffset = this.bucketOffset;
|
|
363
|
+
bucketCount.fill(0);
|
|
364
|
+
for (let i = 0; i < count; i++) {
|
|
365
|
+
const o = i * stride;
|
|
366
|
+
const intensity = stride >= 4 ? data[o + 3] : 1;
|
|
367
|
+
let idx = intensity * invMax * (LUT_BUCKETS - 1) | 0;
|
|
368
|
+
if (idx < 0) idx = 0;
|
|
369
|
+
else if (idx >= LUT_BUCKETS) idx = LUT_BUCKETS - 1;
|
|
370
|
+
bucketCount[idx]++;
|
|
371
|
+
}
|
|
372
|
+
let acc = 0;
|
|
373
|
+
for (let b = 0; b < LUT_BUCKETS; b++) {
|
|
374
|
+
bucketOffset[b] = acc;
|
|
375
|
+
acc += bucketCount[b];
|
|
376
|
+
}
|
|
377
|
+
const writeCursor = bucketCount;
|
|
378
|
+
for (let b = 0; b < LUT_BUCKETS; b++) writeCursor[b] = 0;
|
|
379
|
+
for (let i = 0; i < count; i++) {
|
|
380
|
+
const o = i * stride;
|
|
381
|
+
const intensity = stride >= 4 ? data[o + 3] : 1;
|
|
382
|
+
let idx = intensity * invMax * (LUT_BUCKETS - 1) | 0;
|
|
383
|
+
if (idx < 0) idx = 0;
|
|
384
|
+
else if (idx >= LUT_BUCKETS) idx = LUT_BUCKETS - 1;
|
|
385
|
+
const pos = bucketOffset[idx] + writeCursor[idx];
|
|
386
|
+
writeCursor[idx]++;
|
|
387
|
+
sortedX[pos] = viewport.xToPx(data[o]);
|
|
388
|
+
sortedY[pos] = viewport.yToPx(data[o + 1]);
|
|
389
|
+
}
|
|
390
|
+
for (let b = 0; b < LUT_BUCKETS; b++) {
|
|
391
|
+
const n = writeCursor[b];
|
|
392
|
+
if (n === 0) continue;
|
|
393
|
+
ctx.fillStyle = `rgb(${lut.r[b]},${lut.g[b]},${lut.b[b]})`;
|
|
394
|
+
ctx.beginPath();
|
|
395
|
+
const start = bucketOffset[b];
|
|
396
|
+
const end = start + n;
|
|
397
|
+
for (let i = start; i < end; i++) {
|
|
398
|
+
ctx.rect(sortedX[i] - half, sortedY[i] - half, size, size);
|
|
399
|
+
}
|
|
400
|
+
ctx.fill();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
ensureScratch(count) {
|
|
404
|
+
if (count <= this.scratchCapacity) return;
|
|
405
|
+
const next = Math.max(count, Math.ceil(this.scratchCapacity * 1.25), 1024);
|
|
406
|
+
this.sortedX = new Float32Array(next);
|
|
407
|
+
this.sortedY = new Float32Array(next);
|
|
408
|
+
this.scratchCapacity = next;
|
|
409
|
+
}
|
|
410
|
+
dispose() {
|
|
411
|
+
this.data = null;
|
|
412
|
+
this.sortedX = new Float32Array(0);
|
|
413
|
+
this.sortedY = new Float32Array(0);
|
|
414
|
+
this.scratchCapacity = 0;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
function parseColor(css) {
|
|
418
|
+
if (css.startsWith("#")) {
|
|
419
|
+
const hex = css.slice(1);
|
|
420
|
+
if (hex.length === 3) {
|
|
421
|
+
return [
|
|
422
|
+
parseInt(hex[0] + hex[0], 16),
|
|
423
|
+
parseInt(hex[1] + hex[1], 16),
|
|
424
|
+
parseInt(hex[2] + hex[2], 16)
|
|
425
|
+
];
|
|
426
|
+
}
|
|
427
|
+
if (hex.length === 6) {
|
|
428
|
+
return [
|
|
429
|
+
parseInt(hex.slice(0, 2), 16),
|
|
430
|
+
parseInt(hex.slice(2, 4), 16),
|
|
431
|
+
parseInt(hex.slice(4, 6), 16)
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return [255, 255, 255];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/shared/model/ring-buffer.ts
|
|
439
|
+
var RingBuffer = class {
|
|
440
|
+
stride;
|
|
441
|
+
capacity;
|
|
442
|
+
data;
|
|
443
|
+
head = 0;
|
|
444
|
+
count = 0;
|
|
445
|
+
constructor(capacity, stride) {
|
|
446
|
+
this.capacity = capacity;
|
|
447
|
+
this.stride = stride;
|
|
448
|
+
this.data = new Float32Array(capacity * stride);
|
|
449
|
+
}
|
|
450
|
+
get length() {
|
|
451
|
+
return this.count;
|
|
452
|
+
}
|
|
453
|
+
push(record) {
|
|
454
|
+
const base = this.head * this.stride;
|
|
455
|
+
for (let i = 0; i < this.stride; i++) {
|
|
456
|
+
this.data[base + i] = record[i];
|
|
457
|
+
}
|
|
458
|
+
this.head = (this.head + 1) % this.capacity;
|
|
459
|
+
if (this.count < this.capacity) this.count++;
|
|
460
|
+
}
|
|
461
|
+
pushMany(records) {
|
|
462
|
+
const recCount = records.length / this.stride;
|
|
463
|
+
for (let r = 0; r < recCount; r++) {
|
|
464
|
+
const base = this.head * this.stride;
|
|
465
|
+
const src = r * this.stride;
|
|
466
|
+
for (let i = 0; i < this.stride; i++) {
|
|
467
|
+
this.data[base + i] = records[src + i];
|
|
468
|
+
}
|
|
469
|
+
this.head = (this.head + 1) % this.capacity;
|
|
470
|
+
if (this.count < this.capacity) this.count++;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
forEach(fn) {
|
|
474
|
+
const start = this.count < this.capacity ? 0 : this.head;
|
|
475
|
+
for (let i = 0; i < this.count; i++) {
|
|
476
|
+
const slot = (start + i) % this.capacity;
|
|
477
|
+
fn(this.data, slot * this.stride, i);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
clear() {
|
|
481
|
+
this.head = 0;
|
|
482
|
+
this.count = 0;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// src/entities/line-chart-layer/model/line-chart-layer.ts
|
|
487
|
+
var LineChartLayer = class {
|
|
488
|
+
id;
|
|
489
|
+
color = "#4fc3f7";
|
|
490
|
+
lineWidth = 1;
|
|
491
|
+
ring;
|
|
492
|
+
constructor(id) {
|
|
493
|
+
this.id = id;
|
|
494
|
+
this.ring = new RingBuffer(2048, 2);
|
|
495
|
+
}
|
|
496
|
+
setConfig(config) {
|
|
497
|
+
const c = config;
|
|
498
|
+
if (c.color !== void 0) this.color = c.color;
|
|
499
|
+
if (c.lineWidth !== void 0) this.lineWidth = c.lineWidth;
|
|
500
|
+
if (c.capacity !== void 0 && c.capacity !== this.ring.capacity) {
|
|
501
|
+
this.ring = new RingBuffer(c.capacity, 2);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
setData(buffer, length, viewport) {
|
|
505
|
+
if (length < 2) return;
|
|
506
|
+
const arr = new Float32Array(buffer, 0, length);
|
|
507
|
+
this.ring.pushMany(arr);
|
|
508
|
+
const t = arr[length - 2];
|
|
509
|
+
if (t > viewport.latestT) viewport.latestT = t;
|
|
510
|
+
}
|
|
511
|
+
resize(_viewport) {
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Pre-draw pass: compute the visible-window min/max of y values in this
|
|
515
|
+
* layer's ring buffer and merge them into `viewport.observedYMin/Max`.
|
|
516
|
+
* AxisGridLayer with `yMode: "auto"` reads the aggregate in draw.
|
|
517
|
+
*
|
|
518
|
+
* `viewport.bounds.xMin` was already written by AxisGridLayer.scan (which
|
|
519
|
+
* runs earlier in insertion order), so we can filter stale samples here.
|
|
520
|
+
*/
|
|
521
|
+
scan(viewport) {
|
|
522
|
+
if (this.ring.length === 0) return;
|
|
523
|
+
const xMin = viewport.bounds.xMin;
|
|
524
|
+
let localMin = viewport.observedYMin;
|
|
525
|
+
let localMax = viewport.observedYMax;
|
|
526
|
+
this.ring.forEach((data, off) => {
|
|
527
|
+
const t = data[off];
|
|
528
|
+
if (t < xMin) return;
|
|
529
|
+
const y = data[off + 1];
|
|
530
|
+
if (y < localMin) localMin = y;
|
|
531
|
+
if (y > localMax) localMax = y;
|
|
532
|
+
});
|
|
533
|
+
viewport.observedYMin = localMin;
|
|
534
|
+
viewport.observedYMax = localMax;
|
|
535
|
+
}
|
|
536
|
+
draw(ctx, viewport) {
|
|
537
|
+
if (this.ring.length < 2) return;
|
|
538
|
+
ctx.strokeStyle = this.color;
|
|
539
|
+
ctx.lineWidth = this.lineWidth;
|
|
540
|
+
ctx.beginPath();
|
|
541
|
+
const xMin = viewport.bounds.xMin;
|
|
542
|
+
let first = true;
|
|
543
|
+
this.ring.forEach((data, off) => {
|
|
544
|
+
const t = data[off];
|
|
545
|
+
if (t < xMin) return;
|
|
546
|
+
const px = viewport.xToPx(t);
|
|
547
|
+
const py = viewport.yToPx(data[off + 1]);
|
|
548
|
+
if (first) {
|
|
549
|
+
ctx.moveTo(px, py);
|
|
550
|
+
first = false;
|
|
551
|
+
} else {
|
|
552
|
+
ctx.lineTo(px, py);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
ctx.stroke();
|
|
556
|
+
}
|
|
557
|
+
dispose() {
|
|
558
|
+
this.ring.clear();
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// src/entities/line-chart-static-layer/model/line-chart-static-layer.ts
|
|
563
|
+
var LineChartStaticLayer = class {
|
|
564
|
+
id;
|
|
565
|
+
color = "#4fc3f7";
|
|
566
|
+
lineWidth = 1;
|
|
567
|
+
layout = "xy";
|
|
568
|
+
data = null;
|
|
569
|
+
length = 0;
|
|
570
|
+
constructor(id) {
|
|
571
|
+
this.id = id;
|
|
572
|
+
}
|
|
573
|
+
setConfig(config) {
|
|
574
|
+
const c = config;
|
|
575
|
+
if (c.color !== void 0) this.color = c.color;
|
|
576
|
+
if (c.lineWidth !== void 0) this.lineWidth = c.lineWidth;
|
|
577
|
+
if (c.layout !== void 0) this.layout = c.layout;
|
|
578
|
+
}
|
|
579
|
+
setData(buffer, length, _viewport) {
|
|
580
|
+
this.data = new Float32Array(buffer, 0, length);
|
|
581
|
+
this.length = length;
|
|
582
|
+
}
|
|
583
|
+
resize(_viewport) {
|
|
584
|
+
}
|
|
585
|
+
draw(ctx, viewport) {
|
|
586
|
+
const data = this.data;
|
|
587
|
+
if (!data || this.length < 2) return;
|
|
588
|
+
ctx.strokeStyle = this.color;
|
|
589
|
+
ctx.lineWidth = this.lineWidth;
|
|
590
|
+
ctx.beginPath();
|
|
591
|
+
if (this.layout === "xy") {
|
|
592
|
+
const n = this.length >> 1;
|
|
593
|
+
if (n < 2) return;
|
|
594
|
+
ctx.moveTo(viewport.xToPx(data[0]), viewport.yToPx(data[1]));
|
|
595
|
+
for (let i = 1; i < n; i++) {
|
|
596
|
+
const j = i * 2;
|
|
597
|
+
ctx.lineTo(viewport.xToPx(data[j]), viewport.yToPx(data[j + 1]));
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
const n = this.length;
|
|
601
|
+
const xMin = viewport.bounds.xMin;
|
|
602
|
+
const xMax = viewport.bounds.xMax;
|
|
603
|
+
const step = (xMax - xMin) / Math.max(1, n - 1);
|
|
604
|
+
ctx.moveTo(viewport.xToPx(xMin), viewport.yToPx(data[0]));
|
|
605
|
+
for (let i = 1; i < n; i++) {
|
|
606
|
+
ctx.lineTo(viewport.xToPx(xMin + i * step), viewport.yToPx(data[i]));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
ctx.stroke();
|
|
610
|
+
}
|
|
611
|
+
dispose() {
|
|
612
|
+
this.data = null;
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// src/shared/model/scheduler.ts
|
|
617
|
+
var Scheduler = class {
|
|
618
|
+
dirty = false;
|
|
619
|
+
running = false;
|
|
620
|
+
raf = null;
|
|
621
|
+
tick;
|
|
622
|
+
constructor(tick) {
|
|
623
|
+
this.tick = tick;
|
|
624
|
+
}
|
|
625
|
+
start() {
|
|
626
|
+
if (this.running) return;
|
|
627
|
+
this.running = true;
|
|
628
|
+
this.loop();
|
|
629
|
+
}
|
|
630
|
+
stop() {
|
|
631
|
+
this.running = false;
|
|
632
|
+
if (this.raf != null) {
|
|
633
|
+
if (typeof cancelAnimationFrame !== "undefined") {
|
|
634
|
+
cancelAnimationFrame(this.raf);
|
|
635
|
+
} else {
|
|
636
|
+
clearTimeout(this.raf);
|
|
637
|
+
}
|
|
638
|
+
this.raf = null;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
markDirty() {
|
|
642
|
+
this.dirty = true;
|
|
643
|
+
}
|
|
644
|
+
loop = () => {
|
|
645
|
+
if (!this.running) return;
|
|
646
|
+
if (this.dirty) {
|
|
647
|
+
this.dirty = false;
|
|
648
|
+
this.tick();
|
|
649
|
+
}
|
|
650
|
+
if (typeof requestAnimationFrame !== "undefined") {
|
|
651
|
+
this.raf = requestAnimationFrame(this.loop);
|
|
652
|
+
} else {
|
|
653
|
+
this.raf = setTimeout(this.loop, 16);
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// src/shared/model/viewport.ts
|
|
659
|
+
var Viewport = class {
|
|
660
|
+
widthPx = 0;
|
|
661
|
+
heightPx = 0;
|
|
662
|
+
dpr = 1;
|
|
663
|
+
bounds = { xMin: -1, xMax: 1, yMin: -1, yMax: 1 };
|
|
664
|
+
/**
|
|
665
|
+
* Most recent data timestamp (ms, host-relative) seen across all streaming
|
|
666
|
+
* layers. Streaming `LineChartLayer` updates this on setData; `AxisGridLayer`
|
|
667
|
+
* in time mode uses it to compute a sliding window.
|
|
668
|
+
*/
|
|
669
|
+
latestT = 0;
|
|
670
|
+
/**
|
|
671
|
+
* Per-frame aggregate of observed y values across all data layers that
|
|
672
|
+
* currently overlap the visible time window. `AxisGridLayer` in
|
|
673
|
+
* `yMode: "auto"` reads these in draw to compute bounds.yMin/yMax.
|
|
674
|
+
*
|
|
675
|
+
* Initialised to +/-Infinity by `beginScan()` at the top of every frame,
|
|
676
|
+
* then layers merge their visible-window min/max in via `scan()`.
|
|
677
|
+
*/
|
|
678
|
+
observedYMin = Number.POSITIVE_INFINITY;
|
|
679
|
+
observedYMax = Number.NEGATIVE_INFINITY;
|
|
680
|
+
setSize(width, height, dpr) {
|
|
681
|
+
this.widthPx = width;
|
|
682
|
+
this.heightPx = height;
|
|
683
|
+
this.dpr = dpr;
|
|
684
|
+
}
|
|
685
|
+
setBounds(b) {
|
|
686
|
+
this.bounds = b;
|
|
687
|
+
}
|
|
688
|
+
/** Called by Engine at the start of each render frame before scan pass. */
|
|
689
|
+
beginScan() {
|
|
690
|
+
this.observedYMin = Number.POSITIVE_INFINITY;
|
|
691
|
+
this.observedYMax = Number.NEGATIVE_INFINITY;
|
|
692
|
+
}
|
|
693
|
+
xToPx(x) {
|
|
694
|
+
const { xMin, xMax } = this.bounds;
|
|
695
|
+
return (x - xMin) / (xMax - xMin) * this.widthPx;
|
|
696
|
+
}
|
|
697
|
+
yToPx(y) {
|
|
698
|
+
const { yMin, yMax } = this.bounds;
|
|
699
|
+
return this.heightPx - (y - yMin) / (yMax - yMin) * this.heightPx;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// src/features/engine/model/engine.ts
|
|
704
|
+
function createLayer(id, kind) {
|
|
705
|
+
switch (kind) {
|
|
706
|
+
case "line":
|
|
707
|
+
return new LineChartLayer(id);
|
|
708
|
+
case "line-static":
|
|
709
|
+
return new LineChartStaticLayer(id);
|
|
710
|
+
case "lidar":
|
|
711
|
+
return new LidarScatterLayer(id);
|
|
712
|
+
case "axis-grid":
|
|
713
|
+
return new AxisGridLayer(id);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
var Engine = class {
|
|
717
|
+
canvas = null;
|
|
718
|
+
ctx = null;
|
|
719
|
+
viewport = new Viewport();
|
|
720
|
+
stack = new LayerStack();
|
|
721
|
+
scheduler;
|
|
722
|
+
bgColor = "#0b0d12";
|
|
723
|
+
constructor() {
|
|
724
|
+
this.scheduler = new Scheduler(() => this.render());
|
|
725
|
+
}
|
|
726
|
+
dispatch(msg) {
|
|
727
|
+
switch (msg.op) {
|
|
728
|
+
case Op.INIT:
|
|
729
|
+
this.init(msg.canvas, msg.width, msg.height, msg.dpr);
|
|
730
|
+
break;
|
|
731
|
+
case Op.RESIZE:
|
|
732
|
+
this.resize(msg.width, msg.height, msg.dpr);
|
|
733
|
+
break;
|
|
734
|
+
case Op.ADD_LAYER: {
|
|
735
|
+
const layer = createLayer(msg.id, msg.kind);
|
|
736
|
+
if (msg.config !== void 0) layer.setConfig(msg.config);
|
|
737
|
+
layer.resize(this.viewport);
|
|
738
|
+
this.stack.add(layer);
|
|
739
|
+
this.scheduler.markDirty();
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case Op.REMOVE_LAYER:
|
|
743
|
+
this.stack.remove(msg.id);
|
|
744
|
+
this.scheduler.markDirty();
|
|
745
|
+
break;
|
|
746
|
+
case Op.CONFIG: {
|
|
747
|
+
const layer = this.stack.get(msg.id);
|
|
748
|
+
if (layer) {
|
|
749
|
+
layer.setConfig(msg.config);
|
|
750
|
+
this.scheduler.markDirty();
|
|
751
|
+
}
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case Op.DATA: {
|
|
755
|
+
const layer = this.stack.get(msg.id);
|
|
756
|
+
if (layer) {
|
|
757
|
+
layer.setData(msg.buffer, msg.length, this.viewport);
|
|
758
|
+
this.scheduler.markDirty();
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
case Op.DISPOSE:
|
|
763
|
+
this.dispose();
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
init(canvas, width, height, dpr) {
|
|
768
|
+
this.canvas = canvas;
|
|
769
|
+
this.ctx = canvas.getContext("2d");
|
|
770
|
+
this.resize(width, height, dpr);
|
|
771
|
+
this.scheduler.start();
|
|
772
|
+
this.scheduler.markDirty();
|
|
773
|
+
}
|
|
774
|
+
resize(width, height, dpr) {
|
|
775
|
+
if (!this.canvas) return;
|
|
776
|
+
this.canvas.width = Math.max(1, Math.round(width * dpr));
|
|
777
|
+
this.canvas.height = Math.max(1, Math.round(height * dpr));
|
|
778
|
+
this.viewport.setSize(width, height, dpr);
|
|
779
|
+
this.stack.resizeAll(this.viewport);
|
|
780
|
+
this.scheduler.markDirty();
|
|
781
|
+
}
|
|
782
|
+
render() {
|
|
783
|
+
const ctx = this.ctx;
|
|
784
|
+
if (!ctx || !this.canvas) return;
|
|
785
|
+
this.viewport.beginScan();
|
|
786
|
+
this.stack.scanAll(this.viewport);
|
|
787
|
+
const { dpr } = this.viewport;
|
|
788
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
789
|
+
ctx.fillStyle = this.bgColor;
|
|
790
|
+
ctx.fillRect(0, 0, this.viewport.widthPx, this.viewport.heightPx);
|
|
791
|
+
this.stack.drawAll(ctx, this.viewport);
|
|
792
|
+
}
|
|
793
|
+
dispose() {
|
|
794
|
+
this.scheduler.stop();
|
|
795
|
+
this.stack.disposeAll();
|
|
796
|
+
this.canvas = null;
|
|
797
|
+
this.ctx = null;
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// src/app/worker/fluxion-worker.ts
|
|
802
|
+
var engine = new Engine();
|
|
803
|
+
self.onmessage = (e) => {
|
|
804
|
+
try {
|
|
805
|
+
engine.dispatch(e.data);
|
|
806
|
+
} catch (err) {
|
|
807
|
+
console.error("[fluxion-worker] dispatch error:", err);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
self.addEventListener("error", (e) => {
|
|
811
|
+
console.error("[fluxion-worker] uncaught error:", e.message ?? e);
|
|
812
|
+
});
|
|
813
|
+
self.addEventListener("messageerror", (e) => {
|
|
814
|
+
console.error("[fluxion-worker] message deserialization failed:", e);
|
|
815
|
+
});
|
|
816
|
+
//# sourceMappingURL=fluxion-worker.js.map
|