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 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
+ ![MultiGauge vehicle telemetry dashboard](./docs/multigauge-demo.png)
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
+ }
@@ -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.
package/src/errors.js ADDED
@@ -0,0 +1,7 @@
1
+ /** An error caused by invalid MultiGauge usage or an unavailable GPU. */
2
+ export class MultiGaugeError extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = 'MultiGaugeError';
6
+ }
7
+ }