@addozhang/dsh-3d-model-viewer 0.3.0 → 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
@@ -7,6 +7,8 @@ Models can be inspected directly in the sidebar document preview — no
7
7
  external tool needed — and the plugin doubles as a live preview drawer for
8
8
  active sessions.
9
9
 
10
+ ![3MF document-tab preview](https://github.com/addozhang/dsh-3d-model-viewer/raw/main/docs/screenshot-doctab.webp)
11
+
10
12
  ## Features
11
13
 
12
14
  - **Native sidebar document preview** — clicking an `.stl` / `.3mf` in the
@@ -40,18 +42,13 @@ Everything is parsed in the browser; model files never leave the machine
40
42
  ## Install
41
43
 
42
44
  ```bash
43
- npm install @addozhang/dsh-3d-model-viewer
44
- # or from source: npm install github:addozhang/dsh-3d-model-viewer
45
+ dsh plugin --profile web add @addozhang/dsh-3d-model-viewer
45
46
  ```
46
47
 
47
- Then add to `$DSH_HOME/profiles/web/package.json` dependencies and insert into
48
- `$DSH_HOME/profiles/web/cordis.patch.yml`:
48
+ The package declares its own bundle patch (`cordis.patch.yml`), so the plugin
49
+ manager wires it into the profile — no manual `cordis.patch.yml` editing.
49
50
 
50
- ```yaml
51
- - insert:
52
- - id: 3d-model-viewer
53
- name: '@addozhang/dsh-3d-model-viewer'
54
- ```
51
+ Latest source instead: `dsh plugin --profile web add github:addozhang/dsh-3d-model-viewer`
55
52
 
56
53
  Refresh the Web GUI; if the client roster did not update, restart
57
54
  `dsh web` and refresh again.
@@ -77,9 +74,9 @@ cookie for its private server instance. Env knobs: `E2E_WORKSPACE`,
77
74
 
78
75
  ## Current limitations
79
76
 
80
- - 3MF support covers mesh geometry + `basematerials` colors; build-item
81
- transforms, per-component overrides, and slicer-specific metadata are not
82
- 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.
83
80
  - Files are capped at 250 MB / 5 M triangles; meshes above 400 k triangles
84
81
  fall back to bounding-box-only picking (moot with button-based drag modes).
85
82
  - This is a geometry previewer: no slicing, no printability analysis.
package/README.zh-CN.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  DeepSeek Harness Web GUI 的本地交互式 STL / 3MF 预览插件。模型可直接在侧边栏文档预览中查看,无需外部工具;同时为活动会话提供实时预览抽屉。
6
6
 
7
+ ![3MF 文档预览](https://github.com/addozhang/dsh-3d-model-viewer/raw/main/docs/screenshot-doctab.webp)
8
+
7
9
  ## 功能
8
10
 
9
11
  - **侧边栏原生文档预览**:在「文件」面板点击 `.stl` / `.3mf`,右侧文档标签页像打开 Markdown / PDF / 图片一样打开 3D 视图(通过公开的 `documentPreviews` + `sidebar.right.tab.document` 扩展点接入)
@@ -20,17 +22,12 @@ DeepSeek Harness Web GUI 的本地交互式 STL / 3MF 预览插件。模型可
20
22
  ## 安装
21
23
 
22
24
  ```bash
23
- npm install @addozhang/dsh-3d-model-viewer
24
- # 或从源码安装:npm install github:addozhang/dsh-3d-model-viewer
25
+ dsh plugin --profile web add @addozhang/dsh-3d-model-viewer
25
26
  ```
26
27
 
27
- 然后把包加入 `$DSH_HOME/profiles/web/package.json` 依赖,并在 `$DSH_HOME/profiles/web/cordis.patch.yml` 中插入:
28
+ 包自带 bundle patch(`cordis.patch.yml`),插件管理器会自动接入 profile,无需手改 `cordis.patch.yml`。
28
29
 
29
- ```yaml
30
- - insert:
31
- - id: 3d-model-viewer
32
- name: '@addozhang/dsh-3d-model-viewer'
33
- ```
30
+ 安装最新源码:`dsh plugin --profile web add github:addozhang/dsh-3d-model-viewer`
34
31
 
35
32
  刷新 Web GUI;若客户端 roster 未更新,重启 `dsh web` 后再刷新。
36
33
 
@@ -49,7 +46,7 @@ e2e 脚本依赖本机 python3 + playwright + Chrome,从 `~/.dsh/.credentials.
49
46
 
50
47
  ## 当前限制
51
48
 
52
- - 3MF 支持网格几何 + `basematerials` 颜色;build-item 变换、组件级覆盖与切片器私有元数据不还原
49
+ - 3MF 支持网格几何、build-item / component 变换(多盘工程按真实盘位渲染全部盘)与 `basematerials` 部件颜色;切片器私有元数据忽略
53
50
  - 单文件限制 250 MB / 500 万三角面;超过 40 万三角面的网格拾取只用包围盒(按键式拖动模式下影响甚微)
54
51
  - 这是几何预览器:不做切片,不做可制造性分析
55
52
 
@@ -0,0 +1,12 @@
1
+ # The dsh-3d-model-viewer bundle patch: mounts the 3D viewer into the Web
2
+ # profile's Host plugin tree. The Host half serves the local
3
+ # /3d-model-viewer/files endpoint for the session drawer.
4
+ #
5
+ # The browser half (sidebar document preview, drawer, viewer) is declared
6
+ # through this package's `dsh.client` manifest, so the modules node scans it
7
+ # into the browser roster automatically and no client row is needed here.
8
+
9
+ - insert:
10
+ - id: 3d-model-viewer
11
+ name: '@addozhang/dsh-3d-model-viewer'
12
+ inject: [webServer]
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.0",
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": {
@@ -20,7 +20,9 @@
20
20
  "main": "lib/index.js",
21
21
  "exports": {
22
22
  ".": "./lib/index.js",
23
- "./client": "./lib/client.js"
23
+ "./client": "./lib/client.js",
24
+ "./cordis.patch.yml": "./cordis.patch.yml",
25
+ "./package.json": "./package.json"
24
26
  },
25
27
  "scripts": {
26
28
  "build": "node build.mjs",
@@ -28,6 +30,9 @@
28
30
  "test:e2e": "python3 scripts/e2e.py"
29
31
  },
30
32
  "dsh": {
33
+ "bundle": {
34
+ "patch": "./cordis.patch.yml"
35
+ },
31
36
  "client": {
32
37
  "inject": [
33
38
  "@deepseek-ai/dsh-client-ui-renderer",
@@ -44,5 +49,12 @@
44
49
  "devDependencies": {
45
50
  "esbuild": "^0.25.0"
46
51
  },
47
- "files": ["lib/index.js", "lib/client.js", "README.md", "README.zh-CN.md", "LICENSE"]
52
+ "files": [
53
+ "lib/index.js",
54
+ "lib/client.js",
55
+ "README.md",
56
+ "README.zh-CN.md",
57
+ "LICENSE",
58
+ "cordis.patch.yml"
59
+ ]
48
60
  }