@kupcad/ui 0.1.7 → 0.1.8
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/README.md +20 -9
- package/package.json +5 -4
- package/src/ConsolePanel.svelte +2 -1
- package/src/PreviewPanel.svelte +25 -11
- package/src/ViewerPane.svelte +31 -1
- package/src/console.js +12 -3
- package/src/index.js +8 -1
- package/src/params.js +91 -12
- package/src/run-status.js +5 -1
- package/src/settings.js +53 -19
- package/src/viewer/appearance.js +2 -1
- package/src/viewer/brand.js +13 -0
- package/src/viewer/environment.js +8 -0
- package/src/viewer/faces.js +20 -151
- package/src/viewer/hits.js +41 -0
- package/src/viewer/hover-mark.js +5 -0
- package/src/viewer/model.js +17 -9
- package/src/viewer/section.js +12 -4
- package/src/viewer/theme.js +7 -5
- package/src/viewer/topology.js +223 -0
- package/src/viewer/viewer.js +12 -8
package/README.md
CHANGED
|
@@ -76,10 +76,12 @@ For Svelte, the component around it (`@kupcad/ui`'s `Viewer`):
|
|
|
76
76
|
what is drawn there, so the mark shows on a part of any colour). The face is what its feature edges
|
|
77
77
|
bound (`faces.js`: triangles joined where they meet at less than the edges' 20°), flat when it
|
|
78
78
|
faces one way (its area in mm², its way, and `+Z (top)` when along an axis) or curved (its
|
|
79
|
-
area). Which triangles meet which is found once a mesh,
|
|
80
|
-
arrays (a
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
area). Which triangles meet which is found once a mesh, when its edges are drawn, in typed
|
|
80
|
+
arrays (`topology.js`, a fifth of a second for a million triangles; three.js's `EdgesGeometry`
|
|
81
|
+
took eight times as long), so the edges and the faces' borders are one; the triangle under the
|
|
82
|
+
pointer is found through a bounds tree (`hits.js`, three-mesh-bvh), and each face once, its
|
|
83
|
+
area kept while the mesh doesn't move. A ghost, seen through, is under nothing, and neither is
|
|
84
|
+
what a section cut away.
|
|
83
85
|
* **The view cube** (`ViewCube`, which the Svelte component puts in its corner unless
|
|
84
86
|
`cube={false}`) is turned as the camera is; its faces (labelled TOP, FRONT, RIGHT, ..., each
|
|
85
87
|
upright from its view), edges and corners, 26 ways to look, take the camera there when
|
|
@@ -98,10 +100,14 @@ For Svelte, the component around it (`@kupcad/ui`'s `Viewer`):
|
|
|
98
100
|
Cmd it turns the view, as a drag does. Ctrl is a pinch's: a browser sends one as the wheel
|
|
99
101
|
with Ctrl, so Ctrl and the wheel zoom too.
|
|
100
102
|
* **Rendering** is on demand: when the model, a setting or the camera changes, once a frame. A
|
|
101
|
-
solid shows its feature edges; x-ray sees through it; a wireframe is the edges alone.
|
|
103
|
+
solid shows its feature edges; x-ray sees through it; a wireframe is the edges alone. A
|
|
104
|
+
drawing's shapes are drawn in the SVG's order, each over the floor and the ones before it, as
|
|
105
|
+
SVG draws them (at z = 0 all, depth can't order them), a see-through one too; the axes over
|
|
106
|
+
it.
|
|
102
107
|
* **A section** clips every material of a solid's model at its plane and caps the cut where it
|
|
103
108
|
runs inside a solid (three.js's stencil technique), but in a wireframe; a drawing isn't cut. A
|
|
104
|
-
|
|
109
|
+
face at the offset is kept, so a cut at the bottom shows the footprint and one at the top the
|
|
110
|
+
top face. A part cut away isn't picked.
|
|
105
111
|
* **Draco:** a model built with `--draco` needs `dracoDecoderPath`, where three.js's decoder files
|
|
106
112
|
are; the decoder is loaded only when a model needs it.
|
|
107
113
|
* **Themes** are colours (`LIGHT`, `DARK`, or a host's own, as the VS Code webview maps its
|
|
@@ -154,7 +160,8 @@ webview and the desktop app to the language server.
|
|
|
154
160
|
Edges, Axes, Grid and Plate toggles; Fit, Persp, and the Iso, Top, Front and Right views, the
|
|
155
161
|
one looked from pressed, a turn by hand pressing none), the model's parts in its bottom half,
|
|
156
162
|
each with its colour and copies (a click on one selects its copies, as a click in the viewer
|
|
157
|
-
does: `bind:selected`)
|
|
163
|
+
does: `bind:selected`), and, with an `onsource`, the line that names it (`named`, the
|
|
164
|
+
frame's `parts[].source`, a library's with its file), for the host to show. In a pane narrower than 36em (a phone's) the panel is under the model,
|
|
158
165
|
at most half the pane. The model fills what the rest leave, no less than
|
|
159
166
|
`--kc-model-min-height` (none by default), which a host whose pane is as tall as its content
|
|
160
167
|
sets: the model has no height of its own. A bar above: a section through the model along an axis (a solid's: a drawing, which isn't cut, or no model offers none), screenshot, an
|
|
@@ -285,5 +292,9 @@ The viewer's scene logic in Node (jsdom), with a stand-in for WebGL and models t
|
|
|
285
292
|
makes through `@kupcad/wasm`; the components mounted under jsdom (the `testing` setup and its
|
|
286
293
|
`stubRenderer`), with the schemas and models the real core gives. And a smoke test in headless
|
|
287
294
|
Chromium (`pnpm test:browser`) that draws what the core makes in the page and reads the pixels
|
|
288
|
-
back: the part's colour, the background, a section cutting it all away, and a cap
|
|
289
|
-
|
|
295
|
+
back: the part's colour, the background, a section cutting it all away, and a cap, and one
|
|
296
|
+
on a face (at the bottom, the top, and from below) without streaks; a drawing's shapes in
|
|
297
|
+
order over the grid; the view
|
|
298
|
+
cube, which turns the camera to the part clicked; and the `ViewerPane` laid out by the browser,
|
|
299
|
+
its panel beside the model and, in a pane narrower than 36em, under it (a container query,
|
|
300
|
+
which jsdom doesn't apply), a white part's swatch framed on the light panel.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kupcad/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "KupCAD's UI: a three.js viewer of a model the core made, for any host (@kupcad/ui/viewer), and the Svelte panels around it (the viewer's pane, parameters, console, a language server's preview), styled by --kc-* tokens.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -36,11 +36,12 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@kupcad/protocol": "0.1.
|
|
40
|
-
"three": "^0.186.1"
|
|
39
|
+
"@kupcad/protocol": "0.1.8",
|
|
40
|
+
"three": "^0.186.1",
|
|
41
|
+
"three-mesh-bvh": "^0.9.17"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@kupcad/wasm": "0.1.
|
|
44
|
+
"@kupcad/wasm": "0.1.8",
|
|
44
45
|
"@playwright/test": "^1.64.1",
|
|
45
46
|
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
46
47
|
"@testing-library/svelte": "^5.4.2",
|
package/src/ConsolePanel.svelte
CHANGED
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
<li class={line.kind}>
|
|
57
57
|
{#if line.line !== null && onreveal}
|
|
58
58
|
<button type="button" class="place" onclick={() => onreveal(line)}>
|
|
59
|
-
{line.text}
|
|
59
|
+
{line.text}
|
|
60
|
+
<span class="at">{line.file ? `${line.file} ` : ''}line {line.line}</span>
|
|
60
61
|
</button>
|
|
61
62
|
{:else}
|
|
62
63
|
{line.text}
|
package/src/PreviewPanel.svelte
CHANGED
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
import { appendLines, linesOfLog, linesOfRun } from './console.js';
|
|
23
23
|
import { plainValues } from './values.svelte.js';
|
|
24
24
|
import ParamsPanel from './ParamsPanel.svelte';
|
|
25
|
-
import { memorySettings, sizeSetting } from './settings.js';
|
|
25
|
+
import { memorySettings, paneKeys, sizeSetting } from './settings.js';
|
|
26
26
|
import Splitter from './Splitter.svelte';
|
|
27
27
|
import ViewerPane from './ViewerPane.svelte';
|
|
28
28
|
import { toBase64 } from './base64.js';
|
|
29
29
|
import { problemsOf, runStatusOf } from './run-status.js';
|
|
30
30
|
import { connectViewport } from './viewport-socket.js';
|
|
31
31
|
|
|
32
|
-
/** @import { ExportFormat, ParamValues, Preview, ShownDocument, ViewerHost } from '@kupcad/protocol/types' */
|
|
32
|
+
/** @import { ExportFormat, ParamValues, Part, Preview, ShownDocument, ViewerHost } from '@kupcad/protocol/types' */
|
|
33
33
|
/** @import { ViewerOptions } from './viewer/types.js' */
|
|
34
34
|
/** @import { ConsoleLine } from './console.js' */
|
|
35
35
|
/** @import { Problem } from './run-status.js' */
|
|
@@ -67,13 +67,7 @@
|
|
|
67
67
|
* The settings' keys: the parameters' width and whether they show, whether the view's panel
|
|
68
68
|
* shows, the console's height and whether it is open.
|
|
69
69
|
*/
|
|
70
|
-
const KEYS =
|
|
71
|
-
params: 'preview.params',
|
|
72
|
-
paramsOpen: 'preview.paramsOpen',
|
|
73
|
-
panelOpen: 'preview.panelOpen',
|
|
74
|
-
console: 'preview.console',
|
|
75
|
-
open: 'preview.consoleOpen',
|
|
76
|
-
};
|
|
70
|
+
const KEYS = paneKeys('preview');
|
|
77
71
|
const id = $props.id();
|
|
78
72
|
// As the settings have them; a size dragged is the pane's own until the drag is done
|
|
79
73
|
let paramsWidth = $derived(sizeSetting(settings, KEYS.params, 288));
|
|
@@ -191,7 +185,7 @@
|
|
|
191
185
|
*/
|
|
192
186
|
function revealProblem(problem) {
|
|
193
187
|
if (!shown || problem.line === null) return;
|
|
194
|
-
host.post({ type: 'reveal', uri: shown.uri,
|
|
188
|
+
host.post({ type: 'reveal', uri: shown.uri, ...placeOf(problem) });
|
|
195
189
|
}
|
|
196
190
|
|
|
197
191
|
/**
|
|
@@ -200,7 +194,26 @@
|
|
|
200
194
|
*/
|
|
201
195
|
function revealLine(line) {
|
|
202
196
|
if (!shown || line.line === null) return;
|
|
203
|
-
host.post({ type: 'reveal', uri: shown.uri, line
|
|
197
|
+
host.post({ type: 'reveal', uri: shown.uri, ...placeOf(line) });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Shows the script where `part` is named, in its editor (a library's, opened).
|
|
202
|
+
* @param {Part} part
|
|
203
|
+
*/
|
|
204
|
+
function revealSource(part) {
|
|
205
|
+
if (!shown || !part.source) return;
|
|
206
|
+
host.post({ type: 'reveal', uri: shown.uri, ...placeOf({ file: null, ...part.source }) });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Where a problem or a console line is, as `reveal` says it: in a library, its `file`, which
|
|
211
|
+
* the host opens in place of the script shown.
|
|
212
|
+
* @param {{ line: number | null, column: number | null, file: string | null }} at
|
|
213
|
+
*/
|
|
214
|
+
function placeOf(at) {
|
|
215
|
+
const place = { line: at.line ?? 1, column: at.column ?? 1 };
|
|
216
|
+
return at.file ? { ...place, file: at.file } : place;
|
|
204
217
|
}
|
|
205
218
|
|
|
206
219
|
/** @param {Blob} image */
|
|
@@ -229,6 +242,7 @@
|
|
|
229
242
|
status={run}
|
|
230
243
|
{problems}
|
|
231
244
|
onproblem={revealProblem}
|
|
245
|
+
onsource={revealSource}
|
|
232
246
|
onscreenshot={screenshot}
|
|
233
247
|
onerror={failed}
|
|
234
248
|
>
|
package/src/ViewerPane.svelte
CHANGED
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
* parameters'), after the Panel button: at the end nearest them
|
|
57
57
|
* @property {RunStatus | null} [status] how the run shown went, or none to say
|
|
58
58
|
* @property {Problem[]} [problems] what the run shown said went wrong (`problemsOf(header)`)
|
|
59
|
+
* @property {(part: Part) => void} [onsource] a part's line clicked (where it is named, its
|
|
60
|
+
* `source`): the host shows it; without it, no line is offered
|
|
59
61
|
* @property {(problem: Problem) => void} [onproblem] a problem with a line clicked: the host
|
|
60
62
|
* shows the script there; without it, none can be clicked
|
|
61
63
|
* @property {(error: unknown) => void} [onerror] a model the viewer couldn't show
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
status = null,
|
|
83
85
|
problems = [],
|
|
84
86
|
onproblem,
|
|
87
|
+
onsource,
|
|
85
88
|
onerror,
|
|
86
89
|
settings = memorySettings(),
|
|
87
90
|
shortcuts = [],
|
|
@@ -441,6 +444,15 @@
|
|
|
441
444
|
<span class="name">{part.name}</span>
|
|
442
445
|
{#if part.count > 1}<span class="count">×{part.count}</span>{/if}
|
|
443
446
|
</button>
|
|
447
|
+
{#if onsource && part.source}
|
|
448
|
+
<button
|
|
449
|
+
type="button"
|
|
450
|
+
class="source"
|
|
451
|
+
title="Show where {part.name} is named"
|
|
452
|
+
onclick={() => onsource(part)}
|
|
453
|
+
>{part.source.file ? `${part.source.file} ` : ''}line {part.source.line}</button
|
|
454
|
+
>
|
|
455
|
+
{/if}
|
|
444
456
|
</li>
|
|
445
457
|
{/each}
|
|
446
458
|
</ul>
|
|
@@ -494,7 +506,9 @@
|
|
|
494
506
|
{#snippet said(/** @type {Problem} */ problem)}
|
|
495
507
|
<span class="severity">{SEVERITIES[problem.severity]}</span>
|
|
496
508
|
<span class="message">{problem.message}</span>
|
|
497
|
-
{#if problem.line !== null}<span class="place"
|
|
509
|
+
{#if problem.line !== null}<span class="place"
|
|
510
|
+
>{problem.file ? `${problem.file} ` : ''}line {problem.line}</span
|
|
511
|
+
>{/if}
|
|
498
512
|
{/snippet}
|
|
499
513
|
|
|
500
514
|
<style>
|
|
@@ -623,6 +637,20 @@
|
|
|
623
637
|
margin: 0;
|
|
624
638
|
padding: 0;
|
|
625
639
|
}
|
|
640
|
+
/* The part, and the line that names it beside it */
|
|
641
|
+
.parts li {
|
|
642
|
+
display: flex;
|
|
643
|
+
}
|
|
644
|
+
.parts li > button {
|
|
645
|
+
flex: 1;
|
|
646
|
+
min-width: 0;
|
|
647
|
+
}
|
|
648
|
+
.parts .source {
|
|
649
|
+
flex: none;
|
|
650
|
+
width: auto;
|
|
651
|
+
color: var(--kc-muted);
|
|
652
|
+
font-size: 0.92em;
|
|
653
|
+
}
|
|
626
654
|
.parts button {
|
|
627
655
|
display: flex;
|
|
628
656
|
align-items: center;
|
|
@@ -651,6 +679,8 @@
|
|
|
651
679
|
height: 10px;
|
|
652
680
|
border-radius: 2px;
|
|
653
681
|
flex: none;
|
|
682
|
+
/* Framed, so a part the colour of the panel (white in the light theme) still shows */
|
|
683
|
+
box-shadow: inset 0 0 0 1px var(--kc-border);
|
|
654
684
|
}
|
|
655
685
|
.name {
|
|
656
686
|
flex: 1;
|
package/src/console.js
CHANGED
|
@@ -19,6 +19,8 @@ export const MAX_LINES = 2000;
|
|
|
19
19
|
* @property {string} text
|
|
20
20
|
* @property {number | null} line
|
|
21
21
|
* @property {number | null} column
|
|
22
|
+
* @property {string | null} file the module the line and column are in (its path in the
|
|
23
|
+
* project), null for the script's own
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -26,9 +28,16 @@ export const MAX_LINES = 2000;
|
|
|
26
28
|
* @param {string} text
|
|
27
29
|
* @param {number | null} [line]
|
|
28
30
|
* @param {number | null} [column]
|
|
31
|
+
* @param {string | null} [file]
|
|
29
32
|
* @returns {ConsoleLine}
|
|
30
33
|
*/
|
|
31
|
-
const lineOf = (kind, text, line = null, column = null) => ({
|
|
34
|
+
const lineOf = (kind, text, line = null, column = null, file = null) => ({
|
|
35
|
+
kind,
|
|
36
|
+
text,
|
|
37
|
+
line,
|
|
38
|
+
column,
|
|
39
|
+
file,
|
|
40
|
+
});
|
|
32
41
|
|
|
33
42
|
/**
|
|
34
43
|
* `text`'s lines, without the end its last one has.
|
|
@@ -48,8 +57,8 @@ export function linesOfRun(header, name) {
|
|
|
48
57
|
if (header.outputCut) {
|
|
49
58
|
lines.push(lineOf('log', 'It printed more than a run keeps: the rest is left out.'));
|
|
50
59
|
}
|
|
51
|
-
for (const { message, line, column } of header.warnings) {
|
|
52
|
-
lines.push(lineOf('warning', `Warning: ${message}`, line, column));
|
|
60
|
+
for (const { message, line, column, file } of header.warnings) {
|
|
61
|
+
lines.push(lineOf('warning', `Warning: ${message}`, line, column, file ?? null));
|
|
53
62
|
}
|
|
54
63
|
const { error } = header;
|
|
55
64
|
if (error) lines.push(lineOf('error', `Error: ${error.message}`, error.line, error.column));
|
package/src/index.js
CHANGED
|
@@ -15,7 +15,14 @@ export { default as ViewerPane } from './ViewerPane.svelte';
|
|
|
15
15
|
export { appendLines, linesOfLog, linesOfRun } from './console.js';
|
|
16
16
|
export { fillOf } from './range.js';
|
|
17
17
|
export { problemsOf, runStatusOf } from './run-status.js';
|
|
18
|
-
export {
|
|
18
|
+
export {
|
|
19
|
+
keptSettings,
|
|
20
|
+
localSettings,
|
|
21
|
+
memorySettings,
|
|
22
|
+
paneKeys,
|
|
23
|
+
settingsObject,
|
|
24
|
+
sizeSetting,
|
|
25
|
+
} from './settings.js';
|
|
19
26
|
export { VIEWER_SHORTCUTS } from './shortcuts.js';
|
|
20
27
|
export { watchTokens } from './theme.js';
|
|
21
28
|
export { plainValues } from './values.svelte.js';
|
package/src/params.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// The parameter panel's logic, apart from its markup: which control a parameter (or a part of its
|
|
2
2
|
// value) gets, how the parameters group, which value a control shows, and whether a value given
|
|
3
3
|
// in the panel is one the script takes (its schema's rules, core/src/core/param_rules.zig's),
|
|
4
|
-
// checked before it leaves the panel
|
|
5
|
-
//
|
|
4
|
+
// checked before it leaves the panel as the core's `check` reads it (a number's text as
|
|
5
|
+
// std.fmt.parseFloat reads it, true or false as 1 and 0 too, a Map's fields all of them and no
|
|
6
|
+
// other, a choice compared once read): the panel accepted values the run then refused, and the
|
|
7
|
+
// other way round. The run is the final check, and a frame names a value it refused, and where.
|
|
6
8
|
|
|
7
9
|
/** @import { ParamSpec, ParamValue, ParamValues, PathStep, Rule } from '@kupcad/protocol/types' */
|
|
8
10
|
|
|
@@ -288,24 +290,95 @@ export function validate(rule, input) {
|
|
|
288
290
|
function read(rule, input, path) {
|
|
289
291
|
if (rule.type === 'array') return readArray(rule, input, path);
|
|
290
292
|
if (rule.type === 'map') return readMap(rule, input, path);
|
|
293
|
+
const result = readScalar(rule, input, path);
|
|
294
|
+
if (!('value' in result)) return result;
|
|
295
|
+
// Once read, as the core compares a choice: 20.0 is the choice 20, " 20" no number
|
|
291
296
|
const choices = rule.in;
|
|
292
|
-
if (choices) {
|
|
293
|
-
|
|
294
|
-
return choice === undefined
|
|
295
|
-
? { error: `must be one of ${choices.join(', ')}`, path }
|
|
296
|
-
: { value: choice };
|
|
297
|
+
if (choices && !choices.some((listed) => listed === result.value)) {
|
|
298
|
+
return { error: `must be one of ${choices.join(', ')}`, path };
|
|
297
299
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {Rule} rule a number's, a boolean's or text's
|
|
305
|
+
* @param {Value | null} input
|
|
306
|
+
* @param {PathStep[]} path
|
|
307
|
+
* @returns {{ value: Value } | Refusal}
|
|
308
|
+
*/
|
|
309
|
+
function readScalar(rule, input, path) {
|
|
310
|
+
if (rule.type === 'boolean') {
|
|
311
|
+
const value = booleanOf(input);
|
|
312
|
+
return value === null ? { error: 'must be true or false', path } : { value };
|
|
313
|
+
}
|
|
314
|
+
if (rule.type === 'string') {
|
|
315
|
+
return typeof input === 'string' ? { value: input } : { error: 'must be text', path };
|
|
316
|
+
}
|
|
317
|
+
const number = numberOf(input);
|
|
318
|
+
if (number === null) return { error: 'must be a number', path };
|
|
303
319
|
if (rule.min !== null && number < rule.min)
|
|
304
320
|
return { error: `must be at least ${rule.min}`, path };
|
|
305
321
|
if (rule.max !== null && number > rule.max) return { error: `must be at most ${rule.max}`, path };
|
|
306
322
|
return { value: number };
|
|
307
323
|
}
|
|
308
324
|
|
|
325
|
+
/**
|
|
326
|
+
* A finite number, or text that reads as one as the core reads it (Zig's std.fmt.parseFloat:
|
|
327
|
+
* a decimal with a fraction and an exponent, digits joined by single underscores, a hex number
|
|
328
|
+
* with a binary exponent; no spaces around it, no `inf`); null for anything else.
|
|
329
|
+
* @param {Value | null} input
|
|
330
|
+
* @returns {number | null}
|
|
331
|
+
*/
|
|
332
|
+
export function numberOf(input) {
|
|
333
|
+
if (typeof input === 'number') return Number.isFinite(input) ? input : null;
|
|
334
|
+
if (typeof input !== 'string') return null;
|
|
335
|
+
const hex = HEX_NUMBER.exec(input);
|
|
336
|
+
const number = hex
|
|
337
|
+
? hexNumber(hex)
|
|
338
|
+
: DECIMAL_NUMBER.test(input)
|
|
339
|
+
? Number(input.replaceAll('_', ''))
|
|
340
|
+
: NaN;
|
|
341
|
+
return Number.isFinite(number) ? number : null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Digits joined by single underscores. */
|
|
345
|
+
const DIGITS = String.raw`\d+(?:_\d+)*`;
|
|
346
|
+
const HEX_DIGITS = String.raw`[0-9a-fA-F]+(?:_[0-9a-fA-F]+)*`;
|
|
347
|
+
/** `12`, `1_000`, `1.`, `.5`, `1.5e-3`, with a sign. */
|
|
348
|
+
const DECIMAL_NUMBER = new RegExp(
|
|
349
|
+
String.raw`^[+-]?(?:${DIGITS}(?:\.(?:${DIGITS})?)?|\.${DIGITS})(?:[eE][+-]?${DIGITS})?$`,
|
|
350
|
+
);
|
|
351
|
+
/** `0x10`, `0x1.8p1`, with a sign: the sign, the whole, the fraction and the exponent. */
|
|
352
|
+
const HEX_NUMBER = new RegExp(
|
|
353
|
+
String.raw`^([+-]?)0[xX](?:(${HEX_DIGITS})(?:\.(${HEX_DIGITS})?)?|\.(${HEX_DIGITS}))(?:[pP]([+-]?${DIGITS}))?$`,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
/** @param {RegExpExecArray} match HEX_NUMBER's */
|
|
357
|
+
function hexNumber(match) {
|
|
358
|
+
const sign = match[1];
|
|
359
|
+
const whole = match[2] ?? '';
|
|
360
|
+
const fraction = match[3] ?? match[4] ?? '';
|
|
361
|
+
const exponent = match[5] ?? '0';
|
|
362
|
+
const digits = `${whole}${fraction}`.replaceAll('_', '');
|
|
363
|
+
const places = fraction.replaceAll('_', '').length;
|
|
364
|
+
const magnitude =
|
|
365
|
+
Number.parseInt(digits || '0', 16) * 2 ** (Number(exponent.replaceAll('_', '')) - 4 * places);
|
|
366
|
+
return sign === '-' ? -magnitude : magnitude;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* true or false, as the core reads them: so, as the numbers 1 and 0, or as the text `true`,
|
|
371
|
+
* `false`, `1` or `0`; null for anything else (`yes`, `True`, an empty field).
|
|
372
|
+
* @param {Value | null} input
|
|
373
|
+
* @returns {boolean | null}
|
|
374
|
+
*/
|
|
375
|
+
export function booleanOf(input) {
|
|
376
|
+
if (typeof input === 'boolean') return input;
|
|
377
|
+
if (input === 1 || input === 'true' || input === '1') return true;
|
|
378
|
+
if (input === 0 || input === 'false' || input === '0') return false;
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
309
382
|
/**
|
|
310
383
|
* @param {Rule} rule
|
|
311
384
|
* @param {Value | null} input
|
|
@@ -342,6 +415,12 @@ function readArray(rule, input, path) {
|
|
|
342
415
|
*/
|
|
343
416
|
function readMap(rule, input, path) {
|
|
344
417
|
if (!isRecord(input)) return { error: 'must be a record', path };
|
|
418
|
+
const names = (rule.fields ?? []).map((field) => field.name);
|
|
419
|
+
// Its fields, all of them (one missing is read as nothing, which no field takes) and no other
|
|
420
|
+
const stranger = Object.keys(input).find((key) => !names.includes(key));
|
|
421
|
+
if (stranger !== undefined) {
|
|
422
|
+
return { error: `has no field ${stranger}: only ${names.join(', ')}`, path };
|
|
423
|
+
}
|
|
345
424
|
/** @type {{ [field: string]: Value }} */
|
|
346
425
|
const fields = {};
|
|
347
426
|
for (const field of rule.fields ?? []) {
|
package/src/run-status.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* @property {string} message
|
|
21
21
|
* @property {number | null} line
|
|
22
22
|
* @property {number | null} column
|
|
23
|
+
* @property {string | null} file the path in the project of the module whose code it is in (a
|
|
24
|
+
* library's function), its line and column that module's; null for the script shown
|
|
23
25
|
*/
|
|
24
26
|
|
|
25
27
|
/** @type {Record<RunStatus['state'], string>} */
|
|
@@ -59,11 +61,12 @@ export function describeRun(run) {
|
|
|
59
61
|
export function problemsOf(header) {
|
|
60
62
|
if (!header) return [];
|
|
61
63
|
/** @type {Problem[]} */
|
|
62
|
-
const problems = header.warnings.map(({ message, line, column }) => ({
|
|
64
|
+
const problems = header.warnings.map(({ message, line, column, file }) => ({
|
|
63
65
|
severity: 'warning',
|
|
64
66
|
message,
|
|
65
67
|
line,
|
|
66
68
|
column,
|
|
69
|
+
file: file ?? null,
|
|
67
70
|
}));
|
|
68
71
|
const { error } = header;
|
|
69
72
|
if (error) {
|
|
@@ -72,6 +75,7 @@ export function problemsOf(header) {
|
|
|
72
75
|
message: error.message,
|
|
73
76
|
line: error.line,
|
|
74
77
|
column: error.column,
|
|
78
|
+
file: null,
|
|
75
79
|
});
|
|
76
80
|
}
|
|
77
81
|
return problems;
|
package/src/settings.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// What a person set in a host's panels (the panes' sizes, whether they show), kept where the
|
|
2
2
|
// host keeps such things. The playground keeps it in the page's localStorage (localSettings),
|
|
3
|
-
// the VS Code extension and the desktop app their own way, each on memorySettings
|
|
4
|
-
// keeps nothing gives memorySettings alone.
|
|
3
|
+
// the VS Code extension and the desktop app their own way, each on keptSettings (memorySettings
|
|
4
|
+
// that tell the host each change); a host that keeps nothing gives memorySettings alone.
|
|
5
5
|
//
|
|
6
6
|
// Settings are reactive: a pane reads its value in a $derived, and follows it as it is set,
|
|
7
7
|
// wherever from. A size dragged is the pane's own until the drag is done, and set then, once.
|
|
@@ -30,6 +30,25 @@ export function memorySettings(values = {}) {
|
|
|
30
30
|
return { get: (key) => kept.get(key), set: (key, value) => void kept.set(key, value) };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Settings a host keeps: `memorySettings` from `values`, the host told each change through
|
|
35
|
+
* `keep` (the VS Code extension's page sends it to the extension, the desktop app writes it to
|
|
36
|
+
* its settings file). Each host wrapped memorySettings this way itself.
|
|
37
|
+
* @param {Record<string, unknown>} values what the host kept
|
|
38
|
+
* @param {(key: string, value: unknown) => void} keep
|
|
39
|
+
* @returns {Settings}
|
|
40
|
+
*/
|
|
41
|
+
export function keptSettings(values, keep) {
|
|
42
|
+
const settings = memorySettings(values);
|
|
43
|
+
return {
|
|
44
|
+
get: settings.get,
|
|
45
|
+
set(key, value) {
|
|
46
|
+
settings.set(key, value);
|
|
47
|
+
keep(key, value);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
/**
|
|
34
53
|
* Settings kept in `storage` (the page's localStorage) as one JSON object under `name`. A page
|
|
35
54
|
* that may not keep anything there (a private window's quota, a policy), or finds no JSON
|
|
@@ -39,36 +58,51 @@ export function memorySettings(values = {}) {
|
|
|
39
58
|
* @returns {Settings}
|
|
40
59
|
*/
|
|
41
60
|
export function localSettings(name = 'kupcad.settings', storage = globalThis.localStorage) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const all = { ...read(storage, name), [key]: value };
|
|
49
|
-
storage?.setItem(name, JSON.stringify(all));
|
|
50
|
-
} catch {
|
|
51
|
-
// Kept in memory: the page may not store anything
|
|
52
|
-
}
|
|
53
|
-
},
|
|
61
|
+
const read = () => {
|
|
62
|
+
try {
|
|
63
|
+
return settingsObject(storage?.getItem(name));
|
|
64
|
+
} catch {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
54
67
|
};
|
|
68
|
+
return keptSettings(read(), (key, value) => {
|
|
69
|
+
try {
|
|
70
|
+
storage?.setItem(name, JSON.stringify({ ...read(), [key]: value }));
|
|
71
|
+
} catch {
|
|
72
|
+
// Kept in memory: the page may not store anything
|
|
73
|
+
}
|
|
74
|
+
});
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
/**
|
|
58
|
-
* The
|
|
59
|
-
*
|
|
60
|
-
* @param {string}
|
|
78
|
+
* The settings `text` holds as JSON: its object; an empty one for no text, text that isn't
|
|
79
|
+
* JSON, or JSON that isn't an object (the extension's page and the playground each read it so).
|
|
80
|
+
* @param {string | null | undefined} text
|
|
61
81
|
* @returns {Record<string, unknown>}
|
|
62
82
|
*/
|
|
63
|
-
function
|
|
83
|
+
export function settingsObject(text) {
|
|
64
84
|
try {
|
|
65
|
-
const parsed = JSON.parse(
|
|
85
|
+
const parsed = JSON.parse(text || '{}');
|
|
66
86
|
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
67
87
|
} catch {
|
|
68
88
|
return {};
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The keys a preview panel keeps its layout under, each with `prefix` (the preview's are
|
|
94
|
+
* `preview.params`, the playground's `playground.params`): the parameters pane's width, whether
|
|
95
|
+
* it and the panel show, the console's height and whether it is open. Two panels listed them.
|
|
96
|
+
* @param {string} prefix
|
|
97
|
+
*/
|
|
98
|
+
export const paneKeys = (prefix) => ({
|
|
99
|
+
params: `${prefix}.params`,
|
|
100
|
+
paramsOpen: `${prefix}.paramsOpen`,
|
|
101
|
+
panelOpen: `${prefix}.panelOpen`,
|
|
102
|
+
console: `${prefix}.console`,
|
|
103
|
+
open: `${prefix}.consoleOpen`,
|
|
104
|
+
});
|
|
105
|
+
|
|
72
106
|
/**
|
|
73
107
|
* A pane's size kept under `key`: a finite number of pixels, else `fallback` (none kept, or
|
|
74
108
|
* what was kept is no size).
|
package/src/viewer/appearance.js
CHANGED
|
@@ -37,7 +37,8 @@ export function applyRenderMode(model, mode, edges, theme) {
|
|
|
37
37
|
const material = /** @type {MeshStandardMaterial} */ (shape.mesh.material);
|
|
38
38
|
const xray = mode === 'xray';
|
|
39
39
|
material.visible = mode !== 'wireframe';
|
|
40
|
-
|
|
40
|
+
// A drawing's shapes always: drawn in their order with its outlines (model.js)
|
|
41
|
+
material.transparent = model.kind === '2d' || xray || shape.opacity < 1;
|
|
41
42
|
material.opacity = xray ? Math.min(shape.opacity, XRAY_OPACITY) : shape.opacity;
|
|
42
43
|
material.depthWrite = !xray;
|
|
43
44
|
material.needsUpdate = true;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The KupCAD brand's colours, which tokens.css paints the panels in and the viewer's theme
|
|
2
|
+
// paints a picked part and the edges in: one place for both, in the viewer, which any host may
|
|
3
|
+
// take alone (tokens.test.js holds the CSS, which can't import it, to this).
|
|
4
|
+
|
|
5
|
+
export const BRAND = Object.freeze({
|
|
6
|
+
/** Orangered: the accent, a picked part. */
|
|
7
|
+
orange: '#ff5f14',
|
|
8
|
+
/** The text, the dark background, the edges. */
|
|
9
|
+
black: '#1a1b1c',
|
|
10
|
+
white: '#ffffff',
|
|
11
|
+
/** Gainsboro: the surfaces, the light text on dark. */
|
|
12
|
+
gainsboro: '#e3e7eb',
|
|
13
|
+
});
|
|
@@ -15,6 +15,12 @@ import {
|
|
|
15
15
|
/** @import { Box3 } from 'three' */
|
|
16
16
|
/** @import { Environment, Plate, Theme } from './types.js' */
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Groups drawn in this order, whatever their objects' own: the floor, a drawing lying on it,
|
|
20
|
+
* then the axes over both. A drawing is flat on the floor, where depth can't tell its shapes
|
|
21
|
+
* apart, so they are drawn in order too (model.js).
|
|
22
|
+
*/
|
|
23
|
+
export const LAYERS = { floor: 0, drawing: 1, axes: 2 };
|
|
18
24
|
/** A grid has this many cells along its side. */
|
|
19
25
|
const GRID_CELLS = 20;
|
|
20
26
|
/** A plate's grid has a line every this many millimetres. */
|
|
@@ -40,6 +46,7 @@ const AXIS_COLORS = /** @type {const} */ (['#e5484d', '#30a46c', '#3e63dd']);
|
|
|
40
46
|
*/
|
|
41
47
|
export function environmentOf(kind, box, plate, theme) {
|
|
42
48
|
const group = new Group();
|
|
49
|
+
group.renderOrder = LAYERS.floor;
|
|
43
50
|
if (kind === 'grid') {
|
|
44
51
|
const side = gridSide(box);
|
|
45
52
|
group.add(lines(side, side, side / GRID_CELLS, theme.grid));
|
|
@@ -71,6 +78,7 @@ export function axesOf(kind, box, plate) {
|
|
|
71
78
|
[0, 0, AXES_LIFT_MM, 0, 0, side * Z_AXIS_SHARE],
|
|
72
79
|
];
|
|
73
80
|
const group = new Group();
|
|
81
|
+
group.renderOrder = LAYERS.axes;
|
|
74
82
|
for (const [i, positions] of ends.entries()) {
|
|
75
83
|
const geometry = new BufferGeometry().setAttribute(
|
|
76
84
|
'position',
|