@addozhang/dsh-3d-model-viewer 0.3.1 → 0.4.0
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 +6 -3
- package/README.zh-CN.md +2 -1
- package/lib/client.js +226 -42
- package/package.json +1 -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
|
|
@@ -74,9 +77,9 @@ cookie for its private server instance. Env knobs: `E2E_WORKSPACE`,
|
|
|
74
77
|
|
|
75
78
|
## Current limitations
|
|
76
79
|
|
|
77
|
-
- 3MF support covers mesh geometry
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
- 3MF support covers mesh geometry, build-item and component transforms
|
|
81
|
+
(multi-plate projects render every plate at its true position), and
|
|
82
|
+
`basematerials` part colors; slicer-specific metadata is ignored.
|
|
80
83
|
- Files are capped at 250 MB / 5 M triangles; meshes above 400 k triangles
|
|
81
84
|
fall back to bounding-box-only picking (moot with button-based drag modes).
|
|
82
85
|
- This is a geometry previewer: no slicing, no printability analysis.
|
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)、封闭网格体积、耗材重量估算
|
|
@@ -46,7 +47,7 @@ e2e 脚本依赖本机 python3 + playwright + Chrome,从 `~/.dsh/.credentials.
|
|
|
46
47
|
|
|
47
48
|
## 当前限制
|
|
48
49
|
|
|
49
|
-
- 3MF
|
|
50
|
+
- 3MF 支持网格几何、build-item / component 变换(多盘工程按真实盘位渲染全部盘)与 `basematerials` 部件颜色;切片器私有元数据忽略
|
|
50
51
|
- 单文件限制 250 MB / 500 万三角面;超过 40 万三角面的网格拾取只用包围盒(按键式拖动模式下影响甚微)
|
|
51
52
|
- 这是几何预览器:不做切片,不做可制造性分析
|
|
52
53
|
|
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)}}
|
|
@@ -247,12 +247,108 @@
|
|
|
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";
|
|
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
|
+
}
|
|
250
347
|
async function parse3mf(buf) {
|
|
251
348
|
const files = await unzip(buf);
|
|
252
349
|
const models = files.filter((f) => /\.model$/i.test(f.name));
|
|
253
350
|
if (!models.length) throw Error("3MF \u4E2D\u6CA1\u6709 .model \u7F51\u683C");
|
|
254
|
-
const
|
|
255
|
-
let triangles = 0, sawColor = false;
|
|
351
|
+
const registry = /* @__PURE__ */ new Map();
|
|
256
352
|
for (const f of models) {
|
|
257
353
|
const doc = new DOMParser().parseFromString(new TextDecoder().decode(f.data), "application/xml");
|
|
258
354
|
if (doc.querySelector("parsererror")) continue;
|
|
@@ -261,36 +357,94 @@
|
|
|
261
357
|
for (const bm of doc.getElementsByTagNameNS("*", "basematerials")) {
|
|
262
358
|
materialMap[bm.getAttribute("id")] = [...bm.getElementsByTagNameNS("*", "base")].map((base) => parseHexColor(base.getAttribute("color")));
|
|
263
359
|
}
|
|
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");
|
|
360
|
+
const objects = /* @__PURE__ */ new Map();
|
|
361
|
+
for (const obj of doc.getElementsByTagNameNS("*", "object")) {
|
|
362
|
+
if (obj.getAttribute("id") !== null) objects.set(obj.getAttribute("id"), obj);
|
|
363
|
+
}
|
|
364
|
+
registry.set(f.name.replace(/^\//, ""), { doc, factor, materialMap, objects, name: f.name.replace(/^\//, "") });
|
|
365
|
+
}
|
|
366
|
+
let rootName = "3D/3dmodel.model";
|
|
367
|
+
const rels = files.find((f) => f.name.replace(/^\//, "") === "_rels/.rels");
|
|
368
|
+
if (rels) {
|
|
369
|
+
try {
|
|
370
|
+
const relDoc = new DOMParser().parseFromString(new TextDecoder().decode(rels.data), "application/xml");
|
|
371
|
+
for (const rel of relDoc.getElementsByTagNameNS("*", "Relationship")) {
|
|
372
|
+
if ((rel.getAttribute("Type") || "").endsWith("/3dmodel")) {
|
|
373
|
+
rootName = (rel.getAttribute("Target") || "").replace(/^\//, "");
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
281
376
|
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const root = registry.get(rootName) || registry.get("3D/3dmodel.model");
|
|
381
|
+
if (!root) throw Error("3MF \u4E2D\u627E\u4E0D\u5230\u6839\u6A21\u578B");
|
|
382
|
+
const vals = [], cols = [];
|
|
383
|
+
let triangles = 0, sawColor = false;
|
|
384
|
+
const emitMesh = (mesh2, objEl, M, fileRec) => {
|
|
385
|
+
const materialColors = objEl ? fileRec.materialMap[objEl.getAttribute("pid")] || null : null;
|
|
386
|
+
const objColor = materialColors ? materialColors[+(objEl.getAttribute("pindex") || 0)] || null : null;
|
|
387
|
+
const verts = [...mesh2.getElementsByTagNameNS("*", "vertex")].map((v) => [+v.getAttribute("x") * fileRec.factor, +v.getAttribute("y") * fileRec.factor, +v.getAttribute("z") * fileRec.factor]);
|
|
388
|
+
for (const t of mesh2.getElementsByTagNameNS("*", "triangle")) {
|
|
389
|
+
const ids = [+t.getAttribute("v1"), +t.getAttribute("v2"), +t.getAttribute("v3")];
|
|
390
|
+
const raw = [verts[ids[0]], verts[ids[1]], verts[ids[2]]];
|
|
391
|
+
if (!raw[0] || !raw[1] || !raw[2]) continue;
|
|
392
|
+
const pts = raw.map((p) => M === MAT4_IDENTITY ? p : mat4Apply(M, p[0], p[1], p[2]));
|
|
393
|
+
const [a, b, c] = pts;
|
|
394
|
+
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];
|
|
395
|
+
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx, l = Math.hypot(nx, ny, nz) || 1;
|
|
396
|
+
for (const p of pts) vals.push(p[0], p[1], p[2], nx / l, ny / l, nz / l);
|
|
397
|
+
let color = objColor;
|
|
398
|
+
const matid = t.getAttribute("matid");
|
|
399
|
+
if (materialColors && matid !== null && materialColors[+matid - 1]) color = materialColors[+matid - 1];
|
|
400
|
+
if (color) {
|
|
401
|
+
sawColor = true;
|
|
402
|
+
cols.push(color[0], color[1], color[2], color[0], color[1], color[2], color[0], color[1], color[2]);
|
|
403
|
+
} else {
|
|
404
|
+
cols.push(0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
405
|
+
}
|
|
406
|
+
triangles++;
|
|
407
|
+
if (triangles > 5e6) throw Error("\u6A21\u578B\u8D85\u8FC7 500 \u4E07\u4E09\u89D2\u9762\u9650\u5236");
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const emitObject = (fileRec, objId, M, visiting) => {
|
|
411
|
+
const obj = fileRec.objects.get(objId);
|
|
412
|
+
if (!obj) return;
|
|
413
|
+
const key = fileRec.name + "#" + objId;
|
|
414
|
+
if (visiting.has(key)) return;
|
|
415
|
+
visiting.add(key);
|
|
416
|
+
const mesh2 = obj.getElementsByTagNameNS("*", "mesh")[0] || null;
|
|
417
|
+
if (mesh2) emitMesh(mesh2, obj, M, fileRec);
|
|
418
|
+
for (const comp of obj.getElementsByTagNameNS("*", "component")) {
|
|
419
|
+
const path = comp.getAttributeNS(P_NS, "path");
|
|
420
|
+
const childRec = path ? registry.get(path.replace(/^\//, "")) : fileRec;
|
|
421
|
+
if (!childRec) continue;
|
|
422
|
+
const cm = mat4FromTransform(comp.getAttribute("transform"));
|
|
423
|
+
const combined = M === MAT4_IDENTITY && cm === MAT4_IDENTITY ? MAT4_IDENTITY : mat4Mul(cm, M);
|
|
424
|
+
emitObject(childRec, comp.getAttribute("objectid"), combined, visiting);
|
|
425
|
+
}
|
|
426
|
+
visiting.delete(key);
|
|
427
|
+
};
|
|
428
|
+
const buildItems = [...root.doc.getElementsByTagNameNS("*", "item")];
|
|
429
|
+
const itemRanges = [];
|
|
430
|
+
if (buildItems.length) {
|
|
431
|
+
for (const item of buildItems) {
|
|
432
|
+
const start = triangles;
|
|
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 });
|
|
288
435
|
}
|
|
289
|
-
|
|
436
|
+
} else {
|
|
437
|
+
for (const id of root.objects.keys()) emitObject(root, id, MAT4_IDENTITY, /* @__PURE__ */ new Set());
|
|
290
438
|
}
|
|
291
439
|
if (!triangles) throw Error("3MF \u4E2D\u6CA1\u6709\u53EF\u663E\u793A\u7684\u4E09\u89D2\u7F51\u683C");
|
|
292
|
-
const
|
|
440
|
+
const data = new Float32Array(vals);
|
|
441
|
+
const mesh = finishMesh(data, triangles);
|
|
293
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
|
+
}
|
|
294
448
|
return mesh;
|
|
295
449
|
}
|
|
296
450
|
|
|
@@ -354,10 +508,28 @@
|
|
|
354
508
|
const cw = mvp[3] * x + mvp[7] * y + mvp[11] * z + mvp[15] || 1;
|
|
355
509
|
return [(cx / cw * 0.5 + 0.5) * w, (0.5 - cy / cw * 0.5) * hh];
|
|
356
510
|
}
|
|
357
|
-
function ViewPresetBar({ onPick, color, onPickColor }) {
|
|
511
|
+
function ViewPresetBar({ onPick, color, onPickColor, plates, plate, onPickPlate }) {
|
|
358
512
|
return h(
|
|
359
513
|
"div",
|
|
360
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
|
+
),
|
|
361
533
|
h(
|
|
362
534
|
"div",
|
|
363
535
|
{ className: "d3v-views" },
|
|
@@ -411,19 +583,27 @@
|
|
|
411
583
|
["y", [hi[0] + margin, (lo[1] + hi[1]) / 2, lo[2]]],
|
|
412
584
|
["z", [hi[0] + margin, lo[1], (lo[2] + hi[2]) / 2]]
|
|
413
585
|
];
|
|
414
|
-
function Viewer({ mesh, resetToken, view, color }) {
|
|
586
|
+
function Viewer({ mesh, resetToken, view, color, plate }) {
|
|
415
587
|
const ref = React.useRef(null);
|
|
416
588
|
const orient = React.useRef({ rot: DEFAULT_ROT(), ortho: false });
|
|
417
589
|
const pan = React.useRef([0, 0]);
|
|
418
590
|
const redraw = React.useRef(() => {
|
|
419
591
|
});
|
|
420
592
|
const colorRef = React.useRef(null);
|
|
593
|
+
const plateRef = React.useRef(-1);
|
|
421
594
|
React.useEffect(() => {
|
|
422
595
|
if (!view) return;
|
|
423
596
|
orient.current = { rot: buildRot(view.yaw, view.pitch), ortho: !!view.ortho };
|
|
424
597
|
pan.current = [0, 0];
|
|
425
598
|
redraw.current();
|
|
426
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]);
|
|
427
607
|
React.useEffect(() => {
|
|
428
608
|
if (!color) return;
|
|
429
609
|
colorRef.current = color;
|
|
@@ -500,12 +680,13 @@
|
|
|
500
680
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
501
681
|
const aspect = w / hh;
|
|
502
682
|
const proj = o.ortho ? orthographic(aspect, orthoHalfSize(zoom)) : perspective(aspect);
|
|
503
|
-
const
|
|
683
|
+
const active = plateRef.current >= 0 && mesh.plates && mesh.plates[plateRef.current] || mesh;
|
|
684
|
+
const scale = 1 / active.radius;
|
|
504
685
|
const model = new Float32Array(rot);
|
|
505
686
|
for (const idx of [0, 1, 2, 4, 5, 6, 8, 9, 10]) model[idx] *= scale;
|
|
506
|
-
model[12] = -(model[0] *
|
|
507
|
-
model[13] = -(model[1] *
|
|
508
|
-
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;
|
|
509
690
|
lastModel = model;
|
|
510
691
|
gl.useProgram(program);
|
|
511
692
|
const col = colorRef.current || DEFAULT_COLOR;
|
|
@@ -514,15 +695,16 @@
|
|
|
514
695
|
gl.uniformMatrix4fv(gl.getUniformLocation(program, "model"), false, model);
|
|
515
696
|
const mvp = mul(proj, model);
|
|
516
697
|
gl.uniformMatrix4fv(gl.getUniformLocation(program, "mvp"), false, mvp);
|
|
517
|
-
|
|
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);
|
|
518
700
|
if (o.ortho) {
|
|
519
|
-
const m =
|
|
701
|
+
const m = active.radius * 0.18;
|
|
520
702
|
const inPlane = {
|
|
521
703
|
x: Math.abs(rot[2]) < 0.9,
|
|
522
704
|
y: Math.abs(rot[6]) < 0.9,
|
|
523
705
|
z: Math.abs(rot[10]) < 0.9
|
|
524
706
|
};
|
|
525
|
-
const anchors = DIM_ANCHORS(
|
|
707
|
+
const anchors = DIM_ANCHORS(active.lo, active.hi, m);
|
|
526
708
|
for (const [axis, point] of anchors) {
|
|
527
709
|
const el = labelEls[axis];
|
|
528
710
|
if (!inPlane[axis]) {
|
|
@@ -533,7 +715,7 @@
|
|
|
533
715
|
el.style.display = "";
|
|
534
716
|
el.style.left = sx + "px";
|
|
535
717
|
el.style.top = sy + "px";
|
|
536
|
-
el.textContent =
|
|
718
|
+
el.textContent = active.size[axis === "x" ? 0 : axis === "y" ? 1 : 2].toFixed(2);
|
|
537
719
|
}
|
|
538
720
|
} else {
|
|
539
721
|
for (const el of Object.values(labelEls)) el.style.display = "none";
|
|
@@ -718,6 +900,7 @@
|
|
|
718
900
|
const [state, setState] = React.useState({ status: bytes ? "loading" : "waiting" });
|
|
719
901
|
const [view, setView] = React.useState();
|
|
720
902
|
const [color, setColor] = React.useState(DEFAULT_COLOR);
|
|
903
|
+
const [plate, setPlate] = React.useState(-1);
|
|
721
904
|
React.useEffect(() => {
|
|
722
905
|
if (!bytes) return;
|
|
723
906
|
let live = true;
|
|
@@ -738,8 +921,8 @@
|
|
|
738
921
|
return h(
|
|
739
922
|
"div",
|
|
740
923
|
{ className: "d3v-doc", "data-dsh-3d-docbody": "" },
|
|
741
|
-
state.status === "ready" && h(Viewer, { mesh: state.mesh, resetToken: 0, view, color }),
|
|
742
|
-
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 }),
|
|
743
926
|
state.status === "loading" && h("div", { className: "d3v-doc-status" }, tt("loading")),
|
|
744
927
|
state.status === "waiting" && h("div", { className: "d3v-doc-status" }, tt("waiting")),
|
|
745
928
|
state.status === "error" && h("div", { className: "d3v-error" }, tt("failed", { message: state.message })),
|
|
@@ -771,6 +954,7 @@
|
|
|
771
954
|
const s = useStore(sessionStore);
|
|
772
955
|
const [reset, setReset] = React.useState(0), [query, setQuery] = React.useState("");
|
|
773
956
|
const [view, setView] = React.useState(), [color, setColor] = React.useState(DEFAULT_COLOR);
|
|
957
|
+
const [plate, setPlate] = React.useState(-1);
|
|
774
958
|
const selected = s.files.find((x) => x.path === s.selected) || s.files[0];
|
|
775
959
|
const shown = s.files.filter((f) => f.path.toLowerCase().includes(query.trim().toLowerCase()));
|
|
776
960
|
const load = React.useCallback(async (path) => {
|
|
@@ -854,8 +1038,8 @@
|
|
|
854
1038
|
h(
|
|
855
1039
|
"div",
|
|
856
1040
|
{ className: "d3v-stage" },
|
|
857
|
-
s.mesh && h(Viewer, { mesh: s.mesh, resetToken: reset, view, color }),
|
|
858
|
-
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 }),
|
|
859
1043
|
s.loading && h("div", { className: "d3v-loading" }, "\u6B63\u5728\u66F4\u65B0\u6A21\u578B\u2026"),
|
|
860
1044
|
s.error && h("div", { className: "d3v-error" }, s.error),
|
|
861
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addozhang/dsh-3d-model-viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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": {
|