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