@addozhang/dsh-3d-model-viewer 0.3.1 → 0.3.2

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
@@ -74,9 +74,9 @@ cookie for its private server instance. Env knobs: `E2E_WORKSPACE`,
74
74
 
75
75
  ## Current limitations
76
76
 
77
- - 3MF support covers mesh geometry + `basematerials` colors; build-item
78
- transforms, per-component overrides, and slicer-specific metadata are not
79
- reconstructed.
77
+ - 3MF support covers mesh geometry, build-item and component transforms
78
+ (multi-plate projects render every plate at its true position), and
79
+ `basematerials` part colors; slicer-specific metadata is ignored.
80
80
  - Files are capped at 250 MB / 5 M triangles; meshes above 400 k triangles
81
81
  fall back to bounding-box-only picking (moot with button-based drag modes).
82
82
  - This is a geometry previewer: no slicing, no printability analysis.
package/README.zh-CN.md CHANGED
@@ -46,7 +46,7 @@ e2e 脚本依赖本机 python3 + playwright + Chrome,从 `~/.dsh/.credentials.
46
46
 
47
47
  ## 当前限制
48
48
 
49
- - 3MF 支持网格几何 + `basematerials` 颜色;build-item 变换、组件级覆盖与切片器私有元数据不还原
49
+ - 3MF 支持网格几何、build-item / component 变换(多盘工程按真实盘位渲染全部盘)与 `basematerials` 部件颜色;切片器私有元数据忽略
50
50
  - 单文件限制 250 MB / 500 万三角面;超过 40 万三角面的网格拾取只用包围盒(按键式拖动模式下影响甚微)
51
51
  - 这是几何预览器:不做切片,不做可制造性分析
52
52
 
package/lib/client.js CHANGED
@@ -247,12 +247,36 @@
247
247
  if (hex.length === 3) hex = [...hex].map((c) => c + c).join("");
248
248
  return [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
249
249
  }
250
+ var MAT4_IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
251
+ function mat4FromTransform(text) {
252
+ if (typeof text !== "string") return MAT4_IDENTITY;
253
+ const parts = text.trim().split(/\s+/);
254
+ if (parts.length === 0 || parts.length === 1 && parts[0] === "") return MAT4_IDENTITY;
255
+ if (parts.length !== 12) throw Error(`3MF \u53D8\u6362\u9700\u8981 12 \u4E2A\u6570\u5B57\uFF0C\u5F97\u5230 ${parts.length}`);
256
+ const m = parts.map(Number);
257
+ for (const v of m) if (!Number.isFinite(v)) throw Error("3MF \u53D8\u6362\u5305\u542B\u975E\u6570\u5B57");
258
+ return [m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1];
259
+ }
260
+ function mat4Mul(child, parent) {
261
+ const out = new Array(16);
262
+ for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) {
263
+ out[r * 4 + c] = child[r * 4] * parent[c] + child[r * 4 + 1] * parent[4 + c] + child[r * 4 + 2] * parent[8 + c] + child[r * 4 + 3] * parent[12 + c];
264
+ }
265
+ return out;
266
+ }
267
+ function mat4Apply(m, x, y, z) {
268
+ return [
269
+ x * m[0] + y * m[4] + z * m[8] + m[12],
270
+ x * m[1] + y * m[5] + z * m[9] + m[13],
271
+ x * m[2] + y * m[6] + z * m[10] + m[14]
272
+ ];
273
+ }
274
+ var P_NS = "http://schemas.microsoft.com/3dmanufacturing/production/2015/06";
250
275
  async function parse3mf(buf) {
251
276
  const files = await unzip(buf);
252
277
  const models = files.filter((f) => /\.model$/i.test(f.name));
253
278
  if (!models.length) throw Error("3MF \u4E2D\u6CA1\u6709 .model \u7F51\u683C");
254
- const vals = [], cols = [];
255
- let triangles = 0, sawColor = false;
279
+ const registry = /* @__PURE__ */ new Map();
256
280
  for (const f of models) {
257
281
  const doc = new DOMParser().parseFromString(new TextDecoder().decode(f.data), "application/xml");
258
282
  if (doc.querySelector("parsererror")) continue;
@@ -261,32 +285,81 @@
261
285
  for (const bm of doc.getElementsByTagNameNS("*", "basematerials")) {
262
286
  materialMap[bm.getAttribute("id")] = [...bm.getElementsByTagNameNS("*", "base")].map((base) => parseHexColor(base.getAttribute("color")));
263
287
  }
264
- const emit = (mesh2, defaultColor, materialColors) => {
265
- const verts = [...mesh2.getElementsByTagNameNS("*", "vertex")].map((v) => [+v.getAttribute("x") * factor, +v.getAttribute("y") * factor, +v.getAttribute("z") * factor]);
266
- for (const t of mesh2.getElementsByTagNameNS("*", "triangle")) {
267
- const ids = [+t.getAttribute("v1"), +t.getAttribute("v2"), +t.getAttribute("v3")];
268
- const a = verts[ids[0]], b = verts[ids[1]], c = verts[ids[2]];
269
- if (!a || !b || !c) continue;
270
- const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2], vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
271
- const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx, l = Math.hypot(nx, ny, nz) || 1;
272
- for (const v of [a, b, c]) vals.push(v[0], v[1], v[2], nx / l, ny / l, nz / l);
273
- let color = defaultColor;
274
- const matid = t.getAttribute("matid");
275
- if (materialColors && matid !== null && materialColors[+matid - 1]) color = materialColors[+matid - 1];
276
- if (color) sawColor = true;
277
- if (color) cols.push(color[0], color[1], color[2], color[0], color[1], color[2], color[0], color[1], color[2]);
278
- else cols.push(0, 0, 0, 0, 0, 0, 0, 0, 0);
279
- triangles++;
280
- if (triangles > 5e6) throw Error("\u6A21\u578B\u8D85\u8FC7 500 \u4E07\u4E09\u89D2\u9762\u9650\u5236");
288
+ const objects = /* @__PURE__ */ new Map();
289
+ for (const obj of doc.getElementsByTagNameNS("*", "object")) {
290
+ if (obj.getAttribute("id") !== null) objects.set(obj.getAttribute("id"), obj);
291
+ }
292
+ registry.set(f.name.replace(/^\//, ""), { doc, factor, materialMap, objects, name: f.name.replace(/^\//, "") });
293
+ }
294
+ let rootName = "3D/3dmodel.model";
295
+ const rels = files.find((f) => f.name.replace(/^\//, "") === "_rels/.rels");
296
+ if (rels) {
297
+ try {
298
+ const relDoc = new DOMParser().parseFromString(new TextDecoder().decode(rels.data), "application/xml");
299
+ for (const rel of relDoc.getElementsByTagNameNS("*", "Relationship")) {
300
+ if ((rel.getAttribute("Type") || "").endsWith("/3dmodel")) {
301
+ rootName = (rel.getAttribute("Target") || "").replace(/^\//, "");
302
+ break;
303
+ }
281
304
  }
282
- };
283
- const objects = [...doc.getElementsByTagNameNS("*", "object")];
284
- for (const obj of objects) {
285
- const materialColors = materialMap[obj.getAttribute("pid")] || null;
286
- const objColor = materialColors ? materialColors[+(obj.getAttribute("pindex") || 0)] || null : null;
287
- for (const mesh2 of obj.getElementsByTagNameNS("*", "mesh")) emit(mesh2, objColor, materialColors);
305
+ } catch {
306
+ }
307
+ }
308
+ const root = registry.get(rootName) || registry.get("3D/3dmodel.model");
309
+ if (!root) throw Error("3MF \u4E2D\u627E\u4E0D\u5230\u6839\u6A21\u578B");
310
+ const vals = [], cols = [];
311
+ let triangles = 0, sawColor = false;
312
+ const emitMesh = (mesh2, objEl, M, fileRec) => {
313
+ const materialColors = objEl ? fileRec.materialMap[objEl.getAttribute("pid")] || null : null;
314
+ const objColor = materialColors ? materialColors[+(objEl.getAttribute("pindex") || 0)] || null : null;
315
+ const verts = [...mesh2.getElementsByTagNameNS("*", "vertex")].map((v) => [+v.getAttribute("x") * fileRec.factor, +v.getAttribute("y") * fileRec.factor, +v.getAttribute("z") * fileRec.factor]);
316
+ for (const t of mesh2.getElementsByTagNameNS("*", "triangle")) {
317
+ const ids = [+t.getAttribute("v1"), +t.getAttribute("v2"), +t.getAttribute("v3")];
318
+ const raw = [verts[ids[0]], verts[ids[1]], verts[ids[2]]];
319
+ if (!raw[0] || !raw[1] || !raw[2]) continue;
320
+ const pts = raw.map((p) => M === MAT4_IDENTITY ? p : mat4Apply(M, p[0], p[1], p[2]));
321
+ const [a, b, c] = pts;
322
+ const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2], vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
323
+ const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx, l = Math.hypot(nx, ny, nz) || 1;
324
+ for (const p of pts) vals.push(p[0], p[1], p[2], nx / l, ny / l, nz / l);
325
+ let color = objColor;
326
+ const matid = t.getAttribute("matid");
327
+ if (materialColors && matid !== null && materialColors[+matid - 1]) color = materialColors[+matid - 1];
328
+ if (color) {
329
+ sawColor = true;
330
+ cols.push(color[0], color[1], color[2], color[0], color[1], color[2], color[0], color[1], color[2]);
331
+ } else {
332
+ cols.push(0, 0, 0, 0, 0, 0, 0, 0, 0);
333
+ }
334
+ triangles++;
335
+ if (triangles > 5e6) throw Error("\u6A21\u578B\u8D85\u8FC7 500 \u4E07\u4E09\u89D2\u9762\u9650\u5236");
336
+ }
337
+ };
338
+ const emitObject = (fileRec, objId, M, visiting) => {
339
+ const obj = fileRec.objects.get(objId);
340
+ if (!obj) return;
341
+ const key = fileRec.name + "#" + objId;
342
+ if (visiting.has(key)) return;
343
+ visiting.add(key);
344
+ const mesh2 = obj.getElementsByTagNameNS("*", "mesh")[0] || null;
345
+ if (mesh2) emitMesh(mesh2, obj, M, fileRec);
346
+ for (const comp of obj.getElementsByTagNameNS("*", "component")) {
347
+ const path = comp.getAttributeNS(P_NS, "path");
348
+ const childRec = path ? registry.get(path.replace(/^\//, "")) : fileRec;
349
+ if (!childRec) continue;
350
+ const cm = mat4FromTransform(comp.getAttribute("transform"));
351
+ const combined = M === MAT4_IDENTITY && cm === MAT4_IDENTITY ? MAT4_IDENTITY : mat4Mul(cm, M);
352
+ emitObject(childRec, comp.getAttribute("objectid"), combined, visiting);
353
+ }
354
+ visiting.delete(key);
355
+ };
356
+ const items = [...root.doc.getElementsByTagNameNS("*", "item")];
357
+ if (items.length) {
358
+ for (const item of items) {
359
+ emitObject(root, item.getAttribute("objectid"), mat4FromTransform(item.getAttribute("transform")), /* @__PURE__ */ new Set());
288
360
  }
289
- if (!objects.length) for (const mesh2 of doc.getElementsByTagNameNS("*", "mesh")) emit(mesh2, null, null);
361
+ } else {
362
+ for (const id of root.objects.keys()) emitObject(root, id, MAT4_IDENTITY, /* @__PURE__ */ new Set());
290
363
  }
291
364
  if (!triangles) throw Error("3MF \u4E2D\u6CA1\u6709\u53EF\u663E\u793A\u7684\u4E09\u89D2\u7F51\u683C");
292
365
  const mesh = finishMesh(new Float32Array(vals), triangles);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addozhang/dsh-3d-model-viewer",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Interactive STL/3MF viewer plugin for the DeepSeek Harness Web GUI: sidebar document preview, live session drawer, CAD navigation, orthographic dimension labels, 3MF part colors.",
5
5
  "license": "MIT",
6
6
  "engines": {