copper3d 3.0.0 → 3.0.1

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.
Files changed (45) hide show
  1. package/dist/Utils/segmentation/CommToolsData.d.ts +9 -10
  2. package/dist/Utils/segmentation/CommToolsData.js +4 -13
  3. package/dist/Utils/segmentation/CommToolsData.js.map +1 -1
  4. package/dist/Utils/segmentation/DragOperator.js +4 -4
  5. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  6. package/dist/Utils/segmentation/DrawToolCore.d.ts +5 -6
  7. package/dist/Utils/segmentation/DrawToolCore.js +57 -55
  8. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  9. package/dist/Utils/segmentation/NrrdTools.js +6 -2
  10. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  11. package/dist/Utils/segmentation/core/MaskVolume.d.ts +0 -33
  12. package/dist/Utils/segmentation/core/MaskVolume.js +0 -121
  13. package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -1
  14. package/dist/Utils/segmentation/coreTools/coreType.d.ts +0 -3
  15. package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
  16. package/dist/Utils/segmentation/coreTools/gui.d.ts +6 -3
  17. package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
  18. package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +1 -0
  19. package/dist/Utils/segmentation/eventRouter/EventRouter.js +9 -3
  20. package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -1
  21. package/dist/Utils/segmentation/tools/BaseTool.d.ts +3 -0
  22. package/dist/Utils/segmentation/tools/BaseTool.js.map +1 -1
  23. package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +10 -13
  24. package/dist/Utils/segmentation/tools/ImageStoreHelper.js +13 -27
  25. package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -1
  26. package/dist/Utils/segmentation/tools/SphereTool.d.ts +1 -3
  27. package/dist/Utils/segmentation/tools/SphereTool.js +1 -1
  28. package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -1
  29. package/dist/Utils/segmentation/tools/ZoomTool.js +2 -1
  30. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -1
  31. package/dist/bundle.esm.js +97 -228
  32. package/dist/bundle.umd.js +97 -228
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/types/Utils/segmentation/CommToolsData.d.ts +9 -10
  36. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +5 -6
  37. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +0 -33
  38. package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +0 -3
  39. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +6 -3
  40. package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +1 -0
  41. package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +3 -0
  42. package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +10 -13
  43. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +1 -3
  44. package/dist/types/index.d.ts +1 -1
  45. package/package.json +1 -1
@@ -71146,127 +71146,6 @@ void main() {
71146
71146
  }
71147
71147
  }
71148
71148
  }
71149
- /**
71150
- * Extract a 2D slice as raw RGBA ImageData (lossless round-trip).
71151
- *
71152
- * Requires the volume to have **≥ 4 channels** (ch0=R, ch1=G, ch2=B, ch3=A).
71153
- * Returns the exact pixel data that was stored via `setSliceFromImageData`,
71154
- * bypassing all render modes.
71155
- *
71156
- * @param sliceIndex Index along the specified axis.
71157
- * @param axis `'x'`, `'y'`, or `'z'` (default `'z'`).
71158
- * @returns ImageData with the stored RGBA values.
71159
- *
71160
- * @throws {RangeError} If sliceIndex is out of bounds.
71161
- * @throws {Error} If the volume has fewer than 4 channels.
71162
- */
71163
- getSliceRawImageData(sliceIndex, axis = 'z') {
71164
- if (this.numChannels < 4) {
71165
- throw new Error(`getSliceRawImageData requires ≥ 4 channels, volume has ${this.numChannels}`);
71166
- }
71167
- this.validateSliceIndex(sliceIndex, axis);
71168
- const { width, height } = this.dims;
71169
- const ch = this.numChannels;
71170
- const [sliceWidth, sliceHeight] = this.getSliceDimensions(axis);
71171
- const imageData = new ImageData(sliceWidth, sliceHeight);
71172
- const pixels = imageData.data;
71173
- const volData = this.data;
71174
- if (axis === 'z') {
71175
- // Z-axis (axial): slice data is contiguous in memory — direct bulk copy
71176
- const offset = sliceIndex * this.bytesPerSlice;
71177
- pixels.set(volData.subarray(offset, offset + this.bytesPerSlice));
71178
- }
71179
- else if (axis === 'y') {
71180
- // Y-axis (coronal): each row (fixed z, fixed y, varying x) is contiguous
71181
- // mapping: (i → x, sliceIndex → y, j → z)
71182
- const rowBytes = width * ch;
71183
- let px = 0;
71184
- for (let j = 0; j < sliceHeight; j++) {
71185
- const rowStart = j * height * width * ch + sliceIndex * width * ch;
71186
- pixels.set(volData.subarray(rowStart, rowStart + rowBytes), px);
71187
- px += rowBytes;
71188
- }
71189
- }
71190
- else {
71191
- // X-axis (sagittal): pixels not contiguous, but avoid function call overhead
71192
- // After dimension fix: sliceWidth = depth (Z), sliceHeight = height (Y)
71193
- // j → y (rows), i → z (columns)
71194
- let px = 0;
71195
- for (let j = 0; j < sliceHeight; j++) {
71196
- const yOffset = j * width * ch;
71197
- for (let i = 0; i < sliceWidth; i++) {
71198
- const baseIdx = i * height * width * ch + yOffset + sliceIndex * ch;
71199
- pixels[px] = volData[baseIdx];
71200
- pixels[px + 1] = volData[baseIdx + 1];
71201
- pixels[px + 2] = volData[baseIdx + 2];
71202
- pixels[px + 3] = volData[baseIdx + 3];
71203
- px += 4;
71204
- }
71205
- }
71206
- }
71207
- return imageData;
71208
- }
71209
- /**
71210
- * Write slice data into an existing ImageData buffer (zero-allocation).
71211
- *
71212
- * Same semantics as {@link getSliceRawImageData} but avoids creating a new
71213
- * ImageData object on every call. The caller is responsible for providing
71214
- * a buffer whose dimensions match the expected slice size.
71215
- *
71216
- * All pixels are fully overwritten — no clearing is needed beforehand.
71217
- *
71218
- * @param sliceIndex Index along the specified axis.
71219
- * @param axis `'x'`, `'y'`, or `'z'` (default `'z'`).
71220
- * @param target Pre-allocated ImageData to write into.
71221
- *
71222
- * @throws {Error} If the volume has fewer than 4 channels.
71223
- * @throws {RangeError} If sliceIndex is out of bounds.
71224
- * @throws {Error} If target dimensions don't match the slice.
71225
- */
71226
- getSliceRawImageDataInto(sliceIndex, axis = 'z', target) {
71227
- if (this.numChannels < 4) {
71228
- throw new Error(`getSliceRawImageDataInto requires ≥ 4 channels, volume has ${this.numChannels}`);
71229
- }
71230
- this.validateSliceIndex(sliceIndex, axis);
71231
- const [sliceWidth, sliceHeight] = this.getSliceDimensions(axis);
71232
- if (target.width !== sliceWidth || target.height !== sliceHeight) {
71233
- throw new Error(`Buffer size mismatch: expected ${sliceWidth}×${sliceHeight}, ` +
71234
- `got ${target.width}×${target.height}`);
71235
- }
71236
- const { width, height } = this.dims;
71237
- const ch = this.numChannels;
71238
- const pixels = target.data;
71239
- const volData = this.data;
71240
- if (axis === 'z') {
71241
- const offset = sliceIndex * this.bytesPerSlice;
71242
- pixels.set(volData.subarray(offset, offset + this.bytesPerSlice));
71243
- }
71244
- else if (axis === 'y') {
71245
- const rowBytes = width * ch;
71246
- let px = 0;
71247
- for (let j = 0; j < sliceHeight; j++) {
71248
- const rowStart = j * height * width * ch + sliceIndex * width * ch;
71249
- pixels.set(volData.subarray(rowStart, rowStart + rowBytes), px);
71250
- px += rowBytes;
71251
- }
71252
- }
71253
- else {
71254
- // X-axis (sagittal): sliceWidth = depth (Z), sliceHeight = height (Y)
71255
- // j → y (rows), i → z (columns)
71256
- let px = 0;
71257
- for (let j = 0; j < sliceHeight; j++) {
71258
- const yOffset = j * width * ch;
71259
- for (let i = 0; i < sliceWidth; i++) {
71260
- const baseIdx = i * height * width * ch + yOffset + sliceIndex * ch;
71261
- pixels[px] = volData[baseIdx];
71262
- pixels[px + 1] = volData[baseIdx + 1];
71263
- pixels[px + 2] = volData[baseIdx + 2];
71264
- pixels[px + 3] = volData[baseIdx + 3];
71265
- px += 4;
71266
- }
71267
- }
71268
- }
71269
- }
71270
71149
  // ── Label-based storage (1-channel volumes) ────────────────────────
71271
71150
  /**
71272
71151
  /**
@@ -72789,12 +72668,12 @@ void main() {
72789
72668
  this.eventRouter.subscribeModeChange((prevMode, newMode) => {
72790
72669
  const prev = prevMode;
72791
72670
  const next = newMode;
72792
- // When entering draw or contrast mode, remove drag mode
72793
- if (next === 'draw' || next === 'contrast') {
72671
+ // When entering draw, contrast, or crosshair mode, remove drag mode
72672
+ if (next === 'draw' || next === 'contrast' || next === 'crosshair') {
72794
72673
  this.removeDragMode();
72795
72674
  }
72796
- // When leaving draw or contrast mode (returning to idle), restore drag mode
72797
- if ((prev === 'draw' || prev === 'contrast') && next === 'idle') {
72675
+ // When leaving draw, contrast, or crosshair mode (returning to idle), restore drag mode
72676
+ if ((prev === 'draw' || prev === 'contrast' || prev === 'crosshair') && next === 'idle') {
72798
72677
  if (!this.gui_states.sphere) {
72799
72678
  this.configDragMode();
72800
72679
  }
@@ -72897,7 +72776,6 @@ void main() {
72897
72776
  ratios: { x: 1, y: 1, z: 1 },
72898
72777
  contrastNum: 0,
72899
72778
  showContrast: false,
72900
- enableCursorChoose: false,
72901
72779
  isCursorSelect: false,
72902
72780
  cursorPageX: 0,
72903
72781
  cursorPageY: 0,
@@ -73037,8 +72915,6 @@ void main() {
73037
72915
  skipSlicesDic: {},
73038
72916
  currentShowingSlice: undefined,
73039
72917
  mainPreSlices: undefined,
73040
- Is_Shift_Pressed: false,
73041
- Is_Ctrl_Pressed: false,
73042
72918
  Is_Draw: false,
73043
72919
  axis: "z",
73044
72920
  maskData: {
@@ -73188,12 +73064,6 @@ void main() {
73188
73064
  clearStoreImages() {
73189
73065
  throw new Error("Child class must implement abstract clearStoreImages, currently you can find it in NrrdTools.");
73190
73066
  }
73191
- /**
73192
- * Rewrite this {createEmptyPaintImage} function under NrrdTools
73193
- */
73194
- createEmptyPaintImage(dimensions, paintImages) {
73195
- throw new Error("Child class must implement abstract clearStoreImages, currently you can find it in NrrdTools.");
73196
- }
73197
73067
  /**
73198
73068
  * Rewrite this {resizePaintArea} function under NrrdTools
73199
73069
  */
@@ -73255,13 +73125,13 @@ void main() {
73255
73125
  throw new Error("Child class must implement abstract redrawDisplayCanvas, currently you can find it in NrrdTools.");
73256
73126
  }
73257
73127
  /**
73258
- * Get a painted mask image (IPaintImage) based on current axis and input slice index.
73128
+ * Get a painted mask image based on current axis and input slice index.
73259
73129
  *
73260
- * Phase 3: Reads directly from MaskVolume (no caching needed — reads are fast).
73130
+ * Phase 3: Reads directly from MaskVolume.
73261
73131
  *
73262
73132
  * @param axis "x" | "y" | "z"
73263
73133
  * @param sliceIndex number
73264
- * @returns IPaintImage with the mask for the given slice, or undefined if not found
73134
+ * @returns Object with index and image, or undefined
73265
73135
  */
73266
73136
  filterDrawedImage(axis, sliceIndex) {
73267
73137
  try {
@@ -73318,7 +73188,7 @@ void main() {
73318
73188
  /**
73319
73189
  * Render a layer's slice into a reusable buffer and draw to the target canvas.
73320
73190
  *
73321
- * Uses MaskVolume.getSliceRawImageDataInto() for zero-allocation rendering.
73191
+ * Uses MaskVolume.renderLabelSliceInto() for zero-allocation rendering.
73322
73192
  * The caller should obtain the buffer via getOrCreateSliceBuffer() and reuse
73323
73193
  * it across multiple layer renders.
73324
73194
  *
@@ -73574,11 +73444,13 @@ void main() {
73574
73444
  }
73575
73445
  /**
73576
73446
  * Toggle crosshair mode.
73447
+ * Blocked when draw or contrast mode is active (mutual exclusion).
73577
73448
  */
73578
73449
  toggleCrosshair() {
73579
73450
  if (!DRAWING_TOOLS.has(this.guiTool))
73580
73451
  return;
73581
- if (this.state.shiftHeld || this.state.ctrlHeld)
73452
+ // Block crosshair activation during draw or contrast
73453
+ if (this.state.shiftHeld || this.mode === 'draw' || this.mode === 'contrast')
73582
73454
  return;
73583
73455
  this.state.crosshairEnabled = !this.state.crosshairEnabled;
73584
73456
  this.setMode(this.state.crosshairEnabled ? 'crosshair' : 'idle');
@@ -73697,12 +73569,16 @@ void main() {
73697
73569
  // Update state based on modifier keys
73698
73570
  if (ev.key === this.keyboardSettings.draw) {
73699
73571
  this.state.shiftHeld = true;
73700
- if (DRAWING_TOOLS.has(this.guiTool) && !this.state.ctrlHeld) {
73572
+ // Block draw mode when crosshair or contrast is active (mutual exclusion)
73573
+ if (DRAWING_TOOLS.has(this.guiTool) && !this.state.ctrlHeld && !this.state.crosshairEnabled) {
73701
73574
  this.setMode('draw');
73702
73575
  }
73703
73576
  }
73704
73577
  if (this.contrastEnabled && this.keyboardSettings.contrast.includes(ev.key)) {
73705
- this.state.ctrlHeld = true;
73578
+ // Block contrast state when crosshair or draw is active (mutual exclusion)
73579
+ if (!this.state.crosshairEnabled && this.mode !== 'draw') {
73580
+ this.state.ctrlHeld = true;
73581
+ }
73706
73582
  }
73707
73583
  // Route to external handler
73708
73584
  if (this.keydownHandler) {
@@ -73862,7 +73738,7 @@ void main() {
73862
73738
  this.callbacks.setEmptyCanvasSize(axis);
73863
73739
  this.callbacks.drawImageOnEmptyImage(this.ctx.protectedData.canvases.drawingSphereCanvas);
73864
73740
  const imageData = this.ctx.protectedData.ctxes.emptyCtx.getImageData(0, 0, this.ctx.protectedData.canvases.emptyCanvas.width, this.ctx.protectedData.canvases.emptyCanvas.height);
73865
- this.callbacks.storeImageToAxis(index, { x: [], y: [], z: [] }, imageData, axis);
73741
+ this.callbacks.storeImageToAxis(index, imageData, axis);
73866
73742
  }
73867
73743
  // ===== Multi-View Sphere =====
73868
73744
  drawSphereOnEachViews(decay, axis) {
@@ -74259,7 +74135,8 @@ void main() {
74259
74135
  configMouseZoomWheel() {
74260
74136
  let moveDistance = 1;
74261
74137
  return (e) => {
74262
- if (this.ctx.protectedData.Is_Shift_Pressed) {
74138
+ var _a;
74139
+ if ((_a = this.ctx.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
74263
74140
  return;
74264
74141
  }
74265
74142
  e.preventDefault();
@@ -74396,8 +74273,7 @@ void main() {
74396
74273
  * Extracted from DrawToolCore.ts:
74397
74274
  * - storeAllImages / storeImageToAxis / storeImageToLayer / storeEachLayerImage
74398
74275
  *
74399
- * Phase 2 Day 7: Updated to write/read MaskVolume alongside legacy IPaintImages.
74400
- * Volume is the primary storage; IPaintImages kept for backward compatibility.
74276
+ * Phase 3: MaskVolume is the sole storage backend. All IPaintImages params removed.
74401
74277
  */
74402
74278
  class ImageStoreHelper extends BaseTool {
74403
74279
  constructor(ctx, callbacks) {
@@ -74414,12 +74290,12 @@ void main() {
74414
74290
  */
74415
74291
  getVolumeForLayer(layer) {
74416
74292
  const { volumes } = this.ctx.protectedData.maskData;
74417
- switch (layer) {
74418
- case "layer1": return volumes.layer1;
74419
- case "layer2": return volumes.layer2;
74420
- case "layer3": return volumes.layer3;
74421
- default: return volumes.layer1;
74422
- }
74293
+ const vol = volumes[layer];
74294
+ if (vol)
74295
+ return vol;
74296
+ const firstLayerId = this.ctx.nrrd_states.layers[0];
74297
+ console.warn(`ImageStoreHelper: unknown layer "${layer}", falling back to "${firstLayerId}"`);
74298
+ return volumes[firstLayerId];
74423
74299
  }
74424
74300
  /**
74425
74301
  * Get MaskVolume for the currently active layer.
@@ -74438,19 +74314,17 @@ void main() {
74438
74314
  }
74439
74315
  // ===== Store Image To Axis =====
74440
74316
  /**
74441
- * Phase 3: Simplified to be a no-op.
74442
- * MaskVolume storage happens in storeAllImages via setSliceFromImageData.
74443
- * This method kept for backward compatibility with existing call sites.
74317
+ * Phase 3: No-op MaskVolume storage happens in storeAllImages.
74444
74318
  */
74445
- storeImageToAxis(_index, _paintedImages, _imageData, _axis) {
74319
+ storeImageToAxis(_index, _imageData, _axis) {
74446
74320
  // No-op: MaskVolume is the primary storage, updated in storeAllImages
74447
74321
  }
74448
74322
  /**
74449
74323
  * Retrieve the drawn image for a given axis and slice.
74450
74324
  *
74451
- * Phase 3: Reads exclusively from MaskVolume (no legacy fallback).
74325
+ * Phase 3: Reads exclusively from MaskVolume.
74452
74326
  */
74453
- filterDrawedImage(axis, sliceIndex, _paintedImages) {
74327
+ filterDrawedImage(axis, sliceIndex) {
74454
74328
  try {
74455
74329
  const volume = this.getCurrentVolume();
74456
74330
  if (volume) {
@@ -74539,10 +74413,9 @@ void main() {
74539
74413
  }
74540
74414
  }
74541
74415
  /**
74542
- * Phase 3: Simplified - extracts ImageData from canvas but no longer stores to paintImages.
74543
- * Kept for backward compatibility with existing call sites.
74416
+ * Extract ImageData from canvas (MaskVolume storage is handled in storeAllImages).
74544
74417
  */
74545
- storeImageToLayer(_index, canvas, _paintedImages) {
74418
+ storeImageToLayer(_index, canvas) {
74546
74419
  if (!this.ctx.nrrd_states.loadMaskJson) {
74547
74420
  this.callbacks.setEmptyCanvasSize();
74548
74421
  this.callbacks.drawImageOnEmptyImage(canvas);
@@ -74551,16 +74424,6 @@ void main() {
74551
74424
  // No longer stores to paintedImages - MaskVolume is primary storage
74552
74425
  return imageData;
74553
74426
  }
74554
- // ===== Helper Methods =====
74555
- hasNonZeroPixels(imageData) {
74556
- const data = imageData.data;
74557
- for (let i = 0; i < data.length; i += 4) {
74558
- if (data[i] !== 0 || data[i + 1] !== 0 || data[i + 2] !== 0 || data[i + 3] !== 0) {
74559
- return true;
74560
- }
74561
- }
74562
- return false;
74563
- }
74564
74427
  }
74565
74428
 
74566
74429
  class DrawToolCore extends CommToolsData {
@@ -74639,8 +74502,7 @@ void main() {
74639
74502
  this.sphereTool = new SphereTool(toolCtx, {
74640
74503
  setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
74641
74504
  drawImageOnEmptyImage: (canvas) => this.drawImageOnEmptyImage(canvas),
74642
- storeImageToAxis: (index, paintedImages, imageData, axis) => this.imageStoreHelper.storeImageToAxis(index, paintedImages, imageData, axis),
74643
- createEmptyPaintImage: (dimensions, paintImages) => this.createEmptyPaintImage(dimensions, paintImages),
74505
+ storeImageToAxis: (index, imageData, axis) => this.imageStoreHelper.storeImageToAxis(index, imageData, axis),
74644
74506
  });
74645
74507
  this.crosshairTool = new CrosshairTool(toolCtx);
74646
74508
  this.contrastTool = new ContrastTool(toolCtx, this.container, this.contrastEventPrameters, {
@@ -74663,40 +74525,28 @@ void main() {
74663
74525
  // Use string comparison to avoid TypeScript narrowing issues
74664
74526
  const prev = prevMode;
74665
74527
  const next = newMode;
74666
- // Sync EventRouter mode changes with existing state flags
74667
- if (next === 'draw') {
74668
- this.protectedData.Is_Shift_Pressed = true;
74669
- this.nrrd_states.enableCursorChoose = false;
74670
- }
74671
- else if (prev === 'draw') {
74672
- this.protectedData.Is_Shift_Pressed = false;
74673
- }
74674
74528
  if (next === 'contrast') {
74675
- this.protectedData.Is_Ctrl_Pressed = true;
74676
- this.protectedData.Is_Shift_Pressed = false;
74677
74529
  this.configContrastDragMode();
74678
74530
  }
74679
74531
  else if (prev === 'contrast') {
74680
- this.protectedData.Is_Ctrl_Pressed = false;
74681
74532
  this.removeContrastDragMode();
74682
74533
  this.gui_states.readyToUpdate = true;
74683
74534
  }
74684
74535
  if (next === 'crosshair') {
74685
- this.nrrd_states.enableCursorChoose = true;
74686
74536
  this.protectedData.Is_Draw = false;
74687
74537
  }
74688
- else if (prev === 'crosshair') {
74689
- this.nrrd_states.enableCursorChoose = false;
74690
- }
74691
74538
  }
74692
74539
  });
74540
+ // Inject eventRouter into ToolContext so tools can query mode/state
74541
+ const toolCtx = this.sphereTool['ctx'];
74542
+ toolCtx.eventRouter = this.eventRouter;
74693
74543
  // Configure keyboard settings from class field
74694
74544
  this.eventRouter.setKeyboardSettings(this._keyboardSettings);
74695
74545
  // Track undo flag for Ctrl+Z handling
74696
74546
  let undoFlag = false;
74697
74547
  // Register keyboard handlers with EventRouter
74698
74548
  this.eventRouter.setKeydownHandler((ev) => {
74699
- var _a;
74549
+ var _a, _b;
74700
74550
  if (this._configKeyBoard)
74701
74551
  return;
74702
74552
  // Handle undo (Ctrl+Z)
@@ -74717,15 +74567,16 @@ void main() {
74717
74567
  }
74718
74568
  }
74719
74569
  // Handle draw mode (Shift key) - EventRouter already tracks this
74570
+ // EventRouter's handleKeyDown will enforce mutual exclusion
74720
74571
  if (ev.key === this._keyboardSettings.draw && !this.gui_states.sphere && !this.gui_states.calculator) {
74721
- if (this.protectedData.Is_Ctrl_Pressed) {
74572
+ if ((_b = this.eventRouter) === null || _b === void 0 ? void 0 : _b.isCtrlHeld()) {
74722
74573
  return; // Ctrl takes priority
74723
74574
  }
74724
74575
  // EventRouter will set mode to 'draw' via internal handler
74725
74576
  }
74726
74577
  });
74727
74578
  this.eventRouter.setKeyupHandler((ev) => {
74728
- var _a, _b, _c, _d;
74579
+ var _a, _b, _c, _d, _e, _f;
74729
74580
  if (this._configKeyBoard)
74730
74581
  return;
74731
74582
  // Handle Ctrl key release (contrast mode toggle)
@@ -74738,12 +74589,15 @@ void main() {
74738
74589
  // Skip mode toggle when contrast shortcut is disabled
74739
74590
  if (!((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isContrastEnabled()))
74740
74591
  return;
74592
+ // Block contrast toggle during crosshair or draw (mutual exclusion)
74593
+ if (((_b = this.eventRouter) === null || _b === void 0 ? void 0 : _b.isCrosshairEnabled()) || ((_c = this.eventRouter) === null || _c === void 0 ? void 0 : _c.getMode()) === 'draw')
74594
+ return;
74741
74595
  // Toggle contrast mode manually since it's on keyup
74742
- if (((_b = this.eventRouter) === null || _b === void 0 ? void 0 : _b.getMode()) !== 'contrast') {
74743
- (_c = this.eventRouter) === null || _c === void 0 ? void 0 : _c.setMode('contrast');
74596
+ if (((_d = this.eventRouter) === null || _d === void 0 ? void 0 : _d.getMode()) !== 'contrast') {
74597
+ (_e = this.eventRouter) === null || _e === void 0 ? void 0 : _e.setMode('contrast');
74744
74598
  }
74745
74599
  else {
74746
- (_d = this.eventRouter) === null || _d === void 0 ? void 0 : _d.setMode('idle');
74600
+ (_f = this.eventRouter) === null || _f === void 0 ? void 0 : _f.setMode('idle');
74747
74601
  }
74748
74602
  }
74749
74603
  });
@@ -74919,6 +74773,7 @@ void main() {
74919
74773
  }
74920
74774
  };
74921
74775
  this.drawingPrameters.handleOnDrawingMouseDown = (e) => {
74776
+ var _a, _b, _c, _d;
74922
74777
  if (leftclicked || rightclicked) {
74923
74778
  this.protectedData.canvases.drawingCanvas.removeEventListener("pointerup", this.drawingPrameters.handleOnDrawingMouseUp);
74924
74779
  this.protectedData.ctxes.drawingLayerMasterCtx.closePath();
@@ -74934,7 +74789,7 @@ void main() {
74934
74789
  // remove it when mouse click down
74935
74790
  this.protectedData.canvases.drawingCanvas.removeEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
74936
74791
  if (e.button === 0) {
74937
- if (this.protectedData.Is_Shift_Pressed) {
74792
+ if (((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.getMode()) === 'draw') {
74938
74793
  leftclicked = true;
74939
74794
  lines = [];
74940
74795
  Is_Painting = true;
@@ -74961,13 +74816,13 @@ void main() {
74961
74816
  const vol = this.getVolumeForLayer(this.gui_states.layer);
74962
74817
  this.preDrawSlice = vol.getSliceUint8(this.preDrawSliceIndex, this.preDrawAxis).data.slice();
74963
74818
  }
74964
- catch (_a) {
74819
+ catch (_e) {
74965
74820
  this.preDrawSlice = null;
74966
74821
  }
74967
74822
  this.protectedData.canvases.drawingCanvas.addEventListener("pointerup", this.drawingPrameters.handleOnDrawingMouseUp);
74968
74823
  this.protectedData.canvases.drawingCanvas.addEventListener("pointermove", this.drawingPrameters.handleOnDrawingMouseMove);
74969
74824
  }
74970
- else if (this.nrrd_states.enableCursorChoose) {
74825
+ else if ((_b = this.eventRouter) === null || _b === void 0 ? void 0 : _b.isCrosshairEnabled()) {
74971
74826
  this.nrrd_states.cursorPageX =
74972
74827
  e.offsetX / this.nrrd_states.sizeFoctor;
74973
74828
  this.nrrd_states.cursorPageY =
@@ -74975,10 +74830,10 @@ void main() {
74975
74830
  this.enableCrosshair();
74976
74831
  this.protectedData.canvases.drawingCanvas.addEventListener("pointerup", this.drawingPrameters.handleOnDrawingMouseUp);
74977
74832
  }
74978
- else if (this.gui_states.sphere && !this.nrrd_states.enableCursorChoose) {
74833
+ else if (this.gui_states.sphere && !((_c = this.eventRouter) === null || _c === void 0 ? void 0 : _c.isCrosshairEnabled())) {
74979
74834
  sphere(e);
74980
74835
  }
74981
- else if (this.gui_states.calculator && !this.nrrd_states.enableCursorChoose) {
74836
+ else if (this.gui_states.calculator && !((_d = this.eventRouter) === null || _d === void 0 ? void 0 : _d.isCrosshairEnabled())) {
74982
74837
  this.drawCalSphereDown(e.offsetX, e.offsetY, this.nrrd_states.currentIndex, this.gui_states.cal_distance);
74983
74838
  }
74984
74839
  }
@@ -75043,8 +74898,9 @@ void main() {
75043
74898
  ctx.drawImage(this.protectedData.canvases.emptyCanvas, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
75044
74899
  };
75045
74900
  this.drawingPrameters.handleOnDrawingMouseUp = (e) => {
74901
+ var _a, _b, _c, _d;
75046
74902
  if (e.button === 0) {
75047
- if (this.protectedData.Is_Shift_Pressed || Is_Painting) {
74903
+ if (((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.getMode()) === 'draw' || Is_Painting) {
75048
74904
  leftclicked = false;
75049
74905
  let { ctx, canvas } = this.setCurrentLayer();
75050
74906
  ctx.closePath();
@@ -75092,7 +74948,7 @@ void main() {
75092
74948
  };
75093
74949
  this.undoManager.push(delta);
75094
74950
  }
75095
- catch (_a) {
74951
+ catch (_e) {
75096
74952
  // Volume not ready — skip
75097
74953
  }
75098
74954
  this.preDrawSlice = null;
@@ -75103,7 +74959,7 @@ void main() {
75103
74959
  });
75104
74960
  }
75105
74961
  else if (this.gui_states.sphere &&
75106
- !this.nrrd_states.enableCursorChoose) {
74962
+ !((_b = this.eventRouter) === null || _b === void 0 ? void 0 : _b.isCrosshairEnabled())) {
75107
74963
  // plan B
75108
74964
  // findout all index in the sphere radius range in Axial view
75109
74965
  if (this.nrrd_states.spherePlanB) {
@@ -75123,12 +74979,12 @@ void main() {
75123
74979
  this.protectedData.canvases.drawingCanvas.removeEventListener("pointerup", this.drawingPrameters.handleOnDrawingMouseUp);
75124
74980
  }
75125
74981
  else if ((this.gui_states.sphere || this.gui_states.calculator) &&
75126
- this.nrrd_states.enableCursorChoose) {
74982
+ ((_c = this.eventRouter) === null || _c === void 0 ? void 0 : _c.isCrosshairEnabled())) {
75127
74983
  this.protectedData.canvases.drawingCanvas.addEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
75128
74984
  this.protectedData.canvases.drawingCanvas.removeEventListener("pointerup", this.drawingPrameters.handleOnDrawingMouseUp);
75129
74985
  }
75130
74986
  else if (this.gui_states.calculator &&
75131
- !this.nrrd_states.enableCursorChoose) {
74987
+ !((_d = this.eventRouter) === null || _d === void 0 ? void 0 : _d.isCrosshairEnabled())) {
75132
74988
  // When mouse up
75133
74989
  this.drawCalSphereUp();
75134
74990
  }
@@ -75170,6 +75026,7 @@ void main() {
75170
75026
  }
75171
75027
  });
75172
75028
  this.start = () => {
75029
+ var _a, _b;
75173
75030
  if (this.gui_states.readyToUpdate) {
75174
75031
  this.protectedData.ctxes.drawingCtx.clearRect(0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
75175
75032
  this.protectedData.ctxes.drawingCtx.globalAlpha =
@@ -75183,7 +75040,10 @@ void main() {
75183
75040
  }
75184
75041
  }
75185
75042
  else {
75186
- if (this.protectedData.Is_Shift_Pressed) {
75043
+ // Use EventRouter mode for mutually exclusive crosshair vs draw rendering
75044
+ const currentMode = (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.getMode();
75045
+ if (currentMode === 'draw') {
75046
+ // Draw mode: show brush circle preview
75187
75047
  if (!this.gui_states.pencil &&
75188
75048
  !this.gui_states.Eraser &&
75189
75049
  this.nrrd_states.Mouse_Over) {
@@ -75197,7 +75057,8 @@ void main() {
75197
75057
  this.protectedData.ctxes.drawingCtx.stroke();
75198
75058
  }
75199
75059
  }
75200
- if (this.nrrd_states.enableCursorChoose) {
75060
+ else if (currentMode === 'crosshair' || ((_b = this.eventRouter) === null || _b === void 0 ? void 0 : _b.isCrosshairEnabled())) {
75061
+ // Crosshair mode: show red cross lines (mutually exclusive with draw)
75201
75062
  this.protectedData.ctxes.drawingCtx.clearRect(0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
75202
75063
  const ex = this.nrrd_states.cursorPageX * this.nrrd_states.sizeFoctor;
75203
75064
  const ey = this.nrrd_states.cursorPageY * this.nrrd_states.sizeFoctor;
@@ -75353,19 +75214,18 @@ void main() {
75353
75214
  /**
75354
75215
  * Clear mask on current slice canvas.
75355
75216
  *
75356
- * Phase 2: Clears the MaskVolume slice for all three layers,
75357
- * re-stores, and notifies external via getMask callback with clearFlag=true.
75358
- * Phase 6: Also records a MaskDelta for undo support.
75217
+ * Only clears the active layer's MaskVolume slice data.
75218
+ * Other layers are left untouched. After clearing, all layer canvases
75219
+ * are re-rendered from MaskVolume to keep visuals in sync.
75359
75220
  */
75360
75221
  clearPaint() {
75361
75222
  this.protectedData.Is_Draw = true;
75362
- this.resetLayerCanvas();
75363
75223
  this.protectedData.canvases.originCanvas.width =
75364
75224
  this.protectedData.canvases.originCanvas.width;
75365
75225
  this.protectedData.mainPreSlices.repaint.call(this.protectedData.mainPreSlices);
75366
75226
  this.protectedData.previousDrawingImage =
75367
75227
  this.protectedData.ctxes.emptyCtx.createImageData(1, 1);
75368
- // Phase 2 + 6: Clear volume slices and record undo delta
75228
+ // Clear only the active layer's MaskVolume slice and record undo delta
75369
75229
  try {
75370
75230
  const axis = this.protectedData.axis;
75371
75231
  const idx = this.nrrd_states.currentIndex;
@@ -75373,11 +75233,8 @@ void main() {
75373
75233
  const vol = this.getVolumeForLayer(activeLayer);
75374
75234
  // Capture old slice for undo before clearing
75375
75235
  const oldSlice = vol.getSliceUint8(idx, axis).data.slice();
75376
- // Clear only the active layer (clear also clears all for canvas consistency)
75377
- const { layer1, layer2, layer3 } = this.protectedData.maskData.volumes;
75378
- layer1.clearSlice(idx, axis);
75379
- layer2.clearSlice(idx, axis);
75380
- layer3.clearSlice(idx, axis);
75236
+ // Clear only the active layer's MaskVolume slice
75237
+ vol.clearSlice(idx, axis);
75381
75238
  // New (all-zero) slice for undo newSlice
75382
75239
  const { data: newSlice, width, height } = vol.getSliceUint8(idx, axis);
75383
75240
  // Push clearPaint delta to UndoManager (supports undo)
@@ -75396,12 +75253,23 @@ void main() {
75396
75253
  }
75397
75254
  }
75398
75255
  catch (_a) {
75399
- // Volume not ready (1×1×1 placeholder) — continue with legacy path
75256
+ // Volume not ready (1×1×1 placeholder)
75257
+ }
75258
+ // Re-render ALL layers from MaskVolume to canvas (rebuilds visuals from source of truth)
75259
+ this.resetLayerCanvas();
75260
+ const buffer = this.getOrCreateSliceBuffer(this.protectedData.axis);
75261
+ if (buffer) {
75262
+ const w = this.nrrd_states.changedWidth;
75263
+ const h = this.nrrd_states.changedHeight;
75264
+ for (const layerId of this.nrrd_states.layers) {
75265
+ const target = this.protectedData.layerTargets.get(layerId);
75266
+ if (!target)
75267
+ continue;
75268
+ target.ctx.clearRect(0, 0, w, h);
75269
+ this.renderSliceToCanvas(layerId, this.protectedData.axis, this.nrrd_states.currentIndex, buffer, target.ctx, w, h);
75270
+ }
75400
75271
  }
75401
- this.storeAllImages(this.nrrd_states.currentIndex, this.gui_states.layer);
75402
- const restLayers = this.getRestLayer();
75403
- this.storeEachLayerImage(this.nrrd_states.currentIndex, restLayers[0]);
75404
- this.storeEachLayerImage(this.nrrd_states.currentIndex, restLayers[1]);
75272
+ this.compositeAllLayers();
75405
75273
  this.setIsDrawFalse(1000);
75406
75274
  }
75407
75275
  /**
@@ -75482,14 +75350,11 @@ void main() {
75482
75350
  this.protectedData.ctxes.drawingLayerMasterCtx.getImageData(0, 0, this.protectedData.canvases.drawingCanvasLayerMaster.width, this.protectedData.canvases.drawingCanvasLayerMaster.height);
75483
75351
  }
75484
75352
  /****************************Store images (delegated to ImageStoreHelper)****************************************************/
75485
- storeImageToAxis(index, paintedImages, imageData, axis) {
75486
- this.imageStoreHelper.storeImageToAxis(index, paintedImages, imageData, axis);
75487
- }
75488
75353
  storeAllImages(index, layer) {
75489
75354
  this.imageStoreHelper.storeAllImages(index, layer);
75490
75355
  }
75491
- storeImageToLayer(index, canvas, paintedImages) {
75492
- return this.imageStoreHelper.storeImageToLayer(index, canvas, paintedImages);
75356
+ storeImageToLayer(index, canvas) {
75357
+ return this.imageStoreHelper.storeImageToLayer(index, canvas);
75493
75358
  }
75494
75359
  storeEachLayerImage(index, layer) {
75495
75360
  this.imageStoreHelper.storeEachLayerImage(index, layer);
@@ -76185,8 +76050,9 @@ void main() {
76185
76050
  * @param {string} aixs:"x" | "y" | "z"
76186
76051
  * */
76187
76052
  setSliceOrientation(axisTo) {
76053
+ var _a;
76188
76054
  let convetObj;
76189
- if (this.nrrd_states.enableCursorChoose || this.gui_states.sphere) {
76055
+ if (((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isCrosshairEnabled()) || this.gui_states.sphere) {
76190
76056
  if (this.protectedData.axis === "z") {
76191
76057
  this.cursorPage.z.index = this.nrrd_states.currentIndex;
76192
76058
  this.cursorPage.z.cursorPageX = this.nrrd_states.cursorPageX;
@@ -76571,6 +76437,8 @@ void main() {
76571
76437
  }
76572
76438
  // Invalidate reusable slice buffer
76573
76439
  this.invalidateSliceBuffer();
76440
+ // Reload all layers to canvas (restores other layers' visuals)
76441
+ this.reloadMasksFromVolume();
76574
76442
  }
76575
76443
  /**
76576
76444
  * Reset the draw and display canvases layout after mouse pan.
@@ -76781,7 +76649,8 @@ void main() {
76781
76649
  */
76782
76650
  configMouseSliceWheel() {
76783
76651
  const handleMouseZoomSliceWheelMove = (e) => {
76784
- if (this.protectedData.Is_Shift_Pressed) {
76652
+ var _a;
76653
+ if ((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
76785
76654
  return;
76786
76655
  }
76787
76656
  e.preventDefault();
@@ -77021,7 +76890,7 @@ void main() {
77021
76890
  }
77022
76891
 
77023
76892
  // import * as kiwrious from "copper3d_plugin_heart_k";
77024
- const REVISION = "v3.0.0-beta";
76893
+ const REVISION = "v3.0.1-beta";
77025
76894
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
77026
76895
 
77027
76896
  exports.CHANNEL_COLORS = CHANNEL_COLORS;