hyperbook 0.93.0 → 0.94.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/dist/assets/directive-openscad/client.js +477 -270
- package/dist/assets/directive-openscad/style.css +12 -0
- package/dist/assets/directive-openscad/worker.js +322 -0
- package/dist/assets/directive-pyide/client.js +32 -23
- package/dist/index.js +5 -1
- package/dist/locales/de.json +2 -0
- package/dist/locales/en.json +2 -0
- package/package.json +4 -4
- package/dist/assets/directive-openscad/STLLoader.js +0 -411
|
@@ -16,226 +16,114 @@ hyperbook.openscad = (function () {
|
|
|
16
16
|
]),
|
|
17
17
|
);
|
|
18
18
|
|
|
19
|
-
// Cache the ESM module import (loaded once). Each render creates a fresh
|
|
20
|
-
// WASM instance to avoid C++ singleton state issues
|
|
21
|
-
// (e.g. the Manifold backend throwing a C++ exception on second callMain).
|
|
22
|
-
let openscadModulePromise = null;
|
|
23
19
|
let threePromise = null;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Per-render stderr capture — cleared before each render.
|
|
29
|
-
let openscadStderr = [];
|
|
20
|
+
let openscadWorkerPromise = null;
|
|
21
|
+
let workerRequestId = 0;
|
|
22
|
+
const pendingWorkerRequests = new Map();
|
|
30
23
|
|
|
31
24
|
const i18nGet = (key, fallback = key) => hyperbook.i18n?.get(key) || fallback;
|
|
32
25
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<dir>/fonts</dir>
|
|
37
|
-
</fontconfig>`;
|
|
38
|
-
|
|
39
|
-
// Create a fresh OpenSCAD WASM instance for each render.
|
|
40
|
-
// The ESM module (and its compiled WASM binary) is imported only once;
|
|
41
|
-
// the browser's WebAssembly module cache makes subsequent instantiations fast.
|
|
42
|
-
const getOpenScad = async () => {
|
|
43
|
-
if (!openscadModulePromise) {
|
|
44
|
-
openscadModulePromise = import(/* @vite-ignore */ _scriptBase + "openscad.js");
|
|
45
|
-
}
|
|
46
|
-
const OpenSCAD = (await openscadModulePromise).default;
|
|
47
|
-
const instance = await OpenSCAD({
|
|
48
|
-
noInitialRun: true,
|
|
49
|
-
locateFile: (file) => _scriptBase + file,
|
|
50
|
-
printErr: (text) => openscadStderr.push(text),
|
|
51
|
-
});
|
|
52
|
-
const fs = instance.FS;
|
|
53
|
-
try { fs.mkdir("/tmp"); } catch (_) {}
|
|
54
|
-
try { fs.mkdir("/fonts"); } catch (_) {}
|
|
55
|
-
// Fonts are resolved from $(cwd)/fonts — keep cwd at /
|
|
56
|
-
try { instance.FS.chdir("/"); } catch (_) {}
|
|
57
|
-
try { fs.writeFile("/fonts/fonts.conf", FONTS_CONF); } catch (_) {}
|
|
58
|
-
// Write cached font data if already fetched
|
|
59
|
-
if (robotoFontData) {
|
|
60
|
-
try { fs.writeFile("/fonts/Roboto-Regular.ttf", robotoFontData); } catch (_) {}
|
|
26
|
+
const rejectPendingWorkerRequests = (error) => {
|
|
27
|
+
for (const { reject } of pendingWorkerRequests.values()) {
|
|
28
|
+
reject(error);
|
|
61
29
|
}
|
|
62
|
-
|
|
30
|
+
pendingWorkerRequests.clear();
|
|
63
31
|
};
|
|
64
32
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
BOSL: "https://ochafik.com/openscad2/libraries/BOSL.zip",
|
|
69
|
-
MCAD: "https://ochafik.com/openscad2/libraries/MCAD.zip",
|
|
70
|
-
NopSCADlib: "https://ochafik.com/openscad2/libraries/NopSCADlib.zip",
|
|
71
|
-
fonts: "https://ochafik.com/openscad2/libraries/fonts.zip",
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Per-name cache of extracted file maps: Map<name, { [path]: Uint8Array }>
|
|
75
|
-
const libraryCache = new Map();
|
|
76
|
-
|
|
77
|
-
// Minimal ZIP extractor using the browser-native DecompressionStream API.
|
|
78
|
-
// Supports Stored (method 0) and Deflate (method 8) entries.
|
|
79
|
-
const extractZip = async (buffer) => {
|
|
80
|
-
const view = new DataView(buffer);
|
|
81
|
-
const bytes = new Uint8Array(buffer);
|
|
82
|
-
const files = {};
|
|
83
|
-
const dec = new TextDecoder();
|
|
84
|
-
|
|
85
|
-
// Locate End of Central Directory record.
|
|
86
|
-
let eocdPos = -1;
|
|
87
|
-
for (let i = buffer.byteLength - 22; i >= Math.max(0, buffer.byteLength - 65558); i--) {
|
|
88
|
-
if (view.getUint32(i, true) === 0x06054b50) { eocdPos = i; break; }
|
|
89
|
-
}
|
|
90
|
-
if (eocdPos < 0) throw new Error("Not a valid ZIP file");
|
|
91
|
-
|
|
92
|
-
const entryCount = view.getUint16(eocdPos + 10, true);
|
|
93
|
-
let cdOffset = view.getUint32(eocdPos + 16, true);
|
|
94
|
-
|
|
95
|
-
for (let i = 0; i < entryCount; i++) {
|
|
96
|
-
if (view.getUint32(cdOffset, true) !== 0x02014b50) break;
|
|
97
|
-
const compression = view.getUint16(cdOffset + 10, true);
|
|
98
|
-
const compressedSize = view.getUint32(cdOffset + 20, true);
|
|
99
|
-
const fnLen = view.getUint16(cdOffset + 28, true);
|
|
100
|
-
const extraLen = view.getUint16(cdOffset + 30, true);
|
|
101
|
-
const commentLen = view.getUint16(cdOffset + 32, true);
|
|
102
|
-
const localOffset = view.getUint32(cdOffset + 42, true);
|
|
103
|
-
const name = dec.decode(bytes.subarray(cdOffset + 46, cdOffset + 46 + fnLen));
|
|
104
|
-
cdOffset += 46 + fnLen + extraLen + commentLen;
|
|
105
|
-
|
|
106
|
-
if (name.endsWith("/")) continue;
|
|
107
|
-
|
|
108
|
-
const localFnLen = view.getUint16(localOffset + 26, true);
|
|
109
|
-
const localExtraLen = view.getUint16(localOffset + 28, true);
|
|
110
|
-
const dataStart = localOffset + 30 + localFnLen + localExtraLen;
|
|
111
|
-
const compressed = bytes.subarray(dataStart, dataStart + compressedSize);
|
|
112
|
-
|
|
113
|
-
if (compression === 0) {
|
|
114
|
-
files[name] = new Uint8Array(compressed);
|
|
115
|
-
} else if (compression === 8) {
|
|
116
|
-
const ds = new DecompressionStream("deflate-raw");
|
|
117
|
-
const writer = ds.writable.getWriter();
|
|
118
|
-
const reader = ds.readable.getReader();
|
|
119
|
-
writer.write(compressed);
|
|
120
|
-
writer.close();
|
|
121
|
-
const chunks = [];
|
|
122
|
-
let totalLen = 0;
|
|
123
|
-
while (true) {
|
|
124
|
-
const { done, value } = await reader.read();
|
|
125
|
-
if (done) break;
|
|
126
|
-
chunks.push(value);
|
|
127
|
-
totalLen += value.byteLength;
|
|
128
|
-
}
|
|
129
|
-
const out = new Uint8Array(totalLen);
|
|
130
|
-
let pos = 0;
|
|
131
|
-
for (const c of chunks) { out.set(c, pos); pos += c.byteLength; }
|
|
132
|
-
files[name] = out;
|
|
133
|
-
}
|
|
33
|
+
const getOpenScadWorker = async () => {
|
|
34
|
+
if (!window.Worker) {
|
|
35
|
+
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
134
36
|
}
|
|
135
|
-
|
|
136
|
-
|
|
37
|
+
if (!openscadWorkerPromise) {
|
|
38
|
+
openscadWorkerPromise = new Promise((resolve, reject) => {
|
|
39
|
+
try {
|
|
40
|
+
const worker = new Worker(new URL(_scriptBase + "worker.js", window.location.href), {
|
|
41
|
+
type: "module",
|
|
42
|
+
});
|
|
137
43
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
44
|
+
worker.addEventListener("message", (event) => {
|
|
45
|
+
const { requestId, ok, result, error } = event.data || {};
|
|
46
|
+
if (!requestId || !pendingWorkerRequests.has(requestId)) return;
|
|
47
|
+
const pending = pendingWorkerRequests.get(requestId);
|
|
48
|
+
pendingWorkerRequests.delete(requestId);
|
|
49
|
+
if (ok) {
|
|
50
|
+
pending.resolve(result);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const workerError = new Error(error?.message || "OpenSCAD worker request failed");
|
|
54
|
+
if (Array.isArray(error?.stderr)) {
|
|
55
|
+
workerError.stderr = error.stderr;
|
|
56
|
+
}
|
|
57
|
+
pending.reject(workerError);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
worker.addEventListener("error", (event) => {
|
|
61
|
+
const workerError = new Error(event?.message || "OpenSCAD worker crashed");
|
|
62
|
+
rejectPendingWorkerRequests(workerError);
|
|
63
|
+
openscadWorkerPromise = null;
|
|
64
|
+
});
|
|
149
65
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
dir += "/" + parts[j];
|
|
161
|
-
try { instance.FS.mkdir(dir); } catch (_) {}
|
|
66
|
+
worker.addEventListener("messageerror", () => {
|
|
67
|
+
const workerError = new Error("OpenSCAD worker message error");
|
|
68
|
+
rejectPendingWorkerRequests(workerError);
|
|
69
|
+
openscadWorkerPromise = null;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
resolve(worker);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
openscadWorkerPromise = null;
|
|
75
|
+
reject(error);
|
|
162
76
|
}
|
|
163
|
-
|
|
164
|
-
}
|
|
77
|
+
});
|
|
165
78
|
}
|
|
79
|
+
return openscadWorkerPromise;
|
|
166
80
|
};
|
|
167
81
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
82
|
+
const callOpenScadWorker = async (type, payload, transfer = []) => {
|
|
83
|
+
const worker = await getOpenScadWorker();
|
|
84
|
+
const requestId = ++workerRequestId;
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
pendingWorkerRequests.set(requestId, { resolve, reject });
|
|
87
|
+
try {
|
|
88
|
+
worker.postMessage({ requestId, type, payload }, transfer);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
pendingWorkerRequests.delete(requestId);
|
|
91
|
+
reject(error);
|
|
178
92
|
}
|
|
179
|
-
}
|
|
180
|
-
console.warn("[openscad] Failed to load fonts:", e);
|
|
181
|
-
}
|
|
93
|
+
});
|
|
182
94
|
};
|
|
183
95
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
96
|
+
const getInvocationStderr = (invocationResult) =>
|
|
97
|
+
(invocationResult?.mergedOutputs || [])
|
|
98
|
+
.filter((entry) => typeof entry?.stderr === "string")
|
|
99
|
+
.map((entry) => entry.stderr);
|
|
100
|
+
|
|
101
|
+
// Extract parameters from SCAD code in the worker to keep the main thread responsive.
|
|
187
102
|
const extractParams = async (code, libraryNames = []) => {
|
|
188
103
|
try {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (libraryNames.length > 0) {
|
|
193
|
-
await mountLibraries(instance, libraryNames);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const sourcePath = "/tmp/params_model.scad";
|
|
197
|
-
const outPath = "/tmp/params_out.json";
|
|
198
|
-
|
|
199
|
-
try { instance.FS.unlink(sourcePath); } catch (_) {}
|
|
200
|
-
try { instance.FS.unlink(outPath); } catch (_) {}
|
|
201
|
-
|
|
202
|
-
// Prepend $preview=true as the playground does, to avoid full geometry evaluation.
|
|
203
|
-
instance.FS.writeFile(sourcePath, "$preview=true;\n" + code);
|
|
204
|
-
|
|
205
|
-
const exitCode = instance.callMain([
|
|
206
|
-
sourcePath,
|
|
207
|
-
"-o", outPath,
|
|
208
|
-
"--export-format=param",
|
|
209
|
-
]);
|
|
210
|
-
|
|
211
|
-
if (exitCode === 0) {
|
|
212
|
-
try {
|
|
213
|
-
const json = instance.FS.readFile(outPath, { encoding: "utf8" });
|
|
214
|
-
const paramSet = JSON.parse(json);
|
|
215
|
-
if (Array.isArray(paramSet.parameters) && paramSet.parameters.length > 0) {
|
|
216
|
-
// Filter out OpenSCAD special variables (e.g. $preview, $fn, $fa, $fs)
|
|
217
|
-
// that are internal and should not be exposed in the parameter UI.
|
|
218
|
-
return paramSet.parameters.filter(p => !p.name?.startsWith('$'));
|
|
219
|
-
}
|
|
220
|
-
} catch (e) {
|
|
221
|
-
console.warn("[openscad] Failed to parse param output:", e);
|
|
222
|
-
}
|
|
104
|
+
const result = await callOpenScadWorker("extractParams", { code, libraryNames });
|
|
105
|
+
if (result?.error || result?.exitCode !== 0) {
|
|
106
|
+
return [];
|
|
223
107
|
}
|
|
108
|
+
const output = result?.outputs?.[0]?.[1];
|
|
109
|
+
if (!output) return [];
|
|
110
|
+
const json = new TextDecoder().decode(toUint8Array(output));
|
|
111
|
+
const paramSet = JSON.parse(json);
|
|
112
|
+
if (!Array.isArray(paramSet?.parameters)) return [];
|
|
113
|
+
return paramSet.parameters.filter((p) => !p.name?.startsWith("$"));
|
|
224
114
|
} catch (e) {
|
|
225
|
-
console.warn("[openscad]
|
|
115
|
+
console.warn("[openscad] Worker param extraction failed:", e);
|
|
116
|
+
return [];
|
|
226
117
|
}
|
|
227
|
-
return [];
|
|
228
118
|
};
|
|
229
119
|
|
|
230
120
|
const getThree = async () => {
|
|
231
121
|
if (!threePromise) {
|
|
232
122
|
threePromise = Promise.all([
|
|
233
123
|
import(/* @vite-ignore */ _scriptBase + "three.module.js"),
|
|
234
|
-
import(/* @vite-ignore */ _scriptBase + "STLLoader.js"),
|
|
235
124
|
import(/* @vite-ignore */ _scriptBase + "OrbitControls.js"),
|
|
236
|
-
]).then(([THREE,
|
|
125
|
+
]).then(([THREE, OrbitControlsModule]) => ({
|
|
237
126
|
THREE,
|
|
238
|
-
STLLoader: STLLoaderModule.STLLoader,
|
|
239
127
|
OrbitControls: OrbitControlsModule.OrbitControls,
|
|
240
128
|
}));
|
|
241
129
|
}
|
|
@@ -419,6 +307,318 @@ hyperbook.openscad = (function () {
|
|
|
419
307
|
return new Uint8Array(data || []);
|
|
420
308
|
};
|
|
421
309
|
|
|
310
|
+
const textEncoder = new TextEncoder();
|
|
311
|
+
const DEFAULT_FACE_COLOR = [0xf9 / 255, 0xd7 / 255, 0x2c / 255, 1];
|
|
312
|
+
const PAINT_COLOR_MAP = ["", "8", "0C", "1C", "2C", "3C", "4C", "5C", "6C", "7C", "8C", "9C", "AC", "BC", "CC", "DC"];
|
|
313
|
+
const CRC32_TABLE = (() => {
|
|
314
|
+
const table = new Uint32Array(256);
|
|
315
|
+
for (let n = 0; n < 256; n++) {
|
|
316
|
+
let c = n;
|
|
317
|
+
for (let k = 0; k < 8; k++) {
|
|
318
|
+
c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
319
|
+
}
|
|
320
|
+
table[n] = c >>> 0;
|
|
321
|
+
}
|
|
322
|
+
return table;
|
|
323
|
+
})();
|
|
324
|
+
|
|
325
|
+
const crc32 = (bytes) => {
|
|
326
|
+
let crc = 0xffffffff;
|
|
327
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
328
|
+
crc = CRC32_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
|
|
329
|
+
}
|
|
330
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const concatUint8Arrays = (chunks) => {
|
|
334
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
335
|
+
const out = new Uint8Array(total);
|
|
336
|
+
let offset = 0;
|
|
337
|
+
for (const chunk of chunks) {
|
|
338
|
+
out.set(chunk, offset);
|
|
339
|
+
offset += chunk.length;
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const createZip = (files) => {
|
|
345
|
+
const localChunks = [];
|
|
346
|
+
const centralChunks = [];
|
|
347
|
+
let offset = 0;
|
|
348
|
+
|
|
349
|
+
for (const [name, dataLike] of Object.entries(files)) {
|
|
350
|
+
const nameBytes = textEncoder.encode(name);
|
|
351
|
+
const data = toUint8Array(dataLike);
|
|
352
|
+
const crc = crc32(data);
|
|
353
|
+
const localHeader = new Uint8Array(30 + nameBytes.length);
|
|
354
|
+
const localView = new DataView(localHeader.buffer);
|
|
355
|
+
localView.setUint32(0, 0x04034b50, true);
|
|
356
|
+
localView.setUint16(4, 20, true);
|
|
357
|
+
localView.setUint16(6, 0, true);
|
|
358
|
+
localView.setUint16(8, 0, true);
|
|
359
|
+
localView.setUint16(10, 0, true);
|
|
360
|
+
localView.setUint16(12, 0, true);
|
|
361
|
+
localView.setUint32(14, crc, true);
|
|
362
|
+
localView.setUint32(18, data.length, true);
|
|
363
|
+
localView.setUint32(22, data.length, true);
|
|
364
|
+
localView.setUint16(26, nameBytes.length, true);
|
|
365
|
+
localView.setUint16(28, 0, true);
|
|
366
|
+
localHeader.set(nameBytes, 30);
|
|
367
|
+
localChunks.push(localHeader, data);
|
|
368
|
+
|
|
369
|
+
const centralHeader = new Uint8Array(46 + nameBytes.length);
|
|
370
|
+
const centralView = new DataView(centralHeader.buffer);
|
|
371
|
+
centralView.setUint32(0, 0x02014b50, true);
|
|
372
|
+
centralView.setUint16(4, 20, true);
|
|
373
|
+
centralView.setUint16(6, 20, true);
|
|
374
|
+
centralView.setUint16(8, 0, true);
|
|
375
|
+
centralView.setUint16(10, 0, true);
|
|
376
|
+
centralView.setUint16(12, 0, true);
|
|
377
|
+
centralView.setUint16(14, 0, true);
|
|
378
|
+
centralView.setUint32(16, crc, true);
|
|
379
|
+
centralView.setUint32(20, data.length, true);
|
|
380
|
+
centralView.setUint32(24, data.length, true);
|
|
381
|
+
centralView.setUint16(28, nameBytes.length, true);
|
|
382
|
+
centralView.setUint16(30, 0, true);
|
|
383
|
+
centralView.setUint16(32, 0, true);
|
|
384
|
+
centralView.setUint16(34, 0, true);
|
|
385
|
+
centralView.setUint16(36, 0, true);
|
|
386
|
+
centralView.setUint32(38, 0, true);
|
|
387
|
+
centralView.setUint32(42, offset, true);
|
|
388
|
+
centralHeader.set(nameBytes, 46);
|
|
389
|
+
centralChunks.push(centralHeader);
|
|
390
|
+
|
|
391
|
+
offset += localHeader.length + data.length;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const centralDirectory = concatUint8Arrays(centralChunks);
|
|
395
|
+
const eocd = new Uint8Array(22);
|
|
396
|
+
const eocdView = new DataView(eocd.buffer);
|
|
397
|
+
eocdView.setUint32(0, 0x06054b50, true);
|
|
398
|
+
eocdView.setUint16(4, 0, true);
|
|
399
|
+
eocdView.setUint16(6, 0, true);
|
|
400
|
+
eocdView.setUint16(8, centralChunks.length, true);
|
|
401
|
+
eocdView.setUint16(10, centralChunks.length, true);
|
|
402
|
+
eocdView.setUint32(12, centralDirectory.length, true);
|
|
403
|
+
eocdView.setUint32(16, offset, true);
|
|
404
|
+
eocdView.setUint16(20, 0, true);
|
|
405
|
+
|
|
406
|
+
return concatUint8Arrays([...localChunks, centralDirectory, eocd]);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const toHexByte = (value) => Math.round(Math.max(0, Math.min(1, value)) * 255).toString(16).padStart(2, "0").toUpperCase();
|
|
410
|
+
|
|
411
|
+
const colorToDisplayColor = ([r, g, b, a = 1]) => {
|
|
412
|
+
const base = `#${toHexByte(r)}${toHexByte(g)}${toHexByte(b)}`;
|
|
413
|
+
return a < 1 ? `${base}${toHexByte(a)}` : base;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const createUuid = () =>
|
|
417
|
+
(globalThis.crypto?.randomUUID?.() ||
|
|
418
|
+
`${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}-${Math.random().toString(16).slice(2, 10)}`);
|
|
419
|
+
|
|
420
|
+
const parseOffToIndexedPolyhedron = (offData) => {
|
|
421
|
+
const arrayBuffer = offData.buffer.slice(
|
|
422
|
+
offData.byteOffset,
|
|
423
|
+
offData.byteOffset + offData.byteLength,
|
|
424
|
+
);
|
|
425
|
+
const text = new TextDecoder().decode(arrayBuffer);
|
|
426
|
+
const lines = text
|
|
427
|
+
.split("\n")
|
|
428
|
+
.map((line) => line.trim())
|
|
429
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
430
|
+
|
|
431
|
+
if (lines.length === 0) {
|
|
432
|
+
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let countsLine = "";
|
|
436
|
+
let currentLine = 0;
|
|
437
|
+
if (/^OFF(\s|$)/.test(lines[0])) {
|
|
438
|
+
countsLine = lines[0].substring(3).trim();
|
|
439
|
+
currentLine = 1;
|
|
440
|
+
} else if (lines[0] === "OFF" && lines.length > 1) {
|
|
441
|
+
countsLine = lines[1];
|
|
442
|
+
currentLine = 2;
|
|
443
|
+
} else {
|
|
444
|
+
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const [vertexCountRaw, faceCountRaw] = countsLine.split(/\s+/).map(Number);
|
|
448
|
+
const vertexCount = Number.isFinite(vertexCountRaw) ? Math.floor(vertexCountRaw) : NaN;
|
|
449
|
+
const faceCount = Number.isFinite(faceCountRaw) ? Math.floor(faceCountRaw) : NaN;
|
|
450
|
+
if (!Number.isFinite(vertexCount) || !Number.isFinite(faceCount) || vertexCount <= 0 || faceCount <= 0) {
|
|
451
|
+
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
452
|
+
}
|
|
453
|
+
if (currentLine + vertexCount + faceCount > lines.length) {
|
|
454
|
+
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const vertices = [];
|
|
458
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
459
|
+
const parts = lines[currentLine + i].split(/\s+/).map(Number);
|
|
460
|
+
if (parts.length < 3 || parts.some(Number.isNaN)) {
|
|
461
|
+
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
462
|
+
}
|
|
463
|
+
vertices.push({ x: parts[0], y: parts[1], z: parts[2] });
|
|
464
|
+
}
|
|
465
|
+
currentLine += vertexCount;
|
|
466
|
+
|
|
467
|
+
const colors = [];
|
|
468
|
+
const colorMap = new Map();
|
|
469
|
+
const faces = [];
|
|
470
|
+
|
|
471
|
+
for (let i = 0; i < faceCount; i++) {
|
|
472
|
+
const parts = lines[currentLine + i].split(/\s+/).map(Number);
|
|
473
|
+
const numVerts = Number.isFinite(parts[0]) ? Math.floor(parts[0]) : 0;
|
|
474
|
+
const faceVertices = parts.slice(1, numVerts + 1).map((index) => Math.floor(index));
|
|
475
|
+
if (faceVertices.length < 3) {
|
|
476
|
+
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
let color = DEFAULT_FACE_COLOR;
|
|
480
|
+
if (parts.length >= numVerts + 4) {
|
|
481
|
+
const raw = parts.slice(numVerts + 1, numVerts + 5).filter(Number.isFinite);
|
|
482
|
+
if (raw.length >= 3) {
|
|
483
|
+
const r = raw[0];
|
|
484
|
+
const g = raw[1];
|
|
485
|
+
const b = raw[2];
|
|
486
|
+
const a = raw.length >= 4 ? raw[3] : (Math.max(r, g, b) > 1 ? 255 : 1);
|
|
487
|
+
const divisor = Math.max(r, g, b, a) > 1 ? 255 : 1;
|
|
488
|
+
color = [r / divisor, g / divisor, b / divisor, a / divisor];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const colorKey = color.join(",");
|
|
493
|
+
let colorIndex = colorMap.get(colorKey);
|
|
494
|
+
if (colorIndex == null) {
|
|
495
|
+
colorIndex = colors.length;
|
|
496
|
+
colors.push(color);
|
|
497
|
+
colorMap.set(colorKey, colorIndex);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (faceVertices.length === 3) {
|
|
501
|
+
faces.push({ vertices: faceVertices, colorIndex });
|
|
502
|
+
} else {
|
|
503
|
+
for (let j = 1; j < faceVertices.length - 1; j++) {
|
|
504
|
+
faces.push({
|
|
505
|
+
vertices: [faceVertices[0], faceVertices[j], faceVertices[j + 1]],
|
|
506
|
+
colorIndex,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return { vertices, faces, colors };
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const buildThreeModelFromIndexedPolyhedron = (polyhedron, THREE) => {
|
|
516
|
+
const model = new THREE.Group();
|
|
517
|
+
const facesByColor = new Map();
|
|
518
|
+
|
|
519
|
+
for (const face of polyhedron.faces) {
|
|
520
|
+
const [i1, i2, i3] = face.vertices;
|
|
521
|
+
const v1 = polyhedron.vertices[i1];
|
|
522
|
+
const v2 = polyhedron.vertices[i2];
|
|
523
|
+
const v3 = polyhedron.vertices[i3];
|
|
524
|
+
if (!v1 || !v2 || !v3) continue;
|
|
525
|
+
const color = polyhedron.colors[face.colorIndex] || DEFAULT_FACE_COLOR;
|
|
526
|
+
const colorKey = color.join(",");
|
|
527
|
+
let bucket = facesByColor.get(colorKey);
|
|
528
|
+
if (!bucket) {
|
|
529
|
+
bucket = { color, positions: [] };
|
|
530
|
+
facesByColor.set(colorKey, bucket);
|
|
531
|
+
}
|
|
532
|
+
bucket.positions.push(v1.x, v1.y, v1.z, v2.x, v2.y, v2.z, v3.x, v3.y, v3.z);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (facesByColor.size === 0) {
|
|
536
|
+
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
for (const bucket of facesByColor.values()) {
|
|
540
|
+
const geometry = new THREE.BufferGeometry();
|
|
541
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(bucket.positions, 3));
|
|
542
|
+
geometry.computeBoundingBox();
|
|
543
|
+
geometry.computeVertexNormals();
|
|
544
|
+
const [r, g, b, a = 1] = bucket.color;
|
|
545
|
+
const material = new THREE.MeshStandardMaterial({
|
|
546
|
+
color: new THREE.Color(r, g, b),
|
|
547
|
+
transparent: a < 1,
|
|
548
|
+
opacity: a,
|
|
549
|
+
metalness: 0.1,
|
|
550
|
+
roughness: 0.6,
|
|
551
|
+
});
|
|
552
|
+
model.add(new THREE.Mesh(geometry, material));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return model;
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const exportIndexedPolyhedronTo3mf = (polyhedron) => {
|
|
559
|
+
const objectUuid = createUuid();
|
|
560
|
+
const buildUuid = createUuid();
|
|
561
|
+
const extruderIndexByColorIndex = polyhedron.colors.map((_, idx) => idx % PAINT_COLOR_MAP.length);
|
|
562
|
+
|
|
563
|
+
const modelXml = [
|
|
564
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
565
|
+
'<model unit="millimeter" xml:lang="en-US" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06">',
|
|
566
|
+
'<meta name="BambuStudio:3mfVersion" value="1"/>',
|
|
567
|
+
'<meta name="slic3rpe:Version3mf" value="1"/>',
|
|
568
|
+
'<meta name="slic3rpe:MmPaintingVersion" value="1"/>',
|
|
569
|
+
"<resources>",
|
|
570
|
+
'<basematerials id="2">',
|
|
571
|
+
...polyhedron.colors.map((color, i) => `<base name="color_${i}" displaycolor="${colorToDisplayColor(color)}"/>`),
|
|
572
|
+
"</basematerials>",
|
|
573
|
+
`<object id="1" name="OpenSCAD Model" type="model" p:UUID="${objectUuid}" pid="2" pindex="0">`,
|
|
574
|
+
"<mesh>",
|
|
575
|
+
"<vertices>",
|
|
576
|
+
...polyhedron.vertices.map((vertex) => `<vertex x="${vertex.x}" y="${vertex.y}" z="${vertex.z}" />`),
|
|
577
|
+
"</vertices>",
|
|
578
|
+
"<triangles>",
|
|
579
|
+
...polyhedron.faces.map((face) => {
|
|
580
|
+
const [v1, v2, v3] = face.vertices;
|
|
581
|
+
const attrs = [`v1="${v1}"`, `v2="${v2}"`, `v3="${v3}"`];
|
|
582
|
+
if (face.colorIndex > 0) {
|
|
583
|
+
attrs.push(`pid="2"`, `p1="${face.colorIndex}"`);
|
|
584
|
+
}
|
|
585
|
+
const paintColor = PAINT_COLOR_MAP[extruderIndexByColorIndex[face.colorIndex]];
|
|
586
|
+
if (paintColor) {
|
|
587
|
+
attrs.push(`paint_color="${paintColor}"`);
|
|
588
|
+
}
|
|
589
|
+
return `<triangle ${attrs.join(" ")} />`;
|
|
590
|
+
}),
|
|
591
|
+
"</triangles>",
|
|
592
|
+
"</mesh>",
|
|
593
|
+
"</object>",
|
|
594
|
+
"</resources>",
|
|
595
|
+
`<build p:UUID="${buildUuid}">`,
|
|
596
|
+
`<item objectid="1" p:UUID="${objectUuid}"/>`,
|
|
597
|
+
"</build>",
|
|
598
|
+
"</model>",
|
|
599
|
+
].join("\n");
|
|
600
|
+
|
|
601
|
+
const contentTypesXml = [
|
|
602
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
603
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
|
|
604
|
+
'<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>',
|
|
605
|
+
"</Types>",
|
|
606
|
+
].join("\n");
|
|
607
|
+
|
|
608
|
+
const relationshipsXml = [
|
|
609
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
610
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
|
|
611
|
+
'<Relationship Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel" Target="/3D/3dmodel.model" Id="rel0"/>',
|
|
612
|
+
"</Relationships>",
|
|
613
|
+
].join("\n");
|
|
614
|
+
|
|
615
|
+
return createZip({
|
|
616
|
+
"3D/3dmodel.model": textEncoder.encode(modelXml),
|
|
617
|
+
"[Content_Types].xml": textEncoder.encode(contentTypesXml),
|
|
618
|
+
"_rels/.rels": textEncoder.encode(relationshipsXml),
|
|
619
|
+
});
|
|
620
|
+
};
|
|
621
|
+
|
|
422
622
|
function initElement(elem) {
|
|
423
623
|
if (elem.getAttribute("data-openscad-initialized") === "true") return;
|
|
424
624
|
elem.setAttribute("data-openscad-initialized", "true");
|
|
@@ -444,9 +644,27 @@ hyperbook.openscad = (function () {
|
|
|
444
644
|
|
|
445
645
|
const renderBtn = elem.querySelector("button.render");
|
|
446
646
|
const copyBtn = elem.querySelector("button.copy");
|
|
447
|
-
const
|
|
647
|
+
const downloadBtn = elem.querySelector("button.download-stl");
|
|
448
648
|
const resetBtn = elem.querySelector("button.reset");
|
|
449
649
|
const fullscreenBtn = elem.querySelector("button.fullscreen");
|
|
650
|
+
const bottomButtons = elem.querySelector(".buttons.bottom");
|
|
651
|
+
let downloadFormatSelect = bottomButtons?.querySelector("select.download-format");
|
|
652
|
+
if (!downloadFormatSelect && bottomButtons && downloadBtn) {
|
|
653
|
+
downloadFormatSelect = document.createElement("select");
|
|
654
|
+
downloadFormatSelect.className = "download-format";
|
|
655
|
+
downloadFormatSelect.setAttribute("aria-label", i18nGet("openscad-download-format", "Download format"));
|
|
656
|
+
const stlOption = document.createElement("option");
|
|
657
|
+
stlOption.value = "stl";
|
|
658
|
+
stlOption.textContent = "STL";
|
|
659
|
+
const threeMfOption = document.createElement("option");
|
|
660
|
+
threeMfOption.value = "3mf";
|
|
661
|
+
threeMfOption.textContent = "3MF";
|
|
662
|
+
downloadFormatSelect.append(stlOption, threeMfOption);
|
|
663
|
+
bottomButtons.insertBefore(downloadFormatSelect, downloadBtn);
|
|
664
|
+
}
|
|
665
|
+
if (downloadBtn) {
|
|
666
|
+
downloadBtn.textContent = i18nGet("openscad-download", "Download");
|
|
667
|
+
}
|
|
450
668
|
|
|
451
669
|
// --- Canvas overlay ---
|
|
452
670
|
let overlayDismissTimer = null;
|
|
@@ -510,7 +728,7 @@ hyperbook.openscad = (function () {
|
|
|
510
728
|
camera: null,
|
|
511
729
|
scene: null,
|
|
512
730
|
controls: null,
|
|
513
|
-
|
|
731
|
+
model: null,
|
|
514
732
|
raf: 0,
|
|
515
733
|
disposed: false,
|
|
516
734
|
resizeObserver: null,
|
|
@@ -700,54 +918,42 @@ hyperbook.openscad = (function () {
|
|
|
700
918
|
return Object.entries(parsed).map(([k, v]) => `-D${k}=${formatValue(v)}`);
|
|
701
919
|
};
|
|
702
920
|
|
|
703
|
-
const renderWithFormat = async (format, libraryNames = []) => {
|
|
921
|
+
const renderWithFormat = async (format, libraryNames = [], isPreview = false) => {
|
|
704
922
|
renderBtn?.setAttribute("disabled", "true");
|
|
705
923
|
showOverlay("loading", i18nGet("openscad-rendering", "Rendering..."));
|
|
706
924
|
|
|
707
925
|
try {
|
|
708
926
|
const paramDefinitions = getParamDefinitions();
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
927
|
+
const result = await callOpenScadWorker("render", {
|
|
928
|
+
code: editor?.value || "",
|
|
929
|
+
format,
|
|
930
|
+
libraryNames,
|
|
931
|
+
paramDefinitions,
|
|
932
|
+
isPreview,
|
|
933
|
+
});
|
|
934
|
+
const stderr = getInvocationStderr(result);
|
|
935
|
+
if (result?.error || result?.exitCode !== 0) {
|
|
936
|
+
const error = new Error(result?.error || i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
937
|
+
error.stderr = stderr;
|
|
938
|
+
throw error;
|
|
714
939
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
try {
|
|
721
|
-
instance.FS.unlink(sourcePath);
|
|
722
|
-
} catch (_) {}
|
|
723
|
-
try {
|
|
724
|
-
instance.FS.unlink(outPath);
|
|
725
|
-
} catch (_) {}
|
|
726
|
-
|
|
727
|
-
instance.FS.writeFile(sourcePath, editor?.value || "");
|
|
728
|
-
|
|
729
|
-
const args = [
|
|
730
|
-
sourcePath,
|
|
731
|
-
"-o",
|
|
732
|
-
outPath,
|
|
733
|
-
`--export-format=${exportFormat}`,
|
|
734
|
-
...paramDefinitions,
|
|
735
|
-
];
|
|
736
|
-
|
|
737
|
-
const exitCode = instance.callMain(args);
|
|
738
|
-
if (exitCode !== 0) {
|
|
739
|
-
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
940
|
+
const output = result?.outputs?.[0]?.[1];
|
|
941
|
+
if (!output) {
|
|
942
|
+
const error = new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
943
|
+
error.stderr = stderr;
|
|
944
|
+
throw error;
|
|
740
945
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
946
|
+
return {
|
|
947
|
+
content: toUint8Array(output),
|
|
948
|
+
stderr,
|
|
949
|
+
};
|
|
744
950
|
} finally {
|
|
745
951
|
renderBtn?.removeAttribute("disabled");
|
|
746
952
|
}
|
|
747
953
|
};
|
|
748
954
|
|
|
749
|
-
const downloadBinary = (content, ext) => {
|
|
750
|
-
const blob = new Blob([content], { type:
|
|
955
|
+
const downloadBinary = (content, ext, mimeType = "application/octet-stream") => {
|
|
956
|
+
const blob = new Blob([content], { type: mimeType });
|
|
751
957
|
const a = document.createElement("a");
|
|
752
958
|
a.href = URL.createObjectURL(blob);
|
|
753
959
|
a.download = `openscad-${id || "model"}.${ext}`;
|
|
@@ -756,18 +962,13 @@ hyperbook.openscad = (function () {
|
|
|
756
962
|
};
|
|
757
963
|
|
|
758
964
|
const renderPreview = async () => {
|
|
759
|
-
openscadStderr = [];
|
|
760
965
|
try {
|
|
761
|
-
// Ensure font bytes are fetched before creating the WASM instance
|
|
762
|
-
await loadFonts();
|
|
763
966
|
await save();
|
|
764
|
-
const
|
|
765
|
-
await
|
|
967
|
+
const { content: off } = await renderWithFormat("off", libraryNames, true);
|
|
968
|
+
await renderOff(off);
|
|
766
969
|
hideOverlay();
|
|
767
970
|
} catch (error) {
|
|
768
|
-
|
|
769
|
-
// emscripten throws C++ exceptions as raw numbers (WASM memory pointers).
|
|
770
|
-
const stderrErrors = openscadStderr.filter((l) => /error/i.test(l)).join("\n");
|
|
971
|
+
const stderrErrors = (error?.stderr || []).filter((l) => /error/i.test(l)).join("\n");
|
|
771
972
|
if (stderrErrors) {
|
|
772
973
|
showOverlay("error", stderrErrors);
|
|
773
974
|
} else if (typeof error === "number") {
|
|
@@ -778,8 +979,22 @@ hyperbook.openscad = (function () {
|
|
|
778
979
|
}
|
|
779
980
|
};
|
|
780
981
|
|
|
781
|
-
const
|
|
782
|
-
|
|
982
|
+
const disposeModel = () => {
|
|
983
|
+
if (!viewerState.model || !viewerState.scene) return;
|
|
984
|
+
viewerState.scene.remove(viewerState.model);
|
|
985
|
+
viewerState.model.traverse((child) => {
|
|
986
|
+
child.geometry?.dispose?.();
|
|
987
|
+
if (Array.isArray(child.material)) {
|
|
988
|
+
child.material.forEach((material) => material?.dispose?.());
|
|
989
|
+
} else {
|
|
990
|
+
child.material?.dispose?.();
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
viewerState.model = null;
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
const renderOff = async (offData) => {
|
|
997
|
+
const { THREE, OrbitControls } = await getThree();
|
|
783
998
|
if (!canvas) return;
|
|
784
999
|
|
|
785
1000
|
if (!viewerState.renderer) {
|
|
@@ -828,38 +1043,25 @@ hyperbook.openscad = (function () {
|
|
|
828
1043
|
viewerState.camera.aspect = width / height;
|
|
829
1044
|
viewerState.camera.updateProjectionMatrix();
|
|
830
1045
|
|
|
831
|
-
|
|
832
|
-
viewerState.scene.remove(viewerState.mesh);
|
|
833
|
-
viewerState.mesh.geometry?.dispose();
|
|
834
|
-
}
|
|
1046
|
+
disposeModel();
|
|
835
1047
|
|
|
836
|
-
const
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
);
|
|
841
|
-
const geometry = loader.parse(arrayBuffer);
|
|
842
|
-
geometry.computeBoundingBox();
|
|
843
|
-
geometry.computeVertexNormals();
|
|
1048
|
+
const polyhedron = parseOffToIndexedPolyhedron(offData);
|
|
1049
|
+
const model = buildThreeModelFromIndexedPolyhedron(polyhedron, THREE);
|
|
1050
|
+
viewerState.model = model;
|
|
1051
|
+
viewerState.scene.add(model);
|
|
844
1052
|
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
});
|
|
850
|
-
const mesh = new THREE.Mesh(geometry, material);
|
|
851
|
-
viewerState.mesh = mesh;
|
|
852
|
-
viewerState.scene.add(mesh);
|
|
853
|
-
|
|
854
|
-
const box = geometry.boundingBox;
|
|
1053
|
+
const box = new THREE.Box3().setFromObject(model);
|
|
1054
|
+
if (box.isEmpty()) {
|
|
1055
|
+
throw new Error(i18nGet("openscad-render-failed", "OpenSCAD render failed"));
|
|
1056
|
+
}
|
|
855
1057
|
const size = new THREE.Vector3();
|
|
856
1058
|
const center = new THREE.Vector3();
|
|
857
1059
|
box.getSize(size);
|
|
858
1060
|
box.getCenter(center);
|
|
859
1061
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1062
|
+
model.position.x = -center.x;
|
|
1063
|
+
model.position.y = -center.y;
|
|
1064
|
+
model.position.z = -center.z;
|
|
863
1065
|
|
|
864
1066
|
const maxDim = Math.max(size.x, size.y, size.z) || 1;
|
|
865
1067
|
const distance = maxDim * 1.8;
|
|
@@ -888,17 +1090,22 @@ hyperbook.openscad = (function () {
|
|
|
888
1090
|
|
|
889
1091
|
renderBtn?.addEventListener("click", renderPreview);
|
|
890
1092
|
|
|
891
|
-
|
|
892
|
-
openscadStderr = [];
|
|
1093
|
+
downloadBtn?.addEventListener("click", async () => {
|
|
893
1094
|
try {
|
|
894
|
-
await loadFonts();
|
|
895
1095
|
await save();
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
1096
|
+
const selectedFormat = downloadFormatSelect?.value === "3mf" ? "3mf" : "stl";
|
|
1097
|
+
if (selectedFormat === "3mf") {
|
|
1098
|
+
const { content: off } = await renderWithFormat("off", libraryNames);
|
|
1099
|
+
const polyhedron = parseOffToIndexedPolyhedron(off);
|
|
1100
|
+
const threeMf = exportIndexedPolyhedronTo3mf(polyhedron);
|
|
1101
|
+
downloadBinary(threeMf, "3mf", "model/3mf");
|
|
1102
|
+
} else {
|
|
1103
|
+
const { content: stl } = await renderWithFormat("stl", libraryNames);
|
|
1104
|
+
downloadBinary(stl, "stl");
|
|
1105
|
+
}
|
|
899
1106
|
hideOverlay();
|
|
900
1107
|
} catch (error) {
|
|
901
|
-
const stderrErrors =
|
|
1108
|
+
const stderrErrors = (error?.stderr || []).filter((l) => /error/i.test(l)).join("\n");
|
|
902
1109
|
showOverlay("error", stderrErrors || error?.message || `${error}`);
|
|
903
1110
|
}
|
|
904
1111
|
});
|