copper3d 3.0.0 → 3.0.2

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 (98) hide show
  1. package/README.md +195 -28
  2. package/dist/Utils/segmentation/CommToolsData.d.ts +32 -18
  3. package/dist/Utils/segmentation/CommToolsData.js +69 -152
  4. package/dist/Utils/segmentation/CommToolsData.js.map +1 -1
  5. package/dist/Utils/segmentation/DragOperator.d.ts +10 -2
  6. package/dist/Utils/segmentation/DragOperator.js +18 -8
  7. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  8. package/dist/Utils/segmentation/DrawToolCore.d.ts +24 -18
  9. package/dist/Utils/segmentation/DrawToolCore.js +255 -436
  10. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  11. package/dist/Utils/segmentation/NrrdTools.d.ts +152 -10
  12. package/dist/Utils/segmentation/NrrdTools.js +604 -203
  13. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  14. package/dist/Utils/segmentation/core/MaskVolume.d.ts +1 -34
  15. package/dist/Utils/segmentation/core/MaskVolume.js +24 -124
  16. package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -1
  17. package/dist/Utils/segmentation/core/UndoManager.d.ts +1 -1
  18. package/dist/Utils/segmentation/core/UndoManager.js +1 -1
  19. package/dist/Utils/segmentation/core/index.d.ts +0 -2
  20. package/dist/Utils/segmentation/core/index.js +0 -1
  21. package/dist/Utils/segmentation/core/index.js.map +1 -1
  22. package/dist/Utils/segmentation/core/types.js +24 -24
  23. package/dist/Utils/segmentation/core/types.js.map +1 -1
  24. package/dist/Utils/segmentation/coreTools/GuiState.d.ts +22 -0
  25. package/dist/Utils/segmentation/coreTools/GuiState.js +48 -0
  26. package/dist/Utils/segmentation/coreTools/GuiState.js.map +1 -0
  27. package/dist/Utils/segmentation/coreTools/NrrdState.d.ts +25 -0
  28. package/dist/Utils/segmentation/coreTools/NrrdState.js +85 -0
  29. package/dist/Utils/segmentation/coreTools/NrrdState.js.map +1 -0
  30. package/dist/Utils/segmentation/coreTools/coreType.d.ts +88 -65
  31. package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
  32. package/dist/Utils/segmentation/coreTools/divControlTools.js +1 -1
  33. package/dist/Utils/segmentation/coreTools/divControlTools.js.map +1 -1
  34. package/dist/Utils/segmentation/coreTools/gui.d.ts +12 -9
  35. package/dist/Utils/segmentation/coreTools/gui.js +81 -107
  36. package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
  37. package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +2 -0
  38. package/dist/Utils/segmentation/eventRouter/EventRouter.js +19 -8
  39. package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -1
  40. package/dist/Utils/segmentation/eventRouter/types.d.ts +3 -3
  41. package/dist/Utils/segmentation/tools/BaseTool.d.ts +11 -5
  42. package/dist/Utils/segmentation/tools/BaseTool.js +1 -2
  43. package/dist/Utils/segmentation/tools/BaseTool.js.map +1 -1
  44. package/dist/Utils/segmentation/tools/CrosshairTool.js +25 -25
  45. package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -1
  46. package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +2 -0
  47. package/dist/Utils/segmentation/tools/DragSliceTool.js +63 -53
  48. package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -1
  49. package/dist/Utils/segmentation/tools/DrawingTool.d.ts +93 -0
  50. package/dist/Utils/segmentation/tools/DrawingTool.js +222 -0
  51. package/dist/Utils/segmentation/tools/DrawingTool.js.map +1 -0
  52. package/dist/Utils/segmentation/tools/EraserTool.js +8 -8
  53. package/dist/Utils/segmentation/tools/EraserTool.js.map +1 -1
  54. package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +12 -32
  55. package/dist/Utils/segmentation/tools/ImageStoreHelper.js +45 -102
  56. package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -1
  57. package/dist/Utils/segmentation/tools/PanTool.d.ts +53 -0
  58. package/dist/Utils/segmentation/tools/PanTool.js +90 -0
  59. package/dist/Utils/segmentation/tools/PanTool.js.map +1 -0
  60. package/dist/Utils/segmentation/tools/SphereTool.d.ts +208 -3
  61. package/dist/Utils/segmentation/tools/SphereTool.js +456 -45
  62. package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -1
  63. package/dist/Utils/segmentation/tools/ZoomTool.js +5 -4
  64. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -1
  65. package/dist/Utils/segmentation/tools/index.d.ts +6 -2
  66. package/dist/Utils/segmentation/tools/index.js +4 -1
  67. package/dist/Utils/segmentation/tools/index.js.map +1 -1
  68. package/dist/bundle.esm.js +2451 -1622
  69. package/dist/bundle.umd.js +2451 -1622
  70. package/dist/index.d.ts +6 -3
  71. package/dist/index.js +1 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/types/Utils/segmentation/CommToolsData.d.ts +32 -18
  74. package/dist/types/Utils/segmentation/DragOperator.d.ts +10 -2
  75. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +24 -18
  76. package/dist/types/Utils/segmentation/NrrdTools.d.ts +152 -10
  77. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +1 -34
  78. package/dist/types/Utils/segmentation/core/UndoManager.d.ts +1 -1
  79. package/dist/types/Utils/segmentation/core/index.d.ts +0 -2
  80. package/dist/types/Utils/segmentation/coreTools/GuiState.d.ts +22 -0
  81. package/dist/types/Utils/segmentation/coreTools/NrrdState.d.ts +25 -0
  82. package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +88 -65
  83. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +12 -9
  84. package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +2 -0
  85. package/dist/types/Utils/segmentation/eventRouter/types.d.ts +3 -3
  86. package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +11 -5
  87. package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +2 -0
  88. package/dist/types/Utils/segmentation/tools/DrawingTool.d.ts +93 -0
  89. package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +12 -32
  90. package/dist/types/Utils/segmentation/tools/PanTool.d.ts +53 -0
  91. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +208 -3
  92. package/dist/types/Utils/segmentation/tools/index.d.ts +6 -2
  93. package/dist/types/index.d.ts +6 -3
  94. package/package.json +1 -1
  95. package/dist/Utils/segmentation/core/MigrationUtils.d.ts +0 -97
  96. package/dist/Utils/segmentation/core/MigrationUtils.js +0 -163
  97. package/dist/Utils/segmentation/core/MigrationUtils.js.map +0 -1
  98. package/dist/types/Utils/segmentation/core/MigrationUtils.d.ts +0 -97
@@ -1,8 +1,9 @@
1
1
  import { setupGui } from "./coreTools/gui";
2
- import { autoFocusDiv } from "./coreTools/divControlTools";
2
+ import { autoFocusDiv, enableDownload } from "./coreTools/divControlTools";
3
3
  import { DragOperator } from "./DragOperator";
4
4
  import { DrawToolCore } from "./DrawToolCore";
5
- import { MaskVolume, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./core";
5
+ import { MaskVolume, CHANNEL_HEX_COLORS, MASK_CHANNEL_COLORS, rgbaToHex, rgbaToCss } from "./core";
6
+ import { SPHERE_CHANNEL_MAP, SPHERE_LABELS } from "./tools/SphereTool";
6
7
  /**
7
8
  * Core NRRD annotation tool for medical image segmentation.
8
9
  *
@@ -21,15 +22,19 @@ export class NrrdTools extends DrawToolCore {
21
22
  this.initState = true;
22
23
  this._sliceRAFId = null;
23
24
  this._pendingSliceStep = 0;
25
+ /** Whether calculator mode is active (not part of gui_states interface) */
26
+ this._calculatorActive = false;
27
+ /** Stored closure callbacks from gui.ts setupGui() */
28
+ this.guiCallbacks = null;
24
29
  this.container = container;
25
- this.protectedData.previousDrawingImage =
26
- this.protectedData.ctxes.emptyCtx.createImageData(1, 1);
27
30
  this.init();
28
31
  this.dragOperator = new DragOperator(this.container, this.nrrd_states, this.gui_states, this.protectedData, this.drawingPrameters, this.setSyncsliceNum.bind(this), this.setIsDrawFalse.bind(this), this.flipDisplayImageByAxis.bind(this), this.setEmptyCanvasSize.bind(this), this.getOrCreateSliceBuffer.bind(this), this.renderSliceToCanvas.bind(this));
29
32
  // Inject EventRouter into DragOperator for centralized event handling
30
33
  if (this.eventRouter) {
31
34
  this.dragOperator.setEventRouter(this.eventRouter);
32
35
  }
36
+ // Wire sphere overlay refresh callback into DragOperator → DragSliceTool
37
+ this.dragOperator.setRefreshSphereOverlay(() => this.refreshSphereOverlay());
33
38
  }
34
39
  /**
35
40
  * core function for drag slices
@@ -83,8 +88,8 @@ export class NrrdTools extends DrawToolCore {
83
88
  protectedData: this.protectedData,
84
89
  removeDragMode: this.dragOperator.removeDragMode,
85
90
  configDragMode: this.dragOperator.configDragMode,
86
- clearPaint: this.clearPaint,
87
- clearStoreImages: this.clearStoreImages,
91
+ clearActiveLayer: this.clearActiveLayer,
92
+ clearActiveSlice: this.clearActiveSlice,
88
93
  updateSlicesContrast: this.updateSlicesContrast,
89
94
  setMainAreaSize: this.setMainAreaSize,
90
95
  resetPaintAreaUIPosition: this.resetPaintAreaUIPosition,
@@ -94,81 +99,361 @@ export class NrrdTools extends DrawToolCore {
94
99
  resetLayerCanvas: this.resetLayerCanvas,
95
100
  redrawDisplayCanvas: this.redrawDisplayCanvas,
96
101
  flipDisplayImageByAxis: this.flipDisplayImageByAxis,
97
- filterDrawedImage: this.filterDrawedImage,
98
102
  setEmptyCanvasSize: this.setEmptyCanvasSize,
99
- storeAllImages: this.storeAllImages,
103
+ syncLayerSliceData: this.syncLayerSliceData,
100
104
  drawImageOnEmptyImage: this.drawImageOnEmptyImage,
101
- storeEachLayerImage: this.storeEachLayerImage,
102
- storeImageToLayer: this.storeImageToLayer,
103
105
  getRestLayer: this.getRestLayer,
104
106
  setIsDrawFalse: this.setIsDrawFalse,
105
107
  getVolumeForLayer: this.getVolumeForLayer.bind(this),
108
+ undoLastPainting: this.undoLastPainting.bind(this),
109
+ redoLastPainting: this.redoLastPainting.bind(this),
110
+ resetZoom: () => this.executeAction("resetZoom"),
111
+ downloadCurrentMask: () => this.executeAction("downloadCurrentMask"),
106
112
  };
107
113
  this.guiParameterSettings = setupGui(guiOptions);
114
+ // Store closure callbacks for programmatic access (Phase 1 Task 1.2)
115
+ this.guiCallbacks = {
116
+ updatePencilState: this.guiParameterSettings.pencil.onChange,
117
+ updateEraserState: this.guiParameterSettings.eraser.onChange,
118
+ updateBrushAndEraserSize: this.guiParameterSettings.brushAndEraserSize.onChange,
119
+ updateSphereState: this.guiParameterSettings.sphere.onChange,
120
+ updateCalDistance: this.guiParameterSettings.activeSphereType.onChange,
121
+ updateWindowHigh: this.guiParameterSettings.windowHigh.onChange,
122
+ updateWindowLow: this.guiParameterSettings.windowLow.onChange,
123
+ finishContrastAdjustment: this.guiParameterSettings.windowHigh.onFinished,
124
+ };
108
125
  }
109
- getGuiSettings() {
110
- if (!!this.guiParameterSettings) {
111
- // update image volume
126
+ /**
127
+ * Sync guiParameterSettings with current volume metadata.
128
+ * Called internally when slices are loaded or switched.
129
+ */
130
+ syncGuiParameterSettings() {
131
+ if (this.guiParameterSettings && this.protectedData.mainPreSlices) {
112
132
  this.guiParameterSettings.windowHigh.value = this.guiParameterSettings.windowLow.value = this.protectedData.mainPreSlices.volume;
113
133
  this.guiParameterSettings.windowHigh.max = this.guiParameterSettings.windowLow.max = this.protectedData.mainPreSlices.volume.max;
114
134
  this.guiParameterSettings.windowHigh.min = this.guiParameterSettings.windowLow.min = this.protectedData.mainPreSlices.volume.min;
115
135
  }
116
- return {
117
- guiState: this.gui_states,
118
- guiSetting: this.guiParameterSettings,
119
- };
120
136
  }
121
137
  // ── Layer & Channel Management API ─────────────────────────────────────
122
138
  /**
123
139
  * Set the active layer and update fillColor/brushColor to match the active channel.
124
140
  */
125
141
  setActiveLayer(layerId) {
126
- this.gui_states.layer = layerId;
142
+ this.gui_states.layerChannel.layer = layerId;
127
143
  this.syncBrushColor();
128
144
  }
129
145
  /**
130
146
  * Set the active channel (1-8) and update fillColor/brushColor.
131
147
  */
132
148
  setActiveChannel(channel) {
133
- this.gui_states.activeChannel = channel;
149
+ this.gui_states.layerChannel.activeChannel = channel;
134
150
  this.syncBrushColor();
135
151
  }
152
+ /**
153
+ * Set the active sphere type for the SphereTool.
154
+ * Replaces direct mutation of `gui_states.activeSphereType`.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * nrrdTools.setActiveSphereType('nipple');
159
+ * ```
160
+ */
161
+ setActiveSphereType(type) {
162
+ this.gui_states.mode.activeSphereType = type;
163
+ // Apply color side-effect: update fillColor/brushColor from sphere channel map
164
+ const mapping = SPHERE_CHANNEL_MAP[type];
165
+ if (mapping) {
166
+ const volume = this.getVolumeForLayer(mapping.layer);
167
+ const color = volume
168
+ ? rgbaToHex(volume.getChannelColor(mapping.channel))
169
+ : (CHANNEL_HEX_COLORS[mapping.channel] || '#00ff00');
170
+ this.gui_states.drawing.fillColor = color;
171
+ this.gui_states.drawing.brushColor = color;
172
+ }
173
+ }
174
+ /**
175
+ * Get the currently active sphere type.
176
+ */
177
+ getActiveSphereType() {
178
+ return this.gui_states.mode.activeSphereType;
179
+ }
180
+ // ── Phase 1: GUI API — Mode Management ──────────────────────────────────
181
+ /**
182
+ * Set the current tool mode. Handles deactivation of the previous mode
183
+ * and activation of the new mode, including all gui.ts side-effects.
184
+ *
185
+ * Replaces direct mutation of `guiSettings.value.guiState["pencil"]` etc.
186
+ * from Vue components.
187
+ */
188
+ setMode(mode) {
189
+ if (!this.guiCallbacks)
190
+ return;
191
+ const prevSphere = this.gui_states.mode.sphere;
192
+ const prevCalculator = this._calculatorActive;
193
+ // Reset all mode flags
194
+ this.gui_states.mode.pencil = false;
195
+ this.gui_states.mode.eraser = false;
196
+ this.gui_states.mode.sphere = false;
197
+ this._calculatorActive = false;
198
+ // Activate new mode
199
+ switch (mode) {
200
+ case "pencil":
201
+ this.gui_states.mode.pencil = true;
202
+ this.guiCallbacks.updatePencilState();
203
+ break;
204
+ case "brush":
205
+ // brush = pencil off + eraser off (default brush mode)
206
+ this.guiCallbacks.updatePencilState();
207
+ break;
208
+ case "eraser":
209
+ this.gui_states.mode.eraser = true;
210
+ this.guiCallbacks.updateEraserState();
211
+ break;
212
+ case "sphere":
213
+ this.gui_states.mode.sphere = true;
214
+ break;
215
+ case "calculator":
216
+ this.gui_states.mode.sphere = true;
217
+ this._calculatorActive = true;
218
+ break;
219
+ }
220
+ // Handle sphere mode transitions
221
+ if (prevSphere && !this.gui_states.mode.sphere) {
222
+ this.guiCallbacks.updateSphereState(); // exits sphere mode
223
+ }
224
+ if (!prevSphere && this.gui_states.mode.sphere) {
225
+ this.guiCallbacks.updateSphereState(); // enters sphere mode
226
+ }
227
+ // Handle calculator exit side-effect
228
+ if (prevCalculator && !this._calculatorActive) {
229
+ // calculator was exited — sphere onChange already handled above
230
+ }
231
+ }
232
+ /**
233
+ * Get the current tool mode based on gui_states flags.
234
+ */
235
+ getMode() {
236
+ if (this._calculatorActive)
237
+ return "calculator";
238
+ if (this.gui_states.mode.sphere)
239
+ return "sphere";
240
+ if (this.gui_states.mode.eraser)
241
+ return "eraser";
242
+ if (this.gui_states.mode.pencil)
243
+ return "pencil";
244
+ return "brush";
245
+ }
246
+ /**
247
+ * Check if calculator mode is active.
248
+ */
249
+ isCalculatorActive() {
250
+ return this._calculatorActive;
251
+ }
252
+ // ── Phase 1: GUI API — Slider Methods ───────────────────────────────────
253
+ /**
254
+ * Set mask overlay opacity.
255
+ * @param value Opacity value [0.1, 1]
256
+ */
257
+ setOpacity(value) {
258
+ this.gui_states.drawing.globalAlpha = Math.max(0.1, Math.min(1, value));
259
+ }
260
+ /**
261
+ * Get the current mask overlay opacity.
262
+ */
263
+ getOpacity() {
264
+ return this.gui_states.drawing.globalAlpha;
265
+ }
266
+ /**
267
+ * Set brush and eraser size, and trigger cursor update.
268
+ * @param size Brush size [5, 50]
269
+ */
270
+ setBrushSize(size) {
271
+ var _a;
272
+ this.gui_states.drawing.brushAndEraserSize = Math.max(5, Math.min(50, size));
273
+ (_a = this.guiCallbacks) === null || _a === void 0 ? void 0 : _a.updateBrushAndEraserSize();
274
+ }
275
+ /**
276
+ * Get the current brush/eraser size.
277
+ */
278
+ getBrushSize() {
279
+ return this.gui_states.drawing.brushAndEraserSize;
280
+ }
281
+ /**
282
+ * Set window high (image contrast) value.
283
+ * Call finishWindowAdjustment() when the user finishes dragging.
284
+ */
285
+ setWindowHigh(value) {
286
+ var _a;
287
+ this.gui_states.viewConfig.readyToUpdate = false;
288
+ (_a = this.guiCallbacks) === null || _a === void 0 ? void 0 : _a.updateWindowHigh(value);
289
+ }
290
+ /**
291
+ * Set window low (image center) value.
292
+ * Call finishWindowAdjustment() when the user finishes dragging.
293
+ */
294
+ setWindowLow(value) {
295
+ var _a;
296
+ this.gui_states.viewConfig.readyToUpdate = false;
297
+ (_a = this.guiCallbacks) === null || _a === void 0 ? void 0 : _a.updateWindowLow(value);
298
+ }
299
+ /**
300
+ * Finish a window/contrast adjustment (repaint all contrast slices).
301
+ */
302
+ finishWindowAdjustment() {
303
+ var _a;
304
+ (_a = this.guiCallbacks) === null || _a === void 0 ? void 0 : _a.finishContrastAdjustment();
305
+ }
306
+ /**
307
+ * Adjust contrast by delta, used for drag-based contrast adjustment.
308
+ * @param type "windowHigh" or "windowLow"
309
+ * @param delta Delta amount to adjust
310
+ */
311
+ adjustContrast(type, delta) {
312
+ var _a, _b;
313
+ if (!this.guiParameterSettings)
314
+ return;
315
+ const setting = this.guiParameterSettings[type];
316
+ // setting.value is the volume object at runtime (typed as null in interface)
317
+ const vol = setting.value;
318
+ const currentValue = type === "windowHigh"
319
+ ? ((_a = vol === null || vol === void 0 ? void 0 : vol.windowHigh) !== null && _a !== void 0 ? _a : 0)
320
+ : ((_b = vol === null || vol === void 0 ? void 0 : vol.windowLow) !== null && _b !== void 0 ? _b : 0);
321
+ const newValue = currentValue + delta;
322
+ if (newValue >= setting.max || newValue <= 0)
323
+ return;
324
+ if (type === "windowHigh") {
325
+ this.setWindowHigh(newValue);
326
+ }
327
+ else {
328
+ this.setWindowLow(newValue);
329
+ }
330
+ }
331
+ /**
332
+ * Get slider metadata for UI configuration.
333
+ * @param key Slider key: "globalAlpha", "brushAndEraserSize", "windowHigh", "windowLow"
334
+ * @returns IGuiMeta with min, max, step, and current value
335
+ */
336
+ getSliderMeta(key) {
337
+ var _a, _b, _c, _d, _e, _f, _g, _h;
338
+ if (!this.guiParameterSettings)
339
+ return null;
340
+ const setting = this.guiParameterSettings[key];
341
+ if (!setting)
342
+ return null;
343
+ let value;
344
+ if (key === "windowHigh") {
345
+ value = (_b = (_a = setting.value) === null || _a === void 0 ? void 0 : _a.windowHigh) !== null && _b !== void 0 ? _b : 0;
346
+ }
347
+ else if (key === "windowLow") {
348
+ value = (_d = (_c = setting.value) === null || _c === void 0 ? void 0 : _c.windowLow) !== null && _d !== void 0 ? _d : 0;
349
+ }
350
+ else {
351
+ value = (_e = this.gui_states.drawing[key]) !== null && _e !== void 0 ? _e : 0;
352
+ }
353
+ return {
354
+ min: (_f = setting.min) !== null && _f !== void 0 ? _f : 0,
355
+ max: (_g = setting.max) !== null && _g !== void 0 ? _g : 100,
356
+ step: (_h = setting.step) !== null && _h !== void 0 ? _h : 1,
357
+ value,
358
+ };
359
+ }
360
+ // ── Phase 1: GUI API — Color & Action Methods ──────────────────────────
361
+ /**
362
+ * Set the pencil stroke color.
363
+ */
364
+ setPencilColor(hex) {
365
+ this.gui_states.drawing.color = hex;
366
+ }
367
+ /**
368
+ * Get the current pencil stroke color.
369
+ */
370
+ getPencilColor() {
371
+ return this.gui_states.drawing.color;
372
+ }
373
+ /**
374
+ * Execute a named UI action.
375
+ * @param action Action name
376
+ *
377
+ * Renamed from original gui_states methods:
378
+ * - "clearActiveSliceMask" (was "clear") — clear annotations on current slice
379
+ * - "clearActiveLayerMask" (was "clearAll") — clear annotations on all slices for active layer
380
+ */
381
+ executeAction(action) {
382
+ switch (action) {
383
+ case "undo":
384
+ this.undo();
385
+ break;
386
+ case "redo":
387
+ this.redo();
388
+ break;
389
+ case "clearActiveSliceMask":
390
+ this.clearActiveSlice();
391
+ break;
392
+ case "clearActiveLayerMask": {
393
+ const text = "Are you sure remove annotations on All slice?";
394
+ if (confirm(text) === true) {
395
+ this.nrrd_states.flags.clearAllFlag = true;
396
+ this.clearActiveSlice();
397
+ this.clearActiveLayer();
398
+ }
399
+ this.nrrd_states.flags.clearAllFlag = false;
400
+ break;
401
+ }
402
+ case "resetZoom":
403
+ this.nrrd_states.view.sizeFactor = this.baseCanvasesSize;
404
+ this.gui_states.viewConfig.mainAreaSize = this.baseCanvasesSize;
405
+ this.resizePaintArea(this.nrrd_states.view.sizeFactor);
406
+ this.resetPaintAreaUIPosition();
407
+ break;
408
+ case "downloadCurrentMask": {
409
+ const config = {
410
+ axis: this.protectedData.axis,
411
+ currentSliceIndex: this.nrrd_states.view.currentSliceIndex,
412
+ drawingCanvas: this.protectedData.canvases.drawingCanvas,
413
+ originWidth: this.nrrd_states.image.originWidth,
414
+ originHeight: this.nrrd_states.image.originHeight,
415
+ };
416
+ enableDownload(config);
417
+ break;
418
+ }
419
+ }
420
+ }
136
421
  /**
137
422
  * Sync brush/fill color from the active layer's volume color map.
138
423
  * Falls back to global CHANNEL_HEX_COLORS if volume not available.
139
424
  */
140
425
  syncBrushColor() {
141
- const channel = this.gui_states.activeChannel || 1;
142
- const layer = this.gui_states.layer;
426
+ const channel = this.gui_states.layerChannel.activeChannel || 1;
427
+ const layer = this.gui_states.layerChannel.layer;
143
428
  const volume = this.protectedData.maskData.volumes[layer];
144
429
  if (volume) {
145
430
  const hex = rgbaToHex(volume.getChannelColor(channel));
146
- this.gui_states.fillColor = hex;
147
- this.gui_states.brushColor = hex;
431
+ this.gui_states.drawing.fillColor = hex;
432
+ this.gui_states.drawing.brushColor = hex;
148
433
  }
149
434
  else {
150
435
  const hex = CHANNEL_HEX_COLORS[channel] || '#00ff00';
151
- this.gui_states.fillColor = hex;
152
- this.gui_states.brushColor = hex;
436
+ this.gui_states.drawing.fillColor = hex;
437
+ this.gui_states.drawing.brushColor = hex;
153
438
  }
154
439
  }
155
440
  /**
156
441
  * Get the currently active layer id.
157
442
  */
158
443
  getActiveLayer() {
159
- return this.gui_states.layer;
444
+ return this.gui_states.layerChannel.layer;
160
445
  }
161
446
  /**
162
447
  * Get the currently active channel value.
163
448
  */
164
449
  getActiveChannel() {
165
- return this.gui_states.activeChannel;
450
+ return this.gui_states.layerChannel.activeChannel;
166
451
  }
167
452
  /**
168
453
  * Set visibility of a layer and re-render.
169
454
  */
170
455
  setLayerVisible(layerId, visible) {
171
- this.gui_states.layerVisibility[layerId] = visible;
456
+ this.gui_states.layerChannel.layerVisibility[layerId] = visible;
172
457
  this.reloadMasksFromVolume();
173
458
  }
174
459
  /**
@@ -176,14 +461,14 @@ export class NrrdTools extends DrawToolCore {
176
461
  */
177
462
  isLayerVisible(layerId) {
178
463
  var _a;
179
- return (_a = this.gui_states.layerVisibility[layerId]) !== null && _a !== void 0 ? _a : true;
464
+ return (_a = this.gui_states.layerChannel.layerVisibility[layerId]) !== null && _a !== void 0 ? _a : true;
180
465
  }
181
466
  /**
182
467
  * Set visibility of a specific channel within a layer and re-render.
183
468
  */
184
469
  setChannelVisible(layerId, channel, visible) {
185
- if (this.gui_states.channelVisibility[layerId]) {
186
- this.gui_states.channelVisibility[layerId][channel] = visible;
470
+ if (this.gui_states.layerChannel.channelVisibility[layerId]) {
471
+ this.gui_states.layerChannel.channelVisibility[layerId][channel] = visible;
187
472
  }
188
473
  this.reloadMasksFromVolume();
189
474
  }
@@ -192,21 +477,21 @@ export class NrrdTools extends DrawToolCore {
192
477
  */
193
478
  isChannelVisible(layerId, channel) {
194
479
  var _a, _b;
195
- return (_b = (_a = this.gui_states.channelVisibility[layerId]) === null || _a === void 0 ? void 0 : _a[channel]) !== null && _b !== void 0 ? _b : true;
480
+ return (_b = (_a = this.gui_states.layerChannel.channelVisibility[layerId]) === null || _a === void 0 ? void 0 : _a[channel]) !== null && _b !== void 0 ? _b : true;
196
481
  }
197
482
  /**
198
483
  * Get the full layer visibility map.
199
484
  */
200
485
  getLayerVisibility() {
201
- return Object.assign({}, this.gui_states.layerVisibility);
486
+ return Object.assign({}, this.gui_states.layerChannel.layerVisibility);
202
487
  }
203
488
  /**
204
489
  * Get the full channel visibility map.
205
490
  */
206
491
  getChannelVisibility() {
207
492
  const result = {};
208
- for (const layerId of this.nrrd_states.layers) {
209
- result[layerId] = Object.assign({}, this.gui_states.channelVisibility[layerId]);
493
+ for (const layerId of this.nrrd_states.image.layers) {
494
+ result[layerId] = Object.assign({}, this.gui_states.layerChannel.channelVisibility[layerId]);
210
495
  }
211
496
  return result;
212
497
  }
@@ -255,11 +540,11 @@ export class NrrdTools extends DrawToolCore {
255
540
  return;
256
541
  }
257
542
  volume.setChannelColor(channel, color);
258
- if (layerId === this.gui_states.layer && channel === this.gui_states.activeChannel) {
543
+ if (layerId === this.gui_states.layerChannel.layer && channel === this.gui_states.layerChannel.activeChannel) {
259
544
  this.syncBrushColor();
260
545
  }
261
546
  this.reloadMasksFromVolume();
262
- this.nrrd_states.onChannelColorChanged(layerId, channel, color);
547
+ this.annotationCallbacks.onChannelColorChanged(layerId, channel, color);
263
548
  }
264
549
  /**
265
550
  * Get the current color for a specific channel in a specific layer.
@@ -310,7 +595,7 @@ export class NrrdTools extends DrawToolCore {
310
595
  for (const [ch, color] of Object.entries(colorMap)) {
311
596
  volume.setChannelColor(Number(ch), color);
312
597
  }
313
- if (layerId === this.gui_states.layer) {
598
+ if (layerId === this.gui_states.layerChannel.layer) {
314
599
  this.syncBrushColor();
315
600
  }
316
601
  this.reloadMasksFromVolume();
@@ -322,13 +607,13 @@ export class NrrdTools extends DrawToolCore {
322
607
  * @param color RGBAColor object
323
608
  */
324
609
  setAllLayersChannelColor(channel, color) {
325
- for (const layerId of this.nrrd_states.layers) {
610
+ for (const layerId of this.nrrd_states.image.layers) {
326
611
  const volume = this.protectedData.maskData.volumes[layerId];
327
612
  if (volume) {
328
613
  volume.setChannelColor(channel, color);
329
614
  }
330
615
  }
331
- if (channel === this.gui_states.activeChannel) {
616
+ if (channel === this.gui_states.layerChannel.activeChannel) {
332
617
  this.syncBrushColor();
333
618
  }
334
619
  this.reloadMasksFromVolume();
@@ -347,7 +632,7 @@ export class NrrdTools extends DrawToolCore {
347
632
  * ```
348
633
  */
349
634
  resetChannelColors(layerId, channel) {
350
- const layers = layerId ? [layerId] : this.nrrd_states.layers;
635
+ const layers = layerId ? [layerId] : this.nrrd_states.image.layers;
351
636
  for (const lid of layers) {
352
637
  const volume = this.protectedData.maskData.volumes[lid];
353
638
  if (volume) {
@@ -522,28 +807,39 @@ export class NrrdTools extends DrawToolCore {
522
807
  setAllSlices(allSlices) {
523
808
  this.protectedData.allSlicesArray = [...allSlices];
524
809
  const randomSlice = this.protectedData.allSlicesArray[0];
525
- this.nrrd_states.nrrd_x_mm = randomSlice.z.canvas.width;
526
- this.nrrd_states.nrrd_y_mm = randomSlice.z.canvas.height;
527
- this.nrrd_states.nrrd_z_mm = randomSlice.x.canvas.width;
528
- this.nrrd_states.nrrd_x_pixel = randomSlice.x.volume.dimensions[0];
529
- this.nrrd_states.nrrd_y_pixel = randomSlice.x.volume.dimensions[1];
530
- this.nrrd_states.nrrd_z_pixel = randomSlice.x.volume.dimensions[2];
531
- this.nrrd_states.voxelSpacing = randomSlice.x.volume.spacing;
532
- this.nrrd_states.ratios.x = randomSlice.x.volume.spacing[0];
533
- this.nrrd_states.ratios.y = randomSlice.x.volume.spacing[1];
534
- this.nrrd_states.ratios.z = randomSlice.x.volume.spacing[2];
535
- this.nrrd_states.dimensions = randomSlice.x.volume.dimensions;
810
+ this.nrrd_states.image.nrrd_x_mm = randomSlice.z.canvas.width;
811
+ this.nrrd_states.image.nrrd_y_mm = randomSlice.z.canvas.height;
812
+ this.nrrd_states.image.nrrd_z_mm = randomSlice.x.canvas.width;
813
+ this.nrrd_states.image.nrrd_x_pixel = randomSlice.x.volume.dimensions[0];
814
+ this.nrrd_states.image.nrrd_y_pixel = randomSlice.x.volume.dimensions[1];
815
+ this.nrrd_states.image.nrrd_z_pixel = randomSlice.x.volume.dimensions[2];
816
+ this.nrrd_states.image.voxelSpacing = randomSlice.x.volume.spacing;
817
+ this.nrrd_states.image.ratios.x = randomSlice.x.volume.spacing[0];
818
+ this.nrrd_states.image.ratios.y = randomSlice.x.volume.spacing[1];
819
+ this.nrrd_states.image.ratios.z = randomSlice.x.volume.spacing[2];
820
+ this.nrrd_states.image.dimensions = randomSlice.x.volume.dimensions;
536
821
  // Phase 2 Day 9: Re-initialize MaskVolume with real NRRD dimensions.
537
822
  // This replaces the 1×1×1 placeholders from CommToolsData constructor
538
823
  // and "turns on" all Day 7/8 volume read/write paths.
539
824
  // Invalidate reusable buffer from previous dataset
540
825
  this.invalidateSliceBuffer();
541
- const [vw, vh, vd] = this.nrrd_states.dimensions;
542
- this.protectedData.maskData.volumes = this.nrrd_states.layers.reduce((acc, id) => {
826
+ const [vw, vh, vd] = this.nrrd_states.image.dimensions;
827
+ this.protectedData.maskData.volumes = this.nrrd_states.image.layers.reduce((acc, id) => {
543
828
  acc[id] = new MaskVolume(vw, vh, vd, 1);
544
829
  return acc;
545
830
  }, {});
546
- this.nrrd_states.spaceOrigin = randomSlice.x.volume.header.space_origin.map((item) => {
831
+ // Create dedicated SphereMaskVolume for 3D sphere data.
832
+ // Separate from layer volumes to avoid polluting draw mask data.
833
+ // Cleared in reset() when switching cases.
834
+ this.nrrd_states.sphere.sphereMaskVolume = new MaskVolume(vw, vh, vd, 1);
835
+ // Derive sphere label colors from SPHERE_CHANNEL_MAP → MASK_CHANNEL_COLORS
836
+ // so that volume rendering matches the preview circle colors.
837
+ for (const [type, { channel }] of Object.entries(SPHERE_CHANNEL_MAP)) {
838
+ const label = SPHERE_LABELS[type];
839
+ const c = MASK_CHANNEL_COLORS[channel];
840
+ this.nrrd_states.sphere.sphereMaskVolume.setChannelColor(label, { r: c.r, g: c.g, b: c.b, a: c.a });
841
+ }
842
+ this.nrrd_states.image.spaceOrigin = randomSlice.x.volume.header.space_origin.map((item) => {
547
843
  return item * 1;
548
844
  });
549
845
  this.protectedData.allSlicesArray.forEach((item, index) => {
@@ -552,13 +848,13 @@ export class NrrdTools extends DrawToolCore {
552
848
  item.z.contrastOrder = index;
553
849
  });
554
850
  // Phase 3: initPaintImages removed (MaskVolume initialized separately)
555
- // this.initPaintImages(this.nrrd_states.dimensions);
851
+ // this.initPaintImages(this.nrrd_states.image.dimensions);
556
852
  // init displayslices array, the axis default is "z"
557
853
  this.setDisplaySlicesBaseOnAxis();
558
854
  this.afterLoadSlice();
559
855
  }
560
856
  loadingMaskByLayer(masks, index, imageData) {
561
- let imageDataLable = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_y_pixel);
857
+ let imageDataLable = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.image.nrrd_x_pixel, this.nrrd_states.image.nrrd_y_pixel);
562
858
  this.setEmptyCanvasSize();
563
859
  for (let j = 0; j < masks[index].data.length; j++) {
564
860
  imageDataLable.data[j] = masks[index].data[j];
@@ -569,7 +865,7 @@ export class NrrdTools extends DrawToolCore {
569
865
  // need to remove
570
866
  setMasksData(masksData, loadingBar) {
571
867
  if (!!masksData) {
572
- this.nrrd_states.loadMaskJson = true;
868
+ this.nrrd_states.flags.loadingMaskData = true;
573
869
  if (loadingBar) {
574
870
  let { loadingContainer, progress } = loadingBar;
575
871
  loadingContainer.style.display = "flex";
@@ -578,32 +874,22 @@ export class NrrdTools extends DrawToolCore {
578
874
  this.setEmptyCanvasSize();
579
875
  const len = masksData["layer1"].length;
580
876
  for (let i = 0; i < len; i++) {
581
- let imageData = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.nrrd_x_pixel, this.nrrd_states.nrrd_y_pixel);
582
- let imageDataLayer1, imageDataLayer2, imageDataLayer3;
877
+ let imageData = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.image.nrrd_x_pixel, this.nrrd_states.image.nrrd_y_pixel);
583
878
  if (masksData["layer1"][i].data.length > 0) {
584
- this.setEmptyCanvasSize();
585
- imageDataLayer1 = this.loadingMaskByLayer(masksData["layer1"], i, imageData);
586
- this.protectedData.ctxes.emptyCtx.putImageData(imageDataLayer1, 0, 0);
587
- this.storeEachLayerImage(i, "layer1");
879
+ this.loadingMaskByLayer(masksData["layer1"], i, imageData);
588
880
  }
589
881
  if (masksData["layer2"][i].data.length > 0) {
590
- this.setEmptyCanvasSize();
591
- imageDataLayer2 = this.loadingMaskByLayer(masksData["layer2"], i, imageData);
592
- this.protectedData.ctxes.emptyCtx.putImageData(imageDataLayer2, 0, 0);
593
- this.storeEachLayerImage(i, "layer2");
882
+ this.loadingMaskByLayer(masksData["layer2"], i, imageData);
594
883
  }
595
884
  if (masksData["layer3"][i].data.length > 0) {
596
- this.setEmptyCanvasSize();
597
- imageDataLayer3 = this.loadingMaskByLayer(masksData["layer3"], i, imageData);
598
- this.protectedData.ctxes.emptyCtx.putImageData(imageDataLayer3, 0, 0);
599
- this.storeEachLayerImage(i, "layer3");
885
+ this.loadingMaskByLayer(masksData["layer3"], i, imageData);
600
886
  }
601
887
  this.setEmptyCanvasSize();
602
888
  this.protectedData.ctxes.emptyCtx.putImageData(imageData, 0, 0);
603
- this.storeAllImages(i, "default");
889
+ this.syncLayerSliceData(i, "default");
604
890
  }
605
- this.nrrd_states.loadMaskJson = false;
606
- this.gui_states.resetZoom();
891
+ this.nrrd_states.flags.loadingMaskData = false;
892
+ this.executeAction("resetZoom");
607
893
  if (loadingBar) {
608
894
  loadingBar.loadingContainer.style.display = "none";
609
895
  }
@@ -645,7 +931,7 @@ export class NrrdTools extends DrawToolCore {
645
931
  }
646
932
  // Reload the current slice from MaskVolume to canvas
647
933
  this.reloadMasksFromVolume();
648
- this.gui_states.resetZoom();
934
+ this.executeAction("resetZoom");
649
935
  }
650
936
  catch (error) {
651
937
  console.error("Error loading NIfTI masks:", error);
@@ -657,56 +943,95 @@ export class NrrdTools extends DrawToolCore {
657
943
  }
658
944
  }
659
945
  setShowInMainArea() {
660
- this.nrrd_states.showContrast = true;
946
+ this.nrrd_states.view.showContrast = true;
661
947
  }
662
948
  getCurrentImageDimension() {
663
- return this.nrrd_states.dimensions;
949
+ return this.nrrd_states.image.dimensions;
664
950
  }
665
951
  getVoxelSpacing() {
666
- return this.nrrd_states.voxelSpacing;
952
+ return this.nrrd_states.image.voxelSpacing;
667
953
  }
668
954
  getSpaceOrigin() {
669
- return this.nrrd_states.spaceOrigin;
955
+ return this.nrrd_states.image.spaceOrigin;
670
956
  }
671
957
  getMaskData() {
672
958
  return this.protectedData.maskData;
673
959
  }
674
- // set calculate distance sphere position
960
+ /**
961
+ * Programmatically place a calculator sphere at the given position.
962
+ *
963
+ * Replicates the full mouse-down → mouse-up flow (handleSphereClick + pointerup)
964
+ * so that backend-supplied sphere data is stored identically to a manual click.
965
+ *
966
+ * @param x - X coordinate in unscaled (original) image space
967
+ * @param y - Y coordinate in unscaled (original) image space
968
+ * @param sliceIndex - Target slice index (z-axis)
969
+ * @param cal_position - Sphere type to place
970
+ */
675
971
  setCalculateDistanceSphere(x, y, sliceIndex, cal_position) {
676
- this.nrrd_states.sphereRadius = 5;
677
- // move to tumour slice
678
- const steps = sliceIndex - this.nrrd_states.currentIndex;
972
+ this.nrrd_states.sphere.sphereRadius = 5;
973
+ // move to target slice
974
+ const steps = sliceIndex - this.nrrd_states.view.currentSliceIndex;
679
975
  this.setSliceMoving(steps * this.protectedData.displaySlices.length);
680
- // mock mouse down
681
- // if user zoom the panel, we need to consider the size factor
682
- this.drawCalSphereDown(x * this.nrrd_states.sizeFoctor, y * this.nrrd_states.sizeFoctor, sliceIndex, cal_position);
683
- // mock mouse up
684
- this.drawCalSphereUp();
976
+ // --- simulate mouse-down (mirrors DrawToolCore.handleSphereClick) ---
977
+ // if user has zoomed the panel, we need to consider the size factor
978
+ const mouseX = x * this.nrrd_states.view.sizeFactor;
979
+ const mouseY = y * this.nrrd_states.view.sizeFactor;
980
+ // 1. record origin on current axis
981
+ this.nrrd_states.sphere.sphereOrigin[this.protectedData.axis] = [
982
+ mouseX, mouseY, sliceIndex,
983
+ ];
984
+ // compute origins for all 3 axes (crosshairTool is protected)
985
+ this.crosshairTool.setUpSphereOrigins(mouseX, mouseY, sliceIndex);
986
+ // 2. store a deep copy of the origin for the specific sphere type
987
+ const originCopy = JSON.parse(JSON.stringify(this.nrrd_states.sphere.sphereOrigin));
988
+ switch (cal_position) {
989
+ case "tumour":
990
+ this.nrrd_states.sphere.tumourSphereOrigin = originCopy;
991
+ break;
992
+ case "skin":
993
+ this.nrrd_states.sphere.skinSphereOrigin = originCopy;
994
+ break;
995
+ case "nipple":
996
+ this.nrrd_states.sphere.nippleSphereOrigin = originCopy;
997
+ break;
998
+ case "ribcage":
999
+ this.nrrd_states.sphere.ribSphereOrigin = originCopy;
1000
+ break;
1001
+ }
1002
+ // 3. draw sphere preview on canvas
1003
+ this.drawCalculatorSphere(this.nrrd_states.sphere.sphereRadius);
1004
+ // --- simulate mouse-up ---
1005
+ // 4. write all placed calculator spheres into sphereMaskVolume
1006
+ this.sphereTool.writeAllCalculatorSpheresToVolume();
1007
+ // 5. re-render sphere overlay from volume
1008
+ this.sphereTool.refreshSphereCanvas();
685
1009
  }
686
1010
  /**
687
1011
  * Switch all contrast slices' orientation
688
1012
  * @param {string} aixs:"x" | "y" | "z"
689
1013
  * */
690
1014
  setSliceOrientation(axisTo) {
1015
+ var _a;
691
1016
  let convetObj;
692
- if (this.nrrd_states.enableCursorChoose || this.gui_states.sphere) {
1017
+ if (((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isCrosshairEnabled()) || this.gui_states.mode.sphere) {
693
1018
  if (this.protectedData.axis === "z") {
694
- this.cursorPage.z.index = this.nrrd_states.currentIndex;
695
- this.cursorPage.z.cursorPageX = this.nrrd_states.cursorPageX;
696
- this.cursorPage.z.cursorPageY = this.nrrd_states.cursorPageY;
1019
+ this.cursorPage.z.index = this.nrrd_states.view.currentSliceIndex;
1020
+ this.cursorPage.z.cursorPageX = this.nrrd_states.interaction.cursorPageX;
1021
+ this.cursorPage.z.cursorPageY = this.nrrd_states.interaction.cursorPageY;
697
1022
  }
698
1023
  else if (this.protectedData.axis === "x") {
699
- this.cursorPage.x.index = this.nrrd_states.currentIndex;
700
- this.cursorPage.x.cursorPageX = this.nrrd_states.cursorPageX;
701
- this.cursorPage.x.cursorPageY = this.nrrd_states.cursorPageY;
1024
+ this.cursorPage.x.index = this.nrrd_states.view.currentSliceIndex;
1025
+ this.cursorPage.x.cursorPageX = this.nrrd_states.interaction.cursorPageX;
1026
+ this.cursorPage.x.cursorPageY = this.nrrd_states.interaction.cursorPageY;
702
1027
  }
703
1028
  else if (this.protectedData.axis === "y") {
704
- this.cursorPage.y.index = this.nrrd_states.currentIndex;
705
- this.cursorPage.y.cursorPageX = this.nrrd_states.cursorPageX;
706
- this.cursorPage.y.cursorPageY = this.nrrd_states.cursorPageY;
1029
+ this.cursorPage.y.index = this.nrrd_states.view.currentSliceIndex;
1030
+ this.cursorPage.y.cursorPageX = this.nrrd_states.interaction.cursorPageX;
1031
+ this.cursorPage.y.cursorPageY = this.nrrd_states.interaction.cursorPageY;
707
1032
  }
708
1033
  if (axisTo === "z") {
709
- if (this.nrrd_states.isCursorSelect && !this.cursorPage.z.updated) {
1034
+ if (this.nrrd_states.interaction.isCursorSelect && !this.cursorPage.z.updated) {
710
1035
  if (this.protectedData.axis === "x") {
711
1036
  // convert x to z
712
1037
  convetObj = this.convertCursorPoint("x", "z", this.cursorPage.x.cursorPageX, this.cursorPage.x.cursorPageY, this.cursorPage.x.index);
@@ -718,15 +1043,15 @@ export class NrrdTools extends DrawToolCore {
718
1043
  }
719
1044
  else {
720
1045
  // not cursor select, freedom to switch x -> z or y -> z and z -> x or z -> y
721
- this.nrrd_states.currentIndex = this.cursorPage.z.index;
722
- this.nrrd_states.oldIndex =
723
- this.cursorPage.z.index * this.nrrd_states.ratios.z;
724
- this.nrrd_states.cursorPageX = this.cursorPage.z.cursorPageX;
725
- this.nrrd_states.cursorPageY = this.cursorPage.z.cursorPageY;
1046
+ this.nrrd_states.view.currentSliceIndex = this.cursorPage.z.index;
1047
+ this.nrrd_states.view.preSliceIndex =
1048
+ this.cursorPage.z.index * this.nrrd_states.image.ratios.z;
1049
+ this.nrrd_states.interaction.cursorPageX = this.cursorPage.z.cursorPageX;
1050
+ this.nrrd_states.interaction.cursorPageY = this.cursorPage.z.cursorPageY;
726
1051
  }
727
1052
  }
728
1053
  else if (axisTo === "x") {
729
- if (this.nrrd_states.isCursorSelect && !this.cursorPage.x.updated) {
1054
+ if (this.nrrd_states.interaction.isCursorSelect && !this.cursorPage.x.updated) {
730
1055
  if (this.protectedData.axis === "z") {
731
1056
  // convert z to x
732
1057
  convetObj = this.convertCursorPoint("z", "x", this.cursorPage.z.cursorPageX, this.cursorPage.z.cursorPageY, this.cursorPage.z.index);
@@ -738,15 +1063,15 @@ export class NrrdTools extends DrawToolCore {
738
1063
  }
739
1064
  else {
740
1065
  // not cursor select, freedom to switch z -> x or y -> x and x -> z or x -> y
741
- this.nrrd_states.currentIndex = this.cursorPage.x.index;
742
- this.nrrd_states.oldIndex =
743
- this.cursorPage.x.index * this.nrrd_states.ratios.x;
744
- this.nrrd_states.cursorPageX = this.cursorPage.x.cursorPageX;
745
- this.nrrd_states.cursorPageY = this.cursorPage.x.cursorPageY;
1066
+ this.nrrd_states.view.currentSliceIndex = this.cursorPage.x.index;
1067
+ this.nrrd_states.view.preSliceIndex =
1068
+ this.cursorPage.x.index * this.nrrd_states.image.ratios.x;
1069
+ this.nrrd_states.interaction.cursorPageX = this.cursorPage.x.cursorPageX;
1070
+ this.nrrd_states.interaction.cursorPageY = this.cursorPage.x.cursorPageY;
746
1071
  }
747
1072
  }
748
1073
  else if (axisTo === "y") {
749
- if (this.nrrd_states.isCursorSelect && !this.cursorPage.y.updated) {
1074
+ if (this.nrrd_states.interaction.isCursorSelect && !this.cursorPage.y.updated) {
750
1075
  if (this.protectedData.axis === "z") {
751
1076
  // convert z to y
752
1077
  convetObj = this.convertCursorPoint("z", "y", this.cursorPage.z.cursorPageX, this.cursorPage.z.cursorPageY, this.cursorPage.z.index);
@@ -758,19 +1083,19 @@ export class NrrdTools extends DrawToolCore {
758
1083
  }
759
1084
  else {
760
1085
  // not cursor select, freedom to switch z -> y or x -> y and y -> z or y -> x
761
- this.nrrd_states.currentIndex = this.cursorPage.y.index;
762
- this.nrrd_states.oldIndex =
763
- this.cursorPage.y.index * this.nrrd_states.ratios.y;
764
- this.nrrd_states.cursorPageX = this.cursorPage.y.cursorPageX;
765
- this.nrrd_states.cursorPageY = this.cursorPage.y.cursorPageY;
1086
+ this.nrrd_states.view.currentSliceIndex = this.cursorPage.y.index;
1087
+ this.nrrd_states.view.preSliceIndex =
1088
+ this.cursorPage.y.index * this.nrrd_states.image.ratios.y;
1089
+ this.nrrd_states.interaction.cursorPageX = this.cursorPage.y.cursorPageX;
1090
+ this.nrrd_states.interaction.cursorPageY = this.cursorPage.y.cursorPageY;
766
1091
  }
767
1092
  }
768
1093
  if (convetObj) {
769
1094
  // update convert cursor point, when cursor select
770
- this.nrrd_states.currentIndex = convetObj.currentIndex;
771
- this.nrrd_states.oldIndex = convetObj.oldIndex;
772
- this.nrrd_states.cursorPageX = convetObj.convertCursorNumX;
773
- this.nrrd_states.cursorPageY = convetObj.convertCursorNumY;
1095
+ this.nrrd_states.view.currentSliceIndex = convetObj.currentNewSliceIndex;
1096
+ this.nrrd_states.view.preSliceIndex = convetObj.preSliceIndex;
1097
+ this.nrrd_states.interaction.cursorPageX = convetObj.convertCursorNumX;
1098
+ this.nrrd_states.interaction.cursorPageY = convetObj.convertCursorNumY;
774
1099
  convetObj = undefined;
775
1100
  switch (axisTo) {
776
1101
  case "x":
@@ -788,47 +1113,52 @@ export class NrrdTools extends DrawToolCore {
788
1113
  this.cursorPage.y.updated &&
789
1114
  this.cursorPage.z.updated) {
790
1115
  // one point convert to all axis, reset all updated status
791
- this.nrrd_states.isCursorSelect = false;
1116
+ this.nrrd_states.interaction.isCursorSelect = false;
792
1117
  }
793
1118
  }
794
1119
  this.protectedData.axis = axisTo;
795
1120
  this.resetDisplaySlicesStatus();
796
- // for sphere plan a
797
- if (this.gui_states.sphere && !this.nrrd_states.spherePlanB) {
798
- this.drawSphere(this.nrrd_states.sphereOrigin[axisTo][0] * this.nrrd_states.sizeFoctor, this.nrrd_states.sphereOrigin[axisTo][1] * this.nrrd_states.sizeFoctor, this.nrrd_states.sphereRadius);
799
- }
800
1121
  }
801
1122
  addSkip(index) {
802
1123
  this.protectedData.skipSlicesDic[index] =
803
1124
  this.protectedData.backUpDisplaySlices[index];
804
1125
  if (index >= this.protectedData.displaySlices.length) {
805
- this.nrrd_states.contrastNum = this.protectedData.displaySlices.length;
1126
+ this.nrrd_states.view.contrastNum = this.protectedData.displaySlices.length;
806
1127
  }
807
1128
  else {
808
- this.nrrd_states.contrastNum = index;
1129
+ this.nrrd_states.view.contrastNum = index;
809
1130
  }
810
1131
  this.resetDisplaySlicesStatus();
811
1132
  }
812
1133
  removeSkip(index) {
813
1134
  this.protectedData.skipSlicesDic[index] = undefined;
814
- this.nrrd_states.contrastNum = 0;
1135
+ this.nrrd_states.view.contrastNum = 0;
815
1136
  this.resetDisplaySlicesStatus();
816
1137
  }
817
- clear() {
1138
+ /**
1139
+ * Reset the entire NrrdTools instance comprehensively.
1140
+ * This clears ALL data across ALL layers globally, resets the Canvas visuals,
1141
+ * undo/redo history, volume models, index parameters, and sphere overlays.
1142
+ * Primarily used when switching cases/datasets or when a completely fresh state is needed.
1143
+ * It is heavier than `clearActiveLayer` or `clearActiveSlice`.
1144
+ */
1145
+ reset() {
818
1146
  // To effectively reduce the js memory garbage
819
1147
  this.protectedData.allSlicesArray.length = 0;
820
1148
  this.protectedData.displaySlices.length = 0;
821
1149
  // Phase 6: Clear all undo/redo stacks
822
1150
  this.undoManager.clearAll();
823
1151
  // Phase 3: Reset MaskVolume storage to 1×1×1 placeholders
824
- this.protectedData.maskData.volumes = this.nrrd_states.layers.reduce((acc, id) => {
1152
+ this.protectedData.maskData.volumes = this.nrrd_states.image.layers.reduce((acc, id) => {
825
1153
  acc[id] = new MaskVolume(1, 1, 1, 1);
826
1154
  return acc;
827
1155
  }, {});
1156
+ // Clear dedicated SphereMaskVolume
1157
+ this.nrrd_states.sphere.sphereMaskVolume = null;
828
1158
  // Invalidate reusable slice buffer
829
1159
  this.invalidateSliceBuffer();
830
1160
  this.clearDictionary(this.protectedData.skipSlicesDic);
831
- // this.nrrd_states.previousPanelL = this.nrrd_states.previousPanelT = -99999;
1161
+ // this.nrrd_states.view.previousPanelL = this.nrrd_states.view.previousPanelT = -99999;
832
1162
  this.protectedData.canvases.displayCanvas.style.left =
833
1163
  this.protectedData.canvases.drawingCanvas.style.left = "";
834
1164
  this.protectedData.canvases.displayCanvas.style.top =
@@ -836,21 +1166,19 @@ export class NrrdTools extends DrawToolCore {
836
1166
  this.protectedData.backUpDisplaySlices.length = 0;
837
1167
  this.protectedData.mainPreSlices = undefined;
838
1168
  this.protectedData.currentShowingSlice = undefined;
839
- this.protectedData.previousDrawingImage =
840
- this.protectedData.ctxes.emptyCtx.createImageData(1, 1);
841
1169
  this.initState = true;
842
1170
  this.protectedData.axis = "z";
843
- this.nrrd_states.sizeFoctor = this.baseCanvasesSize;
844
- this.gui_states.mainAreaSize = this.baseCanvasesSize;
1171
+ this.nrrd_states.view.sizeFactor = this.baseCanvasesSize;
1172
+ this.gui_states.viewConfig.mainAreaSize = this.baseCanvasesSize;
845
1173
  this.resetLayerCanvas();
846
1174
  this.protectedData.canvases.drawingCanvas.width =
847
1175
  this.protectedData.canvases.drawingCanvas.width;
848
1176
  this.protectedData.canvases.displayCanvas.width =
849
1177
  this.protectedData.canvases.displayCanvas.width;
850
- this.nrrd_states.tumourSphereOrigin = null;
851
- this.nrrd_states.ribSphereOrigin = null;
852
- this.nrrd_states.skinSphereOrigin = null;
853
- this.nrrd_states.nippleSphereOrigin = null;
1178
+ this.nrrd_states.sphere.tumourSphereOrigin = null;
1179
+ this.nrrd_states.sphere.ribSphereOrigin = null;
1180
+ this.nrrd_states.sphere.skinSphereOrigin = null;
1181
+ this.nrrd_states.sphere.nippleSphereOrigin = null;
854
1182
  }
855
1183
  setSliceMoving(step) {
856
1184
  if (this.protectedData.mainPreSlices) {
@@ -863,7 +1191,7 @@ export class NrrdTools extends DrawToolCore {
863
1191
  this._sliceRAFId = null;
864
1192
  const totalStep = this._pendingSliceStep;
865
1193
  this._pendingSliceStep = 0;
866
- this.protectedData.Is_Draw = true;
1194
+ this.protectedData.isDrawing = true;
867
1195
  this.setSyncsliceNum();
868
1196
  this.dragOperator.updateIndex(totalStep);
869
1197
  this.setIsDrawFalse(1000);
@@ -871,14 +1199,14 @@ export class NrrdTools extends DrawToolCore {
871
1199
  }
872
1200
  }
873
1201
  setMainAreaSize(factor) {
874
- this.nrrd_states.sizeFoctor = factor;
875
- if (this.nrrd_states.sizeFoctor >= 8) {
876
- this.nrrd_states.sizeFoctor = 8;
1202
+ this.nrrd_states.view.sizeFactor = factor;
1203
+ if (this.nrrd_states.view.sizeFactor >= 8) {
1204
+ this.nrrd_states.view.sizeFactor = 8;
877
1205
  }
878
- else if (this.nrrd_states.sizeFoctor <= 1) {
879
- this.nrrd_states.sizeFoctor = 1;
1206
+ else if (this.nrrd_states.view.sizeFactor <= 1) {
1207
+ this.nrrd_states.view.sizeFactor = 1;
880
1208
  }
881
- this.resizePaintArea(this.nrrd_states.sizeFoctor);
1209
+ this.resizePaintArea(this.nrrd_states.view.sizeFactor);
882
1210
  this.resetPaintAreaUIPosition();
883
1211
  // this.setIsDrawFalse(1000);
884
1212
  }
@@ -892,27 +1220,27 @@ export class NrrdTools extends DrawToolCore {
892
1220
  return this.nrrd_states;
893
1221
  }
894
1222
  getMaxSliceNum() {
895
- if (this.nrrd_states.showContrast) {
1223
+ if (this.nrrd_states.view.showContrast) {
896
1224
  return [
897
- this.nrrd_states.maxIndex,
898
- this.nrrd_states.maxIndex * this.protectedData.displaySlices.length,
1225
+ this.nrrd_states.view.maxIndex,
1226
+ this.nrrd_states.view.maxIndex * this.protectedData.displaySlices.length,
899
1227
  ];
900
1228
  }
901
1229
  else {
902
- return [this.nrrd_states.maxIndex];
1230
+ return [this.nrrd_states.view.maxIndex];
903
1231
  }
904
1232
  }
905
1233
  getCurrentSlicesNumAndContrastNum() {
906
1234
  return {
907
- currentIndex: this.nrrd_states.currentIndex,
908
- contrastIndex: this.nrrd_states.contrastNum,
1235
+ currentSliceIndex: this.nrrd_states.view.currentSliceIndex,
1236
+ contrastIndex: this.nrrd_states.view.contrastNum,
909
1237
  };
910
1238
  }
911
1239
  getCurrentSliceIndex() {
912
- return Math.ceil(this.protectedData.mainPreSlices.index / this.nrrd_states.RSARatio);
1240
+ return Math.ceil(this.protectedData.mainPreSlices.index / this.nrrd_states.image.RSARatio);
913
1241
  }
914
1242
  getIsShowContrastState() {
915
- return this.nrrd_states.showContrast;
1243
+ return this.nrrd_states.view.showContrast;
916
1244
  }
917
1245
  /**
918
1246
  * Give a delay time to finish the last drawing before upcoming interrupt opreations.
@@ -922,7 +1250,7 @@ export class NrrdTools extends DrawToolCore {
922
1250
  */
923
1251
  setIsDrawFalse(target) {
924
1252
  this.preTimer = setTimeout(() => {
925
- this.protectedData.Is_Draw = false;
1253
+ this.protectedData.isDrawing = false;
926
1254
  if (this.preTimer) {
927
1255
  window.clearTimeout(this.preTimer);
928
1256
  this.preTimer = undefined;
@@ -977,35 +1305,37 @@ export class NrrdTools extends DrawToolCore {
977
1305
  // (also calls resizePaintArea → reloads masks, resizes canvases)
978
1306
  this.setOriginCanvasAndPre();
979
1307
  // update the show number div on top area
980
- this.dragOperator.updateShowNumDiv(this.nrrd_states.contrastNum);
1308
+ this.dragOperator.updateShowNumDiv(this.nrrd_states.view.contrastNum);
981
1309
  // repaint all contrast images
982
1310
  this.repraintCurrentContrastSlice();
983
1311
  // Refresh display after contrast repaint (no need for full resizePaintArea
984
1312
  // since canvases were already resized in setOriginCanvasAndPre above)
985
1313
  this.redrawDisplayCanvas();
986
1314
  this.compositeAllLayers();
1315
+ // Sync slider metadata with current volume (replaces old getGuiSettings() side-effect)
1316
+ this.syncGuiParameterSettings();
987
1317
  }
988
1318
  setMainPreSlice() {
989
1319
  this.protectedData.mainPreSlices = this.protectedData.displaySlices[0];
990
1320
  if (this.protectedData.mainPreSlices) {
991
- this.nrrd_states.RSARatio = this.protectedData.mainPreSlices.RSARatio;
1321
+ this.nrrd_states.image.RSARatio = this.protectedData.mainPreSlices.RSARatio;
992
1322
  }
993
1323
  }
994
1324
  setOriginCanvasAndPre() {
995
1325
  if (this.protectedData.mainPreSlices) {
996
- if (this.nrrd_states.oldIndex > this.nrrd_states.maxIndex)
997
- this.nrrd_states.oldIndex = this.nrrd_states.maxIndex;
1326
+ if (this.nrrd_states.view.preSliceIndex > this.nrrd_states.view.maxIndex)
1327
+ this.nrrd_states.view.preSliceIndex = this.nrrd_states.view.maxIndex;
998
1328
  if (this.initState) {
999
- this.nrrd_states.oldIndex =
1329
+ this.nrrd_states.view.preSliceIndex =
1000
1330
  this.protectedData.mainPreSlices.initIndex *
1001
- this.nrrd_states.RSARatio;
1002
- this.nrrd_states.currentIndex =
1331
+ this.nrrd_states.image.RSARatio;
1332
+ this.nrrd_states.view.currentSliceIndex =
1003
1333
  this.protectedData.mainPreSlices.initIndex;
1004
1334
  }
1005
1335
  else {
1006
1336
  // !need to change
1007
1337
  // todo
1008
- this.protectedData.mainPreSlices.index = this.nrrd_states.oldIndex;
1338
+ this.protectedData.mainPreSlices.index = this.nrrd_states.view.preSliceIndex;
1009
1339
  }
1010
1340
  this.protectedData.canvases.originCanvas =
1011
1341
  this.protectedData.mainPreSlices.canvas;
@@ -1016,34 +1346,35 @@ export class NrrdTools extends DrawToolCore {
1016
1346
  this.setMainPreSlice();
1017
1347
  this.setOriginCanvasAndPre();
1018
1348
  this.protectedData.currentShowingSlice = this.protectedData.mainPreSlices;
1019
- this.nrrd_states.oldIndex =
1020
- this.protectedData.mainPreSlices.initIndex * this.nrrd_states.RSARatio;
1021
- this.nrrd_states.currentIndex = this.protectedData.mainPreSlices.initIndex;
1349
+ this.nrrd_states.view.preSliceIndex =
1350
+ this.protectedData.mainPreSlices.initIndex * this.nrrd_states.image.RSARatio;
1351
+ this.nrrd_states.view.currentSliceIndex = this.protectedData.mainPreSlices.initIndex;
1022
1352
  // Phase 6: Reset undo/redo stacks on new dataset load
1023
1353
  this.undoManager.clearAll();
1024
1354
  // compute max index
1025
1355
  this.updateMaxIndex();
1026
- this.dragOperator.updateShowNumDiv(this.nrrd_states.contrastNum);
1356
+ this.dragOperator.updateShowNumDiv(this.nrrd_states.view.contrastNum);
1357
+ this.syncGuiParameterSettings();
1027
1358
  this.initState = false;
1028
1359
  }
1029
1360
  updateMaxIndex() {
1030
1361
  if (this.protectedData.mainPreSlices) {
1031
- this.nrrd_states.maxIndex = this.protectedData.mainPreSlices.MaxIndex;
1362
+ this.nrrd_states.view.maxIndex = this.protectedData.mainPreSlices.MaxIndex;
1032
1363
  }
1033
1364
  }
1034
1365
  /**
1035
1366
  * Update the original canvas size, allow set to threejs load one (pixel distance not the mm).
1036
- * Then update the changedWidth and changedHeight based on the sizeFoctor.
1367
+ * Then update the changedWidth and changedHeight based on the sizeFactor.
1037
1368
  */
1038
1369
  updateOriginAndChangedWH() {
1039
- this.nrrd_states.originWidth =
1370
+ this.nrrd_states.image.originWidth =
1040
1371
  this.protectedData.canvases.originCanvas.width;
1041
- this.nrrd_states.originHeight =
1372
+ this.nrrd_states.image.originHeight =
1042
1373
  this.protectedData.canvases.originCanvas.height;
1043
1374
  // Let resizePaintArea be the sole setter of changedWidth/changedHeight.
1044
1375
  // Setting them here would defeat the sizeChanged detection in resizePaintArea,
1045
1376
  // causing canvas elements to keep stale dimensions after axis switches.
1046
- this.resizePaintArea(this.nrrd_states.sizeFoctor);
1377
+ this.resizePaintArea(this.nrrd_states.view.sizeFactor);
1047
1378
  this.resetPaintAreaUIPosition();
1048
1379
  }
1049
1380
  /**
@@ -1060,20 +1391,29 @@ export class NrrdTools extends DrawToolCore {
1060
1391
  appendLoadingbar(loadingbar) {
1061
1392
  this.protectedData.mainAreaContainer.appendChild(loadingbar);
1062
1393
  }
1063
- clearStoreImages() {
1394
+ /**
1395
+ * Clear all annotations on the currently active layer across its entire 3D volume.
1396
+ * This resets all voxels globally for the active layer's `MaskVolume` (depth, width, height)
1397
+ * and triggers the `onClearLayerVolume` event to sync the wiped volume to the backend.
1398
+ * It also clears the undo/redo stack for the active layer ONLY.
1399
+ * Other background layers are not impacted by this operation.
1400
+ */
1401
+ clearActiveLayer() {
1064
1402
  // Phase 3 Task 3.1: Only clear the active layer's MaskVolume
1065
- if (this.nrrd_states.dimensions.length === 3) {
1066
- const [w, h, d] = this.nrrd_states.dimensions;
1067
- const activeLayer = this.gui_states.layer;
1403
+ if (this.nrrd_states.image.dimensions.length === 3) {
1404
+ const [w, h, d] = this.nrrd_states.image.dimensions;
1405
+ const activeLayer = this.gui_states.layerChannel.layer;
1068
1406
  // Re-init only the active layer's MaskVolume
1069
1407
  this.protectedData.maskData.volumes[activeLayer] = new MaskVolume(w, h, d, 1);
1070
1408
  // Phase 6 Task 6.6: Clear undo/redo stack for this layer (volume too large to snapshot)
1071
1409
  this.undoManager.clearLayer(activeLayer);
1072
1410
  // Phase 3 Task 3.2: Notify external that this layer's volume was cleared
1073
- this.nrrd_states.onClearLayerVolume(activeLayer);
1411
+ this.annotationCallbacks.onLayerVolumeCleared(activeLayer);
1074
1412
  }
1075
1413
  // Invalidate reusable slice buffer
1076
1414
  this.invalidateSliceBuffer();
1415
+ // Reload all layers to canvas (restores other layers' visuals)
1416
+ this.reloadMasksFromVolume();
1077
1417
  }
1078
1418
  /**
1079
1419
  * Reset the draw and display canvases layout after mouse pan.
@@ -1107,6 +1447,59 @@ export class NrrdTools extends DrawToolCore {
1107
1447
  target.canvas.width = target.canvas.width;
1108
1448
  }
1109
1449
  }
1450
+ /**
1451
+ * Enter sphere mode.
1452
+ *
1453
+ * Clears all layer canvases and the master composite canvas so that
1454
+ * only the sphere overlay is visible. Does NOT touch MaskVolume data.
1455
+ * Also disables drag mode to prevent slice dragging conflicts.
1456
+ *
1457
+ * Called when sphere mode is toggled on (keyboard shortcut or GUI).
1458
+ */
1459
+ enterSphereMode() {
1460
+ var _a;
1461
+ // Disable left-click drag for slice navigation
1462
+ this.dragOperator.removeDragMode();
1463
+ // Tell EventRouter we're in sphere mode so Shift/draw is blocked
1464
+ (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool('sphere');
1465
+ // Clear all layer canvases (NOT MaskVolumes — just visual canvas)
1466
+ const w = this.nrrd_states.view.changedWidth;
1467
+ const h = this.nrrd_states.view.changedHeight;
1468
+ for (const layerId of this.nrrd_states.image.layers) {
1469
+ const target = this.protectedData.layerTargets.get(layerId);
1470
+ if (target) {
1471
+ target.ctx.clearRect(0, 0, target.canvas.width, target.canvas.height);
1472
+ }
1473
+ }
1474
+ // Clear master composite canvas
1475
+ this.protectedData.ctxes.drawingLayerMasterCtx.clearRect(0, 0, w, h);
1476
+ // Refresh sphere overlay from volume (shows existing sphere data on re-entry)
1477
+ this.refreshSphereOverlay();
1478
+ }
1479
+ /**
1480
+ * Exit sphere mode.
1481
+ *
1482
+ * Clears sphere overlay, restores all layer MaskVolume data onto
1483
+ * their canvases by temporarily lifting the sphere guard in
1484
+ * reloadMasksFromVolume and calling it.
1485
+ *
1486
+ * Called when sphere mode is toggled off (keyboard shortcut or GUI).
1487
+ */
1488
+ exitSphereMode() {
1489
+ var _a;
1490
+ // Restore left-click drag for slice navigation
1491
+ this.dragOperator.configDragMode();
1492
+ // Restore EventRouter guiTool to pencil (default drawing tool)
1493
+ (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool('pencil');
1494
+ // Clear sphere canvas overlay
1495
+ this.protectedData.ctxes.drawingSphereCtx.clearRect(0, 0, this.protectedData.canvases.drawingSphereCanvas.width, this.protectedData.canvases.drawingSphereCanvas.height);
1496
+ // resetLayerCanvas clears the layer canvas elements
1497
+ this.resetLayerCanvas();
1498
+ // Temporarily lift sphere guard so reloadMasksFromVolume can run.
1499
+ // gui_states.sphere is already set to false by the caller before
1500
+ // calling this method, so reloadMasksFromVolume will proceed normally.
1501
+ this.reloadMasksFromVolume();
1502
+ }
1110
1503
  redrawMianPreOnDisplayCanvas() {
1111
1504
  var _a;
1112
1505
  this.protectedData.canvases.displayCanvas.width =
@@ -1118,8 +1511,8 @@ export class NrrdTools extends DrawToolCore {
1118
1511
  if (this.protectedData.mainPreSlices) {
1119
1512
  this.protectedData.mainPreSlices.repaint.call(this.protectedData.mainPreSlices);
1120
1513
  this.flipDisplayImageByAxis();
1121
- (_a = this.protectedData.ctxes.displayCtx) === null || _a === void 0 ? void 0 : _a.drawImage(this.protectedData.canvases.originCanvas, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
1122
- this.resizePaintArea(this.nrrd_states.sizeFoctor);
1514
+ (_a = this.protectedData.ctxes.displayCtx) === null || _a === void 0 ? void 0 : _a.drawImage(this.protectedData.canvases.originCanvas, 0, 0, this.nrrd_states.view.changedWidth, this.nrrd_states.view.changedHeight);
1515
+ this.resizePaintArea(this.nrrd_states.view.sizeFactor);
1123
1516
  }
1124
1517
  }
1125
1518
  /**
@@ -1127,10 +1520,10 @@ export class NrrdTools extends DrawToolCore {
1127
1520
  * @param factor number
1128
1521
  */
1129
1522
  resizePaintArea(factor) {
1130
- const newWidth = Math.floor(this.nrrd_states.originWidth * factor);
1131
- const newHeight = Math.floor(this.nrrd_states.originHeight * factor);
1132
- const sizeChanged = newWidth !== this.nrrd_states.changedWidth ||
1133
- newHeight !== this.nrrd_states.changedHeight;
1523
+ const newWidth = Math.floor(this.nrrd_states.image.originWidth * factor);
1524
+ const newHeight = Math.floor(this.nrrd_states.image.originHeight * factor);
1525
+ const sizeChanged = newWidth !== this.nrrd_states.view.changedWidth ||
1526
+ newHeight !== this.nrrd_states.view.changedHeight;
1134
1527
  // Always clear display/drawing/origin canvases (needed for contrast updates)
1135
1528
  this.protectedData.canvases.originCanvas.width =
1136
1529
  this.protectedData.canvases.originCanvas.width;
@@ -1143,8 +1536,8 @@ export class NrrdTools extends DrawToolCore {
1143
1536
  // Skipping this avoids the expensive reloadMasksFromVolume() call
1144
1537
  // during contrast toggle (where size stays the same).
1145
1538
  this.resetLayerCanvas();
1146
- this.nrrd_states.changedWidth = newWidth;
1147
- this.nrrd_states.changedHeight = newHeight;
1539
+ this.nrrd_states.view.changedWidth = newWidth;
1540
+ this.nrrd_states.view.changedHeight = newHeight;
1148
1541
  this.protectedData.canvases.displayCanvas.width = newWidth;
1149
1542
  this.protectedData.canvases.displayCanvas.height = newHeight;
1150
1543
  this.protectedData.canvases.drawingCanvas.width = newWidth;
@@ -1166,18 +1559,25 @@ export class NrrdTools extends DrawToolCore {
1166
1559
  // valid data, just recomposite to master for the start() render loop.
1167
1560
  this.compositeAllLayers();
1168
1561
  }
1562
+ // Refresh sphere overlay from volume after resize/contrast change
1563
+ this.refreshSphereOverlay();
1169
1564
  }
1170
1565
  /**
1171
1566
  * Phase 3: Reload all mask layers from MaskVolume using buffer reuse
1172
1567
  * Replaces the old reloadMaskToLayer approach
1173
1568
  */
1174
1569
  reloadMasksFromVolume() {
1570
+ // When sphere mode is active, do NOT redraw layer masks.
1571
+ // Layer mask data should remain hidden until the user exits sphere mode.
1572
+ if (this.gui_states.mode.sphere) {
1573
+ return;
1574
+ }
1175
1575
  const axis = this.protectedData.axis;
1176
- let sliceIndex = this.nrrd_states.currentIndex;
1576
+ let sliceIndex = this.nrrd_states.view.currentSliceIndex;
1177
1577
  // Clamp sliceIndex to valid range for current axis
1178
- // (currentIndex may not be updated yet when switching axes)
1578
+ // (currentSliceIndex may not be updated yet when switching axes)
1179
1579
  try {
1180
- const vol = this.getVolumeForLayer(this.nrrd_states.layers[0]);
1580
+ const vol = this.getVolumeForLayer(this.nrrd_states.image.layers[0]);
1181
1581
  const dims = vol.getDimensions();
1182
1582
  const maxSlice = axis === "x" ? dims.width : axis === "y" ? dims.height : dims.depth;
1183
1583
  if (sliceIndex >= maxSlice)
@@ -1190,10 +1590,10 @@ export class NrrdTools extends DrawToolCore {
1190
1590
  const buffer = this.getOrCreateSliceBuffer(axis);
1191
1591
  if (!buffer)
1192
1592
  return;
1193
- const w = this.nrrd_states.changedWidth;
1194
- const h = this.nrrd_states.changedHeight;
1593
+ const w = this.nrrd_states.view.changedWidth;
1594
+ const h = this.nrrd_states.view.changedHeight;
1195
1595
  // Clear and render each layer using the shared buffer
1196
- for (const layerId of this.nrrd_states.layers) {
1596
+ for (const layerId of this.nrrd_states.image.layers) {
1197
1597
  const target = this.protectedData.layerTargets.get(layerId);
1198
1598
  if (!target)
1199
1599
  continue;
@@ -1214,15 +1614,15 @@ export class NrrdTools extends DrawToolCore {
1214
1614
  var _a, _b, _c, _d, _e, _f;
1215
1615
  if (this.protectedData.axis === "x") {
1216
1616
  (_a = this.protectedData.ctxes.displayCtx) === null || _a === void 0 ? void 0 : _a.scale(-1, -1);
1217
- (_b = this.protectedData.ctxes.displayCtx) === null || _b === void 0 ? void 0 : _b.translate(-this.nrrd_states.changedWidth, -this.nrrd_states.changedHeight);
1617
+ (_b = this.protectedData.ctxes.displayCtx) === null || _b === void 0 ? void 0 : _b.translate(-this.nrrd_states.view.changedWidth, -this.nrrd_states.view.changedHeight);
1218
1618
  }
1219
1619
  else if (this.protectedData.axis === "z") {
1220
1620
  (_c = this.protectedData.ctxes.displayCtx) === null || _c === void 0 ? void 0 : _c.scale(1, -1);
1221
- (_d = this.protectedData.ctxes.displayCtx) === null || _d === void 0 ? void 0 : _d.translate(0, -this.nrrd_states.changedHeight);
1621
+ (_d = this.protectedData.ctxes.displayCtx) === null || _d === void 0 ? void 0 : _d.translate(0, -this.nrrd_states.view.changedHeight);
1222
1622
  }
1223
1623
  else if (this.protectedData.axis === "y") {
1224
1624
  (_e = this.protectedData.ctxes.displayCtx) === null || _e === void 0 ? void 0 : _e.scale(1, -1);
1225
- (_f = this.protectedData.ctxes.displayCtx) === null || _f === void 0 ? void 0 : _f.translate(0, -this.nrrd_states.changedHeight);
1625
+ (_f = this.protectedData.ctxes.displayCtx) === null || _f === void 0 ? void 0 : _f.translate(0, -this.nrrd_states.view.changedHeight);
1226
1626
  }
1227
1627
  }
1228
1628
  clearDictionary(dic) {
@@ -1239,21 +1639,21 @@ export class NrrdTools extends DrawToolCore {
1239
1639
  switch (!!axis ? axis : this.protectedData.axis) {
1240
1640
  case "x":
1241
1641
  this.protectedData.canvases.emptyCanvas.width =
1242
- this.nrrd_states.nrrd_z_pixel;
1642
+ this.nrrd_states.image.nrrd_z_pixel;
1243
1643
  this.protectedData.canvases.emptyCanvas.height =
1244
- this.nrrd_states.nrrd_y_pixel;
1644
+ this.nrrd_states.image.nrrd_y_pixel;
1245
1645
  break;
1246
1646
  case "y":
1247
1647
  this.protectedData.canvases.emptyCanvas.width =
1248
- this.nrrd_states.nrrd_x_pixel;
1648
+ this.nrrd_states.image.nrrd_x_pixel;
1249
1649
  this.protectedData.canvases.emptyCanvas.height =
1250
- this.nrrd_states.nrrd_z_pixel;
1650
+ this.nrrd_states.image.nrrd_z_pixel;
1251
1651
  break;
1252
1652
  case "z":
1253
1653
  this.protectedData.canvases.emptyCanvas.width =
1254
- this.nrrd_states.nrrd_x_pixel;
1654
+ this.nrrd_states.image.nrrd_x_pixel;
1255
1655
  this.protectedData.canvases.emptyCanvas.height =
1256
- this.nrrd_states.nrrd_y_pixel;
1656
+ this.nrrd_states.image.nrrd_y_pixel;
1257
1657
  break;
1258
1658
  }
1259
1659
  }
@@ -1275,7 +1675,7 @@ export class NrrdTools extends DrawToolCore {
1275
1675
  this.protectedData.currentShowingSlice.repaint.call(this.protectedData.currentShowingSlice);
1276
1676
  (_a = this.protectedData.ctxes.displayCtx) === null || _a === void 0 ? void 0 : _a.save();
1277
1677
  this.flipDisplayImageByAxis();
1278
- (_b = this.protectedData.ctxes.displayCtx) === null || _b === void 0 ? void 0 : _b.drawImage(this.protectedData.currentShowingSlice.canvas, 0, 0, this.nrrd_states.changedWidth, this.nrrd_states.changedHeight);
1678
+ (_b = this.protectedData.ctxes.displayCtx) === null || _b === void 0 ? void 0 : _b.drawImage(this.protectedData.currentShowingSlice.canvas, 0, 0, this.nrrd_states.view.changedWidth, this.nrrd_states.view.changedHeight);
1279
1679
  (_c = this.protectedData.ctxes.displayCtx) === null || _c === void 0 ? void 0 : _c.restore();
1280
1680
  }
1281
1681
  }
@@ -1284,7 +1684,8 @@ export class NrrdTools extends DrawToolCore {
1284
1684
  */
1285
1685
  configMouseSliceWheel() {
1286
1686
  const handleMouseZoomSliceWheelMove = (e) => {
1287
- if (this.protectedData.Is_Shift_Pressed) {
1687
+ var _a;
1688
+ if ((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
1288
1689
  return;
1289
1690
  }
1290
1691
  e.preventDefault();