@kupcad/ui 0.0.0-stage → 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 +661 -0
- package/README.md +281 -2
- package/package.json +65 -3
- package/src/ConsolePanel.svelte +139 -0
- package/src/ExportMenu.svelte +36 -0
- package/src/ParamField.svelte +390 -0
- package/src/ParamsPanel.svelte +216 -0
- package/src/PreviewPanel.svelte +372 -0
- package/src/ShortcutsDialog.svelte +170 -0
- package/src/Splitter.svelte +148 -0
- package/src/Viewer.svelte +175 -0
- package/src/ViewerPane.svelte +745 -0
- package/src/base64.js +16 -0
- package/src/console.js +78 -0
- package/src/controls.css +183 -0
- package/src/index.js +40 -0
- package/src/measures.js +52 -0
- package/src/params.js +348 -0
- package/src/range.js +19 -0
- package/src/recent.js +44 -0
- package/src/run-status.js +78 -0
- package/src/settings.js +82 -0
- package/src/shortcuts.js +59 -0
- package/src/theme.js +131 -0
- package/src/tokens.css +70 -0
- package/src/values.svelte.js +13 -0
- package/src/view-settings.js +81 -0
- package/src/viewer/appearance.js +90 -0
- package/src/viewer/camera.js +100 -0
- package/src/viewer/dispose.js +30 -0
- package/src/viewer/environment.js +145 -0
- package/src/viewer/faces.js +311 -0
- package/src/viewer/hover-mark.js +125 -0
- package/src/viewer/index.js +7 -0
- package/src/viewer/model.js +293 -0
- package/src/viewer/motion.js +96 -0
- package/src/viewer/section.js +166 -0
- package/src/viewer/testing.js +90 -0
- package/src/viewer/theme.js +32 -0
- package/src/viewer/types.d.ts +136 -0
- package/src/viewer/view-cube.js +314 -0
- package/src/viewer/viewer.js +1027 -0
- package/src/viewer/wheel.js +55 -0
- package/src/viewport-socket.js +64 -0
- package/src/vscode.css +49 -0
package/README.md
CHANGED
|
@@ -1,3 +1,282 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kupcad/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
KupCAD's UI: the viewer of a model the core made, for any host, and the Svelte components a
|
|
4
|
+
host shows around it, styled by the `--kc-*` tokens.
|
|
5
|
+
|
|
6
|
+
| Import | What it is | Needs Svelte |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| `@kupcad/ui/viewer` | the viewer, a class over three.js (`Viewer`, `ViewCube`, `LIGHT`, `DARK`) | no |
|
|
9
|
+
| `@kupcad/ui/viewer/types` | its types, for JSDoc or TypeScript | no |
|
|
10
|
+
| `@kupcad/ui` | the components (`Viewer`, `ViewerPane`, `ParamsPanel`, `ConsolePanel`, `PreviewPanel`, `Splitter`, `ExportMenu`, `ShortcutsDialog`) and what they keep (settings, the view's choices) | yes |
|
|
11
|
+
| `@kupcad/ui/tokens.css`, `vscode.css`, `controls.css` | the tokens, VS Code's theme over them, the controls' look | no |
|
|
12
|
+
| `@kupcad/ui/testing` | what jsdom lacks for the viewer, and `stubRenderer` | no |
|
|
13
|
+
|
|
14
|
+
Svelte is an optional peer: a host that imports the viewer alone needs none.
|
|
15
|
+
|
|
16
|
+
## The viewer
|
|
17
|
+
|
|
18
|
+
The scene of a model the KupCAD core made: its Solids from the GLB, or a drawing from the SVG,
|
|
19
|
+
in the script's own coordinates (millimetres, Z up), with render modes, views, a section plane
|
|
20
|
+
and part picking: a class any host can own. It imports neither Svelte nor the rest of the
|
|
21
|
+
package (`.oxlintrc.json` holds it to that), so a host that isn't Svelte's imports it alone.
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { Viewer, ViewCube, DARK } from '@kupcad/ui/viewer';
|
|
25
|
+
|
|
26
|
+
const viewer = new Viewer(canvas, { theme: DARK, environment: 'grid' });
|
|
27
|
+
await viewer.setModel(preview.model); // @kupcad/protocol's Model, from a preview frame; false
|
|
28
|
+
// when a model asked for later replaced it before it was shown
|
|
29
|
+
viewer.setRenderMode('xray'); // 'solid' | 'xray' | 'wireframe'
|
|
30
|
+
viewer.setEdges(false); // the shapes' edges hidden (a wireframe's stay)
|
|
31
|
+
viewer.setAxes(false); // the X (red), Y (green) and Z (blue) axes through the origin hidden
|
|
32
|
+
viewer.setCamera({ projection: 'orthographic', view: 'front' }); // moves there, framing the model
|
|
33
|
+
viewer.lookFrom([1, 1, 1]); // from any direction (Z up), as the view cube's corner does
|
|
34
|
+
new ViewCube(cubeCanvas, viewer, { theme: DARK }); // the cube, on a canvas of its own
|
|
35
|
+
viewer.fit(); viewer.zoom(1.25); // fits the model from where it looks; zooms in (out, below 1)
|
|
36
|
+
viewer.fit({ instant: true }); // the same at once, no move
|
|
37
|
+
const left = viewer.cameraState(); // plain numbers: what it looks at, from where, how much it sees
|
|
38
|
+
viewer.restoreCamera(left); // there again at once, through either projection, as much seen
|
|
39
|
+
viewer.setSection({ axis: 'z', offset: 5 }); // cut away what is above z = 5, the cut capped
|
|
40
|
+
viewer.addEventListener('section', (event) => event.detail.section); // the wheel with Option moved it
|
|
41
|
+
viewer.addEventListener('select', (event) => console.log(event.detail.part)); // a click (the main
|
|
42
|
+
// button; the others pan and zoom), or null
|
|
43
|
+
viewer.select('bolt'); // tints every copy of the part
|
|
44
|
+
viewer.bounds; // { min, max } in mm: what a section's offset ranges over
|
|
45
|
+
viewer.hoverAt(x, y); // { part, face: { flat, area, normal, facing } }: what is under a point
|
|
46
|
+
viewer.addEventListener('hover', (event) => event.detail); // the same, with x and y, as the pointer moves
|
|
47
|
+
viewer.kind; // '3d' or '2d', the model shown's (a drawing isn't cut), or null for none
|
|
48
|
+
const png = await viewer.screenshot();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For Svelte, the component around it (`@kupcad/ui`'s `Viewer`):
|
|
52
|
+
|
|
53
|
+
```svelte
|
|
54
|
+
<script>
|
|
55
|
+
import { Viewer } from '@kupcad/ui';
|
|
56
|
+
let selected = $state(null);
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<Viewer model={preview.model} renderMode="solid" bind:selected onmodel={(viewer) => viewer.bounds} />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
* **The model:** each copy of a part is a node of the GLB named as the frame's `parts[].nodes`
|
|
63
|
+
say, so the viewer knows every mesh's part; ghosts are seen through and can't be picked. A
|
|
64
|
+
solid the script highlights (`.highlight`, which the core tags in the GLB) is drawn in the
|
|
65
|
+
theme's `highlight` colour and seen through whatever hides it (a see-through copy drawn only
|
|
66
|
+
behind what is in front), in solid mode; it is a part as any other. A
|
|
67
|
+
drawing is seen from the top, orthographic, each filled shape named by its id. A model that
|
|
68
|
+
follows keeps the camera where it is, a model of nothing between them too (one that failed);
|
|
69
|
+
the first, or one of another kind than the last shown, is framed, a solid after a drawing as
|
|
70
|
+
the last solid was seen. The camera's depth takes in the model and what it stands on, however
|
|
71
|
+
small the model and wherever the camera moves. A model without the selected part drops the
|
|
72
|
+
selection, with a `select` event of null.
|
|
73
|
+
* **Under the pointer** the viewer marks and says, a frame at most (a `hover` event, null over
|
|
74
|
+
nothing and while a button drags the camera), the part and, on a solid, the face. The copy under
|
|
75
|
+
it glows a little in the selection's colour and its face more (`hover-mark.js`: light added to
|
|
76
|
+
what is drawn there, so the mark shows on a part of any colour). The face is what its feature edges
|
|
77
|
+
bound (`faces.js`: triangles joined where they meet at less than the edges' 20°), flat when it
|
|
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, as the pointer first meets it, in typed
|
|
80
|
+
arrays (a third of a second for a million triangles), and each face once, its area kept while
|
|
81
|
+
the mesh doesn't move. A ghost, seen through, is under nothing, and neither is what a section
|
|
82
|
+
cut away.
|
|
83
|
+
* **The view cube** (`ViewCube`, which the Svelte component puts in its corner unless
|
|
84
|
+
`cube={false}`) is turned as the camera is; its faces (labelled TOP, FRONT, RIGHT, ..., each
|
|
85
|
+
upright from its view), edges and corners, 26 ways to look, take the camera there when
|
|
86
|
+
clicked, the one under the pointer lit in the selection's colour.
|
|
87
|
+
* **The camera** moves to a view over a moment (`motionMs`, 250 by default; at once when the page
|
|
88
|
+
asks for reduced motion), turning round the model. Turned by hand it is in no named view (`view`
|
|
89
|
+
is null), which a `view` event says, as it says every change. Its controls are Orca Slicer's:
|
|
90
|
+
the left button turns, the middle and right ones pan; once it has the focus (a click gives it),
|
|
91
|
+
`0`–`6` (alone, or with Ctrl or Cmd) look from the iso, top, bottom, front, back, left and
|
|
92
|
+
right, `F` fits, `I` and `O` zoom in and out.
|
|
93
|
+
* **The wheel** (or two fingers on a trackpad: `wheel.js`) zooms, as a pinch does, and while
|
|
94
|
+
the left or right button drags too, though not with the middle one held (the wheel, pressed,
|
|
95
|
+
turns a little). With Shift it pans, the model moving as a page's content scrolls (Orca's
|
|
96
|
+
Shift with two fingers on a Mac); with Option (Alt) it moves the section along its axis, a
|
|
97
|
+
solid without one cut across Z from its top (Orca's Alt), which a `section` event says; with
|
|
98
|
+
Cmd it turns the view, as a drag does. Ctrl is a pinch's: a browser sends one as the wheel
|
|
99
|
+
with Ctrl, so Ctrl and the wheel zoom too.
|
|
100
|
+
* **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.
|
|
102
|
+
* **A section** clips every material of a solid's model at its plane and caps the cut where it
|
|
103
|
+
runs inside a solid (three.js's stencil technique), but in a wireframe; a drawing isn't cut. A
|
|
104
|
+
part cut away isn't picked.
|
|
105
|
+
* **Draco:** a model built with `--draco` needs `dracoDecoderPath`, where three.js's decoder files
|
|
106
|
+
are; the decoder is loaded only when a model needs it.
|
|
107
|
+
* **Themes** are colours (`LIGHT`, `DARK`, or a host's own, as the VS Code webview maps its
|
|
108
|
+
`--vscode-*` variables). `LIGHT` and `DARK` are the KupCAD brand's: a selection in its
|
|
109
|
+
orangered on the neutrals of the package's tokens, a section's cap in dodgerblue so no
|
|
110
|
+
other warm colour reads as the selection. A line takes the colour for what it is drawn on: `edges` over a
|
|
111
|
+
solid's faces, `wire` on the background (a wireframe's edges, a drawing's outlines), so a dark
|
|
112
|
+
theme's dark edges don't vanish in a wireframe.
|
|
113
|
+
|
|
114
|
+
The component's canvas is named for a screen reader by `label` ("The model" by default). `onmodel` is called for each model shown, not for one a later model replaced before it
|
|
115
|
+
was. Without `onerror`, a model it can't show is an unhandled rejection; an error of `onmodel`'s
|
|
116
|
+
is never passed to `onerror`.
|
|
117
|
+
|
|
118
|
+
A host's tests that mount the viewer under jsdom add `@kupcad/ui/testing` to Vitest's
|
|
119
|
+
`setupFiles`: what jsdom lacks of a browser and the viewer uses. Its `stubRenderer()` draws in
|
|
120
|
+
place of WebGL (`options.renderer`) and keeps what it was asked: the frames, the last scene and
|
|
121
|
+
camera, whether it was disposed, and how many renderers were made and let their WebGL context go
|
|
122
|
+
(a viewer and its cube let theirs go when disposed, not when the page collects them: a page has
|
|
123
|
+
few).
|
|
124
|
+
|
|
125
|
+
## The components
|
|
126
|
+
|
|
127
|
+
The Svelte components a KupCAD host shows beside its editor: the viewer with its toolbar and
|
|
128
|
+
part list, the parameter panel, the console, and a language server's preview. They take
|
|
129
|
+
@kupcad/protocol's shapes and give back values; they hold no engine and fetch nothing, so a
|
|
130
|
+
host wires them to its own: the playground to @kupcad/wasm's worker, the VS Code extension's
|
|
131
|
+
webview and the desktop app to the language server.
|
|
132
|
+
|
|
133
|
+
```svelte
|
|
134
|
+
<script>
|
|
135
|
+
import '@kupcad/ui/tokens.css'; // or '@kupcad/ui/vscode.css' in VS Code's webview
|
|
136
|
+
import { ParamsPanel, ViewerPane } from '@kupcad/ui';
|
|
137
|
+
|
|
138
|
+
let { preview } = $props(); // a protocol Preview: a frame read
|
|
139
|
+
let overrides = $state({});
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<ViewerPane model={preview.model} onscreenshot={(png) => save(png)} />
|
|
143
|
+
<ParamsPanel
|
|
144
|
+
schema={preview.header.params.schema}
|
|
145
|
+
values={preview.header.params.values}
|
|
146
|
+
error={preview.header.error}
|
|
147
|
+
bind:overrides
|
|
148
|
+
onchange={(values) => host.setParams(values)}
|
|
149
|
+
/>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
* **`ViewerPane`**: the viewer with its view cube in the corner, and a panel beside it that
|
|
153
|
+
the toolbar's Panel button hides (`bind:panel`, for a host to keep): the view's controls in its top half (Solid, Wire, X-ray;
|
|
154
|
+
Edges, Axes, Grid and Plate toggles; Fit, Persp, and the Iso, Top, Front and Right views, the
|
|
155
|
+
one looked from pressed, a turn by hand pressing none), the model's parts in its bottom half,
|
|
156
|
+
each with its colour and copies (a click on one selects its copies, as a click in the viewer
|
|
157
|
+
does: `bind:selected`). 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
|
|
158
|
+
`actions` snippet (a host's own buttons, the Export menu), the Panel button, and a `toggles`
|
|
159
|
+
snippet after it (a host's toggles of its own panes beside the pane, the Parameters button;
|
|
160
|
+
`.kc-control` with `aria-expanded` gives one the Panel button's look). Below, a status
|
|
161
|
+
line from `status` (`runStatusOf(header)` makes it of a frame's header: Running…, then Ready or
|
|
162
|
+
Failed, with the run's time and its warnings) over the run's problems from `problems`
|
|
163
|
+
(`problemsOf(header)`: why it failed, then its warnings), a few lines high; one with a line,
|
|
164
|
+
clicked, goes to `onproblem`, for the host to show the script there.
|
|
165
|
+
By the pointer a tooltip says what is under it: the part, its copies, volume and size (the
|
|
166
|
+
frame's), and the face, flat with its area and the way it faces, or curved (`measures.js`).
|
|
167
|
+
`bind:viewer` gives the viewer itself. The controls show how the viewer looks (a drawing
|
|
168
|
+
switches it to the top view, orthographic; a turn of the wheel with Option moves the section,
|
|
169
|
+
and its slider), and a section stays where it was put through an edit of the script, while the
|
|
170
|
+
model still reaches it. A model of another `subject` (another script: the preview gives the
|
|
171
|
+
document's URI) is framed: as its camera was left when it was shown last, else whole from
|
|
172
|
+
where the camera looks; the same subject's next model (an edit, a parameter) keeps the camera.
|
|
173
|
+
The cameras are the pane's own, in memory while it is there, the least recently shown let go
|
|
174
|
+
past 32 (`recent.js`); without a subject, every model keeps the camera.
|
|
175
|
+
How the view looks, as the person chose it (the render mode, edges, axes, the floor, the
|
|
176
|
+
projection), is kept in the host's `settings` under the view.* keys (`VIEW_KEYS`), and followed
|
|
177
|
+
as they are set wherever from: a host's settings page shows and sets the same keys
|
|
178
|
+
(`viewChoices(settings)`, `viewChoice(settings, name)`, each a valid value else its default,
|
|
179
|
+
`VIEW_DEFAULTS`, and the values offered: `RENDER_MODES`, `ENVIRONMENTS`, `PROJECTIONS`). A
|
|
180
|
+
drawing's top orthographic view is no choice: it isn't kept, and the next solid is seen as
|
|
181
|
+
chosen. At the end of the bar below, a button lists the keyboard shortcuts (`ShortcutsDialog`):
|
|
182
|
+
a host's own groups (`shortcuts`) before the viewer's (`VIEWER_SHORTCUTS`: its keys, its
|
|
183
|
+
pointer and wheel, named as the system names them); `bind:shortcutsOpen` opens it from a host's
|
|
184
|
+
menu too.
|
|
185
|
+
* **`ParamsPanel`**: a control per parameter of the schema, each `group:` a section under its
|
|
186
|
+
title that folds away (a header bar of its own, `--kc-section-bg` and `--kc-section-fg`, VS
|
|
187
|
+
Code's side bar section headers in its webview), the parameters of no group first: a slider with a
|
|
188
|
+
number for one between a `min` and a `max`, a number field, a select of the `in:` choices, a
|
|
189
|
+
toggle, a text field; a tuple's places and a Map's fields side by side, each labelled on top
|
|
190
|
+
(`partsOf`: a place's `title:`, else its index; a field's, else its name); and a list as its
|
|
191
|
+
elements, each a control (a list of tuples or of Maps: a block of them) with a remove, and an
|
|
192
|
+
add that copies the last (`newElement`, else `blankOf`), each as its `min_items` and
|
|
193
|
+
`max_items` allow (`canRemove`, `canAdd`). A refusal names the place (`[1][:depth] must be at
|
|
194
|
+
least 0.1`) and marks the control there, the panel's own check's (`validate`, which gives the
|
|
195
|
+
path) and the run's (the frame's `error.path`). `step:` and `unit:` as given. A slider without a `step:` moves by 1
|
|
196
|
+
(`stepOf`), and one over a range narrower than 1 by the power of ten that gives it ten
|
|
197
|
+
positions at least; a value typed in its field is taken whatever its step. Each parameter is rows of its own, a line
|
|
198
|
+
between one and the next: its `title:` (its name where it has none, and as the title's tooltip),
|
|
199
|
+
its `description:` (the control's description for a screen reader too), then its control the
|
|
200
|
+
panel's whole width. Each shows the value set in the panel, else
|
|
201
|
+
the one the run took. A value outside the schema's range is refused in the panel, saying why,
|
|
202
|
+
and never leaves it; one the run refused (the frame's `error.param`) is shown on its control.
|
|
203
|
+
A value set is marked and can be reverted, which takes its refusal away too, as setting it
|
|
204
|
+
back to its own value does (the one it showed before the panel set it: the run's, so a
|
|
205
|
+
host's preset's too, else the script's default; all at once:
|
|
206
|
+
Reset all, in a bar at the panel's top, held there as it scrolls, which says how many are
|
|
207
|
+
set; the button keeps its room while none is, so nothing under the bar moves as the first is
|
|
208
|
+
set or the last reverted, and a host's column leaves the bar its top); a refusal is the
|
|
209
|
+
control's description for a screen reader too, after the parameter's (`aria-describedby`,
|
|
210
|
+
`aria-invalid`). A choice
|
|
211
|
+
with no value yet (the script computes its default, and no run took one) shows none.
|
|
212
|
+
`overrides` (bindable) is what the panel sets; `onchange` gets it at each change, for the host
|
|
213
|
+
to send to the core (`plainValues`, a state's values as plain data a message or a worker can
|
|
214
|
+
carry).
|
|
215
|
+
* **`ConsolePanel`** (`console.js`): the console under a model, as OpenSCAD's: each run
|
|
216
|
+
(`linesOfRun`: a line of how it went, then what it printed, its warnings and its error, a line
|
|
217
|
+
that names a place clickable, `onreveal`) and a host's log (`linesOfLog`), the newest
|
|
218
|
+
followed unless the person scrolled up, the oldest dropped past `MAX_LINES`
|
|
219
|
+
(`appendLines`); cleared, folded to its bar, and as tall as the person drags it.
|
|
220
|
+
* **`PreviewPanel`**: a language server's preview, for a host that runs `kupcad lsp` (the VS
|
|
221
|
+
Code extension, the desktop app): the model of the document the host says it shows, as the
|
|
222
|
+
server's viewport socket sends it (`connectViewport`, which opens the socket again when it
|
|
223
|
+
closes; `viewportUrl` changed, a server started again on another port, it follows it, what it
|
|
224
|
+
shows and its console kept), the run's problems, and the parameters with the document's
|
|
225
|
+
presets, which the toolbar's Parameters button hides for the model's width. It talks to the
|
|
226
|
+
host as a `ViewerHost` (`@kupcad/protocol`'s `HostMessage` and `ViewerMessage`): the panel's
|
|
227
|
+
values, a preset, an export (in the format its toolbar's `ExportMenu` chose, those the model's
|
|
228
|
+
kind can be written in), a screenshot (base64) or a problem's place go to the host, which
|
|
229
|
+
gives them to the server or acts on them. Its console (`console`, by default shown) keeps each
|
|
230
|
+
run of the document shown, a frame sent again once, and the lines the host's `log` messages
|
|
231
|
+
give. A host that keeps no presets passes
|
|
232
|
+
`presets={false}`, and none are offered.
|
|
233
|
+
* **`ShortcutsDialog`** (`shortcuts.js`): the keys and the pointer's moves a person can't see on
|
|
234
|
+
the screen, by group (`{ title, keys: [keys, what][], note? }`), in a modal dialog while
|
|
235
|
+
`open` (bindable); Escape or its close button closes it, the focus back where it was.
|
|
236
|
+
`ViewerPane` opens it from its bottom bar; `PreviewPanel` and the playground pass a host's
|
|
237
|
+
`shortcuts` and `shortcutsOpen` through.
|
|
238
|
+
* **Layout** (`Splitter`, `settings.js`): the bar between two panes that resizes one of them,
|
|
239
|
+
dragged or with the keys (the arrows, Shift for longer steps, Home and End), double-clicked
|
|
240
|
+
back, a separator a screen reader knows (`role="separator"`, its value its size); it takes no
|
|
241
|
+
room, the panes keep their borders. The sizes are kept in a host's `Settings` (`get`, `set`),
|
|
242
|
+
which are reactive: a pane reads its size or state in a `$derived` and follows it as it is
|
|
243
|
+
set, wherever from (another pane on the same settings, the host), with no copy of its own; a
|
|
244
|
+
size dragged is the pane's own until the drag is done, and set then, once. Each host's are
|
|
245
|
+
built on `memorySettings` (a `SvelteMap`), the page's life alone: `localSettings` writes each
|
|
246
|
+
change to the page's localStorage too, as one JSON object, kept in memory where the page may
|
|
247
|
+
not store. `sizeSetting` reads a kept size or the pane's own. `PreviewPanel` keeps its
|
|
248
|
+
parameters' width and whether they show, whether the view's panel shows, and its console's
|
|
249
|
+
height and state (`preview.*`) in the `settings` it is given, and follows other settings the
|
|
250
|
+
host gives it.
|
|
251
|
+
* **Tokens**: the panels are styled by `--kc-*` variables, light and dark as the page's colour
|
|
252
|
+
scheme is (`tokens.css`): the KupCAD brand, its orangered accent on white or the brand black,
|
|
253
|
+
gainsboro for the surfaces. The accent is a fill, the brand black the text on it
|
|
254
|
+
(`--kc-accent-fg`: white on the orange is too faint), and orange as text on the background is
|
|
255
|
+
`--kc-accent-text`, a darker orange in light; `--kc-control-border` is a field's edge,
|
|
256
|
+
`--kc-focus` its focus ring, `--kc-accent-subtle` what is chosen in a list. `tokens.test.js`
|
|
257
|
+
holds every pair to WCAG's 4.5:1 for text and 3:1 for a control's edge, in both schemes, and
|
|
258
|
+
every colour token to a mapping in `vscode.css`.
|
|
259
|
+
A host sets its own, as `vscode.css` maps VS Code's theme over them: the webview follows the
|
|
260
|
+
editor's theme, never the brand.
|
|
261
|
+
`--kc-viewer-*` theme the viewer (`themeFromTokens`, which reads each as the page draws it,
|
|
262
|
+
opaque: any CSS colour, a `color-mix()` or an `oklch()` too, which three.js can't read); left
|
|
263
|
+
unset, or set to no colour, it uses its own colours. The pane follows them as the page changes them
|
|
264
|
+
(`watchTokens`: its root's or body's class or style, or the colour scheme), as VS Code does
|
|
265
|
+
when its theme is switched.
|
|
266
|
+
* **Controls**: a button, select or field the panels draw is a `.kc-control` (`controls.css`,
|
|
267
|
+
which `tokens.css` brings): a host's own take the class to look alike, the `actions` it puts
|
|
268
|
+
in the pane's toolbar among them, which the pane's own styles, scoped to its markup, never
|
|
269
|
+
reached. A toggle while on (`aria-pressed`, `aria-expanded`) is tinted and edged in the
|
|
270
|
+
accent; `.kc-primary` makes one the bar's leading action, the accent's fill.
|
|
271
|
+
A range input is a `.kc-range` and a checkbox a `.kc-check`, drawn the same in each browser
|
|
272
|
+
and webview: a range's track is filled to its thumb by its `--kc-range-fill`, which a host
|
|
273
|
+
sets from its value with `fillOf(value, min, max)` (WebKit and Blink fill none themselves).
|
|
274
|
+
|
|
275
|
+
## Tests
|
|
276
|
+
|
|
277
|
+
The viewer's scene logic in Node (jsdom), with a stand-in for WebGL and models the real core
|
|
278
|
+
makes through `@kupcad/wasm`; the components mounted under jsdom (the `testing` setup and its
|
|
279
|
+
`stubRenderer`), with the schemas and models the real core gives. And a smoke test in headless
|
|
280
|
+
Chromium (`pnpm test:browser`) that draws what the core makes in the page and reads the pixels
|
|
281
|
+
back: the part's colour, the background, a section cutting it all away, and a cap; and the view
|
|
282
|
+
cube, which turns the camera to the part clicked.
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kupcad/ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kupcad/kupcad.git",
|
|
9
|
+
"directory": "packages/kupcad-ui"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"!src/**/*.test.js",
|
|
14
|
+
"!src/**/*.test-helper.*"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"**/*.css",
|
|
19
|
+
"./src/viewer/testing.js"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"svelte": "./src/index.js",
|
|
24
|
+
"default": "./src/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./viewer": "./src/viewer/index.js",
|
|
27
|
+
"./viewer/types": {
|
|
28
|
+
"types": "./src/viewer/types.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./testing": "./src/viewer/testing.js",
|
|
31
|
+
"./tokens.css": "./src/tokens.css",
|
|
32
|
+
"./vscode.css": "./src/vscode.css",
|
|
33
|
+
"./controls.css": "./src/controls.css"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@kupcad/protocol": "0.1.0",
|
|
40
|
+
"three": "^0.186.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@kupcad/wasm": "0.1.0",
|
|
44
|
+
"@playwright/test": "^1.64.1",
|
|
45
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
46
|
+
"@testing-library/svelte": "^5.4.2",
|
|
47
|
+
"@types/node": "^26.6.5",
|
|
48
|
+
"@types/three": "~0.186.0",
|
|
49
|
+
"jsdom": "^30.1.3",
|
|
50
|
+
"svelte": "^5.57.2",
|
|
51
|
+
"svelte-check": "^4.7.7",
|
|
52
|
+
"vite": "^8.3.5",
|
|
53
|
+
"vitest": "^5.0.5"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"svelte": "^5.57.2"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"svelte": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"check": "svelte-check --tsconfig ./jsconfig.json --fail-on-warnings",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:browser": "playwright test"
|
|
67
|
+
}
|
|
6
68
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The console under the model, as OpenSCAD's: each run of the script shown and the host's log
|
|
4
|
+
(console.js's lines), the newest at the bottom, followed as they come unless the person has
|
|
5
|
+
scrolled up to read. A line that names a place in the script, clicked, shows it there. It is
|
|
6
|
+
cleared with its button, and folded away to its bar with its title; its lines are `height`
|
|
7
|
+
pixels tall, which a Splitter above it sets.
|
|
8
|
+
-->
|
|
9
|
+
<script>
|
|
10
|
+
/** @import { ConsoleLine } from './console.js' */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} Props
|
|
14
|
+
* @property {readonly ConsoleLine[]} lines
|
|
15
|
+
* @property {boolean} [open] whether its lines show; bindable
|
|
16
|
+
* @property {number} [height] how tall its lines are, in CSS pixels
|
|
17
|
+
* @property {(line: ConsoleLine) => void} [onreveal] a line that names a place, clicked
|
|
18
|
+
* @property {() => void} [onclear]
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** @type {Props} */
|
|
22
|
+
let { lines, open = $bindable(true), height = 144, onreveal, onclear } = $props();
|
|
23
|
+
|
|
24
|
+
/** @type {HTMLOListElement | undefined} */
|
|
25
|
+
let list = $state();
|
|
26
|
+
/** Whether the newest line was in view before the next came: then they are followed. */
|
|
27
|
+
let following = true;
|
|
28
|
+
|
|
29
|
+
$effect.pre(() => {
|
|
30
|
+
void lines.length;
|
|
31
|
+
if (list) following = list.scrollTop + list.clientHeight >= list.scrollHeight - 1;
|
|
32
|
+
});
|
|
33
|
+
$effect(() => {
|
|
34
|
+
void lines.length;
|
|
35
|
+
if (list && following) list.scrollTop = list.scrollHeight;
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<section class="kc-console" aria-label="Console">
|
|
40
|
+
<div class="bar">
|
|
41
|
+
<button type="button" class="title" aria-expanded={open} onclick={() => (open = !open)}>
|
|
42
|
+
Console
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
class="kc-control"
|
|
47
|
+
disabled={lines.length === 0}
|
|
48
|
+
onclick={() => onclear?.()}
|
|
49
|
+
>
|
|
50
|
+
Clear
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
{#if open}
|
|
54
|
+
<ol class="lines" role="log" style:height="{height}px" bind:this={list}>
|
|
55
|
+
{#each lines as line, index (index)}
|
|
56
|
+
<li class={line.kind}>
|
|
57
|
+
{#if line.line !== null && onreveal}
|
|
58
|
+
<button type="button" class="place" onclick={() => onreveal(line)}>
|
|
59
|
+
{line.text} <span class="at">line {line.line}</span>
|
|
60
|
+
</button>
|
|
61
|
+
{:else}
|
|
62
|
+
{line.text}
|
|
63
|
+
{/if}
|
|
64
|
+
</li>
|
|
65
|
+
{/each}
|
|
66
|
+
</ol>
|
|
67
|
+
{/if}
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.kc-console {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
border-top: 1px solid var(--kc-border);
|
|
75
|
+
background: var(--kc-bg);
|
|
76
|
+
font-family: var(--kc-font);
|
|
77
|
+
font-size: var(--kc-font-size);
|
|
78
|
+
color: var(--kc-fg);
|
|
79
|
+
}
|
|
80
|
+
.bar {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: space-between;
|
|
84
|
+
padding: 2px var(--kc-gap);
|
|
85
|
+
}
|
|
86
|
+
.title {
|
|
87
|
+
padding: 2px 0;
|
|
88
|
+
font: inherit;
|
|
89
|
+
color: var(--kc-muted);
|
|
90
|
+
background: none;
|
|
91
|
+
border: none;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
}
|
|
94
|
+
.title::before {
|
|
95
|
+
content: '▾ ';
|
|
96
|
+
}
|
|
97
|
+
.title[aria-expanded='false']::before {
|
|
98
|
+
content: '▸ ';
|
|
99
|
+
}
|
|
100
|
+
.lines {
|
|
101
|
+
margin: 0;
|
|
102
|
+
padding: 0 var(--kc-gap) 4px;
|
|
103
|
+
overflow: auto;
|
|
104
|
+
list-style: none;
|
|
105
|
+
font-family: var(--kc-code-font, ui-monospace, Menlo, Consolas, monospace);
|
|
106
|
+
font-size: 0.92em;
|
|
107
|
+
white-space: pre-wrap;
|
|
108
|
+
overflow-wrap: anywhere;
|
|
109
|
+
}
|
|
110
|
+
.run {
|
|
111
|
+
margin-top: 4px;
|
|
112
|
+
color: var(--kc-muted);
|
|
113
|
+
}
|
|
114
|
+
.log {
|
|
115
|
+
color: var(--kc-muted);
|
|
116
|
+
}
|
|
117
|
+
.warning {
|
|
118
|
+
color: var(--kc-warning);
|
|
119
|
+
}
|
|
120
|
+
.error {
|
|
121
|
+
color: var(--kc-error);
|
|
122
|
+
}
|
|
123
|
+
.place {
|
|
124
|
+
padding: 0;
|
|
125
|
+
font: inherit;
|
|
126
|
+
color: inherit;
|
|
127
|
+
text-align: left;
|
|
128
|
+
background: none;
|
|
129
|
+
border: none;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
.place:hover .at {
|
|
133
|
+
text-decoration: underline;
|
|
134
|
+
}
|
|
135
|
+
/* Where the line points, a link to it in the editor */
|
|
136
|
+
.at {
|
|
137
|
+
color: var(--kc-accent-text);
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The formats a model can be exported in, as a menu: those its kind can be written in (a 2D model's
|
|
4
|
+
drawings alone, protocol's `exportFormatsFor`), each with what it is for. Choosing one gives it
|
|
5
|
+
to `onexport` and shows the menu's name again, so the same format can be chosen next time too.
|
|
6
|
+
A host then asks where: the menu is the page's, since a save dialog's file types may offer no
|
|
7
|
+
choice (macOS's lists them all as one).
|
|
8
|
+
-->
|
|
9
|
+
<script>
|
|
10
|
+
import { exportFormatsFor } from '@kupcad/protocol';
|
|
11
|
+
|
|
12
|
+
/** @import { ExportFormat, Kind } from '@kupcad/protocol/types' */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} Props
|
|
16
|
+
* @property {Kind} kind the model's (none while there is no run to go by)
|
|
17
|
+
* @property {(format: ExportFormat) => void} onexport
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @type {Props} */
|
|
21
|
+
let { kind, onexport } = $props();
|
|
22
|
+
|
|
23
|
+
/** @param {Event & { currentTarget: HTMLSelectElement }} event */
|
|
24
|
+
function chosen(event) {
|
|
25
|
+
const format = /** @type {ExportFormat} */ (event.currentTarget.value);
|
|
26
|
+
event.currentTarget.value = '';
|
|
27
|
+
if (format) onexport(format);
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<select class="kc-control" aria-label="Export" onchange={chosen}>
|
|
32
|
+
<option value="" selected disabled>Export…</option>
|
|
33
|
+
{#each exportFormatsFor(kind) as choice (choice.format)}
|
|
34
|
+
<option value={choice.format} title={choice.detail}>{choice.label}</option>
|
|
35
|
+
{/each}
|
|
36
|
+
</select>
|