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 +1 -0
- package/client/index.html +3 -0
- package/client/viewer.css +5 -0
- package/client/viewer.js +23 -0
- package/package.json +1 -1
- package/src/dev/server.js +18 -0
- package/src/dev/worker.js +22 -15
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
package/client/viewer.css
CHANGED
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
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
|
|
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:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|