@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 +9 -12
- package/README.zh-CN.md +6 -9
- package/cordis.patch.yml +12 -0
- package/lib/client.js +99 -26
- package/package.json +15 -3
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
包自带 bundle patch(`cordis.patch.yml`),插件管理器会自动接入 profile,无需手改 `cordis.patch.yml`。
|
|
28
29
|
|
|
29
|
-
|
|
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
|
|
49
|
+
- 3MF 支持网格几何、build-item / component 变换(多盘工程按真实盘位渲染全部盘)与 `basematerials` 部件颜色;切片器私有元数据忽略
|
|
53
50
|
- 单文件限制 250 MB / 500 万三角面;超过 40 万三角面的网格拾取只用包围盒(按键式拖动模式下影响甚微)
|
|
54
51
|
- 这是几何预览器:不做切片,不做可制造性分析
|
|
55
52
|
|
package/cordis.patch.yml
ADDED
|
@@ -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
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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.
|
|
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": [
|
|
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
|
}
|