@kupcad/playground 0.1.5 → 0.1.7

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
@@ -26,20 +26,22 @@ browser IDE embed; 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
30
- one as another document, the editor made again on it with an undo history of its own (none of
31
- the last one's), run as `load` runs one; `snapshot()` is the editor as it is, its undo history
32
- with it, for `open` to give back when the host opens that script again; `run()` runs the
33
- script again as it is (what it imports changed), and `focus()` puts the focus in the editor
34
- (a script the host just made). `files` is what the
35
- script's imports read, by the paths the script names them by (`import { } from "./lib.kup"`
36
- reads `lib.kup`), given with each run and export as the core's calls take them. `download`
37
- (exported) saves bytes or a Blob as a file, as the exports are saved.
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.
38
39
  * **When it runs** (`runner.js`, `Runner`): once the script stays as it is for a moment after an
39
- edit (`DELAY_MS`, 300), at once when a parameter's value changes or it starts. A run under
40
- way when the next starts is stopped (the worker drops or ends its call), so what is shown is
41
- the last script's. A run is `check` first, whose findings the editor marks at once, then the
42
- 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.
43
45
  * **What the editor marks** (`marks.js`): `check`'s findings, and the places the run named (why
44
46
  it failed, a warning on a line); not the run's error where `check` found errors already (a
45
47
  script that doesn't parse fails the run for the same reason). Findings of a script changed
@@ -53,8 +55,8 @@ browser IDE embed; it keeps nothing of the page's.
53
55
  resize them (`@kupcad/ui`'s `Splitter`), and the toolbar's Parameters button hides the
54
56
  parameters for the model's width; the sizes, and what is hidden or folded, are kept in
55
57
  `settings`, by default the page's localStorage (`localSettings`, the `playground.*` keys), and
56
- how the view looks too (the pane's `view.*` keys), all followed as they are set. A page 800px
57
- wide or less stacks the panes, each at a height of its own (the script a few lines, the model
58
+ how the view looks too (the pane's `view.*` keys), all followed as they are set. Given 800px
59
+ or less (a phone, or a column of a page: it goes by its own width) it stacks the panes, each at a height of its own (the script a few lines, the model
58
60
  4:3 of its width, at most 32em, the console and the parameters as tall as their lines), and
59
61
  the playground scrolls; on a phone the view's panel is under the model (`ViewerPane`'s).
60
62
  * **The console** (`@kupcad/ui`'s `ConsolePanel`, under the model): each run, what it printed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kupcad/playground",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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.5",
28
- "@kupcad/protocol": "0.1.5",
29
- "@kupcad/ui": "0.1.5",
30
- "@kupcad/wasm": "0.1.5"
27
+ "@kupcad/editor": "0.1.7",
28
+ "@kupcad/protocol": "0.1.7",
29
+ "@kupcad/ui": "0.1.7",
30
+ "@kupcad/wasm": "0.1.7"
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,7 +10,8 @@
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`, or `open` for another document, with an undo history of its own), and
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
14
15
  is told of each edit of it (`onsource`), for its own buttons in the toolbar (`actions`) to
15
16
  share or save it. The files the script imports, a page of several scripts gives with them
16
17
  (`files`).
@@ -39,7 +40,7 @@
39
40
  import { marksOf } from './marks.js';
40
41
  import { Runner } from './runner.js';
41
42
 
42
- /** @import { Editor as EditorClass } from '@kupcad/editor' */
43
+ /** @import { Editor as EditorClass, EditorLanguage } from '@kupcad/editor' */
43
44
  /** @import { ExportFormat, Finding, ParamValues, Preview } from '@kupcad/protocol/types' */
44
45
  /** @import { ConsoleLine, Problem, Settings, ShortcutGroup } from '@kupcad/ui' */
45
46
  /** @import { Snippet } from 'svelte' */
@@ -119,6 +120,9 @@
119
120
  let documents = $state(0);
120
121
  /** The editor's snapshot the document opened last starts from, if it has one. */
121
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');
122
126
  let editor = $state(/** @type {EditorClass | null} */ (null));
123
127
  /** `check`'s findings of the script as it is, none while it changed since. */
124
128
  let findings = $state(/** @type {Finding[]} */ ([]));
@@ -135,7 +139,8 @@
135
139
  let runner = null;
136
140
 
137
141
  const header = $derived(ran?.preview.header ?? null);
138
- 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);
139
144
  const problems = $derived(problemsOf(header));
140
145
  // Marked where they are in the script as it is: a run of another (older) script marks nothing
141
146
  const marks = $derived(marksOf(findings, ran?.source === value ? problems : []));
@@ -148,18 +153,19 @@
148
153
  core = kupcad ?? made;
149
154
  runner = new Runner(/** @type {KupcadApi} */ (core), {
150
155
  onfindings: (next, of) => {
151
- if (of === value) findings = next;
156
+ if (runs && of === value) findings = next;
152
157
  },
153
158
  onpreview: (preview, of) => {
159
+ if (!runs) return;
154
160
  ran = { preview, source: of };
155
161
  notice = '';
156
162
  consoleLines = appendLines(consoleLines, linesOfRun(preview.header, name));
157
163
  },
158
164
  onerror: (error) => (notice = errorText(error)),
159
165
  });
160
- runner.run(value, {}, { now: true, files });
166
+ if (value !== '') runner.run(value, {}, { now: true, files });
161
167
  return () => {
162
- runner?.dispose();
168
+ runner?.stop();
163
169
  made?.terminate();
164
170
  };
165
171
  });
@@ -167,28 +173,47 @@
167
173
  /**
168
174
  * Puts `script` in the editor in place of the one there (the editor's undo takes that back),
169
175
  * and runs it at once on its own values (none the panel set for the last), its model framed
170
- * whole.
176
+ * whole. In place of a JSON file, it is a document of its own.
171
177
  * @param {string} script another the page gives: an example, a script it opened
172
178
  */
173
179
  export function load(script) {
174
- value = script;
175
- findings = [];
176
- overrides = {};
177
- loads += 1;
180
+ if (!runs) {
181
+ language = 'kupcad';
182
+ restored = undefined;
183
+ documents += 1;
184
+ }
185
+ show(script);
178
186
  runner?.run(script, {}, { now: true, files });
179
187
  }
180
188
 
181
189
  /**
182
- * Opens `script` in the editor in place of the one there as another document: the editor made
190
+ * Opens a file in the editor in place of the one there as another document: the editor made
183
191
  * again on it, its undo history its own, not the last one's (`restore`, an earlier
184
- * `snapshot()` of it, gives it back), and run at once as `load` runs one.
185
- * @param {string} script one of the page's scripts
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
186
196
  * @param {unknown} [restore] the snapshot the page kept when it opened another
197
+ * @param {EditorLanguage} [of] what it is
187
198
  */
188
- export function open(script, restore) {
199
+ export function open(text, restore, of = 'kupcad') {
189
200
  restored = restore;
201
+ language = of;
190
202
  documents += 1;
191
- load(script);
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;
214
+ findings = [];
215
+ overrides = {};
216
+ loads += 1;
192
217
  }
193
218
 
194
219
  /** The editor as it is now, its undo history with it, for `open` to give back: plain JSON. */
@@ -201,7 +226,7 @@
201
226
  * (`files`), which the page knows of and it doesn't.
202
227
  */
203
228
  export function run() {
204
- runner?.run(value, plainValues(overrides), { now: true, files });
229
+ if (runs) runner?.run(value, plainValues(overrides), { now: true, files });
205
230
  }
206
231
 
207
232
  /** Puts the focus in the editor: a script the page made, to be written. */
@@ -214,10 +239,10 @@
214
239
  consoleLines = appendLines(consoleLines, linesOfLog(line));
215
240
  }
216
241
 
217
- /** @param {string} next the user's script */
242
+ /** @param {string} next the user's script (or JSON file) */
218
243
  function edited(next) {
219
244
  findings = [];
220
- runner?.run(next, plainValues(overrides), { files });
245
+ if (runs) runner?.run(next, plainValues(overrides), { files });
221
246
  onsource?.(next);
222
247
  }
223
248
 
@@ -249,112 +274,121 @@
249
274
  }
250
275
  </script>
251
276
 
252
- <div
253
- class="kc-playground"
254
- bind:clientWidth={width}
255
- style:--editor-width="{editorWidth}px"
256
- style:--params-width="{paramsWidth}px"
257
- >
258
- <section class="code" aria-label="Script">
259
- {#key documents}
260
- <Editor
261
- bind:value
262
- bind:editor
263
- findings={marks}
264
- label={name}
265
- restore={restored}
266
- onchange={edited}
267
- />
268
- {/key}
269
- {#if notice}<p class="notice" role="alert">{notice}</p>{/if}
270
- </section>
271
- <Splitter
272
- bind:size={editorWidth}
273
- min={240}
274
- max={width > 0 ? width - (paramsShown ? paramsWidth : 0) - MIN_VIEW : undefined}
275
- label="Resize the script"
276
- onchange={(size) => settings.set(KEYS.editor, size)}
277
- />
278
- <section class="view" aria-label="Model">
279
- <div class="model">
280
- <ViewerPane
281
- model={ran?.preview.model ?? null}
282
- subject="script {loads}"
283
- options={viewerOptions}
284
- bind:panel={() => panelOpen, (open) => settings.set(KEYS.panelOpen, open)}
285
- {status}
286
- {problems}
287
- onproblem={reveal}
288
- onscreenshot={screenshot}
289
- {settings}
290
- {shortcuts}
291
- bind:shortcutsOpen
292
- >
293
- {#snippet actions()}
294
- <ExportMenu kind={ran?.preview.model.kind ?? 'none'} onexport={exportChosen} />
295
- {@render hostActions?.()}
296
- {/snippet}
297
- {#snippet toggles()}
298
- {#if header && parameters.length > 0}
299
- <button
300
- type="button"
301
- class="kc-control"
302
- aria-controls="{id}-params"
303
- aria-expanded={paramsOpen}
304
- onclick={() => settings.set(KEYS.paramsOpen, !paramsOpen)}
305
- >
306
- Parameters
307
- </button>
308
- {/if}
309
- {/snippet}
310
- </ViewerPane>
311
- </div>
312
- {#if consoleOpen}
313
- <Splitter
314
- between="rows"
315
- pane="after"
316
- bind:size={consoleHeight}
317
- min={48}
318
- label="Resize the console"
319
- onchange={(size) => settings.set(KEYS.console, size)}
320
- />
321
- {/if}
322
- <ConsolePanel
323
- lines={consoleLines}
324
- height={consoleHeight}
325
- bind:open={() => consoleOpen, (open) => settings.set(KEYS.open, open)}
326
- onreveal={reveal}
327
- onclear={() => (consoleLines = [])}
277
+ <!-- The box the playground is laid out by: its own width, wide or a column of a page -->
278
+ <div class="kc-playground-box">
279
+ <div
280
+ class="kc-playground"
281
+ bind:clientWidth={width}
282
+ style:--editor-width="{editorWidth}px"
283
+ style:--params-width="{paramsWidth}px"
284
+ >
285
+ <section class="code" aria-label="Script">
286
+ {#key documents}
287
+ <Editor
288
+ bind:value
289
+ bind:editor
290
+ findings={marks}
291
+ label={name}
292
+ restore={restored}
293
+ {language}
294
+ onchange={edited}
295
+ />
296
+ {/key}
297
+ {#if notice}<p class="notice" role="alert">{notice}</p>{/if}
298
+ </section>
299
+ <Splitter
300
+ bind:size={editorWidth}
301
+ min={240}
302
+ max={width > 0 ? width - (paramsShown ? paramsWidth : 0) - MIN_VIEW : undefined}
303
+ label="Resize the script"
304
+ onchange={(size) => settings.set(KEYS.editor, size)}
328
305
  />
329
- </section>
330
- {#if header && parameters.length > 0}
331
- {#if paramsOpen}
332
- <Splitter
333
- pane="after"
334
- bind:size={paramsWidth}
335
- min={200}
336
- max={640}
337
- label="Resize the parameters"
338
- onchange={(size) => settings.set(KEYS.params, size)}
306
+ <section class="view" aria-label="Model">
307
+ <div class="model">
308
+ <ViewerPane
309
+ model={ran?.preview.model ?? null}
310
+ subject="script {loads}"
311
+ options={viewerOptions}
312
+ bind:panel={() => panelOpen, (open) => settings.set(KEYS.panelOpen, open)}
313
+ {status}
314
+ {problems}
315
+ onproblem={reveal}
316
+ onscreenshot={screenshot}
317
+ {settings}
318
+ {shortcuts}
319
+ bind:shortcutsOpen
320
+ >
321
+ {#snippet actions()}
322
+ <ExportMenu kind={ran?.preview.model.kind ?? 'none'} onexport={exportChosen} />
323
+ {@render hostActions?.()}
324
+ {/snippet}
325
+ {#snippet toggles()}
326
+ {#if header && parameters.length > 0}
327
+ <button
328
+ type="button"
329
+ class="kc-control"
330
+ aria-controls="{id}-params"
331
+ aria-expanded={paramsOpen}
332
+ onclick={() => settings.set(KEYS.paramsOpen, !paramsOpen)}
333
+ >
334
+ Parameters
335
+ </button>
336
+ {/if}
337
+ {/snippet}
338
+ </ViewerPane>
339
+ </div>
340
+ {#if consoleOpen}
341
+ <Splitter
342
+ between="rows"
343
+ pane="after"
344
+ bind:size={consoleHeight}
345
+ min={48}
346
+ label="Resize the console"
347
+ onchange={(size) => settings.set(KEYS.console, size)}
348
+ />
349
+ {/if}
350
+ <ConsolePanel
351
+ lines={consoleLines}
352
+ height={consoleHeight}
353
+ bind:open={() => consoleOpen, (open) => settings.set(KEYS.open, open)}
354
+ onreveal={reveal}
355
+ onclear={() => (consoleLines = [])}
339
356
  />
340
- {/if}
341
- <!-- Hidden, not gone: what its fields hold stays, and the toggle has it to control -->
342
- <aside class="params" id="{id}-params" aria-label="Parameters" hidden={!paramsOpen}>
343
- <!-- Made again for each script loaded: no field keeps what was typed in the last's -->
344
- {#key loads}
345
- <ParamsPanel
346
- schema={header.params.schema}
347
- values={header.params.values}
348
- error={header.error}
349
- bind:overrides
350
- onchange={valuesChanged}
357
+ </section>
358
+ {#if header && parameters.length > 0}
359
+ {#if paramsOpen}
360
+ <Splitter
361
+ pane="after"
362
+ bind:size={paramsWidth}
363
+ min={200}
364
+ max={640}
365
+ label="Resize the parameters"
366
+ onchange={(size) => settings.set(KEYS.params, size)}
351
367
  />
352
- {/key}
353
- </aside>
354
- {/if}
368
+ {/if}
369
+ <!-- Hidden, not gone: what its fields hold stays, and the toggle has it to control -->
370
+ <aside class="params" id="{id}-params" aria-label="Parameters" hidden={!paramsOpen}>
371
+ <!-- Made again for each script loaded: no field keeps what was typed in the last's -->
372
+ {#key loads}
373
+ <ParamsPanel
374
+ schema={header.params.schema}
375
+ values={header.params.values}
376
+ error={header.error}
377
+ bind:overrides
378
+ onchange={valuesChanged}
379
+ />
380
+ {/key}
381
+ </aside>
382
+ {/if}
383
+ </div>
355
384
  </div>
356
385
 
357
386
  <style>
387
+ .kc-playground-box {
388
+ height: 100%;
389
+ min-height: 0;
390
+ container-type: inline-size;
391
+ }
358
392
  .kc-playground {
359
393
  display: flex;
360
394
  height: 100%;
@@ -402,11 +436,10 @@
402
436
  border-radius: var(--kc-radius);
403
437
  background: var(--kc-bg);
404
438
  }
405
- /* A narrow page: the script over the model, the parameters under it */
406
- /* Narrow, the panes stacked, each at a height of its own, and the playground scrolled: the
407
- script a few lines, the model 4:3 of its width (at most 32em), the console and the
408
- parameters as tall as their lines */
409
- @media (max-width: 800px) {
439
+ /* Narrow (a phone, a column of a page), the panes stacked, each at a height of its own, and
440
+ the playground scrolled: the script a few lines, the model 4:3 of its width (at most 32em),
441
+ the console and the parameters as tall as their lines */
442
+ @container (max-width: 800px) {
410
443
  .kc-playground {
411
444
  flex-direction: column;
412
445
  overflow-y: auto;
package/src/runner.js CHANGED
@@ -54,8 +54,11 @@ export class Runner {
54
54
  else this.#waiting = setTimeout(() => void this.#start(source, values, files), this.#delayMs);
55
55
  }
56
56
 
57
- /** Runs nothing more, and stops the run under way. */
58
- 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() {
59
62
  clearTimeout(this.#waiting);
60
63
  this.#running?.abort();
61
64
  this.#running = null;