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