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