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
@@ -40371,8 +40371,10 @@ class Controls {
40371
40371
  this.currentCamera.near = this.viewpoint.nearPlane;
40372
40372
  this.currentCamera.far = this.viewpoint.farPlane;
40373
40373
  this.currentCamera.position.set(this.viewpoint.eyePosition[0], this.viewpoint.eyePosition[1], this.viewpoint.eyePosition[2]);
40374
- this.currentCamera.lookAt(this.viewpoint.targetPosition[0], this.viewpoint.targetPosition[1], this.viewpoint.targetPosition[2]);
40374
+ // up must be set BEFORE lookAt — Three.js lookAt() uses the up vector
40375
+ // to compute the view orientation matrix
40375
40376
  this.currentCamera.up.set(this.viewpoint.upVector[0], this.viewpoint.upVector[1], this.viewpoint.upVector[2]);
40377
+ this.currentCamera.lookAt(this.viewpoint.targetPosition[0], this.viewpoint.targetPosition[1], this.viewpoint.targetPosition[2]);
40376
40378
  this.currentCamera.updateProjectionMatrix();
40377
40379
  this.updateDirectionalLight();
40378
40380
  }
@@ -49860,6 +49862,7 @@ class baseScene extends commonScene {
49860
49862
  viewpoint.targetPosition = viewpointData.targetPosition;
49861
49863
  viewpoint.upVector = viewpointData.upVector;
49862
49864
  this.copperControl.updateCameraViewPoint(viewpoint);
49865
+ this.camera.updateProjectionMatrix();
49863
49866
  }
49864
49867
  getDefaultViewPoint() {
49865
49868
  return this.viewPoint;
@@ -59610,6 +59613,22 @@ class copperScene extends baseScene {
59610
59613
  if (position.z)
59611
59614
  model.position.z = position.z;
59612
59615
  }
59616
+ loadView(viewpointData) {
59617
+ super.loadView(viewpointData);
59618
+ // Sync TrackballControls with the new viewpoint
59619
+ if (this.controls) {
59620
+ const controls = this.controls;
59621
+ if (viewpointData.targetPosition) {
59622
+ controls.target.set(viewpointData.targetPosition[0], viewpointData.targetPosition[1], viewpointData.targetPosition[2]);
59623
+ // Update target0 so controls.reset() returns to this view
59624
+ controls.target0.copy(controls.target);
59625
+ }
59626
+ // Save position0/up0 so controls.reset() returns to this view
59627
+ controls.position0.copy(this.camera.position);
59628
+ controls.up0.copy(this.camera.up);
59629
+ controls.zoom0 = this.camera.zoom;
59630
+ }
59631
+ }
59613
59632
  resetView() {
59614
59633
  this.controls.reset();
59615
59634
  this.updateCamera(this.viewPoint);
@@ -60187,6 +60206,10 @@ class copperMScene extends commonScene {
60187
60206
  if (this.subCopperControl) {
60188
60207
  this.subCopperControl.updateCameraViewPoint(viewpoint);
60189
60208
  }
60209
+ // Sync TrackballControls target with the new viewpoint target position
60210
+ if (this.controls && viewpointData.targetPosition) {
60211
+ this.controls.target.set(viewpointData.targetPosition[0], viewpointData.targetPosition[1], viewpointData.targetPosition[2]);
60212
+ }
60190
60213
  }
60191
60214
  updateCamera(viewpoint) {
60192
60215
  this.cameraPositionFlag = true;
@@ -71932,41 +71955,48 @@ class UndoManager {
71932
71955
  setActiveLayer(layer) {
71933
71956
  this.activeLayer = layer;
71934
71957
  }
71935
- /** 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. */
71936
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) {
71937
71964
  var _a, _b;
71938
- const stack = (_a = this.undoStacks.get(delta.layerId)) !== null && _a !== void 0 ? _a : this.undoStacks.get("layer1");
71939
- 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);
71940
71970
  if (stack.length > MAX_STACK_SIZE) {
71941
71971
  stack.shift();
71942
71972
  }
71943
71973
  // Any new operation invalidates the redo history for that layer
71944
- 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");
71945
71975
  redoStack.length = 0;
71946
71976
  }
71947
71977
  /**
71948
71978
  * Undo the last operation on the active layer.
71949
- * @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.
71950
71980
  */
71951
71981
  undo() {
71952
71982
  const stack = this.undoStacks.get(this.activeLayer);
71953
- const delta = stack.pop();
71954
- if (delta) {
71955
- this.redoStacks.get(this.activeLayer).push(delta);
71983
+ const entry = stack.pop();
71984
+ if (entry) {
71985
+ this.redoStacks.get(this.activeLayer).push(entry);
71956
71986
  }
71957
- return delta;
71987
+ return entry;
71958
71988
  }
71959
71989
  /**
71960
71990
  * Redo the last undone operation on the active layer.
71961
- * @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.
71962
71992
  */
71963
71993
  redo() {
71964
71994
  const stack = this.redoStacks.get(this.activeLayer);
71965
- const delta = stack.pop();
71966
- if (delta) {
71967
- this.undoStacks.get(this.activeLayer).push(delta);
71995
+ const entry = stack.pop();
71996
+ if (entry) {
71997
+ this.undoStacks.get(this.activeLayer).push(entry);
71968
71998
  }
71969
- return delta;
71999
+ return entry;
71970
72000
  }
71971
72001
  canUndo() {
71972
72002
  var _a, _b;
@@ -73335,7 +73365,9 @@ class DragOperator {
73335
73365
  // When leaving draw, contrast, or crosshair mode (returning to idle), restore drag mode
73336
73366
  // Do NOT restore if sphere mode is active
73337
73367
  if ((prev === 'draw' || prev === 'contrast' || prev === 'crosshair') && next === 'idle') {
73338
- 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) {
73339
73371
  this.configDragMode();
73340
73372
  }
73341
73373
  }
@@ -73874,6 +73906,7 @@ class EventRouter {
73874
73906
  if (ev.key === this.keyboardSettings.draw) {
73875
73907
  this.state.shiftHeld = true;
73876
73908
  // Block draw mode when crosshair or contrast is active (mutual exclusion)
73909
+ // Also block when sphereBrush or sphereEraser is active
73877
73910
  if (DRAWING_TOOLS.has(this.guiTool) && !this.state.ctrlHeld && !this.state.crosshairEnabled) {
73878
73911
  this.setMode('draw');
73879
73912
  }
@@ -74273,10 +74306,15 @@ class ZoomTool extends BaseTool {
74273
74306
  configMouseZoomWheel() {
74274
74307
  let moveDistance = 1;
74275
74308
  return (e) => {
74276
- var _a;
74309
+ var _a, _b;
74277
74310
  if ((_a = this.ctx.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
74278
74311
  return;
74279
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
+ }
74280
74318
  e.preventDefault();
74281
74319
  const delta = e.detail ? e.detail > 0 : e.wheelDelta < 0;
74282
74320
  this.ctx.protectedData.isDrawing = true;
@@ -74878,6 +74916,493 @@ class ImageStoreHelper extends BaseTool {
74878
74916
  }
74879
74917
  }
74880
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
+
74881
75406
  /**
74882
75407
  * DrawToolCore — Tool orchestration and event routing.
74883
75408
  *
@@ -74898,6 +75423,7 @@ class DrawToolCore {
74898
75423
  handleOnDrawingBrushCricleMove: (ev) => { },
74899
75424
  handleMouseZoomSliceWheel: (e) => { },
74900
75425
  handleSphereWheel: (e) => { },
75426
+ handleSphereBrushWheel: (e) => { },
74901
75427
  };
74902
75428
  this.contrastEventPrameters = {
74903
75429
  move_x: 0,
@@ -74980,6 +75506,16 @@ class DrawToolCore {
74980
75506
  pushUndoDelta: (delta) => this.undoManager.push(delta),
74981
75507
  getEraserUrls: () => this.eraserUrls,
74982
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
+ });
74983
75519
  }
74984
75520
  initDrawToolCore() {
74985
75521
  // Initialize EventRouter for centralized event handling
@@ -75024,12 +75560,28 @@ class DrawToolCore {
75024
75560
  if ((ev.ctrlKey || ev.metaKey) && (ev.key === redoKey || (ev.shiftKey && ev.key === undoKeyUpper))) {
75025
75561
  this.redoLastPainting();
75026
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
+ }
75027
75576
  // Handle crosshair toggle (allowed in drawing tools AND sphere mode)
75028
75577
  if (ev.key === this.state.keyboardSettings.crosshair) {
75029
75578
  this.eventRouter.toggleCrosshair();
75030
75579
  }
75031
75580
  // Handle draw mode (Shift key) - EventRouter already tracks this
75032
- 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) {
75033
75585
  if (this.eventRouter.isCtrlHeld()) {
75034
75586
  return; // Ctrl takes priority
75035
75587
  }
@@ -75081,11 +75633,16 @@ class DrawToolCore {
75081
75633
  if (this.drawingTool.isActive || this.panTool.isActive) {
75082
75634
  this.drawingPrameters.handleOnDrawingMouseMove(e);
75083
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
+ }
75084
75640
  });
75085
75641
  this.eventRouter.setPointerUpHandler((e) => {
75086
75642
  if (this.drawingTool.isActive || this.drawingTool.painting
75087
75643
  || this.panTool.isActive
75088
- || (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) {
75089
75646
  this.drawingPrameters.handleOnDrawingMouseUp(e);
75090
75647
  }
75091
75648
  });
@@ -75100,6 +75657,9 @@ class DrawToolCore {
75100
75657
  else if (this.activeWheelMode === 'sphere') {
75101
75658
  this.drawingPrameters.handleSphereWheel(e);
75102
75659
  }
75660
+ else if (this.activeWheelMode === 'sphereBrush') {
75661
+ this.drawingPrameters.handleSphereBrushWheel(e);
75662
+ }
75103
75663
  });
75104
75664
  // Bind all event listeners
75105
75665
  this.eventRouter.bindAll();
@@ -75163,6 +75723,8 @@ class DrawToolCore {
75163
75723
  }
75164
75724
  // sphere Wheel
75165
75725
  this.drawingPrameters.handleSphereWheel = this.configMouseSphereWheel();
75726
+ // sphere brush Wheel
75727
+ this.drawingPrameters.handleSphereBrushWheel = this.sphereBrushTool.configSphereBrushWheel();
75166
75728
  // brush circle move — delegated to DrawingTool
75167
75729
  this.drawingPrameters.handleOnDrawingBrushCricleMove =
75168
75730
  this.drawingTool.createBrushTrackingHandler();
@@ -75193,6 +75755,16 @@ class DrawToolCore {
75193
75755
  e.offsetY / this.state.nrrd_states.view.sizeFactor;
75194
75756
  this.enableCrosshair();
75195
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
+ }
75196
75768
  else if (this.state.gui_states.mode.sphere && !this.eventRouter.isCrosshairEnabled()) {
75197
75769
  this.handleSphereClick(e);
75198
75770
  }
@@ -75214,6 +75786,16 @@ class DrawToolCore {
75214
75786
  this.drawingTool.onPointerUp(e);
75215
75787
  this.activeWheelMode = 'zoom';
75216
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
+ }
75217
75799
  else if (this.state.gui_states.mode.sphere &&
75218
75800
  !this.eventRouter.isCrosshairEnabled()) {
75219
75801
  this.sphereTool.onSpherePointerUp();
@@ -75258,7 +75840,9 @@ class DrawToolCore {
75258
75840
  }
75259
75841
  }
75260
75842
  this.state.protectedData.ctxes.drawingCtx.drawImage(this.state.protectedData.canvases.drawingCanvasLayerMaster, 0, 0);
75261
- 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) {
75262
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);
75263
75847
  }
75264
75848
  }
@@ -75445,24 +76029,26 @@ class DrawToolCore {
75445
76029
  * Undo the last drawing operation on the active layer.
75446
76030
  */
75447
76031
  undoLastPainting() {
75448
- const delta = this.undoManager.undo();
75449
- if (!delta)
76032
+ const entry = this.undoManager.undo();
76033
+ if (!entry)
75450
76034
  return;
75451
- try {
75452
- const vol = this.renderer.getVolumeForLayer(delta.layerId);
75453
- vol.setSliceUint8(delta.sliceIndex, delta.oldSlice, delta.axis);
75454
- }
75455
- catch (_a) {
75456
- return;
75457
- }
75458
76035
  this.state.protectedData.isDrawing = true;
75459
- if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
75460
- this.applyUndoRedoToCanvas(delta.layerId);
75461
- }
75462
- if (!this.state.nrrd_states.flags.loadingMaskData) {
75463
- const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
75464
- .getSliceUint8(delta.sliceIndex, delta.axis);
75465
- 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
+ }
75466
76052
  }
75467
76053
  this.setIsDrawFalse(1000);
75468
76054
  }
@@ -75470,24 +76056,26 @@ class DrawToolCore {
75470
76056
  * Redo the last undone operation on the active layer.
75471
76057
  */
75472
76058
  redoLastPainting() {
75473
- const delta = this.undoManager.redo();
75474
- if (!delta)
75475
- return;
75476
- try {
75477
- const vol = this.renderer.getVolumeForLayer(delta.layerId);
75478
- vol.setSliceUint8(delta.sliceIndex, delta.newSlice, delta.axis);
75479
- }
75480
- catch (_a) {
76059
+ const entry = this.undoManager.redo();
76060
+ if (!entry)
75481
76061
  return;
75482
- }
75483
76062
  this.state.protectedData.isDrawing = true;
75484
- if (delta.axis === this.state.protectedData.axis && delta.sliceIndex === this.state.nrrd_states.view.currentSliceIndex) {
75485
- this.applyUndoRedoToCanvas(delta.layerId);
75486
- }
75487
- if (!this.state.nrrd_states.flags.loadingMaskData) {
75488
- const { data: sliceData, width, height } = this.renderer.getVolumeForLayer(delta.layerId)
75489
- .getSliceUint8(delta.sliceIndex, delta.axis);
75490
- 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
+ }
75491
76079
  }
75492
76080
  this.setIsDrawFalse(1000);
75493
76081
  }
@@ -75561,6 +76149,14 @@ class DrawToolCore {
75561
76149
  exitSphereMode() {
75562
76150
  throw new Error("exitSphereMode must be provided by NrrdTools");
75563
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
+ }
75564
76160
  }
75565
76161
 
75566
76162
  /**
@@ -75624,6 +76220,7 @@ class NrrdState {
75624
76220
  nippleSphereOrigin: null,
75625
76221
  sphereMaskVolume: null,
75626
76222
  sphereRadius: 5,
76223
+ sphereBrushRadius: 10,
75627
76224
  };
75628
76225
  this.flags = {
75629
76226
  stepClear: 1,
@@ -75645,6 +76242,7 @@ class NrrdState {
75645
76242
  this.sphere.nippleSphereOrigin = null;
75646
76243
  this.sphere.sphereMaskVolume = null;
75647
76244
  this.sphere.sphereRadius = 5;
76245
+ this.sphere.sphereBrushRadius = 10;
75648
76246
  }
75649
76247
  }
75650
76248
 
@@ -75666,6 +76264,8 @@ class GuiState {
75666
76264
  pencil: true,
75667
76265
  eraser: false,
75668
76266
  sphere: false,
76267
+ sphereBrush: false,
76268
+ sphereEraser: false,
75669
76269
  activeSphereType: "tumour",
75670
76270
  };
75671
76271
  this.drawing = {
@@ -76625,6 +77225,8 @@ class NrrdTools {
76625
77225
  this.drawCore.enterSphereMode = () => this.enterSphereMode();
76626
77226
  this.drawCore.exitSphereMode = () => this.exitSphereMode();
76627
77227
  this.drawCore.configMouseSliceWheel = () => this.configMouseSliceWheel();
77228
+ this.drawCore.reloadMasksFromVolume = () => this.reloadMasksFromVolume();
77229
+ this.drawCore.updateMouseWheelEvent = () => this.updateMouseWheelEvent();
76628
77230
  }
76629
77231
  /**
76630
77232
  * A initialise function for nrrd_tools
@@ -76745,13 +77347,18 @@ class NrrdTools {
76745
77347
  * Set the current tool mode.
76746
77348
  */
76747
77349
  setMode(mode) {
77350
+ var _a, _b, _c;
76748
77351
  if (!this.guiCallbacks)
76749
77352
  return;
76750
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;
76751
77356
  const prevCalculator = this._calculatorActive;
76752
77357
  this.state.gui_states.mode.pencil = false;
76753
77358
  this.state.gui_states.mode.eraser = false;
76754
77359
  this.state.gui_states.mode.sphere = false;
77360
+ this.state.gui_states.mode.sphereBrush = false;
77361
+ this.state.gui_states.mode.sphereEraser = false;
76755
77362
  this._calculatorActive = false;
76756
77363
  switch (mode) {
76757
77364
  case "pencil":
@@ -76772,6 +77379,24 @@ class NrrdTools {
76772
77379
  this.state.gui_states.mode.sphere = true;
76773
77380
  this._calculatorActive = true;
76774
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');
76775
77400
  }
76776
77401
  if (prevSphere && !this.state.gui_states.mode.sphere) {
76777
77402
  this.guiCallbacks.updateSphereState();
@@ -76784,6 +77409,10 @@ class NrrdTools {
76784
77409
  getMode() {
76785
77410
  if (this._calculatorActive)
76786
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";
76787
77416
  if (this.state.gui_states.mode.sphere)
76788
77417
  return "sphere";
76789
77418
  if (this.state.gui_states.mode.eraser)
@@ -76809,6 +77438,12 @@ class NrrdTools {
76809
77438
  getBrushSize() {
76810
77439
  return this.state.gui_states.drawing.brushAndEraserSize;
76811
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
+ }
76812
77447
  setWindowHigh(value) {
76813
77448
  var _a;
76814
77449
  this.state.gui_states.viewConfig.readyToUpdate = false;
@@ -77358,10 +77993,15 @@ class NrrdTools {
77358
77993
  // ═══════════════════════════════════════════════════════════════════════════
77359
77994
  configMouseSliceWheel() {
77360
77995
  const handleMouseZoomSliceWheelMove = (e) => {
77361
- var _a;
77996
+ var _a, _b;
77362
77997
  if ((_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
77363
77998
  return;
77364
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
+ }
77365
78005
  e.preventDefault();
77366
78006
  if (e.deltaY < 0) {
77367
78007
  this.setSliceMoving(-1);
@@ -77629,7 +78269,7 @@ function evaluateElement(element, weights) {
77629
78269
  }
77630
78270
 
77631
78271
  // import * as kiwrious from "copper3d_plugin_heart_k";
77632
- const REVISION = "v3.1.1-beta";
78272
+ const REVISION = "v3.2.0-beta";
77633
78273
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
77634
78274
 
77635
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 };