@omrips/viewstl 2.0.0-rc.1 → 2.0.0-rc.3
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 +41 -13
- package/build/v2/README.md +4 -0
- package/build/v2/node/analysis-worker.js +3833 -0
- package/build/v2/node/index.cjs +382 -0
- package/build/v2/node/index.d.cts +122 -0
- package/build/v2/node/index.d.ts +122 -0
- package/build/v2/node/index.js +352 -0
- package/build/v2/stl_viewer.min.js +1 -1
- package/build/v2/viewstl.cjs +1 -1
- package/build/v2/viewstl.js +1 -1
- package/build/v2/viewstl.min.js +1 -1
- package/docs/v2-compatibility.md +1 -1
- package/docs/v2-usage.md +123 -2
- package/docs/v2-verification.md +48 -0
- package/package.json +15 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// src/v2/node/api.js
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
// src/v2/memory.js
|
|
7
|
+
var MEMORY_OVERHEAD = 64 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
// src/v2/geometry.js
|
|
10
|
+
var DEFAULT_LIMITS = Object.freeze({
|
|
11
|
+
max_file_bytes: 256 * 1024 * 1024,
|
|
12
|
+
max_triangles: 5e6,
|
|
13
|
+
max_uncompressed_bytes: 256 * 1024 * 1024,
|
|
14
|
+
max_archive_entries: 1024,
|
|
15
|
+
max_memory_bytes: Number.MAX_SAFE_INTEGER
|
|
16
|
+
});
|
|
17
|
+
function limitsWith(options = {}) {
|
|
18
|
+
const limits = { ...DEFAULT_LIMITS };
|
|
19
|
+
for (const key of Object.keys(limits))
|
|
20
|
+
if (options[key] !== void 0) {
|
|
21
|
+
if (!Number.isSafeInteger(options[key]) || options[key] < 1)
|
|
22
|
+
throw new Error(`Invalid ${key}`);
|
|
23
|
+
limits[key] = options[key];
|
|
24
|
+
}
|
|
25
|
+
return limits;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/v2/print-options.js
|
|
29
|
+
function printOptions(input = {}) {
|
|
30
|
+
const level = input.level ?? "basic";
|
|
31
|
+
if (!["basic", "detailed"].includes(level))
|
|
32
|
+
throw new Error("Choose basic or detailed print checks");
|
|
33
|
+
const number = (v, name, max = 1e9) => {
|
|
34
|
+
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
35
|
+
throw new Error(`Invalid ${name}`);
|
|
36
|
+
return v;
|
|
37
|
+
};
|
|
38
|
+
let profile = null;
|
|
39
|
+
if (input.profile != null) {
|
|
40
|
+
if (input.profile.technology !== "fdm")
|
|
41
|
+
throw new Error("Only FDM profiles are supported");
|
|
42
|
+
profile = { technology: "fdm" };
|
|
43
|
+
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
44
|
+
if (input.profile[key] != null)
|
|
45
|
+
profile[key] = number(input.profile[key], key);
|
|
46
|
+
if (input.profile.overhang_degrees != null)
|
|
47
|
+
profile.overhang_degrees = number(
|
|
48
|
+
input.profile.overhang_degrees,
|
|
49
|
+
"overhang angle",
|
|
50
|
+
89.9
|
|
51
|
+
);
|
|
52
|
+
if (input.profile.build_volume_mm != null) {
|
|
53
|
+
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
54
|
+
throw new Error("Build volume requires X, Y and Z");
|
|
55
|
+
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
56
|
+
(v) => number(v, "build volume")
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const time = number(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
61
|
+
return {
|
|
62
|
+
level,
|
|
63
|
+
profile,
|
|
64
|
+
apply_transforms: input.apply_transforms !== false,
|
|
65
|
+
units_confirmed: input.units_confirmed === true,
|
|
66
|
+
max_time_ms: time
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/v2/node/api.js
|
|
71
|
+
var MB = 1024 ** 2;
|
|
72
|
+
var defaults = {
|
|
73
|
+
max_file_bytes: 64 * MB,
|
|
74
|
+
max_triangles: 1e6,
|
|
75
|
+
max_uncompressed_bytes: 128 * MB,
|
|
76
|
+
max_archive_entries: 1024,
|
|
77
|
+
max_memory_bytes: 1024 * MB
|
|
78
|
+
};
|
|
79
|
+
var formats = /* @__PURE__ */ new Map([
|
|
80
|
+
["stl", "stl"],
|
|
81
|
+
["obj", "obj"],
|
|
82
|
+
["3mf", "3mf"],
|
|
83
|
+
["step", "step"],
|
|
84
|
+
["stp", "step"],
|
|
85
|
+
["iges", "iges"],
|
|
86
|
+
["igs", "iges"]
|
|
87
|
+
]);
|
|
88
|
+
var units = /* @__PURE__ */ new Set(["mm", "cm", "m", "in", "ft", "um"]);
|
|
89
|
+
function failure(message, code, name = "Error") {
|
|
90
|
+
return Object.assign(new Error(message), { code, name });
|
|
91
|
+
}
|
|
92
|
+
function integer(value, name, min, max) {
|
|
93
|
+
if (!Number.isSafeInteger(value) || value < min || value > max)
|
|
94
|
+
throw new TypeError(`Invalid ${name}`);
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function localPath(input) {
|
|
98
|
+
if (input instanceof URL) {
|
|
99
|
+
if (input.protocol !== "file:" || input.hostname && input.hostname !== "localhost")
|
|
100
|
+
throw new TypeError("Only local file paths and file: URLs are supported");
|
|
101
|
+
input = fileURLToPath(input);
|
|
102
|
+
}
|
|
103
|
+
if (typeof input !== "string" || !input || input.includes("\0") || /^[\\/]{2}/.test(input) || /^[a-z][a-z\d+.-]*:/i.test(input) && !/^[a-z]:[\\/]/i.test(input))
|
|
104
|
+
throw new TypeError("Only local file paths and file: URLs are supported");
|
|
105
|
+
return resolve(input);
|
|
106
|
+
}
|
|
107
|
+
function settings(input, filename) {
|
|
108
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
109
|
+
throw new TypeError("Analysis options must be an object");
|
|
110
|
+
const format = formats.get(
|
|
111
|
+
String(input.format ?? filename.split(".").pop()).toLowerCase()
|
|
112
|
+
);
|
|
113
|
+
if (!format)
|
|
114
|
+
throw failure(
|
|
115
|
+
"Choose STL, OBJ, 3MF, STEP/STP or IGES/IGS",
|
|
116
|
+
"ERR_UNSUPPORTED_FORMAT"
|
|
117
|
+
);
|
|
118
|
+
if (input.units !== void 0 && !units.has(input.units))
|
|
119
|
+
throw new TypeError("Units must be mm, cm, m, in, ft or um");
|
|
120
|
+
if (input.units !== void 0 && input.units !== "mm" && !["stl", "obj"].includes(format))
|
|
121
|
+
throw new TypeError(
|
|
122
|
+
"3MF and CAD file units are honored and converted to mm; units cannot override them"
|
|
123
|
+
);
|
|
124
|
+
if (input.limits !== void 0 && (!input.limits || typeof input.limits !== "object" || Array.isArray(input.limits)))
|
|
125
|
+
throw new TypeError("limits must be an object");
|
|
126
|
+
for (const key of Object.keys(input.limits ?? {}))
|
|
127
|
+
if (!Object.hasOwn(defaults, key))
|
|
128
|
+
throw new TypeError(`Unknown analysis limit: ${key}`);
|
|
129
|
+
const overrides = Object.fromEntries(
|
|
130
|
+
Object.entries(input.limits ?? {}).filter(
|
|
131
|
+
([, value]) => value !== void 0
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
const limits = limitsWith({ ...defaults, ...overrides });
|
|
135
|
+
const timeout_ms = integer(
|
|
136
|
+
input.timeout_ms ?? 12e4,
|
|
137
|
+
"timeout_ms",
|
|
138
|
+
1,
|
|
139
|
+
2147483647
|
|
140
|
+
);
|
|
141
|
+
const checks = printOptions({
|
|
142
|
+
level: input.level ?? "detailed",
|
|
143
|
+
profile: input.profile,
|
|
144
|
+
max_time_ms: input.max_time_ms ?? 3e4
|
|
145
|
+
});
|
|
146
|
+
if (input.on_progress !== void 0 && typeof input.on_progress !== "function")
|
|
147
|
+
throw new TypeError("on_progress must be a function");
|
|
148
|
+
if (input.signal !== void 0 && !(input.signal instanceof AbortSignal))
|
|
149
|
+
throw new TypeError("signal must be an AbortSignal");
|
|
150
|
+
return { filename, format, units: input.units, limits, timeout_ms, checks };
|
|
151
|
+
}
|
|
152
|
+
function createAPI(workerURL) {
|
|
153
|
+
function createAnalyzer2(options = {}) {
|
|
154
|
+
const concurrency = integer(options.concurrency ?? 2, "concurrency", 1, 8);
|
|
155
|
+
const maxQueue = integer(options.max_queue ?? 16, "max_queue", 0, 1024);
|
|
156
|
+
const maxQueuedBytes = integer(
|
|
157
|
+
options.max_queued_bytes ?? 128 * MB,
|
|
158
|
+
"max_queued_bytes",
|
|
159
|
+
1,
|
|
160
|
+
Number.MAX_SAFE_INTEGER
|
|
161
|
+
);
|
|
162
|
+
const queue = [], active = /* @__PURE__ */ new Set();
|
|
163
|
+
let closed = false, queuedBytes = 0;
|
|
164
|
+
const abortError = () => failure("Analysis cancelled", "ABORT_ERR", "AbortError");
|
|
165
|
+
function finish(job, error, result) {
|
|
166
|
+
if (job.done) return job.finished;
|
|
167
|
+
job.done = true;
|
|
168
|
+
clearTimeout(job.timer);
|
|
169
|
+
job.signal?.removeEventListener("abort", job.abort);
|
|
170
|
+
const index = queue.indexOf(job);
|
|
171
|
+
if (index >= 0) {
|
|
172
|
+
queue.splice(index, 1);
|
|
173
|
+
queuedBytes -= job.bytes;
|
|
174
|
+
}
|
|
175
|
+
job.task = null;
|
|
176
|
+
job.finished = (async () => {
|
|
177
|
+
if (job.worker) await job.worker.terminate().catch(() => {
|
|
178
|
+
});
|
|
179
|
+
active.delete(job);
|
|
180
|
+
error ? job.reject(error) : job.resolve(result);
|
|
181
|
+
pump();
|
|
182
|
+
})();
|
|
183
|
+
return job.finished;
|
|
184
|
+
}
|
|
185
|
+
function pump() {
|
|
186
|
+
while (!closed && active.size < concurrency && queue.length) {
|
|
187
|
+
const job = queue.shift();
|
|
188
|
+
queuedBytes -= job.bytes;
|
|
189
|
+
active.add(job);
|
|
190
|
+
try {
|
|
191
|
+
const task = job.task;
|
|
192
|
+
job.worker = new Worker(workerURL, {
|
|
193
|
+
workerData: task,
|
|
194
|
+
transferList: task.buffer ? [task.buffer] : [],
|
|
195
|
+
// CLI eval/test flags are not valid entry flags for a file worker.
|
|
196
|
+
execArgv: [],
|
|
197
|
+
resourceLimits: {
|
|
198
|
+
maxOldGenerationSizeMb: Math.max(
|
|
199
|
+
16,
|
|
200
|
+
Math.min(
|
|
201
|
+
4096,
|
|
202
|
+
Math.floor(task.limits.max_memory_bytes / MB / 2)
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
job.task = null;
|
|
208
|
+
job.worker.on("message", (message) => {
|
|
209
|
+
if (job.done) return;
|
|
210
|
+
if (message.progress) {
|
|
211
|
+
try {
|
|
212
|
+
Promise.resolve(job.progress?.(message.progress)).catch(
|
|
213
|
+
(error) => void finish(job, error)
|
|
214
|
+
);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
void finish(job, error);
|
|
217
|
+
}
|
|
218
|
+
} else if (message.error) {
|
|
219
|
+
void finish(
|
|
220
|
+
job,
|
|
221
|
+
failure(
|
|
222
|
+
message.error.message,
|
|
223
|
+
message.error.code,
|
|
224
|
+
message.error.name
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
} else if (message.result) void finish(job, null, message.result);
|
|
228
|
+
else
|
|
229
|
+
void finish(
|
|
230
|
+
job,
|
|
231
|
+
failure(
|
|
232
|
+
"Invalid analysis worker response",
|
|
233
|
+
"ERR_ANALYSIS_WORKER"
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
job.worker.on("error", (error) => void finish(job, error));
|
|
238
|
+
job.worker.on("exit", (code) => {
|
|
239
|
+
if (!job.done)
|
|
240
|
+
void finish(
|
|
241
|
+
job,
|
|
242
|
+
failure(
|
|
243
|
+
`Analysis worker stopped (exit ${code})`,
|
|
244
|
+
"ERR_ANALYSIS_WORKER"
|
|
245
|
+
)
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
void finish(job, error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function submit(task, input, source) {
|
|
254
|
+
if (closed)
|
|
255
|
+
return Promise.reject(
|
|
256
|
+
failure("Analyzer is closed", "ERR_ANALYZER_CLOSED")
|
|
257
|
+
);
|
|
258
|
+
if (input.signal?.aborted) return Promise.reject(abortError());
|
|
259
|
+
if (active.size >= concurrency && queue.length >= maxQueue)
|
|
260
|
+
return Promise.reject(
|
|
261
|
+
failure("Analysis queue is full", "ERR_ANALYZER_BUSY")
|
|
262
|
+
);
|
|
263
|
+
const bytes = source?.byteLength ?? 0;
|
|
264
|
+
if (bytes > task.limits.max_file_bytes)
|
|
265
|
+
return Promise.reject(
|
|
266
|
+
failure("File size limit exceeded", "ERR_FILE_TOO_LARGE")
|
|
267
|
+
);
|
|
268
|
+
if (bytes * 2 + 64 * MB > task.limits.max_memory_bytes)
|
|
269
|
+
return Promise.reject(
|
|
270
|
+
failure(
|
|
271
|
+
"Input exceeds the analysis memory budget",
|
|
272
|
+
"ERR_ANALYSIS_LIMIT"
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
if (active.size >= concurrency && queuedBytes + bytes > maxQueuedBytes)
|
|
276
|
+
return Promise.reject(
|
|
277
|
+
failure("Queued input memory limit exceeded", "ERR_ANALYZER_BUSY")
|
|
278
|
+
);
|
|
279
|
+
if (source) task.buffer = new Uint8Array(source).buffer;
|
|
280
|
+
return new Promise((resolveJob, reject) => {
|
|
281
|
+
const job = {
|
|
282
|
+
task,
|
|
283
|
+
bytes,
|
|
284
|
+
signal: input.signal,
|
|
285
|
+
progress: input.on_progress,
|
|
286
|
+
resolve: resolveJob,
|
|
287
|
+
reject
|
|
288
|
+
};
|
|
289
|
+
job.abort = () => void finish(job, abortError());
|
|
290
|
+
job.signal?.addEventListener("abort", job.abort, { once: true });
|
|
291
|
+
job.timer = setTimeout(
|
|
292
|
+
() => void finish(
|
|
293
|
+
job,
|
|
294
|
+
failure("Analysis timed out", "ERR_ANALYSIS_TIMEOUT")
|
|
295
|
+
),
|
|
296
|
+
task.timeout_ms
|
|
297
|
+
);
|
|
298
|
+
queue.push(job);
|
|
299
|
+
queuedBytes += bytes;
|
|
300
|
+
pump();
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
async analyzeFile(path, options2 = {}) {
|
|
305
|
+
path = localPath(path);
|
|
306
|
+
return submit({ ...settings(options2, basename(path)), path }, options2);
|
|
307
|
+
},
|
|
308
|
+
async analyzeBuffer(data, options2 = {}) {
|
|
309
|
+
let bytes;
|
|
310
|
+
if (data instanceof ArrayBuffer) bytes = new Uint8Array(data);
|
|
311
|
+
else if (data instanceof Uint8Array && data.buffer instanceof ArrayBuffer)
|
|
312
|
+
bytes = data;
|
|
313
|
+
else
|
|
314
|
+
throw new TypeError(
|
|
315
|
+
"Expected an ArrayBuffer, Uint8Array or Node Buffer"
|
|
316
|
+
);
|
|
317
|
+
const filename = options2.filename ?? (options2.format ? `model.${options2.format}` : "");
|
|
318
|
+
if (typeof filename !== "string" || !filename || filename.includes("\0"))
|
|
319
|
+
throw new TypeError("Provide a filename or format for buffer input");
|
|
320
|
+
return submit(
|
|
321
|
+
settings(options2, filename.split(/[\\/]/).pop()),
|
|
322
|
+
options2,
|
|
323
|
+
bytes
|
|
324
|
+
);
|
|
325
|
+
},
|
|
326
|
+
async dispose() {
|
|
327
|
+
closed = true;
|
|
328
|
+
await Promise.all(
|
|
329
|
+
[...queue, ...active].map(
|
|
330
|
+
(job) => finish(job, failure("Analyzer is closed", "ERR_ANALYZER_CLOSED"))
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const shared = createAnalyzer2();
|
|
337
|
+
return {
|
|
338
|
+
analyzeFile: shared.analyzeFile,
|
|
339
|
+
analyzeBuffer: shared.analyzeBuffer,
|
|
340
|
+
createAnalyzer: createAnalyzer2
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/v2/node/index.js
|
|
345
|
+
var { analyzeFile, analyzeBuffer, createAnalyzer } = createAPI(
|
|
346
|
+
new URL("./analysis-worker.js", import.meta.url)
|
|
347
|
+
);
|
|
348
|
+
export {
|
|
349
|
+
analyzeBuffer,
|
|
350
|
+
analyzeFile,
|
|
351
|
+
createAnalyzer
|
|
352
|
+
};
|