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

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
@@ -22,6 +22,9 @@ active sessions.
22
22
  - **CAD-style navigation** — left-drag rotates (true trackball, no gimbal
23
23
  limits), right / middle / Shift-drag pans, wheel zooms (perspective and
24
24
  orthographic), double-click resets.
25
+ - **Multi-plate switcher** — 3MF projects carrying a Bambu plate table get
26
+ an "全部 | 盘 N…" toolbar row; each plate renders alone, re-centered, with
27
+ its own dimension labels.
25
28
  - **View presets** — front/back/left/right/top/bottom use an orthographic
26
29
  camera and show labelled bounding-box dimensions (mm); iso is the classic
27
30
  perspective view. Keyboard: `1-7` presets, `R` reset (when the canvas is
package/README.zh-CN.md CHANGED
@@ -11,6 +11,7 @@ DeepSeek Harness Web GUI 的本地交互式 STL / 3MF 预览插件。模型可
11
11
  - **侧边栏原生文档预览**:在「文件」面板点击 `.stl` / `.3mf`,右侧文档标签页像打开 Markdown / PDF / 图片一样打开 3D 视图(通过公开的 `documentPreviews` + `sidebar.right.tab.document` 扩展点接入)
12
12
  - **会话实时抽屉**:当前会话引用了模型时,会话标题栏出现 `3D 预览 (N)` 按钮,打开带搜索列表的预览抽屉;文件在磁盘上变化后约 1.5 秒自动重新解析
13
13
  - **CAD 式导航**:左键拖动旋转(真轨迹球,无万向锁)、右键 / 中键 / Shift 拖动平移、滚轮缩放(透视与正交通用)、双击复位
14
+ - **多盘切换**:带 Bambu 盘表的 3MF 工程会出现「全部 | 盘 N…」工具行,逐盘渲染、自动居中、尺寸标签随盘切换
14
15
  - **视角预设**:前/后/左/右/顶/底使用正交相机并显示包围盒三向尺寸标签(毫米);等轴测为经典透视。画布聚焦时快捷键 `1-7` 切视角、`R` 复位
15
16
  - **模型颜色**:七个预置色板(含白色)+ 原生取色器;STL 单色渲染,3MF 的部件颜色(`basematerials`)按顶点多色显示
16
17
  - **打印检查**(当前由 `PRINT_INSIGHTS_ENABLED` 开关关闭):打印机体积适配(默认 X2D)、封闭网格体积、耗材重量估算
@@ -0,0 +1,33 @@
1
+ # 3D Model Viewer · STL / 3MF
2
+
3
+ View 3D-printing models right inside the DeepSeek Harness Web GUI: click a `.stl` / `.3mf` in the Files panel and a 3D view opens as a document tab, exactly like Markdown; when a session references models, a live preview drawer tracks them and re-parses about 1.5s after each on-disk change — no external tools, and model files never leave your machine.
4
+
5
+ ![3MF document preview](https://github.com/addozhang/dsh-3d-model-viewer/raw/main/docs/screenshot-doctab.webp)
6
+
7
+ [![GitHub stars](https://img.shields.io/github/stars/addozhang/dsh-3d-model-viewer?style=social)](https://github.com/addozhang/dsh-3d-model-viewer)
8
+
9
+ **If it works for you, please [star the repo](https://github.com/addozhang/dsh-3d-model-viewer)** — that directly decides how fast this plugin evolves.
10
+
11
+ ## Highlights
12
+
13
+ - **Native sidebar preview** — click to open in the Files panel; the same document-tab experience as Markdown / PDF / images
14
+ - **Live session drawer** — a `3D Preview (N)` button on the session header opens a searchable drawer; on-disk changes re-parse in ~1.5s
15
+ - **CAD navigation** — true trackball rotation (no gimbal lock), pan, zoom, double-click reset; `1-7` view presets, `R` to reset
16
+ - **Orthographic dimension labels** — the six orthographic views show bounding-box XYZ dimensions in mm; isometric stays classic perspective
17
+ - **3MF plates & part colors** — Bambu plate-table projects get an `All | Plate N…` toolbar; `basematerials` part colors render per-vertex
18
+ - **Fully local** — all parsing happens in the browser, model files never leave the machine; plus one-click PNG snapshot export
19
+
20
+ ## Quick start
21
+
22
+ 1. Install: `dsh plugin --profile <profile> add @addozhang/dsh-3d-model-viewer`
23
+ 2. Restart that profile and click any `.stl` / `.3mf` in the Files panel
24
+
25
+ ## Links
26
+
27
+ - Source & docs: <https://github.com/addozhang/dsh-3d-model-viewer>
28
+ - Issues: <https://github.com/addozhang/dsh-3d-model-viewer/issues>
29
+ - npm package: <https://www.npmjs.com/package/@addozhang/dsh-3d-model-viewer>
30
+
31
+ ---
32
+
33
+ Finding it useful? [Leave a Star](https://github.com/addozhang/dsh-3d-model-viewer/stargazers) so I know it helped 🙌
@@ -0,0 +1,33 @@
1
+ # 3D 模型查看器 · STL / 3MF
2
+
3
+ 在 DeepSeek Harness 的 Web GUI 里直接看 3D 打印模型:文件面板点开 `.stl` / `.3mf`,右侧文档标签页像打开 Markdown 一样打开 3D 视图;会话引用了模型时还有实时预览抽屉,文件一变约 1.5 秒自动刷新——全程无需外部工具,模型文件不出本机。
4
+
5
+ ![3MF 文档预览](https://github.com/addozhang/dsh-3d-model-viewer/raw/main/docs/screenshot-doctab.webp)
6
+
7
+ [![GitHub stars](https://img.shields.io/github/stars/addozhang/dsh-3d-model-viewer?style=social)](https://github.com/addozhang/dsh-3d-model-viewer)
8
+
9
+ **好用的话,欢迎去 [GitHub 仓库](https://github.com/addozhang/dsh-3d-model-viewer) 点个 ⭐ Star**——这直接决定这个插件的迭代速度。
10
+
11
+ ## 亮点
12
+
13
+ - **侧边栏原生预览** — 文件面板点击即开,和 Markdown / PDF / 图片同一套文档标签体验
14
+ - **会话实时抽屉** — 会话标题栏 `3D 预览 (N)` 按钮打开带搜索的抽屉;磁盘文件变化约 1.5s 自动重新解析
15
+ - **CAD 式导航** — 真轨迹球旋转(无万向锁)、平移、缩放、双击复位;`1-7` 切视角、`R` 复位
16
+ - **正交尺寸标签** — 六个正交视角显示包围盒三向尺寸(毫米),等轴测为经典透视
17
+ - **3MF 多盘与部件颜色** — Bambu 盘表工程「全部 | 盘 N…」逐盘渲染;`basematerials` 部件颜色按顶点多色显示
18
+ - **本地解析** — 浏览器内完成全部解析,模型文件不会离开本机;附截图导出 PNG
19
+
20
+ ## 快速开始
21
+
22
+ 1. 安装:`dsh plugin --profile <profile> add @addozhang/dsh-3d-model-viewer`
23
+ 2. 重启对应 profile,文件面板点击任意 `.stl` / `.3mf` 即可查看
24
+
25
+ ## 链接
26
+
27
+ - 源码与文档:<https://github.com/addozhang/dsh-3d-model-viewer>
28
+ - 问题反馈:<https://github.com/addozhang/dsh-3d-model-viewer/issues>
29
+ - npm 包:<https://www.npmjs.com/package/@addozhang/dsh-3d-model-viewer>
30
+
31
+ ---
32
+
33
+ 喜欢这个插件?[点个 Star](https://github.com/addozhang/dsh-3d-model-viewer/stargazers) 让我知道它帮到了你 🙌
package/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M18 4.5 31.5 12 18 19.5 4.5 12z" fill="#41E1AC"/>
3
+ <path d="M4.5 12 18 19.5v13L4.5 25z" fill="#5865F2"/>
4
+ <path d="M31.5 12 18 19.5v13l13.5-7.5z" fill="#658EFF" opacity="0.9"/>
5
+ </svg>
package/lib/client.js CHANGED
@@ -64,7 +64,7 @@
64
64
  .d3v-views{display:flex;gap:3px;background:rgba(129,138,150,.18);border-radius:8px;padding:4px;flex-wrap:wrap;max-width:calc(100% - 20px);justify-content:flex-end}
65
65
  @media(prefers-color-scheme:dark){.d3v-views{background:rgba(10,12,17,.65)}}
66
66
  .d3v-view-btn{appearance:none;border:0;background:transparent;color:#3f4854;border-radius:6px;padding:4px 8px;cursor:pointer;font:inherit;font-size:11px;line-height:1.2;white-space:nowrap}
67
- .d3v-view-btn:hover{background:rgba(96,165,250,.22);color:#173a63}
67
+ .d3v-view-btn:hover{background:rgba(96,165,250,.22);color:#173a63}.d3v-view-btn.active{background:rgba(96,165,250,.4);color:#0f2a4d;font-weight:600}
68
68
  @media(prefers-color-scheme:dark){.d3v-view-btn{color:#c8d0dc}.d3v-view-btn:hover{color:#e8eef7}}
69
69
  .d3v-colors{display:flex;gap:5px;align-items:center;background:rgba(129,138,150,.18);border-radius:8px;padding:5px}
70
70
  @media(prefers-color-scheme:dark){.d3v-colors{background:rgba(10,12,17,.65)}}
@@ -272,6 +272,78 @@
272
272
  ];
273
273
  }
274
274
  var P_NS = "http://schemas.microsoft.com/3dmanufacturing/production/2015/06";
275
+ function buildPlates(items, plateOfObject, plateMeta, data) {
276
+ if (!plateOfObject || plateOfObject.size === 0) return null;
277
+ const byPlate = /* @__PURE__ */ new Map();
278
+ for (const it of items) {
279
+ const plate = plateOfObject.get(String(it.objectid));
280
+ if (plate === void 0) continue;
281
+ if (!byPlate.has(plate)) byPlate.set(plate, []);
282
+ byPlate.get(plate).push(it);
283
+ }
284
+ if (byPlate.size < 2) return null;
285
+ const metaById = new Map((plateMeta || []).map((m) => [String(m.id), m]));
286
+ const ids = [...byPlate.keys()].sort((a, b) => Number(a) - Number(b));
287
+ const plates = [];
288
+ for (const id of ids) {
289
+ const ranges = byPlate.get(id).map((it) => ({ startTri: it.startTri, triCount: it.triCount }));
290
+ let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity], triangles = 0;
291
+ for (const r of ranges) {
292
+ triangles += r.triCount;
293
+ for (let i = r.startTri * 18; i < (r.startTri + r.triCount) * 18; i += 6) {
294
+ for (let j = 0; j < 3; j++) {
295
+ const v = data[i + j];
296
+ if (v < lo[j]) lo[j] = v;
297
+ if (v > hi[j]) hi[j] = v;
298
+ }
299
+ }
300
+ }
301
+ if (!Number.isFinite(lo[0])) continue;
302
+ const size = lo.map((x, i) => hi[i] - x);
303
+ plates.push({
304
+ id,
305
+ label: (metaById.get(String(id)) || {}).label || `\u76D8 ${id}`,
306
+ ranges,
307
+ triangles,
308
+ lo,
309
+ hi,
310
+ size,
311
+ center: lo.map((x, i) => (x + hi[i]) / 2),
312
+ radius: Math.hypot(...size) / 2 || 1
313
+ });
314
+ }
315
+ return plates.length > 1 ? plates : null;
316
+ }
317
+ function readPlateTable(files) {
318
+ const entry = files.find((f) => f.name.replace(/^\//, "") === "Metadata/model_settings.config");
319
+ if (!entry) return null;
320
+ try {
321
+ const doc = new DOMParser().parseFromString(new TextDecoder().decode(entry.data), "application/xml");
322
+ if (doc.querySelector("parsererror")) return null;
323
+ const plateOfObject = /* @__PURE__ */ new Map();
324
+ const plateMeta = [];
325
+ for (const plate of doc.getElementsByTagName("plate")) {
326
+ let id = null, label = "";
327
+ for (const md of plate.getElementsByTagName("metadata")) {
328
+ if (md.getAttribute("key") === "plater_id") id = md.getAttribute("value");
329
+ if (md.getAttribute("key") === "plater_name" && md.getAttribute("value")) label = md.getAttribute("value");
330
+ }
331
+ if (id === null) continue;
332
+ plateMeta.push({ id, label });
333
+ for (const inst of plate.getElementsByTagName("model_instance")) {
334
+ for (const md of inst.getElementsByTagName("metadata")) {
335
+ if (md.getAttribute("key") === "object_id") {
336
+ const objectId = md.getAttribute("value");
337
+ if (!plateOfObject.has(objectId)) plateOfObject.set(objectId, id);
338
+ }
339
+ }
340
+ }
341
+ }
342
+ return plateOfObject.size ? { plateOfObject, plateMeta } : null;
343
+ } catch {
344
+ return null;
345
+ }
346
+ }
275
347
  async function parse3mf(buf) {
276
348
  const files = await unzip(buf);
277
349
  const models = files.filter((f) => /\.model$/i.test(f.name));
@@ -353,17 +425,26 @@
353
425
  }
354
426
  visiting.delete(key);
355
427
  };
356
- const items = [...root.doc.getElementsByTagNameNS("*", "item")];
357
- if (items.length) {
358
- for (const item of items) {
428
+ const buildItems = [...root.doc.getElementsByTagNameNS("*", "item")];
429
+ const itemRanges = [];
430
+ if (buildItems.length) {
431
+ for (const item of buildItems) {
432
+ const start = triangles;
359
433
  emitObject(root, item.getAttribute("objectid"), mat4FromTransform(item.getAttribute("transform")), /* @__PURE__ */ new Set());
434
+ if (triangles > start) itemRanges.push({ objectid: item.getAttribute("objectid"), startTri: start, triCount: triangles - start });
360
435
  }
361
436
  } else {
362
437
  for (const id of root.objects.keys()) emitObject(root, id, MAT4_IDENTITY, /* @__PURE__ */ new Set());
363
438
  }
364
439
  if (!triangles) throw Error("3MF \u4E2D\u6CA1\u6709\u53EF\u663E\u793A\u7684\u4E09\u89D2\u7F51\u683C");
365
- const mesh = finishMesh(new Float32Array(vals), triangles);
440
+ const data = new Float32Array(vals);
441
+ const mesh = finishMesh(data, triangles);
366
442
  if (sawColor) mesh.colors = new Float32Array(cols);
443
+ const plateTable = readPlateTable(files);
444
+ if (plateTable) {
445
+ const plates = buildPlates(itemRanges, plateTable.plateOfObject, plateTable.plateMeta, data);
446
+ if (plates) mesh.plates = plates;
447
+ }
367
448
  return mesh;
368
449
  }
369
450
 
@@ -427,10 +508,28 @@
427
508
  const cw = mvp[3] * x + mvp[7] * y + mvp[11] * z + mvp[15] || 1;
428
509
  return [(cx / cw * 0.5 + 0.5) * w, (0.5 - cy / cw * 0.5) * hh];
429
510
  }
430
- function ViewPresetBar({ onPick, color, onPickColor }) {
511
+ function ViewPresetBar({ onPick, color, onPickColor, plates, plate, onPickPlate }) {
431
512
  return h(
432
513
  "div",
433
514
  { className: "d3v-viewtools", "data-dsh-3d-views": "" },
515
+ plates && plates.length > 1 && h(
516
+ "div",
517
+ { className: "d3v-views", title: "\u5207\u6362\u6253\u5370\u76D8" },
518
+ h("button", {
519
+ type: "button",
520
+ className: "d3v-view-btn" + (plate < 0 ? " active" : ""),
521
+ onPointerDown: (e) => e.stopPropagation(),
522
+ onClick: () => onPickPlate(-1)
523
+ }, "\u5168\u90E8"),
524
+ plates.map((p, i) => h("button", {
525
+ key: p.id,
526
+ type: "button",
527
+ className: "d3v-view-btn" + (plate === i ? " active" : ""),
528
+ title: `${p.label} \xB7 ${p.triangles.toLocaleString()} \u25B3`,
529
+ onPointerDown: (e) => e.stopPropagation(),
530
+ onClick: () => onPickPlate(i)
531
+ }, p.label))
532
+ ),
434
533
  h(
435
534
  "div",
436
535
  { className: "d3v-views" },
@@ -484,19 +583,27 @@
484
583
  ["y", [hi[0] + margin, (lo[1] + hi[1]) / 2, lo[2]]],
485
584
  ["z", [hi[0] + margin, lo[1], (lo[2] + hi[2]) / 2]]
486
585
  ];
487
- function Viewer({ mesh, resetToken, view, color }) {
586
+ function Viewer({ mesh, resetToken, view, color, plate }) {
488
587
  const ref = React.useRef(null);
489
588
  const orient = React.useRef({ rot: DEFAULT_ROT(), ortho: false });
490
589
  const pan = React.useRef([0, 0]);
491
590
  const redraw = React.useRef(() => {
492
591
  });
493
592
  const colorRef = React.useRef(null);
593
+ const plateRef = React.useRef(-1);
494
594
  React.useEffect(() => {
495
595
  if (!view) return;
496
596
  orient.current = { rot: buildRot(view.yaw, view.pitch), ortho: !!view.ortho };
497
597
  pan.current = [0, 0];
498
598
  redraw.current();
499
599
  }, [view]);
600
+ React.useEffect(() => {
601
+ const idx = Number.isInteger(plate) && mesh.plates && plate >= 0 && plate < mesh.plates.length ? plate : -1;
602
+ if (plateRef.current === idx) return;
603
+ plateRef.current = idx;
604
+ pan.current = [0, 0];
605
+ redraw.current();
606
+ }, [plate, mesh]);
500
607
  React.useEffect(() => {
501
608
  if (!color) return;
502
609
  colorRef.current = color;
@@ -573,12 +680,13 @@
573
680
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
574
681
  const aspect = w / hh;
575
682
  const proj = o.ortho ? orthographic(aspect, orthoHalfSize(zoom)) : perspective(aspect);
576
- const scale = 1 / mesh.radius;
683
+ const active = plateRef.current >= 0 && mesh.plates && mesh.plates[plateRef.current] || mesh;
684
+ const scale = 1 / active.radius;
577
685
  const model = new Float32Array(rot);
578
686
  for (const idx of [0, 1, 2, 4, 5, 6, 8, 9, 10]) model[idx] *= scale;
579
- model[12] = -(model[0] * mesh.center[0] + model[4] * mesh.center[1] + model[8] * mesh.center[2]) + pan.current[0];
580
- model[13] = -(model[1] * mesh.center[0] + model[5] * mesh.center[1] + model[9] * mesh.center[2]) + pan.current[1];
581
- model[14] = -(model[2] * mesh.center[0] + model[6] * mesh.center[1] + model[10] * mesh.center[2]) - zoom;
687
+ model[12] = -(model[0] * active.center[0] + model[4] * active.center[1] + model[8] * active.center[2]) + pan.current[0];
688
+ model[13] = -(model[1] * active.center[0] + model[5] * active.center[1] + model[9] * active.center[2]) + pan.current[1];
689
+ model[14] = -(model[2] * active.center[0] + model[6] * active.center[1] + model[10] * active.center[2]) - zoom;
582
690
  lastModel = model;
583
691
  gl.useProgram(program);
584
692
  const col = colorRef.current || DEFAULT_COLOR;
@@ -587,15 +695,16 @@
587
695
  gl.uniformMatrix4fv(gl.getUniformLocation(program, "model"), false, model);
588
696
  const mvp = mul(proj, model);
589
697
  gl.uniformMatrix4fv(gl.getUniformLocation(program, "mvp"), false, mvp);
590
- gl.drawArrays(gl.TRIANGLES, 0, mesh.data.length / 6);
698
+ const ranges = active.ranges || [{ startTri: 0, triCount: mesh.data.length / 18 }];
699
+ for (const r of ranges) gl.drawArrays(gl.TRIANGLES, r.startTri * 3, r.triCount * 3);
591
700
  if (o.ortho) {
592
- const m = mesh.radius * 0.18;
701
+ const m = active.radius * 0.18;
593
702
  const inPlane = {
594
703
  x: Math.abs(rot[2]) < 0.9,
595
704
  y: Math.abs(rot[6]) < 0.9,
596
705
  z: Math.abs(rot[10]) < 0.9
597
706
  };
598
- const anchors = DIM_ANCHORS(mesh.lo, mesh.hi, m);
707
+ const anchors = DIM_ANCHORS(active.lo, active.hi, m);
599
708
  for (const [axis, point] of anchors) {
600
709
  const el = labelEls[axis];
601
710
  if (!inPlane[axis]) {
@@ -606,7 +715,7 @@
606
715
  el.style.display = "";
607
716
  el.style.left = sx + "px";
608
717
  el.style.top = sy + "px";
609
- el.textContent = mesh.size[axis === "x" ? 0 : axis === "y" ? 1 : 2].toFixed(2);
718
+ el.textContent = active.size[axis === "x" ? 0 : axis === "y" ? 1 : 2].toFixed(2);
610
719
  }
611
720
  } else {
612
721
  for (const el of Object.values(labelEls)) el.style.display = "none";
@@ -791,6 +900,7 @@
791
900
  const [state, setState] = React.useState({ status: bytes ? "loading" : "waiting" });
792
901
  const [view, setView] = React.useState();
793
902
  const [color, setColor] = React.useState(DEFAULT_COLOR);
903
+ const [plate, setPlate] = React.useState(-1);
794
904
  React.useEffect(() => {
795
905
  if (!bytes) return;
796
906
  let live = true;
@@ -811,8 +921,8 @@
811
921
  return h(
812
922
  "div",
813
923
  { className: "d3v-doc", "data-dsh-3d-docbody": "" },
814
- state.status === "ready" && h(Viewer, { mesh: state.mesh, resetToken: 0, view, color }),
815
- state.status === "ready" && h(ViewPresetBar, { onPick: (yaw, pitch, ortho) => setView({ yaw, pitch, ortho }), color, onPickColor: setColor }),
924
+ state.status === "ready" && h(Viewer, { mesh: state.mesh, resetToken: 0, view, color, plate }),
925
+ state.status === "ready" && h(ViewPresetBar, { onPick: (yaw, pitch, ortho) => setView({ yaw, pitch, ortho }), color, onPickColor: setColor, plates: state.mesh.plates, plate, onPickPlate: setPlate }),
816
926
  state.status === "loading" && h("div", { className: "d3v-doc-status" }, tt("loading")),
817
927
  state.status === "waiting" && h("div", { className: "d3v-doc-status" }, tt("waiting")),
818
928
  state.status === "error" && h("div", { className: "d3v-error" }, tt("failed", { message: state.message })),
@@ -844,6 +954,7 @@
844
954
  const s = useStore(sessionStore);
845
955
  const [reset, setReset] = React.useState(0), [query, setQuery] = React.useState("");
846
956
  const [view, setView] = React.useState(), [color, setColor] = React.useState(DEFAULT_COLOR);
957
+ const [plate, setPlate] = React.useState(-1);
847
958
  const selected = s.files.find((x) => x.path === s.selected) || s.files[0];
848
959
  const shown = s.files.filter((f) => f.path.toLowerCase().includes(query.trim().toLowerCase()));
849
960
  const load = React.useCallback(async (path) => {
@@ -927,8 +1038,8 @@
927
1038
  h(
928
1039
  "div",
929
1040
  { className: "d3v-stage" },
930
- s.mesh && h(Viewer, { mesh: s.mesh, resetToken: reset, view, color }),
931
- s.mesh && h(ViewPresetBar, { onPick: (yaw, pitch, ortho) => setView({ yaw, pitch, ortho }), color, onPickColor: setColor }),
1041
+ s.mesh && h(Viewer, { mesh: s.mesh, resetToken: reset, view, color, plate }),
1042
+ s.mesh && h(ViewPresetBar, { onPick: (yaw, pitch, ortho) => setView({ yaw, pitch, ortho }), color, onPickColor: setColor, plates: s.mesh.plates, plate, onPickPlate: setPlate }),
932
1043
  s.loading && h("div", { className: "d3v-loading" }, "\u6B63\u5728\u66F4\u65B0\u6A21\u578B\u2026"),
933
1044
  s.error && h("div", { className: "d3v-error" }, s.error),
934
1045
  s.mesh && h("div", { className: "d3v-help" }, "\u5DE6\u952E\u65CB\u8F6C \xB7 \u53F3\u952E/Shift \u5E73\u79FB \xB7 \u6EDA\u8F6E\u7F29\u653E \xB7 \u53CC\u51FB\u590D\u4F4D \xB7 \u6587\u4EF6\u53D8\u5316\u81EA\u52A8\u66F4\u65B0")
package/locale/en.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "3D Model Viewer",
4
+ "description": "Interactive STL/3MF preview for the DeepSeek Harness Web GUI: sidebar document tabs, a live session drawer, CAD navigation, orthographic dimension labels, 3MF part colors, and multi-plate switching — all parsed locally.",
5
+ "detail": "docs/plugin-detail.en.md"
6
+ }
7
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "3D 模型查看器",
4
+ "description": "STL/3MF 交互式预览:侧边栏文档标签直接打开模型,会话实时抽屉、CAD 式导航、正交尺寸标签、3MF 部件颜色与多盘切换——解析全程本地完成。",
5
+ "detail": "docs/plugin-detail.zh.md"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@addozhang/dsh-3d-model-viewer",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
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
+ "icon": "./icon.svg",
6
7
  "engines": {
7
8
  "node": ">=20"
8
9
  },
@@ -22,6 +23,8 @@
22
23
  ".": "./lib/index.js",
23
24
  "./client": "./lib/client.js",
24
25
  "./cordis.patch.yml": "./cordis.patch.yml",
26
+ "./locale/*.json": "./locale/*.json",
27
+ "./icon": "./icon.svg",
25
28
  "./package.json": "./package.json"
26
29
  },
27
30
  "scripts": {
@@ -52,6 +55,10 @@
52
55
  "files": [
53
56
  "lib/index.js",
54
57
  "lib/client.js",
58
+ "locale/*.json",
59
+ "docs/plugin-detail.zh.md",
60
+ "docs/plugin-detail.en.md",
61
+ "icon.svg",
55
62
  "README.md",
56
63
  "README.zh-CN.md",
57
64
  "LICENSE",