copper3d 3.6.5 → 3.6.7

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.
@@ -61165,6 +61165,15 @@ class MeshGraph {
61165
61165
  const nm = new Matrix3().getNormalMatrix(mesh.matrixWorld);
61166
61166
  return n.applyMatrix3(nm).normalize();
61167
61167
  }
61168
+ /** Vertex local normal at index i (reads from the welded graphGeo normal attribute). */
61169
+ vertexNormalLocal(i) {
61170
+ return new Vector3(this.normals ? this.normals[i * 3] : 0, this.normals ? this.normals[i * 3 + 1] : 0, this.normals ? this.normals[i * 3 + 2] : 1);
61171
+ }
61172
+ /** Normal at the vertex nearest to localPoint (for recovering normals from imported point coordinates). */
61173
+ nearestNormalLocal(p) {
61174
+ const i = this.nearestVertex(p);
61175
+ return this.vertexNormalLocal(i);
61176
+ }
61168
61177
  /** Vertex local coordinates + local normal (the graph geometry is already local space). */
61169
61178
  vertexLocal(i) {
61170
61179
  return {
@@ -61390,6 +61399,13 @@ class AnnotationStore {
61390
61399
  this.notify();
61391
61400
  }
61392
61401
  }
61402
+ setVisible(id, visible) {
61403
+ const a = this.get(id);
61404
+ if (a) {
61405
+ a.visible = visible;
61406
+ this.notify();
61407
+ }
61408
+ }
61393
61409
  toJSON(modelName, mesh, opts = {}) {
61394
61410
  var _a;
61395
61411
  const space = (_a = opts.space) !== null && _a !== void 0 ? _a : "local";
@@ -61414,6 +61430,7 @@ class AnnotationStore {
61414
61430
  label: a.label,
61415
61431
  color: a.color,
61416
61432
  closed: a.closed,
61433
+ visible: a.visible,
61417
61434
  points: a.vertices.map(toPt),
61418
61435
  })),
61419
61436
  };
@@ -61433,6 +61450,10 @@ class SurfaceAnnotator {
61433
61450
  var _a, _b, _c;
61434
61451
  this.mode = "navigate";
61435
61452
  this.spaceHeld = false;
61453
+ this.drawLock = false;
61454
+ this.armed = "freehand";
61455
+ this.spaceDownAt = 0;
61456
+ this.spaceDragged = false;
61436
61457
  this.pointerDown = false;
61437
61458
  this.store = new AnnotationStore();
61438
61459
  this.managed = new Set();
@@ -61459,14 +61480,18 @@ class SurfaceAnnotator {
61459
61480
  (this.activeGeoLine.material.resolution.set(w, h));
61460
61481
  };
61461
61482
  this.onPointerDown = (e) => {
61462
- if (this.spaceHeld)
61463
- return;
61464
61483
  if (e.button !== 0)
61465
61484
  return; // left button only
61466
61485
  if (!this.insideContainer(e))
61467
61486
  return;
61468
61487
  this.pointerDown = true;
61469
- if (this.mode === "point") {
61488
+ // Only act with the armed tool when in drawing mode (drawLock or spaceHeld).
61489
+ // When not drawing, navigation is the default — pointer events go to camera controls.
61490
+ if (!this.drawing)
61491
+ return;
61492
+ // Use armed tool (not this.mode which may be "navigate")
61493
+ const activeTool = this.armed;
61494
+ if (activeTool === "point") {
61470
61495
  const h = this.hit(e);
61471
61496
  if (!h)
61472
61497
  return;
@@ -61480,13 +61505,14 @@ class SurfaceAnnotator {
61480
61505
  label: `Point ${this.seq}`,
61481
61506
  color: this.pointColorVal,
61482
61507
  closed: false,
61508
+ visible: true,
61483
61509
  vertices: [v],
61484
61510
  object3D: marker,
61485
61511
  };
61486
61512
  this.store.add(ann);
61487
61513
  return;
61488
61514
  }
61489
- if (this.mode === "freehand") {
61515
+ if (activeTool === "freehand") {
61490
61516
  this.activeStroke = new StrokeContour(this.minGap, this.maxJump, this.o.mesh);
61491
61517
  this.activeStroke.begin();
61492
61518
  const h = this.hit(e);
@@ -61496,7 +61522,7 @@ class SurfaceAnnotator {
61496
61522
  this.o.scene.add(this.activeLine);
61497
61523
  return;
61498
61524
  }
61499
- if (this.mode === "geodesic") {
61525
+ if (activeTool === "geodesic") {
61500
61526
  // First check whether an existing anchor was clicked → cancel that point (any point can be canceled).
61501
61527
  const pick = this.pickGeoMarker(e);
61502
61528
  if (pick >= 0 && this.activeGeo) {
@@ -61523,15 +61549,26 @@ class SurfaceAnnotator {
61523
61549
  }
61524
61550
  };
61525
61551
  this.onPointerMove = (e) => {
61526
- if (this.spaceHeld) {
61527
- this.setGeoHover(-1);
61552
+ // Track if user dragged while space was held (so a hold-drag is not treated as a tap).
61553
+ if (this.spaceHeld && this.pointerDown) {
61554
+ this.spaceDragged = true;
61555
+ }
61556
+ // When not drawing, navigation is default — don't interfere with camera controls.
61557
+ if (!this.drawing) {
61558
+ // In geodesic mode we can still show the hover hint when not drawing.
61559
+ if (this.armed === "geodesic" && !this.pointerDown) {
61560
+ this.setGeoHover(this.insideContainer(e) ? this.pickGeoMarker(e) : -1);
61561
+ }
61562
+ else {
61563
+ this.setGeoHover(-1);
61564
+ }
61528
61565
  return;
61529
61566
  }
61530
- // In geodesic mode, show the "✕" hint when hovering an anchor sphere (that point can be clicked to cancel).
61531
- if (this.mode === "geodesic" && !this.pointerDown) {
61567
+ // Drawing mode: geodesic hover hint
61568
+ if (this.armed === "geodesic" && !this.pointerDown) {
61532
61569
  this.setGeoHover(this.insideContainer(e) ? this.pickGeoMarker(e) : -1);
61533
61570
  }
61534
- if (this.mode === "freehand" &&
61571
+ if (this.armed === "freehand" &&
61535
61572
  this.pointerDown &&
61536
61573
  this.activeStroke &&
61537
61574
  this.activeLine) {
@@ -61556,6 +61593,7 @@ class SurfaceAnnotator {
61556
61593
  label: `Contour ${this.seq}`,
61557
61594
  color: this.freehandColor,
61558
61595
  closed: false,
61596
+ visible: true,
61559
61597
  vertices: verts,
61560
61598
  object3D: this.activeLine,
61561
61599
  };
@@ -61573,11 +61611,17 @@ class SurfaceAnnotator {
61573
61611
  if (this.isTypingTarget(e))
61574
61612
  return;
61575
61613
  if (e.code === "Space") {
61614
+ if (!this.spaceHeld) {
61615
+ this.spaceDownAt = performance.now();
61616
+ this.spaceDragged = false;
61617
+ }
61576
61618
  this.spaceHeld = true;
61577
61619
  this.applyCameraGating();
61620
+ e.preventDefault();
61578
61621
  return;
61579
61622
  }
61580
61623
  if (e.key === "Escape") {
61624
+ this.drawLock = false;
61581
61625
  this.setMode("navigate");
61582
61626
  return;
61583
61627
  }
@@ -61610,7 +61654,11 @@ class SurfaceAnnotator {
61610
61654
  };
61611
61655
  this.onKeyUp = (e) => {
61612
61656
  if (e.code === "Space") {
61657
+ const held = performance.now() - this.spaceDownAt;
61613
61658
  this.spaceHeld = false;
61659
+ if (held <= SurfaceAnnotator.TAP_MS && !this.spaceDragged) {
61660
+ this.drawLock = !this.drawLock; // tap toggles lock
61661
+ }
61614
61662
  this.applyCameraGating();
61615
61663
  }
61616
61664
  };
@@ -61666,6 +61714,10 @@ class SurfaceAnnotator {
61666
61714
  if (m !== "geodesic" && this.activeGeo)
61667
61715
  this.clearActiveGeo();
61668
61716
  this.mode = m;
61717
+ // Record armed tool when choosing a drawing mode (not navigate).
61718
+ // Camera gating is NOT changed here — the user must use Space to enter drawing.
61719
+ if (m !== "navigate")
61720
+ this.armed = m;
61669
61721
  this.applyCameraGating();
61670
61722
  (_b = (_a = this.o).onModeChange) === null || _b === void 0 ? void 0 : _b.call(_a, m);
61671
61723
  }
@@ -61728,10 +61780,92 @@ class SurfaceAnnotator {
61728
61780
  exportJSON(modelName, opts) {
61729
61781
  return this.store.toJSON(modelName, this.o.mesh, opts);
61730
61782
  }
61783
+ setVisible(id, visible) {
61784
+ this.store.setVisible(id, visible);
61785
+ }
61786
+ /**
61787
+ * Rebuild annotations from an exported payload (local-space points). Normals are taken from the
61788
+ * point when present ([x,y,z,nx,ny,nz]); otherwise recovered from the welded graph's nearest
61789
+ * vertex. Each imported item becomes first-class (select/recolor/hide/delete/export).
61790
+ */
61791
+ importAnnotations(payload) {
61792
+ var _a, _b, _c, _d;
61793
+ let count = 0;
61794
+ let maxImported = this.seq;
61795
+ for (const a of (_a = payload.annotations) !== null && _a !== void 0 ? _a : []) {
61796
+ const verts = a.points.map((p) => {
61797
+ const [x, y, z] = p;
61798
+ let nx = p[3], ny = p[4], nz = p[5];
61799
+ if (nx === undefined || ny === undefined || nz === undefined) {
61800
+ const nrm = this.graph.nearestNormalLocal(new Vector3(x, y, z));
61801
+ nx = nrm.x;
61802
+ ny = nrm.y;
61803
+ nz = nrm.z;
61804
+ }
61805
+ return { x, y, z, nx, ny, nz, faceIndex: 0 };
61806
+ });
61807
+ if (a.type === "points") {
61808
+ for (const v of verts) {
61809
+ // Only reuse the provided id for a single-point entry (the round-trip case);
61810
+ // a multi-point points entry must get a fresh id per marker to avoid duplicate keys.
61811
+ const id = a.id && verts.length === 1 ? a.id : this.nextId();
61812
+ const marker = makePointMarker(v, this.o.mesh, a.color, this.markerRadius);
61813
+ this.o.scene.add(marker);
61814
+ this.store.add({
61815
+ id,
61816
+ type: "points",
61817
+ mode: null,
61818
+ label: a.label,
61819
+ color: a.color,
61820
+ closed: false,
61821
+ visible: (_b = a.visible) !== null && _b !== void 0 ? _b : true,
61822
+ vertices: [v],
61823
+ object3D: marker,
61824
+ });
61825
+ // track max numeric id
61826
+ const m = id.match(/^a(\d+)$/);
61827
+ if (m)
61828
+ maxImported = Math.max(maxImported, parseInt(m[1], 10));
61829
+ count++;
61830
+ }
61831
+ }
61832
+ else {
61833
+ if (verts.length < 2)
61834
+ continue;
61835
+ const id = (_c = a.id) !== null && _c !== void 0 ? _c : this.nextId();
61836
+ const line = makeContourLine(verts, a.color, a.closed, this.o.container, this.epsilon, this.o.mesh);
61837
+ this.o.scene.add(line);
61838
+ this.store.add({
61839
+ id,
61840
+ type: "contour",
61841
+ mode: a.mode,
61842
+ label: a.label,
61843
+ color: a.color,
61844
+ closed: a.closed,
61845
+ visible: (_d = a.visible) !== null && _d !== void 0 ? _d : true,
61846
+ vertices: verts,
61847
+ object3D: line,
61848
+ });
61849
+ const m = id.match(/^a(\d+)$/);
61850
+ if (m)
61851
+ maxImported = Math.max(maxImported, parseInt(m[1], 10));
61852
+ count++;
61853
+ }
61854
+ }
61855
+ // keep seq ahead of any imported numeric ids to avoid collisions
61856
+ if (maxImported > this.seq)
61857
+ this.seq = maxImported;
61858
+ return count;
61859
+ }
61731
61860
  // ---- Internal ----
61861
+ get drawing() {
61862
+ return this.drawLock || this.spaceHeld;
61863
+ }
61732
61864
  applyCameraGating() {
61733
- // Holding Space for temporary rotation takes priority; otherwise enable the camera only in navigate mode.
61734
- this.o.controls.enabled = this.spaceHeld || this.mode === "navigate";
61865
+ var _a, _b;
61866
+ // Default is navigate (camera enabled). Drawing only when drawLock or spaceHeld.
61867
+ this.o.controls.enabled = !this.drawing;
61868
+ (_b = (_a = this.o).onInteractionChange) === null || _b === void 0 ? void 0 : _b.call(_a, { drawing: this.drawing, armed: this.armed, locked: this.drawLock });
61735
61869
  }
61736
61870
  /** Reconcile the scene against store.list(): add missing objects, remove deleted ones (no dispose, kept for undo restore). */
61737
61871
  reconcile() {
@@ -61749,6 +61883,11 @@ class SurfaceAnnotator {
61749
61883
  this.o.scene.add(o);
61750
61884
  }
61751
61885
  this.managed = present;
61886
+ // Apply per-annotation visibility
61887
+ for (const a of this.store.list()) {
61888
+ if (a.object3D)
61889
+ a.object3D.visible = a.visible;
61890
+ }
61752
61891
  this.applySelection();
61753
61892
  (_b = (_a = this.o).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, this.store.list());
61754
61893
  }
@@ -61756,6 +61895,8 @@ class SurfaceAnnotator {
61756
61895
  for (const a of this.store.list()) {
61757
61896
  if (!a.object3D)
61758
61897
  continue;
61898
+ if (!a.visible)
61899
+ continue;
61759
61900
  const sel = a.id === this.selectedId;
61760
61901
  if (a.type === "contour") {
61761
61902
  const mat = a.object3D.material;
@@ -61988,6 +62129,7 @@ class SurfaceAnnotator {
61988
62129
  label: `Contour ${this.seq}`,
61989
62130
  color: this.geodesicColor,
61990
62131
  closed,
62132
+ visible: true,
61991
62133
  vertices: verts,
61992
62134
  object3D: this.activeGeoLine,
61993
62135
  };
@@ -62021,7 +62163,8 @@ class SurfaceAnnotator {
62021
62163
  window.removeEventListener("keyup", this.onKeyUp);
62022
62164
  window.removeEventListener("resize", this.onResize);
62023
62165
  }
62024
- }
62166
+ }
62167
+ SurfaceAnnotator.TAP_MS = 250;
62025
62168
 
62026
62169
  class copperScene extends baseScene {
62027
62170
  // rayster pick
@@ -83011,12 +83154,20 @@ class NrrdTools {
83011
83154
  return this.drawCore.aiAssistTool.getScratchSegments();
83012
83155
  }
83013
83156
  /**
83014
- * Merge the AI scratch into a target layer as a single undoable group (sandbox
83015
- * merge — best-practice: non-destructive + one Ctrl+Z). The scratch's channel
83016
- * labels are PRESERVED (each AI channel maps onto the same channel of the
83017
- * target layer). Scans all z-slices, so voxels painted from any view are caught.
83157
+ * Merge the AI scratch into a target layer with an explicit per-segmentation
83158
+ * mapping, as a single undoable group (sandbox merge — non-destructive + one
83159
+ * Ctrl+Z). `mapping` is `{ AI scratch label -> target channel(1-8) }`; any label
83160
+ * absent from the mapping (or mapped to 0) is DISCARDED (not merged). Several AI
83161
+ * labels MAY map to the SAME channel — after merge the channel is the identity
83162
+ * (CLAUDE.md decision M2), so the merged voxels take that channel's fixed colour.
83163
+ *
83164
+ * UNION semantics (M4): an AI voxel only fills a target voxel that is currently
83165
+ * EMPTY (0) — existing manual annotation is NEVER erased. Where two AI labels map
83166
+ * to different channels and overlap the same voxel, the first one written wins
83167
+ * (iteration order), an accepted edge case for a single-value-per-voxel mask.
83168
+ * Scans all z-slices, so voxels painted from any view are caught.
83018
83169
  */
83019
- aiCommitToLayer(targetLayer = "layer1") {
83170
+ aiCommitToLayerMapped(targetLayer, mapping) {
83020
83171
  const scratch = this.drawCore.aiAssistTool.getScratchVolume();
83021
83172
  if (!scratch)
83022
83173
  return;
@@ -83038,10 +83189,21 @@ class NrrdTools {
83038
83189
  continue;
83039
83190
  const oldSlice = target.getSliceUint8(z, "z").data; // copy
83040
83191
  const newSlice = oldSlice.slice();
83192
+ let changed = false;
83041
83193
  for (let i = 0; i < newSlice.length; i++) {
83042
- if (sc[i] !== 0)
83043
- newSlice[i] = sc[i]; // preserve the AI channel label
83194
+ const label = sc[i];
83195
+ if (label === 0)
83196
+ continue;
83197
+ const ch = mapping[label];
83198
+ if (!ch)
83199
+ continue; // unmapped / discarded segmentation
83200
+ if (newSlice[i] === 0) { // union: fill only EMPTY voxels (M4)
83201
+ newSlice[i] = ch;
83202
+ changed = true;
83203
+ }
83044
83204
  }
83205
+ if (!changed)
83206
+ continue; // nothing landed on this slice → no delta
83045
83207
  target.setSliceUint8(z, newSlice, "z");
83046
83208
  deltas.push({ layerId: targetLayer, axis: "z", sliceIndex: z, oldSlice, newSlice: newSlice.slice() });
83047
83209
  }
@@ -83049,6 +83211,19 @@ class NrrdTools {
83049
83211
  this.drawCore.undoManager.pushGroup(deltas);
83050
83212
  this.reloadMasksFromVolume();
83051
83213
  }
83214
+ /**
83215
+ * Back-compat shim: merge with an IDENTITY mapping (each AI label → the same
83216
+ * channel number) via the mapped path above. Newer callers should use
83217
+ * `aiCommitToLayerMapped` with an explicit per-segmentation mapping.
83218
+ */
83219
+ aiCommitToLayer(targetLayer = "layer1") {
83220
+ var _a;
83221
+ const segs = this.drawCore.aiAssistTool.getScratchSegments();
83222
+ const mapping = {};
83223
+ for (const s of (_a = segs === null || segs === void 0 ? void 0 : segs.segments) !== null && _a !== void 0 ? _a : [])
83224
+ mapping[s.label] = s.label;
83225
+ this.aiCommitToLayerMapped(targetLayer, mapping);
83226
+ }
83052
83227
  // ═══════════════════════════════════════════════════════════════════════════
83053
83228
  // 11. Clear / Reset
83054
83229
  // ═══════════════════════════════════════════════════════════════════════════
@@ -83412,8 +83587,17 @@ function evaluateElement(element, weights) {
83412
83587
  }
83413
83588
 
83414
83589
  // import * as kiwrious from "copper3d_plugin_heart_k";
83415
- // "v3.6.5" is injected at build time by rollup @rollup/plugin-replace, sourced from package.json version
83416
- const REVISION = "v3.6.5";
83590
+ // "v3.6.7" is injected at build time by rollup @rollup/plugin-replace, sourced from package.json version.
83591
+ // When copper3d is consumed from local source (no rollup replace step), the identifier is undefined and
83592
+ // referencing it throws a ReferenceError — guard it so local loading still works.
83593
+ let _revision = "unknown";
83594
+ try {
83595
+ _revision = "v3.6.7";
83596
+ }
83597
+ catch (_a) {
83598
+ /* "v3.6.7" not injected (local source build) */
83599
+ }
83600
+ const REVISION = _revision;
83417
83601
  // Expose on global so the version can be read in a production browser console via window.__COPPER3D_VERSION__
83418
83602
  if (typeof window !== "undefined") {
83419
83603
  window.__COPPER3D_VERSION__ = REVISION;