copper3d 3.6.0 → 3.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59581,11 +59581,18 @@ function makePointMarker(v, mesh, color, radius) {
59581
59581
  * 顶点以 local 存(由 mesh 把世界命中点转 local)。
59582
59582
  */
59583
59583
  class StrokeContour {
59584
- constructor(minGap, mesh) {
59584
+ /**
59585
+ * @param minGap 采样最小间距(世界系),去抖
59586
+ * @param maxJump 跳变阈值(世界系):距离 > maxJump 且法线大幅翻转时丢弃该样本,
59587
+ * 避免笔触掠过沟缝/轮廓边时相邻点落在不同深度、直线段横穿模型。
59588
+ */
59589
+ constructor(minGap, maxJump, mesh) {
59585
59590
  this.minGap = minGap;
59591
+ this.maxJump = maxJump;
59586
59592
  this.mesh = mesh;
59587
59593
  this.verts = [];
59588
59594
  this.last = new Vector3();
59595
+ this.lastNormal = new Vector3();
59589
59596
  this.has = false;
59590
59597
  }
59591
59598
  begin() {
@@ -59593,11 +59600,19 @@ class StrokeContour {
59593
59600
  this.has = false;
59594
59601
  }
59595
59602
  addSample(hit) {
59596
- // gap 判定用世界距离(屏幕笔触在世界系采样)
59597
- if (this.has && hit.point.distanceTo(this.last) < this.minGap)
59598
- return;
59603
+ if (this.has) {
59604
+ const d = hit.point.distanceTo(this.last);
59605
+ if (d < this.minGap)
59606
+ return; // 太近,去抖
59607
+ // 跳变剔除:距离突然很大「且」法线大幅翻转(>~75°)→ 多半跳到了背面/远面,丢弃,
59608
+ // 否则相邻两点之间的直线段会横穿模型内部。两个条件同时满足才剔除:
59609
+ // 快速画(距离大、法线相近)不误删;画过尖锐棱边(法线变、距离近)也不误删。
59610
+ if (d > this.maxJump && hit.normal.dot(this.lastNormal) < 0.25)
59611
+ return;
59612
+ }
59599
59613
  this.verts.push(worldHitToLocalVertex(hit, this.mesh));
59600
59614
  this.last.copy(hit.point);
59615
+ this.lastNormal.copy(hit.normal);
59601
59616
  this.has = true;
59602
59617
  }
59603
59618
  get vertices() {
@@ -61147,16 +61162,54 @@ class GeodesicContour {
61147
61162
  this.mesh = mesh;
61148
61163
  this.anchors = []; // 顶点索引
61149
61164
  this.segments = []; // 相邻锚点间路径(含端点)
61165
+ this.history = []; // 每次增删前的锚点快照,用于撤销
61166
+ }
61167
+ /** 任何改动锚点前先存一份快照(顶点索引数组很小,快照成本可忽略)。 */
61168
+ snapshot() {
61169
+ this.history.push(this.anchors.slice());
61150
61170
  }
61151
61171
  /** 传入 local 命中点,snap 到最近顶点并对上一锚点求路径。 */
61152
61172
  addAnchor(localHitPoint) {
61153
61173
  const v = this.graph.nearestVertex(localHitPoint);
61174
+ this.snapshot();
61154
61175
  if (this.anchors.length > 0) {
61155
61176
  const prev = this.anchors[this.anchors.length - 1];
61156
61177
  this.segments.push(this.graph.shortestPath(prev, v));
61157
61178
  }
61158
61179
  this.anchors.push(v);
61159
61180
  }
61181
+ /** 删除指定下标的锚点,重算相邻段(支持取消中间任意一点)。 */
61182
+ removeAnchorAt(index) {
61183
+ if (index < 0 || index >= this.anchors.length)
61184
+ return;
61185
+ this.snapshot();
61186
+ this.anchors.splice(index, 1);
61187
+ this.rebuildSegments();
61188
+ }
61189
+ /** 是否还有可撤销的编辑(加点/删点)。 */
61190
+ canUndo() {
61191
+ return this.history.length > 0;
61192
+ }
61193
+ /** 撤销上一次锚点编辑(加点 → 删回去;删点 → 恢复)。无历史返回 false。 */
61194
+ undoEdit() {
61195
+ const prev = this.history.pop();
61196
+ if (!prev)
61197
+ return false;
61198
+ this.anchors = prev;
61199
+ this.rebuildSegments();
61200
+ return true;
61201
+ }
61202
+ /** 据当前锚点序列整体重算所有相邻段路径。 */
61203
+ rebuildSegments() {
61204
+ this.segments = [];
61205
+ for (let i = 1; i < this.anchors.length; i++) {
61206
+ this.segments.push(this.graph.shortestPath(this.anchors[i - 1], this.anchors[i]));
61207
+ }
61208
+ }
61209
+ /** 各锚点的 local 顶点(用于绘制可见的锚点标记)。 */
61210
+ getAnchorLocals() {
61211
+ return this.anchors.map((i) => this.graph.vertexLocal(i));
61212
+ }
61160
61213
  get anchorCount() {
61161
61214
  return this.anchors.length;
61162
61215
  }
@@ -61301,6 +61354,11 @@ class SurfaceAnnotator {
61301
61354
  this.managed = new Set();
61302
61355
  this.seq = 0;
61303
61356
  this.selectedId = null;
61357
+ this.activeGeoMarkers = []; // 进行中测地线的可见锚点
61358
+ this.geoRay = new Raycaster();
61359
+ this.geoNdc = new Vector2();
61360
+ this.hoveredGeoMarker = -1;
61361
+ this._projV = new Vector3();
61304
61362
  /** 窗口尺寸变化时更新所有 fat line 的像素分辨率,否则线宽会失真。 */
61305
61363
  this.onResize = () => {
61306
61364
  const w = this.o.container.clientWidth;
@@ -61345,7 +61403,7 @@ class SurfaceAnnotator {
61345
61403
  return;
61346
61404
  }
61347
61405
  if (this.mode === "freehand") {
61348
- this.activeStroke = new StrokeContour(this.minGap, this.o.mesh);
61406
+ this.activeStroke = new StrokeContour(this.minGap, this.maxJump, this.o.mesh);
61349
61407
  this.activeStroke.begin();
61350
61408
  const h = this.hit(e);
61351
61409
  if (h)
@@ -61355,6 +61413,19 @@ class SurfaceAnnotator {
61355
61413
  return;
61356
61414
  }
61357
61415
  if (this.mode === "geodesic") {
61416
+ // 先判断是否点中了一个已有锚点 → 取消该点(支持取消其中任意一点)。
61417
+ const pick = this.pickGeoMarker(e);
61418
+ if (pick >= 0 && this.activeGeo) {
61419
+ this.activeGeo.removeAnchorAt(pick);
61420
+ if (this.activeGeo.anchorCount === 0)
61421
+ this.clearActiveGeo();
61422
+ else {
61423
+ this.rebuildGeoMarkers();
61424
+ this.redrawGeoLine();
61425
+ }
61426
+ return;
61427
+ }
61428
+ // 否则在表面落一个新锚点。
61358
61429
  const h = this.hit(e);
61359
61430
  if (!h)
61360
61431
  return;
@@ -61363,19 +61434,19 @@ class SurfaceAnnotator {
61363
61434
  }
61364
61435
  const local = this.o.mesh.worldToLocal(h.point.clone());
61365
61436
  this.activeGeo.addAnchor(local);
61366
- const verts = this.activeGeo.buildVertices(false);
61367
- if (!this.activeGeoLine) {
61368
- this.activeGeoLine = makeContourLine(verts, this.geodesicColor, false, this.o.container, this.epsilon, this.o.mesh);
61369
- this.o.scene.add(this.activeGeoLine);
61370
- }
61371
- else {
61372
- updateContourLine(this.activeGeoLine, verts, false, this.epsilon, this.o.mesh);
61373
- }
61437
+ this.rebuildGeoMarkers();
61438
+ this.redrawGeoLine();
61374
61439
  }
61375
61440
  };
61376
61441
  this.onPointerMove = (e) => {
61377
- if (this.spaceHeld)
61442
+ if (this.spaceHeld) {
61443
+ this.setGeoHover(-1);
61378
61444
  return;
61445
+ }
61446
+ // 测地线模式下,悬停到锚点小球时显示"✕"提示(该点可被点击取消)。
61447
+ if (this.mode === "geodesic" && !this.pointerDown) {
61448
+ this.setGeoHover(this.insideContainer(e) ? this.pickGeoMarker(e) : -1);
61449
+ }
61379
61450
  if (this.mode === "freehand" &&
61380
61451
  this.pointerDown &&
61381
61452
  this.activeStroke &&
@@ -61465,9 +61536,10 @@ class SurfaceAnnotator {
61465
61536
  meshGeo.computeVertexNormals(); // 渲染需要法线
61466
61537
  meshGeo.computeBoundingBox();
61467
61538
  const diag = (_a = opts.bboxDiagonal) !== null && _a !== void 0 ? _a : meshGeo.boundingBox.getSize(new Vector3()).length();
61468
- this.markerRadius = (_b = opts.markerRadius) !== null && _b !== void 0 ? _b : diag * 0.006;
61539
+ this.markerRadius = (_b = opts.markerRadius) !== null && _b !== void 0 ? _b : diag * 0.0032;
61469
61540
  this.epsilon = diag * 0.002;
61470
61541
  this.minGap = diag * 0.004;
61542
+ this.maxJump = diag * 0.05;
61471
61543
  // 测地线连通性:单独焊接一份"仅位置"的几何给 MeshGraph 用——
61472
61544
  // 不动被渲染的 mesh(保留其法线/UV/纹理)。仅位置可保证同表面位置的顶点
61473
61545
  // 被 mergeVertices 合并(否则逐面法线/UV 会阻止合并,图断裂 → 闭合穿模)。
@@ -61505,6 +61577,9 @@ class SurfaceAnnotator {
61505
61577
  }
61506
61578
  setMode(m) {
61507
61579
  var _a, _b;
61580
+ // 离开测地线模式时丢弃尚未 Enter 落定的进行中测地线(清掉残留的锚点与线)。
61581
+ if (m !== "geodesic" && this.activeGeo)
61582
+ this.clearActiveGeo();
61508
61583
  this.mode = m;
61509
61584
  this.applyCameraGating();
61510
61585
  (_b = (_a = this.o).onModeChange) === null || _b === void 0 ? void 0 : _b.call(_a, m);
@@ -61517,6 +61592,18 @@ class SurfaceAnnotator {
61517
61592
  return this.store.list();
61518
61593
  }
61519
61594
  undo() {
61595
+ // 测地线进行中 → 撤销最近一次锚点编辑(加点 / 取消点都能回退);
61596
+ // 否则撤销最近一条已落定的标注。
61597
+ if (this.activeGeo && this.activeGeo.canUndo()) {
61598
+ this.activeGeo.undoEdit();
61599
+ if (this.activeGeo.anchorCount === 0)
61600
+ this.clearActiveGeo();
61601
+ else {
61602
+ this.rebuildGeoMarkers();
61603
+ this.redrawGeoLine();
61604
+ }
61605
+ return;
61606
+ }
61520
61607
  this.store.undo();
61521
61608
  }
61522
61609
  clearAll() {
@@ -61529,6 +61616,7 @@ class SurfaceAnnotator {
61529
61616
  });
61530
61617
  this.managed.clear();
61531
61618
  this.selectedId = null;
61619
+ this.clearActiveGeo();
61532
61620
  }
61533
61621
  deleteAnnotation(id) {
61534
61622
  if (this.selectedId === id)
@@ -61609,10 +61697,153 @@ class SurfaceAnnotator {
61609
61697
  hit(e) {
61610
61698
  return raycastSurface(this.o.camera, this.o.container, this.o.mesh, e.clientX, e.clientY);
61611
61699
  }
61612
- /** 事件目标是否落在标注容器内(排除面板/页面其它区域的点击)。 */
61700
+ /**
61701
+ * 事件目标是否是容器内的 WebGL canvas。
61702
+ * 仅认 canvas:面板(GUIDE / 控制面板 / 标注列表的 ✕ 按钮)都是 container 的子元素,
61703
+ * 若只判断 contains 会把面板上的点击也当成在模型上作画 —— 这会"穿透"删除按钮、
61704
+ * 让 ✕ 难以点中。只响应 canvas 上的指针事件即可彻底隔离 UI 与画布。
61705
+ */
61613
61706
  insideContainer(e) {
61614
61707
  const t = e.target;
61615
- return !!t && this.o.container.contains(t);
61708
+ return !!t && t.tagName === "CANVAS" && this.o.container.contains(t);
61709
+ }
61710
+ /** 射线拾取进行中的锚点小球,返回锚点下标;未命中返回 -1。 */
61711
+ pickGeoMarker(e) {
61712
+ if (!this.activeGeoMarkers.length)
61713
+ return -1;
61714
+ const rect = this.o.container.getBoundingClientRect();
61715
+ this.geoNdc.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
61716
+ this.geoNdc.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
61717
+ this.geoRay.setFromCamera(this.geoNdc, this.o.camera);
61718
+ const hits = this.geoRay.intersectObjects(this.activeGeoMarkers, false);
61719
+ if (!hits.length)
61720
+ return -1;
61721
+ return this.activeGeoMarkers.indexOf(hits[0].object);
61722
+ }
61723
+ /** 设置当前悬停的锚点(-1 = 无):更新高亮缩放、光标与"✕(可取消)"悬浮标。 */
61724
+ setGeoHover(idx) {
61725
+ if (idx === this.hoveredGeoMarker) {
61726
+ if (idx >= 0)
61727
+ this.positionGeoBadge(this.activeGeoMarkers[idx]);
61728
+ return;
61729
+ }
61730
+ const prev = this.activeGeoMarkers[this.hoveredGeoMarker];
61731
+ if (prev)
61732
+ prev.scale.setScalar(1);
61733
+ this.hoveredGeoMarker = idx;
61734
+ const cur = this.activeGeoMarkers[idx];
61735
+ if (cur) {
61736
+ cur.scale.setScalar(1.3);
61737
+ this.ensureGeoBadge().style.display = "flex";
61738
+ this.positionGeoBadge(cur);
61739
+ this.o.container.style.cursor = "pointer";
61740
+ }
61741
+ else {
61742
+ if (this.geoHoverBadge)
61743
+ this.geoHoverBadge.style.display = "none";
61744
+ this.o.container.style.cursor = "";
61745
+ }
61746
+ }
61747
+ /** 懒创建悬浮 ✕ 标(挂在 container 内,pointer-events:none 不挡点击)。 */
61748
+ ensureGeoBadge() {
61749
+ if (this.geoHoverBadge)
61750
+ return this.geoHoverBadge;
61751
+ const b = document.createElement("div");
61752
+ b.textContent = "✕";
61753
+ Object.assign(b.style, {
61754
+ position: "absolute",
61755
+ width: "16px",
61756
+ height: "16px",
61757
+ borderRadius: "50%",
61758
+ background: "#ff5d6c",
61759
+ color: "#fff",
61760
+ font: "700 10px/1 system-ui, sans-serif",
61761
+ display: "none",
61762
+ alignItems: "center",
61763
+ justifyContent: "center",
61764
+ pointerEvents: "none",
61765
+ zIndex: "15",
61766
+ boxShadow: "0 2px 6px rgba(0,0,0,.45)",
61767
+ transform: "translate(-50%, -50%)",
61768
+ left: "0px",
61769
+ top: "0px",
61770
+ });
61771
+ this.o.container.appendChild(b);
61772
+ this.geoHoverBadge = b;
61773
+ return b;
61774
+ }
61775
+ /** 把 ✕ 标定位到锚点投影到屏幕的位置(正中心,即光标悬停处,避免歧义)。 */
61776
+ positionGeoBadge(marker) {
61777
+ if (!marker)
61778
+ return;
61779
+ const b = this.ensureGeoBadge();
61780
+ this._projV.copy(marker.position).project(this.o.camera);
61781
+ const rect = this.o.container.getBoundingClientRect();
61782
+ const x = (this._projV.x * 0.5 + 0.5) * rect.width;
61783
+ const y = (-this._projV.y * 0.5 + 0.5) * rect.height;
61784
+ b.style.left = `${x}px`;
61785
+ b.style.top = `${y}px`;
61786
+ }
61787
+ /** 据当前锚点重建可见的锚点小球(锚点比放点稍大,便于看见与点中取消)。 */
61788
+ rebuildGeoMarkers() {
61789
+ this.setGeoHover(-1);
61790
+ for (const m of this.activeGeoMarkers) {
61791
+ this.o.scene.remove(m);
61792
+ this.disposeObject(m);
61793
+ }
61794
+ this.activeGeoMarkers = [];
61795
+ if (!this.activeGeo)
61796
+ return;
61797
+ for (const v of this.activeGeo.getAnchorLocals()) {
61798
+ const marker = makePointMarker(v, this.o.mesh, this.geodesicColor, this.markerRadius * 1.15);
61799
+ this.o.scene.add(marker);
61800
+ this.activeGeoMarkers.push(marker);
61801
+ }
61802
+ }
61803
+ /** 据当前锚点重画进行中的测地线(不闭合);不足两点时移除线。 */
61804
+ redrawGeoLine() {
61805
+ if (!this.activeGeo)
61806
+ return;
61807
+ const verts = this.activeGeo.buildVertices(false);
61808
+ if (verts.length < 2) {
61809
+ if (this.activeGeoLine) {
61810
+ this.o.scene.remove(this.activeGeoLine);
61811
+ this.disposeObject(this.activeGeoLine);
61812
+ this.activeGeoLine = undefined;
61813
+ }
61814
+ return;
61815
+ }
61816
+ if (!this.activeGeoLine) {
61817
+ this.activeGeoLine = makeContourLine(verts, this.geodesicColor, false, this.o.container, this.epsilon, this.o.mesh);
61818
+ this.o.scene.add(this.activeGeoLine);
61819
+ }
61820
+ else {
61821
+ updateContourLine(this.activeGeoLine, verts, false, this.epsilon, this.o.mesh);
61822
+ }
61823
+ }
61824
+ /** 丢弃进行中的测地线:移除并 dispose 线与全部锚点。 */
61825
+ clearActiveGeo() {
61826
+ this.setGeoHover(-1);
61827
+ if (this.activeGeoLine) {
61828
+ this.o.scene.remove(this.activeGeoLine);
61829
+ this.disposeObject(this.activeGeoLine);
61830
+ this.activeGeoLine = undefined;
61831
+ }
61832
+ for (const m of this.activeGeoMarkers) {
61833
+ this.o.scene.remove(m);
61834
+ this.disposeObject(m);
61835
+ }
61836
+ this.activeGeoMarkers = [];
61837
+ this.activeGeo = undefined;
61838
+ }
61839
+ /** 移除进行中测地线的全部锚点小球(落定后调用:只留下线)。 */
61840
+ removeGeoMarkers() {
61841
+ this.setGeoHover(-1);
61842
+ for (const m of this.activeGeoMarkers) {
61843
+ this.o.scene.remove(m);
61844
+ this.disposeObject(m);
61845
+ }
61846
+ this.activeGeoMarkers = [];
61616
61847
  }
61617
61848
  closeLastContour() {
61618
61849
  const last = this.lastFreehand;
@@ -61655,7 +61886,10 @@ class SurfaceAnnotator {
61655
61886
  }
61656
61887
  else {
61657
61888
  this.o.scene.remove(this.activeGeoLine);
61889
+ this.disposeObject(this.activeGeoLine);
61658
61890
  }
61891
+ // 落定后清掉可见锚点,只留下贴合表面的线。
61892
+ this.removeGeoMarkers();
61659
61893
  this.activeGeo = undefined;
61660
61894
  this.activeGeoLine = undefined;
61661
61895
  }
@@ -61667,6 +61901,10 @@ class SurfaceAnnotator {
61667
61901
  return tag === "INPUT" || tag === "TEXTAREA" || t.isContentEditable;
61668
61902
  }
61669
61903
  dispose() {
61904
+ var _a;
61905
+ this.clearActiveGeo();
61906
+ (_a = this.geoHoverBadge) === null || _a === void 0 ? void 0 : _a.remove();
61907
+ this.geoHoverBadge = undefined;
61670
61908
  window.removeEventListener("pointerdown", this.onPointerDown, true);
61671
61909
  window.removeEventListener("pointermove", this.onPointerMove, true);
61672
61910
  window.removeEventListener("pointerup", this.onPointerUp, true);
@@ -82654,7 +82892,7 @@ function evaluateElement(element, weights) {
82654
82892
  }
82655
82893
 
82656
82894
  // import * as kiwrious from "copper3d_plugin_heart_k";
82657
- const REVISION = "v3.6.0-beta";
82895
+ const REVISION = "v3.6.2-beta";
82658
82896
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
82659
82897
 
82660
82898
  export { AI_CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, CHANNEL_COLORS, CHANNEL_HEX_COLORS, CameraViewPoint, Copper3dTrackballControls, GaussianSmoother, MeshNodeTool, NrrdTools, REVISION, SurfaceAnnotator, addBoxHelper, addLabelToScene, configKiwriousHeart, convert3DPostoScreenPos, convertScreenPosto3DPos, copperMScene, copperMSceneRenderer, copperRenderer, copperRendererOnDemond, copperScene, copperSceneOnDemond, createTexture2D_NRRD, fullScreenListenner, kiwrious, loading, removeGuiFolderChilden, rgbaToCss, rgbaToHex, setHDRFilePath, throttle };