@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 +3 -0
- package/README.zh-CN.md +1 -0
- package/docs/plugin-detail.en.md +33 -0
- package/docs/plugin-detail.zh.md +33 -0
- package/icon.svg +5 -0
- package/lib/client.js +130 -19
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +8 -1
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
|
+

|
|
6
|
+
|
|
7
|
+
[](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
|
+

|
|
6
|
+
|
|
7
|
+
[](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
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
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
|
|
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] *
|
|
580
|
-
model[13] = -(model[1] *
|
|
581
|
-
model[14] = -(model[2] *
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addozhang/dsh-3d-model-viewer",
|
|
3
|
-
"version": "0.
|
|
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",
|