copper3d 3.4.3 → 3.4.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ZoomTool.js","sourceRoot":"","sources":["../../../../src/Utils/segmentation/tools/ZoomTool.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ZoomTool.js","sourceRoot":"","sources":["../../../../src/Utils/segmentation/tools/ZoomTool.ts"],"names":[],"mappings":"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"}
@@ -70871,6 +70871,17 @@ class MaskVolume {
70871
70871
  * @throws {RangeError} If any dimension or the channel count is < 1.
70872
70872
  */
70873
70873
  constructor(width, height, depth, channels = 1, customColorMap) {
70874
+ /**
70875
+ * Monotonic data-mutation counter.
70876
+ *
70877
+ * Incremented by every method that mutates voxel data (not color).
70878
+ * Consumers (e.g. the contour cache in RenderingUtils) key cached
70879
+ * derivations on this value so an edit automatically invalidates them —
70880
+ * it is impossible to forget an invalidation point because every write
70881
+ * bumps the version. Color-map changes do **not** bump it (colors are
70882
+ * applied at fill time, never cached).
70883
+ */
70884
+ this.version = 0;
70874
70885
  if (width < 1 || height < 1 || depth < 1) {
70875
70886
  throw new RangeError(`Dimensions must be ≥ 1, got (${width}, ${height}, ${depth})`);
70876
70887
  }
@@ -70960,6 +70971,16 @@ class MaskVolume {
70960
70971
  */
70961
70972
  setVoxel(x, y, z, value, channel = 0) {
70962
70973
  this.data[this.getIndex(x, y, z, channel)] = value;
70974
+ this.version++;
70975
+ }
70976
+ /**
70977
+ * Return the current data-mutation version.
70978
+ *
70979
+ * Increments on every voxel write. Use as a cache key so derived data
70980
+ * (e.g. extracted contours) is invalidated whenever the volume changes.
70981
+ */
70982
+ getVersion() {
70983
+ return this.version;
70963
70984
  }
70964
70985
  // ── Color map management ──────────────────────────────────────────
70965
70986
  /**
@@ -71116,6 +71137,7 @@ class MaskVolume {
71116
71137
  throw new Error(`ImageData size mismatch: expected ${expectedW}×${expectedH}, ` +
71117
71138
  `got ${imageData.width}×${imageData.height}`);
71118
71139
  }
71140
+ this.version++;
71119
71141
  const pixels = imageData.data;
71120
71142
  const { width, height } = this.dims;
71121
71143
  const ch = this.numChannels;
@@ -71217,6 +71239,7 @@ class MaskVolume {
71217
71239
  throw new Error(`ImageData size mismatch: expected ${expectedW}×${expectedH}, ` +
71218
71240
  `got ${imageData.width}×${imageData.height}`);
71219
71241
  }
71242
+ this.version++;
71220
71243
  // Build RGB→channel lookup map for O(1) reverse lookup
71221
71244
  const rgbToChannel = this.buildRgbToChannelMap();
71222
71245
  const pixels = imageData.data;
@@ -71481,6 +71504,7 @@ class MaskVolume {
71481
71504
  throw new Error(`Data length mismatch: expected ${this.data.length}, got ${newData.length}`);
71482
71505
  }
71483
71506
  this.data = newData;
71507
+ this.version++;
71484
71508
  }
71485
71509
  /**
71486
71510
  * Create an independent deep copy of this volume.
@@ -71520,6 +71544,7 @@ class MaskVolume {
71520
71544
  */
71521
71545
  clear() {
71522
71546
  this.data.fill(0);
71547
+ this.version++;
71523
71548
  }
71524
71549
  /**
71525
71550
  * Check if the volume contains any non-zero voxel data.
@@ -71572,6 +71597,7 @@ class MaskVolume {
71572
71597
  */
71573
71598
  clearSlice(sliceIndex, axis = 'z', channel) {
71574
71599
  this.validateSliceIndex(sliceIndex, axis);
71600
+ this.version++;
71575
71601
  const [sliceW, sliceH] = this.getSliceDimensions(axis);
71576
71602
  const volData = this.data;
71577
71603
  const nch = this.numChannels;
@@ -71683,6 +71709,7 @@ class MaskVolume {
71683
71709
  */
71684
71710
  setSliceUint8(sliceIndex, data, axis = 'z') {
71685
71711
  this.validateSliceIndex(sliceIndex, axis);
71712
+ this.version++;
71686
71713
  const [sliceW, sliceH] = this.getSliceDimensions(axis);
71687
71714
  const nch = this.numChannels;
71688
71715
  const rowStride = this.dims.width * nch;
@@ -72126,10 +72153,18 @@ const CELL_POLYGONS = [
72126
72153
  /* 15 (full) */ [[[0, 0], [1, 0], [1, 1], [0, 1]]],
72127
72154
  ];
72128
72155
  /**
72129
- * Emit all per-cell polygons covering the voxels where `labels === targetLabel`.
72156
+ * Emit polygons covering the voxels where `labels === targetLabel`.
72130
72157
  * Pure geometry function — no Canvas dependency. Consumers can build a Path2D
72131
72158
  * (see {@link extractLabelContours}) or walk the vertex arrays directly
72132
72159
  * (tests, export, stroke rendering).
72160
+ *
72161
+ * Boundary cells (marching-squares cases 1–14) emit their cut polygon as
72162
+ * usual. Solid interior (case 15) is **coalesced into horizontal run-length
72163
+ * rectangles** instead of one unit square per cell, collapsing the subpath
72164
+ * count from O(area) to ≈ O(perimeter + rows). The filled region is
72165
+ * pixel-identical (contiguous full-cell squares tile exactly into a rectangle
72166
+ * with the same TL→TR→BR→BL winding), which is what keeps slice-scrubbing
72167
+ * fast on large masks without changing the rendered silhouette.
72133
72168
  */
72134
72169
  function extractLabelPolygons(labels, width, height, targetLabel, stride = 1, channelOffset = 0, bbox) {
72135
72170
  var _a, _b, _c, _d;
@@ -72151,7 +72186,25 @@ function extractLabelPolygons(labels, width, height, targetLabel, stride = 1, ch
72151
72186
  // map to voxel-square CENTERS in render space. Voxel (i, j) occupies
72152
72187
  // render square [i, i+1] × [j, j+1]; its center is (i+0.5, j+0.5).
72153
72188
  const SHIFT = 0.5;
72189
+ // Emit a single rectangle covering a contiguous run of full (code 15)
72190
+ // cells [runStart .. runEnd] on row j. A full cell i covers render square
72191
+ // [i+0.5, i+1.5] × [j+0.5, j+1.5]; consecutive full cells tile perfectly,
72192
+ // so the run is pixel-identical to the per-cell squares it replaces.
72193
+ // Vertices use the same TL→TR→BR→BL (CCW screen-space) winding as the
72194
+ // case-15 unit square, so the nonzero fill matches at shared edges.
72195
+ const pushFullRun = (runStart, runEnd, j) => {
72196
+ const x0 = runStart + SHIFT;
72197
+ const x1 = runEnd + 1 + SHIFT;
72198
+ const y0 = j + SHIFT;
72199
+ const y1 = j + 1 + SHIFT;
72200
+ out.push([[x0, y0], [x1, y0], [x1, y1], [x0, y1]]);
72201
+ };
72154
72202
  for (let j = j0; j < j1; j++) {
72203
+ // Index where the current contiguous run of full (code 15) cells began,
72204
+ // or -1 when no run is open. Coalescing the solid interior into row runs
72205
+ // drops the per-cell square count from O(area) to O(rows + perimeter),
72206
+ // which is what keeps fast slice-scrubbing smooth on large masks.
72207
+ let runStart = -1;
72155
72208
  for (let i = i0; i < i1; i++) {
72156
72209
  const tl = sample(i, j);
72157
72210
  const tr = sample(i + 1, j);
@@ -72161,6 +72214,17 @@ function extractLabelPolygons(labels, width, height, targetLabel, stride = 1, ch
72161
72214
  (tr ? 4 : 0) |
72162
72215
  (br ? 2 : 0) |
72163
72216
  (bl ? 1 : 0);
72217
+ if (code === 15) {
72218
+ // Extend (or open) the current full-cell run; defer emission.
72219
+ if (runStart === -1)
72220
+ runStart = i;
72221
+ continue;
72222
+ }
72223
+ // Non-full cell ends any open run — flush it as one rectangle.
72224
+ if (runStart !== -1) {
72225
+ pushFullRun(runStart, i - 1, j);
72226
+ runStart = -1;
72227
+ }
72164
72228
  if (code === 0)
72165
72229
  continue;
72166
72230
  const polygons = CELL_POLYGONS[code];
@@ -72173,6 +72237,10 @@ function extractLabelPolygons(labels, width, height, targetLabel, stride = 1, ch
72173
72237
  out.push(abs);
72174
72238
  }
72175
72239
  }
72240
+ // Flush a run that reached the end of the row (last visited cell is i1-1).
72241
+ if (runStart !== -1) {
72242
+ pushFullRun(runStart, i1 - 1, j);
72243
+ }
72176
72244
  }
72177
72245
  return out;
72178
72246
  }
@@ -73637,6 +73705,23 @@ class RenderingUtils {
73637
73705
  this._reusableSliceBuffer = null;
73638
73706
  this._reusableBufferWidth = 0;
73639
73707
  this._reusableBufferHeight = 0;
73708
+ /**
73709
+ * Per-layer contour cache.
73710
+ *
73711
+ * Caches the *expensive* part of slice rendering — `getSliceUint8` +
73712
+ * `findLabelsInSlice` + `extractLabelContours` (Path2D build) — keyed by
73713
+ * `${axis}:${sliceIndex}:${volume.version}`. The contours live in voxel
73714
+ * coordinates; zoom only changes the `ctx.scale` transform, so on zoom /
73715
+ * recomposite we reuse the cached Path2D and just re-fill, skipping
73716
+ * marching-squares entirely. A volume edit bumps its version → cache miss
73717
+ * → recompute (correct, automatic). Colors and channel visibility are
73718
+ * applied at fill time and intentionally *not* cached, so toggling them
73719
+ * needs no recompute.
73720
+ *
73721
+ * One entry per layer (the current slice). Switching slice/axis or
73722
+ * editing overwrites it.
73723
+ */
73724
+ this._contourCache = new Map();
73640
73725
  this.state = state;
73641
73726
  }
73642
73727
  // ── Volume Accessor Helpers ──────────────────────────────────────
@@ -73735,14 +73820,27 @@ class RenderingUtils {
73735
73820
  const volume = this.getVolumeForLayer(layer);
73736
73821
  if (!volume)
73737
73822
  return;
73738
- const channelVis = this.state.gui_states.layerChannel.channelVisibility[layer];
73739
- const slice = volume.getSliceUint8(sliceIndex, axis);
73740
- const W = slice.width;
73741
- const H = slice.height;
73742
73823
  const stride = volume.getChannels();
73743
- const labels = findLabelsInSlice(slice.data, W, H, stride, 0);
73744
- if (labels.length === 0)
73824
+ const cacheKey = `${axis}:${sliceIndex}:${volume.getVersion()}`;
73825
+ // Cache miss → run the expensive extraction once. Hits (zoom,
73826
+ // recomposite, contrast toggle) skip straight to the fill below.
73827
+ let entry = this._contourCache.get(layer);
73828
+ if (!entry || entry.key !== cacheKey) {
73829
+ const slice = volume.getSliceUint8(sliceIndex, axis);
73830
+ const W = slice.width;
73831
+ const H = slice.height;
73832
+ const labels = findLabelsInSlice(slice.data, W, H, stride, 0);
73833
+ const paths = new Map();
73834
+ for (const lbl of labels) {
73835
+ paths.set(lbl, extractLabelContours(slice.data, W, H, lbl, stride, 0));
73836
+ }
73837
+ entry = { key: cacheKey, W, H, labels, paths };
73838
+ this._contourCache.set(layer, entry);
73839
+ }
73840
+ if (entry.labels.length === 0)
73745
73841
  return;
73842
+ const { W, H, labels, paths } = entry;
73843
+ const channelVis = this.state.gui_states.layerChannel.channelVisibility[layer];
73746
73844
  targetCtx.save();
73747
73845
  // Vector fill — imageSmoothingEnabled is irrelevant here, but keep
73748
73846
  // it off to match the rest of the pipeline.
@@ -73759,8 +73857,10 @@ class RenderingUtils {
73759
73857
  for (const lbl of labels) {
73760
73858
  if (channelVis && channelVis[lbl] === false)
73761
73859
  continue;
73860
+ const path = paths.get(lbl);
73861
+ if (!path)
73862
+ continue;
73762
73863
  const color = volume.getChannelColor(lbl);
73763
- const path = extractLabelContours(slice.data, W, H, lbl, stride, 0);
73764
73864
  targetCtx.fillStyle =
73765
73865
  `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a / 255})`;
73766
73866
  targetCtx.fill(path, 'nonzero');
@@ -73778,6 +73878,7 @@ class RenderingUtils {
73778
73878
  this._reusableSliceBuffer = null;
73779
73879
  this._reusableBufferWidth = 0;
73780
73880
  this._reusableBufferHeight = 0;
73881
+ this._contourCache.clear();
73781
73882
  }
73782
73883
  /**
73783
73884
  * Apply the same flip transform used by flipDisplayImageByAxis() to any
@@ -74536,7 +74637,30 @@ class ZoomTool extends BaseTool {
74536
74637
  }
74537
74638
  // ===== Zoom Wheel =====
74538
74639
  configMouseZoomWheel() {
74539
- let moveDistance = 1;
74640
+ // Coalesce multiple wheel events fired within one frame into a single
74641
+ // resizePaintArea() call via requestAnimationFrame. Each resize triggers
74642
+ // a full display redraw (+ mask recomposite), so without coalescing a
74643
+ // fast scroll fires dozens of these per frame. The accumulation is done
74644
+ // on the logical zoom (sizeFactor) — not the DOM offsetWidth — so deltas
74645
+ // still compound correctly even though the DOM size hasn't updated yet.
74646
+ let rafId = null;
74647
+ let pending = null;
74648
+ const flush = () => {
74649
+ rafId = null;
74650
+ if (!pending)
74651
+ return;
74652
+ const p = pending;
74653
+ pending = null;
74654
+ if (p.recenter) {
74655
+ this.callbacks.resetPaintAreaUIPosition();
74656
+ }
74657
+ else {
74658
+ this.callbacks.resetPaintAreaUIPosition(p.l, p.t);
74659
+ }
74660
+ this.callbacks.resizePaintArea(p.moveDistance);
74661
+ this.callbacks.setIsDrawFalse(1000);
74662
+ this.ctx.nrrd_states.view.sizeFactor = p.moveDistance;
74663
+ };
74540
74664
  return (e) => {
74541
74665
  var _a, _b;
74542
74666
  if ((_a = this.ctx.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
@@ -74551,36 +74675,38 @@ class ZoomTool extends BaseTool {
74551
74675
  const delta = e.detail ? e.detail > 0 : e.wheelDelta < 0;
74552
74676
  this.ctx.protectedData.isDrawing = true;
74553
74677
  const rect = this.container.getBoundingClientRect();
74554
- const ratioL = (e.clientX -
74555
- rect.left -
74556
- this.mainAreaContainer.offsetLeft -
74557
- this.ctx.protectedData.canvases.drawingCanvas.offsetLeft) /
74558
- this.ctx.protectedData.canvases.drawingCanvas.offsetWidth;
74559
- const ratioT = (e.clientY -
74560
- rect.top -
74561
- this.mainAreaContainer.offsetTop -
74562
- this.ctx.protectedData.canvases.drawingCanvas.offsetTop) /
74563
- this.ctx.protectedData.canvases.drawingCanvas.offsetHeight;
74678
+ const drawingCanvas = this.ctx.protectedData.canvases.drawingCanvas;
74679
+ const ratioL = (e.clientX - rect.left - this.mainAreaContainer.offsetLeft - drawingCanvas.offsetLeft) /
74680
+ drawingCanvas.offsetWidth;
74681
+ const ratioT = (e.clientY - rect.top - this.mainAreaContainer.offsetTop - drawingCanvas.offsetTop) /
74682
+ drawingCanvas.offsetHeight;
74564
74683
  const ratioDelta = !delta ? 1 + 0.1 : 1 - 0.1;
74565
- const w = this.ctx.protectedData.canvases.drawingCanvas.offsetWidth * ratioDelta;
74566
- const h = this.ctx.protectedData.canvases.drawingCanvas.offsetHeight * ratioDelta;
74567
- const l = Math.round(e.clientX - this.mainAreaContainer.offsetLeft - w * ratioL - rect.left);
74568
- const t = Math.round(e.clientY - this.mainAreaContainer.offsetTop - h * ratioT - rect.top);
74569
- moveDistance = w / this.ctx.nrrd_states.image.originWidth;
74684
+ // Compound from the latest pending target (this frame) or the current
74685
+ // committed sizeFactor.
74686
+ const base = pending ? pending.moveDistance : this.ctx.nrrd_states.view.sizeFactor;
74687
+ let moveDistance = base * ratioDelta;
74570
74688
  if (moveDistance > 8) {
74689
+ // Max zoom: original behaviour clamps without re-laying out.
74571
74690
  moveDistance = 8;
74691
+ this.ctx.nrrd_states.view.sizeFactor = moveDistance;
74692
+ this.callbacks.setIsDrawFalse(1000);
74693
+ return;
74572
74694
  }
74573
- else if (moveDistance < 1) {
74695
+ if (moveDistance < 1) {
74574
74696
  moveDistance = 1;
74575
- this.callbacks.resetPaintAreaUIPosition();
74576
- this.callbacks.resizePaintArea(moveDistance);
74697
+ pending = { moveDistance, l: 0, t: 0, recenter: true };
74577
74698
  }
74578
74699
  else {
74579
- this.callbacks.resetPaintAreaUIPosition(l, t);
74580
- this.callbacks.resizePaintArea(moveDistance);
74700
+ // Target displayed size for this zoom level → keep cursor anchored.
74701
+ const w = this.ctx.nrrd_states.image.originWidth * moveDistance;
74702
+ const h = this.ctx.nrrd_states.image.originHeight * moveDistance;
74703
+ const l = Math.round(e.clientX - this.mainAreaContainer.offsetLeft - w * ratioL - rect.left);
74704
+ const t = Math.round(e.clientY - this.mainAreaContainer.offsetTop - h * ratioT - rect.top);
74705
+ pending = { moveDistance, l, t, recenter: false };
74706
+ }
74707
+ if (rafId === null) {
74708
+ rafId = requestAnimationFrame(flush);
74581
74709
  }
74582
- this.callbacks.setIsDrawFalse(1000);
74583
- this.ctx.nrrd_states.view.sizeFactor = moveDistance;
74584
74710
  };
74585
74711
  }
74586
74712
  }
@@ -79324,7 +79450,7 @@ function evaluateElement(element, weights) {
79324
79450
  }
79325
79451
 
79326
79452
  // import * as kiwrious from "copper3d_plugin_heart_k";
79327
- const REVISION = "v3.4.3-beta";
79453
+ const REVISION = "v3.4.5-beta";
79328
79454
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
79329
79455
 
79330
79456
  export { CHANNEL_COLORS, CHANNEL_HEX_COLORS, CameraViewPoint, Copper3dTrackballControls, GaussianSmoother, MeshNodeTool, NrrdTools, REVISION, addBoxHelper, addLabelToScene, configKiwriousHeart, convert3DPostoScreenPos, convertScreenPosto3DPos, copperMScene, copperMSceneRenderer, copperRenderer, copperRendererOnDemond, copperScene, copperSceneOnDemond, createTexture2D_NRRD, fullScreenListenner, kiwrious, loading, removeGuiFolderChilden, rgbaToCss, rgbaToHex, setHDRFilePath, throttle };