multi-gauge 0.1.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/LICENSE +21 -0
- package/README.md +124 -0
- package/docs/multigauge-demo.png +0 -0
- package/package.json +29 -0
- package/src/MultiGauge.js +358 -0
- package/src/assets/fonts/InterVariable.woff2 +0 -0
- package/src/assets/fonts/LICENSE.txt +92 -0
- package/src/errors.js +7 -0
- package/src/gpu/GpuRuntime.js +153 -0
- package/src/gpu/IconCache.js +69 -0
- package/src/gpu/Renderer.js +370 -0
- package/src/gpu/SceneBuilder.js +736 -0
- package/src/gpu/TextAtlas.js +304 -0
- package/src/gpu/shaders.js +212 -0
- package/src/index.js +2 -0
- package/src/interaction/GridEditor.js +446 -0
- package/src/layout/CellLayout.js +167 -0
- package/src/layout/GridLayout.js +405 -0
- package/src/layout/PanelLayout.js +70 -0
- package/src/model/GaugeModel.js +196 -0
- package/src/theme.js +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MultiGauge contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# MultiGauge
|
|
2
|
+
|
|
3
|
+
> A tiny, fast WebGPU instrument panel for realtime signals.
|
|
4
|
+
|
|
5
|
+
MultiGauge renders modern, dark telemetry panels directly with WebGPU. It is plain JavaScript ESM, has no runtime dependencies, draws only when something changes, and does not care where values come from.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { MultiGauge } from 'multi-gauge';
|
|
11
|
+
|
|
12
|
+
const panel = await MultiGauge.create(document.querySelector('canvas'), {
|
|
13
|
+
grid: { rows: 2, columns: 3, gap: 8 },
|
|
14
|
+
accent: '#00eaff',
|
|
15
|
+
gauges: [
|
|
16
|
+
{
|
|
17
|
+
id: 'speed',
|
|
18
|
+
type: 'arc',
|
|
19
|
+
label: 'SPEED',
|
|
20
|
+
min: 0,
|
|
21
|
+
max: 300,
|
|
22
|
+
unit: 'km/h',
|
|
23
|
+
bands: [
|
|
24
|
+
{ from: 0, to: 160, kind: 'normal' },
|
|
25
|
+
{ from: 240, to: 300, kind: 'critical' }
|
|
26
|
+
],
|
|
27
|
+
markers: [{ id: 'target', value: 145 }]
|
|
28
|
+
},
|
|
29
|
+
{ id: 'heading', type: 'compass', label: 'HDG' },
|
|
30
|
+
{ id: 'radar', type: 'status', label: 'RADAR' }
|
|
31
|
+
]
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
panel.update({ speed: 127, heading: 182, radar: true });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## What it renders
|
|
38
|
+
|
|
39
|
+
- `arc`: scalar dial with configurable range, bands and markers.
|
|
40
|
+
- `linear`: horizontal or vertical, with `fill`, `marker`, or `fill-marker` mode.
|
|
41
|
+
- `compass`: cyclic dial; values wrap correctly around zero.
|
|
42
|
+
- `status`: discrete boolean, number or string states with semantic colors.
|
|
43
|
+
|
|
44
|
+
Bands are shared concepts across continuous gauge families. Arc, linear and compass bands can include a `label`, shown near the midpoint of the colored range when its measured text fits. Radial band labels sit close outside the arc with their baseline rotated along the midpoint tangent; labeled targets use a second, farther radial tier to avoid collisions. Status gauges use `states[].label` instead. `normal`, `warning`, `critical`, `inactive`, `target`, and custom theme keys remain separate from the panel accent.
|
|
45
|
+
|
|
46
|
+
Moving value indicators and progress tracks use the panel accent. Static key markers use `target` by default—or their configured semantic `kind`—and cross the gauge track with the same short-stroke language in linear and radial gauges. Numeric readouts use the regular text color outside bands and adopt the semantic color of the active band; units remain neutral.
|
|
47
|
+
|
|
48
|
+
Radial scale divisions use muted rounded strokes pointing toward the dial center. They scale with the dial and disappear below a useful radius. Arc endpoints and the midpoint are emphasized with slightly longer major ticks; compass cardinal positions remain reserved for `N`, `E`, `S`, and `W`.
|
|
49
|
+
|
|
50
|
+
Responsive radial layout prioritizes the dial itself. Band and target labels reserve an outer tier only when the complete text fits and the remaining dial keeps a useful radius; otherwise the label disappears without an ellipsis while the marker remains visible.
|
|
51
|
+
|
|
52
|
+
## Grid and header
|
|
53
|
+
|
|
54
|
+
Cells use `row`, `col`, `rowSpan`, and `colSpan`. Omit `row` and `col` to use deterministic auto-placement. A header is independent from the grid:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const panel = await MultiGauge.create(canvas, {
|
|
58
|
+
grid: { rows: 3, columns: 3 },
|
|
59
|
+
header: {
|
|
60
|
+
icon: new URL('./vehicle.svg', import.meta.url),
|
|
61
|
+
title: 'VEHICLE 01',
|
|
62
|
+
subtitle: 'Prototype',
|
|
63
|
+
badge: 'ONLINE'
|
|
64
|
+
},
|
|
65
|
+
gauges: []
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
SVG, PNG and WebP header icons are decoded by browser APIs, uploaded once, and cached by URL in the shared GPU runtime. Text uses one shared GPU texture containing a small set of fixed Inter Variable raster styles. Smaller cells automatically reduce labels, ticks and secondary information.
|
|
70
|
+
|
|
71
|
+
## Runtime updates
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
panel.set('speed', 130);
|
|
75
|
+
panel.update({ speed: 132, heading: 184 });
|
|
76
|
+
panel.setMarker('speed', 'target', 150);
|
|
77
|
+
|
|
78
|
+
panel.add({ id: 'temperature', type: 'linear', min: -40, max: 150 });
|
|
79
|
+
panel.remove('temperature');
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Multiple updates in one browser frame produce one render. There is no permanent animation loop and no scheduler per gauge. Dynamic values are written through reusable staging memory; glyph buffers are rebuilt only when a formatted readout or its semantic color changes. Text measurements use a bounded atlas cache. All panels on a page share one `GPUDevice`, pipelines, glyph atlas and icon cache.
|
|
83
|
+
|
|
84
|
+
## Layout editing
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
panel.move('speed', 0, 1);
|
|
88
|
+
panel.resize('speed', 2, 2);
|
|
89
|
+
panel.maximize('speed');
|
|
90
|
+
panel.restoreGauge('speed');
|
|
91
|
+
panel.minimize('speed');
|
|
92
|
+
|
|
93
|
+
panel.setEditing(false); // optionally disable direct manipulation
|
|
94
|
+
panel.setEditing(true); // enable it again
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Direct manipulation is enabled by default: drag anywhere inside a gauge to move it, or drag from any edge or corner to resize it. There is no edit mode or visible resize handle. Move and resize run as transactions: an outline follows the pointer, a placeholder shows the resolved grid destination, and the committed layout changes only on pointer release. Press `Escape` to cancel. Exact compatible footprints swap; other collisions use deterministic push/reflow. Failed operations leave the previous layout intact. The occupancy map—not the editor DOM—is the source of truth.
|
|
98
|
+
|
|
99
|
+
Arc and compass gauges use their complete content region and place the numeric value and unit on separate centered lines inside the dial. Compass headings use a moving perimeter mark instead of a center needle, leaving the middle available for the heading value. Horizontal linear gauges keep their readout below the bar; vertical linear gauges place value and unit on one line to the right so the bar can use the full available height. Labels are measured against the glyph atlas, reduced through 9–12 px UI buckets, and truncated with an ellipsis only when necessary. The bundled Inter Variable font is loaded before atlas generation; glyph styles are rasterized at the active `devicePixelRatio`, use real font metrics and snap text quads to the physical pixel grid. Numeric values use stable tabular advances without introducing a second font family.
|
|
100
|
+
|
|
101
|
+
## Persistence and lifecycle
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
const json = JSON.stringify(panel.serialize());
|
|
105
|
+
panel.restore(JSON.parse(json));
|
|
106
|
+
|
|
107
|
+
console.log(panel.getStats());
|
|
108
|
+
panel.destroy();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Serialized state contains only public grid, layout, gauge, header and visual configuration. `destroy()` cancels pending work and releases canvas-specific GPU resources, observers, editor DOM and icon references; shared resources remain available to other panels.
|
|
112
|
+
|
|
113
|
+
WebGPU is required. MultiGauge fails with a clear `MultiGaugeError` rather than silently switching renderer.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
npm test
|
|
119
|
+
npm run demo
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Open `http://localhost:8080/demo/` for two simultaneous panels or `/benchmark/` for 1, 16, 64 and 100-gauge workloads at 10, 30 and 60 Hz. The benchmark also compares sixteen 1×1 panels with one 4×4 panel.
|
|
123
|
+
|
|
124
|
+
MultiGauge is source-agnostic: call `set()` or `update()` from WebSocket, SSE, sensors, shared memory, audio, simulations, or manual controls. Transport adapters belong outside this core package.
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "multi-gauge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A tiny, fast WebGPU instrument panel for realtime signals.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"docs/multigauge-demo.png",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test",
|
|
17
|
+
"demo": "python3 -m http.server 8080 --bind localhost"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"webgpu",
|
|
21
|
+
"gauge",
|
|
22
|
+
"telemetry",
|
|
23
|
+
"hmi"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { MultiGaugeError } from './errors.js';
|
|
2
|
+
import { GridLayout } from './layout/GridLayout.js';
|
|
3
|
+
import { layoutPanel } from './layout/PanelLayout.js';
|
|
4
|
+
import { coerceValue, normalizeGauge, serializeGauge, setGaugeMarker } from './model/GaugeModel.js';
|
|
5
|
+
import { Renderer } from './gpu/Renderer.js';
|
|
6
|
+
import { SharedGpuRuntime } from './gpu/GpuRuntime.js';
|
|
7
|
+
import { GridEditor } from './interaction/GridEditor.js';
|
|
8
|
+
import { resolveTheme } from './theme.js';
|
|
9
|
+
|
|
10
|
+
/** A source-agnostic WebGPU panel containing multiple realtime gauges. */
|
|
11
|
+
export class MultiGauge {
|
|
12
|
+
#canvas;
|
|
13
|
+
#runtime;
|
|
14
|
+
#renderer;
|
|
15
|
+
#layout;
|
|
16
|
+
#gauges = new Map();
|
|
17
|
+
#header;
|
|
18
|
+
#accent;
|
|
19
|
+
#theme;
|
|
20
|
+
#frame;
|
|
21
|
+
#staticDirty = true;
|
|
22
|
+
#dynamicDirty = true;
|
|
23
|
+
#destroyed = false;
|
|
24
|
+
#resizeObserver;
|
|
25
|
+
#width = 1;
|
|
26
|
+
#height = 1;
|
|
27
|
+
#panelLayout = layoutPanel({ width: 1, height: 1 });
|
|
28
|
+
#editor;
|
|
29
|
+
#previewEntries;
|
|
30
|
+
#stats = { updatesReceived: 0, rendersCoalesced: 0 };
|
|
31
|
+
|
|
32
|
+
/** Create a panel after the shared WebGPU runtime is ready. */
|
|
33
|
+
static async create(canvas, options = {}) {
|
|
34
|
+
if (!canvas || typeof canvas.getContext !== 'function') {
|
|
35
|
+
throw new MultiGaugeError('MultiGauge.create() requires a canvas.');
|
|
36
|
+
}
|
|
37
|
+
const runtime = await SharedGpuRuntime.get();
|
|
38
|
+
const panel = new MultiGauge(canvas, options, runtime);
|
|
39
|
+
panel.#initialize();
|
|
40
|
+
return panel;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
constructor(canvas, options, runtime) {
|
|
44
|
+
this.#canvas = canvas;
|
|
45
|
+
this.#runtime = runtime;
|
|
46
|
+
this.#layout = new GridLayout(options.grid);
|
|
47
|
+
this.#accent = options.accent ?? '#00eaff';
|
|
48
|
+
this.#theme = resolveTheme(options.theme, this.#accent);
|
|
49
|
+
this.#header = options.header ? { ...options.header } : null;
|
|
50
|
+
this.#renderer = new Renderer(canvas, runtime, (all) => this.#invalidate(all));
|
|
51
|
+
for (const gauge of options.gauges ?? []) {
|
|
52
|
+
this.#addNow(gauge);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Add a gauge, auto-placing it when row/col are omitted. */
|
|
57
|
+
add(configuration) {
|
|
58
|
+
this.#assertAlive();
|
|
59
|
+
const gauge = this.#addNow(configuration);
|
|
60
|
+
this.#invalidate(true);
|
|
61
|
+
return { ...gauge };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Remove a gauge and free its cell occupancy. */
|
|
65
|
+
remove(id) {
|
|
66
|
+
this.#assertAlive();
|
|
67
|
+
if (!this.#gauges.delete(id)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
this.#layout.remove(id);
|
|
71
|
+
this.#invalidate(true);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Set one signal value. Numeric values clamp; compass values wrap. */
|
|
76
|
+
set(id, value) {
|
|
77
|
+
this.#assertAlive();
|
|
78
|
+
const gauge = this.#gauges.get(id);
|
|
79
|
+
if (!gauge) {
|
|
80
|
+
throw new MultiGaugeError(`Unknown gauge: ${id}.`);
|
|
81
|
+
}
|
|
82
|
+
this.#stats.updatesReceived += 1;
|
|
83
|
+
const next = coerceValue(gauge, value);
|
|
84
|
+
if (!Object.is(next, gauge.value)) {
|
|
85
|
+
gauge.value = next;
|
|
86
|
+
this.#dynamicDirty = true;
|
|
87
|
+
this.#schedule();
|
|
88
|
+
}
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Set several signals; all changes are coalesced into one animation frame. */
|
|
93
|
+
update(values) {
|
|
94
|
+
this.#assertAlive();
|
|
95
|
+
for (const [id, value] of Object.entries(values)) {
|
|
96
|
+
this.set(id, value);
|
|
97
|
+
}
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Add or update a marker by id. */
|
|
102
|
+
setMarker(gaugeId, markerId, value, options = {}) {
|
|
103
|
+
this.#assertAlive();
|
|
104
|
+
const gauge = this.#gauges.get(gaugeId);
|
|
105
|
+
if (!gauge) {
|
|
106
|
+
throw new MultiGaugeError(`Unknown gauge: ${gaugeId}.`);
|
|
107
|
+
}
|
|
108
|
+
setGaugeMarker(gauge, markerId, value, options);
|
|
109
|
+
this.#invalidate(true);
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
move(id, row, col) {
|
|
114
|
+
this.#assertAlive();
|
|
115
|
+
const result = this.#layout.move(id, row, col);
|
|
116
|
+
if (result) {
|
|
117
|
+
this.#invalidate(true);
|
|
118
|
+
}
|
|
119
|
+
return Boolean(result);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
resize(id, rowSpan, colSpan) {
|
|
123
|
+
this.#assertAlive();
|
|
124
|
+
const result = this.#layout.resize(id, rowSpan, colSpan);
|
|
125
|
+
if (result) {
|
|
126
|
+
this.#invalidate(true);
|
|
127
|
+
}
|
|
128
|
+
return Boolean(result);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
maximize(id) {
|
|
132
|
+
this.#assertAlive();
|
|
133
|
+
this.#layout.maximize(id);
|
|
134
|
+
this.#invalidate(true);
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
minimize(id) {
|
|
139
|
+
this.#assertAlive();
|
|
140
|
+
const result = this.#layout.minimize(id);
|
|
141
|
+
if (result) {
|
|
142
|
+
this.#invalidate(true);
|
|
143
|
+
}
|
|
144
|
+
return Boolean(result);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
restoreGauge(id) {
|
|
148
|
+
this.#assertAlive();
|
|
149
|
+
const result = this.#layout.restore(id);
|
|
150
|
+
if (result) {
|
|
151
|
+
this.#invalidate(true);
|
|
152
|
+
}
|
|
153
|
+
return Boolean(result);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Enable or disable direct layout manipulation. Enabled by default. */
|
|
157
|
+
setEditing(enabled) {
|
|
158
|
+
this.#assertAlive();
|
|
159
|
+
if (enabled && !this.#editor) {
|
|
160
|
+
this.#editor = new GridEditor(this.#canvas, this.#layout, {
|
|
161
|
+
preview: (entries) => this.#setLayoutPreview(entries),
|
|
162
|
+
commit: () => this.#finishLayoutPreview(),
|
|
163
|
+
cancel: () => this.#finishLayoutPreview()
|
|
164
|
+
});
|
|
165
|
+
this.#refreshEditor();
|
|
166
|
+
} else if (!enabled && this.#editor) {
|
|
167
|
+
this.#editor.destroy();
|
|
168
|
+
this.#editor = null;
|
|
169
|
+
}
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Return clean JSON-compatible public state. */
|
|
174
|
+
serialize() {
|
|
175
|
+
this.#assertAlive();
|
|
176
|
+
return {
|
|
177
|
+
version: 1,
|
|
178
|
+
grid: this.#layout.config,
|
|
179
|
+
accent: this.#accent,
|
|
180
|
+
theme: { ...this.#theme },
|
|
181
|
+
header: this.#header ? {
|
|
182
|
+
...this.#header,
|
|
183
|
+
icon: this.#header.icon instanceof URL ? this.#header.icon.href : this.#header.icon
|
|
184
|
+
} : null,
|
|
185
|
+
gauges: [...this.#gauges.values()].map((gauge) => serializeGauge(gauge, this.#layout.get(gauge.id)))
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Atomically replace public configuration from serialized state. */
|
|
190
|
+
restore(state) {
|
|
191
|
+
this.#assertAlive();
|
|
192
|
+
if (!state || !Array.isArray(state.gauges)) {
|
|
193
|
+
throw new MultiGaugeError('Invalid serialized MultiGauge state.');
|
|
194
|
+
}
|
|
195
|
+
const layout = new GridLayout(state.grid);
|
|
196
|
+
const gauges = new Map();
|
|
197
|
+
for (const input of state.gauges) {
|
|
198
|
+
const gauge = normalizeGauge(input);
|
|
199
|
+
if (gauges.has(gauge.id)) {
|
|
200
|
+
throw new MultiGaugeError(`Gauge id already exists: ${gauge.id}.`);
|
|
201
|
+
}
|
|
202
|
+
layout.add(gauge.id, gauge);
|
|
203
|
+
gauges.set(gauge.id, gauge);
|
|
204
|
+
}
|
|
205
|
+
this.#layout = layout;
|
|
206
|
+
this.#previewEntries = undefined;
|
|
207
|
+
this.#gauges = gauges;
|
|
208
|
+
this.#accent = state.accent ?? '#00eaff';
|
|
209
|
+
this.#theme = resolveTheme(state.theme, this.#accent);
|
|
210
|
+
this.#header = state.header ? { ...state.header } : null;
|
|
211
|
+
if (this.#editor) {
|
|
212
|
+
this.#editor.destroy();
|
|
213
|
+
this.#editor = null;
|
|
214
|
+
this.setEditing(true);
|
|
215
|
+
}
|
|
216
|
+
this.#invalidate(true);
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Lightweight counters intended for demos and diagnostics. */
|
|
221
|
+
getStats() {
|
|
222
|
+
return {
|
|
223
|
+
...this.#stats,
|
|
224
|
+
...this.#renderer.stats,
|
|
225
|
+
runtimeGeneration: this.#runtime.generation
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
destroy() {
|
|
230
|
+
if (this.#destroyed) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.#destroyed = true;
|
|
234
|
+
if (this.#frame !== undefined) {
|
|
235
|
+
cancelAnimationFrame(this.#frame);
|
|
236
|
+
}
|
|
237
|
+
this.#resizeObserver?.disconnect();
|
|
238
|
+
this.#editor?.destroy();
|
|
239
|
+
this.#renderer.destroy();
|
|
240
|
+
this.#gauges.clear();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#initialize() {
|
|
244
|
+
const resize = () => {
|
|
245
|
+
const style = getComputedStyle(this.#canvas);
|
|
246
|
+
const horizontalPadding = (Number.parseFloat(style.paddingLeft) || 0)
|
|
247
|
+
+ (Number.parseFloat(style.paddingRight) || 0);
|
|
248
|
+
const verticalPadding = (Number.parseFloat(style.paddingTop) || 0)
|
|
249
|
+
+ (Number.parseFloat(style.paddingBottom) || 0);
|
|
250
|
+
this.#width = Math.max(1, this.#canvas.clientWidth - horizontalPadding
|
|
251
|
+
|| this.#canvas.width || 1);
|
|
252
|
+
this.#height = Math.max(1, this.#canvas.clientHeight - verticalPadding
|
|
253
|
+
|| this.#canvas.height || 1);
|
|
254
|
+
if (this.#renderer.resize(this.#width, this.#height)) {
|
|
255
|
+
this.#invalidate(true);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
if (typeof ResizeObserver === 'function') {
|
|
259
|
+
this.#resizeObserver = new ResizeObserver(resize);
|
|
260
|
+
this.#resizeObserver.observe(this.#canvas);
|
|
261
|
+
}
|
|
262
|
+
resize();
|
|
263
|
+
this.setEditing(true);
|
|
264
|
+
this.#invalidate(true);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#addNow(configuration) {
|
|
268
|
+
const gauge = normalizeGauge(configuration);
|
|
269
|
+
if (this.#gauges.has(gauge.id)) {
|
|
270
|
+
throw new MultiGaugeError(`Gauge id already exists: ${gauge.id}.`);
|
|
271
|
+
}
|
|
272
|
+
this.#layout.add(gauge.id, gauge);
|
|
273
|
+
this.#gauges.set(gauge.id, gauge);
|
|
274
|
+
return gauge;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#invalidate(all) {
|
|
278
|
+
if (this.#destroyed) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this.#staticDirty ||= all;
|
|
282
|
+
this.#dynamicDirty = true;
|
|
283
|
+
this.#schedule();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#schedule() {
|
|
287
|
+
if (this.#frame !== undefined) {
|
|
288
|
+
this.#stats.rendersCoalesced += 1;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.#frame = requestAnimationFrame(() => {
|
|
292
|
+
this.#frame = undefined;
|
|
293
|
+
this.#render();
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#render() {
|
|
298
|
+
if (this.#destroyed || (!this.#staticDirty && !this.#dynamicDirty)) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.#panelLayout = layoutPanel({
|
|
302
|
+
width: this.#width,
|
|
303
|
+
height: this.#height,
|
|
304
|
+
hasHeader: Boolean(this.#header)
|
|
305
|
+
});
|
|
306
|
+
const previewing = Boolean(this.#previewEntries);
|
|
307
|
+
const rectangles = this.#previewEntries
|
|
308
|
+
? this.#layout.rectangles(this.#panelLayout.gridRect, this.#previewEntries)
|
|
309
|
+
: this.#layout.rectangles(this.#panelLayout.gridRect);
|
|
310
|
+
const refreshEditor = this.#staticDirty && !previewing;
|
|
311
|
+
let gauges = [...this.#gauges.values()];
|
|
312
|
+
const maximized = gauges.find((gauge) => this.#layout.get(gauge.id)?.maximized);
|
|
313
|
+
if (maximized) {
|
|
314
|
+
gauges = [maximized];
|
|
315
|
+
}
|
|
316
|
+
if (this.#staticDirty) {
|
|
317
|
+
this.#renderer.rebuildStatic(gauges, rectangles, this.#header, this.#theme, this.#panelLayout);
|
|
318
|
+
}
|
|
319
|
+
if (this.#dynamicDirty) {
|
|
320
|
+
this.#renderer.updateDynamic(gauges, this.#theme);
|
|
321
|
+
}
|
|
322
|
+
this.#renderer.render(this.#theme);
|
|
323
|
+
this.#staticDirty = false;
|
|
324
|
+
this.#dynamicDirty = false;
|
|
325
|
+
if (refreshEditor) {
|
|
326
|
+
this.#refreshEditor(rectangles);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#setLayoutPreview(entries) {
|
|
331
|
+
this.#previewEntries = entries;
|
|
332
|
+
this.#invalidate(true);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#finishLayoutPreview() {
|
|
336
|
+
this.#previewEntries = undefined;
|
|
337
|
+
this.#invalidate(true);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#refreshEditor(rectangles) {
|
|
341
|
+
if (!this.#editor) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.#panelLayout = layoutPanel({
|
|
345
|
+
width: this.#width,
|
|
346
|
+
height: this.#height,
|
|
347
|
+
hasHeader: Boolean(this.#header)
|
|
348
|
+
});
|
|
349
|
+
const actualRectangles = rectangles ?? this.#layout.rectangles(this.#panelLayout.gridRect);
|
|
350
|
+
this.#editor.refresh(actualRectangles, this.#panelLayout);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#assertAlive() {
|
|
354
|
+
if (this.#destroyed) {
|
|
355
|
+
throw new MultiGaugeError('This MultiGauge has been destroyed.');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
-----------------------------------------------------------
|
|
8
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
PREAMBLE
|
|
12
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
13
|
+
development of collaborative font projects, to support the font creation
|
|
14
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
15
|
+
open framework in which fonts may be shared and improved in partnership
|
|
16
|
+
with others.
|
|
17
|
+
|
|
18
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
19
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
20
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
redistributed and/or sold with any software provided that any reserved
|
|
22
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
23
|
+
however, cannot be released under any other type of license. The
|
|
24
|
+
requirement for fonts to remain under this license does not apply
|
|
25
|
+
to any document created using the fonts or their derivatives.
|
|
26
|
+
|
|
27
|
+
DEFINITIONS
|
|
28
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
29
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
30
|
+
include source files, build scripts and documentation.
|
|
31
|
+
|
|
32
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
33
|
+
copyright statement(s).
|
|
34
|
+
|
|
35
|
+
"Original Version" refers to the collection of Font Software components as
|
|
36
|
+
distributed by the Copyright Holder(s).
|
|
37
|
+
|
|
38
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
39
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
40
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
41
|
+
new environment.
|
|
42
|
+
|
|
43
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
44
|
+
writer or other person who contributed to the Font Software.
|
|
45
|
+
|
|
46
|
+
PERMISSION AND CONDITIONS
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
48
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
49
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
50
|
+
Software, subject to the following conditions:
|
|
51
|
+
|
|
52
|
+
1) Neither the Font Software nor any of its individual components,
|
|
53
|
+
in Original or Modified Versions, may be sold by itself.
|
|
54
|
+
|
|
55
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
56
|
+
redistributed and/or sold with any software, provided that each copy
|
|
57
|
+
contains the above copyright notice and this license. These can be
|
|
58
|
+
included either as stand-alone text files, human-readable headers or
|
|
59
|
+
in the appropriate machine-readable metadata fields within text or
|
|
60
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
61
|
+
|
|
62
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
63
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
64
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
65
|
+
presented to the users.
|
|
66
|
+
|
|
67
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
68
|
+
Software shall not be used to promote, endorse or advertise any
|
|
69
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
70
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
71
|
+
permission.
|
|
72
|
+
|
|
73
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
74
|
+
must be distributed entirely under this license, and must not be
|
|
75
|
+
distributed under any other license. The requirement for fonts to
|
|
76
|
+
remain under this license does not apply to any document created
|
|
77
|
+
using the Font Software.
|
|
78
|
+
|
|
79
|
+
TERMINATION
|
|
80
|
+
This license becomes null and void if any of the above conditions are
|
|
81
|
+
not met.
|
|
82
|
+
|
|
83
|
+
DISCLAIMER
|
|
84
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
87
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
88
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
89
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
90
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
91
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
92
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|