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.
@@ -59589,11 +59589,18 @@ void main() {
59589
59589
  * 顶点以 local 存(由 mesh 把世界命中点转 local)。
59590
59590
  */
59591
59591
  class StrokeContour {
59592
- constructor(minGap, mesh) {
59592
+ /**
59593
+ * @param minGap 采样最小间距(世界系),去抖
59594
+ * @param maxJump 跳变阈值(世界系):距离 > maxJump 且法线大幅翻转时丢弃该样本,
59595
+ * 避免笔触掠过沟缝/轮廓边时相邻点落在不同深度、直线段横穿模型。
59596
+ */
59597
+ constructor(minGap, maxJump, mesh) {
59593
59598
  this.minGap = minGap;
59599
+ this.maxJump = maxJump;
59594
59600
  this.mesh = mesh;
59595
59601
  this.verts = [];
59596
59602
  this.last = new Vector3();
59603
+ this.lastNormal = new Vector3();
59597
59604
  this.has = false;
59598
59605
  }
59599
59606
  begin() {
@@ -59601,11 +59608,19 @@ void main() {
59601
59608
  this.has = false;
59602
59609
  }
59603
59610
  addSample(hit) {
59604
- // gap 判定用世界距离(屏幕笔触在世界系采样)
59605
- if (this.has && hit.point.distanceTo(this.last) < this.minGap)
59606
- return;
59611
+ if (this.has) {
59612
+ const d = hit.point.distanceTo(this.last);
59613
+ if (d < this.minGap)
59614
+ return; // 太近,去抖
59615
+ // 跳变剔除:距离突然很大「且」法线大幅翻转(>~75°)→ 多半跳到了背面/远面,丢弃,
59616
+ // 否则相邻两点之间的直线段会横穿模型内部。两个条件同时满足才剔除:
59617
+ // 快速画(距离大、法线相近)不误删;画过尖锐棱边(法线变、距离近)也不误删。
59618
+ if (d > this.maxJump && hit.normal.dot(this.lastNormal) < 0.25)
59619
+ return;
59620
+ }
59607
59621
  this.verts.push(worldHitToLocalVertex(hit, this.mesh));
59608
59622
  this.last.copy(hit.point);
59623
+ this.lastNormal.copy(hit.normal);
59609
59624
  this.has = true;
59610
59625
  }
59611
59626
  get vertices() {
@@ -61155,16 +61170,54 @@ void main() {
61155
61170
  this.mesh = mesh;
61156
61171
  this.anchors = []; // 顶点索引
61157
61172
  this.segments = []; // 相邻锚点间路径(含端点)
61173
+ this.history = []; // 每次增删前的锚点快照,用于撤销
61174
+ }
61175
+ /** 任何改动锚点前先存一份快照(顶点索引数组很小,快照成本可忽略)。 */
61176
+ snapshot() {
61177
+ this.history.push(this.anchors.slice());
61158
61178
  }
61159
61179
  /** 传入 local 命中点,snap 到最近顶点并对上一锚点求路径。 */
61160
61180
  addAnchor(localHitPoint) {
61161
61181
  const v = this.graph.nearestVertex(localHitPoint);
61182
+ this.snapshot();
61162
61183
  if (this.anchors.length > 0) {
61163
61184
  const prev = this.anchors[this.anchors.length - 1];
61164
61185
  this.segments.push(this.graph.shortestPath(prev, v));
61165
61186
  }
61166
61187
  this.anchors.push(v);
61167
61188
  }
61189
+ /** 删除指定下标的锚点,重算相邻段(支持取消中间任意一点)。 */
61190
+ removeAnchorAt(index) {
61191
+ if (index < 0 || index >= this.anchors.length)
61192
+ return;
61193
+ this.snapshot();
61194
+ this.anchors.splice(index, 1);
61195
+ this.rebuildSegments();
61196
+ }
61197
+ /** 是否还有可撤销的编辑(加点/删点)。 */
61198
+ canUndo() {
61199
+ return this.history.length > 0;
61200
+ }
61201
+ /** 撤销上一次锚点编辑(加点 → 删回去;删点 → 恢复)。无历史返回 false。 */
61202
+ undoEdit() {
61203
+ const prev = this.history.pop();
61204
+ if (!prev)
61205
+ return false;
61206
+ this.anchors = prev;
61207
+ this.rebuildSegments();
61208
+ return true;
61209
+ }
61210
+ /** 据当前锚点序列整体重算所有相邻段路径。 */
61211
+ rebuildSegments() {
61212
+ this.segments = [];
61213
+ for (let i = 1; i < this.anchors.length; i++) {
61214
+ this.segments.push(this.graph.shortestPath(this.anchors[i - 1], this.anchors[i]));
61215
+ }
61216
+ }
61217
+ /** 各锚点的 local 顶点(用于绘制可见的锚点标记)。 */
61218
+ getAnchorLocals() {
61219
+ return this.anchors.map((i) => this.graph.vertexLocal(i));
61220
+ }
61168
61221
  get anchorCount() {
61169
61222
  return this.anchors.length;
61170
61223
  }
@@ -61309,6 +61362,11 @@ void main() {
61309
61362
  this.managed = new Set();
61310
61363
  this.seq = 0;
61311
61364
  this.selectedId = null;
61365
+ this.activeGeoMarkers = []; // 进行中测地线的可见锚点
61366
+ this.geoRay = new Raycaster();
61367
+ this.geoNdc = new Vector2();
61368
+ this.hoveredGeoMarker = -1;
61369
+ this._projV = new Vector3();
61312
61370
  /** 窗口尺寸变化时更新所有 fat line 的像素分辨率,否则线宽会失真。 */
61313
61371
  this.onResize = () => {
61314
61372
  const w = this.o.container.clientWidth;
@@ -61353,7 +61411,7 @@ void main() {
61353
61411
  return;
61354
61412
  }
61355
61413
  if (this.mode === "freehand") {
61356
- this.activeStroke = new StrokeContour(this.minGap, this.o.mesh);
61414
+ this.activeStroke = new StrokeContour(this.minGap, this.maxJump, this.o.mesh);
61357
61415
  this.activeStroke.begin();
61358
61416
  const h = this.hit(e);
61359
61417
  if (h)
@@ -61363,6 +61421,19 @@ void main() {
61363
61421
  return;
61364
61422
  }
61365
61423
  if (this.mode === "geodesic") {
61424
+ // 先判断是否点中了一个已有锚点 → 取消该点(支持取消其中任意一点)。
61425
+ const pick = this.pickGeoMarker(e);
61426
+ if (pick >= 0 && this.activeGeo) {
61427
+ this.activeGeo.removeAnchorAt(pick);
61428
+ if (this.activeGeo.anchorCount === 0)
61429
+ this.clearActiveGeo();
61430
+ else {
61431
+ this.rebuildGeoMarkers();
61432
+ this.redrawGeoLine();
61433
+ }
61434
+ return;
61435
+ }
61436
+ // 否则在表面落一个新锚点。
61366
61437
  const h = this.hit(e);
61367
61438
  if (!h)
61368
61439
  return;
@@ -61371,19 +61442,19 @@ void main() {
61371
61442
  }
61372
61443
  const local = this.o.mesh.worldToLocal(h.point.clone());
61373
61444
  this.activeGeo.addAnchor(local);
61374
- const verts = this.activeGeo.buildVertices(false);
61375
- if (!this.activeGeoLine) {
61376
- this.activeGeoLine = makeContourLine(verts, this.geodesicColor, false, this.o.container, this.epsilon, this.o.mesh);
61377
- this.o.scene.add(this.activeGeoLine);
61378
- }
61379
- else {
61380
- updateContourLine(this.activeGeoLine, verts, false, this.epsilon, this.o.mesh);
61381
- }
61445
+ this.rebuildGeoMarkers();
61446
+ this.redrawGeoLine();
61382
61447
  }
61383
61448
  };
61384
61449
  this.onPointerMove = (e) => {
61385
- if (this.spaceHeld)
61450
+ if (this.spaceHeld) {
61451
+ this.setGeoHover(-1);
61386
61452
  return;
61453
+ }
61454
+ // 测地线模式下,悬停到锚点小球时显示"✕"提示(该点可被点击取消)。
61455
+ if (this.mode === "geodesic" && !this.pointerDown) {
61456
+ this.setGeoHover(this.insideContainer(e) ? this.pickGeoMarker(e) : -1);
61457
+ }
61387
61458
  if (this.mode === "freehand" &&
61388
61459
  this.pointerDown &&
61389
61460
  this.activeStroke &&
@@ -61473,9 +61544,10 @@ void main() {
61473
61544
  meshGeo.computeVertexNormals(); // 渲染需要法线
61474
61545
  meshGeo.computeBoundingBox();
61475
61546
  const diag = (_a = opts.bboxDiagonal) !== null && _a !== void 0 ? _a : meshGeo.boundingBox.getSize(new Vector3()).length();
61476
- this.markerRadius = (_b = opts.markerRadius) !== null && _b !== void 0 ? _b : diag * 0.006;
61547
+ this.markerRadius = (_b = opts.markerRadius) !== null && _b !== void 0 ? _b : diag * 0.0032;
61477
61548
  this.epsilon = diag * 0.002;
61478
61549
  this.minGap = diag * 0.004;
61550
+ this.maxJump = diag * 0.05;
61479
61551
  // 测地线连通性:单独焊接一份"仅位置"的几何给 MeshGraph 用——
61480
61552
  // 不动被渲染的 mesh(保留其法线/UV/纹理)。仅位置可保证同表面位置的顶点
61481
61553
  // 被 mergeVertices 合并(否则逐面法线/UV 会阻止合并,图断裂 → 闭合穿模)。
@@ -61513,6 +61585,9 @@ void main() {
61513
61585
  }
61514
61586
  setMode(m) {
61515
61587
  var _a, _b;
61588
+ // 离开测地线模式时丢弃尚未 Enter 落定的进行中测地线(清掉残留的锚点与线)。
61589
+ if (m !== "geodesic" && this.activeGeo)
61590
+ this.clearActiveGeo();
61516
61591
  this.mode = m;
61517
61592
  this.applyCameraGating();
61518
61593
  (_b = (_a = this.o).onModeChange) === null || _b === void 0 ? void 0 : _b.call(_a, m);
@@ -61525,6 +61600,18 @@ void main() {
61525
61600
  return this.store.list();
61526
61601
  }
61527
61602
  undo() {
61603
+ // 测地线进行中 → 撤销最近一次锚点编辑(加点 / 取消点都能回退);
61604
+ // 否则撤销最近一条已落定的标注。
61605
+ if (this.activeGeo && this.activeGeo.canUndo()) {
61606
+ this.activeGeo.undoEdit();
61607
+ if (this.activeGeo.anchorCount === 0)
61608
+ this.clearActiveGeo();
61609
+ else {
61610
+ this.rebuildGeoMarkers();
61611
+ this.redrawGeoLine();
61612
+ }
61613
+ return;
61614
+ }
61528
61615
  this.store.undo();
61529
61616
  }
61530
61617
  clearAll() {
@@ -61537,6 +61624,7 @@ void main() {
61537
61624
  });
61538
61625
  this.managed.clear();
61539
61626
  this.selectedId = null;
61627
+ this.clearActiveGeo();
61540
61628
  }
61541
61629
  deleteAnnotation(id) {
61542
61630
  if (this.selectedId === id)
@@ -61617,10 +61705,153 @@ void main() {
61617
61705
  hit(e) {
61618
61706
  return raycastSurface(this.o.camera, this.o.container, this.o.mesh, e.clientX, e.clientY);
61619
61707
  }
61620
- /** 事件目标是否落在标注容器内(排除面板/页面其它区域的点击)。 */
61708
+ /**
61709
+ * 事件目标是否是容器内的 WebGL canvas。
61710
+ * 仅认 canvas:面板(GUIDE / 控制面板 / 标注列表的 ✕ 按钮)都是 container 的子元素,
61711
+ * 若只判断 contains 会把面板上的点击也当成在模型上作画 —— 这会"穿透"删除按钮、
61712
+ * 让 ✕ 难以点中。只响应 canvas 上的指针事件即可彻底隔离 UI 与画布。
61713
+ */
61621
61714
  insideContainer(e) {
61622
61715
  const t = e.target;
61623
- return !!t && this.o.container.contains(t);
61716
+ return !!t && t.tagName === "CANVAS" && this.o.container.contains(t);
61717
+ }
61718
+ /** 射线拾取进行中的锚点小球,返回锚点下标;未命中返回 -1。 */
61719
+ pickGeoMarker(e) {
61720
+ if (!this.activeGeoMarkers.length)
61721
+ return -1;
61722
+ const rect = this.o.container.getBoundingClientRect();
61723
+ this.geoNdc.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
61724
+ this.geoNdc.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
61725
+ this.geoRay.setFromCamera(this.geoNdc, this.o.camera);
61726
+ const hits = this.geoRay.intersectObjects(this.activeGeoMarkers, false);
61727
+ if (!hits.length)
61728
+ return -1;
61729
+ return this.activeGeoMarkers.indexOf(hits[0].object);
61730
+ }
61731
+ /** 设置当前悬停的锚点(-1 = 无):更新高亮缩放、光标与"✕(可取消)"悬浮标。 */
61732
+ setGeoHover(idx) {
61733
+ if (idx === this.hoveredGeoMarker) {
61734
+ if (idx >= 0)
61735
+ this.positionGeoBadge(this.activeGeoMarkers[idx]);
61736
+ return;
61737
+ }
61738
+ const prev = this.activeGeoMarkers[this.hoveredGeoMarker];
61739
+ if (prev)
61740
+ prev.scale.setScalar(1);
61741
+ this.hoveredGeoMarker = idx;
61742
+ const cur = this.activeGeoMarkers[idx];
61743
+ if (cur) {
61744
+ cur.scale.setScalar(1.3);
61745
+ this.ensureGeoBadge().style.display = "flex";
61746
+ this.positionGeoBadge(cur);
61747
+ this.o.container.style.cursor = "pointer";
61748
+ }
61749
+ else {
61750
+ if (this.geoHoverBadge)
61751
+ this.geoHoverBadge.style.display = "none";
61752
+ this.o.container.style.cursor = "";
61753
+ }
61754
+ }
61755
+ /** 懒创建悬浮 ✕ 标(挂在 container 内,pointer-events:none 不挡点击)。 */
61756
+ ensureGeoBadge() {
61757
+ if (this.geoHoverBadge)
61758
+ return this.geoHoverBadge;
61759
+ const b = document.createElement("div");
61760
+ b.textContent = "✕";
61761
+ Object.assign(b.style, {
61762
+ position: "absolute",
61763
+ width: "16px",
61764
+ height: "16px",
61765
+ borderRadius: "50%",
61766
+ background: "#ff5d6c",
61767
+ color: "#fff",
61768
+ font: "700 10px/1 system-ui, sans-serif",
61769
+ display: "none",
61770
+ alignItems: "center",
61771
+ justifyContent: "center",
61772
+ pointerEvents: "none",
61773
+ zIndex: "15",
61774
+ boxShadow: "0 2px 6px rgba(0,0,0,.45)",
61775
+ transform: "translate(-50%, -50%)",
61776
+ left: "0px",
61777
+ top: "0px",
61778
+ });
61779
+ this.o.container.appendChild(b);
61780
+ this.geoHoverBadge = b;
61781
+ return b;
61782
+ }
61783
+ /** 把 ✕ 标定位到锚点投影到屏幕的位置(正中心,即光标悬停处,避免歧义)。 */
61784
+ positionGeoBadge(marker) {
61785
+ if (!marker)
61786
+ return;
61787
+ const b = this.ensureGeoBadge();
61788
+ this._projV.copy(marker.position).project(this.o.camera);
61789
+ const rect = this.o.container.getBoundingClientRect();
61790
+ const x = (this._projV.x * 0.5 + 0.5) * rect.width;
61791
+ const y = (-this._projV.y * 0.5 + 0.5) * rect.height;
61792
+ b.style.left = `${x}px`;
61793
+ b.style.top = `${y}px`;
61794
+ }
61795
+ /** 据当前锚点重建可见的锚点小球(锚点比放点稍大,便于看见与点中取消)。 */
61796
+ rebuildGeoMarkers() {
61797
+ this.setGeoHover(-1);
61798
+ for (const m of this.activeGeoMarkers) {
61799
+ this.o.scene.remove(m);
61800
+ this.disposeObject(m);
61801
+ }
61802
+ this.activeGeoMarkers = [];
61803
+ if (!this.activeGeo)
61804
+ return;
61805
+ for (const v of this.activeGeo.getAnchorLocals()) {
61806
+ const marker = makePointMarker(v, this.o.mesh, this.geodesicColor, this.markerRadius * 1.15);
61807
+ this.o.scene.add(marker);
61808
+ this.activeGeoMarkers.push(marker);
61809
+ }
61810
+ }
61811
+ /** 据当前锚点重画进行中的测地线(不闭合);不足两点时移除线。 */
61812
+ redrawGeoLine() {
61813
+ if (!this.activeGeo)
61814
+ return;
61815
+ const verts = this.activeGeo.buildVertices(false);
61816
+ if (verts.length < 2) {
61817
+ if (this.activeGeoLine) {
61818
+ this.o.scene.remove(this.activeGeoLine);
61819
+ this.disposeObject(this.activeGeoLine);
61820
+ this.activeGeoLine = undefined;
61821
+ }
61822
+ return;
61823
+ }
61824
+ if (!this.activeGeoLine) {
61825
+ this.activeGeoLine = makeContourLine(verts, this.geodesicColor, false, this.o.container, this.epsilon, this.o.mesh);
61826
+ this.o.scene.add(this.activeGeoLine);
61827
+ }
61828
+ else {
61829
+ updateContourLine(this.activeGeoLine, verts, false, this.epsilon, this.o.mesh);
61830
+ }
61831
+ }
61832
+ /** 丢弃进行中的测地线:移除并 dispose 线与全部锚点。 */
61833
+ clearActiveGeo() {
61834
+ this.setGeoHover(-1);
61835
+ if (this.activeGeoLine) {
61836
+ this.o.scene.remove(this.activeGeoLine);
61837
+ this.disposeObject(this.activeGeoLine);
61838
+ this.activeGeoLine = undefined;
61839
+ }
61840
+ for (const m of this.activeGeoMarkers) {
61841
+ this.o.scene.remove(m);
61842
+ this.disposeObject(m);
61843
+ }
61844
+ this.activeGeoMarkers = [];
61845
+ this.activeGeo = undefined;
61846
+ }
61847
+ /** 移除进行中测地线的全部锚点小球(落定后调用:只留下线)。 */
61848
+ removeGeoMarkers() {
61849
+ this.setGeoHover(-1);
61850
+ for (const m of this.activeGeoMarkers) {
61851
+ this.o.scene.remove(m);
61852
+ this.disposeObject(m);
61853
+ }
61854
+ this.activeGeoMarkers = [];
61624
61855
  }
61625
61856
  closeLastContour() {
61626
61857
  const last = this.lastFreehand;
@@ -61663,7 +61894,10 @@ void main() {
61663
61894
  }
61664
61895
  else {
61665
61896
  this.o.scene.remove(this.activeGeoLine);
61897
+ this.disposeObject(this.activeGeoLine);
61666
61898
  }
61899
+ // 落定后清掉可见锚点,只留下贴合表面的线。
61900
+ this.removeGeoMarkers();
61667
61901
  this.activeGeo = undefined;
61668
61902
  this.activeGeoLine = undefined;
61669
61903
  }
@@ -61675,6 +61909,10 @@ void main() {
61675
61909
  return tag === "INPUT" || tag === "TEXTAREA" || t.isContentEditable;
61676
61910
  }
61677
61911
  dispose() {
61912
+ var _a;
61913
+ this.clearActiveGeo();
61914
+ (_a = this.geoHoverBadge) === null || _a === void 0 ? void 0 : _a.remove();
61915
+ this.geoHoverBadge = undefined;
61678
61916
  window.removeEventListener("pointerdown", this.onPointerDown, true);
61679
61917
  window.removeEventListener("pointermove", this.onPointerMove, true);
61680
61918
  window.removeEventListener("pointerup", this.onPointerUp, true);
@@ -82662,7 +82900,7 @@ void main() {
82662
82900
  }
82663
82901
 
82664
82902
  // import * as kiwrious from "copper3d_plugin_heart_k";
82665
- const REVISION = "v3.6.0-beta";
82903
+ const REVISION = "v3.6.2-beta";
82666
82904
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
82667
82905
 
82668
82906
  exports.AI_CHANNEL_HEX_COLORS = AI_CHANNEL_HEX_COLORS;
package/dist/index.d.ts CHANGED
@@ -28,6 +28,6 @@ import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, AI_CHANNEL_
28
28
  import type { LayerId, ChannelValue } from "./Utils/segmentation/core/index";
29
29
  import type { AiPromptTool, AiPromptPoint, AiPromptPayload, AiMaskResult } from "./Utils/segmentation/tools/AiAssistTool";
30
30
  import "./css/style.css";
31
- export declare const REVISION = "v3.6.0-beta";
31
+ export declare const REVISION = "v3.6.2-beta";
32
32
  export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, AI_CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, GaussianSmoother, SurfaceAnnotator, };
33
33
  export type { positionType, screenPosType, optsType, nrrdMeshesType, nrrdSliceType, SensorDecodedValue_kiwrious, SensorReadResult_kiwrious, HeartRateResult_kiwrious, loadingBarType, IPaintImage, exportPaintImageType, IOptVTKLoader, aligned4DSurfaceType, aligned4DOptsType, Aligned4DController, ICommXYZ, IGUIStates, IGuiParameterSettings, INrrdStates, NrrdState, GuiState, IGuiMeta, ToolMode, IAnnotationCallbacks, LayerId, ChannelValue, AiPromptTool, AiPromptPoint, AiPromptPayload, AiMaskResult, SurfaceAnnotatorOptions, Annotation, AnnotationMode, ExportOptions, };
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import { removeGuiFolderChilden } from "./Utils/segmentation/coreTools/gui";
22
22
  import { SurfaceAnnotator } from "./Utils/surfaceAnnotation";
23
23
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, AI_CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
24
24
  import "./css/style.css";
25
- export const REVISION = "v3.6.0-beta";
25
+ export const REVISION = "v3.6.2-beta";
26
26
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
27
27
  export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, AI_CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, GaussianSmoother, SurfaceAnnotator, };
28
28
  //# sourceMappingURL=index.js.map
@@ -38,6 +38,7 @@ export declare class SurfaceAnnotator {
38
38
  private readonly markerRadius;
39
39
  private readonly epsilon;
40
40
  private readonly minGap;
41
+ private readonly maxJump;
41
42
  private graph;
42
43
  private store;
43
44
  private managed;
@@ -48,6 +49,12 @@ export declare class SurfaceAnnotator {
48
49
  private lastFreehand?;
49
50
  private activeGeo?;
50
51
  private activeGeoLine?;
52
+ private activeGeoMarkers;
53
+ private geoRay;
54
+ private geoNdc;
55
+ private geoHoverBadge?;
56
+ private hoveredGeoMarker;
57
+ private _projV;
51
58
  constructor(opts: SurfaceAnnotatorOptions);
52
59
  /** 窗口尺寸变化时更新所有 fat line 的像素分辨率,否则线宽会失真。 */
53
60
  private onResize;
@@ -86,9 +93,30 @@ export declare class SurfaceAnnotator {
86
93
  private disposeObject;
87
94
  private nextId;
88
95
  private hit;
89
- /** 事件目标是否落在标注容器内(排除面板/页面其它区域的点击)。 */
96
+ /**
97
+ * 事件目标是否是容器内的 WebGL canvas。
98
+ * 仅认 canvas:面板(GUIDE / 控制面板 / 标注列表的 ✕ 按钮)都是 container 的子元素,
99
+ * 若只判断 contains 会把面板上的点击也当成在模型上作画 —— 这会"穿透"删除按钮、
100
+ * 让 ✕ 难以点中。只响应 canvas 上的指针事件即可彻底隔离 UI 与画布。
101
+ */
90
102
  private insideContainer;
91
103
  private onPointerDown;
104
+ /** 射线拾取进行中的锚点小球,返回锚点下标;未命中返回 -1。 */
105
+ private pickGeoMarker;
106
+ /** 设置当前悬停的锚点(-1 = 无):更新高亮缩放、光标与"✕(可取消)"悬浮标。 */
107
+ private setGeoHover;
108
+ /** 懒创建悬浮 ✕ 标(挂在 container 内,pointer-events:none 不挡点击)。 */
109
+ private ensureGeoBadge;
110
+ /** 把 ✕ 标定位到锚点投影到屏幕的位置(正中心,即光标悬停处,避免歧义)。 */
111
+ private positionGeoBadge;
112
+ /** 据当前锚点重建可见的锚点小球(锚点比放点稍大,便于看见与点中取消)。 */
113
+ private rebuildGeoMarkers;
114
+ /** 据当前锚点重画进行中的测地线(不闭合);不足两点时移除线。 */
115
+ private redrawGeoLine;
116
+ /** 丢弃进行中的测地线:移除并 dispose 线与全部锚点。 */
117
+ private clearActiveGeo;
118
+ /** 移除进行中测地线的全部锚点小球(落定后调用:只留下线)。 */
119
+ private removeGeoMarkers;
92
120
  private onPointerMove;
93
121
  private onPointerUp;
94
122
  private closeLastContour;
@@ -10,9 +10,22 @@ export declare class GeodesicContour {
10
10
  private mesh;
11
11
  private anchors;
12
12
  private segments;
13
+ private history;
13
14
  constructor(graph: MeshGraph, mesh: THREE.Mesh);
15
+ /** 任何改动锚点前先存一份快照(顶点索引数组很小,快照成本可忽略)。 */
16
+ private snapshot;
14
17
  /** 传入 local 命中点,snap 到最近顶点并对上一锚点求路径。 */
15
18
  addAnchor(localHitPoint: THREE.Vector3): void;
19
+ /** 删除指定下标的锚点,重算相邻段(支持取消中间任意一点)。 */
20
+ removeAnchorAt(index: number): void;
21
+ /** 是否还有可撤销的编辑(加点/删点)。 */
22
+ canUndo(): boolean;
23
+ /** 撤销上一次锚点编辑(加点 → 删回去;删点 → 恢复)。无历史返回 false。 */
24
+ undoEdit(): boolean;
25
+ /** 据当前锚点序列整体重算所有相邻段路径。 */
26
+ private rebuildSegments;
27
+ /** 各锚点的 local 顶点(用于绘制可见的锚点标记)。 */
28
+ getAnchorLocals(): AnnotationVertex[];
16
29
  get anchorCount(): number;
17
30
  /** 把所有路径顶点(world)+ 法线 拼成折线;closed 时补一段首尾路径。 */
18
31
  buildVertices(closed: boolean): AnnotationVertex[];
@@ -7,11 +7,18 @@ import type { AnnotationVertex, SurfaceHit } from "./types";
7
7
  */
8
8
  export declare class StrokeContour {
9
9
  private minGap;
10
+ private maxJump;
10
11
  private mesh;
11
12
  private verts;
12
13
  private last;
14
+ private lastNormal;
13
15
  private has;
14
- constructor(minGap: number, mesh: THREE.Mesh);
16
+ /**
17
+ * @param minGap 采样最小间距(世界系),去抖
18
+ * @param maxJump 跳变阈值(世界系):距离 > maxJump 且法线大幅翻转时丢弃该样本,
19
+ * 避免笔触掠过沟缝/轮廓边时相邻点落在不同深度、直线段横穿模型。
20
+ */
21
+ constructor(minGap: number, maxJump: number, mesh: THREE.Mesh);
15
22
  begin(): void;
16
23
  addSample(hit: SurfaceHit): void;
17
24
  get vertices(): AnnotationVertex[];
@@ -28,6 +28,6 @@ import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, AI_CHANNEL_
28
28
  import type { LayerId, ChannelValue } from "./Utils/segmentation/core/index";
29
29
  import type { AiPromptTool, AiPromptPoint, AiPromptPayload, AiMaskResult } from "./Utils/segmentation/tools/AiAssistTool";
30
30
  import "./css/style.css";
31
- export declare const REVISION = "v3.6.0-beta";
31
+ export declare const REVISION = "v3.6.2-beta";
32
32
  export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, AI_MASK_CHANNEL_COLORS, AI_CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, GaussianSmoother, SurfaceAnnotator, };
33
33
  export type { positionType, screenPosType, optsType, nrrdMeshesType, nrrdSliceType, SensorDecodedValue_kiwrious, SensorReadResult_kiwrious, HeartRateResult_kiwrious, loadingBarType, IPaintImage, exportPaintImageType, IOptVTKLoader, aligned4DSurfaceType, aligned4DOptsType, Aligned4DController, ICommXYZ, IGUIStates, IGuiParameterSettings, INrrdStates, NrrdState, GuiState, IGuiMeta, ToolMode, IAnnotationCallbacks, LayerId, ChannelValue, AiPromptTool, AiPromptPoint, AiPromptPayload, AiMaskResult, SurfaceAnnotatorOptions, Annotation, AnnotationMode, ExportOptions, };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "copper3d",
3
3
  "description": "A 3d visualisation package base on threejs provides multiple scenes and Nrrd image load funtion.",
4
- "version": "3.6.0",
4
+ "version": "3.6.2",
5
5
  "main": "dist/bundle.umd.js",
6
6
  "moudle": "dist/bundle.esm.js",
7
7
  "types": "dist/types/index.d.ts",