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