@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.
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/README.md CHANGED
@@ -1,3 +1,282 @@
1
- # Temporary Holding Version
1
+ # @kupcad/ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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>