@kupcad/playground 0.1.4 → 0.1.6

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @kupcad/playground
2
2
 
3
3
  KupCAD in a page: a script's editor beside its model and its parameters, all run in the browser
4
- by the core compiled to WebAssembly, in a worker. One Svelte component, which the site (and later
5
- the PWA) embeds; it keeps nothing of the page's.
4
+ by the core compiled to WebAssembly, in a worker. One Svelte component, which the site and the
5
+ browser IDE embed; it keeps nothing of the page's.
6
6
 
7
7
  ```svelte
8
8
  <script>
@@ -18,7 +18,7 @@ the PWA) embeds; it keeps nothing of the page's.
18
18
  with itself), `wasm` (that worker's module, when not the one `@kupcad/wasm` ships),
19
19
  `viewerOptions`, `settings`, `shortcuts` (a page's own, listed before the viewer's in the
20
20
  pane's shortcuts dialog) and `shortcutsOpen` (bindable).
21
- * **For a host that shares or saves the script** (a PWA later; nothing of it here): `onsource`
21
+ * **For a host that shares or saves the script** (the site's Share, the browser IDE): `onsource`
22
22
  is called with the script at each of the person's edits (not with the `source` it was given),
23
23
  and an `actions` snippet puts the host's own buttons in the model's toolbar, after Export
24
24
  (`.kc-control` gives them its look). It keeps no draft: a page loaded again shows `source`.
@@ -26,11 +26,22 @@ the PWA) embeds; it keeps nothing of the page's.
26
26
  component's, by `bind:this`) puts one in the editor in place of the person's, which the
27
27
  editor's undo takes back, and runs it at once on its own values, its model framed whole (an
28
28
  edit's keeps the camera); the core, the console and the layout stay as they are.
29
+ * **For a host of several scripts** (the browser IDE's project): `open(script, restore)` opens one
30
+ as another document, the editor made again on it with an undo history of its own (none of the last
31
+ one's), run as `load` runs one; `open(text, restore, 'json')` opens a JSON file of the project (a
32
+ package's `kupcad.json` or `kupcad.lock`, data a script imports) the same way, edited with nothing
33
+ run, no model, status or parameters shown. `snapshot()` is the editor as it is, its undo history
34
+ with it, for `open` to give back when the host opens that file again; `run()` runs the script
35
+ again as it is (what it imports changed), and `focus()` puts the focus in the editor (a script the
36
+ host just made). `files` is what the script's imports read, by the paths the script names them by
37
+ (`import { } from "./lib.kup"` reads `lib.kup`), given with each run and export as the core's
38
+ calls take them. `download` (exported) saves bytes or a Blob as a file, as the exports are saved.
29
39
  * **When it runs** (`runner.js`, `Runner`): once the script stays as it is for a moment after an
30
- edit (`DELAY_MS`, 300), at once when a parameter's value changes or it starts. A run under
31
- way when the next starts is stopped (the worker drops or ends its call), so what is shown is
32
- the last script's. A run is `check` first, whose findings the editor marks at once, then the
33
- preview: the model, how the run went, its parameters.
40
+ edit (`DELAY_MS`, 300), at once when a parameter's value changes or it starts (with a script: one
41
+ that starts empty runs nothing, and says nothing of a run, until something is typed). A run under
42
+ way when the next starts is stopped (the worker drops or ends its call), so what is shown is the
43
+ last script's. A run is `check` first, whose findings the editor marks at once, then the preview:
44
+ the model, how the run went, its parameters.
34
45
  * **What the editor marks** (`marks.js`): `check`'s findings, and the places the run named (why
35
46
  it failed, a warning on a line); not the run's error where `check` found errors already (a
36
47
  script that doesn't parse fails the run for the same reason). Findings of a script changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kupcad/playground",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "KupCAD in the browser: a script's editor, its model and its parameters, running on the core compiled to WebAssembly in a worker. One Svelte component a page embeds.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -24,12 +24,13 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "@kupcad/editor": "0.1.4",
28
- "@kupcad/protocol": "0.1.4",
29
- "@kupcad/ui": "0.1.4",
30
- "@kupcad/wasm": "0.1.4"
27
+ "@kupcad/editor": "0.1.6",
28
+ "@kupcad/protocol": "0.1.6",
29
+ "@kupcad/ui": "0.1.6",
30
+ "@kupcad/wasm": "0.1.6"
31
31
  },
32
32
  "devDependencies": {
33
+ "@codemirror/language": "^6.13.1",
33
34
  "@codemirror/lint": "^6.9.7",
34
35
  "@codemirror/view": "^6.43.15",
35
36
  "@playwright/test": "^1.64.1",
@@ -10,8 +10,11 @@
10
10
  model's width); the sizes, what is hidden or folded, and how the view looks are kept in
11
11
  `settings` (the page's localStorage by default), and follow them as they are set. It keeps
12
12
  nothing else of the page's: the page gives the script it starts with (`source`) and another
13
- in its place (`load`), and is told of each edit of it (`onsource`), for its own buttons in the
14
- toolbar (`actions`) to share or save it.
13
+ in its place (`load`, or `open` for another document, with an undo history of its own: a
14
+ script, or a JSON file of the project, edited with nothing run and nothing in the viewer), and
15
+ is told of each edit of it (`onsource`), for its own buttons in the toolbar (`actions`) to
16
+ share or save it. The files the script imports, a page of several scripts gives with them
17
+ (`files`).
15
18
  -->
16
19
  <script>
17
20
  import { onMount, untrack } from 'svelte';
@@ -37,12 +40,12 @@
37
40
  import { marksOf } from './marks.js';
38
41
  import { Runner } from './runner.js';
39
42
 
40
- /** @import { Editor as EditorClass } from '@kupcad/editor' */
43
+ /** @import { Editor as EditorClass, EditorLanguage } from '@kupcad/editor' */
41
44
  /** @import { ExportFormat, Finding, ParamValues, Preview } from '@kupcad/protocol/types' */
42
45
  /** @import { ConsoleLine, Problem, Settings, ShortcutGroup } from '@kupcad/ui' */
43
46
  /** @import { Snippet } from 'svelte' */
44
47
  /** @import { ViewerOptions } from '@kupcad/ui/viewer/types' */
45
- /** @import { KupcadApi, WorkerWasmSource } from '@kupcad/wasm/types' */
48
+ /** @import { KupcadApi, PreviewOptions, WorkerWasmSource } from '@kupcad/wasm/types' */
46
49
 
47
50
  /**
48
51
  * @typedef {object} Props
@@ -57,6 +60,9 @@
57
60
  * by default the page's localStorage
58
61
  * @property {(source: string) => void} [onsource] the person's edit changed the script: it now
59
62
  * is `source` (for a host to share it, or save it)
63
+ * @property {PreviewOptions['files']} [files] what the script's imports read, by the paths it
64
+ * names them by (`import { } from "./lib.kup"` reads `lib.kup`), given with each run and
65
+ * export
60
66
  * @property {Snippet} [actions] a host's own buttons in the model's toolbar, after Export
61
67
  * (`.kc-control` gives them its look)
62
68
  * @property {readonly ShortcutGroup[]} [shortcuts] the host's own, listed before the viewer's
@@ -72,6 +78,7 @@
72
78
  viewerOptions = {},
73
79
  settings = localSettings(),
74
80
  onsource,
81
+ files,
75
82
  actions: hostActions,
76
83
  shortcuts = [],
77
84
  shortcutsOpen = $bindable(false),
@@ -109,6 +116,13 @@
109
116
  * view frames each one's model whole (another subject), where an edit's keeps the camera.
110
117
  */
111
118
  let loads = $state(0);
119
+ /** How many other documents the page opened: the editor is made again for each. */
120
+ let documents = $state(0);
121
+ /** The editor's snapshot the document opened last starts from, if it has one. */
122
+ let restored = $state.raw(/** @type {unknown} */ (undefined));
123
+ /** What the document open is: a script, which runs, or a JSON file, which nothing runs of. */
124
+ let language = $state(/** @type {EditorLanguage} */ ('kupcad'));
125
+ const runs = $derived(language === 'kupcad');
112
126
  let editor = $state(/** @type {EditorClass | null} */ (null));
113
127
  /** `check`'s findings of the script as it is, none while it changed since. */
114
128
  let findings = $state(/** @type {Finding[]} */ ([]));
@@ -125,7 +139,8 @@
125
139
  let runner = null;
126
140
 
127
141
  const header = $derived(ran?.preview.header ?? null);
128
- const status = $derived(runStatusOf(header));
142
+ // None to say with nothing to run yet: a page that starts empty (a host with no file open)
143
+ const status = $derived(runs && (ran || value !== '') ? runStatusOf(header) : null);
129
144
  const problems = $derived(problemsOf(header));
130
145
  // Marked where they are in the script as it is: a run of another (older) script marks nothing
131
146
  const marks = $derived(marksOf(findings, ran?.source === value ? problems : []));
@@ -138,18 +153,19 @@
138
153
  core = kupcad ?? made;
139
154
  runner = new Runner(/** @type {KupcadApi} */ (core), {
140
155
  onfindings: (next, of) => {
141
- if (of === value) findings = next;
156
+ if (runs && of === value) findings = next;
142
157
  },
143
158
  onpreview: (preview, of) => {
159
+ if (!runs) return;
144
160
  ran = { preview, source: of };
145
161
  notice = '';
146
162
  consoleLines = appendLines(consoleLines, linesOfRun(preview.header, name));
147
163
  },
148
164
  onerror: (error) => (notice = errorText(error)),
149
165
  });
150
- runner.run(value, {}, { now: true });
166
+ if (value !== '') runner.run(value, {}, { now: true, files });
151
167
  return () => {
152
- runner?.dispose();
168
+ runner?.stop();
153
169
  made?.terminate();
154
170
  };
155
171
  });
@@ -157,15 +173,65 @@
157
173
  /**
158
174
  * Puts `script` in the editor in place of the one there (the editor's undo takes that back),
159
175
  * and runs it at once on its own values (none the panel set for the last), its model framed
160
- * whole.
176
+ * whole. In place of a JSON file, it is a document of its own.
161
177
  * @param {string} script another the page gives: an example, a script it opened
162
178
  */
163
179
  export function load(script) {
164
- value = script;
180
+ if (!runs) {
181
+ language = 'kupcad';
182
+ restored = undefined;
183
+ documents += 1;
184
+ }
185
+ show(script);
186
+ runner?.run(script, {}, { now: true, files });
187
+ }
188
+
189
+ /**
190
+ * Opens a file in the editor in place of the one there as another document: the editor made
191
+ * again on it, its undo history its own, not the last one's (`restore`, an earlier
192
+ * `snapshot()` of it, gives it back). A script runs at once as `load` runs one; a JSON file (a
193
+ * package's kupcad.json or kupcad.lock, data a script imports) is edited with nothing run,
194
+ * nothing in the viewer.
195
+ * @param {string} text one of the page's files
196
+ * @param {unknown} [restore] the snapshot the page kept when it opened another
197
+ * @param {EditorLanguage} [of] what it is
198
+ */
199
+ export function open(text, restore, of = 'kupcad') {
200
+ restored = restore;
201
+ language = of;
202
+ documents += 1;
203
+ show(text);
204
+ if (of === 'kupcad') runner?.run(text, {}, { now: true, files });
205
+ else {
206
+ runner?.stop();
207
+ ran = null;
208
+ }
209
+ }
210
+
211
+ /** @param {string} text the document's, in place of the last one's, with none of its runs' */
212
+ function show(text) {
213
+ value = text;
165
214
  findings = [];
166
215
  overrides = {};
167
216
  loads += 1;
168
- runner?.run(script, {}, { now: true });
217
+ }
218
+
219
+ /** The editor as it is now, its undo history with it, for `open` to give back: plain JSON. */
220
+ export function snapshot() {
221
+ return editor?.snapshot();
222
+ }
223
+
224
+ /**
225
+ * Runs the script again at once, as it is, on the panel's values: what it imports changed
226
+ * (`files`), which the page knows of and it doesn't.
227
+ */
228
+ export function run() {
229
+ if (runs) runner?.run(value, plainValues(overrides), { now: true, files });
230
+ }
231
+
232
+ /** Puts the focus in the editor: a script the page made, to be written. */
233
+ export function focus() {
234
+ editor?.focus();
169
235
  }
170
236
 
171
237
  /** @param {string} line of the core's log, which goes to the console */
@@ -173,16 +239,16 @@
173
239
  consoleLines = appendLines(consoleLines, linesOfLog(line));
174
240
  }
175
241
 
176
- /** @param {string} next the user's script */
242
+ /** @param {string} next the user's script (or JSON file) */
177
243
  function edited(next) {
178
244
  findings = [];
179
- runner?.run(next, plainValues(overrides));
245
+ if (runs) runner?.run(next, plainValues(overrides), { files });
180
246
  onsource?.(next);
181
247
  }
182
248
 
183
249
  /** @param {ParamValues} values the panel's */
184
250
  function valuesChanged(values) {
185
- runner?.run(value, values, { now: true });
251
+ runner?.run(value, values, { now: true, files });
186
252
  }
187
253
 
188
254
  /** @param {Problem | ConsoleLine} place a problem or a console line with a line, clicked */
@@ -194,7 +260,7 @@
194
260
  async function exportChosen(format) {
195
261
  if (!core) return;
196
262
  try {
197
- const bytes = await core.build(value, { format, params: plainValues(overrides) });
263
+ const bytes = await core.build(value, { format, params: plainValues(overrides), files });
198
264
  download(bytes, `${name}.${format}`);
199
265
  notice = '';
200
266
  } catch (error) {
@@ -215,7 +281,17 @@
215
281
  style:--params-width="{paramsWidth}px"
216
282
  >
217
283
  <section class="code" aria-label="Script">
218
- <Editor bind:value bind:editor findings={marks} label={name} onchange={edited} />
284
+ {#key documents}
285
+ <Editor
286
+ bind:value
287
+ bind:editor
288
+ findings={marks}
289
+ label={name}
290
+ restore={restored}
291
+ {language}
292
+ onchange={edited}
293
+ />
294
+ {/key}
219
295
  {#if notice}<p class="notice" role="alert">{notice}</p>{/if}
220
296
  </section>
221
297
  <Splitter
@@ -352,7 +428,6 @@
352
428
  border-radius: var(--kc-radius);
353
429
  background: var(--kc-bg);
354
430
  }
355
- /* A narrow page: the script over the model, the parameters under it */
356
431
  /* Narrow, the panes stacked, each at a height of its own, and the playground scrolled: the
357
432
  script a few lines, the model 4:3 of its width (at most 32em), the console and the
358
433
  parameters as tall as their lines */
package/src/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // @kupcad/playground: KupCAD in a page, a script's editor, model and parameters on the core in a
2
- // worker (Playground.svelte), and when it runs the script (Runner), for a host of its own.
2
+ // worker (Playground.svelte), when it runs the script (Runner), for a host of its own, and how
3
+ // it gives the person a file (download).
3
4
 
4
5
  export { default as Playground } from './Playground.svelte';
6
+ export { download } from './download.js';
5
7
  export { DELAY_MS, Runner } from './runner.js';
package/src/runner.js CHANGED
@@ -2,10 +2,11 @@
2
2
  // edit, and at once when a parameter's value changes or the playground starts. A run under way
3
3
  // when the next starts is stopped (the core's worker drops or ends its call), so what comes is
4
4
  // the last script's. A run is the script's findings (`check`, quick: the editor marks them
5
- // first), then its preview (the model, its parameters, why it failed).
5
+ // first), then its preview (the model, its parameters, why it failed), the files its imports
6
+ // read given with it.
6
7
 
7
8
  /** @import { Finding, ParamValues, Preview } from '@kupcad/protocol/types' */
8
- /** @import { KupcadApi } from '@kupcad/wasm/types' */
9
+ /** @import { KupcadApi, PreviewOptions } from '@kupcad/wasm/types' */
9
10
 
10
11
  /** How long the script stays as it is after an edit before it runs. */
11
12
  export const DELAY_MS = 300;
@@ -42,19 +43,22 @@ export class Runner {
42
43
 
43
44
  /**
44
45
  * Runs `source` with `values` once it stays so for the delay (a later call waits again), or at
45
- * once.
46
+ * once; with `files`, what its imports read.
46
47
  * @param {string} source
47
48
  * @param {ParamValues} values
48
- * @param {{ now?: boolean }} [when]
49
+ * @param {{ now?: boolean, files?: PreviewOptions['files'] }} [options]
49
50
  */
50
- run(source, values, { now = false } = {}) {
51
+ run(source, values, { now = false, files } = {}) {
51
52
  clearTimeout(this.#waiting);
52
- if (now) void this.#start(source, values);
53
- else this.#waiting = setTimeout(() => void this.#start(source, values), this.#delayMs);
53
+ if (now) void this.#start(source, values, files);
54
+ else this.#waiting = setTimeout(() => void this.#start(source, values, files), this.#delayMs);
54
55
  }
55
56
 
56
- /** Runs nothing more, and stops the run under way. */
57
- dispose() {
57
+ /**
58
+ * Stops the run waiting for the delay and the run under way: nothing comes of either. The
59
+ * next `run` runs again; a host done with the runner stops it and lets it go.
60
+ */
61
+ stop() {
58
62
  clearTimeout(this.#waiting);
59
63
  this.#running?.abort();
60
64
  this.#running = null;
@@ -63,8 +67,9 @@ export class Runner {
63
67
  /**
64
68
  * @param {string} source
65
69
  * @param {ParamValues} values
70
+ * @param {PreviewOptions['files']} files
66
71
  */
67
- async #start(source, values) {
72
+ async #start(source, values, files) {
68
73
  this.#running?.abort();
69
74
  const running = new AbortController();
70
75
  this.#running = running;
@@ -73,7 +78,7 @@ export class Runner {
73
78
  const findings = await this.#kupcad.check(source, { signal });
74
79
  if (signal.aborted) return;
75
80
  this.#handlers.onfindings(findings, source);
76
- const preview = await this.#kupcad.preview(source, { params: values, signal });
81
+ const preview = await this.#kupcad.preview(source, { params: values, files, signal });
77
82
  if (signal.aborted) return;
78
83
  this.#handlers.onpreview(preview, source);
79
84
  } catch (error) {