copper3d 2.2.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +1056 -5
  2. package/dist/Scene/copperScene.d.ts +3 -0
  3. package/dist/Scene/copperScene.js +13 -0
  4. package/dist/Scene/copperScene.js.map +1 -1
  5. package/dist/Utils/segmentation/CommToolsData.d.ts +109 -6
  6. package/dist/Utils/segmentation/CommToolsData.js +314 -50
  7. package/dist/Utils/segmentation/CommToolsData.js.map +1 -1
  8. package/dist/Utils/segmentation/DragOperator.d.ts +13 -7
  9. package/dist/Utils/segmentation/DragOperator.js +51 -165
  10. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  11. package/dist/Utils/segmentation/DrawToolCore.d.ts +57 -76
  12. package/dist/Utils/segmentation/DrawToolCore.js +368 -878
  13. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  14. package/dist/Utils/segmentation/NrrdTools.d.ts +277 -15
  15. package/dist/Utils/segmentation/NrrdTools.js +594 -232
  16. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  17. package/dist/Utils/segmentation/core/MaskVolume.d.ts +477 -0
  18. package/dist/Utils/segmentation/core/MaskVolume.js +1238 -0
  19. package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -0
  20. package/dist/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
  21. package/dist/Utils/segmentation/core/MigrationUtils.js +163 -0
  22. package/dist/Utils/segmentation/core/MigrationUtils.js.map +1 -0
  23. package/dist/Utils/segmentation/core/UndoManager.d.ts +47 -0
  24. package/dist/Utils/segmentation/core/UndoManager.js +86 -0
  25. package/dist/Utils/segmentation/core/UndoManager.js.map +1 -0
  26. package/dist/Utils/segmentation/core/index.d.ts +76 -0
  27. package/dist/Utils/segmentation/core/index.js +75 -0
  28. package/dist/Utils/segmentation/core/index.js.map +1 -0
  29. package/dist/Utils/segmentation/core/types.d.ts +106 -0
  30. package/dist/Utils/segmentation/core/types.js +98 -0
  31. package/dist/Utils/segmentation/core/types.js.map +1 -0
  32. package/dist/Utils/segmentation/coreTools/coreType.d.ts +55 -36
  33. package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
  34. package/dist/Utils/segmentation/coreTools/gui.d.ts +6 -16
  35. package/dist/Utils/segmentation/coreTools/gui.js +23 -27
  36. package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
  37. package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
  38. package/dist/Utils/segmentation/eventRouter/EventRouter.js +369 -0
  39. package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -0
  40. package/dist/Utils/segmentation/eventRouter/index.d.ts +7 -0
  41. package/dist/Utils/segmentation/eventRouter/index.js +7 -0
  42. package/dist/Utils/segmentation/eventRouter/index.js.map +1 -0
  43. package/dist/Utils/segmentation/eventRouter/types.d.ts +70 -0
  44. package/dist/Utils/segmentation/eventRouter/types.js +7 -0
  45. package/dist/Utils/segmentation/eventRouter/types.js.map +1 -0
  46. package/dist/Utils/segmentation/tools/BaseTool.d.ts +25 -0
  47. package/dist/Utils/segmentation/tools/BaseTool.js +18 -0
  48. package/dist/Utils/segmentation/tools/BaseTool.js.map +1 -0
  49. package/dist/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
  50. package/dist/Utils/segmentation/tools/ContrastTool.js +118 -0
  51. package/dist/Utils/segmentation/tools/ContrastTool.js.map +1 -0
  52. package/dist/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
  53. package/dist/Utils/segmentation/tools/CrosshairTool.js +157 -0
  54. package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -0
  55. package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
  56. package/dist/Utils/segmentation/tools/DragSliceTool.js +170 -0
  57. package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -0
  58. package/dist/Utils/segmentation/tools/EraserTool.d.ts +16 -0
  59. package/dist/Utils/segmentation/tools/EraserTool.js +92 -0
  60. package/dist/Utils/segmentation/tools/EraserTool.js.map +1 -0
  61. package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
  62. package/dist/Utils/segmentation/tools/ImageStoreHelper.js +174 -0
  63. package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -0
  64. package/dist/Utils/segmentation/tools/SphereTool.d.ts +38 -0
  65. package/dist/Utils/segmentation/tools/SphereTool.js +193 -0
  66. package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -0
  67. package/dist/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
  68. package/dist/Utils/segmentation/tools/ZoomTool.js +59 -0
  69. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -0
  70. package/dist/Utils/segmentation/tools/index.d.ts +22 -0
  71. package/dist/Utils/segmentation/tools/index.js +23 -0
  72. package/dist/Utils/segmentation/tools/index.js.map +1 -0
  73. package/dist/bundle.esm.js +4740 -1979
  74. package/dist/bundle.umd.js +4743 -1978
  75. package/dist/index.d.ts +5 -3
  76. package/dist/index.js +4 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/types/Renderer/copperRenderer.d.ts +1 -1
  79. package/dist/types/Renderer/copperRendererOnDemond.d.ts +1 -1
  80. package/dist/types/Scene/copperScene.d.ts +3 -0
  81. package/dist/types/Utils/segmentation/CommToolsData.d.ts +109 -6
  82. package/dist/types/Utils/segmentation/DragOperator.d.ts +13 -7
  83. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +57 -76
  84. package/dist/types/Utils/segmentation/NrrdTools.d.ts +277 -15
  85. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +477 -0
  86. package/dist/types/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
  87. package/dist/types/Utils/segmentation/core/UndoManager.d.ts +47 -0
  88. package/dist/types/Utils/segmentation/core/index.d.ts +76 -0
  89. package/dist/types/Utils/segmentation/core/types.d.ts +106 -0
  90. package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +55 -36
  91. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +6 -16
  92. package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
  93. package/dist/types/Utils/segmentation/eventRouter/index.d.ts +7 -0
  94. package/dist/types/Utils/segmentation/eventRouter/types.d.ts +70 -0
  95. package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +25 -0
  96. package/dist/types/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
  97. package/dist/types/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
  98. package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
  99. package/dist/types/Utils/segmentation/tools/EraserTool.d.ts +16 -0
  100. package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
  101. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +38 -0
  102. package/dist/types/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
  103. package/dist/types/Utils/segmentation/tools/index.d.ts +22 -0
  104. package/dist/types/index.d.ts +5 -3
  105. package/dist/types/types/types.d.ts +3 -3
  106. package/dist/types/types.d.ts +3 -3
  107. package/package.json +1 -1
  108. package/dist/Utils/segmentation/coreTools/archive.d.ts +0 -1
  109. package/dist/Utils/segmentation/coreTools/archive.js +0 -23
  110. package/dist/Utils/segmentation/coreTools/archive.js.map +0 -1
  111. package/dist/types/Utils/segmentation/coreTools/archive.d.ts +0 -1
@@ -1,9 +1,17 @@
1
1
  import { CommToolsData } from "./CommToolsData";
2
- import { switchEraserSize, switchPencilIcon, throttle } from "../utils";
2
+ import { switchEraserSize, switchPencilIcon } from "../utils";
3
+ import { EventRouter } from "./eventRouter";
4
+ import { SphereTool } from "./tools/SphereTool";
5
+ import { CrosshairTool } from "./tools/CrosshairTool";
6
+ import { ContrastTool } from "./tools/ContrastTool";
7
+ import { ZoomTool } from "./tools/ZoomTool";
8
+ import { EraserTool } from "./tools/EraserTool";
9
+ import { ImageStoreHelper } from "./tools/ImageStoreHelper";
10
+ import { UndoManager } from "./core";
3
11
  export class DrawToolCore extends CommToolsData {
4
- constructor(container) {
12
+ constructor(container, options) {
5
13
  const mainAreaContainer = document.createElement("div");
6
- super(container, mainAreaContainer);
14
+ super(container, mainAreaContainer, options);
7
15
  this.drawingPrameters = {
8
16
  handleOnDrawingMouseDown: (ev) => { },
9
17
  handleOnDrawingMouseMove: (ev) => { },
@@ -28,7 +36,13 @@ export class DrawToolCore extends CommToolsData {
28
36
  };
29
37
  this.eraserUrls = [];
30
38
  this.pencilUrls = [];
31
- this.undoArray = [];
39
+ this.undoManager = new UndoManager();
40
+ /** Snapshot of the active layer's slice captured on mouse-down (before drawing). */
41
+ this.preDrawSlice = null;
42
+ this.preDrawAxis = "z";
43
+ this.preDrawSliceIndex = 0;
44
+ // Centralized event router
45
+ this.eventRouter = null;
32
46
  // need to return to parent
33
47
  this.start = () => { };
34
48
  /*************************************May consider to move outside *******************************************/
@@ -40,69 +54,146 @@ export class DrawToolCore extends CommToolsData {
40
54
  this.protectedData.ctxes.drawingCtx.stroke();
41
55
  };
42
56
  this.configContrastDragMode = () => {
43
- this.container.style.cursor = "pointer";
44
- this.container.addEventListener("pointerdown", this.contrastEventPrameters.handleOnContrastMouseDown, true);
45
- this.container.addEventListener("pointerup", this.contrastEventPrameters.handleOnContrastMouseUp, true);
57
+ this.contrastTool.configContrastDragMode();
46
58
  };
47
59
  this.removeContrastDragMode = () => {
48
- this.container.style.cursor = "";
49
- this.container.removeEventListener("pointerdown", this.contrastEventPrameters.handleOnContrastMouseDown, true);
50
- this.container.removeEventListener("pointermove", this.contrastEventPrameters.handleOnContrastMouseMove, true);
51
- this.container.removeEventListener("pointerup", this.contrastEventPrameters.handleOnContrastMouseUp, true);
52
- this.container.removeEventListener("pointerleave", this.contrastEventPrameters.handleOnContrastMouseLeave, true);
53
- this.setIsDrawFalse(1000);
60
+ this.contrastTool.removeContrastDragMode();
61
+ };
62
+ this.updateSlicesContrast = (value, flag) => {
63
+ this.contrastTool.updateSlicesContrast(value, flag);
64
+ };
65
+ this.repraintCurrentContrastSlice = () => {
66
+ this.contrastTool.repraintCurrentContrastSlice();
54
67
  };
55
68
  this.container = container;
56
69
  this.mainAreaContainer = mainAreaContainer;
70
+ this.initTools();
57
71
  this.initDrawToolCore();
58
72
  }
73
+ initTools() {
74
+ const toolCtx = {
75
+ nrrd_states: this.nrrd_states,
76
+ gui_states: this.gui_states,
77
+ protectedData: this.protectedData,
78
+ cursorPage: this.cursorPage,
79
+ };
80
+ this.imageStoreHelper = new ImageStoreHelper(toolCtx, {
81
+ setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
82
+ drawImageOnEmptyImage: (canvas) => this.drawImageOnEmptyImage(canvas),
83
+ });
84
+ this.sphereTool = new SphereTool(toolCtx, {
85
+ setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
86
+ drawImageOnEmptyImage: (canvas) => this.drawImageOnEmptyImage(canvas),
87
+ storeImageToAxis: (index, paintedImages, imageData, axis) => this.imageStoreHelper.storeImageToAxis(index, paintedImages, imageData, axis),
88
+ createEmptyPaintImage: (dimensions, paintImages) => this.createEmptyPaintImage(dimensions, paintImages),
89
+ });
90
+ this.crosshairTool = new CrosshairTool(toolCtx);
91
+ this.contrastTool = new ContrastTool(toolCtx, this.container, this.contrastEventPrameters, {
92
+ setIsDrawFalse: (target) => this.setIsDrawFalse(target),
93
+ setSyncsliceNum: () => this.setSyncsliceNum(),
94
+ });
95
+ this.zoomTool = new ZoomTool(toolCtx, this.container, this.mainAreaContainer, {
96
+ resetPaintAreaUIPosition: (l, t) => this.resetPaintAreaUIPosition(l, t),
97
+ resizePaintArea: (moveDistance) => this.resizePaintArea(moveDistance),
98
+ setIsDrawFalse: (target) => this.setIsDrawFalse(target),
99
+ });
100
+ this.eraserTool = new EraserTool(toolCtx);
101
+ }
59
102
  initDrawToolCore() {
60
- let undoFlag = false;
61
- this.container.addEventListener("keydown", (ev) => {
62
- if (this.nrrd_states.configKeyBoard)
63
- return;
64
- if (ev.key === this.nrrd_states.keyboardSettings.draw && !this.gui_states.sphere && !this.gui_states.calculator) {
65
- if (this.protectedData.Is_Ctrl_Pressed) {
103
+ // Initialize EventRouter for centralized event handling
104
+ this.eventRouter = new EventRouter({
105
+ container: this.container,
106
+ canvas: this.protectedData.canvases.drawingCanvas,
107
+ onModeChange: (prevMode, newMode) => {
108
+ // Use string comparison to avoid TypeScript narrowing issues
109
+ const prev = prevMode;
110
+ const next = newMode;
111
+ // Sync EventRouter mode changes with existing state flags
112
+ if (next === 'draw') {
113
+ this.protectedData.Is_Shift_Pressed = true;
114
+ this.nrrd_states.enableCursorChoose = false;
115
+ }
116
+ else if (prev === 'draw') {
66
117
  this.protectedData.Is_Shift_Pressed = false;
67
- return;
68
118
  }
69
- this.protectedData.Is_Shift_Pressed = true;
70
- this.nrrd_states.enableCursorChoose = false;
71
- }
72
- if (ev.key === this.nrrd_states.keyboardSettings.crosshair) {
73
- this.protectedData.Is_Draw = false;
74
- this.nrrd_states.enableCursorChoose =
75
- !this.nrrd_states.enableCursorChoose;
119
+ if (next === 'contrast') {
120
+ this.protectedData.Is_Ctrl_Pressed = true;
121
+ this.protectedData.Is_Shift_Pressed = false;
122
+ this.configContrastDragMode();
123
+ }
124
+ else if (prev === 'contrast') {
125
+ this.protectedData.Is_Ctrl_Pressed = false;
126
+ this.removeContrastDragMode();
127
+ this.gui_states.readyToUpdate = true;
128
+ }
129
+ if (next === 'crosshair') {
130
+ this.nrrd_states.enableCursorChoose = true;
131
+ this.protectedData.Is_Draw = false;
132
+ }
133
+ else if (prev === 'crosshair') {
134
+ this.nrrd_states.enableCursorChoose = false;
135
+ }
76
136
  }
77
- if ((ev.ctrlKey || ev.metaKey) && ev.key === this.nrrd_states.keyboardSettings.undo) {
137
+ });
138
+ // Configure keyboard settings from class field
139
+ this.eventRouter.setKeyboardSettings(this._keyboardSettings);
140
+ // Track undo flag for Ctrl+Z handling
141
+ let undoFlag = false;
142
+ // Register keyboard handlers with EventRouter
143
+ this.eventRouter.setKeydownHandler((ev) => {
144
+ var _a;
145
+ if (this._configKeyBoard)
146
+ return;
147
+ // Handle undo (Ctrl+Z)
148
+ if ((ev.ctrlKey || ev.metaKey) && ev.key === this._keyboardSettings.undo) {
78
149
  undoFlag = true;
79
150
  this.undoLastPainting();
80
151
  }
152
+ // Handle redo (Ctrl+<redo key> or Ctrl+Shift+<undo key>)
153
+ const redoKey = this._keyboardSettings.redo;
154
+ const undoKeyUpper = this._keyboardSettings.undo.toUpperCase();
155
+ if ((ev.ctrlKey || ev.metaKey) && (ev.key === redoKey || (ev.shiftKey && ev.key === undoKeyUpper))) {
156
+ this.redoLastPainting();
157
+ }
158
+ // Handle crosshair toggle
159
+ if (ev.key === this._keyboardSettings.crosshair) {
160
+ if (!this.gui_states.sphere && !this.gui_states.calculator) {
161
+ (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.toggleCrosshair();
162
+ }
163
+ }
164
+ // Handle draw mode (Shift key) - EventRouter already tracks this
165
+ if (ev.key === this._keyboardSettings.draw && !this.gui_states.sphere && !this.gui_states.calculator) {
166
+ if (this.protectedData.Is_Ctrl_Pressed) {
167
+ return; // Ctrl takes priority
168
+ }
169
+ // EventRouter will set mode to 'draw' via internal handler
170
+ }
81
171
  });
82
- this.container.addEventListener("keyup", (ev) => {
83
- if (this.nrrd_states.configKeyBoard)
172
+ this.eventRouter.setKeyupHandler((ev) => {
173
+ var _a, _b, _c, _d;
174
+ if (this._configKeyBoard)
84
175
  return;
85
- if (this.nrrd_states.keyboardSettings.contrast.includes(ev.key)) {
176
+ // Handle Ctrl key release (contrast mode toggle)
177
+ if (this._keyboardSettings.contrast.includes(ev.key)) {
86
178
  if (undoFlag) {
87
179
  this.gui_states.readyToUpdate = true;
88
180
  undoFlag = false;
89
181
  return;
90
182
  }
91
- // Ctrl key pressed on either Windows or macOS
92
- this.protectedData.Is_Shift_Pressed = false;
93
- this.protectedData.Is_Ctrl_Pressed = !this.protectedData.Is_Ctrl_Pressed;
94
- if (this.protectedData.Is_Ctrl_Pressed) {
95
- this.configContrastDragMode();
183
+ // Skip mode toggle when contrast shortcut is disabled
184
+ if (!((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isContrastEnabled()))
185
+ return;
186
+ // Toggle contrast mode manually since it's on keyup
187
+ if (((_b = this.eventRouter) === null || _b === void 0 ? void 0 : _b.getMode()) !== 'contrast') {
188
+ (_c = this.eventRouter) === null || _c === void 0 ? void 0 : _c.setMode('contrast');
96
189
  }
97
190
  else {
98
- this.removeContrastDragMode();
99
- this.gui_states.readyToUpdate = true;
191
+ (_d = this.eventRouter) === null || _d === void 0 ? void 0 : _d.setMode('idle');
100
192
  }
101
193
  }
102
- if (ev.key === this.nrrd_states.keyboardSettings.draw) {
103
- this.protectedData.Is_Shift_Pressed = false;
104
- }
105
194
  });
195
+ // Bind all event listeners
196
+ this.eventRouter.bindAll();
106
197
  }
107
198
  setEraserUrls(urls) {
108
199
  this.eraserUrls = urls;
@@ -114,31 +205,21 @@ export class DrawToolCore extends CommToolsData {
114
205
  this.gui_states.defaultPaintCursor;
115
206
  }
116
207
  setCurrentLayer() {
117
- let ctx;
118
- let canvas;
119
- switch (this.gui_states.label) {
120
- case "label1":
121
- ctx = this.protectedData.ctxes.drawingLayerOneCtx;
122
- canvas = this.protectedData.canvases.drawingCanvasLayerOne;
123
- break;
124
- case "label2":
125
- ctx = this.protectedData.ctxes.drawingLayerTwoCtx;
126
- canvas = this.protectedData.canvases.drawingCanvasLayerTwo;
127
- break;
128
- case "label3":
129
- ctx = this.protectedData.ctxes.drawingLayerThreeCtx;
130
- canvas = this.protectedData.canvases.drawingCanvasLayerThree;
131
- break;
132
- default:
133
- ctx = this.protectedData.ctxes.drawingLayerOneCtx;
134
- canvas = this.protectedData.canvases.drawingCanvasLayerOne;
135
- break;
208
+ const layer = this.gui_states.layer;
209
+ let target = this.protectedData.layerTargets.get(layer);
210
+ if (!target) {
211
+ // Fallback to first layer
212
+ const firstId = this.nrrd_states.layers[0];
213
+ target = this.protectedData.layerTargets.get(firstId);
136
214
  }
137
- return { ctx, canvas };
215
+ return { ctx: target.ctx, canvas: target.canvas };
138
216
  }
139
217
  draw(opts) {
140
218
  if (!!opts) {
141
219
  this.nrrd_states.getMask = opts === null || opts === void 0 ? void 0 : opts.getMaskData;
220
+ if (opts === null || opts === void 0 ? void 0 : opts.onClearLayerVolume) {
221
+ this.nrrd_states.onClearLayerVolume = opts.onClearLayerVolume;
222
+ }
142
223
  this.nrrd_states.getSphere = opts === null || opts === void 0 ? void 0 : opts.getSphereData;
143
224
  this.nrrd_states.getCalculateSpherePositions = opts === null || opts === void 0 ? void 0 : opts.getCalculateSpherePositionsData;
144
225
  }
@@ -195,10 +276,7 @@ export class DrawToolCore extends CommToolsData {
195
276
  this.protectedData.canvases.drawingCanvas.addEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
196
277
  }
197
278
  clearSpherePrintStoreImages() {
198
- this.protectedData.maskData.paintImages.x.length = 0;
199
- this.protectedData.maskData.paintImages.y.length = 0;
200
- this.protectedData.maskData.paintImages.z.length = 0;
201
- this.createEmptyPaintImage(this.nrrd_states.dimensions, this.protectedData.maskData.paintImages);
279
+ this.sphereTool.clearSpherePrintStoreImages();
202
280
  }
203
281
  paintOnCanvas() {
204
282
  var _a, _b, _c;
@@ -225,13 +303,16 @@ export class DrawToolCore extends CommToolsData {
225
303
  this.protectedData.previousDrawingImage =
226
304
  this.protectedData.ctxes.drawingCtx.getImageData(0, 0, this.protectedData.canvases.drawingCanvas.width, this.protectedData.canvases.drawingCanvas.height);
227
305
  // let a global variable to store the wheel move event
228
- if (this.nrrd_states.keyboardSettings.mouseWheel === "Scroll:Zoom") {
306
+ // Remove existing listener before creating a new one to prevent leaks
307
+ this.protectedData.canvases.drawingCanvas.removeEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
308
+ if (this._keyboardSettings.mouseWheel === "Scroll:Zoom") {
229
309
  this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseZoomWheel();
230
310
  }
231
311
  else {
232
312
  this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseSliceWheel();
233
313
  }
234
- // init to add it
314
+ // Keep wheel as direct addEventListener due to dynamic add/remove patterns in handleOnDrawingMouseUp
315
+ // EventRouter routing would conflict with the dynamic wheel switching (zoom vs sphere wheel)
235
316
  this.protectedData.canvases.drawingCanvas.addEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel, {
236
317
  passive: false,
237
318
  });
@@ -318,6 +399,16 @@ export class DrawToolCore extends CommToolsData {
318
399
  }
319
400
  this.nrrd_states.drawStartPos.x = e.offsetX;
320
401
  this.nrrd_states.drawStartPos.y = e.offsetY;
402
+ // Capture pre-draw slice snapshot for undo
403
+ try {
404
+ this.preDrawAxis = this.protectedData.axis;
405
+ this.preDrawSliceIndex = this.nrrd_states.currentIndex;
406
+ const vol = this.getVolumeForLayer(this.gui_states.layer);
407
+ this.preDrawSlice = vol.getSliceUint8(this.preDrawSliceIndex, this.preDrawAxis).data.slice();
408
+ }
409
+ catch (_a) {
410
+ this.preDrawSlice = null;
411
+ }
321
412
  this.protectedData.canvases.drawingCanvas.addEventListener("pointerup", this.drawingPrameters.handleOnDrawingMouseUp);
322
413
  this.protectedData.canvases.drawingCanvas.addEventListener("pointermove", this.drawingPrameters.handleOnDrawingMouseMove);
323
414
  }
@@ -350,8 +441,17 @@ export class DrawToolCore extends CommToolsData {
350
441
  return;
351
442
  }
352
443
  };
353
- // disable browser right click menu
354
- this.protectedData.canvases.drawingCanvas.addEventListener("pointerdown", this.drawingPrameters.handleOnDrawingMouseDown, true);
444
+ // Route pointerdown through EventRouter for centralized event management
445
+ // The handler is still the existing logic, just registered through EventRouter
446
+ if (this.eventRouter) {
447
+ this.eventRouter.setPointerDownHandler((e) => {
448
+ this.drawingPrameters.handleOnDrawingMouseDown(e);
449
+ });
450
+ }
451
+ else {
452
+ // Fallback for legacy mode without EventRouter
453
+ this.protectedData.canvases.drawingCanvas.addEventListener("pointerdown", this.drawingPrameters.handleOnDrawingMouseDown, true);
454
+ }
355
455
  const sphere = (e) => {
356
456
  // set sphere size
357
457
  this.protectedData.canvases.drawingCanvas.removeEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
@@ -372,31 +472,19 @@ export class DrawToolCore extends CommToolsData {
372
472
  this.protectedData.canvases.drawingCanvas.addEventListener("wheel", this.drawingPrameters.handleSphereWheel, true);
373
473
  this.protectedData.canvases.drawingCanvas.addEventListener("pointerup", this.drawingPrameters.handleOnDrawingMouseUp);
374
474
  };
375
- const redrawPreviousImageToLabelCtx = (ctx, label = "default") => {
475
+ const redrawPreviousImageToLayerCtx = (ctx) => {
376
476
  var _a;
377
- let paintImages;
378
- switch (label) {
379
- case "label1":
380
- paintImages = this.protectedData.maskData.paintImagesLabel1;
381
- break;
382
- case "label2":
383
- paintImages = this.protectedData.maskData.paintImagesLabel2;
384
- break;
385
- case "label3":
386
- paintImages = this.protectedData.maskData.paintImagesLabel3;
387
- break;
388
- default:
389
- paintImages = this.protectedData.maskData.paintImages;
390
- break;
391
- }
392
- const tempPreImg = (_a = this.filterDrawedImage(this.protectedData.axis, this.nrrd_states.currentIndex, paintImages)) === null || _a === void 0 ? void 0 : _a.image;
477
+ const tempPreImg = (_a = this.filterDrawedImage(this.protectedData.axis, this.nrrd_states.currentIndex)) === null || _a === void 0 ? void 0 : _a.image;
393
478
  this.protectedData.canvases.emptyCanvas.width =
394
479
  this.protectedData.canvases.emptyCanvas.width;
395
- if (tempPreImg && label == "default") {
480
+ if (tempPreImg) {
396
481
  this.protectedData.previousDrawingImage = tempPreImg;
397
482
  }
398
483
  this.protectedData.ctxes.emptyCtx.putImageData(tempPreImg, 0, 0);
399
- // draw privous image
484
+ // No flip needed: MaskVolume stores in source coordinates (matching the
485
+ // Three.js / layer canvas convention). The layer canvas is in screen
486
+ // coordinates, which already match the source coordinate system.
487
+ ctx.imageSmoothingEnabled = false;
400
488
  ctx.drawImage(this.protectedData.canvases.emptyCanvas, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
401
489
  };
402
490
  this.drawingPrameters.handleOnDrawingMouseUp = (e) => {
@@ -407,13 +495,12 @@ export class DrawToolCore extends CommToolsData {
407
495
  ctx.closePath();
408
496
  this.protectedData.canvases.drawingCanvas.removeEventListener("pointermove", this.drawingPrameters.handleOnDrawingMouseMove);
409
497
  if (!this.gui_states.Eraser) {
410
- if (this.gui_states.segmentation) {
411
- this.protectedData.canvases.drawingCanvasLayerMaster.width =
412
- this.protectedData.canvases.drawingCanvasLayerMaster.width;
498
+ if (this.gui_states.pencil) {
499
+ // Clear only the current layer canvas (NOT master)
413
500
  canvas.width = canvas.width;
414
- redrawPreviousImageToLabelCtx(this.protectedData.ctxes.drawingLayerMasterCtx);
415
- redrawPreviousImageToLabelCtx(ctx, this.gui_states.label);
416
- // draw new drawings
501
+ // Redraw previous layer data from volume
502
+ redrawPreviousImageToLayerCtx(ctx);
503
+ // Draw new pencil strokes on current layer canvas
417
504
  ctx.beginPath();
418
505
  ctx.moveTo(lines[0].x, lines[0].y);
419
506
  for (let i = 1; i < lines.length; i++) {
@@ -423,36 +510,37 @@ export class DrawToolCore extends CommToolsData {
423
510
  ctx.lineWidth = 1;
424
511
  ctx.fillStyle = this.gui_states.fillColor;
425
512
  ctx.fill();
426
- // draw layer to master layer
427
- this.protectedData.ctxes.drawingLayerMasterCtx.drawImage(canvas, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
513
+ // Composite ALL layers to master (not just current layer)
514
+ this.compositeAllLayers();
428
515
  }
429
516
  }
430
517
  this.protectedData.previousDrawingImage =
431
518
  this.protectedData.ctxes.drawingLayerMasterCtx.getImageData(0, 0, this.protectedData.canvases.drawingCanvasLayerMaster.width, this.protectedData.canvases.drawingCanvasLayerMaster.height);
432
- this.storeAllImages(this.nrrd_states.currentIndex, this.gui_states.label);
519
+ this.storeAllImages(this.nrrd_states.currentIndex, this.gui_states.layer);
433
520
  if (this.gui_states.Eraser) {
434
- const restLabels = this.getRestLabel();
435
- this.storeEachLayerImage(this.nrrd_states.currentIndex, restLabels[0]);
436
- this.storeEachLayerImage(this.nrrd_states.currentIndex, restLabels[1]);
521
+ const restLayers = this.getRestLayer();
522
+ this.storeEachLayerImage(this.nrrd_states.currentIndex, restLayers[0]);
523
+ this.storeEachLayerImage(this.nrrd_states.currentIndex, restLayers[1]);
437
524
  }
438
525
  Is_Painting = false;
439
- /**
440
- * store undo array
441
- */
442
- const currentUndoObj = this.getCurrentUndo();
443
- const src = this.protectedData.canvases.drawingCanvasLayerMaster.toDataURL();
444
- const image = new Image();
445
- image.src = src;
446
- if (currentUndoObj.length > 0) {
447
- currentUndoObj[0].layers[this.gui_states.label].push(image);
448
- }
449
- else {
450
- const undoObj = {
451
- sliceIndex: this.nrrd_states.currentIndex,
452
- layers: { label1: [], label2: [], label3: [] },
453
- };
454
- undoObj.layers[this.gui_states.label].push(image);
455
- this.undoArray.push(undoObj);
526
+ // Push delta to UndoManager (new Delta-based undo system)
527
+ if (this.preDrawSlice) {
528
+ try {
529
+ const vol = this.getVolumeForLayer(this.gui_states.layer);
530
+ const { data: newSlice } = vol.getSliceUint8(this.preDrawSliceIndex, this.preDrawAxis);
531
+ const delta = {
532
+ layerId: this.gui_states.layer,
533
+ axis: this.preDrawAxis,
534
+ sliceIndex: this.preDrawSliceIndex,
535
+ oldSlice: this.preDrawSlice,
536
+ newSlice: newSlice.slice(),
537
+ };
538
+ this.undoManager.push(delta);
539
+ }
540
+ catch (_a) {
541
+ // Volume not ready — skip
542
+ }
543
+ this.preDrawSlice = null;
456
544
  }
457
545
  // add wheel after pointer up
458
546
  this.protectedData.canvases.drawingCanvas.addEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel, {
@@ -504,13 +592,12 @@ export class DrawToolCore extends CommToolsData {
504
592
  else {
505
593
  return;
506
594
  }
507
- if (!this.gui_states.segmentation) {
595
+ if (!this.gui_states.pencil) {
508
596
  this.setIsDrawFalse(100);
509
597
  }
510
598
  };
511
599
  this.protectedData.canvases.drawingCanvas.addEventListener("pointerleave", (e) => {
512
600
  Is_Painting = false;
513
- // (this.sceneIn as copperScene).controls.enabled = true;
514
601
  if (leftclicked) {
515
602
  leftclicked = false;
516
603
  this.protectedData.ctxes.drawingLayerMasterCtx.closePath();
@@ -523,7 +610,7 @@ export class DrawToolCore extends CommToolsData {
523
610
  this.protectedData.canvases.drawingCanvas.removeEventListener("pointermove", this.drawingPrameters.handleOnPanMouseMove);
524
611
  }
525
612
  this.setIsDrawFalse(100);
526
- if (this.gui_states.segmentation) {
613
+ if (this.gui_states.pencil) {
527
614
  this.setIsDrawFalse(1000);
528
615
  }
529
616
  });
@@ -535,16 +622,14 @@ export class DrawToolCore extends CommToolsData {
535
622
  if (this.protectedData.Is_Draw) {
536
623
  this.protectedData.ctxes.drawingLayerMasterCtx.lineCap = "round";
537
624
  this.protectedData.ctxes.drawingLayerMasterCtx.globalAlpha = 1;
538
- this.protectedData.ctxes.drawingLayerOneCtx.lineCap = "round";
539
- this.protectedData.ctxes.drawingLayerOneCtx.globalAlpha = 1;
540
- this.protectedData.ctxes.drawingLayerTwoCtx.lineCap = "round";
541
- this.protectedData.ctxes.drawingLayerTwoCtx.globalAlpha = 1;
542
- this.protectedData.ctxes.drawingLayerThreeCtx.lineCap = "round";
543
- this.protectedData.ctxes.drawingLayerThreeCtx.globalAlpha = 1;
625
+ for (const [, target] of this.protectedData.layerTargets) {
626
+ target.ctx.lineCap = "round";
627
+ target.ctx.globalAlpha = 1;
628
+ }
544
629
  }
545
630
  else {
546
631
  if (this.protectedData.Is_Shift_Pressed) {
547
- if (!this.gui_states.segmentation &&
632
+ if (!this.gui_states.pencil &&
548
633
  !this.gui_states.Eraser &&
549
634
  this.nrrd_states.Mouse_Over) {
550
635
  this.protectedData.ctxes.drawingCtx.clearRect(0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
@@ -565,6 +650,8 @@ export class DrawToolCore extends CommToolsData {
565
650
  this.drawLine(0, ey, this.nrrd_states.changedHeight, ey);
566
651
  }
567
652
  }
653
+ // globalAlpha was set to gui_states.globalAlpha at the top of start().
654
+ // Master stores full-alpha pixels; transparency applied here only.
568
655
  this.protectedData.ctxes.drawingCtx.drawImage(this.protectedData.canvases.drawingCanvasLayerMaster, 0, 0);
569
656
  }
570
657
  else {
@@ -575,7 +662,7 @@ export class DrawToolCore extends CommToolsData {
575
662
  drawLinesOnLayer(ctx, x, y) {
576
663
  ctx.beginPath();
577
664
  ctx.moveTo(this.nrrd_states.drawStartPos.x, this.nrrd_states.drawStartPos.y);
578
- if (this.gui_states.segmentation) {
665
+ if (this.gui_states.pencil) {
579
666
  ctx.strokeStyle = this.gui_states.color;
580
667
  ctx.lineWidth = this.gui_states.lineWidth;
581
668
  }
@@ -589,8 +676,10 @@ export class DrawToolCore extends CommToolsData {
589
676
  }
590
677
  paintOnCanvasLayer(x, y) {
591
678
  let { ctx, canvas } = this.setCurrentLayer();
679
+ // Draw only on the current layer canvas (not master directly)
592
680
  this.drawLinesOnLayer(ctx, x, y);
593
- this.drawLinesOnLayer(this.protectedData.ctxes.drawingLayerMasterCtx, x, y);
681
+ // Composite all layers to master to preserve other layers' data
682
+ this.compositeAllLayers();
594
683
  // reset drawing start position to current position.
595
684
  this.nrrd_states.drawStartPos.x = x;
596
685
  this.nrrd_states.drawStartPos.y = y;
@@ -624,15 +713,13 @@ export class DrawToolCore extends CommToolsData {
624
713
  * it should be hide, so we don't need to add it to mainAreaContainer
625
714
  */
626
715
  this.protectedData.canvases.drawingCanvasLayerMaster.width =
627
- this.protectedData.canvases.drawingCanvasLayerOne.width =
628
- this.protectedData.canvases.drawingCanvasLayerTwo.width =
629
- this.protectedData.canvases.drawingCanvasLayerThree.width =
630
- this.nrrd_states.changedWidth;
716
+ this.nrrd_states.changedWidth;
631
717
  this.protectedData.canvases.drawingCanvasLayerMaster.height =
632
- this.protectedData.canvases.drawingCanvasLayerOne.height =
633
- this.protectedData.canvases.drawingCanvasLayerTwo.height =
634
- this.protectedData.canvases.drawingCanvasLayerThree.height =
635
- this.nrrd_states.changedHeight;
718
+ this.nrrd_states.changedHeight;
719
+ for (const [, target] of this.protectedData.layerTargets) {
720
+ target.canvas.width = this.nrrd_states.changedWidth;
721
+ target.canvas.height = this.nrrd_states.changedHeight;
722
+ }
636
723
  /**
637
724
  * display and drawing canvas container
638
725
  */
@@ -646,67 +733,11 @@ export class DrawToolCore extends CommToolsData {
646
733
  this.mainAreaContainer.appendChild(this.protectedData.canvases.drawingCanvas);
647
734
  }
648
735
  useEraser() {
649
- const clearArc = (x, y, radius) => {
650
- var calcWidth = radius - this.nrrd_states.stepClear;
651
- var calcHeight = Math.sqrt(radius * radius - calcWidth * calcWidth);
652
- var posX = x - calcWidth;
653
- var posY = y - calcHeight;
654
- var widthX = 2 * calcWidth;
655
- var heightY = 2 * calcHeight;
656
- if (this.nrrd_states.stepClear <= radius) {
657
- this.protectedData.ctxes.drawingLayerMasterCtx.clearRect(posX, posY, widthX, heightY);
658
- this.protectedData.ctxes.drawingLayerOneCtx.clearRect(posX, posY, widthX, heightY);
659
- this.protectedData.ctxes.drawingLayerTwoCtx.clearRect(posX, posY, widthX, heightY);
660
- this.protectedData.ctxes.drawingLayerThreeCtx.clearRect(posX, posY, widthX, heightY);
661
- this.nrrd_states.stepClear += 1;
662
- clearArc(x, y, radius);
663
- }
664
- };
665
- return clearArc;
736
+ return this.eraserTool.createClearArc();
666
737
  }
667
738
  // drawing canvas mouse zoom wheel
668
739
  configMouseZoomWheel() {
669
- let moveDistance = 1;
670
- const handleMouseZoomSliceWheelMove = (e) => {
671
- if (this.protectedData.Is_Shift_Pressed) {
672
- return;
673
- }
674
- e.preventDefault();
675
- // this.nrrd_states.originWidth;
676
- const delta = e.detail ? e.detail > 0 : e.wheelDelta < 0;
677
- this.protectedData.Is_Draw = true;
678
- var rect = this.container.getBoundingClientRect();
679
- const ratioL = (e.clientX - rect.left -
680
- this.mainAreaContainer.offsetLeft -
681
- this.protectedData.canvases.drawingCanvas.offsetLeft) /
682
- this.protectedData.canvases.drawingCanvas.offsetWidth;
683
- const ratioT = (e.clientY - rect.top -
684
- this.mainAreaContainer.offsetTop -
685
- this.protectedData.canvases.drawingCanvas.offsetTop) /
686
- this.protectedData.canvases.drawingCanvas.offsetHeight;
687
- const ratioDelta = !delta ? 1 + 0.1 : 1 - 0.1;
688
- const w = this.protectedData.canvases.drawingCanvas.offsetWidth * ratioDelta;
689
- const h = this.protectedData.canvases.drawingCanvas.offsetHeight * ratioDelta;
690
- const l = Math.round(e.clientX - this.mainAreaContainer.offsetLeft - w * ratioL - rect.left);
691
- const t = Math.round(e.clientY - this.mainAreaContainer.offsetTop - h * ratioT - rect.top);
692
- moveDistance = w / this.nrrd_states.originWidth;
693
- if (moveDistance > 8) {
694
- moveDistance = 8;
695
- // this.resetPaintAreaUIPosition();
696
- }
697
- else if (moveDistance < 1) {
698
- moveDistance = 1;
699
- this.resetPaintAreaUIPosition();
700
- this.resizePaintArea(moveDistance);
701
- }
702
- else {
703
- this.resetPaintAreaUIPosition(l, t);
704
- this.resizePaintArea(moveDistance);
705
- }
706
- this.setIsDrawFalse(1000);
707
- this.nrrd_states.sizeFoctor = moveDistance;
708
- };
709
- return handleMouseZoomSliceWheelMove;
740
+ return this.zoomTool.configMouseZoomWheel();
710
741
  }
711
742
  configMouseSliceWheel() {
712
743
  /**
@@ -716,348 +747,60 @@ export class DrawToolCore extends CommToolsData {
716
747
  throw new Error("Child class must implement abstract redrawDisplayCanvas, currently you can find it in NrrdTools.");
717
748
  }
718
749
  enableCrosshair() {
719
- this.nrrd_states.isCursorSelect = true;
720
- switch (this.protectedData.axis) {
721
- case "x":
722
- this.cursorPage.x.updated = true;
723
- this.cursorPage.y.updated = false;
724
- this.cursorPage.z.updated = false;
725
- break;
726
- case "y":
727
- this.cursorPage.x.updated = false;
728
- this.cursorPage.y.updated = true;
729
- this.cursorPage.z.updated = false;
730
- break;
731
- case "z":
732
- this.cursorPage.x.updated = false;
733
- this.cursorPage.y.updated = false;
734
- this.cursorPage.z.updated = true;
735
- break;
736
- }
750
+ this.crosshairTool.enableCrosshair();
737
751
  }
738
752
  drawImageOnEmptyImage(canvas) {
739
- this.protectedData.ctxes.emptyCtx.drawImage(canvas, 0, 0, this.protectedData.canvases.emptyCanvas.width, this.protectedData.canvases.emptyCanvas.height);
740
- }
741
- /****************************Sphere calculate distance functions****************************************************/
742
- getSpherePosition(position, axis) {
743
- const mouseX = position[axis][0];
744
- const mouseY = position[axis][1];
745
- const sliceIndex = position[axis][2];
746
- return { x: mouseX, y: mouseY, z: sliceIndex };
747
- }
753
+ const ctx = this.protectedData.ctxes.emptyCtx;
754
+ const w = this.protectedData.canvases.emptyCanvas.width;
755
+ const h = this.protectedData.canvases.emptyCanvas.height;
756
+ // No flip needed: the layer canvas screen coordinates already match the
757
+ // Three.js source coordinate system used by MaskVolume. Applying a flip
758
+ // here would invert cross-axis slice indices (e.g. coronal slice 220
759
+ // becoming 228 when total=448).
760
+ ctx.imageSmoothingEnabled = false;
761
+ ctx.drawImage(canvas, 0, 0, w, h);
762
+ }
763
+ /****************************Sphere functions (delegated to SphereTool)****************************************************/
748
764
  drawCalculatorSphereOnEachViews(axis) {
749
- // init sphere canvas width and height
750
- this.setSphereCanvasSize(axis);
751
- // get drawingSphere canvas for storing
752
- const ctx = this.protectedData.ctxes.drawingSphereCtx;
753
- const canvas = this.protectedData.canvases.drawingSphereCanvas;
754
- let tumourPosition = !!this.nrrd_states.tumourSphereOrigin ? Object.assign(this.getSpherePosition(this.nrrd_states.tumourSphereOrigin, axis), { color: this.nrrd_states.tumourColor }) : null;
755
- let skinPosition = !!this.nrrd_states.skinSphereOrigin ? Object.assign(this.getSpherePosition(this.nrrd_states.skinSphereOrigin, axis), { color: this.nrrd_states.skinColor }) : null;
756
- let ribcagePosition = !!this.nrrd_states.ribSphereOrigin ? Object.assign(this.getSpherePosition(this.nrrd_states.ribSphereOrigin, axis), { color: this.nrrd_states.ribcageColor }) : null;
757
- let nipplePosition = !!this.nrrd_states.nippleSphereOrigin ? Object.assign(this.getSpherePosition(this.nrrd_states.nippleSphereOrigin, axis), { color: this.nrrd_states.nippleColor }) : null;
758
- let positionGroup = [];
759
- if (!!tumourPosition)
760
- positionGroup.push(tumourPosition);
761
- if (!!skinPosition)
762
- positionGroup.push(skinPosition);
763
- if (!!ribcagePosition)
764
- positionGroup.push(ribcagePosition);
765
- if (!!nipplePosition)
766
- positionGroup.push(nipplePosition);
767
- let copyPosition = JSON.parse(JSON.stringify(positionGroup));
768
- let rePositionGroup = [];
769
- // group same slice points
770
- positionGroup.forEach((p) => {
771
- let temp = [];
772
- let sameIndex = [];
773
- for (let i = 0; i < copyPosition.length; i++) {
774
- if (p.z == copyPosition[i].z) {
775
- temp.push(copyPosition[i]);
776
- sameIndex.push(i);
777
- }
778
- }
779
- sameIndex.reverse();
780
- sameIndex.forEach(i => copyPosition.splice(i, 1));
781
- if (temp.length > 0)
782
- rePositionGroup.push(temp);
783
- if (copyPosition.length == 0) {
784
- return;
785
- }
786
- });
787
- rePositionGroup.forEach((group) => {
788
- group.forEach(p => {
789
- this.drawSphereCore(ctx, p.x, p.y, this.nrrd_states.sphereRadius, p.color);
790
- });
791
- this.storeSphereImages(group[0].z, axis);
792
- ctx.clearRect(0, 0, canvas.width, canvas.height);
793
- });
794
- }
795
- /****************************Sphere functions****************************************************/
796
- // for sphere
797
- storeSphereImages(index, axis) {
798
- this.setEmptyCanvasSize(axis);
799
- this.drawImageOnEmptyImage(this.protectedData.canvases.drawingSphereCanvas);
800
- let imageData = this.protectedData.ctxes.emptyCtx.getImageData(0, 0, this.protectedData.canvases.emptyCanvas.width, this.protectedData.canvases.emptyCanvas.height);
801
- this.storeImageToAxis(index, this.protectedData.maskData.paintImages, imageData, axis);
765
+ this.sphereTool.drawCalculatorSphereOnEachViews(axis);
802
766
  }
803
767
  drawSphereOnEachViews(decay, axis) {
804
- // init sphere canvas width and height
805
- this.setSphereCanvasSize(axis);
806
- const mouseX = this.nrrd_states.sphereOrigin[axis][0];
807
- const mouseY = this.nrrd_states.sphereOrigin[axis][1];
808
- const originIndex = this.nrrd_states.sphereOrigin[axis][2];
809
- const preIndex = originIndex - decay;
810
- const nextIndex = originIndex + decay;
811
- const ctx = this.protectedData.ctxes.drawingSphereCtx;
812
- const canvas = this.protectedData.canvases.drawingSphereCanvas;
813
- if (preIndex === nextIndex) {
814
- this.drawSphereCore(ctx, mouseX, mouseY, this.nrrd_states.sphereRadius, this.gui_states.fillColor);
815
- this.storeSphereImages(preIndex, axis);
816
- }
817
- else {
818
- this.drawSphereCore(ctx, mouseX, mouseY, (this.nrrd_states.sphereRadius - decay), this.gui_states.fillColor);
819
- this.drawImageOnEmptyImage(canvas);
820
- this.storeSphereImages(preIndex, axis);
821
- this.storeSphereImages(nextIndex, axis);
822
- }
823
- ctx.clearRect(0, 0, canvas.width, canvas.height);
824
- }
825
- /**
826
- *
827
- * @param ctx draw sphere canvas ctx
828
- * @param x width must be match the origin size, size factor 1, ignore the size factor
829
- * @param y height must be match the origin size, size factor 1, ignore the size factor
830
- * @param radius radius must be match the origin size, size factor 1, ignore the size factor
831
- * @param color sphere color
832
- */
833
- drawSphereCore(ctx, x, y, radius, color) {
834
- ctx.beginPath();
835
- ctx.arc(x, y, radius, 0, 2 * Math.PI);
836
- ctx.fillStyle = color;
837
- ctx.fill();
838
- ctx.closePath();
768
+ this.sphereTool.drawSphereOnEachViews(decay, axis);
839
769
  }
840
- setSphereCanvasSize(axis) {
841
- switch (!!axis ? axis : this.protectedData.axis) {
842
- case "x":
843
- this.protectedData.canvases.drawingSphereCanvas.width =
844
- this.nrrd_states.nrrd_z_mm;
845
- this.protectedData.canvases.drawingSphereCanvas.height =
846
- this.nrrd_states.nrrd_y_mm;
847
- break;
848
- case "y":
849
- this.protectedData.canvases.drawingSphereCanvas.width =
850
- this.nrrd_states.nrrd_x_mm;
851
- this.protectedData.canvases.drawingSphereCanvas.height =
852
- this.nrrd_states.nrrd_z_mm;
853
- break;
854
- case "z":
855
- this.protectedData.canvases.drawingSphereCanvas.width =
856
- this.nrrd_states.nrrd_x_mm;
857
- this.protectedData.canvases.drawingSphereCanvas.height =
858
- this.nrrd_states.nrrd_y_mm;
859
- break;
860
- }
861
- }
862
- // drawing canvas mouse shpere wheel
863
770
  configMouseSphereWheel() {
864
- const sphereEvent = (e) => {
865
- e.preventDefault();
866
- if (e.deltaY < 0) {
867
- this.nrrd_states.sphereRadius += 1;
868
- }
869
- else {
870
- this.nrrd_states.sphereRadius -= 1;
871
- }
872
- // limited the radius max and min
873
- this.nrrd_states.sphereRadius = Math.max(1, Math.min(this.nrrd_states.sphereRadius, 50));
874
- // get mouse position
875
- const mouseX = this.nrrd_states.sphereOrigin[this.protectedData.axis][0];
876
- const mouseY = this.nrrd_states.sphereOrigin[this.protectedData.axis][1];
877
- this.drawSphere(mouseX, mouseY, this.nrrd_states.sphereRadius);
878
- };
879
- return sphereEvent;
771
+ return this.sphereTool.configMouseSphereWheel();
880
772
  }
881
773
  drawCalculatorSphere(radius) {
882
- // clear canvas
883
- const [canvas, ctx] = this.clearSphereCanvas();
884
- if (!!this.nrrd_states.tumourSphereOrigin && this.nrrd_states.tumourSphereOrigin[this.protectedData.axis][2] === this.nrrd_states.currentIndex) {
885
- this.drawSphereCore(ctx, this.nrrd_states.tumourSphereOrigin[this.protectedData.axis][0], this.nrrd_states.tumourSphereOrigin[this.protectedData.axis][1], radius, this.nrrd_states.tumourColor);
886
- }
887
- if (!!this.nrrd_states.skinSphereOrigin && this.nrrd_states.skinSphereOrigin[this.protectedData.axis][2] === this.nrrd_states.currentIndex) {
888
- this.drawSphereCore(ctx, this.nrrd_states.skinSphereOrigin[this.protectedData.axis][0], this.nrrd_states.skinSphereOrigin[this.protectedData.axis][1], radius, this.nrrd_states.skinColor);
889
- }
890
- if (!!this.nrrd_states.ribSphereOrigin && this.nrrd_states.ribSphereOrigin[this.protectedData.axis][2] === this.nrrd_states.currentIndex) {
891
- this.drawSphereCore(ctx, this.nrrd_states.ribSphereOrigin[this.protectedData.axis][0], this.nrrd_states.ribSphereOrigin[this.protectedData.axis][1], radius, this.nrrd_states.ribcageColor);
892
- }
893
- if (!!this.nrrd_states.nippleSphereOrigin && this.nrrd_states.nippleSphereOrigin[this.protectedData.axis][2] === this.nrrd_states.currentIndex) {
894
- this.drawSphereCore(ctx, this.nrrd_states.nippleSphereOrigin[this.protectedData.axis][0], this.nrrd_states.nippleSphereOrigin[this.protectedData.axis][1], radius, this.nrrd_states.nippleColor);
895
- }
896
- this.protectedData.ctxes.drawingLayerMasterCtx.drawImage(canvas, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
774
+ this.sphereTool.drawCalculatorSphere(radius);
897
775
  }
898
776
  drawSphere(mouseX, mouseY, radius) {
899
- console.log(radius);
900
- // clear canvas
901
- const [canvas, ctx] = this.clearSphereCanvas();
902
- this.drawSphereCore(ctx, mouseX, mouseY, radius, this.gui_states.fillColor);
903
- console.log("when drawing shpere on canvas");
904
- console.log("Line 1256 mastercanvasesize:", this.protectedData.canvases.drawingCanvasLayerMaster.width, this.protectedData.canvases.drawingCanvasLayerMaster.height);
905
- console.log("current xy:", mouseX, mouseY);
906
- console.log("current changed width:", this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
907
- this.protectedData.ctxes.drawingLayerMasterCtx.drawImage(canvas, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
908
- }
909
- clearSphereCanvas() {
910
- // this.protectedData.canvases.drawingSphereCanvas.width =
911
- // this.protectedData.canvases.drawingCanvasLayerMaster.width;
912
- // this.protectedData.canvases.drawingSphereCanvas.height =
913
- // this.protectedData.canvases.drawingCanvasLayerMaster.height;
914
- // clear drawingCanvasLayerMaster
915
- this.protectedData.canvases.drawingCanvasLayerMaster.width = this.protectedData.canvases.drawingCanvasLayerMaster.width;
916
- // resize sphere canvas size to original size
917
- this.protectedData.canvases.drawingSphereCanvas.width = this.protectedData.canvases.originCanvas.width;
918
- this.protectedData.canvases.drawingSphereCanvas.height = this.protectedData.canvases.originCanvas.height;
919
- const canvas = this.protectedData.canvases.drawingSphereCanvas;
920
- const ctx = this.protectedData.ctxes.drawingSphereCtx;
921
- return [canvas, ctx];
777
+ this.sphereTool.drawSphere(mouseX, mouseY, radius);
922
778
  }
923
779
  /**
924
- * We generate the MRI slice from threejs based on mm, but when we display it is based on pixel size/distance.
925
- * So, the index munber on each axis (sagittal, axial, coronal) is the slice's depth in mm distance. And the width and height displayed on screen is the slice's width and height in pixel distance.
926
- *
927
- * When we switch into different axis' views, we need to convert current view's the depth to the pixel distance in other views width or height, and convert the current view's width or height from pixel distance to mm distance as other views' depth (slice index) in general.
928
- *
929
- * Then as for the crosshair (Cursor Inspector), we also need to convert the cursor point (x, y, z) to other views' (x, y, z).
930
- *
931
- * @param from "x" | "y" | "z", current view axis, "x: sagittle, y: coronal, z: axial".
932
- * @param to "x" | "y" | "z", target view axis (where you want jump to), "x: sagittle, y: coronal, z: axial".
933
- * @param cursorNumX number, cursor point x on current axis's slice. (pixel distance)
934
- * @param cursorNumY number, cursor point y on current axis's slice. (pixel distance)
935
- * @param currentSliceIndex number, current axis's slice's index/depth. (mm distance)
936
- * @returns
937
- */
780
+ * Convert cursor point between axis views.
781
+ * Delegated to CrosshairTool.
782
+ */
938
783
  convertCursorPoint(from, to, cursorNumX, cursorNumY, currentSliceIndex) {
939
- const nrrd = this.nrrd_states;
940
- const dimensions = nrrd.dimensions;
941
- const ratios = nrrd.ratios;
942
- const { nrrd_x_mm, nrrd_y_mm, nrrd_z_mm } = nrrd;
943
- let currentIndex = 0;
944
- let oldIndex = 0;
945
- let convertCursorNumX = 0;
946
- let convertCursorNumY = 0;
947
- const convertIndex = {
948
- x: {
949
- y: (val) => Math.ceil((val / nrrd_x_mm) * dimensions[0]),
950
- z: (val) => Math.ceil((val / nrrd_z_mm) * dimensions[2]),
951
- },
952
- y: {
953
- x: (val) => Math.ceil((val / nrrd_y_mm) * dimensions[1]),
954
- z: (val) => Math.ceil((1 - val / nrrd_z_mm) * dimensions[2]),
955
- },
956
- z: {
957
- x: (val) => Math.ceil((val / nrrd_x_mm) * dimensions[0]),
958
- y: (val) => Math.ceil((val / nrrd_y_mm) * dimensions[1]),
959
- },
960
- };
961
- const convertCursor = {
962
- x: {
963
- y: (sliceIndex) => Math.ceil((sliceIndex / dimensions[0]) * nrrd_x_mm),
964
- z: (sliceIndex) => Math.ceil((sliceIndex / dimensions[0]) * nrrd_x_mm),
965
- },
966
- y: {
967
- x: (sliceIndex) => Math.ceil((sliceIndex / dimensions[1]) * nrrd_y_mm),
968
- z: (sliceIndex) => Math.ceil((sliceIndex / dimensions[1]) * nrrd_y_mm),
969
- },
970
- z: {
971
- x: (sliceIndex) => Math.ceil((sliceIndex / dimensions[2]) * nrrd_z_mm),
972
- y: (sliceIndex) => Math.ceil((1 - sliceIndex / dimensions[2]) * nrrd_z_mm),
973
- },
974
- };
975
- if (from === to) {
976
- return;
977
- }
978
- if (from === "z" && to === "x") {
979
- currentIndex = convertIndex[from][to](cursorNumX);
980
- oldIndex = currentIndex * ratios[to];
981
- convertCursorNumX = convertCursor[from][to](currentSliceIndex);
982
- convertCursorNumY = cursorNumY;
983
- }
984
- else if (from === "y" && to === "x") {
985
- currentIndex = convertIndex[from][to](cursorNumX);
986
- oldIndex = currentIndex * ratios.x;
987
- convertCursorNumY = convertCursor[from][to](currentSliceIndex);
988
- convertCursorNumX = dimensions[2] * ratios.z - cursorNumY;
989
- }
990
- else if (from === "z" && to === "y") {
991
- currentIndex = convertIndex[from][to](cursorNumY);
992
- oldIndex = currentIndex * ratios[to];
993
- convertCursorNumY = convertCursor[from][to](currentSliceIndex);
994
- convertCursorNumX = cursorNumX;
995
- }
996
- else if (from === "x" && to === "y") {
997
- currentIndex = convertIndex[from][to](cursorNumY);
998
- oldIndex = currentIndex * ratios[to];
999
- convertCursorNumX = convertCursor[from][to](currentSliceIndex);
1000
- convertCursorNumY = dimensions[2] * ratios.z - cursorNumX;
1001
- }
1002
- else if (from === "x" && to === "z") {
1003
- currentIndex = convertIndex[from][to](cursorNumX);
1004
- oldIndex = currentIndex * ratios[to];
1005
- convertCursorNumX = convertCursor[from][to](currentSliceIndex);
1006
- convertCursorNumY = cursorNumY;
1007
- }
1008
- else if (from === "y" && to === "z") {
1009
- currentIndex = convertIndex[from][to](cursorNumY);
1010
- oldIndex = currentIndex * ratios.z;
1011
- convertCursorNumY = convertCursor[from][to](currentSliceIndex);
1012
- convertCursorNumX = cursorNumX;
1013
- }
1014
- else {
1015
- return;
1016
- }
1017
- return { currentIndex, oldIndex, convertCursorNumX, convertCursorNumY };
784
+ return this.crosshairTool.convertCursorPoint(from, to, cursorNumX, cursorNumY, currentSliceIndex);
1018
785
  }
1019
786
  setUpSphereOrigins(mouseX, mouseY, sliceIndex) {
1020
- const convertCursor = (from, to) => {
1021
- const convertObj = this.convertCursorPoint(from, to, mouseX, mouseY, sliceIndex);
1022
- return {
1023
- convertCursorNumX: convertObj === null || convertObj === void 0 ? void 0 : convertObj.convertCursorNumX,
1024
- convertCursorNumY: convertObj === null || convertObj === void 0 ? void 0 : convertObj.convertCursorNumY,
1025
- currentIndex: convertObj === null || convertObj === void 0 ? void 0 : convertObj.currentIndex,
1026
- };
1027
- };
1028
- const axisConversions = {
1029
- x: { axisTo1: "y", axisTo2: "z" },
1030
- y: { axisTo1: "z", axisTo2: "x" },
1031
- z: { axisTo1: "x", axisTo2: "y" },
1032
- };
1033
- const { axisTo1, axisTo2 } = axisConversions[this.protectedData.axis];
1034
- this.nrrd_states.sphereOrigin[axisTo1] = [
1035
- convertCursor(this.protectedData.axis, axisTo1).convertCursorNumX,
1036
- convertCursor(this.protectedData.axis, axisTo1).convertCursorNumY,
1037
- convertCursor(this.protectedData.axis, axisTo1).currentIndex,
1038
- ];
1039
- this.nrrd_states.sphereOrigin[axisTo2] = [
1040
- convertCursor(this.protectedData.axis, axisTo2).convertCursorNumX,
1041
- convertCursor(this.protectedData.axis, axisTo2).convertCursorNumY,
1042
- convertCursor(this.protectedData.axis, axisTo2).currentIndex,
1043
- ];
1044
- }
1045
- /****************************label div controls****************************************************/
1046
- getRestLabel() {
1047
- const labels = this.nrrd_states.labels;
1048
- const restLabel = labels.filter((item) => {
1049
- return item !== this.gui_states.label;
1050
- });
1051
- return restLabel;
787
+ this.crosshairTool.setUpSphereOrigins(mouseX, mouseY, sliceIndex);
1052
788
  }
1053
- /**************************** Undo clear functions****************************************************/
1054
- getCurrentUndo() {
1055
- return this.undoArray.filter((item) => {
1056
- return item.sliceIndex === this.nrrd_states.currentIndex;
789
+ /****************************layer div controls****************************************************/
790
+ getRestLayer() {
791
+ const layers = this.nrrd_states.layers;
792
+ const restLayer = layers.filter((item) => {
793
+ return item !== this.gui_states.layer;
1057
794
  });
795
+ return restLayer;
1058
796
  }
797
+ /**************************** Undo/Redo functions (Phase 6 — Delta-based) ****************************/
1059
798
  /**
1060
- * Clear mask on current slice canvas
799
+ * Clear mask on current slice canvas.
800
+ *
801
+ * Phase 2: Clears the MaskVolume slice for all three layers,
802
+ * re-stores, and notifies external via getMask callback with clearFlag=true.
803
+ * Phase 6: Also records a MaskDelta for undo support.
1061
804
  */
1062
805
  clearPaint() {
1063
806
  this.protectedData.Is_Draw = true;
@@ -1067,391 +810,138 @@ export class DrawToolCore extends CommToolsData {
1067
810
  this.protectedData.mainPreSlices.repaint.call(this.protectedData.mainPreSlices);
1068
811
  this.protectedData.previousDrawingImage =
1069
812
  this.protectedData.ctxes.emptyCtx.createImageData(1, 1);
1070
- this.storeAllImages(this.nrrd_states.currentIndex, this.gui_states.label);
1071
- const restLabels = this.getRestLabel();
1072
- this.storeEachLayerImage(this.nrrd_states.currentIndex, restLabels[0]);
1073
- this.storeEachLayerImage(this.nrrd_states.currentIndex, restLabels[1]);
1074
- this.setIsDrawFalse(1000);
1075
- }
1076
- // need to update
1077
- undoLastPainting() {
1078
- let { ctx, canvas } = this.setCurrentLayer();
1079
- this.protectedData.Is_Draw = true;
1080
- this.protectedData.canvases.drawingCanvasLayerMaster.width =
1081
- this.protectedData.canvases.drawingCanvasLayerMaster.width;
1082
- canvas.width = canvas.width;
1083
- this.protectedData.mainPreSlices.repaint.call(this.protectedData.mainPreSlices);
1084
- const currentUndoObj = this.getCurrentUndo();
1085
- if (currentUndoObj.length > 0) {
1086
- const undo = currentUndoObj[0];
1087
- const layerUndos = undo.layers[this.gui_states.label];
1088
- const layerLen = layerUndos.length;
1089
- // if (layerLen === 0) return;
1090
- layerUndos.pop();
1091
- if (layerLen > 0) {
1092
- // const imageSrc = undo.undos[undo.undos.length - 1];
1093
- const image = layerUndos[layerLen - 1];
1094
- if (!!image) {
1095
- ctx.drawImage(image, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
1096
- }
1097
- }
1098
- if (undo.layers.label1.length > 0) {
1099
- const image = undo.layers.label1[undo.layers.label1.length - 1];
1100
- this.protectedData.ctxes.drawingLayerMasterCtx.drawImage(image, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
1101
- }
1102
- if (undo.layers.label2.length > 0) {
1103
- const image = undo.layers.label2[undo.layers.label2.length - 1];
1104
- this.protectedData.ctxes.drawingLayerMasterCtx.drawImage(image, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
1105
- }
1106
- if (undo.layers.label3.length > 0) {
1107
- const image = undo.layers.label3[undo.layers.label3.length - 1];
1108
- this.protectedData.ctxes.drawingLayerMasterCtx.drawImage(image, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
813
+ // Phase 2 + 6: Clear volume slices and record undo delta
814
+ try {
815
+ const axis = this.protectedData.axis;
816
+ const idx = this.nrrd_states.currentIndex;
817
+ const activeLayer = this.gui_states.layer;
818
+ const vol = this.getVolumeForLayer(activeLayer);
819
+ // Capture old slice for undo before clearing
820
+ const oldSlice = vol.getSliceUint8(idx, axis).data.slice();
821
+ // Clear only the active layer (clear also clears all for canvas consistency)
822
+ const { layer1, layer2, layer3 } = this.protectedData.maskData.volumes;
823
+ layer1.clearSlice(idx, axis);
824
+ layer2.clearSlice(idx, axis);
825
+ layer3.clearSlice(idx, axis);
826
+ // New (all-zero) slice for undo newSlice
827
+ const { data: newSlice, width, height } = vol.getSliceUint8(idx, axis);
828
+ // Push clearPaint delta to UndoManager (supports undo)
829
+ this.undoManager.push({
830
+ layerId: activeLayer,
831
+ axis,
832
+ sliceIndex: idx,
833
+ oldSlice,
834
+ newSlice: newSlice.slice(),
835
+ });
836
+ // Notify external that slice was cleared
837
+ if (!this.nrrd_states.loadMaskJson && !this.gui_states.sphere && !this.gui_states.calculator) {
838
+ const activeChannel = this.gui_states.activeChannel || 1;
839
+ this.nrrd_states.getMask(newSlice, activeLayer, activeChannel, idx, axis, width, height, true // clearFlag = true
840
+ );
1109
841
  }
1110
- this.protectedData.previousDrawingImage =
1111
- this.protectedData.ctxes.drawingLayerMasterCtx.getImageData(0, 0, this.protectedData.canvases.drawingCanvasLayerMaster.width, this.protectedData.canvases.drawingCanvasLayerMaster.height);
1112
- this.storeAllImages(this.nrrd_states.currentIndex, this.gui_states.label);
1113
- this.setIsDrawFalse(1000);
1114
842
  }
1115
- }
1116
- /****************************Store images****************************************************/
1117
- storeImageToAxis(index, paintedImages, imageData, axis) {
1118
- var _a, _b, _c;
1119
- let temp = {
1120
- index,
1121
- image: imageData,
1122
- };
1123
- let drawedImage;
1124
- switch (!!axis ? axis : this.protectedData.axis) {
1125
- case "x":
1126
- drawedImage = this.filterDrawedImage("x", index, paintedImages);
1127
- drawedImage
1128
- ? (drawedImage.image = imageData)
1129
- : (_a = paintedImages.x) === null || _a === void 0 ? void 0 : _a.push(temp);
1130
- break;
1131
- case "y":
1132
- drawedImage = this.filterDrawedImage("y", index, paintedImages);
1133
- drawedImage
1134
- ? (drawedImage.image = imageData)
1135
- : (_b = paintedImages.y) === null || _b === void 0 ? void 0 : _b.push(temp);
1136
- break;
1137
- case "z":
1138
- drawedImage = this.filterDrawedImage("z", index, paintedImages);
1139
- drawedImage
1140
- ? (drawedImage.image = imageData)
1141
- : (_c = paintedImages.z) === null || _c === void 0 ? void 0 : _c.push(temp);
1142
- break;
843
+ catch (_a) {
844
+ // Volume not ready (1×1×1 placeholder) — continue with legacy path
1143
845
  }
846
+ this.storeAllImages(this.nrrd_states.currentIndex, this.gui_states.layer);
847
+ const restLayers = this.getRestLayer();
848
+ this.storeEachLayerImage(this.nrrd_states.currentIndex, restLayers[0]);
849
+ this.storeEachLayerImage(this.nrrd_states.currentIndex, restLayers[1]);
850
+ this.setIsDrawFalse(1000);
1144
851
  }
1145
- storeAllImages(index, label) {
1146
- // const image: HTMLImageElement = new Image();
1147
- // resize the drawing image data
1148
- if (!this.nrrd_states.loadMaskJson && !this.gui_states.sphere && !this.gui_states.calculator) {
1149
- this.setEmptyCanvasSize();
1150
- this.drawImageOnEmptyImage(this.protectedData.canvases.drawingCanvasLayerMaster);
852
+ /**
853
+ * Undo the last drawing operation on the active layer.
854
+ * Restores the MaskVolume slice to its pre-draw state, re-renders
855
+ * the canvas, and notifies the backend.
856
+ */
857
+ undoLastPainting() {
858
+ const delta = this.undoManager.undo();
859
+ if (!delta)
860
+ return;
861
+ try {
862
+ const vol = this.getVolumeForLayer(delta.layerId);
863
+ vol.setSliceUint8(delta.sliceIndex, delta.oldSlice, delta.axis);
1151
864
  }
1152
- let imageData = this.protectedData.ctxes.emptyCtx.getImageData(0, 0, this.protectedData.canvases.emptyCanvas.width, this.protectedData.canvases.emptyCanvas.height);
1153
- // 1.12.23
1154
- switch (this.protectedData.axis) {
1155
- case "x":
1156
- const maskData_x = this.checkSharedPlaceSlice(this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_y_pixel, imageData);
1157
- const marked_a_x = this.sliceArrayV(maskData_x, this.nrrd_states.nrrd_y_pixel, this.nrrd_states.nrrd_z_pixel);
1158
- const marked_b_x = this.sliceArrayH(maskData_x, this.nrrd_states.nrrd_y_pixel, this.nrrd_states.nrrd_z_pixel);
1159
- // const ratio_a_x =
1160
- // this.nrrd_states.nrrd_z / this.nrrd_states.dimensions[2];
1161
- // const ratio_b_x =
1162
- // this.nrrd_states.nrrd_y / this.nrrd_states.dimensions[1];
1163
- const convertXIndex = index;
1164
- // from x the target z will replace the col pixel
1165
- this.replaceVerticalColPixels(this.protectedData.maskData.paintImages.z, this.nrrd_states.dimensions[2],
1166
- // this.nrrd_states.ratios.z,
1167
- 1, marked_a_x, this.nrrd_states.nrrd_x_pixel, convertXIndex);
1168
- // from x the target y will replace the col pixel
1169
- this.replaceVerticalColPixels(this.protectedData.maskData.paintImages.y, this.nrrd_states.dimensions[1],
1170
- // this.nrrd_states.ratios.y,
1171
- 1, marked_b_x, this.nrrd_states.nrrd_x_pixel, convertXIndex);
1172
- break;
1173
- case "y":
1174
- const maskData_y = this.checkSharedPlaceSlice(this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_y_pixel, imageData);
1175
- const marked_a_y = this.sliceArrayV(maskData_y, this.nrrd_states.nrrd_z_pixel, this.nrrd_states.nrrd_x_pixel);
1176
- const marked_b_y = this.sliceArrayH(maskData_y, this.nrrd_states.nrrd_z_pixel, this.nrrd_states.nrrd_x_pixel);
1177
- // const ratio_a_y =
1178
- // this.nrrd_states.nrrd_x / this.nrrd_states.dimensions[0];
1179
- // const ratio_b_y =
1180
- // this.nrrd_states.nrrd_z / this.nrrd_states.dimensions[2];
1181
- const convertYIndex = index;
1182
- this.replaceHorizontalRowPixels(this.protectedData.maskData.paintImages.x, this.nrrd_states.dimensions[0],
1183
- // this.nrrd_states.ratios.x,
1184
- 1, marked_a_y, this.nrrd_states.nrrd_z_pixel, convertYIndex);
1185
- this.replaceHorizontalRowPixels(this.protectedData.maskData.paintImages.z, this.nrrd_states.dimensions[2],
1186
- // this.nrrd_states.ratios.z,
1187
- 1, marked_b_y, this.nrrd_states.nrrd_x_pixel, convertYIndex);
1188
- break;
1189
- case "z":
1190
- // for x slices get cols' pixels
1191
- // for y slices get rows' pixels
1192
- // 1. slice z 的 y轴对应了slice y的index,所以我们可以通过slice z 确定在y轴上那些行是有pixels的,我们就可以将它的y坐标(或者是行号)对应到slice y的index,并将该index下的marked image提取出来。
1193
- // 2. 接着我们可以通过当前slice z 的index,来确定marked image 需要替换或重组的 行 pixel array。
1194
- const maskData_z = this.checkSharedPlaceSlice(this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_y_pixel, imageData);
1195
- // 1. get slice z's each row's and col's pixel as a 2d array.
1196
- // 1.1 get the cols' 2d array for slice x
1197
- const marked_a_z = this.sliceArrayV(maskData_z, this.nrrd_states.nrrd_y_pixel, this.nrrd_states.nrrd_x_pixel);
1198
- // 1.2 get the rows' 2d array for slice y
1199
- const marked_b_z = this.sliceArrayH(maskData_z, this.nrrd_states.nrrd_y_pixel, this.nrrd_states.nrrd_x_pixel);
1200
- // 1.3 get x axis ratio for converting, to match the number slice x with the slice z's x axis pixel number.
1201
- // const ratio_a_z =
1202
- // this.nrrd_states.nrrd_x / this.nrrd_states.dimensions[0];
1203
- // // 1.4 get y axis ratio for converting
1204
- // const ratio_b_z =
1205
- // this.nrrd_states.nrrd_y / this.nrrd_states.dimensions[1];
1206
- // 1.5 To identify which row/col data should be replace
1207
- const convertZIndex = index;
1208
- // 2. Mapping coordinates
1209
- // from z the target x will replace the col pixel
1210
- this.replaceVerticalColPixels(this.protectedData.maskData.paintImages.x, this.nrrd_states.dimensions[0],
1211
- // this.nrrd_states.ratios.x,
1212
- 1, marked_a_z, this.nrrd_states.nrrd_z_pixel, convertZIndex);
1213
- // from z the target y will replace row pixel
1214
- this.replaceHorizontalRowPixels(this.protectedData.maskData.paintImages.y, this.nrrd_states.dimensions[1],
1215
- // this.nrrd_states.ratios.y,
1216
- 1, marked_b_z, this.nrrd_states.nrrd_x_pixel, convertZIndex);
1217
- break;
865
+ catch (_a) {
866
+ return; // Volume not ready
1218
867
  }
1219
- this.storeImageToAxis(index, this.protectedData.maskData.paintImages, imageData);
1220
- if (!this.nrrd_states.loadMaskJson && !this.gui_states.sphere && !this.gui_states.calculator) {
1221
- this.storeEachLayerImage(index, label);
868
+ this.protectedData.Is_Draw = true;
869
+ if (delta.axis === this.protectedData.axis && delta.sliceIndex === this.nrrd_states.currentIndex) {
870
+ this.applyUndoRedoToCanvas(delta.layerId);
1222
871
  }
1223
- }
1224
- storeImageToLabel(index, canvas, paintedImages) {
1225
872
  if (!this.nrrd_states.loadMaskJson) {
1226
- this.setEmptyCanvasSize();
1227
- this.drawImageOnEmptyImage(canvas);
873
+ const { data: sliceData, width, height } = this.getVolumeForLayer(delta.layerId)
874
+ .getSliceUint8(delta.sliceIndex, delta.axis);
875
+ this.nrrd_states.getMask(sliceData, delta.layerId, this.gui_states.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
1228
876
  }
1229
- const imageData = this.protectedData.ctxes.emptyCtx.getImageData(0, 0, this.protectedData.canvases.emptyCanvas.width, this.protectedData.canvases.emptyCanvas.height);
1230
- this.storeImageToAxis(index, paintedImages, imageData);
1231
- // this.setEmptyCanvasSize()
1232
- return imageData;
877
+ this.setIsDrawFalse(1000);
1233
878
  }
1234
- storeEachLayerImage(index, label) {
1235
- if (!this.nrrd_states.loadMaskJson) {
1236
- this.setEmptyCanvasSize();
1237
- }
1238
- let imageData;
1239
- switch (label) {
1240
- case "label1":
1241
- imageData = this.storeImageToLabel(index, this.protectedData.canvases.drawingCanvasLayerOne, this.protectedData.maskData.paintImagesLabel1);
1242
- break;
1243
- case "label2":
1244
- imageData = this.storeImageToLabel(index, this.protectedData.canvases.drawingCanvasLayerTwo, this.protectedData.maskData.paintImagesLabel2);
1245
- break;
1246
- case "label3":
1247
- imageData = this.storeImageToLabel(index, this.protectedData.canvases.drawingCanvasLayerThree, this.protectedData.maskData.paintImagesLabel3);
1248
- break;
879
+ /**
880
+ * Redo the last undone operation on the active layer.
881
+ * Reapplies the MaskVolume slice to its post-draw state, re-renders
882
+ * the canvas, and notifies the backend.
883
+ */
884
+ redoLastPainting() {
885
+ const delta = this.undoManager.redo();
886
+ if (!delta)
887
+ return;
888
+ try {
889
+ const vol = this.getVolumeForLayer(delta.layerId);
890
+ vol.setSliceUint8(delta.sliceIndex, delta.newSlice, delta.axis);
1249
891
  }
1250
- // callback function to return the painted image
1251
- if (!this.nrrd_states.loadMaskJson && this.protectedData.axis == "z") {
1252
- this.nrrd_states.getMask(imageData, this.nrrd_states.currentIndex, label, this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_y_pixel, this.nrrd_states.clearAllFlag);
892
+ catch (_a) {
893
+ return; // Volume not ready
1253
894
  }
1254
- }
1255
- // slice array to 2d array
1256
- sliceArrayH(arr, row, col) {
1257
- const arr2D = [];
1258
- for (let i = 0; i < row; i++) {
1259
- const start = i * col * 4;
1260
- const end = (i + 1) * col * 4;
1261
- const temp = arr.slice(start, end);
1262
- arr2D.push(temp);
895
+ this.protectedData.Is_Draw = true;
896
+ if (delta.axis === this.protectedData.axis && delta.sliceIndex === this.nrrd_states.currentIndex) {
897
+ this.applyUndoRedoToCanvas(delta.layerId);
1263
898
  }
1264
- return arr2D;
1265
- }
1266
- sliceArrayV(arr, row, col) {
1267
- const arr2D = [];
1268
- const base = col * 4;
1269
- for (let i = 0; i < col; i++) {
1270
- const temp = [];
1271
- for (let j = 0; j < row; j++) {
1272
- const index = base * j + i * 4;
1273
- temp.push(arr[index]);
1274
- temp.push(arr[index + 1]);
1275
- temp.push(arr[index + 2]);
1276
- temp.push(arr[index + 3]);
1277
- }
1278
- arr2D.push(temp);
899
+ if (!this.nrrd_states.loadMaskJson) {
900
+ const { data: sliceData, width, height } = this.getVolumeForLayer(delta.layerId)
901
+ .getSliceUint8(delta.sliceIndex, delta.axis);
902
+ this.nrrd_states.getMask(sliceData, delta.layerId, this.gui_states.activeChannel || 1, delta.sliceIndex, delta.axis, width, height, false);
1279
903
  }
1280
- return arr2D;
904
+ this.setIsDrawFalse(1000);
1281
905
  }
1282
906
  /**
1283
- *
1284
- * @param paintImageArray : the target view slice's marked images array
1285
- * @param length : the target view slice's dimention (total slice index num)
1286
- * @param ratio : the target slice image's width/height ratio of its dimention length
1287
- * @param markedArr : current painted image's vertical 2d Array
1288
- * @param targetWidth : the target image width
1289
- * @param convertIndex : Mapping current image's index to target slice image's width/height pixel start point
907
+ * Re-render a layer canvas from MaskVolume and composite to master.
908
+ * Called after writing oldSlice/newSlice back to the volume during undo/redo.
1290
909
  */
1291
- replaceVerticalColPixels(paintImageArray, length, ratio, markedArr, targetWidth, convertIndex) {
1292
- for (let i = 0, len = length; i < len; i++) {
1293
- const index = Math.floor(i * ratio);
1294
- const convertImageArray = paintImageArray[i].image.data;
1295
- const mark_data = markedArr[index];
1296
- const base_a = targetWidth * 4;
1297
- for (let j = 0, len = mark_data.length; j < len; j += 4) {
1298
- const start = (j / 4) * base_a + convertIndex * 4;
1299
- convertImageArray[start] = mark_data[j];
1300
- convertImageArray[start + 1] = mark_data[j + 1];
1301
- convertImageArray[start + 2] = mark_data[j + 2];
1302
- convertImageArray[start + 3] = mark_data[j + 3];
1303
- }
910
+ applyUndoRedoToCanvas(layerId) {
911
+ let target = this.protectedData.layerTargets.get(layerId);
912
+ if (!target) {
913
+ const firstId = this.nrrd_states.layers[0];
914
+ target = this.protectedData.layerTargets.get(firstId);
915
+ }
916
+ const { ctx, canvas } = target;
917
+ // Clear and re-render the affected layer canvas from MaskVolume
918
+ canvas.width = canvas.width;
919
+ const buffer = this.getOrCreateSliceBuffer(this.protectedData.axis);
920
+ if (buffer) {
921
+ this.renderSliceToCanvas(layerId, this.protectedData.axis, this.nrrd_states.currentIndex, buffer, ctx, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
1304
922
  }
923
+ // Re-composite all layers to master
924
+ this.compositeAllLayers();
925
+ // Update previousDrawingImage from master
926
+ this.protectedData.previousDrawingImage =
927
+ this.protectedData.ctxes.drawingLayerMasterCtx.getImageData(0, 0, this.protectedData.canvases.drawingCanvasLayerMaster.width, this.protectedData.canvases.drawingCanvasLayerMaster.height);
1305
928
  }
1306
- /**
1307
- *
1308
- * @param paintImageArray : the target view slice's marked images array
1309
- * @param length : the target view slice's dimention (total slice index num)
1310
- * @param ratio : the target slice image's width/height ratio of its dimention length
1311
- * @param markedArr : current painted image's horizontal 2d Array
1312
- * @param targetWidth : the target image width
1313
- * @param convertIndex : Mapping current image's index to target slice image's width/height pixel start point
1314
- */
1315
- replaceHorizontalRowPixels(paintImageArray, length, ratio, markedArr, targetWidth, convertIndex) {
1316
- for (let i = 0, len = length; i < len; i++) {
1317
- const index = Math.floor(i * ratio);
1318
- const convertImageArray = paintImageArray[i].image.data;
1319
- const mark_data = markedArr[index];
1320
- const start = targetWidth * convertIndex * 4;
1321
- for (let j = 0, len = mark_data.length; j < len; j++) {
1322
- convertImageArray[start + j] = mark_data[j];
1323
- }
1324
- }
929
+ /****************************Store images (delegated to ImageStoreHelper)****************************************************/
930
+ storeImageToAxis(index, paintedImages, imageData, axis) {
931
+ this.imageStoreHelper.storeImageToAxis(index, paintedImages, imageData, axis);
1325
932
  }
1326
- /****************************** Utils for store image and itksnap core **************************************/
1327
- checkSharedPlaceSlice(width, height, imageData) {
1328
- let maskData = this.protectedData.ctxes.emptyCtx.createImageData(width, height).data;
1329
- if (this.nrrd_states.sharedPlace.z.includes(this.nrrd_states.currentIndex)) {
1330
- const sharedPlaceArr = this.findSliceInSharedPlace();
1331
- sharedPlaceArr.push(imageData);
1332
- if (sharedPlaceArr.length > 0) {
1333
- for (let i = 0; i < sharedPlaceArr.length; i++) {
1334
- this.replaceArray(maskData, sharedPlaceArr[i].data);
1335
- }
1336
- }
1337
- }
1338
- else {
1339
- maskData = imageData.data;
1340
- }
1341
- return maskData;
933
+ storeAllImages(index, layer) {
934
+ this.imageStoreHelper.storeAllImages(index, layer);
1342
935
  }
1343
- // replace Array
1344
- replaceArray(mainArr, replaceArr) {
1345
- for (let i = 0, len = replaceArr.length; i < len; i++) {
1346
- if (replaceArr[i] === 0 || mainArr[i] !== 0) {
1347
- continue;
1348
- }
1349
- else {
1350
- mainArr[i] = replaceArr[i];
1351
- }
1352
- }
936
+ storeImageToLayer(index, canvas, paintedImages) {
937
+ return this.imageStoreHelper.storeImageToLayer(index, canvas, paintedImages);
1353
938
  }
1354
- findSliceInSharedPlace() {
1355
- const sharedPlaceImages = [];
1356
- const base = Math.floor(this.nrrd_states.currentIndex *
1357
- this.nrrd_states.ratios[this.protectedData.axis]);
1358
- for (let i = 1; i <= 3; i++) {
1359
- const index = this.nrrd_states.currentIndex - i;
1360
- if (index < this.nrrd_states.minIndex) {
1361
- break;
1362
- }
1363
- else {
1364
- const newIndex = Math.floor(index * this.nrrd_states.ratios[this.protectedData.axis]);
1365
- if (newIndex === base) {
1366
- sharedPlaceImages.push(this.protectedData.maskData.paintImages[this.protectedData.axis][index].image);
1367
- }
1368
- }
1369
- }
1370
- for (let i = 1; i <= 3; i++) {
1371
- const index = this.nrrd_states.currentIndex + i;
1372
- if (index > this.nrrd_states.maxIndex) {
1373
- break;
1374
- }
1375
- else {
1376
- const newIndex = Math.floor(index * this.nrrd_states.ratios[this.protectedData.axis]);
1377
- if (newIndex === base) {
1378
- sharedPlaceImages.push(this.protectedData.maskData.paintImages[this.protectedData.axis][index].image);
1379
- }
1380
- }
1381
- }
1382
- return sharedPlaceImages;
939
+ storeEachLayerImage(index, layer) {
940
+ this.imageStoreHelper.storeEachLayerImage(index, layer);
1383
941
  }
1384
- /******************************** Utils gui related functions ***************************************/
1385
- /**
1386
- * Set up root container events fns for drag function
1387
- * @param callback
1388
- */
942
+ /******************************** Utils gui related functions (delegated to ContrastTool) ***************************************/
1389
943
  setupConrastEvents(callback) {
1390
- this.contrastEventPrameters.w = this.container.offsetWidth;
1391
- this.contrastEventPrameters.h = this.container.offsetHeight;
1392
- this.contrastEventPrameters.handleOnContrastMouseDown = (ev) => {
1393
- if (ev.button === 0) {
1394
- this.contrastEventPrameters.x = ev.offsetX / this.contrastEventPrameters.x;
1395
- this.contrastEventPrameters.y = ev.offsetY / this.contrastEventPrameters.h;
1396
- this.container.addEventListener("pointermove", this.contrastEventPrameters.handleOnContrastMouseMove);
1397
- }
1398
- };
1399
- this.contrastEventPrameters.handleOnContrastMouseUp = (ev) => {
1400
- this.container.removeEventListener("pointermove", this.contrastEventPrameters.handleOnContrastMouseMove);
1401
- };
1402
- this.contrastEventPrameters.handleOnContrastMouseMove = throttle((ev) => {
1403
- if (this.contrastEventPrameters.y - ev.offsetY / this.contrastEventPrameters.h >= 0) {
1404
- this.contrastEventPrameters.move_y = -Math.ceil((this.contrastEventPrameters.y - ev.offsetY / this.contrastEventPrameters.h) * 10);
1405
- }
1406
- else {
1407
- this.contrastEventPrameters.move_y = -Math.floor((this.contrastEventPrameters.y - ev.offsetY / this.contrastEventPrameters.h) * 10);
1408
- }
1409
- if (this.contrastEventPrameters.move_y !== 0 && Math.abs(this.contrastEventPrameters.move_y) === 1) {
1410
- callback(this.contrastEventPrameters.move_y, "vertical");
1411
- }
1412
- if (this.contrastEventPrameters.x - ev.offsetX / this.contrastEventPrameters.w >= 0) {
1413
- this.contrastEventPrameters.move_x = -Math.ceil((this.contrastEventPrameters.x - ev.offsetX / this.contrastEventPrameters.w) * 10);
1414
- }
1415
- else {
1416
- this.contrastEventPrameters.move_x = -Math.floor((this.contrastEventPrameters.x - ev.offsetX / this.contrastEventPrameters.w) * 10);
1417
- }
1418
- if (this.contrastEventPrameters.move_x !== 0 && Math.abs(this.contrastEventPrameters.move_x) === 1) {
1419
- callback(this.contrastEventPrameters.move_x, "horizental");
1420
- }
1421
- this.contrastEventPrameters.x = ev.offsetX / this.contrastEventPrameters.w;
1422
- this.contrastEventPrameters.y = ev.offsetY / this.contrastEventPrameters.h;
1423
- }, 100);
1424
- }
1425
- updateSlicesContrast(value, flag) {
1426
- switch (flag) {
1427
- case "lowerThreshold":
1428
- this.protectedData.displaySlices.forEach((slice, index) => {
1429
- slice.volume.lowerThreshold = value;
1430
- });
1431
- break;
1432
- case "upperThreshold":
1433
- this.protectedData.displaySlices.forEach((slice, index) => {
1434
- slice.volume.upperThreshold = value;
1435
- });
1436
- break;
1437
- case "windowLow":
1438
- this.protectedData.displaySlices.forEach((slice, index) => {
1439
- slice.volume.windowLow = value;
1440
- });
1441
- break;
1442
- case "windowHigh":
1443
- this.protectedData.displaySlices.forEach((slice, index) => {
1444
- slice.volume.windowHigh = value;
1445
- });
1446
- break;
1447
- }
1448
- this.repraintCurrentContrastSlice();
1449
- }
1450
- repraintCurrentContrastSlice() {
1451
- this.setSyncsliceNum();
1452
- this.protectedData.displaySlices.forEach((slice, index) => {
1453
- slice.repaint.call(slice);
1454
- });
944
+ this.contrastTool.setupConrastEvents(callback);
1455
945
  }
1456
946
  }
1457
947
  //# sourceMappingURL=DrawToolCore.js.map