@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 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 + `basematerials` colors; build-item
78
- transforms, per-component overrides, and slicer-specific metadata are not
79
- reconstructed.
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 支持网格几何 + `basematerials` 颜色;build-item 变换、组件级覆盖与切片器私有元数据不还原
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 vals = [], cols = [];
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 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");
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
- 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);
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
- if (!objects.length) for (const mesh2 of doc.getElementsByTagNameNS("*", "mesh")) emit(mesh2, null, null);
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 mesh = finishMesh(new Float32Array(vals), triangles);
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 scale = 1 / mesh.radius;
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] * mesh.center[0] + model[4] * mesh.center[1] + model[8] * mesh.center[2]) + pan.current[0];
507
- model[13] = -(model[1] * mesh.center[0] + model[5] * mesh.center[1] + model[9] * mesh.center[2]) + pan.current[1];
508
- 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;
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
- 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);
518
700
  if (o.ortho) {
519
- const m = mesh.radius * 0.18;
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(mesh.lo, mesh.hi, m);
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 = 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);
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.1",
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": {