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.
@@ -70879,6 +70879,17 @@ void main() {
70879
70879
  * @throws {RangeError} If any dimension or the channel count is < 1.
70880
70880
  */
70881
70881
  constructor(width, height, depth, channels = 1, customColorMap) {
70882
+ /**
70883
+ * Monotonic data-mutation counter.
70884
+ *
70885
+ * Incremented by every method that mutates voxel data (not color).
70886
+ * Consumers (e.g. the contour cache in RenderingUtils) key cached
70887
+ * derivations on this value so an edit automatically invalidates them —
70888
+ * it is impossible to forget an invalidation point because every write
70889
+ * bumps the version. Color-map changes do **not** bump it (colors are
70890
+ * applied at fill time, never cached).
70891
+ */
70892
+ this.version = 0;
70882
70893
  if (width < 1 || height < 1 || depth < 1) {
70883
70894
  throw new RangeError(`Dimensions must be ≥ 1, got (${width}, ${height}, ${depth})`);
70884
70895
  }
@@ -70968,6 +70979,16 @@ void main() {
70968
70979
  */
70969
70980
  setVoxel(x, y, z, value, channel = 0) {
70970
70981
  this.data[this.getIndex(x, y, z, channel)] = value;
70982
+ this.version++;
70983
+ }
70984
+ /**
70985
+ * Return the current data-mutation version.
70986
+ *
70987
+ * Increments on every voxel write. Use as a cache key so derived data
70988
+ * (e.g. extracted contours) is invalidated whenever the volume changes.
70989
+ */
70990
+ getVersion() {
70991
+ return this.version;
70971
70992
  }
70972
70993
  // ── Color map management ──────────────────────────────────────────
70973
70994
  /**
@@ -71124,6 +71145,7 @@ void main() {
71124
71145
  throw new Error(`ImageData size mismatch: expected ${expectedW}×${expectedH}, ` +
71125
71146
  `got ${imageData.width}×${imageData.height}`);
71126
71147
  }
71148
+ this.version++;
71127
71149
  const pixels = imageData.data;
71128
71150
  const { width, height } = this.dims;
71129
71151
  const ch = this.numChannels;
@@ -71225,6 +71247,7 @@ void main() {
71225
71247
  throw new Error(`ImageData size mismatch: expected ${expectedW}×${expectedH}, ` +
71226
71248
  `got ${imageData.width}×${imageData.height}`);
71227
71249
  }
71250
+ this.version++;
71228
71251
  // Build RGB→channel lookup map for O(1) reverse lookup
71229
71252
  const rgbToChannel = this.buildRgbToChannelMap();
71230
71253
  const pixels = imageData.data;
@@ -71489,6 +71512,7 @@ void main() {
71489
71512
  throw new Error(`Data length mismatch: expected ${this.data.length}, got ${newData.length}`);
71490
71513
  }
71491
71514
  this.data = newData;
71515
+ this.version++;
71492
71516
  }
71493
71517
  /**
71494
71518
  * Create an independent deep copy of this volume.
@@ -71528,6 +71552,7 @@ void main() {
71528
71552
  */
71529
71553
  clear() {
71530
71554
  this.data.fill(0);
71555
+ this.version++;
71531
71556
  }
71532
71557
  /**
71533
71558
  * Check if the volume contains any non-zero voxel data.
@@ -71580,6 +71605,7 @@ void main() {
71580
71605
  */
71581
71606
  clearSlice(sliceIndex, axis = 'z', channel) {
71582
71607
  this.validateSliceIndex(sliceIndex, axis);
71608
+ this.version++;
71583
71609
  const [sliceW, sliceH] = this.getSliceDimensions(axis);
71584
71610
  const volData = this.data;
71585
71611
  const nch = this.numChannels;
@@ -71691,6 +71717,7 @@ void main() {
71691
71717
  */
71692
71718
  setSliceUint8(sliceIndex, data, axis = 'z') {
71693
71719
  this.validateSliceIndex(sliceIndex, axis);
71720
+ this.version++;
71694
71721
  const [sliceW, sliceH] = this.getSliceDimensions(axis);
71695
71722
  const nch = this.numChannels;
71696
71723
  const rowStride = this.dims.width * nch;
@@ -72134,10 +72161,18 @@ void main() {
72134
72161
  /* 15 (full) */ [[[0, 0], [1, 0], [1, 1], [0, 1]]],
72135
72162
  ];
72136
72163
  /**
72137
- * Emit all per-cell polygons covering the voxels where `labels === targetLabel`.
72164
+ * Emit polygons covering the voxels where `labels === targetLabel`.
72138
72165
  * Pure geometry function — no Canvas dependency. Consumers can build a Path2D
72139
72166
  * (see {@link extractLabelContours}) or walk the vertex arrays directly
72140
72167
  * (tests, export, stroke rendering).
72168
+ *
72169
+ * Boundary cells (marching-squares cases 1–14) emit their cut polygon as
72170
+ * usual. Solid interior (case 15) is **coalesced into horizontal run-length
72171
+ * rectangles** instead of one unit square per cell, collapsing the subpath
72172
+ * count from O(area) to ≈ O(perimeter + rows). The filled region is
72173
+ * pixel-identical (contiguous full-cell squares tile exactly into a rectangle
72174
+ * with the same TL→TR→BR→BL winding), which is what keeps slice-scrubbing
72175
+ * fast on large masks without changing the rendered silhouette.
72141
72176
  */
72142
72177
  function extractLabelPolygons(labels, width, height, targetLabel, stride = 1, channelOffset = 0, bbox) {
72143
72178
  var _a, _b, _c, _d;
@@ -72159,7 +72194,25 @@ void main() {
72159
72194
  // map to voxel-square CENTERS in render space. Voxel (i, j) occupies
72160
72195
  // render square [i, i+1] × [j, j+1]; its center is (i+0.5, j+0.5).
72161
72196
  const SHIFT = 0.5;
72197
+ // Emit a single rectangle covering a contiguous run of full (code 15)
72198
+ // cells [runStart .. runEnd] on row j. A full cell i covers render square
72199
+ // [i+0.5, i+1.5] × [j+0.5, j+1.5]; consecutive full cells tile perfectly,
72200
+ // so the run is pixel-identical to the per-cell squares it replaces.
72201
+ // Vertices use the same TL→TR→BR→BL (CCW screen-space) winding as the
72202
+ // case-15 unit square, so the nonzero fill matches at shared edges.
72203
+ const pushFullRun = (runStart, runEnd, j) => {
72204
+ const x0 = runStart + SHIFT;
72205
+ const x1 = runEnd + 1 + SHIFT;
72206
+ const y0 = j + SHIFT;
72207
+ const y1 = j + 1 + SHIFT;
72208
+ out.push([[x0, y0], [x1, y0], [x1, y1], [x0, y1]]);
72209
+ };
72162
72210
  for (let j = j0; j < j1; j++) {
72211
+ // Index where the current contiguous run of full (code 15) cells began,
72212
+ // or -1 when no run is open. Coalescing the solid interior into row runs
72213
+ // drops the per-cell square count from O(area) to O(rows + perimeter),
72214
+ // which is what keeps fast slice-scrubbing smooth on large masks.
72215
+ let runStart = -1;
72163
72216
  for (let i = i0; i < i1; i++) {
72164
72217
  const tl = sample(i, j);
72165
72218
  const tr = sample(i + 1, j);
@@ -72169,6 +72222,17 @@ void main() {
72169
72222
  (tr ? 4 : 0) |
72170
72223
  (br ? 2 : 0) |
72171
72224
  (bl ? 1 : 0);
72225
+ if (code === 15) {
72226
+ // Extend (or open) the current full-cell run; defer emission.
72227
+ if (runStart === -1)
72228
+ runStart = i;
72229
+ continue;
72230
+ }
72231
+ // Non-full cell ends any open run — flush it as one rectangle.
72232
+ if (runStart !== -1) {
72233
+ pushFullRun(runStart, i - 1, j);
72234
+ runStart = -1;
72235
+ }
72172
72236
  if (code === 0)
72173
72237
  continue;
72174
72238
  const polygons = CELL_POLYGONS[code];
@@ -72181,6 +72245,10 @@ void main() {
72181
72245
  out.push(abs);
72182
72246
  }
72183
72247
  }
72248
+ // Flush a run that reached the end of the row (last visited cell is i1-1).
72249
+ if (runStart !== -1) {
72250
+ pushFullRun(runStart, i1 - 1, j);
72251
+ }
72184
72252
  }
72185
72253
  return out;
72186
72254
  }
@@ -73645,6 +73713,23 @@ void main() {
73645
73713
  this._reusableSliceBuffer = null;
73646
73714
  this._reusableBufferWidth = 0;
73647
73715
  this._reusableBufferHeight = 0;
73716
+ /**
73717
+ * Per-layer contour cache.
73718
+ *
73719
+ * Caches the *expensive* part of slice rendering — `getSliceUint8` +
73720
+ * `findLabelsInSlice` + `extractLabelContours` (Path2D build) — keyed by
73721
+ * `${axis}:${sliceIndex}:${volume.version}`. The contours live in voxel
73722
+ * coordinates; zoom only changes the `ctx.scale` transform, so on zoom /
73723
+ * recomposite we reuse the cached Path2D and just re-fill, skipping
73724
+ * marching-squares entirely. A volume edit bumps its version → cache miss
73725
+ * → recompute (correct, automatic). Colors and channel visibility are
73726
+ * applied at fill time and intentionally *not* cached, so toggling them
73727
+ * needs no recompute.
73728
+ *
73729
+ * One entry per layer (the current slice). Switching slice/axis or
73730
+ * editing overwrites it.
73731
+ */
73732
+ this._contourCache = new Map();
73648
73733
  this.state = state;
73649
73734
  }
73650
73735
  // ── Volume Accessor Helpers ──────────────────────────────────────
@@ -73743,14 +73828,27 @@ void main() {
73743
73828
  const volume = this.getVolumeForLayer(layer);
73744
73829
  if (!volume)
73745
73830
  return;
73746
- const channelVis = this.state.gui_states.layerChannel.channelVisibility[layer];
73747
- const slice = volume.getSliceUint8(sliceIndex, axis);
73748
- const W = slice.width;
73749
- const H = slice.height;
73750
73831
  const stride = volume.getChannels();
73751
- const labels = findLabelsInSlice(slice.data, W, H, stride, 0);
73752
- if (labels.length === 0)
73832
+ const cacheKey = `${axis}:${sliceIndex}:${volume.getVersion()}`;
73833
+ // Cache miss → run the expensive extraction once. Hits (zoom,
73834
+ // recomposite, contrast toggle) skip straight to the fill below.
73835
+ let entry = this._contourCache.get(layer);
73836
+ if (!entry || entry.key !== cacheKey) {
73837
+ const slice = volume.getSliceUint8(sliceIndex, axis);
73838
+ const W = slice.width;
73839
+ const H = slice.height;
73840
+ const labels = findLabelsInSlice(slice.data, W, H, stride, 0);
73841
+ const paths = new Map();
73842
+ for (const lbl of labels) {
73843
+ paths.set(lbl, extractLabelContours(slice.data, W, H, lbl, stride, 0));
73844
+ }
73845
+ entry = { key: cacheKey, W, H, labels, paths };
73846
+ this._contourCache.set(layer, entry);
73847
+ }
73848
+ if (entry.labels.length === 0)
73753
73849
  return;
73850
+ const { W, H, labels, paths } = entry;
73851
+ const channelVis = this.state.gui_states.layerChannel.channelVisibility[layer];
73754
73852
  targetCtx.save();
73755
73853
  // Vector fill — imageSmoothingEnabled is irrelevant here, but keep
73756
73854
  // it off to match the rest of the pipeline.
@@ -73767,8 +73865,10 @@ void main() {
73767
73865
  for (const lbl of labels) {
73768
73866
  if (channelVis && channelVis[lbl] === false)
73769
73867
  continue;
73868
+ const path = paths.get(lbl);
73869
+ if (!path)
73870
+ continue;
73770
73871
  const color = volume.getChannelColor(lbl);
73771
- const path = extractLabelContours(slice.data, W, H, lbl, stride, 0);
73772
73872
  targetCtx.fillStyle =
73773
73873
  `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a / 255})`;
73774
73874
  targetCtx.fill(path, 'nonzero');
@@ -73786,6 +73886,7 @@ void main() {
73786
73886
  this._reusableSliceBuffer = null;
73787
73887
  this._reusableBufferWidth = 0;
73788
73888
  this._reusableBufferHeight = 0;
73889
+ this._contourCache.clear();
73789
73890
  }
73790
73891
  /**
73791
73892
  * Apply the same flip transform used by flipDisplayImageByAxis() to any
@@ -74544,7 +74645,30 @@ void main() {
74544
74645
  }
74545
74646
  // ===== Zoom Wheel =====
74546
74647
  configMouseZoomWheel() {
74547
- let moveDistance = 1;
74648
+ // Coalesce multiple wheel events fired within one frame into a single
74649
+ // resizePaintArea() call via requestAnimationFrame. Each resize triggers
74650
+ // a full display redraw (+ mask recomposite), so without coalescing a
74651
+ // fast scroll fires dozens of these per frame. The accumulation is done
74652
+ // on the logical zoom (sizeFactor) — not the DOM offsetWidth — so deltas
74653
+ // still compound correctly even though the DOM size hasn't updated yet.
74654
+ let rafId = null;
74655
+ let pending = null;
74656
+ const flush = () => {
74657
+ rafId = null;
74658
+ if (!pending)
74659
+ return;
74660
+ const p = pending;
74661
+ pending = null;
74662
+ if (p.recenter) {
74663
+ this.callbacks.resetPaintAreaUIPosition();
74664
+ }
74665
+ else {
74666
+ this.callbacks.resetPaintAreaUIPosition(p.l, p.t);
74667
+ }
74668
+ this.callbacks.resizePaintArea(p.moveDistance);
74669
+ this.callbacks.setIsDrawFalse(1000);
74670
+ this.ctx.nrrd_states.view.sizeFactor = p.moveDistance;
74671
+ };
74548
74672
  return (e) => {
74549
74673
  var _a, _b;
74550
74674
  if ((_a = this.ctx.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
@@ -74559,36 +74683,38 @@ void main() {
74559
74683
  const delta = e.detail ? e.detail > 0 : e.wheelDelta < 0;
74560
74684
  this.ctx.protectedData.isDrawing = true;
74561
74685
  const rect = this.container.getBoundingClientRect();
74562
- const ratioL = (e.clientX -
74563
- rect.left -
74564
- this.mainAreaContainer.offsetLeft -
74565
- this.ctx.protectedData.canvases.drawingCanvas.offsetLeft) /
74566
- this.ctx.protectedData.canvases.drawingCanvas.offsetWidth;
74567
- const ratioT = (e.clientY -
74568
- rect.top -
74569
- this.mainAreaContainer.offsetTop -
74570
- this.ctx.protectedData.canvases.drawingCanvas.offsetTop) /
74571
- this.ctx.protectedData.canvases.drawingCanvas.offsetHeight;
74686
+ const drawingCanvas = this.ctx.protectedData.canvases.drawingCanvas;
74687
+ const ratioL = (e.clientX - rect.left - this.mainAreaContainer.offsetLeft - drawingCanvas.offsetLeft) /
74688
+ drawingCanvas.offsetWidth;
74689
+ const ratioT = (e.clientY - rect.top - this.mainAreaContainer.offsetTop - drawingCanvas.offsetTop) /
74690
+ drawingCanvas.offsetHeight;
74572
74691
  const ratioDelta = !delta ? 1 + 0.1 : 1 - 0.1;
74573
- const w = this.ctx.protectedData.canvases.drawingCanvas.offsetWidth * ratioDelta;
74574
- const h = this.ctx.protectedData.canvases.drawingCanvas.offsetHeight * ratioDelta;
74575
- const l = Math.round(e.clientX - this.mainAreaContainer.offsetLeft - w * ratioL - rect.left);
74576
- const t = Math.round(e.clientY - this.mainAreaContainer.offsetTop - h * ratioT - rect.top);
74577
- moveDistance = w / this.ctx.nrrd_states.image.originWidth;
74692
+ // Compound from the latest pending target (this frame) or the current
74693
+ // committed sizeFactor.
74694
+ const base = pending ? pending.moveDistance : this.ctx.nrrd_states.view.sizeFactor;
74695
+ let moveDistance = base * ratioDelta;
74578
74696
  if (moveDistance > 8) {
74697
+ // Max zoom: original behaviour clamps without re-laying out.
74579
74698
  moveDistance = 8;
74699
+ this.ctx.nrrd_states.view.sizeFactor = moveDistance;
74700
+ this.callbacks.setIsDrawFalse(1000);
74701
+ return;
74580
74702
  }
74581
- else if (moveDistance < 1) {
74703
+ if (moveDistance < 1) {
74582
74704
  moveDistance = 1;
74583
- this.callbacks.resetPaintAreaUIPosition();
74584
- this.callbacks.resizePaintArea(moveDistance);
74705
+ pending = { moveDistance, l: 0, t: 0, recenter: true };
74585
74706
  }
74586
74707
  else {
74587
- this.callbacks.resetPaintAreaUIPosition(l, t);
74588
- this.callbacks.resizePaintArea(moveDistance);
74708
+ // Target displayed size for this zoom level → keep cursor anchored.
74709
+ const w = this.ctx.nrrd_states.image.originWidth * moveDistance;
74710
+ const h = this.ctx.nrrd_states.image.originHeight * moveDistance;
74711
+ const l = Math.round(e.clientX - this.mainAreaContainer.offsetLeft - w * ratioL - rect.left);
74712
+ const t = Math.round(e.clientY - this.mainAreaContainer.offsetTop - h * ratioT - rect.top);
74713
+ pending = { moveDistance, l, t, recenter: false };
74714
+ }
74715
+ if (rafId === null) {
74716
+ rafId = requestAnimationFrame(flush);
74589
74717
  }
74590
- this.callbacks.setIsDrawFalse(1000);
74591
- this.ctx.nrrd_states.view.sizeFactor = moveDistance;
74592
74718
  };
74593
74719
  }
74594
74720
  }
@@ -79332,7 +79458,7 @@ void main() {
79332
79458
  }
79333
79459
 
79334
79460
  // import * as kiwrious from "copper3d_plugin_heart_k";
79335
- const REVISION = "v3.4.3-beta";
79461
+ const REVISION = "v3.4.5-beta";
79336
79462
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
79337
79463
 
79338
79464
  exports.CHANNEL_COLORS = CHANNEL_COLORS;
package/dist/index.d.ts CHANGED
@@ -25,6 +25,6 @@ import type { ToolMode, IAnnotationCallbacks } from "./Utils/segmentation/core/t
25
25
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
26
26
  import type { LayerId, ChannelValue } from "./Utils/segmentation/core/index";
27
27
  import "./css/style.css";
28
- export declare const REVISION = "v3.4.3-beta";
28
+ export declare const REVISION = "v3.4.5-beta";
29
29
  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, rgbaToHex, rgbaToCss, GaussianSmoother, };
30
30
  export type { positionType, screenPosType, optsType, nrrdMeshesType, nrrdSliceType, SensorDecodedValue_kiwrious, SensorReadResult_kiwrious, HeartRateResult_kiwrious, loadingBarType, IPaintImage, exportPaintImageType, IOptVTKLoader, ICommXYZ, IGUIStates, IGuiParameterSettings, INrrdStates, NrrdState, GuiState, IGuiMeta, ToolMode, IAnnotationCallbacks, LayerId, ChannelValue, };
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import { MeshNodeTool } from "./Utils/MeshNodeTool";
21
21
  import { removeGuiFolderChilden } from "./Utils/segmentation/coreTools/gui";
22
22
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
23
23
  import "./css/style.css";
24
- export const REVISION = "v3.4.3-beta";
24
+ export const REVISION = "v3.4.5-beta";
25
25
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
26
26
  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, rgbaToHex, rgbaToCss, GaussianSmoother, };
27
27
  //# sourceMappingURL=index.js.map
@@ -18,6 +18,23 @@ export declare class RenderingUtils {
18
18
  private _reusableSliceBuffer;
19
19
  private _reusableBufferWidth;
20
20
  private _reusableBufferHeight;
21
+ /**
22
+ * Per-layer contour cache.
23
+ *
24
+ * Caches the *expensive* part of slice rendering — `getSliceUint8` +
25
+ * `findLabelsInSlice` + `extractLabelContours` (Path2D build) — keyed by
26
+ * `${axis}:${sliceIndex}:${volume.version}`. The contours live in voxel
27
+ * coordinates; zoom only changes the `ctx.scale` transform, so on zoom /
28
+ * recomposite we reuse the cached Path2D and just re-fill, skipping
29
+ * marching-squares entirely. A volume edit bumps its version → cache miss
30
+ * → recompute (correct, automatic). Colors and channel visibility are
31
+ * applied at fill time and intentionally *not* cached, so toggling them
32
+ * needs no recompute.
33
+ *
34
+ * One entry per layer (the current slice). Switching slice/axis or
35
+ * editing overwrites it.
36
+ */
37
+ private _contourCache;
21
38
  constructor(state: CanvasState);
22
39
  /**
23
40
  * Get MaskVolume for a specific layer
@@ -35,10 +35,18 @@ export interface ContourBBox {
35
35
  /** A single polygon's vertices in voxel-space coordinates. */
36
36
  export type ContourPolygon = ReadonlyArray<readonly [number, number]>;
37
37
  /**
38
- * Emit all per-cell polygons covering the voxels where `labels === targetLabel`.
38
+ * Emit polygons covering the voxels where `labels === targetLabel`.
39
39
  * Pure geometry function — no Canvas dependency. Consumers can build a Path2D
40
40
  * (see {@link extractLabelContours}) or walk the vertex arrays directly
41
41
  * (tests, export, stroke rendering).
42
+ *
43
+ * Boundary cells (marching-squares cases 1–14) emit their cut polygon as
44
+ * usual. Solid interior (case 15) is **coalesced into horizontal run-length
45
+ * rectangles** instead of one unit square per cell, collapsing the subpath
46
+ * count from O(area) to ≈ O(perimeter + rows). The filled region is
47
+ * pixel-identical (contiguous full-cell squares tile exactly into a rectangle
48
+ * with the same TL→TR→BR→BL winding), which is what keeps slice-scrubbing
49
+ * fast on large masks without changing the rendered silhouette.
42
50
  */
43
51
  export declare function extractLabelPolygons(labels: Uint8Array, width: number, height: number, targetLabel: number, stride?: number, channelOffset?: number, bbox?: ContourBBox): ContourPolygon[];
44
52
  /**
@@ -45,6 +45,17 @@ export declare class MaskVolume {
45
45
  private readonly bytesPerSlice;
46
46
  /** Per-channel color map used for colored rendering modes. */
47
47
  private colorMap;
48
+ /**
49
+ * Monotonic data-mutation counter.
50
+ *
51
+ * Incremented by every method that mutates voxel data (not color).
52
+ * Consumers (e.g. the contour cache in RenderingUtils) key cached
53
+ * derivations on this value so an edit automatically invalidates them —
54
+ * it is impossible to forget an invalidation point because every write
55
+ * bumps the version. Color-map changes do **not** bump it (colors are
56
+ * applied at fill time, never cached).
57
+ */
58
+ private version;
48
59
  /**
49
60
  * Create a new MaskVolume.
50
61
  *
@@ -105,6 +116,13 @@ export declare class MaskVolume {
105
116
  * @throws {RangeError} If any coordinate is out of bounds.
106
117
  */
107
118
  setVoxel(x: number, y: number, z: number, value: number, channel?: number): void;
119
+ /**
120
+ * Return the current data-mutation version.
121
+ *
122
+ * Increments on every voxel write. Use as a cache key so derived data
123
+ * (e.g. extracted contours) is invalidated whenever the volume changes.
124
+ */
125
+ getVersion(): number;
108
126
  /**
109
127
  * Update the color for a specific label/channel in the color map.
110
128
  *
@@ -25,6 +25,6 @@ import type { ToolMode, IAnnotationCallbacks } from "./Utils/segmentation/core/t
25
25
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
26
26
  import type { LayerId, ChannelValue } from "./Utils/segmentation/core/index";
27
27
  import "./css/style.css";
28
- export declare const REVISION = "v3.4.3-beta";
28
+ export declare const REVISION = "v3.4.5-beta";
29
29
  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, rgbaToHex, rgbaToCss, GaussianSmoother, };
30
30
  export type { positionType, screenPosType, optsType, nrrdMeshesType, nrrdSliceType, SensorDecodedValue_kiwrious, SensorReadResult_kiwrious, HeartRateResult_kiwrious, loadingBarType, IPaintImage, exportPaintImageType, IOptVTKLoader, ICommXYZ, IGUIStates, IGuiParameterSettings, INrrdStates, NrrdState, GuiState, IGuiMeta, ToolMode, IAnnotationCallbacks, LayerId, ChannelValue, };
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.4.3",
4
+ "version": "3.4.5",
5
5
  "main": "dist/bundle.umd.js",
6
6
  "moudle": "dist/bundle.esm.js",
7
7
  "types": "dist/types/index.d.ts",