copper3d 3.1.2 → 3.2.0

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/DragOperator.js +3 -1
  2. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  3. package/dist/Utils/segmentation/DrawToolCore.d.ts +6 -0
  4. package/dist/Utils/segmentation/DrawToolCore.js +107 -35
  5. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  6. package/dist/Utils/segmentation/NrrdTools.d.ts +2 -0
  7. package/dist/Utils/segmentation/NrrdTools.js +41 -1
  8. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  9. package/dist/Utils/segmentation/core/UndoManager.d.ts +9 -5
  10. package/dist/Utils/segmentation/core/UndoManager.js +21 -14
  11. package/dist/Utils/segmentation/core/UndoManager.js.map +1 -1
  12. package/dist/Utils/segmentation/core/index.d.ts +1 -1
  13. package/dist/Utils/segmentation/core/types.d.ts +5 -1
  14. package/dist/Utils/segmentation/core/types.js.map +1 -1
  15. package/dist/Utils/segmentation/coreTools/GuiState.js +2 -0
  16. package/dist/Utils/segmentation/coreTools/GuiState.js.map +1 -1
  17. package/dist/Utils/segmentation/coreTools/NrrdState.js +2 -0
  18. package/dist/Utils/segmentation/coreTools/NrrdState.js.map +1 -1
  19. package/dist/Utils/segmentation/eventRouter/EventRouter.js +1 -0
  20. package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -1
  21. package/dist/Utils/segmentation/eventRouter/types.d.ts +1 -1
  22. package/dist/Utils/segmentation/tools/SphereBrushTool.d.ts +127 -0
  23. package/dist/Utils/segmentation/tools/SphereBrushTool.js +489 -0
  24. package/dist/Utils/segmentation/tools/SphereBrushTool.js.map +1 -0
  25. package/dist/Utils/segmentation/tools/ToolHost.d.ts +3 -0
  26. package/dist/Utils/segmentation/tools/ZoomTool.js +6 -1
  27. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -1
  28. package/dist/Utils/segmentation/tools/index.d.ts +2 -1
  29. package/dist/Utils/segmentation/tools/index.js +1 -0
  30. package/dist/Utils/segmentation/tools/index.js.map +1 -1
  31. package/dist/bundle.esm.js +670 -53
  32. package/dist/bundle.umd.js +670 -53
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +6 -0
  36. package/dist/types/Utils/segmentation/NrrdTools.d.ts +2 -0
  37. package/dist/types/Utils/segmentation/core/UndoManager.d.ts +9 -5
  38. package/dist/types/Utils/segmentation/core/index.d.ts +1 -1
  39. package/dist/types/Utils/segmentation/core/types.d.ts +5 -1
  40. package/dist/types/Utils/segmentation/eventRouter/types.d.ts +1 -1
  41. package/dist/types/Utils/segmentation/tools/SphereBrushTool.d.ts +127 -0
  42. package/dist/types/Utils/segmentation/tools/ToolHost.d.ts +3 -0
  43. package/dist/types/Utils/segmentation/tools/index.d.ts +2 -1
  44. package/dist/types/index.d.ts +1 -1
  45. package/package.json +1 -1
@@ -71955,41 +71955,48 @@ class UndoManager {
71955
71955
  setActiveLayer(layer) {
71956
71956
  this.activeLayer = layer;
71957
71957
  }
71958
- /** Push a delta onto the active layer's undo stack and clear the redo stack. */
71958
+ /** Push a single delta onto the active layer's undo stack and clear the redo stack. */
71959
71959
  push(delta) {
71960
+ this.pushGroup([delta]);
71961
+ }
71962
+ /** Push a group of deltas as a single undo unit (e.g. 3D sphere spanning multiple slices). */
71963
+ pushGroup(deltas) {
71960
71964
  var _a, _b;
71961
- const stack = (_a = this.undoStacks.get(delta.layerId)) !== null && _a !== void 0 ? _a : this.undoStacks.get("layer1");
71962
- stack.push(delta);
71965
+ if (deltas.length === 0)
71966
+ return;
71967
+ const layerId = deltas[0].layerId;
71968
+ const stack = (_a = this.undoStacks.get(layerId)) !== null && _a !== void 0 ? _a : this.undoStacks.get("layer1");
71969
+ stack.push(deltas);
71963
71970
  if (stack.length > MAX_STACK_SIZE) {
71964
71971
  stack.shift();
71965
71972
  }
71966
71973
  // Any new operation invalidates the redo history for that layer
71967
- const redoStack = (_b = this.redoStacks.get(delta.layerId)) !== null && _b !== void 0 ? _b : this.redoStacks.get("layer1");
71974
+ const redoStack = (_b = this.redoStacks.get(layerId)) !== null && _b !== void 0 ? _b : this.redoStacks.get("layer1");
71968
71975
  redoStack.length = 0;
71969
71976
  }
71970
71977
  /**
71971
71978
  * Undo the last operation on the active layer.
71972
- * @returns The delta that was undone, or undefined if nothing to undo.
71979
+ * @returns The delta(s) that were undone, or undefined if nothing to undo.
71973
71980
  */
71974
71981
  undo() {
71975
71982
  const stack = this.undoStacks.get(this.activeLayer);
71976
- const delta = stack.pop();
71977
- if (delta) {
71978
- this.redoStacks.get(this.activeLayer).push(delta);
71983
+ const entry = stack.pop();
71984
+ if (entry) {
71985
+ this.redoStacks.get(this.activeLayer).push(entry);
71979
71986
  }
71980
- return delta;
71987
+ return entry;
71981
71988
  }
71982
71989
  /**
71983
71990
  * Redo the last undone operation on the active layer.
71984
- * @returns The delta that was redone, or undefined if nothing to redo.
71991
+ * @returns The delta(s) that were redone, or undefined if nothing to redo.
71985
71992
  */
71986
71993
  redo() {
71987
71994
  const stack = this.redoStacks.get(this.activeLayer);
71988
- const delta = stack.pop();
71989
- if (delta) {
71990
- this.undoStacks.get(this.activeLayer).push(delta);
71995
+ const entry = stack.pop();
71996
+ if (entry) {
71997
+ this.undoStacks.get(this.activeLayer).push(entry);
71991
71998
  }
71992
- return delta;
71999
+ return entry;
71993
72000
  }
71994
72001
  canUndo() {
71995
72002
  var _a, _b;
@@ -73358,7 +73365,9 @@ class DragOperator {
73358
73365
  // When leaving draw, contrast, or crosshair mode (returning to idle), restore drag mode
73359
73366
  // Do NOT restore if sphere mode is active
73360
73367
  if ((prev === 'draw' || prev === 'contrast' || prev === 'crosshair') && next === 'idle') {
73361
- if (!this.gui_states.mode.sphere) {
73368
+ if (!this.gui_states.mode.sphere
73369
+ && !this.gui_states.mode.sphereBrush
73370
+ && !this.gui_states.mode.sphereEraser) {
73362
73371
  this.configDragMode();
73363
73372
  }
73364
73373
  }
@@ -73897,6 +73906,7 @@ class EventRouter {
73897
73906
  if (ev.key === this.keyboardSettings.draw) {
73898
73907
  this.state.shiftHeld = true;
73899
73908
  // Block draw mode when crosshair or contrast is active (mutual exclusion)
73909
+ // Also block when sphereBrush or sphereEraser is active
73900
73910
  if (DRAWING_TOOLS.has(this.guiTool) && !this.state.ctrlHeld && !this.state.crosshairEnabled) {
73901
73911
  this.setMode('draw');
73902
73912
  }
@@ -74296,10 +74306,15 @@ class ZoomTool extends BaseTool {
74296
74306
  configMouseZoomWheel() {
74297
74307
  let moveDistance = 1;
74298
74308
  return (e) => {
74299
- var _a;
74309
+ var _a, _b;
74300
74310
  if ((_a = this.ctx.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
74301
74311
  return;
74302
74312
  }
74313
+ // Block zoom wheel when sphereBrush/sphereEraser is actively placing (left button held)
74314
+ if ((this.ctx.gui_states.mode.sphereBrush || this.ctx.gui_states.mode.sphereEraser)
74315
+ && ((_b = this.ctx.eventRouter) === null || _b === void 0 ? void 0 : _b.isLeftButtonDown())) {
74316
+ return;
74317
+ }
74303
74318
  e.preventDefault();
74304
74319
  const delta = e.detail ? e.detail > 0 : e.wheelDelta < 0;
74305
74320
  this.ctx.protectedData.isDrawing = true;
@@ -74901,6 +74916,493 @@ class ImageStoreHelper extends BaseTool {
74901
74916
  }
74902
74917
  }
74903
74918
 
74919
+ /**
74920
+ * SphereBrushTool — 3D sphere brush and eraser on layer MaskVolume.
74921
+ *
74922
+ * Unlike SphereTool (which uses its own isolated sphereMaskVolume),
74923
+ * this tool writes/erases directly in the active layer's shared MaskVolume —
74924
+ * the same volume used by pencil/brush/eraser tools.
74925
+ *
74926
+ * Two operation modes handled by one class:
74927
+ * - **sphereBrush**: Direct click to paint a 3D sphere of the active channel label.
74928
+ * - **sphereEraser**: Shift+click to erase a 3D sphere (only voxels matching active channel).
74929
+ *
74930
+ * Interaction:
74931
+ * 1. Pointer-down records sphere center, switches wheel to radius mode.
74932
+ * 2. Scroll wheel adjusts sphereBrushRadius [1, 50].
74933
+ * 3. Pointer-up writes/erases sphere to volume, captures grouped undo, refreshes canvas.
74934
+ */
74935
+ class SphereBrushTool extends BaseTool {
74936
+ constructor(ctx, callbacks) {
74937
+ super(ctx);
74938
+ /** Recorded sphere center in canvas mm-space */
74939
+ this.centerX = 0;
74940
+ this.centerY = 0;
74941
+ this.centerSlice = 0;
74942
+ /** Whether a sphere placement is in progress */
74943
+ this.active = false;
74944
+ /** Current operation mode for the active placement */
74945
+ this.mode = "brush";
74946
+ /** Cumulative "before" snapshots for drag-erase undo (z-index → slice data) */
74947
+ this.dragBeforeSnapshots = new Map();
74948
+ /** Whether a drag-erase has actually moved (vs. simple click-release) */
74949
+ this.dragMoved = false;
74950
+ this.callbacks = callbacks;
74951
+ }
74952
+ setCallbacks(callbacks) {
74953
+ this.callbacks = callbacks;
74954
+ }
74955
+ // ── Geometry (ported from SphereTool) ──────────────────────────
74956
+ /**
74957
+ * Convert canvas mm-space coordinates to 3D voxel coordinates.
74958
+ */
74959
+ canvasToVoxelCenter(canvasX, canvasY, sliceIndex, axis) {
74960
+ const nrrd = this.ctx.nrrd_states;
74961
+ switch (axis) {
74962
+ case 'z':
74963
+ return {
74964
+ x: canvasX * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm,
74965
+ y: canvasY * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm,
74966
+ z: sliceIndex,
74967
+ };
74968
+ case 'y':
74969
+ return {
74970
+ x: canvasX * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm,
74971
+ y: sliceIndex,
74972
+ z: (nrrd.image.nrrd_z_mm - canvasY) * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm,
74973
+ };
74974
+ case 'x':
74975
+ return {
74976
+ x: sliceIndex,
74977
+ y: canvasY * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm,
74978
+ z: canvasX * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm,
74979
+ };
74980
+ }
74981
+ }
74982
+ /**
74983
+ * Convert mm radius to per-axis voxel radii.
74984
+ */
74985
+ getVoxelRadii(radius) {
74986
+ const nrrd = this.ctx.nrrd_states;
74987
+ return {
74988
+ rx: radius * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm,
74989
+ ry: radius * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm,
74990
+ rz: radius * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm,
74991
+ };
74992
+ }
74993
+ /**
74994
+ * Compute clamped bounding box for an ellipsoid in voxel space.
74995
+ */
74996
+ getBoundingBox(center, radii, dims) {
74997
+ return {
74998
+ minX: Math.max(0, Math.floor(center.x - radii.rx)),
74999
+ maxX: Math.min(dims.width - 1, Math.ceil(center.x + radii.rx)),
75000
+ minY: Math.max(0, Math.floor(center.y - radii.ry)),
75001
+ maxY: Math.min(dims.height - 1, Math.ceil(center.y + radii.ry)),
75002
+ minZ: Math.max(0, Math.floor(center.z - radii.rz)),
75003
+ maxZ: Math.min(dims.depth - 1, Math.ceil(center.z + radii.rz)),
75004
+ };
75005
+ }
75006
+ // ── 3D Volume Write / Erase ────────────────────────────────────
75007
+ /**
75008
+ * Write a 3D sphere of the active channel label into the layer's MaskVolume.
75009
+ */
75010
+ write3DSphereToBrush(vol, center, radii, bb, label) {
75011
+ const { rx, ry, rz } = radii;
75012
+ for (let z = bb.minZ; z <= bb.maxZ; z++) {
75013
+ for (let y = bb.minY; y <= bb.maxY; y++) {
75014
+ for (let x = bb.minX; x <= bb.maxX; x++) {
75015
+ const dx = rx > 0 ? (x - center.x) / rx : 0;
75016
+ const dy = ry > 0 ? (y - center.y) / ry : 0;
75017
+ const dz = rz > 0 ? (z - center.z) / rz : 0;
75018
+ if (dx * dx + dy * dy + dz * dz <= 1.0) {
75019
+ vol.setVoxel(x, y, z, label);
75020
+ }
75021
+ }
75022
+ }
75023
+ }
75024
+ }
75025
+ /**
75026
+ * Erase a 3D sphere from the layer's MaskVolume.
75027
+ * Only clears voxels whose current value equals the active channel label.
75028
+ */
75029
+ erase3DSphereFromVolume(vol, center, radii, bb, label) {
75030
+ const { rx, ry, rz } = radii;
75031
+ for (let z = bb.minZ; z <= bb.maxZ; z++) {
75032
+ for (let y = bb.minY; y <= bb.maxY; y++) {
75033
+ for (let x = bb.minX; x <= bb.maxX; x++) {
75034
+ const dx = rx > 0 ? (x - center.x) / rx : 0;
75035
+ const dy = ry > 0 ? (y - center.y) / ry : 0;
75036
+ const dz = rz > 0 ? (z - center.z) / rz : 0;
75037
+ if (dx * dx + dy * dy + dz * dz <= 1.0) {
75038
+ if (vol.getVoxel(x, y, z) === label) {
75039
+ vol.setVoxel(x, y, z, 0);
75040
+ }
75041
+ }
75042
+ }
75043
+ }
75044
+ }
75045
+ }
75046
+ // ── Undo Capture ───────────────────────────────────────────────
75047
+ /**
75048
+ * Capture slice snapshots for all Z-slices in the bounding box.
75049
+ * Used before and after the sphere operation to build MaskDelta groups.
75050
+ *
75051
+ * We always capture along the Z axis since the 3D sphere spans Z slices.
75052
+ */
75053
+ captureSliceSnapshots(vol, minZ, maxZ) {
75054
+ const snapshots = new Map();
75055
+ for (let z = minZ; z <= maxZ; z++) {
75056
+ try {
75057
+ const { data } = vol.getSliceUint8(z, 'z');
75058
+ snapshots.set(z, data.slice());
75059
+ }
75060
+ catch (_a) {
75061
+ // slice out of bounds — skip
75062
+ }
75063
+ }
75064
+ return snapshots;
75065
+ }
75066
+ /**
75067
+ * Build MaskDelta group from before/after snapshots.
75068
+ */
75069
+ buildUndoGroup(layerId, before, after) {
75070
+ const deltas = [];
75071
+ for (const [sliceIndex, oldSlice] of before) {
75072
+ const newSlice = after.get(sliceIndex);
75073
+ if (!newSlice)
75074
+ continue;
75075
+ // Only include slices that actually changed
75076
+ let changed = false;
75077
+ for (let i = 0; i < oldSlice.length; i++) {
75078
+ if (oldSlice[i] !== newSlice[i]) {
75079
+ changed = true;
75080
+ break;
75081
+ }
75082
+ }
75083
+ if (changed) {
75084
+ deltas.push({ layerId, axis: 'z', sliceIndex, oldSlice, newSlice });
75085
+ }
75086
+ }
75087
+ return deltas;
75088
+ }
75089
+ // ── Preview Rendering ──────────────────────────────────────────
75090
+ /**
75091
+ * Draw a preview circle on the sphere canvas during sphere placement.
75092
+ */
75093
+ drawPreview(mouseX, mouseY, radius, isEraser) {
75094
+ const sphereCanvas = this.ctx.protectedData.canvases.drawingSphereCanvas;
75095
+ const sphereCtx = this.ctx.protectedData.ctxes.drawingSphereCtx;
75096
+ // Clear and resize sphere canvas
75097
+ sphereCanvas.width = this.ctx.protectedData.canvases.originCanvas.width;
75098
+ sphereCanvas.height = this.ctx.protectedData.canvases.originCanvas.height;
75099
+ sphereCtx.beginPath();
75100
+ sphereCtx.arc(mouseX, mouseY, radius, 0, 2 * Math.PI);
75101
+ if (isEraser) {
75102
+ // Eraser preview: dashed outline
75103
+ sphereCtx.strokeStyle = "#ff4444";
75104
+ sphereCtx.lineWidth = 2;
75105
+ sphereCtx.setLineDash([4, 4]);
75106
+ sphereCtx.stroke();
75107
+ sphereCtx.setLineDash([]);
75108
+ }
75109
+ else {
75110
+ // Brush preview: semi-transparent fill with active channel color
75111
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75112
+ const color = this.getActiveChannelColor(channel);
75113
+ sphereCtx.fillStyle = color;
75114
+ sphereCtx.globalAlpha = 0.5;
75115
+ sphereCtx.fill();
75116
+ sphereCtx.globalAlpha = 1.0;
75117
+ }
75118
+ sphereCtx.closePath();
75119
+ }
75120
+ /**
75121
+ * Clear the sphere preview canvas.
75122
+ */
75123
+ clearPreview() {
75124
+ const sphereCanvas = this.ctx.protectedData.canvases.drawingSphereCanvas;
75125
+ sphereCanvas.width = sphereCanvas.width;
75126
+ }
75127
+ /**
75128
+ * Get the hex color for the active channel from the layer's MaskVolume.
75129
+ */
75130
+ getActiveChannelColor(channel) {
75131
+ var _a;
75132
+ const layer = this.ctx.gui_states.layerChannel.layer;
75133
+ const volumes = this.ctx.protectedData.maskData.volumes;
75134
+ const vol = volumes[layer];
75135
+ if (vol) {
75136
+ const rgba = vol.getChannelColor(channel);
75137
+ const r = rgba.r.toString(16).padStart(2, '0');
75138
+ const g = rgba.g.toString(16).padStart(2, '0');
75139
+ const b = rgba.b.toString(16).padStart(2, '0');
75140
+ return `#${r}${g}${b}`;
75141
+ }
75142
+ return (_a = CHANNEL_HEX_COLORS[channel]) !== null && _a !== void 0 ? _a : "#ff0000";
75143
+ }
75144
+ // ── Wheel Handler ──────────────────────────────────────────────
75145
+ /**
75146
+ * Returns a wheel event handler for adjusting sphereBrushRadius.
75147
+ * Used in both sphereBrush and sphereEraser modes.
75148
+ */
75149
+ configSphereBrushWheel() {
75150
+ return (e) => {
75151
+ e.preventDefault();
75152
+ const sphere = this.ctx.nrrd_states.sphere;
75153
+ if (e.deltaY < 0) {
75154
+ sphere.sphereBrushRadius += 1;
75155
+ }
75156
+ else {
75157
+ sphere.sphereBrushRadius -= 1;
75158
+ }
75159
+ sphere.sphereBrushRadius = Math.max(1, Math.min(sphere.sphereBrushRadius, 50));
75160
+ // Redraw preview
75161
+ if (this.active) {
75162
+ this.drawPreview(this.centerX, this.centerY, sphere.sphereBrushRadius, this.mode === "eraser");
75163
+ }
75164
+ };
75165
+ }
75166
+ // ── Sphere Brush Click/PointerUp ───────────────────────────────
75167
+ /**
75168
+ * Handle pointer-down in sphereBrush mode (direct click, no Shift needed).
75169
+ */
75170
+ onSphereBrushClick(e) {
75171
+ this.centerX = e.offsetX / this.ctx.nrrd_states.view.sizeFactor;
75172
+ this.centerY = e.offsetY / this.ctx.nrrd_states.view.sizeFactor;
75173
+ this.centerSlice = this.ctx.nrrd_states.view.currentSliceIndex;
75174
+ this.active = true;
75175
+ this.mode = "brush";
75176
+ this.drawPreview(this.centerX, this.centerY, this.ctx.nrrd_states.sphere.sphereBrushRadius, false);
75177
+ }
75178
+ /**
75179
+ * Handle pointer-up in sphereBrush mode — write sphere to volume.
75180
+ */
75181
+ onSphereBrushPointerUp() {
75182
+ if (!this.active || this.mode !== "brush")
75183
+ return;
75184
+ this.active = false;
75185
+ const layer = this.ctx.gui_states.layerChannel.layer;
75186
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75187
+ const axis = this.ctx.protectedData.axis;
75188
+ const radius = this.ctx.nrrd_states.sphere.sphereBrushRadius;
75189
+ let vol;
75190
+ try {
75191
+ vol = this.callbacks.getVolumeForLayer(layer);
75192
+ }
75193
+ catch (_a) {
75194
+ this.clearPreview();
75195
+ return;
75196
+ }
75197
+ const dims = vol.getDimensions();
75198
+ const center = this.canvasToVoxelCenter(this.centerX, this.centerY, this.centerSlice, axis);
75199
+ const radii = this.getVoxelRadii(radius);
75200
+ const bb = this.getBoundingBox(center, radii, dims);
75201
+ // Capture pre-write snapshots
75202
+ const before = this.captureSliceSnapshots(vol, bb.minZ, bb.maxZ);
75203
+ // Write sphere
75204
+ this.write3DSphereToBrush(vol, center, radii, bb, channel);
75205
+ // Capture post-write snapshots and push undo group
75206
+ const after = this.captureSliceSnapshots(vol, bb.minZ, bb.maxZ);
75207
+ const deltas = this.buildUndoGroup(layer, before, after);
75208
+ if (deltas.length > 0) {
75209
+ this.callbacks.pushUndoGroup(deltas);
75210
+ }
75211
+ // Refresh display and notify backend of ALL changed slices
75212
+ this.refreshDisplay(layer, deltas);
75213
+ this.clearPreview();
75214
+ }
75215
+ // ── Sphere Eraser Click/PointerUp ──────────────────────────────
75216
+ /**
75217
+ * Handle pointer-down in sphereEraser mode.
75218
+ * Initializes drag tracking for cumulative undo.
75219
+ */
75220
+ onSphereEraserClick(e) {
75221
+ this.centerX = e.offsetX / this.ctx.nrrd_states.view.sizeFactor;
75222
+ this.centerY = e.offsetY / this.ctx.nrrd_states.view.sizeFactor;
75223
+ this.centerSlice = this.ctx.nrrd_states.view.currentSliceIndex;
75224
+ this.active = true;
75225
+ this.mode = "eraser";
75226
+ this.dragMoved = false;
75227
+ this.dragBeforeSnapshots.clear();
75228
+ // Capture initial "before" snapshots for the first sphere position
75229
+ const layer = this.ctx.gui_states.layerChannel.layer;
75230
+ const radius = this.ctx.nrrd_states.sphere.sphereBrushRadius;
75231
+ try {
75232
+ const vol = this.callbacks.getVolumeForLayer(layer);
75233
+ const dims = vol.getDimensions();
75234
+ const axis = this.ctx.protectedData.axis;
75235
+ const center = this.canvasToVoxelCenter(this.centerX, this.centerY, this.centerSlice, axis);
75236
+ const radii = this.getVoxelRadii(radius);
75237
+ const bb = this.getBoundingBox(center, radii, dims);
75238
+ this.expandDragBeforeSnapshots(vol, bb.minZ, bb.maxZ);
75239
+ }
75240
+ catch (_a) {
75241
+ // Volume not ready
75242
+ }
75243
+ this.drawPreview(this.centerX, this.centerY, radius, true);
75244
+ }
75245
+ /**
75246
+ * Handle pointer-move in sphereEraser mode — drag to continuously erase.
75247
+ */
75248
+ onSphereEraserMove(e) {
75249
+ if (!this.active || this.mode !== "eraser")
75250
+ return;
75251
+ this.dragMoved = true;
75252
+ const mouseX = e.offsetX / this.ctx.nrrd_states.view.sizeFactor;
75253
+ const mouseY = e.offsetY / this.ctx.nrrd_states.view.sizeFactor;
75254
+ const sliceIndex = this.ctx.nrrd_states.view.currentSliceIndex;
75255
+ const layer = this.ctx.gui_states.layerChannel.layer;
75256
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75257
+ const axis = this.ctx.protectedData.axis;
75258
+ const radius = this.ctx.nrrd_states.sphere.sphereBrushRadius;
75259
+ let vol;
75260
+ try {
75261
+ vol = this.callbacks.getVolumeForLayer(layer);
75262
+ }
75263
+ catch (_a) {
75264
+ return;
75265
+ }
75266
+ const dims = vol.getDimensions();
75267
+ const center = this.canvasToVoxelCenter(mouseX, mouseY, sliceIndex, axis);
75268
+ const radii = this.getVoxelRadii(radius);
75269
+ const bb = this.getBoundingBox(center, radii, dims);
75270
+ // Expand "before" snapshots to cover any new Z slices
75271
+ this.expandDragBeforeSnapshots(vol, bb.minZ, bb.maxZ);
75272
+ // Erase sphere at current position
75273
+ this.erase3DSphereFromVolume(vol, center, radii, bb, channel);
75274
+ // Update preview position
75275
+ this.drawPreview(mouseX, mouseY, radius, true);
75276
+ // Refresh display so user sees the erase in real-time
75277
+ this.refreshDisplay(layer);
75278
+ }
75279
+ /**
75280
+ * Handle pointer-up in sphereEraser mode — finalize erase + push undo group.
75281
+ * Works for both click-release and drag-release.
75282
+ */
75283
+ onSphereEraserPointerUp() {
75284
+ if (!this.active || this.mode !== "eraser")
75285
+ return;
75286
+ this.active = false;
75287
+ const layer = this.ctx.gui_states.layerChannel.layer;
75288
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75289
+ const axis = this.ctx.protectedData.axis;
75290
+ const radius = this.ctx.nrrd_states.sphere.sphereBrushRadius;
75291
+ let vol;
75292
+ try {
75293
+ vol = this.callbacks.getVolumeForLayer(layer);
75294
+ }
75295
+ catch (_a) {
75296
+ this.clearPreview();
75297
+ this.dragBeforeSnapshots.clear();
75298
+ return;
75299
+ }
75300
+ // If no drag occurred, erase at the click position (original click-release behavior)
75301
+ if (!this.dragMoved) {
75302
+ const dims = vol.getDimensions();
75303
+ const center = this.canvasToVoxelCenter(this.centerX, this.centerY, this.centerSlice, axis);
75304
+ const radii = this.getVoxelRadii(radius);
75305
+ const bb = this.getBoundingBox(center, radii, dims);
75306
+ this.erase3DSphereFromVolume(vol, center, radii, bb, channel);
75307
+ }
75308
+ // Build undo group from cumulative "before" snapshots vs current state
75309
+ const after = this.captureSliceSnapshots(vol, this.dragMinZ(), this.dragMaxZ());
75310
+ const deltas = this.buildUndoGroup(layer, this.dragBeforeSnapshots, after);
75311
+ if (deltas.length > 0) {
75312
+ this.callbacks.pushUndoGroup(deltas);
75313
+ }
75314
+ // Refresh display and notify backend of ALL changed slices
75315
+ this.refreshDisplay(layer, deltas);
75316
+ this.clearPreview();
75317
+ this.dragBeforeSnapshots.clear();
75318
+ }
75319
+ /**
75320
+ * Expand cumulative "before" snapshots to cover z range [minZ, maxZ].
75321
+ * Only captures slices not yet in the map (preserves original state).
75322
+ */
75323
+ expandDragBeforeSnapshots(vol, minZ, maxZ) {
75324
+ for (let z = minZ; z <= maxZ; z++) {
75325
+ if (!this.dragBeforeSnapshots.has(z)) {
75326
+ try {
75327
+ const { data } = vol.getSliceUint8(z, 'z');
75328
+ this.dragBeforeSnapshots.set(z, data.slice());
75329
+ }
75330
+ catch (_a) {
75331
+ // slice out of bounds
75332
+ }
75333
+ }
75334
+ }
75335
+ }
75336
+ /** Get minimum Z index from drag snapshots */
75337
+ dragMinZ() {
75338
+ let min = Infinity;
75339
+ for (const z of this.dragBeforeSnapshots.keys()) {
75340
+ if (z < min)
75341
+ min = z;
75342
+ }
75343
+ return min === Infinity ? 0 : min;
75344
+ }
75345
+ /** Get maximum Z index from drag snapshots */
75346
+ dragMaxZ() {
75347
+ let max = -Infinity;
75348
+ for (const z of this.dragBeforeSnapshots.keys()) {
75349
+ if (z > max)
75350
+ max = z;
75351
+ }
75352
+ return max === -Infinity ? 0 : max;
75353
+ }
75354
+ // ── Post-write Display Refresh ─────────────────────────────────
75355
+ /**
75356
+ * After writing/erasing the sphere in the volume, re-render the current
75357
+ * slice's layer canvas and composite all layers to master.
75358
+ *
75359
+ * @param changedDeltas - If provided, fire onMaskChanged for ALL changed
75360
+ * slices (not just the current view slice) so the backend receives the
75361
+ * full 3D sphere data for NII/GLTF export.
75362
+ */
75363
+ refreshDisplay(layerId, changedDeltas) {
75364
+ // Re-render layer canvas from volume for the current slice
75365
+ const target = this.ctx.protectedData.layerTargets.get(layerId);
75366
+ if (target) {
75367
+ const { ctx, canvas } = target;
75368
+ canvas.width = canvas.width; // clear
75369
+ const buffer = this.callbacks.getOrCreateSliceBuffer(this.ctx.protectedData.axis);
75370
+ if (buffer) {
75371
+ this.callbacks.renderSliceToCanvas(layerId, this.ctx.protectedData.axis, this.ctx.nrrd_states.view.currentSliceIndex, buffer, ctx, this.ctx.nrrd_states.view.changedWidth, this.ctx.nrrd_states.view.changedHeight);
75372
+ }
75373
+ }
75374
+ // Composite all layers to master canvas
75375
+ this.callbacks.compositeAllLayers();
75376
+ // Fire onMaskChanged for ALL changed slices (not just the current view slice)
75377
+ // This ensures the backend receives the full 3D sphere data for export.
75378
+ try {
75379
+ const vol = this.callbacks.getVolumeForLayer(layerId);
75380
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75381
+ if (changedDeltas && changedDeltas.length > 0) {
75382
+ // Report every changed Z slice
75383
+ for (const delta of changedDeltas) {
75384
+ const { data: sliceData, width, height } = vol.getSliceUint8(delta.sliceIndex, delta.axis);
75385
+ this.ctx.callbacks.onMaskChanged(sliceData, layerId, channel, delta.sliceIndex, delta.axis, width, height, false);
75386
+ }
75387
+ }
75388
+ else {
75389
+ // Fallback: report only the current slice
75390
+ const axis = this.ctx.protectedData.axis;
75391
+ const sliceIndex = this.ctx.nrrd_states.view.currentSliceIndex;
75392
+ const { data: sliceData, width, height } = vol.getSliceUint8(sliceIndex, axis);
75393
+ this.ctx.callbacks.onMaskChanged(sliceData, layerId, channel, sliceIndex, axis, width, height, false);
75394
+ }
75395
+ }
75396
+ catch (_a) {
75397
+ // Volume not ready
75398
+ }
75399
+ }
75400
+ /** Whether a sphere placement is currently in progress */
75401
+ get isActive() {
75402
+ return this.active;
75403
+ }
75404
+ }
75405
+
74904
75406
  /**
74905
75407
  * DrawToolCore — Tool orchestration and event routing.
74906
75408
  *
@@ -74921,6 +75423,7 @@ class DrawToolCore {
74921
75423
  handleOnDrawingBrushCricleMove: (ev) => { },
74922
75424
  handleMouseZoomSliceWheel: (e) => { },
74923
75425
  handleSphereWheel: (e) => { },
75426
+ handleSphereBrushWheel: (e) => { },
74924
75427
  };
74925
75428
  this.contrastEventPrameters = {
74926
75429
  move_x: 0,
@@ -75003,6 +75506,16 @@ class DrawToolCore {
75003
75506
  pushUndoDelta: (delta) => this.undoManager.push(delta),
75004
75507
  getEraserUrls: () => this.eraserUrls,
75005
75508
  });
75509
+ this.sphereBrushTool = new SphereBrushTool(toolCtx, {
75510
+ getVolumeForLayer: (layer) => this.renderer.getVolumeForLayer(layer),
75511
+ compositeAllLayers: () => this.renderer.compositeAllLayers(),
75512
+ pushUndoGroup: (deltas) => this.undoManager.pushGroup(deltas),
75513
+ renderSliceToCanvas: (layer, axis, sliceIndex, buffer, ctx, w, h) => this.renderer.renderSliceToCanvas(layer, axis, sliceIndex, buffer, ctx, w, h),
75514
+ getOrCreateSliceBuffer: (axis) => this.renderer.getOrCreateSliceBuffer(axis),
75515
+ setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
75516
+ reloadMasksFromVolume: () => this.reloadMasksFromVolume(),
75517
+ getEraserUrls: () => this.eraserUrls,
75518
+ });
75006
75519
  }
75007
75520
  initDrawToolCore() {
75008
75521
  // Initialize EventRouter for centralized event handling
@@ -75047,12 +75560,28 @@ class DrawToolCore {
75047
75560
  if ((ev.ctrlKey || ev.metaKey) && (ev.key === redoKey || (ev.shiftKey && ev.key === undoKeyUpper))) {
75048
75561
  this.redoLastPainting();
75049
75562
  }
75563
+ // Handle mouse wheel mode toggle (Ctrl+1 = Scroll:Zoom, Ctrl+2 = Scroll:Slice)
75564
+ if ((ev.ctrlKey || ev.metaKey) && ev.key === '1') {
75565
+ ev.preventDefault();
75566
+ this.state.keyboardSettings.mouseWheel = 'Scroll:Zoom';
75567
+ this.updateMouseWheelEvent();
75568
+ return;
75569
+ }
75570
+ if ((ev.ctrlKey || ev.metaKey) && ev.key === '2') {
75571
+ ev.preventDefault();
75572
+ this.state.keyboardSettings.mouseWheel = 'Scroll:Slice';
75573
+ this.updateMouseWheelEvent();
75574
+ return;
75575
+ }
75050
75576
  // Handle crosshair toggle (allowed in drawing tools AND sphere mode)
75051
75577
  if (ev.key === this.state.keyboardSettings.crosshair) {
75052
75578
  this.eventRouter.toggleCrosshair();
75053
75579
  }
75054
75580
  // Handle draw mode (Shift key) - EventRouter already tracks this
75055
- if (ev.key === this.state.keyboardSettings.draw && !this.state.gui_states.mode.sphere) {
75581
+ if (ev.key === this.state.keyboardSettings.draw
75582
+ && !this.state.gui_states.mode.sphere
75583
+ && !this.state.gui_states.mode.sphereBrush
75584
+ && !this.state.gui_states.mode.sphereEraser) {
75056
75585
  if (this.eventRouter.isCtrlHeld()) {
75057
75586
  return; // Ctrl takes priority
75058
75587
  }
@@ -75104,11 +75633,16 @@ class DrawToolCore {
75104
75633
  if (this.drawingTool.isActive || this.panTool.isActive) {
75105
75634
  this.drawingPrameters.handleOnDrawingMouseMove(e);
75106
75635
  }
75636
+ // Drag-erase: route move to sphereBrushTool when sphereEraser is active
75637
+ if (this.sphereBrushTool.isActive && this.state.gui_states.mode.sphereEraser) {
75638
+ this.sphereBrushTool.onSphereEraserMove(e);
75639
+ }
75107
75640
  });
75108
75641
  this.eventRouter.setPointerUpHandler((e) => {
75109
75642
  if (this.drawingTool.isActive || this.drawingTool.painting
75110
75643
  || this.panTool.isActive
75111
- || (this.state.gui_states.mode.sphere && this.eventRouter.getMode() !== 'crosshair')) {
75644
+ || (this.state.gui_states.mode.sphere && this.eventRouter.getMode() !== 'crosshair')
75645
+ || this.sphereBrushTool.isActive) {
75112
75646
  this.drawingPrameters.handleOnDrawingMouseUp(e);
75113
75647
  }
75114
75648
  });
@@ -75123,6 +75657,9 @@ class DrawToolCore {
75123
75657
  else if (this.activeWheelMode === 'sphere') {
75124
75658
  this.drawingPrameters.handleSphereWheel(e);
75125
75659
  }
75660
+ else if (this.activeWheelMode === 'sphereBrush') {
75661
+ this.drawingPrameters.handleSphereBrushWheel(e);
75662
+ }
75126
75663
  });
75127
75664
  // Bind all event listeners
75128
75665
  this.eventRouter.bindAll();
@@ -75186,6 +75723,8 @@ class DrawToolCore {
75186
75723
  }
75187
75724
  // sphere Wheel
75188
75725
  this.drawingPrameters.handleSphereWheel = this.configMouseSphereWheel();
75726
+ // sphere brush Wheel
75727
+ this.drawingPrameters.handleSphereBrushWheel = this.sphereBrushTool.configSphereBrushWheel();
75189
75728
  // brush circle move — delegated to DrawingTool
75190
75729
  this.drawingPrameters.handleOnDrawingBrushCricleMove =
75191
75730
  this.drawingTool.createBrushTrackingHandler();
@@ -75216,6 +75755,16 @@ class DrawToolCore {
75216
75755
  e.offsetY / this.state.nrrd_states.view.sizeFactor;
75217
75756
  this.enableCrosshair();
75218
75757
  }
75758
+ else if (this.state.gui_states.mode.sphereBrush && !this.eventRouter.isCrosshairEnabled()) {
75759
+ // SphereBrush: direct click (no Shift needed)
75760
+ this.activeWheelMode = 'sphereBrush';
75761
+ this.sphereBrushTool.onSphereBrushClick(e);
75762
+ }
75763
+ else if (this.state.gui_states.mode.sphereEraser && !this.eventRouter.isCrosshairEnabled()) {
75764
+ // SphereEraser: direct click (Shift handled at mode level)
75765
+ this.activeWheelMode = 'sphereBrush';
75766
+ this.sphereBrushTool.onSphereEraserClick(e);
75767
+ }
75219
75768
  else if (this.state.gui_states.mode.sphere && !this.eventRouter.isCrosshairEnabled()) {
75220
75769
  this.handleSphereClick(e);
75221
75770
  }
@@ -75237,6 +75786,16 @@ class DrawToolCore {
75237
75786
  this.drawingTool.onPointerUp(e);
75238
75787
  this.activeWheelMode = 'zoom';
75239
75788
  }
75789
+ else if (this.sphereBrushTool.isActive) {
75790
+ // SphereBrush or SphereEraser pointer-up
75791
+ if (this.state.gui_states.mode.sphereBrush) {
75792
+ this.sphereBrushTool.onSphereBrushPointerUp();
75793
+ }
75794
+ else if (this.state.gui_states.mode.sphereEraser) {
75795
+ this.sphereBrushTool.onSphereEraserPointerUp();
75796
+ }
75797
+ this.activeWheelMode = 'zoom';
75798
+ }
75240
75799
  else if (this.state.gui_states.mode.sphere &&
75241
75800
  !this.eventRouter.isCrosshairEnabled()) {
75242
75801
  this.sphereTool.onSpherePointerUp();
@@ -75281,7 +75840,9 @@ class DrawToolCore {
75281
75840
  }
75282
75841
  }
75283
75842
  this.state.protectedData.ctxes.drawingCtx.drawImage(this.state.protectedData.canvases.drawingCanvasLayerMaster, 0, 0);
75284
- if (this.state.gui_states.mode.sphere) {
75843
+ if (this.state.gui_states.mode.sphere
75844
+ || this.state.gui_states.mode.sphereBrush
75845
+ || this.state.gui_states.mode.sphereEraser) {
75285
75846
  this.state.protectedData.ctxes.drawingCtx.drawImage(this.state.protectedData.canvases.drawingSphereCanvas, 0, 0, this.state.nrrd_states.view.changedWidth, this.state.nrrd_states.view.changedHeight);
75286
75847
  }
75287
75848
  }
@@ -75468,24 +76029,26 @@ class DrawToolCore {
75468
76029
  * Undo the last drawing operation on the active layer.
75469
76030
  */
75470
76031
  undoLastPainting() {
75471
- const delta = this.undoManager.undo();
75472
- if (!delta)
75473
- return;
75474
- try {
75475
- const vol = this.renderer.getVolumeForLayer(delta.layerId);
75476
- vol.setSliceUint8(delta.sliceIndex, delta.oldSlice, delta.axis);
75477
- }
75478
- catch (_a) {
76032
+ const entry = this.undoManager.undo();
76033
+ if (!entry)
75479
76034
  return;
75480
- }
75481
76035
  this.state.protectedData.isDrawing = true;
75482
- if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
75483
- this.applyUndoRedoToCanvas(delta.layerId);
75484
- }
75485
- if (!this.state.nrrd_states.flags.loadingMaskData) {
75486
- const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
75487
- .getSliceUint8(delta.sliceIndex, delta.axis);
75488
- this.state.annotationCallbacks.onMaskChanged(sliceData, delta.layerId, this.state.gui_states.layerChannel.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
76036
+ for (const delta of entry) {
76037
+ try {
76038
+ const vol = this.renderer.getVolumeForLayer(delta.layerId);
76039
+ vol.setSliceUint8(delta.sliceIndex, delta.oldSlice, delta.axis);
76040
+ }
76041
+ catch (_a) {
76042
+ continue;
76043
+ }
76044
+ if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
76045
+ this.applyUndoRedoToCanvas(delta.layerId);
76046
+ }
76047
+ if (!this.state.nrrd_states.flags.loadingMaskData) {
76048
+ const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
76049
+ .getSliceUint8(delta.sliceIndex, delta.axis);
76050
+ this.state.annotationCallbacks.onMaskChanged(sliceData, delta.layerId, this.state.gui_states.layerChannel.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
76051
+ }
75489
76052
  }
75490
76053
  this.setIsDrawFalse(1000);
75491
76054
  }
@@ -75493,24 +76056,26 @@ class DrawToolCore {
75493
76056
  * Redo the last undone operation on the active layer.
75494
76057
  */
75495
76058
  redoLastPainting() {
75496
- const delta = this.undoManager.redo();
75497
- if (!delta)
75498
- return;
75499
- try {
75500
- const vol = this.renderer.getVolumeForLayer(delta.layerId);
75501
- vol.setSliceUint8(delta.sliceIndex, delta.newSlice, delta.axis);
75502
- }
75503
- catch (_a) {
76059
+ const entry = this.undoManager.redo();
76060
+ if (!entry)
75504
76061
  return;
75505
- }
75506
76062
  this.state.protectedData.isDrawing = true;
75507
- if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
75508
- this.applyUndoRedoToCanvas(delta.layerId);
75509
- }
75510
- if (!this.state.nrrd_states.flags.loadingMaskData) {
75511
- const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
75512
- .getSliceUint8(delta.sliceIndex, delta.axis);
75513
- this.state.annotationCallbacks.onMaskChanged(sliceData, delta.layerId, this.state.gui_states.layerChannel.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
76063
+ for (const delta of entry) {
76064
+ try {
76065
+ const vol = this.renderer.getVolumeForLayer(delta.layerId);
76066
+ vol.setSliceUint8(delta.sliceIndex, delta.newSlice, delta.axis);
76067
+ }
76068
+ catch (_a) {
76069
+ continue;
76070
+ }
76071
+ if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
76072
+ this.applyUndoRedoToCanvas(delta.layerId);
76073
+ }
76074
+ if (!this.state.nrrd_states.flags.loadingMaskData) {
76075
+ const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
76076
+ .getSliceUint8(delta.sliceIndex, delta.axis);
76077
+ this.state.annotationCallbacks.onMaskChanged(sliceData, delta.layerId, this.state.gui_states.layerChannel.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
76078
+ }
75514
76079
  }
75515
76080
  this.setIsDrawFalse(1000);
75516
76081
  }
@@ -75584,6 +76149,14 @@ class DrawToolCore {
75584
76149
  exitSphereMode() {
75585
76150
  throw new Error("exitSphereMode must be provided by NrrdTools");
75586
76151
  }
76152
+ /** Override in NrrdTools */
76153
+ reloadMasksFromVolume() {
76154
+ throw new Error("reloadMasksFromVolume must be provided by NrrdTools");
76155
+ }
76156
+ /** Override in NrrdTools */
76157
+ updateMouseWheelEvent() {
76158
+ throw new Error("updateMouseWheelEvent must be provided by NrrdTools");
76159
+ }
75587
76160
  }
75588
76161
 
75589
76162
  /**
@@ -75647,6 +76220,7 @@ class NrrdState {
75647
76220
  nippleSphereOrigin: null,
75648
76221
  sphereMaskVolume: null,
75649
76222
  sphereRadius: 5,
76223
+ sphereBrushRadius: 10,
75650
76224
  };
75651
76225
  this.flags = {
75652
76226
  stepClear: 1,
@@ -75668,6 +76242,7 @@ class NrrdState {
75668
76242
  this.sphere.nippleSphereOrigin = null;
75669
76243
  this.sphere.sphereMaskVolume = null;
75670
76244
  this.sphere.sphereRadius = 5;
76245
+ this.sphere.sphereBrushRadius = 10;
75671
76246
  }
75672
76247
  }
75673
76248
 
@@ -75689,6 +76264,8 @@ class GuiState {
75689
76264
  pencil: true,
75690
76265
  eraser: false,
75691
76266
  sphere: false,
76267
+ sphereBrush: false,
76268
+ sphereEraser: false,
75692
76269
  activeSphereType: "tumour",
75693
76270
  };
75694
76271
  this.drawing = {
@@ -76648,6 +77225,8 @@ class NrrdTools {
76648
77225
  this.drawCore.enterSphereMode = () => this.enterSphereMode();
76649
77226
  this.drawCore.exitSphereMode = () => this.exitSphereMode();
76650
77227
  this.drawCore.configMouseSliceWheel = () => this.configMouseSliceWheel();
77228
+ this.drawCore.reloadMasksFromVolume = () => this.reloadMasksFromVolume();
77229
+ this.drawCore.updateMouseWheelEvent = () => this.updateMouseWheelEvent();
76651
77230
  }
76652
77231
  /**
76653
77232
  * A initialise function for nrrd_tools
@@ -76768,13 +77347,18 @@ class NrrdTools {
76768
77347
  * Set the current tool mode.
76769
77348
  */
76770
77349
  setMode(mode) {
77350
+ var _a, _b, _c;
76771
77351
  if (!this.guiCallbacks)
76772
77352
  return;
76773
77353
  const prevSphere = this.state.gui_states.mode.sphere;
77354
+ const prevSphereBrush = this.state.gui_states.mode.sphereBrush;
77355
+ const prevSphereEraser = this.state.gui_states.mode.sphereEraser;
76774
77356
  const prevCalculator = this._calculatorActive;
76775
77357
  this.state.gui_states.mode.pencil = false;
76776
77358
  this.state.gui_states.mode.eraser = false;
76777
77359
  this.state.gui_states.mode.sphere = false;
77360
+ this.state.gui_states.mode.sphereBrush = false;
77361
+ this.state.gui_states.mode.sphereEraser = false;
76778
77362
  this._calculatorActive = false;
76779
77363
  switch (mode) {
76780
77364
  case "pencil":
@@ -76795,6 +77379,24 @@ class NrrdTools {
76795
77379
  this.state.gui_states.mode.sphere = true;
76796
77380
  this._calculatorActive = true;
76797
77381
  break;
77382
+ case "sphereBrush":
77383
+ this.state.gui_states.mode.sphereBrush = true;
77384
+ this.dragOperator.removeDragMode();
77385
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool('sphereBrush');
77386
+ break;
77387
+ case "sphereEraser":
77388
+ this.state.gui_states.mode.sphereEraser = true;
77389
+ this.dragOperator.removeDragMode();
77390
+ (_b = this.drawCore.eventRouter) === null || _b === void 0 ? void 0 : _b.setGuiTool('sphereEraser');
77391
+ break;
77392
+ }
77393
+ // Restore drag mode when leaving sphereBrush/sphereEraser
77394
+ if ((prevSphereBrush || prevSphereEraser)
77395
+ && !this.state.gui_states.mode.sphereBrush
77396
+ && !this.state.gui_states.mode.sphereEraser
77397
+ && !this.state.gui_states.mode.sphere) {
77398
+ this.dragOperator.configDragMode();
77399
+ (_c = this.drawCore.eventRouter) === null || _c === void 0 ? void 0 : _c.setGuiTool('pencil');
76798
77400
  }
76799
77401
  if (prevSphere && !this.state.gui_states.mode.sphere) {
76800
77402
  this.guiCallbacks.updateSphereState();
@@ -76807,6 +77409,10 @@ class NrrdTools {
76807
77409
  getMode() {
76808
77410
  if (this._calculatorActive)
76809
77411
  return "calculator";
77412
+ if (this.state.gui_states.mode.sphereBrush)
77413
+ return "sphereBrush";
77414
+ if (this.state.gui_states.mode.sphereEraser)
77415
+ return "sphereEraser";
76810
77416
  if (this.state.gui_states.mode.sphere)
76811
77417
  return "sphere";
76812
77418
  if (this.state.gui_states.mode.eraser)
@@ -76832,6 +77438,12 @@ class NrrdTools {
76832
77438
  getBrushSize() {
76833
77439
  return this.state.gui_states.drawing.brushAndEraserSize;
76834
77440
  }
77441
+ setSphereBrushRadius(radius) {
77442
+ this.state.nrrd_states.sphere.sphereBrushRadius = Math.max(1, Math.min(50, radius));
77443
+ }
77444
+ getSphereBrushRadius() {
77445
+ return this.state.nrrd_states.sphere.sphereBrushRadius;
77446
+ }
76835
77447
  setWindowHigh(value) {
76836
77448
  var _a;
76837
77449
  this.state.gui_states.viewConfig.readyToUpdate = false;
@@ -77381,10 +77993,15 @@ class NrrdTools {
77381
77993
  // ═══════════════════════════════════════════════════════════════════════════
77382
77994
  configMouseSliceWheel() {
77383
77995
  const handleMouseZoomSliceWheelMove = (e) => {
77384
- var _a;
77996
+ var _a, _b;
77385
77997
  if ((_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
77386
77998
  return;
77387
77999
  }
78000
+ // Block slice wheel when sphereBrush/sphereEraser is actively placing (left button held)
78001
+ if ((this.state.gui_states.mode.sphereBrush || this.state.gui_states.mode.sphereEraser)
78002
+ && ((_b = this.drawCore.eventRouter) === null || _b === void 0 ? void 0 : _b.isLeftButtonDown())) {
78003
+ return;
78004
+ }
77388
78005
  e.preventDefault();
77389
78006
  if (e.deltaY < 0) {
77390
78007
  this.setSliceMoving(-1);
@@ -77652,7 +78269,7 @@ function evaluateElement(element, weights) {
77652
78269
  }
77653
78270
 
77654
78271
  // import * as kiwrious from "copper3d_plugin_heart_k";
77655
- const REVISION = "v3.1.2-beta";
78272
+ const REVISION = "v3.2.0-beta";
77656
78273
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
77657
78274
 
77658
78275
  export { CHANNEL_COLORS, CHANNEL_HEX_COLORS, CameraViewPoint, Copper3dTrackballControls, MeshNodeTool, NrrdTools, REVISION, addBoxHelper, addLabelToScene, configKiwriousHeart, convert3DPostoScreenPos, convertScreenPosto3DPos, copperMScene, copperMSceneRenderer, copperRenderer, copperRendererOnDemond, copperScene, copperSceneOnDemond, createTexture2D_NRRD, fullScreenListenner, kiwrious, loading, removeGuiFolderChilden, rgbaToCss, rgbaToHex, setHDRFilePath, throttle };