cadcode 0.1.0 → 0.2.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/README.md CHANGED
@@ -61,6 +61,7 @@ cadcode png case.js --part assembly --view sheet --size 1600x1200
61
61
  - **Zoom**: mouse wheel
62
62
  - **Fit the model to the view**: F, or the Fit button
63
63
  - **Switch parts**: the part dropdown
64
+ - **Download the part as STL**: the STL button. It is the same file `cadcode stl` writes.
64
65
 
65
66
  Errors appear in the page and in the terminal, with the line in your file. The last good model stays on screen until you fix them. Whatever the program prints with `console.log` appears in the corner of the page.
66
67
 
package/client/index.html CHANGED
@@ -33,6 +33,9 @@
33
33
  <button id="fit" type="button" title="Fit model to view (F)">
34
34
  Fit
35
35
  </button>
36
+ <button id="download" type="button" title="Download the part as STL">
37
+ STL
38
+ </button>
36
39
  </header>
37
40
 
38
41
  <section id="logs" class="logs" hidden></section>
package/client/viewer.css CHANGED
@@ -112,6 +112,11 @@ button {
112
112
  cursor: pointer;
113
113
  }
114
114
 
115
+ button:disabled {
116
+ cursor: progress;
117
+ opacity: 0.55;
118
+ }
119
+
115
120
  button:hover,
116
121
  select:hover {
117
122
  border-color: rgba(20, 30, 40, 0.28);
package/client/viewer.js CHANGED
@@ -246,6 +246,29 @@ function showParts(names, current) {
246
246
  partSelect.replaceChildren(...names.map((name) => new Option(name, name, false, name === current)));
247
247
  }
248
248
 
249
+ // --- Download ----------------------------------------------------------------
250
+ // Fetched rather than navigated to, so a failing program shows its error here
251
+ // instead of replacing the viewer.
252
+
253
+ const downloadButton = document.getElementById('download');
254
+
255
+ downloadButton.addEventListener('click', async () => {
256
+ downloadButton.disabled = true;
257
+ try {
258
+ const response = await fetch(`/stl?part=${encodeURIComponent(partSelect.value)}`);
259
+ if (!response.ok) throw new Error(await response.text());
260
+ const link = document.createElement('a');
261
+ link.href = URL.createObjectURL(await response.blob());
262
+ link.download = response.headers.get('Content-Disposition').match(/filename="(.+)"/)[1];
263
+ link.click();
264
+ setTimeout(() => URL.revokeObjectURL(link.href)); // after the browser has started the download
265
+ } catch (error) {
266
+ showError(`STL download failed: ${error.message}`);
267
+ } finally {
268
+ downloadButton.disabled = false;
269
+ }
270
+ });
271
+
249
272
  // --- Connection --------------------------------------------------------------
250
273
 
251
274
  let socket = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cadcode",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "scripts": {
5
5
  "test": "node --test \"test/*.test.js\""
6
6
  },
package/src/dev/server.js CHANGED
@@ -63,6 +63,7 @@ function openBrowser(url) {
63
63
  export async function startDevServer({ file, port, host, part, overrides, open }) {
64
64
  const relative = path.relative(process.cwd(), path.resolve(file));
65
65
  const displayName = relative.startsWith('..') ? path.resolve(file) : relative;
66
+ const baseName = path.basename(file).replace(/\.[^.]+$/, '');
66
67
  // lastError is replayed to browsers that connect while the program is broken.
67
68
  const state = { part, lastFrame: null, lastError: null, worker: null, bundle: null };
68
69
  const sockets = new Set();
@@ -102,6 +103,22 @@ export async function startDevServer({ file, port, host, part, overrides, open }
102
103
  });
103
104
  }
104
105
 
106
+ // A download evaluates in its own worker, so it never cancels or waits for the preview.
107
+ function downloadSTL(res, part) {
108
+ if (!state.bundle) return void res.writeHead(503, { 'Content-Type': 'text/plain' }).end('The program has not built yet');
109
+ const worker = new Worker(WORKER_URL, { workerData: { bundle: state.bundle, part, overrides, output: 'stl' } });
110
+ const fail = (message) => res.writeHead(500, { 'Content-Type': 'text/plain' }).end(message);
111
+ worker.once('message', (result) => {
112
+ worker.terminate();
113
+ if (!result.ok) return fail(result.message);
114
+ const name = result.partNames.length > 1 ? `${baseName}-${result.part}.stl` : `${baseName}.stl`;
115
+ res.writeHead(200, { 'Content-Type': 'model/stl', 'Content-Disposition': `attachment; filename="${name}"` });
116
+ res.end(Buffer.from(result.stl.buffer, result.stl.byteOffset, result.stl.byteLength));
117
+ console.log(`↓ ${name}`);
118
+ });
119
+ worker.once('error', (error) => fail(error.message));
120
+ }
121
+
105
122
  const options = bundleOptions(file);
106
123
  const ctx = await esbuild.context({
107
124
  ...options,
@@ -127,6 +144,7 @@ export async function startDevServer({ file, port, host, part, overrides, open }
127
144
  const server = http.createServer((req, res) => {
128
145
  const url = new URL(req.url ?? '/', 'http://localhost');
129
146
  if (url.pathname === '/') return serveFile(res, CLIENT_DIR, 'index.html');
147
+ if (url.pathname === '/stl') return downloadSTL(res, url.searchParams.get('part') ?? state.part);
130
148
  if (url.pathname.startsWith('/vendor/three/')) return serveFile(res, THREE_DIR, url.pathname.slice('/vendor/three/'.length));
131
149
  serveFile(res, CLIENT_DIR, url.pathname.slice(1));
132
150
  });
package/src/dev/worker.js CHANGED
@@ -1,10 +1,12 @@
1
- // Evaluates a program in a worker thread. Each evaluation gets a fresh worker:
1
+ // Evaluates a program in a worker thread, for the viewer's meshes or, with
2
+ // output 'stl', for a download. Each evaluation gets a fresh worker:
2
3
  // that frees all WASM memory between runs, resets library state such as
3
4
  // setResolution(), and lets the server kill a run that a newer save made stale.
4
5
  import { parentPort, workerData } from 'node:worker_threads';
5
6
  import { format } from 'node:util';
6
7
  import { evaluateProgram } from '../core/program.js';
7
8
  import { meshGroups } from '../core/mesh.js';
9
+ import { toSTL } from '../export/stl.js';
8
10
 
9
11
  // Bundles carry inline source maps; this makes stack traces point at the user's files.
10
12
  process.setSourceMapsEnabled(true);
@@ -18,22 +20,27 @@ for (const level of ['log', 'info', 'warn', 'error']) {
18
20
  };
19
21
  }
20
22
 
21
- const { bundle, part, overrides } = workerData;
23
+ const { bundle, part, overrides, output } = workerData;
22
24
  const started = performance.now();
23
25
  try {
24
- const result = await evaluateProgram(bundle, { part, overrides, fallbackToFirst: true });
25
- const groups = meshGroups(result.solids);
26
- parentPort.postMessage(
27
- {
28
- ok: true,
29
- partNames: result.partNames,
30
- part: result.part,
31
- groups,
32
- logs,
33
- ms: Math.round(performance.now() - started),
34
- },
35
- groups.flatMap((g) => [g.positions.buffer, g.indices.buffer]),
36
- );
26
+ const result = await evaluateProgram(bundle, { part, overrides, fallbackToFirst: output !== 'stl' });
27
+ if (output === 'stl') {
28
+ const stl = toSTL(result.solids);
29
+ parentPort.postMessage({ ok: true, partNames: result.partNames, part: result.part, stl }, [stl.buffer]);
30
+ } else {
31
+ const groups = meshGroups(result.solids);
32
+ parentPort.postMessage(
33
+ {
34
+ ok: true,
35
+ partNames: result.partNames,
36
+ part: result.part,
37
+ groups,
38
+ logs,
39
+ ms: Math.round(performance.now() - started),
40
+ },
41
+ groups.flatMap((g) => [g.positions.buffer, g.indices.buffer]),
42
+ );
43
+ }
37
44
  } catch (error) {
38
45
  parentPort.postMessage({ ok: false, message: error?.message ?? String(error), stack: error?.stack, logs });
39
46
  }