copper3d 3.1.1 → 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 (55) hide show
  1. package/dist/Controls/copperControls.js +3 -1
  2. package/dist/Controls/copperControls.js.map +1 -1
  3. package/dist/Scene/baseScene.js +1 -0
  4. package/dist/Scene/baseScene.js.map +1 -1
  5. package/dist/Scene/copperMScene.js +4 -0
  6. package/dist/Scene/copperMScene.js.map +1 -1
  7. package/dist/Scene/copperScene.d.ts +1 -0
  8. package/dist/Scene/copperScene.js +16 -0
  9. package/dist/Scene/copperScene.js.map +1 -1
  10. package/dist/Utils/segmentation/DragOperator.js +3 -1
  11. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  12. package/dist/Utils/segmentation/DrawToolCore.d.ts +6 -0
  13. package/dist/Utils/segmentation/DrawToolCore.js +107 -35
  14. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  15. package/dist/Utils/segmentation/NrrdTools.d.ts +2 -0
  16. package/dist/Utils/segmentation/NrrdTools.js +41 -1
  17. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  18. package/dist/Utils/segmentation/core/UndoManager.d.ts +9 -5
  19. package/dist/Utils/segmentation/core/UndoManager.js +21 -14
  20. package/dist/Utils/segmentation/core/UndoManager.js.map +1 -1
  21. package/dist/Utils/segmentation/core/index.d.ts +1 -1
  22. package/dist/Utils/segmentation/core/types.d.ts +5 -1
  23. package/dist/Utils/segmentation/core/types.js.map +1 -1
  24. package/dist/Utils/segmentation/coreTools/GuiState.js +2 -0
  25. package/dist/Utils/segmentation/coreTools/GuiState.js.map +1 -1
  26. package/dist/Utils/segmentation/coreTools/NrrdState.js +2 -0
  27. package/dist/Utils/segmentation/coreTools/NrrdState.js.map +1 -1
  28. package/dist/Utils/segmentation/eventRouter/EventRouter.js +1 -0
  29. package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -1
  30. package/dist/Utils/segmentation/eventRouter/types.d.ts +1 -1
  31. package/dist/Utils/segmentation/tools/SphereBrushTool.d.ts +127 -0
  32. package/dist/Utils/segmentation/tools/SphereBrushTool.js +489 -0
  33. package/dist/Utils/segmentation/tools/SphereBrushTool.js.map +1 -0
  34. package/dist/Utils/segmentation/tools/ToolHost.d.ts +3 -0
  35. package/dist/Utils/segmentation/tools/ZoomTool.js +6 -1
  36. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -1
  37. package/dist/Utils/segmentation/tools/index.d.ts +2 -1
  38. package/dist/Utils/segmentation/tools/index.js +1 -0
  39. package/dist/Utils/segmentation/tools/index.js.map +1 -1
  40. package/dist/bundle.esm.js +694 -54
  41. package/dist/bundle.umd.js +694 -54
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/types/Scene/copperScene.d.ts +1 -0
  45. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +6 -0
  46. package/dist/types/Utils/segmentation/NrrdTools.d.ts +2 -0
  47. package/dist/types/Utils/segmentation/core/UndoManager.d.ts +9 -5
  48. package/dist/types/Utils/segmentation/core/index.d.ts +1 -1
  49. package/dist/types/Utils/segmentation/core/types.d.ts +5 -1
  50. package/dist/types/Utils/segmentation/eventRouter/types.d.ts +1 -1
  51. package/dist/types/Utils/segmentation/tools/SphereBrushTool.d.ts +127 -0
  52. package/dist/types/Utils/segmentation/tools/ToolHost.d.ts +3 -0
  53. package/dist/types/Utils/segmentation/tools/index.d.ts +2 -1
  54. package/dist/types/index.d.ts +1 -1
  55. package/package.json +1 -1
@@ -40379,8 +40379,10 @@ void main() {
40379
40379
  this.currentCamera.near = this.viewpoint.nearPlane;
40380
40380
  this.currentCamera.far = this.viewpoint.farPlane;
40381
40381
  this.currentCamera.position.set(this.viewpoint.eyePosition[0], this.viewpoint.eyePosition[1], this.viewpoint.eyePosition[2]);
40382
- this.currentCamera.lookAt(this.viewpoint.targetPosition[0], this.viewpoint.targetPosition[1], this.viewpoint.targetPosition[2]);
40382
+ // up must be set BEFORE lookAt — Three.js lookAt() uses the up vector
40383
+ // to compute the view orientation matrix
40383
40384
  this.currentCamera.up.set(this.viewpoint.upVector[0], this.viewpoint.upVector[1], this.viewpoint.upVector[2]);
40385
+ this.currentCamera.lookAt(this.viewpoint.targetPosition[0], this.viewpoint.targetPosition[1], this.viewpoint.targetPosition[2]);
40384
40386
  this.currentCamera.updateProjectionMatrix();
40385
40387
  this.updateDirectionalLight();
40386
40388
  }
@@ -49868,6 +49870,7 @@ void main() {
49868
49870
  viewpoint.targetPosition = viewpointData.targetPosition;
49869
49871
  viewpoint.upVector = viewpointData.upVector;
49870
49872
  this.copperControl.updateCameraViewPoint(viewpoint);
49873
+ this.camera.updateProjectionMatrix();
49871
49874
  }
49872
49875
  getDefaultViewPoint() {
49873
49876
  return this.viewPoint;
@@ -59618,6 +59621,22 @@ void main() {
59618
59621
  if (position.z)
59619
59622
  model.position.z = position.z;
59620
59623
  }
59624
+ loadView(viewpointData) {
59625
+ super.loadView(viewpointData);
59626
+ // Sync TrackballControls with the new viewpoint
59627
+ if (this.controls) {
59628
+ const controls = this.controls;
59629
+ if (viewpointData.targetPosition) {
59630
+ controls.target.set(viewpointData.targetPosition[0], viewpointData.targetPosition[1], viewpointData.targetPosition[2]);
59631
+ // Update target0 so controls.reset() returns to this view
59632
+ controls.target0.copy(controls.target);
59633
+ }
59634
+ // Save position0/up0 so controls.reset() returns to this view
59635
+ controls.position0.copy(this.camera.position);
59636
+ controls.up0.copy(this.camera.up);
59637
+ controls.zoom0 = this.camera.zoom;
59638
+ }
59639
+ }
59621
59640
  resetView() {
59622
59641
  this.controls.reset();
59623
59642
  this.updateCamera(this.viewPoint);
@@ -60195,6 +60214,10 @@ void main() {
60195
60214
  if (this.subCopperControl) {
60196
60215
  this.subCopperControl.updateCameraViewPoint(viewpoint);
60197
60216
  }
60217
+ // Sync TrackballControls target with the new viewpoint target position
60218
+ if (this.controls && viewpointData.targetPosition) {
60219
+ this.controls.target.set(viewpointData.targetPosition[0], viewpointData.targetPosition[1], viewpointData.targetPosition[2]);
60220
+ }
60198
60221
  }
60199
60222
  updateCamera(viewpoint) {
60200
60223
  this.cameraPositionFlag = true;
@@ -71940,41 +71963,48 @@ void main() {
71940
71963
  setActiveLayer(layer) {
71941
71964
  this.activeLayer = layer;
71942
71965
  }
71943
- /** Push a delta onto the active layer's undo stack and clear the redo stack. */
71966
+ /** Push a single delta onto the active layer's undo stack and clear the redo stack. */
71944
71967
  push(delta) {
71968
+ this.pushGroup([delta]);
71969
+ }
71970
+ /** Push a group of deltas as a single undo unit (e.g. 3D sphere spanning multiple slices). */
71971
+ pushGroup(deltas) {
71945
71972
  var _a, _b;
71946
- const stack = (_a = this.undoStacks.get(delta.layerId)) !== null && _a !== void 0 ? _a : this.undoStacks.get("layer1");
71947
- stack.push(delta);
71973
+ if (deltas.length === 0)
71974
+ return;
71975
+ const layerId = deltas[0].layerId;
71976
+ const stack = (_a = this.undoStacks.get(layerId)) !== null && _a !== void 0 ? _a : this.undoStacks.get("layer1");
71977
+ stack.push(deltas);
71948
71978
  if (stack.length > MAX_STACK_SIZE) {
71949
71979
  stack.shift();
71950
71980
  }
71951
71981
  // Any new operation invalidates the redo history for that layer
71952
- const redoStack = (_b = this.redoStacks.get(delta.layerId)) !== null && _b !== void 0 ? _b : this.redoStacks.get("layer1");
71982
+ const redoStack = (_b = this.redoStacks.get(layerId)) !== null && _b !== void 0 ? _b : this.redoStacks.get("layer1");
71953
71983
  redoStack.length = 0;
71954
71984
  }
71955
71985
  /**
71956
71986
  * Undo the last operation on the active layer.
71957
- * @returns The delta that was undone, or undefined if nothing to undo.
71987
+ * @returns The delta(s) that were undone, or undefined if nothing to undo.
71958
71988
  */
71959
71989
  undo() {
71960
71990
  const stack = this.undoStacks.get(this.activeLayer);
71961
- const delta = stack.pop();
71962
- if (delta) {
71963
- this.redoStacks.get(this.activeLayer).push(delta);
71991
+ const entry = stack.pop();
71992
+ if (entry) {
71993
+ this.redoStacks.get(this.activeLayer).push(entry);
71964
71994
  }
71965
- return delta;
71995
+ return entry;
71966
71996
  }
71967
71997
  /**
71968
71998
  * Redo the last undone operation on the active layer.
71969
- * @returns The delta that was redone, or undefined if nothing to redo.
71999
+ * @returns The delta(s) that were redone, or undefined if nothing to redo.
71970
72000
  */
71971
72001
  redo() {
71972
72002
  const stack = this.redoStacks.get(this.activeLayer);
71973
- const delta = stack.pop();
71974
- if (delta) {
71975
- this.undoStacks.get(this.activeLayer).push(delta);
72003
+ const entry = stack.pop();
72004
+ if (entry) {
72005
+ this.undoStacks.get(this.activeLayer).push(entry);
71976
72006
  }
71977
- return delta;
72007
+ return entry;
71978
72008
  }
71979
72009
  canUndo() {
71980
72010
  var _a, _b;
@@ -73343,7 +73373,9 @@ void main() {
73343
73373
  // When leaving draw, contrast, or crosshair mode (returning to idle), restore drag mode
73344
73374
  // Do NOT restore if sphere mode is active
73345
73375
  if ((prev === 'draw' || prev === 'contrast' || prev === 'crosshair') && next === 'idle') {
73346
- if (!this.gui_states.mode.sphere) {
73376
+ if (!this.gui_states.mode.sphere
73377
+ && !this.gui_states.mode.sphereBrush
73378
+ && !this.gui_states.mode.sphereEraser) {
73347
73379
  this.configDragMode();
73348
73380
  }
73349
73381
  }
@@ -73882,6 +73914,7 @@ void main() {
73882
73914
  if (ev.key === this.keyboardSettings.draw) {
73883
73915
  this.state.shiftHeld = true;
73884
73916
  // Block draw mode when crosshair or contrast is active (mutual exclusion)
73917
+ // Also block when sphereBrush or sphereEraser is active
73885
73918
  if (DRAWING_TOOLS.has(this.guiTool) && !this.state.ctrlHeld && !this.state.crosshairEnabled) {
73886
73919
  this.setMode('draw');
73887
73920
  }
@@ -74281,10 +74314,15 @@ void main() {
74281
74314
  configMouseZoomWheel() {
74282
74315
  let moveDistance = 1;
74283
74316
  return (e) => {
74284
- var _a;
74317
+ var _a, _b;
74285
74318
  if ((_a = this.ctx.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
74286
74319
  return;
74287
74320
  }
74321
+ // Block zoom wheel when sphereBrush/sphereEraser is actively placing (left button held)
74322
+ if ((this.ctx.gui_states.mode.sphereBrush || this.ctx.gui_states.mode.sphereEraser)
74323
+ && ((_b = this.ctx.eventRouter) === null || _b === void 0 ? void 0 : _b.isLeftButtonDown())) {
74324
+ return;
74325
+ }
74288
74326
  e.preventDefault();
74289
74327
  const delta = e.detail ? e.detail > 0 : e.wheelDelta < 0;
74290
74328
  this.ctx.protectedData.isDrawing = true;
@@ -74886,6 +74924,493 @@ void main() {
74886
74924
  }
74887
74925
  }
74888
74926
 
74927
+ /**
74928
+ * SphereBrushTool — 3D sphere brush and eraser on layer MaskVolume.
74929
+ *
74930
+ * Unlike SphereTool (which uses its own isolated sphereMaskVolume),
74931
+ * this tool writes/erases directly in the active layer's shared MaskVolume —
74932
+ * the same volume used by pencil/brush/eraser tools.
74933
+ *
74934
+ * Two operation modes handled by one class:
74935
+ * - **sphereBrush**: Direct click to paint a 3D sphere of the active channel label.
74936
+ * - **sphereEraser**: Shift+click to erase a 3D sphere (only voxels matching active channel).
74937
+ *
74938
+ * Interaction:
74939
+ * 1. Pointer-down records sphere center, switches wheel to radius mode.
74940
+ * 2. Scroll wheel adjusts sphereBrushRadius [1, 50].
74941
+ * 3. Pointer-up writes/erases sphere to volume, captures grouped undo, refreshes canvas.
74942
+ */
74943
+ class SphereBrushTool extends BaseTool {
74944
+ constructor(ctx, callbacks) {
74945
+ super(ctx);
74946
+ /** Recorded sphere center in canvas mm-space */
74947
+ this.centerX = 0;
74948
+ this.centerY = 0;
74949
+ this.centerSlice = 0;
74950
+ /** Whether a sphere placement is in progress */
74951
+ this.active = false;
74952
+ /** Current operation mode for the active placement */
74953
+ this.mode = "brush";
74954
+ /** Cumulative "before" snapshots for drag-erase undo (z-index → slice data) */
74955
+ this.dragBeforeSnapshots = new Map();
74956
+ /** Whether a drag-erase has actually moved (vs. simple click-release) */
74957
+ this.dragMoved = false;
74958
+ this.callbacks = callbacks;
74959
+ }
74960
+ setCallbacks(callbacks) {
74961
+ this.callbacks = callbacks;
74962
+ }
74963
+ // ── Geometry (ported from SphereTool) ──────────────────────────
74964
+ /**
74965
+ * Convert canvas mm-space coordinates to 3D voxel coordinates.
74966
+ */
74967
+ canvasToVoxelCenter(canvasX, canvasY, sliceIndex, axis) {
74968
+ const nrrd = this.ctx.nrrd_states;
74969
+ switch (axis) {
74970
+ case 'z':
74971
+ return {
74972
+ x: canvasX * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm,
74973
+ y: canvasY * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm,
74974
+ z: sliceIndex,
74975
+ };
74976
+ case 'y':
74977
+ return {
74978
+ x: canvasX * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm,
74979
+ y: sliceIndex,
74980
+ z: (nrrd.image.nrrd_z_mm - canvasY) * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm,
74981
+ };
74982
+ case 'x':
74983
+ return {
74984
+ x: sliceIndex,
74985
+ y: canvasY * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm,
74986
+ z: canvasX * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm,
74987
+ };
74988
+ }
74989
+ }
74990
+ /**
74991
+ * Convert mm radius to per-axis voxel radii.
74992
+ */
74993
+ getVoxelRadii(radius) {
74994
+ const nrrd = this.ctx.nrrd_states;
74995
+ return {
74996
+ rx: radius * nrrd.image.nrrd_x_pixel / nrrd.image.nrrd_x_mm,
74997
+ ry: radius * nrrd.image.nrrd_y_pixel / nrrd.image.nrrd_y_mm,
74998
+ rz: radius * nrrd.image.nrrd_z_pixel / nrrd.image.nrrd_z_mm,
74999
+ };
75000
+ }
75001
+ /**
75002
+ * Compute clamped bounding box for an ellipsoid in voxel space.
75003
+ */
75004
+ getBoundingBox(center, radii, dims) {
75005
+ return {
75006
+ minX: Math.max(0, Math.floor(center.x - radii.rx)),
75007
+ maxX: Math.min(dims.width - 1, Math.ceil(center.x + radii.rx)),
75008
+ minY: Math.max(0, Math.floor(center.y - radii.ry)),
75009
+ maxY: Math.min(dims.height - 1, Math.ceil(center.y + radii.ry)),
75010
+ minZ: Math.max(0, Math.floor(center.z - radii.rz)),
75011
+ maxZ: Math.min(dims.depth - 1, Math.ceil(center.z + radii.rz)),
75012
+ };
75013
+ }
75014
+ // ── 3D Volume Write / Erase ────────────────────────────────────
75015
+ /**
75016
+ * Write a 3D sphere of the active channel label into the layer's MaskVolume.
75017
+ */
75018
+ write3DSphereToBrush(vol, center, radii, bb, label) {
75019
+ const { rx, ry, rz } = radii;
75020
+ for (let z = bb.minZ; z <= bb.maxZ; z++) {
75021
+ for (let y = bb.minY; y <= bb.maxY; y++) {
75022
+ for (let x = bb.minX; x <= bb.maxX; x++) {
75023
+ const dx = rx > 0 ? (x - center.x) / rx : 0;
75024
+ const dy = ry > 0 ? (y - center.y) / ry : 0;
75025
+ const dz = rz > 0 ? (z - center.z) / rz : 0;
75026
+ if (dx * dx + dy * dy + dz * dz <= 1.0) {
75027
+ vol.setVoxel(x, y, z, label);
75028
+ }
75029
+ }
75030
+ }
75031
+ }
75032
+ }
75033
+ /**
75034
+ * Erase a 3D sphere from the layer's MaskVolume.
75035
+ * Only clears voxels whose current value equals the active channel label.
75036
+ */
75037
+ erase3DSphereFromVolume(vol, center, radii, bb, label) {
75038
+ const { rx, ry, rz } = radii;
75039
+ for (let z = bb.minZ; z <= bb.maxZ; z++) {
75040
+ for (let y = bb.minY; y <= bb.maxY; y++) {
75041
+ for (let x = bb.minX; x <= bb.maxX; x++) {
75042
+ const dx = rx > 0 ? (x - center.x) / rx : 0;
75043
+ const dy = ry > 0 ? (y - center.y) / ry : 0;
75044
+ const dz = rz > 0 ? (z - center.z) / rz : 0;
75045
+ if (dx * dx + dy * dy + dz * dz <= 1.0) {
75046
+ if (vol.getVoxel(x, y, z) === label) {
75047
+ vol.setVoxel(x, y, z, 0);
75048
+ }
75049
+ }
75050
+ }
75051
+ }
75052
+ }
75053
+ }
75054
+ // ── Undo Capture ───────────────────────────────────────────────
75055
+ /**
75056
+ * Capture slice snapshots for all Z-slices in the bounding box.
75057
+ * Used before and after the sphere operation to build MaskDelta groups.
75058
+ *
75059
+ * We always capture along the Z axis since the 3D sphere spans Z slices.
75060
+ */
75061
+ captureSliceSnapshots(vol, minZ, maxZ) {
75062
+ const snapshots = new Map();
75063
+ for (let z = minZ; z <= maxZ; z++) {
75064
+ try {
75065
+ const { data } = vol.getSliceUint8(z, 'z');
75066
+ snapshots.set(z, data.slice());
75067
+ }
75068
+ catch (_a) {
75069
+ // slice out of bounds — skip
75070
+ }
75071
+ }
75072
+ return snapshots;
75073
+ }
75074
+ /**
75075
+ * Build MaskDelta group from before/after snapshots.
75076
+ */
75077
+ buildUndoGroup(layerId, before, after) {
75078
+ const deltas = [];
75079
+ for (const [sliceIndex, oldSlice] of before) {
75080
+ const newSlice = after.get(sliceIndex);
75081
+ if (!newSlice)
75082
+ continue;
75083
+ // Only include slices that actually changed
75084
+ let changed = false;
75085
+ for (let i = 0; i < oldSlice.length; i++) {
75086
+ if (oldSlice[i] !== newSlice[i]) {
75087
+ changed = true;
75088
+ break;
75089
+ }
75090
+ }
75091
+ if (changed) {
75092
+ deltas.push({ layerId, axis: 'z', sliceIndex, oldSlice, newSlice });
75093
+ }
75094
+ }
75095
+ return deltas;
75096
+ }
75097
+ // ── Preview Rendering ──────────────────────────────────────────
75098
+ /**
75099
+ * Draw a preview circle on the sphere canvas during sphere placement.
75100
+ */
75101
+ drawPreview(mouseX, mouseY, radius, isEraser) {
75102
+ const sphereCanvas = this.ctx.protectedData.canvases.drawingSphereCanvas;
75103
+ const sphereCtx = this.ctx.protectedData.ctxes.drawingSphereCtx;
75104
+ // Clear and resize sphere canvas
75105
+ sphereCanvas.width = this.ctx.protectedData.canvases.originCanvas.width;
75106
+ sphereCanvas.height = this.ctx.protectedData.canvases.originCanvas.height;
75107
+ sphereCtx.beginPath();
75108
+ sphereCtx.arc(mouseX, mouseY, radius, 0, 2 * Math.PI);
75109
+ if (isEraser) {
75110
+ // Eraser preview: dashed outline
75111
+ sphereCtx.strokeStyle = "#ff4444";
75112
+ sphereCtx.lineWidth = 2;
75113
+ sphereCtx.setLineDash([4, 4]);
75114
+ sphereCtx.stroke();
75115
+ sphereCtx.setLineDash([]);
75116
+ }
75117
+ else {
75118
+ // Brush preview: semi-transparent fill with active channel color
75119
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75120
+ const color = this.getActiveChannelColor(channel);
75121
+ sphereCtx.fillStyle = color;
75122
+ sphereCtx.globalAlpha = 0.5;
75123
+ sphereCtx.fill();
75124
+ sphereCtx.globalAlpha = 1.0;
75125
+ }
75126
+ sphereCtx.closePath();
75127
+ }
75128
+ /**
75129
+ * Clear the sphere preview canvas.
75130
+ */
75131
+ clearPreview() {
75132
+ const sphereCanvas = this.ctx.protectedData.canvases.drawingSphereCanvas;
75133
+ sphereCanvas.width = sphereCanvas.width;
75134
+ }
75135
+ /**
75136
+ * Get the hex color for the active channel from the layer's MaskVolume.
75137
+ */
75138
+ getActiveChannelColor(channel) {
75139
+ var _a;
75140
+ const layer = this.ctx.gui_states.layerChannel.layer;
75141
+ const volumes = this.ctx.protectedData.maskData.volumes;
75142
+ const vol = volumes[layer];
75143
+ if (vol) {
75144
+ const rgba = vol.getChannelColor(channel);
75145
+ const r = rgba.r.toString(16).padStart(2, '0');
75146
+ const g = rgba.g.toString(16).padStart(2, '0');
75147
+ const b = rgba.b.toString(16).padStart(2, '0');
75148
+ return `#${r}${g}${b}`;
75149
+ }
75150
+ return (_a = CHANNEL_HEX_COLORS[channel]) !== null && _a !== void 0 ? _a : "#ff0000";
75151
+ }
75152
+ // ── Wheel Handler ──────────────────────────────────────────────
75153
+ /**
75154
+ * Returns a wheel event handler for adjusting sphereBrushRadius.
75155
+ * Used in both sphereBrush and sphereEraser modes.
75156
+ */
75157
+ configSphereBrushWheel() {
75158
+ return (e) => {
75159
+ e.preventDefault();
75160
+ const sphere = this.ctx.nrrd_states.sphere;
75161
+ if (e.deltaY < 0) {
75162
+ sphere.sphereBrushRadius += 1;
75163
+ }
75164
+ else {
75165
+ sphere.sphereBrushRadius -= 1;
75166
+ }
75167
+ sphere.sphereBrushRadius = Math.max(1, Math.min(sphere.sphereBrushRadius, 50));
75168
+ // Redraw preview
75169
+ if (this.active) {
75170
+ this.drawPreview(this.centerX, this.centerY, sphere.sphereBrushRadius, this.mode === "eraser");
75171
+ }
75172
+ };
75173
+ }
75174
+ // ── Sphere Brush Click/PointerUp ───────────────────────────────
75175
+ /**
75176
+ * Handle pointer-down in sphereBrush mode (direct click, no Shift needed).
75177
+ */
75178
+ onSphereBrushClick(e) {
75179
+ this.centerX = e.offsetX / this.ctx.nrrd_states.view.sizeFactor;
75180
+ this.centerY = e.offsetY / this.ctx.nrrd_states.view.sizeFactor;
75181
+ this.centerSlice = this.ctx.nrrd_states.view.currentSliceIndex;
75182
+ this.active = true;
75183
+ this.mode = "brush";
75184
+ this.drawPreview(this.centerX, this.centerY, this.ctx.nrrd_states.sphere.sphereBrushRadius, false);
75185
+ }
75186
+ /**
75187
+ * Handle pointer-up in sphereBrush mode — write sphere to volume.
75188
+ */
75189
+ onSphereBrushPointerUp() {
75190
+ if (!this.active || this.mode !== "brush")
75191
+ return;
75192
+ this.active = false;
75193
+ const layer = this.ctx.gui_states.layerChannel.layer;
75194
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75195
+ const axis = this.ctx.protectedData.axis;
75196
+ const radius = this.ctx.nrrd_states.sphere.sphereBrushRadius;
75197
+ let vol;
75198
+ try {
75199
+ vol = this.callbacks.getVolumeForLayer(layer);
75200
+ }
75201
+ catch (_a) {
75202
+ this.clearPreview();
75203
+ return;
75204
+ }
75205
+ const dims = vol.getDimensions();
75206
+ const center = this.canvasToVoxelCenter(this.centerX, this.centerY, this.centerSlice, axis);
75207
+ const radii = this.getVoxelRadii(radius);
75208
+ const bb = this.getBoundingBox(center, radii, dims);
75209
+ // Capture pre-write snapshots
75210
+ const before = this.captureSliceSnapshots(vol, bb.minZ, bb.maxZ);
75211
+ // Write sphere
75212
+ this.write3DSphereToBrush(vol, center, radii, bb, channel);
75213
+ // Capture post-write snapshots and push undo group
75214
+ const after = this.captureSliceSnapshots(vol, bb.minZ, bb.maxZ);
75215
+ const deltas = this.buildUndoGroup(layer, before, after);
75216
+ if (deltas.length > 0) {
75217
+ this.callbacks.pushUndoGroup(deltas);
75218
+ }
75219
+ // Refresh display and notify backend of ALL changed slices
75220
+ this.refreshDisplay(layer, deltas);
75221
+ this.clearPreview();
75222
+ }
75223
+ // ── Sphere Eraser Click/PointerUp ──────────────────────────────
75224
+ /**
75225
+ * Handle pointer-down in sphereEraser mode.
75226
+ * Initializes drag tracking for cumulative undo.
75227
+ */
75228
+ onSphereEraserClick(e) {
75229
+ this.centerX = e.offsetX / this.ctx.nrrd_states.view.sizeFactor;
75230
+ this.centerY = e.offsetY / this.ctx.nrrd_states.view.sizeFactor;
75231
+ this.centerSlice = this.ctx.nrrd_states.view.currentSliceIndex;
75232
+ this.active = true;
75233
+ this.mode = "eraser";
75234
+ this.dragMoved = false;
75235
+ this.dragBeforeSnapshots.clear();
75236
+ // Capture initial "before" snapshots for the first sphere position
75237
+ const layer = this.ctx.gui_states.layerChannel.layer;
75238
+ const radius = this.ctx.nrrd_states.sphere.sphereBrushRadius;
75239
+ try {
75240
+ const vol = this.callbacks.getVolumeForLayer(layer);
75241
+ const dims = vol.getDimensions();
75242
+ const axis = this.ctx.protectedData.axis;
75243
+ const center = this.canvasToVoxelCenter(this.centerX, this.centerY, this.centerSlice, axis);
75244
+ const radii = this.getVoxelRadii(radius);
75245
+ const bb = this.getBoundingBox(center, radii, dims);
75246
+ this.expandDragBeforeSnapshots(vol, bb.minZ, bb.maxZ);
75247
+ }
75248
+ catch (_a) {
75249
+ // Volume not ready
75250
+ }
75251
+ this.drawPreview(this.centerX, this.centerY, radius, true);
75252
+ }
75253
+ /**
75254
+ * Handle pointer-move in sphereEraser mode — drag to continuously erase.
75255
+ */
75256
+ onSphereEraserMove(e) {
75257
+ if (!this.active || this.mode !== "eraser")
75258
+ return;
75259
+ this.dragMoved = true;
75260
+ const mouseX = e.offsetX / this.ctx.nrrd_states.view.sizeFactor;
75261
+ const mouseY = e.offsetY / this.ctx.nrrd_states.view.sizeFactor;
75262
+ const sliceIndex = this.ctx.nrrd_states.view.currentSliceIndex;
75263
+ const layer = this.ctx.gui_states.layerChannel.layer;
75264
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75265
+ const axis = this.ctx.protectedData.axis;
75266
+ const radius = this.ctx.nrrd_states.sphere.sphereBrushRadius;
75267
+ let vol;
75268
+ try {
75269
+ vol = this.callbacks.getVolumeForLayer(layer);
75270
+ }
75271
+ catch (_a) {
75272
+ return;
75273
+ }
75274
+ const dims = vol.getDimensions();
75275
+ const center = this.canvasToVoxelCenter(mouseX, mouseY, sliceIndex, axis);
75276
+ const radii = this.getVoxelRadii(radius);
75277
+ const bb = this.getBoundingBox(center, radii, dims);
75278
+ // Expand "before" snapshots to cover any new Z slices
75279
+ this.expandDragBeforeSnapshots(vol, bb.minZ, bb.maxZ);
75280
+ // Erase sphere at current position
75281
+ this.erase3DSphereFromVolume(vol, center, radii, bb, channel);
75282
+ // Update preview position
75283
+ this.drawPreview(mouseX, mouseY, radius, true);
75284
+ // Refresh display so user sees the erase in real-time
75285
+ this.refreshDisplay(layer);
75286
+ }
75287
+ /**
75288
+ * Handle pointer-up in sphereEraser mode — finalize erase + push undo group.
75289
+ * Works for both click-release and drag-release.
75290
+ */
75291
+ onSphereEraserPointerUp() {
75292
+ if (!this.active || this.mode !== "eraser")
75293
+ return;
75294
+ this.active = false;
75295
+ const layer = this.ctx.gui_states.layerChannel.layer;
75296
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75297
+ const axis = this.ctx.protectedData.axis;
75298
+ const radius = this.ctx.nrrd_states.sphere.sphereBrushRadius;
75299
+ let vol;
75300
+ try {
75301
+ vol = this.callbacks.getVolumeForLayer(layer);
75302
+ }
75303
+ catch (_a) {
75304
+ this.clearPreview();
75305
+ this.dragBeforeSnapshots.clear();
75306
+ return;
75307
+ }
75308
+ // If no drag occurred, erase at the click position (original click-release behavior)
75309
+ if (!this.dragMoved) {
75310
+ const dims = vol.getDimensions();
75311
+ const center = this.canvasToVoxelCenter(this.centerX, this.centerY, this.centerSlice, axis);
75312
+ const radii = this.getVoxelRadii(radius);
75313
+ const bb = this.getBoundingBox(center, radii, dims);
75314
+ this.erase3DSphereFromVolume(vol, center, radii, bb, channel);
75315
+ }
75316
+ // Build undo group from cumulative "before" snapshots vs current state
75317
+ const after = this.captureSliceSnapshots(vol, this.dragMinZ(), this.dragMaxZ());
75318
+ const deltas = this.buildUndoGroup(layer, this.dragBeforeSnapshots, after);
75319
+ if (deltas.length > 0) {
75320
+ this.callbacks.pushUndoGroup(deltas);
75321
+ }
75322
+ // Refresh display and notify backend of ALL changed slices
75323
+ this.refreshDisplay(layer, deltas);
75324
+ this.clearPreview();
75325
+ this.dragBeforeSnapshots.clear();
75326
+ }
75327
+ /**
75328
+ * Expand cumulative "before" snapshots to cover z range [minZ, maxZ].
75329
+ * Only captures slices not yet in the map (preserves original state).
75330
+ */
75331
+ expandDragBeforeSnapshots(vol, minZ, maxZ) {
75332
+ for (let z = minZ; z <= maxZ; z++) {
75333
+ if (!this.dragBeforeSnapshots.has(z)) {
75334
+ try {
75335
+ const { data } = vol.getSliceUint8(z, 'z');
75336
+ this.dragBeforeSnapshots.set(z, data.slice());
75337
+ }
75338
+ catch (_a) {
75339
+ // slice out of bounds
75340
+ }
75341
+ }
75342
+ }
75343
+ }
75344
+ /** Get minimum Z index from drag snapshots */
75345
+ dragMinZ() {
75346
+ let min = Infinity;
75347
+ for (const z of this.dragBeforeSnapshots.keys()) {
75348
+ if (z < min)
75349
+ min = z;
75350
+ }
75351
+ return min === Infinity ? 0 : min;
75352
+ }
75353
+ /** Get maximum Z index from drag snapshots */
75354
+ dragMaxZ() {
75355
+ let max = -Infinity;
75356
+ for (const z of this.dragBeforeSnapshots.keys()) {
75357
+ if (z > max)
75358
+ max = z;
75359
+ }
75360
+ return max === -Infinity ? 0 : max;
75361
+ }
75362
+ // ── Post-write Display Refresh ─────────────────────────────────
75363
+ /**
75364
+ * After writing/erasing the sphere in the volume, re-render the current
75365
+ * slice's layer canvas and composite all layers to master.
75366
+ *
75367
+ * @param changedDeltas - If provided, fire onMaskChanged for ALL changed
75368
+ * slices (not just the current view slice) so the backend receives the
75369
+ * full 3D sphere data for NII/GLTF export.
75370
+ */
75371
+ refreshDisplay(layerId, changedDeltas) {
75372
+ // Re-render layer canvas from volume for the current slice
75373
+ const target = this.ctx.protectedData.layerTargets.get(layerId);
75374
+ if (target) {
75375
+ const { ctx, canvas } = target;
75376
+ canvas.width = canvas.width; // clear
75377
+ const buffer = this.callbacks.getOrCreateSliceBuffer(this.ctx.protectedData.axis);
75378
+ if (buffer) {
75379
+ 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);
75380
+ }
75381
+ }
75382
+ // Composite all layers to master canvas
75383
+ this.callbacks.compositeAllLayers();
75384
+ // Fire onMaskChanged for ALL changed slices (not just the current view slice)
75385
+ // This ensures the backend receives the full 3D sphere data for export.
75386
+ try {
75387
+ const vol = this.callbacks.getVolumeForLayer(layerId);
75388
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75389
+ if (changedDeltas && changedDeltas.length > 0) {
75390
+ // Report every changed Z slice
75391
+ for (const delta of changedDeltas) {
75392
+ const { data: sliceData, width, height } = vol.getSliceUint8(delta.sliceIndex, delta.axis);
75393
+ this.ctx.callbacks.onMaskChanged(sliceData, layerId, channel, delta.sliceIndex, delta.axis, width, height, false);
75394
+ }
75395
+ }
75396
+ else {
75397
+ // Fallback: report only the current slice
75398
+ const axis = this.ctx.protectedData.axis;
75399
+ const sliceIndex = this.ctx.nrrd_states.view.currentSliceIndex;
75400
+ const { data: sliceData, width, height } = vol.getSliceUint8(sliceIndex, axis);
75401
+ this.ctx.callbacks.onMaskChanged(sliceData, layerId, channel, sliceIndex, axis, width, height, false);
75402
+ }
75403
+ }
75404
+ catch (_a) {
75405
+ // Volume not ready
75406
+ }
75407
+ }
75408
+ /** Whether a sphere placement is currently in progress */
75409
+ get isActive() {
75410
+ return this.active;
75411
+ }
75412
+ }
75413
+
74889
75414
  /**
74890
75415
  * DrawToolCore — Tool orchestration and event routing.
74891
75416
  *
@@ -74906,6 +75431,7 @@ void main() {
74906
75431
  handleOnDrawingBrushCricleMove: (ev) => { },
74907
75432
  handleMouseZoomSliceWheel: (e) => { },
74908
75433
  handleSphereWheel: (e) => { },
75434
+ handleSphereBrushWheel: (e) => { },
74909
75435
  };
74910
75436
  this.contrastEventPrameters = {
74911
75437
  move_x: 0,
@@ -74988,6 +75514,16 @@ void main() {
74988
75514
  pushUndoDelta: (delta) => this.undoManager.push(delta),
74989
75515
  getEraserUrls: () => this.eraserUrls,
74990
75516
  });
75517
+ this.sphereBrushTool = new SphereBrushTool(toolCtx, {
75518
+ getVolumeForLayer: (layer) => this.renderer.getVolumeForLayer(layer),
75519
+ compositeAllLayers: () => this.renderer.compositeAllLayers(),
75520
+ pushUndoGroup: (deltas) => this.undoManager.pushGroup(deltas),
75521
+ renderSliceToCanvas: (layer, axis, sliceIndex, buffer, ctx, w, h) => this.renderer.renderSliceToCanvas(layer, axis, sliceIndex, buffer, ctx, w, h),
75522
+ getOrCreateSliceBuffer: (axis) => this.renderer.getOrCreateSliceBuffer(axis),
75523
+ setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
75524
+ reloadMasksFromVolume: () => this.reloadMasksFromVolume(),
75525
+ getEraserUrls: () => this.eraserUrls,
75526
+ });
74991
75527
  }
74992
75528
  initDrawToolCore() {
74993
75529
  // Initialize EventRouter for centralized event handling
@@ -75032,12 +75568,28 @@ void main() {
75032
75568
  if ((ev.ctrlKey || ev.metaKey) && (ev.key === redoKey || (ev.shiftKey && ev.key === undoKeyUpper))) {
75033
75569
  this.redoLastPainting();
75034
75570
  }
75571
+ // Handle mouse wheel mode toggle (Ctrl+1 = Scroll:Zoom, Ctrl+2 = Scroll:Slice)
75572
+ if ((ev.ctrlKey || ev.metaKey) && ev.key === '1') {
75573
+ ev.preventDefault();
75574
+ this.state.keyboardSettings.mouseWheel = 'Scroll:Zoom';
75575
+ this.updateMouseWheelEvent();
75576
+ return;
75577
+ }
75578
+ if ((ev.ctrlKey || ev.metaKey) && ev.key === '2') {
75579
+ ev.preventDefault();
75580
+ this.state.keyboardSettings.mouseWheel = 'Scroll:Slice';
75581
+ this.updateMouseWheelEvent();
75582
+ return;
75583
+ }
75035
75584
  // Handle crosshair toggle (allowed in drawing tools AND sphere mode)
75036
75585
  if (ev.key === this.state.keyboardSettings.crosshair) {
75037
75586
  this.eventRouter.toggleCrosshair();
75038
75587
  }
75039
75588
  // Handle draw mode (Shift key) - EventRouter already tracks this
75040
- if (ev.key === this.state.keyboardSettings.draw && !this.state.gui_states.mode.sphere) {
75589
+ if (ev.key === this.state.keyboardSettings.draw
75590
+ && !this.state.gui_states.mode.sphere
75591
+ && !this.state.gui_states.mode.sphereBrush
75592
+ && !this.state.gui_states.mode.sphereEraser) {
75041
75593
  if (this.eventRouter.isCtrlHeld()) {
75042
75594
  return; // Ctrl takes priority
75043
75595
  }
@@ -75089,11 +75641,16 @@ void main() {
75089
75641
  if (this.drawingTool.isActive || this.panTool.isActive) {
75090
75642
  this.drawingPrameters.handleOnDrawingMouseMove(e);
75091
75643
  }
75644
+ // Drag-erase: route move to sphereBrushTool when sphereEraser is active
75645
+ if (this.sphereBrushTool.isActive && this.state.gui_states.mode.sphereEraser) {
75646
+ this.sphereBrushTool.onSphereEraserMove(e);
75647
+ }
75092
75648
  });
75093
75649
  this.eventRouter.setPointerUpHandler((e) => {
75094
75650
  if (this.drawingTool.isActive || this.drawingTool.painting
75095
75651
  || this.panTool.isActive
75096
- || (this.state.gui_states.mode.sphere && this.eventRouter.getMode() !== 'crosshair')) {
75652
+ || (this.state.gui_states.mode.sphere && this.eventRouter.getMode() !== 'crosshair')
75653
+ || this.sphereBrushTool.isActive) {
75097
75654
  this.drawingPrameters.handleOnDrawingMouseUp(e);
75098
75655
  }
75099
75656
  });
@@ -75108,6 +75665,9 @@ void main() {
75108
75665
  else if (this.activeWheelMode === 'sphere') {
75109
75666
  this.drawingPrameters.handleSphereWheel(e);
75110
75667
  }
75668
+ else if (this.activeWheelMode === 'sphereBrush') {
75669
+ this.drawingPrameters.handleSphereBrushWheel(e);
75670
+ }
75111
75671
  });
75112
75672
  // Bind all event listeners
75113
75673
  this.eventRouter.bindAll();
@@ -75171,6 +75731,8 @@ void main() {
75171
75731
  }
75172
75732
  // sphere Wheel
75173
75733
  this.drawingPrameters.handleSphereWheel = this.configMouseSphereWheel();
75734
+ // sphere brush Wheel
75735
+ this.drawingPrameters.handleSphereBrushWheel = this.sphereBrushTool.configSphereBrushWheel();
75174
75736
  // brush circle move — delegated to DrawingTool
75175
75737
  this.drawingPrameters.handleOnDrawingBrushCricleMove =
75176
75738
  this.drawingTool.createBrushTrackingHandler();
@@ -75201,6 +75763,16 @@ void main() {
75201
75763
  e.offsetY / this.state.nrrd_states.view.sizeFactor;
75202
75764
  this.enableCrosshair();
75203
75765
  }
75766
+ else if (this.state.gui_states.mode.sphereBrush && !this.eventRouter.isCrosshairEnabled()) {
75767
+ // SphereBrush: direct click (no Shift needed)
75768
+ this.activeWheelMode = 'sphereBrush';
75769
+ this.sphereBrushTool.onSphereBrushClick(e);
75770
+ }
75771
+ else if (this.state.gui_states.mode.sphereEraser && !this.eventRouter.isCrosshairEnabled()) {
75772
+ // SphereEraser: direct click (Shift handled at mode level)
75773
+ this.activeWheelMode = 'sphereBrush';
75774
+ this.sphereBrushTool.onSphereEraserClick(e);
75775
+ }
75204
75776
  else if (this.state.gui_states.mode.sphere && !this.eventRouter.isCrosshairEnabled()) {
75205
75777
  this.handleSphereClick(e);
75206
75778
  }
@@ -75222,6 +75794,16 @@ void main() {
75222
75794
  this.drawingTool.onPointerUp(e);
75223
75795
  this.activeWheelMode = 'zoom';
75224
75796
  }
75797
+ else if (this.sphereBrushTool.isActive) {
75798
+ // SphereBrush or SphereEraser pointer-up
75799
+ if (this.state.gui_states.mode.sphereBrush) {
75800
+ this.sphereBrushTool.onSphereBrushPointerUp();
75801
+ }
75802
+ else if (this.state.gui_states.mode.sphereEraser) {
75803
+ this.sphereBrushTool.onSphereEraserPointerUp();
75804
+ }
75805
+ this.activeWheelMode = 'zoom';
75806
+ }
75225
75807
  else if (this.state.gui_states.mode.sphere &&
75226
75808
  !this.eventRouter.isCrosshairEnabled()) {
75227
75809
  this.sphereTool.onSpherePointerUp();
@@ -75266,7 +75848,9 @@ void main() {
75266
75848
  }
75267
75849
  }
75268
75850
  this.state.protectedData.ctxes.drawingCtx.drawImage(this.state.protectedData.canvases.drawingCanvasLayerMaster, 0, 0);
75269
- if (this.state.gui_states.mode.sphere) {
75851
+ if (this.state.gui_states.mode.sphere
75852
+ || this.state.gui_states.mode.sphereBrush
75853
+ || this.state.gui_states.mode.sphereEraser) {
75270
75854
  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);
75271
75855
  }
75272
75856
  }
@@ -75453,24 +76037,26 @@ void main() {
75453
76037
  * Undo the last drawing operation on the active layer.
75454
76038
  */
75455
76039
  undoLastPainting() {
75456
- const delta = this.undoManager.undo();
75457
- if (!delta)
76040
+ const entry = this.undoManager.undo();
76041
+ if (!entry)
75458
76042
  return;
75459
- try {
75460
- const vol = this.renderer.getVolumeForLayer(delta.layerId);
75461
- vol.setSliceUint8(delta.sliceIndex, delta.oldSlice, delta.axis);
75462
- }
75463
- catch (_a) {
75464
- return;
75465
- }
75466
76043
  this.state.protectedData.isDrawing = true;
75467
- if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
75468
- this.applyUndoRedoToCanvas(delta.layerId);
75469
- }
75470
- if (!this.state.nrrd_states.flags.loadingMaskData) {
75471
- const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
75472
- .getSliceUint8(delta.sliceIndex, delta.axis);
75473
- this.state.annotationCallbacks.onMaskChanged(sliceData, delta.layerId, this.state.gui_states.layerChannel.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
76044
+ for (const delta of entry) {
76045
+ try {
76046
+ const vol = this.renderer.getVolumeForLayer(delta.layerId);
76047
+ vol.setSliceUint8(delta.sliceIndex, delta.oldSlice, delta.axis);
76048
+ }
76049
+ catch (_a) {
76050
+ continue;
76051
+ }
76052
+ if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
76053
+ this.applyUndoRedoToCanvas(delta.layerId);
76054
+ }
76055
+ if (!this.state.nrrd_states.flags.loadingMaskData) {
76056
+ const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
76057
+ .getSliceUint8(delta.sliceIndex, delta.axis);
76058
+ this.state.annotationCallbacks.onMaskChanged(sliceData, delta.layerId, this.state.gui_states.layerChannel.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
76059
+ }
75474
76060
  }
75475
76061
  this.setIsDrawFalse(1000);
75476
76062
  }
@@ -75478,24 +76064,26 @@ void main() {
75478
76064
  * Redo the last undone operation on the active layer.
75479
76065
  */
75480
76066
  redoLastPainting() {
75481
- const delta = this.undoManager.redo();
75482
- if (!delta)
75483
- return;
75484
- try {
75485
- const vol = this.renderer.getVolumeForLayer(delta.layerId);
75486
- vol.setSliceUint8(delta.sliceIndex, delta.newSlice, delta.axis);
75487
- }
75488
- catch (_a) {
76067
+ const entry = this.undoManager.redo();
76068
+ if (!entry)
75489
76069
  return;
75490
- }
75491
76070
  this.state.protectedData.isDrawing = true;
75492
- if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
75493
- this.applyUndoRedoToCanvas(delta.layerId);
75494
- }
75495
- if (!this.state.nrrd_states.flags.loadingMaskData) {
75496
- const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
75497
- .getSliceUint8(delta.sliceIndex, delta.axis);
75498
- this.state.annotationCallbacks.onMaskChanged(sliceData, delta.layerId, this.state.gui_states.layerChannel.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
76071
+ for (const delta of entry) {
76072
+ try {
76073
+ const vol = this.renderer.getVolumeForLayer(delta.layerId);
76074
+ vol.setSliceUint8(delta.sliceIndex, delta.newSlice, delta.axis);
76075
+ }
76076
+ catch (_a) {
76077
+ continue;
76078
+ }
76079
+ if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
76080
+ this.applyUndoRedoToCanvas(delta.layerId);
76081
+ }
76082
+ if (!this.state.nrrd_states.flags.loadingMaskData) {
76083
+ const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
76084
+ .getSliceUint8(delta.sliceIndex, delta.axis);
76085
+ this.state.annotationCallbacks.onMaskChanged(sliceData, delta.layerId, this.state.gui_states.layerChannel.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
76086
+ }
75499
76087
  }
75500
76088
  this.setIsDrawFalse(1000);
75501
76089
  }
@@ -75569,6 +76157,14 @@ void main() {
75569
76157
  exitSphereMode() {
75570
76158
  throw new Error("exitSphereMode must be provided by NrrdTools");
75571
76159
  }
76160
+ /** Override in NrrdTools */
76161
+ reloadMasksFromVolume() {
76162
+ throw new Error("reloadMasksFromVolume must be provided by NrrdTools");
76163
+ }
76164
+ /** Override in NrrdTools */
76165
+ updateMouseWheelEvent() {
76166
+ throw new Error("updateMouseWheelEvent must be provided by NrrdTools");
76167
+ }
75572
76168
  }
75573
76169
 
75574
76170
  /**
@@ -75632,6 +76228,7 @@ void main() {
75632
76228
  nippleSphereOrigin: null,
75633
76229
  sphereMaskVolume: null,
75634
76230
  sphereRadius: 5,
76231
+ sphereBrushRadius: 10,
75635
76232
  };
75636
76233
  this.flags = {
75637
76234
  stepClear: 1,
@@ -75653,6 +76250,7 @@ void main() {
75653
76250
  this.sphere.nippleSphereOrigin = null;
75654
76251
  this.sphere.sphereMaskVolume = null;
75655
76252
  this.sphere.sphereRadius = 5;
76253
+ this.sphere.sphereBrushRadius = 10;
75656
76254
  }
75657
76255
  }
75658
76256
 
@@ -75674,6 +76272,8 @@ void main() {
75674
76272
  pencil: true,
75675
76273
  eraser: false,
75676
76274
  sphere: false,
76275
+ sphereBrush: false,
76276
+ sphereEraser: false,
75677
76277
  activeSphereType: "tumour",
75678
76278
  };
75679
76279
  this.drawing = {
@@ -76633,6 +77233,8 @@ void main() {
76633
77233
  this.drawCore.enterSphereMode = () => this.enterSphereMode();
76634
77234
  this.drawCore.exitSphereMode = () => this.exitSphereMode();
76635
77235
  this.drawCore.configMouseSliceWheel = () => this.configMouseSliceWheel();
77236
+ this.drawCore.reloadMasksFromVolume = () => this.reloadMasksFromVolume();
77237
+ this.drawCore.updateMouseWheelEvent = () => this.updateMouseWheelEvent();
76636
77238
  }
76637
77239
  /**
76638
77240
  * A initialise function for nrrd_tools
@@ -76753,13 +77355,18 @@ void main() {
76753
77355
  * Set the current tool mode.
76754
77356
  */
76755
77357
  setMode(mode) {
77358
+ var _a, _b, _c;
76756
77359
  if (!this.guiCallbacks)
76757
77360
  return;
76758
77361
  const prevSphere = this.state.gui_states.mode.sphere;
77362
+ const prevSphereBrush = this.state.gui_states.mode.sphereBrush;
77363
+ const prevSphereEraser = this.state.gui_states.mode.sphereEraser;
76759
77364
  const prevCalculator = this._calculatorActive;
76760
77365
  this.state.gui_states.mode.pencil = false;
76761
77366
  this.state.gui_states.mode.eraser = false;
76762
77367
  this.state.gui_states.mode.sphere = false;
77368
+ this.state.gui_states.mode.sphereBrush = false;
77369
+ this.state.gui_states.mode.sphereEraser = false;
76763
77370
  this._calculatorActive = false;
76764
77371
  switch (mode) {
76765
77372
  case "pencil":
@@ -76780,6 +77387,24 @@ void main() {
76780
77387
  this.state.gui_states.mode.sphere = true;
76781
77388
  this._calculatorActive = true;
76782
77389
  break;
77390
+ case "sphereBrush":
77391
+ this.state.gui_states.mode.sphereBrush = true;
77392
+ this.dragOperator.removeDragMode();
77393
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool('sphereBrush');
77394
+ break;
77395
+ case "sphereEraser":
77396
+ this.state.gui_states.mode.sphereEraser = true;
77397
+ this.dragOperator.removeDragMode();
77398
+ (_b = this.drawCore.eventRouter) === null || _b === void 0 ? void 0 : _b.setGuiTool('sphereEraser');
77399
+ break;
77400
+ }
77401
+ // Restore drag mode when leaving sphereBrush/sphereEraser
77402
+ if ((prevSphereBrush || prevSphereEraser)
77403
+ && !this.state.gui_states.mode.sphereBrush
77404
+ && !this.state.gui_states.mode.sphereEraser
77405
+ && !this.state.gui_states.mode.sphere) {
77406
+ this.dragOperator.configDragMode();
77407
+ (_c = this.drawCore.eventRouter) === null || _c === void 0 ? void 0 : _c.setGuiTool('pencil');
76783
77408
  }
76784
77409
  if (prevSphere && !this.state.gui_states.mode.sphere) {
76785
77410
  this.guiCallbacks.updateSphereState();
@@ -76792,6 +77417,10 @@ void main() {
76792
77417
  getMode() {
76793
77418
  if (this._calculatorActive)
76794
77419
  return "calculator";
77420
+ if (this.state.gui_states.mode.sphereBrush)
77421
+ return "sphereBrush";
77422
+ if (this.state.gui_states.mode.sphereEraser)
77423
+ return "sphereEraser";
76795
77424
  if (this.state.gui_states.mode.sphere)
76796
77425
  return "sphere";
76797
77426
  if (this.state.gui_states.mode.eraser)
@@ -76817,6 +77446,12 @@ void main() {
76817
77446
  getBrushSize() {
76818
77447
  return this.state.gui_states.drawing.brushAndEraserSize;
76819
77448
  }
77449
+ setSphereBrushRadius(radius) {
77450
+ this.state.nrrd_states.sphere.sphereBrushRadius = Math.max(1, Math.min(50, radius));
77451
+ }
77452
+ getSphereBrushRadius() {
77453
+ return this.state.nrrd_states.sphere.sphereBrushRadius;
77454
+ }
76820
77455
  setWindowHigh(value) {
76821
77456
  var _a;
76822
77457
  this.state.gui_states.viewConfig.readyToUpdate = false;
@@ -77366,10 +78001,15 @@ void main() {
77366
78001
  // ═══════════════════════════════════════════════════════════════════════════
77367
78002
  configMouseSliceWheel() {
77368
78003
  const handleMouseZoomSliceWheelMove = (e) => {
77369
- var _a;
78004
+ var _a, _b;
77370
78005
  if ((_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
77371
78006
  return;
77372
78007
  }
78008
+ // Block slice wheel when sphereBrush/sphereEraser is actively placing (left button held)
78009
+ if ((this.state.gui_states.mode.sphereBrush || this.state.gui_states.mode.sphereEraser)
78010
+ && ((_b = this.drawCore.eventRouter) === null || _b === void 0 ? void 0 : _b.isLeftButtonDown())) {
78011
+ return;
78012
+ }
77373
78013
  e.preventDefault();
77374
78014
  if (e.deltaY < 0) {
77375
78015
  this.setSliceMoving(-1);
@@ -77637,7 +78277,7 @@ void main() {
77637
78277
  }
77638
78278
 
77639
78279
  // import * as kiwrious from "copper3d_plugin_heart_k";
77640
- const REVISION = "v3.1.1-beta";
78280
+ const REVISION = "v3.2.0-beta";
77641
78281
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
77642
78282
 
77643
78283
  exports.CHANNEL_COLORS = CHANNEL_COLORS;