@kupcad/ui 0.0.0-stage → 0.1.1

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.
Files changed (45) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +281 -2
  3. package/package.json +65 -3
  4. package/src/ConsolePanel.svelte +139 -0
  5. package/src/ExportMenu.svelte +36 -0
  6. package/src/ParamField.svelte +390 -0
  7. package/src/ParamsPanel.svelte +216 -0
  8. package/src/PreviewPanel.svelte +372 -0
  9. package/src/ShortcutsDialog.svelte +170 -0
  10. package/src/Splitter.svelte +148 -0
  11. package/src/Viewer.svelte +175 -0
  12. package/src/ViewerPane.svelte +745 -0
  13. package/src/base64.js +16 -0
  14. package/src/console.js +78 -0
  15. package/src/controls.css +183 -0
  16. package/src/index.js +40 -0
  17. package/src/measures.js +52 -0
  18. package/src/params.js +348 -0
  19. package/src/range.js +19 -0
  20. package/src/recent.js +44 -0
  21. package/src/run-status.js +78 -0
  22. package/src/settings.js +82 -0
  23. package/src/shortcuts.js +59 -0
  24. package/src/theme.js +131 -0
  25. package/src/tokens.css +70 -0
  26. package/src/values.svelte.js +13 -0
  27. package/src/view-settings.js +81 -0
  28. package/src/viewer/appearance.js +90 -0
  29. package/src/viewer/camera.js +100 -0
  30. package/src/viewer/dispose.js +30 -0
  31. package/src/viewer/environment.js +145 -0
  32. package/src/viewer/faces.js +311 -0
  33. package/src/viewer/hover-mark.js +125 -0
  34. package/src/viewer/index.js +7 -0
  35. package/src/viewer/model.js +293 -0
  36. package/src/viewer/motion.js +96 -0
  37. package/src/viewer/section.js +166 -0
  38. package/src/viewer/testing.js +90 -0
  39. package/src/viewer/theme.js +32 -0
  40. package/src/viewer/types.d.ts +136 -0
  41. package/src/viewer/view-cube.js +314 -0
  42. package/src/viewer/viewer.js +1027 -0
  43. package/src/viewer/wheel.js +55 -0
  44. package/src/viewport-socket.js +64 -0
  45. package/src/vscode.css +49 -0
package/src/base64.js ADDED
@@ -0,0 +1,16 @@
1
+ // Bytes as base64, which the page's messages carry a screenshot in.
2
+
3
+ /** How many bytes go to String.fromCharCode at once: it takes only so many arguments. */
4
+ const CHUNK_BYTES = 0x8000;
5
+
6
+ /**
7
+ * @param {Uint8Array} bytes
8
+ * @returns {string}
9
+ */
10
+ export function toBase64(bytes) {
11
+ let text = '';
12
+ for (let at = 0; at < bytes.length; at += CHUNK_BYTES) {
13
+ text += String.fromCharCode(...bytes.subarray(at, at + CHUNK_BYTES));
14
+ }
15
+ return btoa(text);
16
+ }
package/src/console.js ADDED
@@ -0,0 +1,78 @@
1
+ // The console's lines, as OpenSCAD's console keeps them: each run of the script shown (a line
2
+ // saying it ran and how it went, then what it printed, its warnings and its error, a line of
3
+ // the script beside each that names one) and the host's own log, in the order they came, the
4
+ // oldest dropped past MAX_LINES.
5
+
6
+ import { describeRun, runStatusOf } from './run-status.js';
7
+
8
+ /** @import { FrameHeader } from '@kupcad/protocol/types' */
9
+
10
+ /** The most lines kept: past them the oldest go. */
11
+ export const MAX_LINES = 2000;
12
+
13
+ /**
14
+ * A line of the console: of a run (`run`, what it printed, a warning, its error) or of the
15
+ * host's log; with the script's line and column it names (1-based, the column in UTF-8 bytes),
16
+ * null for none.
17
+ * @typedef {object} ConsoleLine
18
+ * @property {'run' | 'output' | 'warning' | 'error' | 'log'} kind
19
+ * @property {string} text
20
+ * @property {number | null} line
21
+ * @property {number | null} column
22
+ */
23
+
24
+ /**
25
+ * @param {ConsoleLine['kind']} kind
26
+ * @param {string} text
27
+ * @param {number | null} [line]
28
+ * @param {number | null} [column]
29
+ * @returns {ConsoleLine}
30
+ */
31
+ const lineOf = (kind, text, line = null, column = null) => ({ kind, text, line, column });
32
+
33
+ /**
34
+ * `text`'s lines, without the end its last one has.
35
+ * @param {string} text
36
+ */
37
+ const linesIn = (text) => text.replace(/\r?\n$/, '').split(/\r?\n/);
38
+
39
+ /**
40
+ * The lines of the run whose frame is `header`, of the script called `name`.
41
+ * @param {FrameHeader} header
42
+ * @param {string} name
43
+ * @returns {ConsoleLine[]}
44
+ */
45
+ export function linesOfRun(header, name) {
46
+ const lines = [lineOf('run', `${name}: ${describeRun(runStatusOf(header))}`)];
47
+ if (header.output) lines.push(...linesIn(header.output).map((text) => lineOf('output', text)));
48
+ if (header.outputCut) {
49
+ lines.push(lineOf('log', 'It printed more than a run keeps: the rest is left out.'));
50
+ }
51
+ for (const { message, line, column } of header.warnings) {
52
+ lines.push(lineOf('warning', `Warning: ${message}`, line, column));
53
+ }
54
+ const { error } = header;
55
+ if (error) lines.push(lineOf('error', `Error: ${error.message}`, error.line, error.column));
56
+ return lines;
57
+ }
58
+
59
+ /**
60
+ * The lines of the host's log in `text`.
61
+ * @param {string} text
62
+ * @returns {ConsoleLine[]}
63
+ */
64
+ export const linesOfLog = (text) =>
65
+ linesIn(text)
66
+ .filter((line) => line.trim() !== '')
67
+ .map((line) => lineOf('log', line));
68
+
69
+ /**
70
+ * `lines` with `added` after them, at most MAX_LINES: the oldest go.
71
+ * @param {readonly ConsoleLine[]} lines
72
+ * @param {readonly ConsoleLine[]} added
73
+ * @returns {ConsoleLine[]}
74
+ */
75
+ export function appendLines(lines, added) {
76
+ const all = [...lines, ...added];
77
+ return all.length > MAX_LINES ? all.slice(all.length - MAX_LINES) : all;
78
+ }
@@ -0,0 +1,183 @@
1
+ /*
2
+ A control as the panels draw theirs (a button, a select, a field), by the --kc-* tokens: what a
3
+ host gives its own beside them to look alike, a toolbar's actions say. A panel's own styles are
4
+ scoped to its markup, so a host's buttons in a panel's toolbar got none of them. A range input
5
+ and a checkbox are drawn here too (.kc-range, .kc-check), the same in each browser and webview:
6
+ left to the browser, a slider is its own blue, and `accent-color` puts a white tick on the
7
+ orange. Classes, not elements: a host's page keeps its own inputs as they are.
8
+ */
9
+ .kc-control {
10
+ font: inherit;
11
+ color: var(--kc-fg);
12
+ background: var(--kc-input-bg);
13
+ border: 1px solid var(--kc-control-border);
14
+ border-radius: var(--kc-radius);
15
+ padding: 2px 8px;
16
+ }
17
+
18
+ button.kc-control,
19
+ select.kc-control {
20
+ cursor: pointer;
21
+ }
22
+
23
+ button.kc-control:hover:not(:disabled) {
24
+ background: var(--kc-hover);
25
+ }
26
+
27
+ /*
28
+ A toggle while on, and a pane's toggle while the pane shows: tinted, its edge in the accent.
29
+ Several are on at once (a view's edges, axes and grid, the panes shown), and filled they made
30
+ a bar of solid orange; the fill is kept for what there is one of: the bar's leading action.
31
+ */
32
+ .kc-control[aria-pressed='true'],
33
+ .kc-control[aria-expanded='true'] {
34
+ color: var(--kc-fg);
35
+ background: var(--kc-accent-subtle);
36
+ border-color: var(--kc-accent);
37
+ }
38
+
39
+ button.kc-control[aria-pressed='true']:hover:not(:disabled),
40
+ button.kc-control[aria-expanded='true']:hover:not(:disabled) {
41
+ background: var(--kc-accent-subtle);
42
+ box-shadow: inset 0 0 0 1px var(--kc-accent);
43
+ }
44
+
45
+ /* The one action a bar leads with (Save): the accent's fill */
46
+ .kc-control.kc-primary {
47
+ font-weight: 600;
48
+ color: var(--kc-accent-fg);
49
+ background: var(--kc-accent);
50
+ border-color: var(--kc-accent);
51
+ }
52
+
53
+ button.kc-control.kc-primary:hover:not(:disabled) {
54
+ background: var(--kc-accent-hover);
55
+ border-color: var(--kc-accent-hover);
56
+ }
57
+
58
+ /* With nothing to do, as any control with nothing to do: faded orange read as a colour of its own */
59
+ .kc-control.kc-primary:disabled {
60
+ color: var(--kc-fg);
61
+ background: var(--kc-input-bg);
62
+ border-color: var(--kc-control-border);
63
+ }
64
+
65
+ .kc-control:disabled,
66
+ .kc-range:disabled,
67
+ .kc-check:disabled {
68
+ cursor: default;
69
+ opacity: 0.5;
70
+ }
71
+
72
+ .kc-control:focus-visible,
73
+ .kc-check:focus-visible {
74
+ outline: 2px solid var(--kc-focus);
75
+ outline-offset: 1px;
76
+ }
77
+
78
+ /*
79
+ A range input: the track filled to the thumb in the accent. WebKit and Blink draw no fill of
80
+ their own, so the panel sets --kc-range-fill (how far along the thumb is, `fillOf`) as it
81
+ moves; Firefox draws it as the range's progress.
82
+ */
83
+ .kc-range {
84
+ -webkit-appearance: none;
85
+ appearance: none;
86
+ height: 18px;
87
+ margin: 0;
88
+ background: transparent;
89
+ cursor: pointer;
90
+ }
91
+ .kc-range::-webkit-slider-runnable-track {
92
+ height: 4px;
93
+ border-radius: 2px;
94
+ background: linear-gradient(
95
+ to right,
96
+ var(--kc-accent) 0 var(--kc-range-fill, 0%),
97
+ var(--kc-track) var(--kc-range-fill, 0%) 100%
98
+ );
99
+ }
100
+ .kc-range::-webkit-slider-thumb {
101
+ -webkit-appearance: none;
102
+ appearance: none;
103
+ box-sizing: border-box;
104
+ width: 14px;
105
+ height: 14px;
106
+ margin-top: -5px;
107
+ border-radius: 50%;
108
+ background: var(--kc-accent);
109
+ border: 2px solid var(--kc-bg);
110
+ box-shadow: 0 0 0 1px var(--kc-accent);
111
+ }
112
+ .kc-range::-moz-range-track {
113
+ height: 4px;
114
+ border-radius: 2px;
115
+ background: var(--kc-track);
116
+ }
117
+ .kc-range::-moz-range-progress {
118
+ height: 4px;
119
+ border-radius: 2px;
120
+ background: var(--kc-accent);
121
+ }
122
+ .kc-range::-moz-range-thumb {
123
+ box-sizing: border-box;
124
+ width: 14px;
125
+ height: 14px;
126
+ border-radius: 50%;
127
+ background: var(--kc-accent);
128
+ border: 2px solid var(--kc-bg);
129
+ box-shadow: 0 0 0 1px var(--kc-accent);
130
+ }
131
+ .kc-range:hover:not(:disabled)::-webkit-slider-thumb {
132
+ background: var(--kc-accent-hover);
133
+ }
134
+ .kc-range:hover:not(:disabled)::-moz-range-thumb {
135
+ background: var(--kc-accent-hover);
136
+ }
137
+ /* Its focus is the thumb's ring: an outline round the whole track says less of where it is */
138
+ .kc-range:focus-visible {
139
+ outline: none;
140
+ }
141
+ .kc-range:focus-visible::-webkit-slider-thumb {
142
+ box-shadow:
143
+ 0 0 0 1px var(--kc-accent),
144
+ 0 0 0 4px var(--kc-focus);
145
+ }
146
+ .kc-range:focus-visible::-moz-range-thumb {
147
+ box-shadow:
148
+ 0 0 0 1px var(--kc-accent),
149
+ 0 0 0 4px var(--kc-focus);
150
+ }
151
+
152
+ /* A checkbox: the brand black tick on the accent once checked */
153
+ .kc-check {
154
+ -webkit-appearance: none;
155
+ appearance: none;
156
+ display: inline-grid;
157
+ place-content: center;
158
+ flex: none;
159
+ width: 16px;
160
+ height: 16px;
161
+ margin: 0;
162
+ vertical-align: middle;
163
+ background: var(--kc-input-bg);
164
+ border: 1px solid var(--kc-control-border);
165
+ border-radius: var(--kc-radius);
166
+ cursor: pointer;
167
+ }
168
+ .kc-check::before {
169
+ content: '';
170
+ width: 8px;
171
+ height: 4px;
172
+ border-left: 2px solid var(--kc-accent-fg);
173
+ border-bottom: 2px solid var(--kc-accent-fg);
174
+ transform: translateY(-1px) rotate(-45deg);
175
+ visibility: hidden;
176
+ }
177
+ .kc-check:checked {
178
+ background: var(--kc-accent);
179
+ border-color: var(--kc-accent);
180
+ }
181
+ .kc-check:checked::before {
182
+ visibility: visible;
183
+ }
package/src/index.js ADDED
@@ -0,0 +1,40 @@
1
+ // @kupcad/ui, for Svelte: the viewer (Viewer), the panels a KupCAD host shows beside its editor,
2
+ // and a language server's preview (PreviewPanel, over its viewport socket, connectViewport).
3
+ // Styled by the --kc-* tokens of `@kupcad/ui/tokens.css` (or `@kupcad/ui/vscode.css` in VS
4
+ // Code's webview), which a page loads. The viewer for any host, Svelte or not, is
5
+ // `@kupcad/ui/viewer`.
6
+
7
+ export { default as ConsolePanel } from './ConsolePanel.svelte';
8
+ export { default as ExportMenu } from './ExportMenu.svelte';
9
+ export { default as ParamsPanel } from './ParamsPanel.svelte';
10
+ export { default as PreviewPanel } from './PreviewPanel.svelte';
11
+ export { default as ShortcutsDialog } from './ShortcutsDialog.svelte';
12
+ export { default as Splitter } from './Splitter.svelte';
13
+ export { default as Viewer } from './Viewer.svelte';
14
+ export { default as ViewerPane } from './ViewerPane.svelte';
15
+ export { appendLines, linesOfLog, linesOfRun } from './console.js';
16
+ export { fillOf } from './range.js';
17
+ export { problemsOf, runStatusOf } from './run-status.js';
18
+ export { localSettings, memorySettings, sizeSetting } from './settings.js';
19
+ export { VIEWER_SHORTCUTS } from './shortcuts.js';
20
+ export { watchTokens } from './theme.js';
21
+ export { plainValues } from './values.svelte.js';
22
+ export {
23
+ ENVIRONMENTS,
24
+ PROJECTIONS,
25
+ VIEW_DEFAULTS,
26
+ VIEW_KEYS,
27
+ viewChoice,
28
+ viewChoices,
29
+ } from './view-settings.js';
30
+ // What a settings page offers of the view: the viewer's render modes
31
+ export { RENDER_MODES } from './viewer/index.js';
32
+ export { connectViewport } from './viewport-socket.js';
33
+
34
+ /** @typedef {import('./console.js').ConsoleLine} ConsoleLine */
35
+ /** @typedef {import('./run-status.js').Problem} Problem */
36
+ /** @typedef {import('./run-status.js').RunStatus} RunStatus */
37
+ /** @typedef {import('./settings.js').Settings} Settings */
38
+ /** @typedef {import('./shortcuts.js').ShortcutGroup} ShortcutGroup */
39
+ /** @typedef {import('./view-settings.js').ViewChoices} ViewChoices */
40
+ /** @typedef {import('./viewport-socket.js').ViewportEvents} ViewportEvents */
@@ -0,0 +1,52 @@
1
+ // What the viewer's tooltip says of what is under the pointer: a part's name, copies, volume and
2
+ // size, a face's area and the way it faces, in the model's millimetres.
3
+
4
+ /** @import { Part } from '@kupcad/protocol/types' */
5
+ /** @import { Face } from './viewer/types.js' */
6
+
7
+ /** From this many mm³ a volume is said in cm³, as a slicer says it. */
8
+ const MM3_PER_CM3 = 1000;
9
+
10
+ /**
11
+ * `n` with at most `digits` decimals, the trailing ones left out, thousands grouped.
12
+ * @param {number} n
13
+ * @param {number} digits
14
+ */
15
+ const decimal = (n, digits) => n.toLocaleString('en', { maximumFractionDigits: digits });
16
+
17
+ /** @param {number} mm */
18
+ export const formatLength = (mm) => decimal(mm, 2);
19
+
20
+ /** @param {number} mm2 */
21
+ export const formatArea = (mm2) => `${decimal(mm2, 1)} mm²`;
22
+
23
+ /** @param {number} mm3 */
24
+ export const formatVolume = (mm3) =>
25
+ mm3 >= MM3_PER_CM3 ? `${decimal(mm3 / MM3_PER_CM3, 2)} cm³` : `${decimal(mm3, 1)} mm³`;
26
+
27
+ /**
28
+ * A part as its tooltip names it: its name, and how many copies when more than one.
29
+ * @param {Pick<Part, 'name' | 'count'>} part
30
+ */
31
+ export const partTitle = (part) => (part.count > 1 ? `${part.name} ×${part.count}` : part.name);
32
+
33
+ /**
34
+ * A part's volume and size (width × depth × height), or null when they weren't measured.
35
+ * @param {Pick<Part, 'volume' | 'size'>} part
36
+ * @returns {string | null}
37
+ */
38
+ export function partMeasures(part) {
39
+ if (part.volume === null || part.volume === undefined || !part.size) return null;
40
+ return `${formatVolume(part.volume)} · ${part.size.map(formatLength).join(' × ')} mm`;
41
+ }
42
+
43
+ /**
44
+ * A face as its tooltip says it: flat (with the axis it faces along, or its way) or curved, and
45
+ * its area.
46
+ * @param {Face} face
47
+ */
48
+ export function faceLine(face) {
49
+ if (!face.flat) return `Curved face · ${formatArea(face.area)}`;
50
+ const way = face.facing ?? `(${(face.normal ?? []).map((v) => v.toFixed(2)).join(', ')})`;
51
+ return `Flat face · ${formatArea(face.area)} · facing ${way}`;
52
+ }