copper3d 3.0.1 → 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 +24 -9
  3. package/dist/Utils/segmentation/CommToolsData.js +65 -139
  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 +14 -4
  7. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  8. package/dist/Utils/segmentation/DrawToolCore.d.ts +23 -16
  9. package/dist/Utils/segmentation/DrawToolCore.js +218 -401
  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 +599 -202
  13. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  14. package/dist/Utils/segmentation/core/MaskVolume.d.ts +1 -1
  15. package/dist/Utils/segmentation/core/MaskVolume.js +24 -3
  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 -62
  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 -12
  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 -1
  38. package/dist/Utils/segmentation/eventRouter/EventRouter.js +15 -10
  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 +8 -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 +11 -28
  55. package/dist/Utils/segmentation/tools/ImageStoreHelper.js +39 -82
  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 -1
  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 +3 -3
  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 +2452 -1492
  69. package/dist/bundle.umd.js +2452 -1492
  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 +24 -9
  74. package/dist/types/Utils/segmentation/DragOperator.d.ts +10 -2
  75. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +23 -16
  76. package/dist/types/Utils/segmentation/NrrdTools.d.ts +152 -10
  77. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +1 -1
  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 -62
  83. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +12 -12
  84. package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +2 -1
  85. package/dist/types/Utils/segmentation/eventRouter/types.d.ts +3 -3
  86. package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +8 -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 +11 -28
  90. package/dist/types/Utils/segmentation/tools/PanTool.d.ts +53 -0
  91. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +208 -1
  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,31 +943,69 @@ 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
@@ -690,24 +1014,24 @@ export class NrrdTools extends DrawToolCore {
690
1014
  setSliceOrientation(axisTo) {
691
1015
  var _a;
692
1016
  let convetObj;
693
- if (((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isCrosshairEnabled()) || this.gui_states.sphere) {
1017
+ if (((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isCrosshairEnabled()) || this.gui_states.mode.sphere) {
694
1018
  if (this.protectedData.axis === "z") {
695
- this.cursorPage.z.index = this.nrrd_states.currentIndex;
696
- this.cursorPage.z.cursorPageX = this.nrrd_states.cursorPageX;
697
- 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;
698
1022
  }
699
1023
  else if (this.protectedData.axis === "x") {
700
- this.cursorPage.x.index = this.nrrd_states.currentIndex;
701
- this.cursorPage.x.cursorPageX = this.nrrd_states.cursorPageX;
702
- 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;
703
1027
  }
704
1028
  else if (this.protectedData.axis === "y") {
705
- this.cursorPage.y.index = this.nrrd_states.currentIndex;
706
- this.cursorPage.y.cursorPageX = this.nrrd_states.cursorPageX;
707
- 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;
708
1032
  }
709
1033
  if (axisTo === "z") {
710
- if (this.nrrd_states.isCursorSelect && !this.cursorPage.z.updated) {
1034
+ if (this.nrrd_states.interaction.isCursorSelect && !this.cursorPage.z.updated) {
711
1035
  if (this.protectedData.axis === "x") {
712
1036
  // convert x to z
713
1037
  convetObj = this.convertCursorPoint("x", "z", this.cursorPage.x.cursorPageX, this.cursorPage.x.cursorPageY, this.cursorPage.x.index);
@@ -719,15 +1043,15 @@ export class NrrdTools extends DrawToolCore {
719
1043
  }
720
1044
  else {
721
1045
  // not cursor select, freedom to switch x -> z or y -> z and z -> x or z -> y
722
- this.nrrd_states.currentIndex = this.cursorPage.z.index;
723
- this.nrrd_states.oldIndex =
724
- this.cursorPage.z.index * this.nrrd_states.ratios.z;
725
- this.nrrd_states.cursorPageX = this.cursorPage.z.cursorPageX;
726
- 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;
727
1051
  }
728
1052
  }
729
1053
  else if (axisTo === "x") {
730
- if (this.nrrd_states.isCursorSelect && !this.cursorPage.x.updated) {
1054
+ if (this.nrrd_states.interaction.isCursorSelect && !this.cursorPage.x.updated) {
731
1055
  if (this.protectedData.axis === "z") {
732
1056
  // convert z to x
733
1057
  convetObj = this.convertCursorPoint("z", "x", this.cursorPage.z.cursorPageX, this.cursorPage.z.cursorPageY, this.cursorPage.z.index);
@@ -739,15 +1063,15 @@ export class NrrdTools extends DrawToolCore {
739
1063
  }
740
1064
  else {
741
1065
  // not cursor select, freedom to switch z -> x or y -> x and x -> z or x -> y
742
- this.nrrd_states.currentIndex = this.cursorPage.x.index;
743
- this.nrrd_states.oldIndex =
744
- this.cursorPage.x.index * this.nrrd_states.ratios.x;
745
- this.nrrd_states.cursorPageX = this.cursorPage.x.cursorPageX;
746
- 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;
747
1071
  }
748
1072
  }
749
1073
  else if (axisTo === "y") {
750
- if (this.nrrd_states.isCursorSelect && !this.cursorPage.y.updated) {
1074
+ if (this.nrrd_states.interaction.isCursorSelect && !this.cursorPage.y.updated) {
751
1075
  if (this.protectedData.axis === "z") {
752
1076
  // convert z to y
753
1077
  convetObj = this.convertCursorPoint("z", "y", this.cursorPage.z.cursorPageX, this.cursorPage.z.cursorPageY, this.cursorPage.z.index);
@@ -759,19 +1083,19 @@ export class NrrdTools extends DrawToolCore {
759
1083
  }
760
1084
  else {
761
1085
  // not cursor select, freedom to switch z -> y or x -> y and y -> z or y -> x
762
- this.nrrd_states.currentIndex = this.cursorPage.y.index;
763
- this.nrrd_states.oldIndex =
764
- this.cursorPage.y.index * this.nrrd_states.ratios.y;
765
- this.nrrd_states.cursorPageX = this.cursorPage.y.cursorPageX;
766
- 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;
767
1091
  }
768
1092
  }
769
1093
  if (convetObj) {
770
1094
  // update convert cursor point, when cursor select
771
- this.nrrd_states.currentIndex = convetObj.currentIndex;
772
- this.nrrd_states.oldIndex = convetObj.oldIndex;
773
- this.nrrd_states.cursorPageX = convetObj.convertCursorNumX;
774
- 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;
775
1099
  convetObj = undefined;
776
1100
  switch (axisTo) {
777
1101
  case "x":
@@ -789,47 +1113,52 @@ export class NrrdTools extends DrawToolCore {
789
1113
  this.cursorPage.y.updated &&
790
1114
  this.cursorPage.z.updated) {
791
1115
  // one point convert to all axis, reset all updated status
792
- this.nrrd_states.isCursorSelect = false;
1116
+ this.nrrd_states.interaction.isCursorSelect = false;
793
1117
  }
794
1118
  }
795
1119
  this.protectedData.axis = axisTo;
796
1120
  this.resetDisplaySlicesStatus();
797
- // for sphere plan a
798
- if (this.gui_states.sphere && !this.nrrd_states.spherePlanB) {
799
- 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);
800
- }
801
1121
  }
802
1122
  addSkip(index) {
803
1123
  this.protectedData.skipSlicesDic[index] =
804
1124
  this.protectedData.backUpDisplaySlices[index];
805
1125
  if (index >= this.protectedData.displaySlices.length) {
806
- this.nrrd_states.contrastNum = this.protectedData.displaySlices.length;
1126
+ this.nrrd_states.view.contrastNum = this.protectedData.displaySlices.length;
807
1127
  }
808
1128
  else {
809
- this.nrrd_states.contrastNum = index;
1129
+ this.nrrd_states.view.contrastNum = index;
810
1130
  }
811
1131
  this.resetDisplaySlicesStatus();
812
1132
  }
813
1133
  removeSkip(index) {
814
1134
  this.protectedData.skipSlicesDic[index] = undefined;
815
- this.nrrd_states.contrastNum = 0;
1135
+ this.nrrd_states.view.contrastNum = 0;
816
1136
  this.resetDisplaySlicesStatus();
817
1137
  }
818
- 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() {
819
1146
  // To effectively reduce the js memory garbage
820
1147
  this.protectedData.allSlicesArray.length = 0;
821
1148
  this.protectedData.displaySlices.length = 0;
822
1149
  // Phase 6: Clear all undo/redo stacks
823
1150
  this.undoManager.clearAll();
824
1151
  // Phase 3: Reset MaskVolume storage to 1×1×1 placeholders
825
- this.protectedData.maskData.volumes = this.nrrd_states.layers.reduce((acc, id) => {
1152
+ this.protectedData.maskData.volumes = this.nrrd_states.image.layers.reduce((acc, id) => {
826
1153
  acc[id] = new MaskVolume(1, 1, 1, 1);
827
1154
  return acc;
828
1155
  }, {});
1156
+ // Clear dedicated SphereMaskVolume
1157
+ this.nrrd_states.sphere.sphereMaskVolume = null;
829
1158
  // Invalidate reusable slice buffer
830
1159
  this.invalidateSliceBuffer();
831
1160
  this.clearDictionary(this.protectedData.skipSlicesDic);
832
- // this.nrrd_states.previousPanelL = this.nrrd_states.previousPanelT = -99999;
1161
+ // this.nrrd_states.view.previousPanelL = this.nrrd_states.view.previousPanelT = -99999;
833
1162
  this.protectedData.canvases.displayCanvas.style.left =
834
1163
  this.protectedData.canvases.drawingCanvas.style.left = "";
835
1164
  this.protectedData.canvases.displayCanvas.style.top =
@@ -837,21 +1166,19 @@ export class NrrdTools extends DrawToolCore {
837
1166
  this.protectedData.backUpDisplaySlices.length = 0;
838
1167
  this.protectedData.mainPreSlices = undefined;
839
1168
  this.protectedData.currentShowingSlice = undefined;
840
- this.protectedData.previousDrawingImage =
841
- this.protectedData.ctxes.emptyCtx.createImageData(1, 1);
842
1169
  this.initState = true;
843
1170
  this.protectedData.axis = "z";
844
- this.nrrd_states.sizeFoctor = this.baseCanvasesSize;
845
- this.gui_states.mainAreaSize = this.baseCanvasesSize;
1171
+ this.nrrd_states.view.sizeFactor = this.baseCanvasesSize;
1172
+ this.gui_states.viewConfig.mainAreaSize = this.baseCanvasesSize;
846
1173
  this.resetLayerCanvas();
847
1174
  this.protectedData.canvases.drawingCanvas.width =
848
1175
  this.protectedData.canvases.drawingCanvas.width;
849
1176
  this.protectedData.canvases.displayCanvas.width =
850
1177
  this.protectedData.canvases.displayCanvas.width;
851
- this.nrrd_states.tumourSphereOrigin = null;
852
- this.nrrd_states.ribSphereOrigin = null;
853
- this.nrrd_states.skinSphereOrigin = null;
854
- 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;
855
1182
  }
856
1183
  setSliceMoving(step) {
857
1184
  if (this.protectedData.mainPreSlices) {
@@ -864,7 +1191,7 @@ export class NrrdTools extends DrawToolCore {
864
1191
  this._sliceRAFId = null;
865
1192
  const totalStep = this._pendingSliceStep;
866
1193
  this._pendingSliceStep = 0;
867
- this.protectedData.Is_Draw = true;
1194
+ this.protectedData.isDrawing = true;
868
1195
  this.setSyncsliceNum();
869
1196
  this.dragOperator.updateIndex(totalStep);
870
1197
  this.setIsDrawFalse(1000);
@@ -872,14 +1199,14 @@ export class NrrdTools extends DrawToolCore {
872
1199
  }
873
1200
  }
874
1201
  setMainAreaSize(factor) {
875
- this.nrrd_states.sizeFoctor = factor;
876
- if (this.nrrd_states.sizeFoctor >= 8) {
877
- 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;
878
1205
  }
879
- else if (this.nrrd_states.sizeFoctor <= 1) {
880
- this.nrrd_states.sizeFoctor = 1;
1206
+ else if (this.nrrd_states.view.sizeFactor <= 1) {
1207
+ this.nrrd_states.view.sizeFactor = 1;
881
1208
  }
882
- this.resizePaintArea(this.nrrd_states.sizeFoctor);
1209
+ this.resizePaintArea(this.nrrd_states.view.sizeFactor);
883
1210
  this.resetPaintAreaUIPosition();
884
1211
  // this.setIsDrawFalse(1000);
885
1212
  }
@@ -893,27 +1220,27 @@ export class NrrdTools extends DrawToolCore {
893
1220
  return this.nrrd_states;
894
1221
  }
895
1222
  getMaxSliceNum() {
896
- if (this.nrrd_states.showContrast) {
1223
+ if (this.nrrd_states.view.showContrast) {
897
1224
  return [
898
- this.nrrd_states.maxIndex,
899
- this.nrrd_states.maxIndex * this.protectedData.displaySlices.length,
1225
+ this.nrrd_states.view.maxIndex,
1226
+ this.nrrd_states.view.maxIndex * this.protectedData.displaySlices.length,
900
1227
  ];
901
1228
  }
902
1229
  else {
903
- return [this.nrrd_states.maxIndex];
1230
+ return [this.nrrd_states.view.maxIndex];
904
1231
  }
905
1232
  }
906
1233
  getCurrentSlicesNumAndContrastNum() {
907
1234
  return {
908
- currentIndex: this.nrrd_states.currentIndex,
909
- contrastIndex: this.nrrd_states.contrastNum,
1235
+ currentSliceIndex: this.nrrd_states.view.currentSliceIndex,
1236
+ contrastIndex: this.nrrd_states.view.contrastNum,
910
1237
  };
911
1238
  }
912
1239
  getCurrentSliceIndex() {
913
- return Math.ceil(this.protectedData.mainPreSlices.index / this.nrrd_states.RSARatio);
1240
+ return Math.ceil(this.protectedData.mainPreSlices.index / this.nrrd_states.image.RSARatio);
914
1241
  }
915
1242
  getIsShowContrastState() {
916
- return this.nrrd_states.showContrast;
1243
+ return this.nrrd_states.view.showContrast;
917
1244
  }
918
1245
  /**
919
1246
  * Give a delay time to finish the last drawing before upcoming interrupt opreations.
@@ -923,7 +1250,7 @@ export class NrrdTools extends DrawToolCore {
923
1250
  */
924
1251
  setIsDrawFalse(target) {
925
1252
  this.preTimer = setTimeout(() => {
926
- this.protectedData.Is_Draw = false;
1253
+ this.protectedData.isDrawing = false;
927
1254
  if (this.preTimer) {
928
1255
  window.clearTimeout(this.preTimer);
929
1256
  this.preTimer = undefined;
@@ -978,35 +1305,37 @@ export class NrrdTools extends DrawToolCore {
978
1305
  // (also calls resizePaintArea → reloads masks, resizes canvases)
979
1306
  this.setOriginCanvasAndPre();
980
1307
  // update the show number div on top area
981
- this.dragOperator.updateShowNumDiv(this.nrrd_states.contrastNum);
1308
+ this.dragOperator.updateShowNumDiv(this.nrrd_states.view.contrastNum);
982
1309
  // repaint all contrast images
983
1310
  this.repraintCurrentContrastSlice();
984
1311
  // Refresh display after contrast repaint (no need for full resizePaintArea
985
1312
  // since canvases were already resized in setOriginCanvasAndPre above)
986
1313
  this.redrawDisplayCanvas();
987
1314
  this.compositeAllLayers();
1315
+ // Sync slider metadata with current volume (replaces old getGuiSettings() side-effect)
1316
+ this.syncGuiParameterSettings();
988
1317
  }
989
1318
  setMainPreSlice() {
990
1319
  this.protectedData.mainPreSlices = this.protectedData.displaySlices[0];
991
1320
  if (this.protectedData.mainPreSlices) {
992
- this.nrrd_states.RSARatio = this.protectedData.mainPreSlices.RSARatio;
1321
+ this.nrrd_states.image.RSARatio = this.protectedData.mainPreSlices.RSARatio;
993
1322
  }
994
1323
  }
995
1324
  setOriginCanvasAndPre() {
996
1325
  if (this.protectedData.mainPreSlices) {
997
- if (this.nrrd_states.oldIndex > this.nrrd_states.maxIndex)
998
- 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;
999
1328
  if (this.initState) {
1000
- this.nrrd_states.oldIndex =
1329
+ this.nrrd_states.view.preSliceIndex =
1001
1330
  this.protectedData.mainPreSlices.initIndex *
1002
- this.nrrd_states.RSARatio;
1003
- this.nrrd_states.currentIndex =
1331
+ this.nrrd_states.image.RSARatio;
1332
+ this.nrrd_states.view.currentSliceIndex =
1004
1333
  this.protectedData.mainPreSlices.initIndex;
1005
1334
  }
1006
1335
  else {
1007
1336
  // !need to change
1008
1337
  // todo
1009
- this.protectedData.mainPreSlices.index = this.nrrd_states.oldIndex;
1338
+ this.protectedData.mainPreSlices.index = this.nrrd_states.view.preSliceIndex;
1010
1339
  }
1011
1340
  this.protectedData.canvases.originCanvas =
1012
1341
  this.protectedData.mainPreSlices.canvas;
@@ -1017,34 +1346,35 @@ export class NrrdTools extends DrawToolCore {
1017
1346
  this.setMainPreSlice();
1018
1347
  this.setOriginCanvasAndPre();
1019
1348
  this.protectedData.currentShowingSlice = this.protectedData.mainPreSlices;
1020
- this.nrrd_states.oldIndex =
1021
- this.protectedData.mainPreSlices.initIndex * this.nrrd_states.RSARatio;
1022
- 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;
1023
1352
  // Phase 6: Reset undo/redo stacks on new dataset load
1024
1353
  this.undoManager.clearAll();
1025
1354
  // compute max index
1026
1355
  this.updateMaxIndex();
1027
- this.dragOperator.updateShowNumDiv(this.nrrd_states.contrastNum);
1356
+ this.dragOperator.updateShowNumDiv(this.nrrd_states.view.contrastNum);
1357
+ this.syncGuiParameterSettings();
1028
1358
  this.initState = false;
1029
1359
  }
1030
1360
  updateMaxIndex() {
1031
1361
  if (this.protectedData.mainPreSlices) {
1032
- this.nrrd_states.maxIndex = this.protectedData.mainPreSlices.MaxIndex;
1362
+ this.nrrd_states.view.maxIndex = this.protectedData.mainPreSlices.MaxIndex;
1033
1363
  }
1034
1364
  }
1035
1365
  /**
1036
1366
  * Update the original canvas size, allow set to threejs load one (pixel distance not the mm).
1037
- * Then update the changedWidth and changedHeight based on the sizeFoctor.
1367
+ * Then update the changedWidth and changedHeight based on the sizeFactor.
1038
1368
  */
1039
1369
  updateOriginAndChangedWH() {
1040
- this.nrrd_states.originWidth =
1370
+ this.nrrd_states.image.originWidth =
1041
1371
  this.protectedData.canvases.originCanvas.width;
1042
- this.nrrd_states.originHeight =
1372
+ this.nrrd_states.image.originHeight =
1043
1373
  this.protectedData.canvases.originCanvas.height;
1044
1374
  // Let resizePaintArea be the sole setter of changedWidth/changedHeight.
1045
1375
  // Setting them here would defeat the sizeChanged detection in resizePaintArea,
1046
1376
  // causing canvas elements to keep stale dimensions after axis switches.
1047
- this.resizePaintArea(this.nrrd_states.sizeFoctor);
1377
+ this.resizePaintArea(this.nrrd_states.view.sizeFactor);
1048
1378
  this.resetPaintAreaUIPosition();
1049
1379
  }
1050
1380
  /**
@@ -1061,17 +1391,24 @@ export class NrrdTools extends DrawToolCore {
1061
1391
  appendLoadingbar(loadingbar) {
1062
1392
  this.protectedData.mainAreaContainer.appendChild(loadingbar);
1063
1393
  }
1064
- 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() {
1065
1402
  // Phase 3 Task 3.1: Only clear the active layer's MaskVolume
1066
- if (this.nrrd_states.dimensions.length === 3) {
1067
- const [w, h, d] = this.nrrd_states.dimensions;
1068
- 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;
1069
1406
  // Re-init only the active layer's MaskVolume
1070
1407
  this.protectedData.maskData.volumes[activeLayer] = new MaskVolume(w, h, d, 1);
1071
1408
  // Phase 6 Task 6.6: Clear undo/redo stack for this layer (volume too large to snapshot)
1072
1409
  this.undoManager.clearLayer(activeLayer);
1073
1410
  // Phase 3 Task 3.2: Notify external that this layer's volume was cleared
1074
- this.nrrd_states.onClearLayerVolume(activeLayer);
1411
+ this.annotationCallbacks.onLayerVolumeCleared(activeLayer);
1075
1412
  }
1076
1413
  // Invalidate reusable slice buffer
1077
1414
  this.invalidateSliceBuffer();
@@ -1110,6 +1447,59 @@ export class NrrdTools extends DrawToolCore {
1110
1447
  target.canvas.width = target.canvas.width;
1111
1448
  }
1112
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
+ }
1113
1503
  redrawMianPreOnDisplayCanvas() {
1114
1504
  var _a;
1115
1505
  this.protectedData.canvases.displayCanvas.width =
@@ -1121,8 +1511,8 @@ export class NrrdTools extends DrawToolCore {
1121
1511
  if (this.protectedData.mainPreSlices) {
1122
1512
  this.protectedData.mainPreSlices.repaint.call(this.protectedData.mainPreSlices);
1123
1513
  this.flipDisplayImageByAxis();
1124
- (_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);
1125
- 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);
1126
1516
  }
1127
1517
  }
1128
1518
  /**
@@ -1130,10 +1520,10 @@ export class NrrdTools extends DrawToolCore {
1130
1520
  * @param factor number
1131
1521
  */
1132
1522
  resizePaintArea(factor) {
1133
- const newWidth = Math.floor(this.nrrd_states.originWidth * factor);
1134
- const newHeight = Math.floor(this.nrrd_states.originHeight * factor);
1135
- const sizeChanged = newWidth !== this.nrrd_states.changedWidth ||
1136
- 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;
1137
1527
  // Always clear display/drawing/origin canvases (needed for contrast updates)
1138
1528
  this.protectedData.canvases.originCanvas.width =
1139
1529
  this.protectedData.canvases.originCanvas.width;
@@ -1146,8 +1536,8 @@ export class NrrdTools extends DrawToolCore {
1146
1536
  // Skipping this avoids the expensive reloadMasksFromVolume() call
1147
1537
  // during contrast toggle (where size stays the same).
1148
1538
  this.resetLayerCanvas();
1149
- this.nrrd_states.changedWidth = newWidth;
1150
- this.nrrd_states.changedHeight = newHeight;
1539
+ this.nrrd_states.view.changedWidth = newWidth;
1540
+ this.nrrd_states.view.changedHeight = newHeight;
1151
1541
  this.protectedData.canvases.displayCanvas.width = newWidth;
1152
1542
  this.protectedData.canvases.displayCanvas.height = newHeight;
1153
1543
  this.protectedData.canvases.drawingCanvas.width = newWidth;
@@ -1169,18 +1559,25 @@ export class NrrdTools extends DrawToolCore {
1169
1559
  // valid data, just recomposite to master for the start() render loop.
1170
1560
  this.compositeAllLayers();
1171
1561
  }
1562
+ // Refresh sphere overlay from volume after resize/contrast change
1563
+ this.refreshSphereOverlay();
1172
1564
  }
1173
1565
  /**
1174
1566
  * Phase 3: Reload all mask layers from MaskVolume using buffer reuse
1175
1567
  * Replaces the old reloadMaskToLayer approach
1176
1568
  */
1177
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
+ }
1178
1575
  const axis = this.protectedData.axis;
1179
- let sliceIndex = this.nrrd_states.currentIndex;
1576
+ let sliceIndex = this.nrrd_states.view.currentSliceIndex;
1180
1577
  // Clamp sliceIndex to valid range for current axis
1181
- // (currentIndex may not be updated yet when switching axes)
1578
+ // (currentSliceIndex may not be updated yet when switching axes)
1182
1579
  try {
1183
- const vol = this.getVolumeForLayer(this.nrrd_states.layers[0]);
1580
+ const vol = this.getVolumeForLayer(this.nrrd_states.image.layers[0]);
1184
1581
  const dims = vol.getDimensions();
1185
1582
  const maxSlice = axis === "x" ? dims.width : axis === "y" ? dims.height : dims.depth;
1186
1583
  if (sliceIndex >= maxSlice)
@@ -1193,10 +1590,10 @@ export class NrrdTools extends DrawToolCore {
1193
1590
  const buffer = this.getOrCreateSliceBuffer(axis);
1194
1591
  if (!buffer)
1195
1592
  return;
1196
- const w = this.nrrd_states.changedWidth;
1197
- const h = this.nrrd_states.changedHeight;
1593
+ const w = this.nrrd_states.view.changedWidth;
1594
+ const h = this.nrrd_states.view.changedHeight;
1198
1595
  // Clear and render each layer using the shared buffer
1199
- for (const layerId of this.nrrd_states.layers) {
1596
+ for (const layerId of this.nrrd_states.image.layers) {
1200
1597
  const target = this.protectedData.layerTargets.get(layerId);
1201
1598
  if (!target)
1202
1599
  continue;
@@ -1217,15 +1614,15 @@ export class NrrdTools extends DrawToolCore {
1217
1614
  var _a, _b, _c, _d, _e, _f;
1218
1615
  if (this.protectedData.axis === "x") {
1219
1616
  (_a = this.protectedData.ctxes.displayCtx) === null || _a === void 0 ? void 0 : _a.scale(-1, -1);
1220
- (_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);
1221
1618
  }
1222
1619
  else if (this.protectedData.axis === "z") {
1223
1620
  (_c = this.protectedData.ctxes.displayCtx) === null || _c === void 0 ? void 0 : _c.scale(1, -1);
1224
- (_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);
1225
1622
  }
1226
1623
  else if (this.protectedData.axis === "y") {
1227
1624
  (_e = this.protectedData.ctxes.displayCtx) === null || _e === void 0 ? void 0 : _e.scale(1, -1);
1228
- (_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);
1229
1626
  }
1230
1627
  }
1231
1628
  clearDictionary(dic) {
@@ -1242,21 +1639,21 @@ export class NrrdTools extends DrawToolCore {
1242
1639
  switch (!!axis ? axis : this.protectedData.axis) {
1243
1640
  case "x":
1244
1641
  this.protectedData.canvases.emptyCanvas.width =
1245
- this.nrrd_states.nrrd_z_pixel;
1642
+ this.nrrd_states.image.nrrd_z_pixel;
1246
1643
  this.protectedData.canvases.emptyCanvas.height =
1247
- this.nrrd_states.nrrd_y_pixel;
1644
+ this.nrrd_states.image.nrrd_y_pixel;
1248
1645
  break;
1249
1646
  case "y":
1250
1647
  this.protectedData.canvases.emptyCanvas.width =
1251
- this.nrrd_states.nrrd_x_pixel;
1648
+ this.nrrd_states.image.nrrd_x_pixel;
1252
1649
  this.protectedData.canvases.emptyCanvas.height =
1253
- this.nrrd_states.nrrd_z_pixel;
1650
+ this.nrrd_states.image.nrrd_z_pixel;
1254
1651
  break;
1255
1652
  case "z":
1256
1653
  this.protectedData.canvases.emptyCanvas.width =
1257
- this.nrrd_states.nrrd_x_pixel;
1654
+ this.nrrd_states.image.nrrd_x_pixel;
1258
1655
  this.protectedData.canvases.emptyCanvas.height =
1259
- this.nrrd_states.nrrd_y_pixel;
1656
+ this.nrrd_states.image.nrrd_y_pixel;
1260
1657
  break;
1261
1658
  }
1262
1659
  }
@@ -1278,7 +1675,7 @@ export class NrrdTools extends DrawToolCore {
1278
1675
  this.protectedData.currentShowingSlice.repaint.call(this.protectedData.currentShowingSlice);
1279
1676
  (_a = this.protectedData.ctxes.displayCtx) === null || _a === void 0 ? void 0 : _a.save();
1280
1677
  this.flipDisplayImageByAxis();
1281
- (_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);
1282
1679
  (_c = this.protectedData.ctxes.displayCtx) === null || _c === void 0 ? void 0 : _c.restore();
1283
1680
  }
1284
1681
  }