copper3d 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +183 -1028
  2. package/dist/Utils/segmentation/CanvasState.d.ts +31 -0
  3. package/dist/Utils/segmentation/CanvasState.js +167 -0
  4. package/dist/Utils/segmentation/CanvasState.js.map +1 -0
  5. package/dist/Utils/segmentation/DragOperator.d.ts +1 -1
  6. package/dist/Utils/segmentation/DragOperator.js +2 -4
  7. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  8. package/dist/Utils/segmentation/DrawToolCore.d.ts +49 -27
  9. package/dist/Utils/segmentation/DrawToolCore.js +275 -344
  10. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  11. package/dist/Utils/segmentation/NrrdTools.d.ts +89 -467
  12. package/dist/Utils/segmentation/NrrdTools.js +479 -1335
  13. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  14. package/dist/Utils/segmentation/RenderingUtils.d.ts +73 -0
  15. package/dist/Utils/segmentation/RenderingUtils.js +180 -0
  16. package/dist/Utils/segmentation/RenderingUtils.js.map +1 -0
  17. package/dist/Utils/segmentation/core/MaskVolume.d.ts +1 -1
  18. package/dist/Utils/segmentation/core/MaskVolume.js +1 -1
  19. package/dist/Utils/segmentation/core/index.d.ts +1 -1
  20. package/dist/Utils/segmentation/core/index.js.map +1 -1
  21. package/dist/Utils/segmentation/core/types.d.ts +384 -0
  22. package/dist/Utils/segmentation/core/types.js +1 -0
  23. package/dist/Utils/segmentation/core/types.js.map +1 -1
  24. package/dist/Utils/segmentation/coreTools/GuiState.d.ts +1 -1
  25. package/dist/Utils/segmentation/coreTools/NrrdState.d.ts +1 -1
  26. package/dist/Utils/segmentation/coreTools/divControlTools.d.ts +1 -1
  27. package/dist/Utils/segmentation/coreTools/gui.d.ts +1 -1
  28. package/dist/Utils/segmentation/tools/BaseTool.d.ts +1 -1
  29. package/dist/Utils/segmentation/tools/ContrastTool.d.ts +3 -6
  30. package/dist/Utils/segmentation/tools/ContrastTool.js.map +1 -1
  31. package/dist/Utils/segmentation/tools/CrosshairTool.d.ts +11 -1
  32. package/dist/Utils/segmentation/tools/CrosshairTool.js +25 -0
  33. package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -1
  34. package/dist/Utils/segmentation/tools/DataLoader.d.ts +33 -0
  35. package/dist/Utils/segmentation/tools/DataLoader.js +159 -0
  36. package/dist/Utils/segmentation/tools/DataLoader.js.map +1 -0
  37. package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +3 -12
  38. package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -1
  39. package/dist/Utils/segmentation/tools/DrawingTool.d.ts +18 -26
  40. package/dist/Utils/segmentation/tools/DrawingTool.js +48 -0
  41. package/dist/Utils/segmentation/tools/DrawingTool.js.map +1 -1
  42. package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +2 -5
  43. package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -1
  44. package/dist/Utils/segmentation/tools/LayerChannelManager.d.ts +52 -0
  45. package/dist/Utils/segmentation/tools/LayerChannelManager.js +175 -0
  46. package/dist/Utils/segmentation/tools/LayerChannelManager.js.map +1 -0
  47. package/dist/Utils/segmentation/tools/PanTool.d.ts +2 -8
  48. package/dist/Utils/segmentation/tools/PanTool.js.map +1 -1
  49. package/dist/Utils/segmentation/tools/SliceRenderPipeline.d.ts +80 -0
  50. package/dist/Utils/segmentation/tools/SliceRenderPipeline.js +355 -0
  51. package/dist/Utils/segmentation/tools/SliceRenderPipeline.js.map +1 -0
  52. package/dist/Utils/segmentation/tools/SphereTool.d.ts +25 -19
  53. package/dist/Utils/segmentation/tools/SphereTool.js +61 -7
  54. package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -1
  55. package/dist/Utils/segmentation/tools/ToolHost.d.ts +74 -0
  56. package/dist/Utils/segmentation/tools/ToolHost.js +10 -0
  57. package/dist/Utils/segmentation/tools/ToolHost.js.map +1 -0
  58. package/dist/Utils/segmentation/tools/ZoomTool.d.ts +2 -6
  59. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -1
  60. package/dist/Utils/segmentation/tools/index.d.ts +5 -7
  61. package/dist/Utils/segmentation/tools/index.js +6 -0
  62. package/dist/Utils/segmentation/tools/index.js.map +1 -1
  63. package/dist/Utils/workers/reformatSaveDataWorker.d.ts +1 -1
  64. package/dist/bundle.esm.js +2109 -2357
  65. package/dist/bundle.umd.js +2109 -2357
  66. package/dist/index.d.ts +3 -3
  67. package/dist/index.js +1 -1
  68. package/dist/types/Utils/segmentation/CanvasState.d.ts +31 -0
  69. package/dist/types/Utils/segmentation/DragOperator.d.ts +1 -1
  70. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +49 -27
  71. package/dist/types/Utils/segmentation/NrrdTools.d.ts +89 -467
  72. package/dist/types/Utils/segmentation/RenderingUtils.d.ts +73 -0
  73. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +1 -1
  74. package/dist/types/Utils/segmentation/core/index.d.ts +1 -1
  75. package/dist/types/Utils/segmentation/core/types.d.ts +384 -0
  76. package/dist/types/Utils/segmentation/coreTools/GuiState.d.ts +1 -1
  77. package/dist/types/Utils/segmentation/coreTools/NrrdState.d.ts +1 -1
  78. package/dist/types/Utils/segmentation/coreTools/divControlTools.d.ts +1 -1
  79. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +1 -1
  80. package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +1 -1
  81. package/dist/types/Utils/segmentation/tools/ContrastTool.d.ts +3 -6
  82. package/dist/types/Utils/segmentation/tools/CrosshairTool.d.ts +11 -1
  83. package/dist/types/Utils/segmentation/tools/DataLoader.d.ts +33 -0
  84. package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +3 -12
  85. package/dist/types/Utils/segmentation/tools/DrawingTool.d.ts +18 -26
  86. package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +2 -5
  87. package/dist/types/Utils/segmentation/tools/LayerChannelManager.d.ts +52 -0
  88. package/dist/types/Utils/segmentation/tools/PanTool.d.ts +2 -8
  89. package/dist/types/Utils/segmentation/tools/SliceRenderPipeline.d.ts +80 -0
  90. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +25 -19
  91. package/dist/types/Utils/segmentation/tools/ToolHost.d.ts +74 -0
  92. package/dist/types/Utils/segmentation/tools/ZoomTool.d.ts +2 -6
  93. package/dist/types/Utils/segmentation/tools/index.d.ts +5 -7
  94. package/dist/types/Utils/workers/reformatSaveDataWorker.d.ts +1 -1
  95. package/dist/types/index.d.ts +3 -3
  96. package/package.json +1 -1
  97. package/dist/Utils/segmentation/CommToolsData.d.ts +0 -192
  98. package/dist/Utils/segmentation/CommToolsData.js +0 -474
  99. package/dist/Utils/segmentation/CommToolsData.js.map +0 -1
  100. package/dist/Utils/segmentation/coreTools/coreType.d.ts +0 -389
  101. package/dist/Utils/segmentation/coreTools/coreType.js +0 -3
  102. package/dist/Utils/segmentation/coreTools/coreType.js.map +0 -1
  103. package/dist/types/Utils/segmentation/CommToolsData.d.ts +0 -192
  104. package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +0 -389
@@ -2,11 +2,23 @@ import { setupGui } from "./coreTools/gui";
2
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, MASK_CHANNEL_COLORS, rgbaToHex, rgbaToCss } from "./core";
6
- import { SPHERE_CHANNEL_MAP, SPHERE_LABELS } from "./tools/SphereTool";
5
+ import { CanvasState } from "./CanvasState";
6
+ import { MaskVolume } from "./core";
7
+ import { LayerChannelManager } from "./tools/LayerChannelManager";
8
+ import { SliceRenderPipeline } from "./tools/SliceRenderPipeline";
9
+ import { DataLoader } from "./tools/DataLoader";
7
10
  /**
8
11
  * Core NRRD annotation tool for medical image segmentation.
9
12
  *
13
+ * Acts as a **Facade** that delegates to composed modules:
14
+ * - {@link CanvasState} — pure state container
15
+ * - {@link DrawToolCore} — tool orchestration, event routing
16
+ * - {@link LayerChannelManager} — layer/channel/sphere-type management
17
+ * - {@link SliceRenderPipeline} — slice rendering, canvas flip, mask reload
18
+ * - {@link DataLoader} — NRRD slice loading, legacy mask loading, NIfTI loading
19
+ *
20
+ * No longer extends DrawToolCore — uses composition instead.
21
+ *
10
22
  * @example
11
23
  * ```ts
12
24
  * // Default 3 layers: layer1, layer2, layer3
@@ -16,10 +28,11 @@ import { SPHERE_CHANNEL_MAP, SPHERE_LABELS } from "./tools/SphereTool";
16
28
  * const tools = new NrrdTools(container, { layers: ["layer1", "layer2"] });
17
29
  * ```
18
30
  */
19
- export class NrrdTools extends DrawToolCore {
31
+ export class NrrdTools {
32
+ // ═══════════════════════════════════════════════════════════════════════════
33
+ // 1. Constructor + Module Initialisation
34
+ // ═══════════════════════════════════════════════════════════════════════════
20
35
  constructor(container, options) {
21
- super(container, options);
22
- this.initState = true;
23
36
  this._sliceRAFId = null;
24
37
  this._pendingSliceStep = 0;
25
38
  /** Whether calculator mode is active (not part of gui_states interface) */
@@ -27,47 +40,91 @@ export class NrrdTools extends DrawToolCore {
27
40
  /** Stored closure callbacks from gui.ts setupGui() */
28
41
  this.guiCallbacks = null;
29
42
  this.container = container;
43
+ // Create shared state
44
+ const mainAreaContainer = document.createElement("div");
45
+ this.state = new CanvasState(container, mainAreaContainer, options);
46
+ // Create DrawToolCore with shared state
47
+ this.drawCore = new DrawToolCore(container, this.state);
48
+ // Wire DrawToolCore's overridable methods to NrrdTools implementations
49
+ this.wireDrawCoreMethods();
50
+ // Wire RenderingUtils' setEmptyCanvasSize callback
51
+ this.drawCore.renderer.setEmptyCanvasSize = (axis) => this.setEmptyCanvasSize(axis);
30
52
  this.init();
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));
53
+ this.dragOperator = new DragOperator(this.container, this.state.nrrd_states, this.state.gui_states, this.state.protectedData, this.drawCore.drawingPrameters, this.setSyncsliceNum.bind(this), this.setIsDrawFalse.bind(this), this.flipDisplayImageByAxis.bind(this), this.setEmptyCanvasSize.bind(this), this.drawCore.renderer.getOrCreateSliceBuffer.bind(this.drawCore.renderer), this.drawCore.renderer.renderSliceToCanvas.bind(this.drawCore.renderer));
32
54
  // Inject EventRouter into DragOperator for centralized event handling
33
- if (this.eventRouter) {
34
- this.dragOperator.setEventRouter(this.eventRouter);
55
+ if (this.drawCore.eventRouter) {
56
+ this.dragOperator.setEventRouter(this.drawCore.eventRouter);
35
57
  }
36
58
  // Wire sphere overlay refresh callback into DragOperator → DragSliceTool
37
59
  this.dragOperator.setRefreshSphereOverlay(() => this.refreshSphereOverlay());
60
+ // Initialize extracted modules
61
+ this.initNrrdToolsModules();
38
62
  }
39
63
  /**
40
- * core function for drag slices
41
- * @param opts
64
+ * Wire DrawToolCore's overridable methods to NrrdTools implementations.
65
+ * This replaces the old inheritance approach.
42
66
  */
43
- drag(opts) {
44
- this.dragOperator.drag(opts);
67
+ wireDrawCoreMethods() {
68
+ this.drawCore.setIsDrawFalse = (target) => this.setIsDrawFalse(target);
69
+ this.drawCore.setSyncsliceNum = () => this.setSyncsliceNum();
70
+ this.drawCore.resetPaintAreaUIPosition = (l, t) => this.resetPaintAreaUIPosition(l, t);
71
+ this.drawCore.resizePaintArea = (factor) => this.resizePaintArea(factor);
72
+ this.drawCore.setEmptyCanvasSize = (axis) => this.setEmptyCanvasSize(axis);
73
+ this.drawCore.flipDisplayImageByAxis = () => this.flipDisplayImageByAxis();
74
+ this.drawCore.updateOriginAndChangedWH = () => this.updateOriginAndChangedWH();
75
+ this.drawCore.resetLayerCanvas = () => this.resetLayerCanvas();
76
+ this.drawCore.redrawDisplayCanvas = () => this.redrawDisplayCanvas();
77
+ this.drawCore.enterSphereMode = () => this.enterSphereMode();
78
+ this.drawCore.exitSphereMode = () => this.exitSphereMode();
79
+ this.drawCore.configMouseSliceWheel = () => this.configMouseSliceWheel();
45
80
  }
46
81
  /**
47
- * Set the Draw Display Canvas base size
48
- * @param size number
82
+ * A initialise function for nrrd_tools
49
83
  */
50
- setBaseDrawDisplayCanvasesSize(size) {
51
- if (size > 8) {
52
- this.baseCanvasesSize = 8;
53
- }
54
- else if (size < 1 || typeof size !== "number") {
55
- this.baseCanvasesSize = 1;
56
- }
57
- else {
58
- this.baseCanvasesSize = size;
59
- }
60
- }
61
- setDisplaySliceIndexPanel(panel) {
62
- this.dragOperator.setShowDragNumberDiv(panel);
84
+ init() {
85
+ this.state.protectedData.mainAreaContainer.classList.add("copper3D_drawingCanvasContainer");
86
+ this.container.appendChild(this.state.protectedData.mainAreaContainer);
87
+ autoFocusDiv(this.container);
88
+ this.setShowInMainArea();
63
89
  }
64
- /**
65
- * Enable the drag function for contrast images window center and window high.
66
- * @param callback
67
- */
68
- enableContrastDragEvents(callback) {
69
- this.setupConrastEvents(callback);
90
+ initNrrdToolsModules() {
91
+ const toolCtx = {
92
+ nrrd_states: this.state.nrrd_states,
93
+ gui_states: this.state.gui_states,
94
+ protectedData: this.state.protectedData,
95
+ cursorPage: this.state.cursorPage,
96
+ callbacks: this.state.annotationCallbacks,
97
+ };
98
+ this.layerChannelManager = new LayerChannelManager(toolCtx, {
99
+ reloadMasksFromVolume: () => this.reloadMasksFromVolume(),
100
+ getVolumeForLayer: (layer) => this.drawCore.renderer.getVolumeForLayer(layer),
101
+ onChannelColorChanged: (layerId, ch, color) => this.state.annotationCallbacks.onChannelColorChanged(layerId, ch, color),
102
+ });
103
+ this.sliceRenderPipeline = new SliceRenderPipeline(toolCtx, {
104
+ compositeAllLayers: () => this.drawCore.renderer.compositeAllLayers(),
105
+ getOrCreateSliceBuffer: (axis) => this.drawCore.renderer.getOrCreateSliceBuffer(axis),
106
+ renderSliceToCanvas: (layer, axis, sliceIndex, buffer, targetCtx, w, h) => this.drawCore.renderer.renderSliceToCanvas(layer, axis, sliceIndex, buffer, targetCtx, w, h),
107
+ getVolumeForLayer: (layer) => this.drawCore.renderer.getVolumeForLayer(layer),
108
+ refreshSphereOverlay: () => this.refreshSphereOverlay(),
109
+ syncGuiParameterSettings: () => this.syncGuiParameterSettings(),
110
+ repraintCurrentContrastSlice: () => this.drawCore.repraintCurrentContrastSlice(),
111
+ clearUndoHistory: () => this.drawCore.undoManager.clearAll(),
112
+ updateShowNumDiv: (contrastNum) => this.dragOperator.updateShowNumDiv(contrastNum),
113
+ updateCurrentContrastSlice: () => this.dragOperator.updateCurrentContrastSlice(),
114
+ });
115
+ this.dataLoader = new DataLoader(toolCtx, {
116
+ invalidateSliceBuffer: () => this.drawCore.renderer.invalidateSliceBuffer(),
117
+ setDisplaySlicesBaseOnAxis: () => this.sliceRenderPipeline.setDisplaySlicesBaseOnAxis(),
118
+ afterLoadSlice: () => this.sliceRenderPipeline.afterLoadSlice(),
119
+ setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
120
+ syncLayerSliceData: (index, mode) => this.drawCore.syncLayerSliceData(index, mode),
121
+ reloadMasksFromVolume: () => this.reloadMasksFromVolume(),
122
+ resetZoom: () => this.executeAction("resetZoom"),
123
+ });
70
124
  }
125
+ // ═══════════════════════════════════════════════════════════════════════════
126
+ // 2. GUI Setup
127
+ // ═══════════════════════════════════════════════════════════════════════════
71
128
  /**
72
129
  * Set up GUI for drawing panel
73
130
  * @param gui GUI
@@ -78,35 +135,35 @@ export class NrrdTools extends DrawToolCore {
78
135
  const guiOptions = {
79
136
  modeFolder,
80
137
  dragOperator: this.dragOperator,
81
- gui_states: this.gui_states,
82
- nrrd_states: this.nrrd_states,
83
- drawingCanvas: this.protectedData.canvases.drawingCanvas,
84
- drawingPrameters: this.drawingPrameters,
85
- eraserUrls: this.eraserUrls,
86
- pencilUrls: this.pencilUrls,
87
- mainPreSlices: this.protectedData.mainPreSlices,
88
- protectedData: this.protectedData,
138
+ gui_states: this.state.gui_states,
139
+ nrrd_states: this.state.nrrd_states,
140
+ drawingCanvas: this.state.protectedData.canvases.drawingCanvas,
141
+ drawingPrameters: this.drawCore.drawingPrameters,
142
+ eraserUrls: this.drawCore.eraserUrls,
143
+ pencilUrls: this.drawCore.pencilUrls,
144
+ mainPreSlices: this.state.protectedData.mainPreSlices,
145
+ protectedData: this.state.protectedData,
89
146
  removeDragMode: this.dragOperator.removeDragMode,
90
147
  configDragMode: this.dragOperator.configDragMode,
91
- clearActiveLayer: this.clearActiveLayer,
92
- clearActiveSlice: this.clearActiveSlice,
93
- updateSlicesContrast: this.updateSlicesContrast,
94
- setMainAreaSize: this.setMainAreaSize,
95
- resetPaintAreaUIPosition: this.resetPaintAreaUIPosition,
96
- resizePaintArea: this.resizePaintArea,
97
- repraintCurrentContrastSlice: this.repraintCurrentContrastSlice,
98
- setSyncsliceNum: this.setSyncsliceNum,
99
- resetLayerCanvas: this.resetLayerCanvas,
100
- redrawDisplayCanvas: this.redrawDisplayCanvas,
101
- flipDisplayImageByAxis: this.flipDisplayImageByAxis,
102
- setEmptyCanvasSize: this.setEmptyCanvasSize,
103
- syncLayerSliceData: this.syncLayerSliceData,
104
- drawImageOnEmptyImage: this.drawImageOnEmptyImage,
105
- getRestLayer: this.getRestLayer,
106
- setIsDrawFalse: this.setIsDrawFalse,
107
- getVolumeForLayer: this.getVolumeForLayer.bind(this),
108
- undoLastPainting: this.undoLastPainting.bind(this),
109
- redoLastPainting: this.redoLastPainting.bind(this),
148
+ clearActiveLayer: this.clearActiveLayer.bind(this),
149
+ clearActiveSlice: this.drawCore.clearActiveSlice.bind(this.drawCore),
150
+ updateSlicesContrast: this.drawCore.updateSlicesContrast,
151
+ setMainAreaSize: this.setMainAreaSize.bind(this),
152
+ resetPaintAreaUIPosition: this.resetPaintAreaUIPosition.bind(this),
153
+ resizePaintArea: this.resizePaintArea.bind(this),
154
+ repraintCurrentContrastSlice: this.drawCore.repraintCurrentContrastSlice,
155
+ setSyncsliceNum: this.setSyncsliceNum.bind(this),
156
+ resetLayerCanvas: this.resetLayerCanvas.bind(this),
157
+ redrawDisplayCanvas: this.redrawDisplayCanvas.bind(this),
158
+ flipDisplayImageByAxis: this.flipDisplayImageByAxis.bind(this),
159
+ setEmptyCanvasSize: this.setEmptyCanvasSize.bind(this),
160
+ syncLayerSliceData: this.drawCore.syncLayerSliceData.bind(this.drawCore),
161
+ drawImageOnEmptyImage: this.drawCore.drawImageOnEmptyImage.bind(this.drawCore),
162
+ getRestLayer: this.drawCore.getRestLayer.bind(this.drawCore),
163
+ setIsDrawFalse: this.setIsDrawFalse.bind(this),
164
+ getVolumeForLayer: this.drawCore.renderer.getVolumeForLayer.bind(this.drawCore.renderer),
165
+ undoLastPainting: this.drawCore.undoLastPainting.bind(this.drawCore),
166
+ redoLastPainting: this.drawCore.redoLastPainting.bind(this.drawCore),
110
167
  resetZoom: () => this.executeAction("resetZoom"),
111
168
  downloadCurrentMask: () => this.executeAction("downloadCurrentMask"),
112
169
  };
@@ -125,195 +182,106 @@ export class NrrdTools extends DrawToolCore {
125
182
  }
126
183
  /**
127
184
  * Sync guiParameterSettings with current volume metadata.
128
- * Called internally when slices are loaded or switched.
129
185
  */
130
186
  syncGuiParameterSettings() {
131
- if (this.guiParameterSettings && this.protectedData.mainPreSlices) {
132
- this.guiParameterSettings.windowHigh.value = this.guiParameterSettings.windowLow.value = this.protectedData.mainPreSlices.volume;
133
- this.guiParameterSettings.windowHigh.max = this.guiParameterSettings.windowLow.max = this.protectedData.mainPreSlices.volume.max;
134
- this.guiParameterSettings.windowHigh.min = this.guiParameterSettings.windowLow.min = this.protectedData.mainPreSlices.volume.min;
135
- }
136
- }
137
- // ── Layer & Channel Management API ─────────────────────────────────────
138
- /**
139
- * Set the active layer and update fillColor/brushColor to match the active channel.
140
- */
141
- setActiveLayer(layerId) {
142
- this.gui_states.layerChannel.layer = layerId;
143
- this.syncBrushColor();
144
- }
145
- /**
146
- * Set the active channel (1-8) and update fillColor/brushColor.
147
- */
148
- setActiveChannel(channel) {
149
- this.gui_states.layerChannel.activeChannel = channel;
150
- this.syncBrushColor();
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;
187
+ if (this.guiParameterSettings && this.state.protectedData.mainPreSlices) {
188
+ this.guiParameterSettings.windowHigh.value = this.guiParameterSettings.windowLow.value = this.state.protectedData.mainPreSlices.volume;
189
+ this.guiParameterSettings.windowHigh.max = this.guiParameterSettings.windowLow.max = this.state.protectedData.mainPreSlices.volume.max;
190
+ this.guiParameterSettings.windowHigh.min = this.guiParameterSettings.windowLow.min = this.state.protectedData.mainPreSlices.volume.min;
172
191
  }
173
192
  }
193
+ // ═══════════════════════════════════════════════════════════════════════════
194
+ // 3. Public API — Mode / Slider / Color / Action
195
+ // ═══════════════════════════════════════════════════════════════════════════
174
196
  /**
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.
197
+ * Set the current tool mode.
187
198
  */
188
199
  setMode(mode) {
189
200
  if (!this.guiCallbacks)
190
201
  return;
191
- const prevSphere = this.gui_states.mode.sphere;
202
+ const prevSphere = this.state.gui_states.mode.sphere;
192
203
  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;
204
+ this.state.gui_states.mode.pencil = false;
205
+ this.state.gui_states.mode.eraser = false;
206
+ this.state.gui_states.mode.sphere = false;
197
207
  this._calculatorActive = false;
198
- // Activate new mode
199
208
  switch (mode) {
200
209
  case "pencil":
201
- this.gui_states.mode.pencil = true;
210
+ this.state.gui_states.mode.pencil = true;
202
211
  this.guiCallbacks.updatePencilState();
203
212
  break;
204
213
  case "brush":
205
- // brush = pencil off + eraser off (default brush mode)
206
214
  this.guiCallbacks.updatePencilState();
207
215
  break;
208
216
  case "eraser":
209
- this.gui_states.mode.eraser = true;
217
+ this.state.gui_states.mode.eraser = true;
210
218
  this.guiCallbacks.updateEraserState();
211
219
  break;
212
220
  case "sphere":
213
- this.gui_states.mode.sphere = true;
221
+ this.state.gui_states.mode.sphere = true;
214
222
  break;
215
223
  case "calculator":
216
- this.gui_states.mode.sphere = true;
224
+ this.state.gui_states.mode.sphere = true;
217
225
  this._calculatorActive = true;
218
226
  break;
219
227
  }
220
- // Handle sphere mode transitions
221
- if (prevSphere && !this.gui_states.mode.sphere) {
222
- this.guiCallbacks.updateSphereState(); // exits sphere mode
228
+ if (prevSphere && !this.state.gui_states.mode.sphere) {
229
+ this.guiCallbacks.updateSphereState();
223
230
  }
224
- if (!prevSphere && this.gui_states.mode.sphere) {
225
- this.guiCallbacks.updateSphereState(); // enters sphere mode
231
+ if (!prevSphere && this.state.gui_states.mode.sphere) {
232
+ this.guiCallbacks.updateSphereState();
226
233
  }
227
- // Handle calculator exit side-effect
228
234
  if (prevCalculator && !this._calculatorActive) {
229
- // calculator was exited — sphere onChange already handled above
235
+ // calculator was exited
230
236
  }
231
237
  }
232
- /**
233
- * Get the current tool mode based on gui_states flags.
234
- */
235
238
  getMode() {
236
239
  if (this._calculatorActive)
237
240
  return "calculator";
238
- if (this.gui_states.mode.sphere)
241
+ if (this.state.gui_states.mode.sphere)
239
242
  return "sphere";
240
- if (this.gui_states.mode.eraser)
243
+ if (this.state.gui_states.mode.eraser)
241
244
  return "eraser";
242
- if (this.gui_states.mode.pencil)
245
+ if (this.state.gui_states.mode.pencil)
243
246
  return "pencil";
244
247
  return "brush";
245
248
  }
246
- /**
247
- * Check if calculator mode is active.
248
- */
249
249
  isCalculatorActive() {
250
250
  return this._calculatorActive;
251
251
  }
252
- // ── Phase 1: GUI API — Slider Methods ───────────────────────────────────
253
- /**
254
- * Set mask overlay opacity.
255
- * @param value Opacity value [0.1, 1]
256
- */
257
252
  setOpacity(value) {
258
- this.gui_states.drawing.globalAlpha = Math.max(0.1, Math.min(1, value));
253
+ this.state.gui_states.drawing.globalAlpha = Math.max(0.1, Math.min(1, value));
259
254
  }
260
- /**
261
- * Get the current mask overlay opacity.
262
- */
263
255
  getOpacity() {
264
- return this.gui_states.drawing.globalAlpha;
256
+ return this.state.gui_states.drawing.globalAlpha;
265
257
  }
266
- /**
267
- * Set brush and eraser size, and trigger cursor update.
268
- * @param size Brush size [5, 50]
269
- */
270
258
  setBrushSize(size) {
271
259
  var _a;
272
- this.gui_states.drawing.brushAndEraserSize = Math.max(5, Math.min(50, size));
260
+ this.state.gui_states.drawing.brushAndEraserSize = Math.max(5, Math.min(50, size));
273
261
  (_a = this.guiCallbacks) === null || _a === void 0 ? void 0 : _a.updateBrushAndEraserSize();
274
262
  }
275
- /**
276
- * Get the current brush/eraser size.
277
- */
278
263
  getBrushSize() {
279
- return this.gui_states.drawing.brushAndEraserSize;
264
+ return this.state.gui_states.drawing.brushAndEraserSize;
280
265
  }
281
- /**
282
- * Set window high (image contrast) value.
283
- * Call finishWindowAdjustment() when the user finishes dragging.
284
- */
285
266
  setWindowHigh(value) {
286
267
  var _a;
287
- this.gui_states.viewConfig.readyToUpdate = false;
268
+ this.state.gui_states.viewConfig.readyToUpdate = false;
288
269
  (_a = this.guiCallbacks) === null || _a === void 0 ? void 0 : _a.updateWindowHigh(value);
289
270
  }
290
- /**
291
- * Set window low (image center) value.
292
- * Call finishWindowAdjustment() when the user finishes dragging.
293
- */
294
271
  setWindowLow(value) {
295
272
  var _a;
296
- this.gui_states.viewConfig.readyToUpdate = false;
273
+ this.state.gui_states.viewConfig.readyToUpdate = false;
297
274
  (_a = this.guiCallbacks) === null || _a === void 0 ? void 0 : _a.updateWindowLow(value);
298
275
  }
299
- /**
300
- * Finish a window/contrast adjustment (repaint all contrast slices).
301
- */
302
276
  finishWindowAdjustment() {
303
277
  var _a;
304
278
  (_a = this.guiCallbacks) === null || _a === void 0 ? void 0 : _a.finishContrastAdjustment();
305
279
  }
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
280
  adjustContrast(type, delta) {
312
281
  var _a, _b;
313
282
  if (!this.guiParameterSettings)
314
283
  return;
315
284
  const setting = this.guiParameterSettings[type];
316
- // setting.value is the volume object at runtime (typed as null in interface)
317
285
  const vol = setting.value;
318
286
  const currentValue = type === "windowHigh"
319
287
  ? ((_a = vol === null || vol === void 0 ? void 0 : vol.windowHigh) !== null && _a !== void 0 ? _a : 0)
@@ -328,11 +296,6 @@ export class NrrdTools extends DrawToolCore {
328
296
  this.setWindowLow(newValue);
329
297
  }
330
298
  }
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
299
  getSliderMeta(key) {
337
300
  var _a, _b, _c, _d, _e, _f, _g, _h;
338
301
  if (!this.guiParameterSettings)
@@ -348,7 +311,7 @@ export class NrrdTools extends DrawToolCore {
348
311
  value = (_d = (_c = setting.value) === null || _c === void 0 ? void 0 : _c.windowLow) !== null && _d !== void 0 ? _d : 0;
349
312
  }
350
313
  else {
351
- value = (_e = this.gui_states.drawing[key]) !== null && _e !== void 0 ? _e : 0;
314
+ value = (_e = this.state.gui_states.drawing[key]) !== null && _e !== void 0 ? _e : 0;
352
315
  }
353
316
  return {
354
317
  min: (_f = setting.min) !== null && _f !== void 0 ? _f : 0,
@@ -357,27 +320,12 @@ export class NrrdTools extends DrawToolCore {
357
320
  value,
358
321
  };
359
322
  }
360
- // ── Phase 1: GUI API — Color & Action Methods ──────────────────────────
361
- /**
362
- * Set the pencil stroke color.
363
- */
364
323
  setPencilColor(hex) {
365
- this.gui_states.drawing.color = hex;
324
+ this.state.gui_states.drawing.color = hex;
366
325
  }
367
- /**
368
- * Get the current pencil stroke color.
369
- */
370
326
  getPencilColor() {
371
- return this.gui_states.drawing.color;
327
+ return this.state.gui_states.drawing.color;
372
328
  }
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
329
  executeAction(action) {
382
330
  switch (action) {
383
331
  case "undo":
@@ -387,811 +335,237 @@ export class NrrdTools extends DrawToolCore {
387
335
  this.redo();
388
336
  break;
389
337
  case "clearActiveSliceMask":
390
- this.clearActiveSlice();
338
+ this.drawCore.clearActiveSlice();
391
339
  break;
392
340
  case "clearActiveLayerMask": {
393
341
  const text = "Are you sure remove annotations on All slice?";
394
342
  if (confirm(text) === true) {
395
- this.nrrd_states.flags.clearAllFlag = true;
396
- this.clearActiveSlice();
343
+ this.state.nrrd_states.flags.clearAllFlag = true;
344
+ this.drawCore.clearActiveSlice();
397
345
  this.clearActiveLayer();
398
346
  }
399
- this.nrrd_states.flags.clearAllFlag = false;
347
+ this.state.nrrd_states.flags.clearAllFlag = false;
400
348
  break;
401
349
  }
402
350
  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);
351
+ this.state.nrrd_states.view.sizeFactor = this.state.baseCanvasesSize;
352
+ this.state.gui_states.viewConfig.mainAreaSize = this.state.baseCanvasesSize;
353
+ this.resizePaintArea(this.state.nrrd_states.view.sizeFactor);
406
354
  this.resetPaintAreaUIPosition();
407
355
  break;
408
356
  case "downloadCurrentMask": {
409
357
  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,
358
+ axis: this.state.protectedData.axis,
359
+ currentSliceIndex: this.state.nrrd_states.view.currentSliceIndex,
360
+ drawingCanvas: this.state.protectedData.canvases.drawingCanvas,
361
+ originWidth: this.state.nrrd_states.image.originWidth,
362
+ originHeight: this.state.nrrd_states.image.originHeight,
415
363
  };
416
364
  enableDownload(config);
417
365
  break;
418
366
  }
419
367
  }
420
368
  }
421
- /**
422
- * Sync brush/fill color from the active layer's volume color map.
423
- * Falls back to global CHANNEL_HEX_COLORS if volume not available.
424
- */
425
- syncBrushColor() {
426
- const channel = this.gui_states.layerChannel.activeChannel || 1;
427
- const layer = this.gui_states.layerChannel.layer;
428
- const volume = this.protectedData.maskData.volumes[layer];
429
- if (volume) {
430
- const hex = rgbaToHex(volume.getChannelColor(channel));
431
- this.gui_states.drawing.fillColor = hex;
432
- this.gui_states.drawing.brushColor = hex;
433
- }
434
- else {
435
- const hex = CHANNEL_HEX_COLORS[channel] || '#00ff00';
436
- this.gui_states.drawing.fillColor = hex;
437
- this.gui_states.drawing.brushColor = hex;
438
- }
439
- }
440
- /**
441
- * Get the currently active layer id.
442
- */
443
- getActiveLayer() {
444
- return this.gui_states.layerChannel.layer;
445
- }
446
- /**
447
- * Get the currently active channel value.
448
- */
449
- getActiveChannel() {
450
- return this.gui_states.layerChannel.activeChannel;
451
- }
452
- /**
453
- * Set visibility of a layer and re-render.
454
- */
455
- setLayerVisible(layerId, visible) {
456
- this.gui_states.layerChannel.layerVisibility[layerId] = visible;
457
- this.reloadMasksFromVolume();
458
- }
459
- /**
460
- * Check if a layer is visible.
461
- */
462
- isLayerVisible(layerId) {
463
- var _a;
464
- return (_a = this.gui_states.layerChannel.layerVisibility[layerId]) !== null && _a !== void 0 ? _a : true;
465
- }
466
- /**
467
- * Set visibility of a specific channel within a layer and re-render.
468
- */
469
- setChannelVisible(layerId, channel, visible) {
470
- if (this.gui_states.layerChannel.channelVisibility[layerId]) {
471
- this.gui_states.layerChannel.channelVisibility[layerId][channel] = visible;
472
- }
473
- this.reloadMasksFromVolume();
474
- }
475
- /**
476
- * Check if a specific channel within a layer is visible.
477
- */
478
- isChannelVisible(layerId, channel) {
479
- var _a, _b;
480
- return (_b = (_a = this.gui_states.layerChannel.channelVisibility[layerId]) === null || _a === void 0 ? void 0 : _a[channel]) !== null && _b !== void 0 ? _b : true;
481
- }
482
- /**
483
- * Get the full layer visibility map.
484
- */
485
- getLayerVisibility() {
486
- return Object.assign({}, this.gui_states.layerChannel.layerVisibility);
487
- }
488
- /**
489
- * Get the full channel visibility map.
490
- */
491
- getChannelVisibility() {
492
- const result = {};
493
- for (const layerId of this.nrrd_states.image.layers) {
494
- result[layerId] = Object.assign({}, this.gui_states.layerChannel.channelVisibility[layerId]);
495
- }
496
- return result;
497
- }
498
- /**
499
- * Check if a specific layer contains any non-zero mask data.
500
- *
501
- * This is useful for determining if a layer has actual annotations
502
- * before saving or exporting to avoid processing empty layers.
503
- *
504
- * @param layerId - The layer to check ('layer1', 'layer2', or 'layer3')
505
- * @returns True if the layer has any non-zero voxel data, false if empty
506
- *
507
- * @example
508
- * ```ts
509
- * if (nrrdTools.hasLayerData('layer1')) {
510
- * await useSaveMasks(caseId, 'layer1');
511
- * }
512
- * ```
513
- */
514
- hasLayerData(layerId) {
515
- const volume = this.protectedData.maskData.volumes[layerId];
516
- if (!volume) {
517
- return false;
518
- }
519
- return volume.hasData();
520
- }
521
- // ── Custom Channel Color API ────────────────────────────────────────────
522
- /**
523
- * Set a custom color for a specific channel in a specific layer.
524
- * Only affects this layer; other layers remain unchanged.
525
- *
526
- * @param layerId Layer to customize (e.g. 'layer1', 'layer2')
527
- * @param channel Channel label (1-8)
528
- * @param color RGBAColor object { r, g, b, a } (each 0-255)
529
- *
530
- * @example
531
- * ```ts
532
- * // Make channel 2 in layer2 orange
533
- * nrrdTools.setChannelColor('layer2', 2, { r: 255, g: 128, b: 0, a: 255 });
534
- * ```
535
- */
536
- setChannelColor(layerId, channel, color) {
537
- const volume = this.protectedData.maskData.volumes[layerId];
538
- if (!volume) {
539
- console.warn(`setChannelColor: unknown layer "${layerId}"`);
540
- return;
541
- }
542
- volume.setChannelColor(channel, color);
543
- if (layerId === this.gui_states.layerChannel.layer && channel === this.gui_states.layerChannel.activeChannel) {
544
- this.syncBrushColor();
545
- }
546
- this.reloadMasksFromVolume();
547
- this.annotationCallbacks.onChannelColorChanged(layerId, channel, color);
548
- }
549
- /**
550
- * Get the current color for a specific channel in a specific layer.
551
- *
552
- * @param layerId Layer to query
553
- * @param channel Channel label (1-8)
554
- * @returns RGBAColor object (copy, safe to mutate)
555
- */
556
- getChannelColor(layerId, channel) {
557
- const volume = this.protectedData.maskData.volumes[layerId];
558
- if (!volume) {
559
- return { r: 0, g: 255, b: 0, a: 255 };
560
- }
561
- return volume.getChannelColor(channel);
562
- }
563
- /**
564
- * Get a hex color string for a channel in a layer (e.g. '#ff8000').
565
- */
566
- getChannelHexColor(layerId, channel) {
567
- return rgbaToHex(this.getChannelColor(layerId, channel));
568
- }
569
- /**
570
- * Get a CSS rgba() color string for a channel in a layer.
571
- */
572
- getChannelCssColor(layerId, channel) {
573
- return rgbaToCss(this.getChannelColor(layerId, channel));
574
- }
575
- /**
576
- * Batch-set multiple channel colors for a single layer (single re-render).
577
- *
578
- * @param layerId Layer to customize
579
- * @param colorMap Partial map of channel -> RGBAColor
580
- *
581
- * @example
582
- * ```ts
583
- * nrrdTools.setChannelColors('layer2', {
584
- * 2: { r: 255, g: 128, b: 0, a: 255 },
585
- * 3: { r: 100, g: 200, b: 50, a: 255 },
586
- * });
587
- * ```
588
- */
589
- setChannelColors(layerId, colorMap) {
590
- const volume = this.protectedData.maskData.volumes[layerId];
591
- if (!volume) {
592
- console.warn(`setChannelColors: unknown layer "${layerId}"`);
593
- return;
594
- }
595
- for (const [ch, color] of Object.entries(colorMap)) {
596
- volume.setChannelColor(Number(ch), color);
597
- }
598
- if (layerId === this.gui_states.layerChannel.layer) {
599
- this.syncBrushColor();
600
- }
601
- this.reloadMasksFromVolume();
602
- }
603
- /**
604
- * Set the same color for a specific channel across ALL layers (single re-render).
605
- *
606
- * @param channel Channel label (1-8)
607
- * @param color RGBAColor object
608
- */
609
- setAllLayersChannelColor(channel, color) {
610
- for (const layerId of this.nrrd_states.image.layers) {
611
- const volume = this.protectedData.maskData.volumes[layerId];
612
- if (volume) {
613
- volume.setChannelColor(channel, color);
614
- }
615
- }
616
- if (channel === this.gui_states.layerChannel.activeChannel) {
617
- this.syncBrushColor();
618
- }
619
- this.reloadMasksFromVolume();
620
- }
621
- /**
622
- * Reset channel colors to system defaults.
623
- *
624
- * @param layerId Optional. If omitted, resets all layers.
625
- * @param channel Optional. If omitted, resets all channels in the layer(s).
626
- *
627
- * @example
628
- * ```ts
629
- * nrrdTools.resetChannelColors('layer2', 2); // Reset only ch2 in layer2
630
- * nrrdTools.resetChannelColors('layer2'); // Reset all channels in layer2
631
- * nrrdTools.resetChannelColors(); // Reset all layers
632
- * ```
633
- */
634
- resetChannelColors(layerId, channel) {
635
- const layers = layerId ? [layerId] : this.nrrd_states.image.layers;
636
- for (const lid of layers) {
637
- const volume = this.protectedData.maskData.volumes[lid];
638
- if (volume) {
639
- volume.resetChannelColors(channel);
640
- }
641
- }
642
- this.syncBrushColor();
643
- this.reloadMasksFromVolume();
644
- }
645
- // ── Keyboard & History API ──────────────────────────────────────────────
646
- /**
647
- * Programmatically trigger an undo operation.
648
- *
649
- * Equivalent to pressing Ctrl+Z. Reverts the most recent drawing stroke
650
- * on the currently active layer and syncs the result to the backend via
651
- * the `getMask` callback.
652
- *
653
- * @example
654
- * ```ts
655
- * undoBtn.addEventListener('click', () => nrrdTools.undo());
656
- * ```
657
- */
369
+ // ═══════════════════════════════════════════════════════════════════════════
370
+ // 4. Public API Keyboard & History
371
+ // ═══════════════════════════════════════════════════════════════════════════
658
372
  undo() {
659
- this.undoLastPainting();
373
+ this.drawCore.undoLastPainting();
660
374
  }
661
- /**
662
- * Programmatically trigger a redo operation.
663
- *
664
- * Equivalent to pressing Ctrl+Y. Re-applies the most recently undone
665
- * drawing stroke on the currently active layer and syncs the result to
666
- * the backend via the `getMask` callback.
667
- *
668
- * @example
669
- * ```ts
670
- * redoBtn.addEventListener('click', () => nrrdTools.redo());
671
- * ```
672
- */
673
375
  redo() {
674
- this.redoLastPainting();
376
+ this.drawCore.redoLastPainting();
675
377
  }
676
- /**
677
- * Enter keyboard-configuration mode.
678
- *
679
- * While active, every keydown/keyup handler in DrawToolCore and DragOperator
680
- * is suppressed so the user can press arbitrary keys in the settings dialog
681
- * without accidentally triggering drawing, undo, or contrast shortcuts.
682
- *
683
- * Always pair with {@link exitKeyboardConfig} when the dialog closes.
684
- *
685
- * @example
686
- * ```ts
687
- * dialog.addEventListener('open', () => nrrdTools.enterKeyboardConfig());
688
- * ```
689
- */
690
378
  enterKeyboardConfig() {
691
- this._configKeyBoard = true;
379
+ this.state.configKeyBoard = true;
692
380
  }
693
- /**
694
- * Exit keyboard-configuration mode and resume normal shortcut handling.
695
- *
696
- * @example
697
- * ```ts
698
- * dialog.addEventListener('close', () => nrrdTools.exitKeyboardConfig());
699
- * ```
700
- */
701
381
  exitKeyboardConfig() {
702
- this._configKeyBoard = false;
382
+ this.state.configKeyBoard = false;
703
383
  }
704
- /**
705
- * Enable or disable the contrast window/level shortcut (Ctrl/Meta key).
706
- *
707
- * When disabled:
708
- * - Holding Ctrl no longer enters contrast mode
709
- * - If contrast mode is currently active it is immediately exited
710
- * - All other Ctrl-based shortcuts (Ctrl+Z undo, Ctrl+Y redo) are
711
- * unaffected because they are checked in the keydown handler before
712
- * the contrast guard runs
713
- *
714
- * @param enabled - Pass `false` to disable, `true` to re-enable.
715
- *
716
- * @example
717
- * ```ts
718
- * // Disable contrast when sphere tool is active
719
- * nrrdTools.setContrastShortcutEnabled(false);
720
- *
721
- * // Re-enable when returning to draw mode
722
- * nrrdTools.setContrastShortcutEnabled(true);
723
- * ```
724
- */
725
384
  setContrastShortcutEnabled(enabled) {
726
385
  var _a;
727
- (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.setContrastEnabled(enabled);
386
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setContrastEnabled(enabled);
728
387
  }
729
- /**
730
- * Returns whether the contrast shortcut is currently enabled.
731
- */
732
388
  isContrastShortcutEnabled() {
733
389
  var _a, _b;
734
- return (_b = (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isContrastEnabled()) !== null && _b !== void 0 ? _b : true;
390
+ return (_b = (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.isContrastEnabled()) !== null && _b !== void 0 ? _b : true;
735
391
  }
736
- /**
737
- * Update keyboard shortcut bindings.
738
- *
739
- * Synchronises both the internal class field (read by every keydown handler)
740
- * and the EventRouter's internal copy (used for modifier-key mode tracking)
741
- * so the two never drift apart. If `mouseWheel` is changed the wheel event
742
- * listener is automatically re-bound via {@link updateMouseWheelEvent}.
743
- *
744
- * Only the fields you supply are updated; omitted fields keep their
745
- * current values.
746
- *
747
- * @param settings - Partial keyboard settings to override.
748
- *
749
- * @example
750
- * ```ts
751
- * nrrdTools.setKeyboardSettings({
752
- * undo: 'z',
753
- * redo: 'y',
754
- * crosshair: 'c',
755
- * mouseWheel: 'Scroll:Slice',
756
- * });
757
- * ```
758
- */
759
392
  setKeyboardSettings(settings) {
760
393
  var _a;
761
394
  const mouseWheelChanged = settings.mouseWheel !== undefined
762
- && settings.mouseWheel !== this._keyboardSettings.mouseWheel;
763
- Object.assign(this._keyboardSettings, settings);
764
- (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.setKeyboardSettings(settings);
395
+ && settings.mouseWheel !== this.state.keyboardSettings.mouseWheel;
396
+ Object.assign(this.state.keyboardSettings, settings);
397
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setKeyboardSettings(settings);
765
398
  if (mouseWheelChanged) {
766
399
  this.updateMouseWheelEvent();
767
400
  }
768
401
  }
769
- /**
770
- * Get a snapshot of the current keyboard shortcut bindings.
771
- *
772
- * Returns a shallow copy so callers cannot accidentally mutate internal
773
- * state. To update settings use {@link setKeyboardSettings} instead.
774
- *
775
- * @returns Current keyboard settings.
776
- *
777
- * @example
778
- * ```ts
779
- * const { undo, redo } = nrrdTools.getKeyboardSettings();
780
- * console.log(`Undo: Ctrl+${undo}, Redo: Ctrl+${redo}`);
781
- * ```
782
- */
783
402
  getKeyboardSettings() {
784
- return Object.assign({}, this._keyboardSettings);
403
+ return Object.assign({}, this.state.keyboardSettings);
785
404
  }
786
- /**
787
- * A initialise function for nrrd_tools
788
- */
789
- init() {
790
- this.protectedData.mainAreaContainer.classList.add("copper3D_drawingCanvasContainer");
791
- this.container.appendChild(this.protectedData.mainAreaContainer);
792
- autoFocusDiv(this.container);
793
- this.setShowInMainArea();
405
+ // ═══════════════════════════════════════════════════════════════════════════
406
+ // 5. Public API View Control (drag, zoom, pan, slice navigation)
407
+ // ═══════════════════════════════════════════════════════════════════════════
408
+ drag(opts) {
409
+ this.dragOperator.drag(opts);
794
410
  }
795
411
  /**
796
- *
797
- * entry function
798
- * * {
799
- * x:slice,
800
- * y:slice,
801
- * z:slice
802
- * }
803
- *
804
- * @param allSlices - all nrrd contrast slices
805
-
412
+ * Core drawing entry point.
806
413
  */
807
- setAllSlices(allSlices) {
808
- this.protectedData.allSlicesArray = [...allSlices];
809
- const randomSlice = this.protectedData.allSlicesArray[0];
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;
821
- // Phase 2 Day 9: Re-initialize MaskVolume with real NRRD dimensions.
822
- // This replaces the 1×1×1 placeholders from CommToolsData constructor
823
- // and "turns on" all Day 7/8 volume read/write paths.
824
- // Invalidate reusable buffer from previous dataset
825
- this.invalidateSliceBuffer();
826
- const [vw, vh, vd] = this.nrrd_states.image.dimensions;
827
- this.protectedData.maskData.volumes = this.nrrd_states.image.layers.reduce((acc, id) => {
828
- acc[id] = new MaskVolume(vw, vh, vd, 1);
829
- return acc;
830
- }, {});
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) => {
843
- return item * 1;
844
- });
845
- this.protectedData.allSlicesArray.forEach((item, index) => {
846
- item.x.contrastOrder = index;
847
- item.y.contrastOrder = index;
848
- item.z.contrastOrder = index;
849
- });
850
- // Phase 3: initPaintImages removed (MaskVolume initialized separately)
851
- // this.initPaintImages(this.nrrd_states.image.dimensions);
852
- // init displayslices array, the axis default is "z"
853
- this.setDisplaySlicesBaseOnAxis();
854
- this.afterLoadSlice();
855
- }
856
- loadingMaskByLayer(masks, index, imageData) {
857
- let imageDataLable = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.image.nrrd_x_pixel, this.nrrd_states.image.nrrd_y_pixel);
858
- this.setEmptyCanvasSize();
859
- for (let j = 0; j < masks[index].data.length; j++) {
860
- imageDataLable.data[j] = masks[index].data[j];
861
- imageData.data[j] += masks[index].data[j];
862
- }
863
- return imageDataLable;
864
- }
865
- // need to remove
866
- setMasksData(masksData, loadingBar) {
867
- if (!!masksData) {
868
- this.nrrd_states.flags.loadingMaskData = true;
869
- if (loadingBar) {
870
- let { loadingContainer, progress } = loadingBar;
871
- loadingContainer.style.display = "flex";
872
- progress.innerText = "Loading masks data......";
873
- }
874
- this.setEmptyCanvasSize();
875
- const len = masksData["layer1"].length;
876
- for (let i = 0; i < len; i++) {
877
- let imageData = this.protectedData.ctxes.emptyCtx.createImageData(this.nrrd_states.image.nrrd_x_pixel, this.nrrd_states.image.nrrd_y_pixel);
878
- if (masksData["layer1"][i].data.length > 0) {
879
- this.loadingMaskByLayer(masksData["layer1"], i, imageData);
880
- }
881
- if (masksData["layer2"][i].data.length > 0) {
882
- this.loadingMaskByLayer(masksData["layer2"], i, imageData);
883
- }
884
- if (masksData["layer3"][i].data.length > 0) {
885
- this.loadingMaskByLayer(masksData["layer3"], i, imageData);
886
- }
887
- this.setEmptyCanvasSize();
888
- this.protectedData.ctxes.emptyCtx.putImageData(imageData, 0, 0);
889
- this.syncLayerSliceData(i, "default");
890
- }
891
- this.nrrd_states.flags.loadingMaskData = false;
892
- this.executeAction("resetZoom");
893
- if (loadingBar) {
894
- loadingBar.loadingContainer.style.display = "none";
895
- }
896
- }
414
+ draw(opts) {
415
+ this.drawCore.draw(opts);
897
416
  }
898
417
  /**
899
- * Load raw voxel data into MaskVolume layers.
900
- *
901
- * Expects pre-extracted voxel bytes (e.g. from useNiftiVoxelData).
902
- *
903
- * @param layerVoxels Map of layer ID to raw voxel Uint8Array
904
- * Keys should be 'layer1', 'layer2', 'layer3'
905
- * @param loadingBar Optional loading bar UI
418
+ * Set the Draw Display Canvas base size
906
419
  */
907
- setMasksFromNIfTI(layerVoxels, loadingBar) {
908
- if (!layerVoxels || layerVoxels.size === 0)
909
- return;
910
- if (loadingBar) {
911
- loadingBar.loadingContainer.style.display = "flex";
912
- loadingBar.progress.innerText = "Loading mask layers from NIfTI...";
913
- }
914
- try {
915
- for (const [layerId, rawData] of layerVoxels) {
916
- const volume = this.protectedData.maskData.volumes[layerId];
917
- if (!volume) {
918
- console.warn(`setMasksFromNIfTI: unknown layer "${layerId}", skipping`);
919
- continue;
920
- }
921
- const expectedLen = volume.getRawData().length;
922
- // Ensure we copy exactly the right number of bytes
923
- if (rawData.length >= expectedLen) {
924
- volume.setRawData(rawData.slice(0, expectedLen));
925
- }
926
- else {
927
- const padded = new Uint8Array(expectedLen);
928
- padded.set(rawData);
929
- volume.setRawData(padded);
930
- }
931
- }
932
- // Reload the current slice from MaskVolume to canvas
933
- this.reloadMasksFromVolume();
934
- this.executeAction("resetZoom");
420
+ setBaseDrawDisplayCanvasesSize(size) {
421
+ if (size > 8) {
422
+ this.state.baseCanvasesSize = 8;
935
423
  }
936
- catch (error) {
937
- console.error("Error loading NIfTI masks:", error);
424
+ else if (size < 1 || typeof size !== "number") {
425
+ this.state.baseCanvasesSize = 1;
938
426
  }
939
- finally {
940
- if (loadingBar) {
941
- loadingBar.loadingContainer.style.display = "none";
942
- }
427
+ else {
428
+ this.state.baseCanvasesSize = size;
943
429
  }
944
430
  }
945
- setShowInMainArea() {
946
- this.nrrd_states.view.showContrast = true;
947
- }
948
- getCurrentImageDimension() {
949
- return this.nrrd_states.image.dimensions;
431
+ setDisplaySliceIndexPanel(panel) {
432
+ this.dragOperator.setShowDragNumberDiv(panel);
950
433
  }
951
- getVoxelSpacing() {
952
- return this.nrrd_states.image.voxelSpacing;
434
+ enableContrastDragEvents(callback) {
435
+ this.drawCore.setupConrastEvents(callback);
953
436
  }
954
- getSpaceOrigin() {
955
- return this.nrrd_states.image.spaceOrigin;
437
+ setEraserUrls(urls) {
438
+ this.drawCore.setEraserUrls(urls);
956
439
  }
957
- getMaskData() {
958
- return this.protectedData.maskData;
959
- }
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
- */
971
- setCalculateDistanceSphere(x, y, sliceIndex, cal_position) {
972
- this.nrrd_states.sphere.sphereRadius = 5;
973
- // move to target slice
974
- const steps = sliceIndex - this.nrrd_states.view.currentSliceIndex;
975
- this.setSliceMoving(steps * this.protectedData.displaySlices.length);
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();
440
+ setPencilIconUrls(urls) {
441
+ this.drawCore.setPencilIconUrls(urls);
1009
442
  }
1010
443
  /**
1011
444
  * Switch all contrast slices' orientation
1012
- * @param {string} aixs:"x" | "y" | "z"
1013
- * */
445
+ */
1014
446
  setSliceOrientation(axisTo) {
1015
447
  var _a;
1016
448
  let convetObj;
1017
- if (((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isCrosshairEnabled()) || this.gui_states.mode.sphere) {
1018
- if (this.protectedData.axis === "z") {
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;
449
+ if (((_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.isCrosshairEnabled()) || this.state.gui_states.mode.sphere) {
450
+ if (this.state.protectedData.axis === "z") {
451
+ this.state.cursorPage.z.index = this.state.nrrd_states.view.currentSliceIndex;
452
+ this.state.cursorPage.z.cursorPageX = this.state.nrrd_states.interaction.cursorPageX;
453
+ this.state.cursorPage.z.cursorPageY = this.state.nrrd_states.interaction.cursorPageY;
1022
454
  }
1023
- else if (this.protectedData.axis === "x") {
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;
455
+ else if (this.state.protectedData.axis === "x") {
456
+ this.state.cursorPage.x.index = this.state.nrrd_states.view.currentSliceIndex;
457
+ this.state.cursorPage.x.cursorPageX = this.state.nrrd_states.interaction.cursorPageX;
458
+ this.state.cursorPage.x.cursorPageY = this.state.nrrd_states.interaction.cursorPageY;
1027
459
  }
1028
- else if (this.protectedData.axis === "y") {
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;
460
+ else if (this.state.protectedData.axis === "y") {
461
+ this.state.cursorPage.y.index = this.state.nrrd_states.view.currentSliceIndex;
462
+ this.state.cursorPage.y.cursorPageX = this.state.nrrd_states.interaction.cursorPageX;
463
+ this.state.cursorPage.y.cursorPageY = this.state.nrrd_states.interaction.cursorPageY;
1032
464
  }
1033
465
  if (axisTo === "z") {
1034
- if (this.nrrd_states.interaction.isCursorSelect && !this.cursorPage.z.updated) {
1035
- if (this.protectedData.axis === "x") {
1036
- // convert x to z
1037
- convetObj = this.convertCursorPoint("x", "z", this.cursorPage.x.cursorPageX, this.cursorPage.x.cursorPageY, this.cursorPage.x.index);
466
+ if (this.state.nrrd_states.interaction.isCursorSelect && !this.state.cursorPage.z.updated) {
467
+ if (this.state.protectedData.axis === "x") {
468
+ convetObj = this.drawCore.convertCursorPoint("x", "z", this.state.cursorPage.x.cursorPageX, this.state.cursorPage.x.cursorPageY, this.state.cursorPage.x.index);
1038
469
  }
1039
- if (this.protectedData.axis === "y") {
1040
- // convert y to z
1041
- convetObj = this.convertCursorPoint("y", "z", this.cursorPage.y.cursorPageX, this.cursorPage.y.cursorPageY, this.cursorPage.y.index);
470
+ if (this.state.protectedData.axis === "y") {
471
+ convetObj = this.drawCore.convertCursorPoint("y", "z", this.state.cursorPage.y.cursorPageX, this.state.cursorPage.y.cursorPageY, this.state.cursorPage.y.index);
1042
472
  }
1043
473
  }
1044
474
  else {
1045
- // not cursor select, freedom to switch x -> z or y -> z and z -> x or z -> y
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;
475
+ this.state.nrrd_states.view.currentSliceIndex = this.state.cursorPage.z.index;
476
+ this.state.nrrd_states.view.preSliceIndex =
477
+ this.state.cursorPage.z.index * this.state.nrrd_states.image.ratios.z;
478
+ this.state.nrrd_states.interaction.cursorPageX = this.state.cursorPage.z.cursorPageX;
479
+ this.state.nrrd_states.interaction.cursorPageY = this.state.cursorPage.z.cursorPageY;
1051
480
  }
1052
481
  }
1053
482
  else if (axisTo === "x") {
1054
- if (this.nrrd_states.interaction.isCursorSelect && !this.cursorPage.x.updated) {
1055
- if (this.protectedData.axis === "z") {
1056
- // convert z to x
1057
- convetObj = this.convertCursorPoint("z", "x", this.cursorPage.z.cursorPageX, this.cursorPage.z.cursorPageY, this.cursorPage.z.index);
483
+ if (this.state.nrrd_states.interaction.isCursorSelect && !this.state.cursorPage.x.updated) {
484
+ if (this.state.protectedData.axis === "z") {
485
+ convetObj = this.drawCore.convertCursorPoint("z", "x", this.state.cursorPage.z.cursorPageX, this.state.cursorPage.z.cursorPageY, this.state.cursorPage.z.index);
1058
486
  }
1059
- if (this.protectedData.axis === "y") {
1060
- // convert y to x
1061
- convetObj = this.convertCursorPoint("y", "x", this.cursorPage.y.cursorPageX, this.cursorPage.y.cursorPageY, this.cursorPage.y.index);
487
+ if (this.state.protectedData.axis === "y") {
488
+ convetObj = this.drawCore.convertCursorPoint("y", "x", this.state.cursorPage.y.cursorPageX, this.state.cursorPage.y.cursorPageY, this.state.cursorPage.y.index);
1062
489
  }
1063
490
  }
1064
491
  else {
1065
- // not cursor select, freedom to switch z -> x or y -> x and x -> z or x -> y
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;
492
+ this.state.nrrd_states.view.currentSliceIndex = this.state.cursorPage.x.index;
493
+ this.state.nrrd_states.view.preSliceIndex =
494
+ this.state.cursorPage.x.index * this.state.nrrd_states.image.ratios.x;
495
+ this.state.nrrd_states.interaction.cursorPageX = this.state.cursorPage.x.cursorPageX;
496
+ this.state.nrrd_states.interaction.cursorPageY = this.state.cursorPage.x.cursorPageY;
1071
497
  }
1072
498
  }
1073
499
  else if (axisTo === "y") {
1074
- if (this.nrrd_states.interaction.isCursorSelect && !this.cursorPage.y.updated) {
1075
- if (this.protectedData.axis === "z") {
1076
- // convert z to y
1077
- convetObj = this.convertCursorPoint("z", "y", this.cursorPage.z.cursorPageX, this.cursorPage.z.cursorPageY, this.cursorPage.z.index);
500
+ if (this.state.nrrd_states.interaction.isCursorSelect && !this.state.cursorPage.y.updated) {
501
+ if (this.state.protectedData.axis === "z") {
502
+ convetObj = this.drawCore.convertCursorPoint("z", "y", this.state.cursorPage.z.cursorPageX, this.state.cursorPage.z.cursorPageY, this.state.cursorPage.z.index);
1078
503
  }
1079
- if (this.protectedData.axis === "x") {
1080
- // convert x to y
1081
- convetObj = this.convertCursorPoint("x", "y", this.cursorPage.x.cursorPageX, this.cursorPage.x.cursorPageY, this.cursorPage.x.index);
504
+ if (this.state.protectedData.axis === "x") {
505
+ convetObj = this.drawCore.convertCursorPoint("x", "y", this.state.cursorPage.x.cursorPageX, this.state.cursorPage.x.cursorPageY, this.state.cursorPage.x.index);
1082
506
  }
1083
507
  }
1084
508
  else {
1085
- // not cursor select, freedom to switch z -> y or x -> y and y -> z or y -> x
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;
509
+ this.state.nrrd_states.view.currentSliceIndex = this.state.cursorPage.y.index;
510
+ this.state.nrrd_states.view.preSliceIndex =
511
+ this.state.cursorPage.y.index * this.state.nrrd_states.image.ratios.y;
512
+ this.state.nrrd_states.interaction.cursorPageX = this.state.cursorPage.y.cursorPageX;
513
+ this.state.nrrd_states.interaction.cursorPageY = this.state.cursorPage.y.cursorPageY;
1091
514
  }
1092
515
  }
1093
516
  if (convetObj) {
1094
- // update convert cursor point, when cursor select
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;
517
+ this.state.nrrd_states.view.currentSliceIndex = convetObj.currentNewSliceIndex;
518
+ this.state.nrrd_states.view.preSliceIndex = convetObj.preSliceIndex;
519
+ this.state.nrrd_states.interaction.cursorPageX = convetObj.convertCursorNumX;
520
+ this.state.nrrd_states.interaction.cursorPageY = convetObj.convertCursorNumY;
1099
521
  convetObj = undefined;
1100
522
  switch (axisTo) {
1101
523
  case "x":
1102
- this.cursorPage.x.updated = true;
524
+ this.state.cursorPage.x.updated = true;
1103
525
  break;
1104
526
  case "y":
1105
- this.cursorPage.y.updated = true;
527
+ this.state.cursorPage.y.updated = true;
1106
528
  break;
1107
529
  case "z":
1108
- this.cursorPage.z.updated = true;
530
+ this.state.cursorPage.z.updated = true;
1109
531
  break;
1110
532
  }
1111
533
  }
1112
- if (this.cursorPage.x.updated &&
1113
- this.cursorPage.y.updated &&
1114
- this.cursorPage.z.updated) {
1115
- // one point convert to all axis, reset all updated status
1116
- this.nrrd_states.interaction.isCursorSelect = false;
534
+ if (this.state.cursorPage.x.updated &&
535
+ this.state.cursorPage.y.updated &&
536
+ this.state.cursorPage.z.updated) {
537
+ this.state.nrrd_states.interaction.isCursorSelect = false;
1117
538
  }
1118
539
  }
1119
- this.protectedData.axis = axisTo;
1120
- this.resetDisplaySlicesStatus();
540
+ this.state.protectedData.axis = axisTo;
541
+ this.sliceRenderPipeline.resetDisplaySlicesStatus();
1121
542
  }
1122
543
  addSkip(index) {
1123
- this.protectedData.skipSlicesDic[index] =
1124
- this.protectedData.backUpDisplaySlices[index];
1125
- if (index >= this.protectedData.displaySlices.length) {
1126
- this.nrrd_states.view.contrastNum = this.protectedData.displaySlices.length;
544
+ this.state.protectedData.skipSlicesDic[index] =
545
+ this.state.protectedData.backUpDisplaySlices[index];
546
+ if (index >= this.state.protectedData.displaySlices.length) {
547
+ this.state.nrrd_states.view.contrastNum = this.state.protectedData.displaySlices.length;
1127
548
  }
1128
549
  else {
1129
- this.nrrd_states.view.contrastNum = index;
550
+ this.state.nrrd_states.view.contrastNum = index;
1130
551
  }
1131
- this.resetDisplaySlicesStatus();
552
+ this.sliceRenderPipeline.resetDisplaySlicesStatus();
1132
553
  }
1133
554
  removeSkip(index) {
1134
- this.protectedData.skipSlicesDic[index] = undefined;
1135
- this.nrrd_states.view.contrastNum = 0;
1136
- this.resetDisplaySlicesStatus();
1137
- }
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() {
1146
- // To effectively reduce the js memory garbage
1147
- this.protectedData.allSlicesArray.length = 0;
1148
- this.protectedData.displaySlices.length = 0;
1149
- // Phase 6: Clear all undo/redo stacks
1150
- this.undoManager.clearAll();
1151
- // Phase 3: Reset MaskVolume storage to 1×1×1 placeholders
1152
- this.protectedData.maskData.volumes = this.nrrd_states.image.layers.reduce((acc, id) => {
1153
- acc[id] = new MaskVolume(1, 1, 1, 1);
1154
- return acc;
1155
- }, {});
1156
- // Clear dedicated SphereMaskVolume
1157
- this.nrrd_states.sphere.sphereMaskVolume = null;
1158
- // Invalidate reusable slice buffer
1159
- this.invalidateSliceBuffer();
1160
- this.clearDictionary(this.protectedData.skipSlicesDic);
1161
- // this.nrrd_states.view.previousPanelL = this.nrrd_states.view.previousPanelT = -99999;
1162
- this.protectedData.canvases.displayCanvas.style.left =
1163
- this.protectedData.canvases.drawingCanvas.style.left = "";
1164
- this.protectedData.canvases.displayCanvas.style.top =
1165
- this.protectedData.canvases.drawingCanvas.style.top = "";
1166
- this.protectedData.backUpDisplaySlices.length = 0;
1167
- this.protectedData.mainPreSlices = undefined;
1168
- this.protectedData.currentShowingSlice = undefined;
1169
- this.initState = true;
1170
- this.protectedData.axis = "z";
1171
- this.nrrd_states.view.sizeFactor = this.baseCanvasesSize;
1172
- this.gui_states.viewConfig.mainAreaSize = this.baseCanvasesSize;
1173
- this.resetLayerCanvas();
1174
- this.protectedData.canvases.drawingCanvas.width =
1175
- this.protectedData.canvases.drawingCanvas.width;
1176
- this.protectedData.canvases.displayCanvas.width =
1177
- this.protectedData.canvases.displayCanvas.width;
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;
555
+ this.state.protectedData.skipSlicesDic[index] = undefined;
556
+ this.state.nrrd_states.view.contrastNum = 0;
557
+ this.sliceRenderPipeline.resetDisplaySlicesStatus();
1182
558
  }
1183
559
  setSliceMoving(step) {
1184
- if (this.protectedData.mainPreSlices) {
1185
- // Accumulate steps so no keydown events are lost
560
+ if (this.state.protectedData.mainPreSlices) {
1186
561
  this._pendingSliceStep += step;
1187
- // RAF throttle: render at most once per frame, but apply ALL accumulated steps
1188
562
  if (this._sliceRAFId !== null)
1189
563
  return;
1190
564
  this._sliceRAFId = requestAnimationFrame(() => {
1191
565
  this._sliceRAFId = null;
1192
566
  const totalStep = this._pendingSliceStep;
1193
567
  this._pendingSliceStep = 0;
1194
- this.protectedData.isDrawing = true;
568
+ this.state.protectedData.isDrawing = true;
1195
569
  this.setSyncsliceNum();
1196
570
  this.dragOperator.updateIndex(totalStep);
1197
571
  this.setIsDrawFalse(1000);
@@ -1199,493 +573,233 @@ export class NrrdTools extends DrawToolCore {
1199
573
  }
1200
574
  }
1201
575
  setMainAreaSize(factor) {
1202
- this.nrrd_states.view.sizeFactor = factor;
1203
- if (this.nrrd_states.view.sizeFactor >= 8) {
1204
- this.nrrd_states.view.sizeFactor = 8;
576
+ this.state.nrrd_states.view.sizeFactor = factor;
577
+ if (this.state.nrrd_states.view.sizeFactor >= 8) {
578
+ this.state.nrrd_states.view.sizeFactor = 8;
1205
579
  }
1206
- else if (this.nrrd_states.view.sizeFactor <= 1) {
1207
- this.nrrd_states.view.sizeFactor = 1;
580
+ else if (this.state.nrrd_states.view.sizeFactor <= 1) {
581
+ this.state.nrrd_states.view.sizeFactor = 1;
1208
582
  }
1209
- this.resizePaintArea(this.nrrd_states.view.sizeFactor);
583
+ this.resizePaintArea(this.state.nrrd_states.view.sizeFactor);
1210
584
  this.resetPaintAreaUIPosition();
1211
- // this.setIsDrawFalse(1000);
585
+ }
586
+ switchAllSlicesArrayData(allSlices) {
587
+ this.state.protectedData.allSlicesArray.length = 0;
588
+ this.state.protectedData.allSlicesArray = [...allSlices];
589
+ this.sliceRenderPipeline.resetDisplaySlicesStatus();
590
+ }
591
+ appendLoadingbar(loadingbar) {
592
+ this.state.protectedData.mainAreaContainer.appendChild(loadingbar);
593
+ }
594
+ // ═══════════════════════════════════════════════════════════════════════════
595
+ // 6. Public API — Data Getters
596
+ // ═══════════════════════════════════════════════════════════════════════════
597
+ getCurrentImageDimension() {
598
+ return this.state.nrrd_states.image.dimensions;
599
+ }
600
+ getVoxelSpacing() {
601
+ return this.state.nrrd_states.image.voxelSpacing;
602
+ }
603
+ getSpaceOrigin() {
604
+ return this.state.nrrd_states.image.spaceOrigin;
605
+ }
606
+ getMaskData() {
607
+ return this.state.protectedData.maskData;
1212
608
  }
1213
609
  getContainer() {
1214
- return this.protectedData.mainAreaContainer;
610
+ return this.state.protectedData.mainAreaContainer;
1215
611
  }
1216
612
  getDrawingCanvas() {
1217
- return this.protectedData.canvases.drawingCanvas;
613
+ return this.state.protectedData.canvases.drawingCanvas;
1218
614
  }
1219
615
  getNrrdToolsSettings() {
1220
- return this.nrrd_states;
616
+ return this.state.nrrd_states;
1221
617
  }
1222
618
  getMaxSliceNum() {
1223
- if (this.nrrd_states.view.showContrast) {
619
+ if (this.state.nrrd_states.view.showContrast) {
1224
620
  return [
1225
- this.nrrd_states.view.maxIndex,
1226
- this.nrrd_states.view.maxIndex * this.protectedData.displaySlices.length,
621
+ this.state.nrrd_states.view.maxIndex,
622
+ this.state.nrrd_states.view.maxIndex * this.state.protectedData.displaySlices.length,
1227
623
  ];
1228
624
  }
1229
625
  else {
1230
- return [this.nrrd_states.view.maxIndex];
626
+ return [this.state.nrrd_states.view.maxIndex];
1231
627
  }
1232
628
  }
1233
629
  getCurrentSlicesNumAndContrastNum() {
1234
630
  return {
1235
- currentSliceIndex: this.nrrd_states.view.currentSliceIndex,
1236
- contrastIndex: this.nrrd_states.view.contrastNum,
631
+ currentSliceIndex: this.state.nrrd_states.view.currentSliceIndex,
632
+ contrastIndex: this.state.nrrd_states.view.contrastNum,
1237
633
  };
1238
634
  }
1239
635
  getCurrentSliceIndex() {
1240
- return Math.ceil(this.protectedData.mainPreSlices.index / this.nrrd_states.image.RSARatio);
636
+ return Math.ceil(this.state.protectedData.mainPreSlices.index / this.state.nrrd_states.image.RSARatio);
1241
637
  }
1242
638
  getIsShowContrastState() {
1243
- return this.nrrd_states.view.showContrast;
1244
- }
1245
- /**
1246
- * Give a delay time to finish the last drawing before upcoming interrupt opreations.
1247
- * Give a delay time number (ms) to disable the draw function,
1248
- * After your interrupt opeartion, you should enable the draw fucntion.
1249
- * @param target number
1250
- */
1251
- setIsDrawFalse(target) {
1252
- this.preTimer = setTimeout(() => {
1253
- this.protectedData.isDrawing = false;
1254
- if (this.preTimer) {
1255
- window.clearTimeout(this.preTimer);
1256
- this.preTimer = undefined;
1257
- }
1258
- }, target);
1259
- }
1260
- setDisplaySlicesBaseOnAxis() {
1261
- this.protectedData.displaySlices.length = 0;
1262
- this.protectedData.backUpDisplaySlices.length = 0;
1263
- this.protectedData.allSlicesArray.forEach((slices) => {
1264
- this.protectedData.backUpDisplaySlices.push(slices[this.protectedData.axis]);
1265
- });
1266
- this.loadDisplaySlicesArray();
1267
- }
1268
- loadDisplaySlicesArray() {
1269
- const remainSlices = Object.values(this.protectedData.skipSlicesDic);
1270
- if (remainSlices.length === 0) {
1271
- // load all display slices
1272
- this.protectedData.backUpDisplaySlices.forEach((slice, index) => {
1273
- this.protectedData.skipSlicesDic[index] = slice;
1274
- this.protectedData.displaySlices.push(slice);
1275
- });
1276
- }
1277
- else {
1278
- remainSlices.forEach((slice, index) => {
1279
- if (!!slice) {
1280
- this.protectedData.displaySlices.push(this.protectedData.backUpDisplaySlices[index]);
1281
- this.protectedData.skipSlicesDic[index] =
1282
- this.protectedData.backUpDisplaySlices[index];
1283
- }
1284
- });
1285
- }
1286
- }
1287
- switchAllSlicesArrayData(allSlices) {
1288
- this.protectedData.allSlicesArray.length = 0;
1289
- this.protectedData.allSlicesArray = [...allSlices];
1290
- this.resetDisplaySlicesStatus();
1291
- }
1292
- resetDisplaySlicesStatus() {
1293
- // reload slice data
1294
- this.setDisplaySlicesBaseOnAxis();
1295
- // reset canvas attribute for drag and draw
1296
- this.setupConfigs();
1297
- }
1298
- setupConfigs() {
1299
- // reset main slice
1300
- this.setMainPreSlice();
1301
- // update the max index for drag and slider
1302
- this.updateMaxIndex();
1303
- // reset origin canvas and the nrrd_states origin Width/height
1304
- // reset the current index
1305
- // (also calls resizePaintArea → reloads masks, resizes canvases)
1306
- this.setOriginCanvasAndPre();
1307
- // update the show number div on top area
1308
- this.dragOperator.updateShowNumDiv(this.nrrd_states.view.contrastNum);
1309
- // repaint all contrast images
1310
- this.repraintCurrentContrastSlice();
1311
- // Refresh display after contrast repaint (no need for full resizePaintArea
1312
- // since canvases were already resized in setOriginCanvasAndPre above)
1313
- this.redrawDisplayCanvas();
1314
- this.compositeAllLayers();
1315
- // Sync slider metadata with current volume (replaces old getGuiSettings() side-effect)
1316
- this.syncGuiParameterSettings();
1317
- }
1318
- setMainPreSlice() {
1319
- this.protectedData.mainPreSlices = this.protectedData.displaySlices[0];
1320
- if (this.protectedData.mainPreSlices) {
1321
- this.nrrd_states.image.RSARatio = this.protectedData.mainPreSlices.RSARatio;
1322
- }
1323
- }
1324
- setOriginCanvasAndPre() {
1325
- if (this.protectedData.mainPreSlices) {
1326
- if (this.nrrd_states.view.preSliceIndex > this.nrrd_states.view.maxIndex)
1327
- this.nrrd_states.view.preSliceIndex = this.nrrd_states.view.maxIndex;
1328
- if (this.initState) {
1329
- this.nrrd_states.view.preSliceIndex =
1330
- this.protectedData.mainPreSlices.initIndex *
1331
- this.nrrd_states.image.RSARatio;
1332
- this.nrrd_states.view.currentSliceIndex =
1333
- this.protectedData.mainPreSlices.initIndex;
1334
- }
1335
- else {
1336
- // !need to change
1337
- // todo
1338
- this.protectedData.mainPreSlices.index = this.nrrd_states.view.preSliceIndex;
1339
- }
1340
- this.protectedData.canvases.originCanvas =
1341
- this.protectedData.mainPreSlices.canvas;
1342
- this.updateOriginAndChangedWH();
1343
- }
1344
- }
1345
- afterLoadSlice() {
1346
- this.setMainPreSlice();
1347
- this.setOriginCanvasAndPre();
1348
- this.protectedData.currentShowingSlice = this.protectedData.mainPreSlices;
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;
1352
- // Phase 6: Reset undo/redo stacks on new dataset load
1353
- this.undoManager.clearAll();
1354
- // compute max index
1355
- this.updateMaxIndex();
1356
- this.dragOperator.updateShowNumDiv(this.nrrd_states.view.contrastNum);
1357
- this.syncGuiParameterSettings();
1358
- this.initState = false;
1359
- }
1360
- updateMaxIndex() {
1361
- if (this.protectedData.mainPreSlices) {
1362
- this.nrrd_states.view.maxIndex = this.protectedData.mainPreSlices.MaxIndex;
1363
- }
1364
- }
1365
- /**
1366
- * Update the original canvas size, allow set to threejs load one (pixel distance not the mm).
1367
- * Then update the changedWidth and changedHeight based on the sizeFactor.
1368
- */
1369
- updateOriginAndChangedWH() {
1370
- this.nrrd_states.image.originWidth =
1371
- this.protectedData.canvases.originCanvas.width;
1372
- this.nrrd_states.image.originHeight =
1373
- this.protectedData.canvases.originCanvas.height;
1374
- // Let resizePaintArea be the sole setter of changedWidth/changedHeight.
1375
- // Setting them here would defeat the sizeChanged detection in resizePaintArea,
1376
- // causing canvas elements to keep stale dimensions after axis switches.
1377
- this.resizePaintArea(this.nrrd_states.view.sizeFactor);
1378
- this.resetPaintAreaUIPosition();
1379
- }
1380
- /**
1381
- * Keep all contrast slice index to same.
1382
- * Synchronize the slice indexes of all the contrasts so that they are consistent with the main slice's index.
1383
- */
1384
- setSyncsliceNum() {
1385
- this.protectedData.displaySlices.forEach((slice, index) => {
1386
- if (index !== 0) {
1387
- slice.index = this.protectedData.mainPreSlices.index;
1388
- }
1389
- });
1390
- }
1391
- appendLoadingbar(loadingbar) {
1392
- this.protectedData.mainAreaContainer.appendChild(loadingbar);
1393
- }
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() {
1402
- // Phase 3 Task 3.1: Only clear the active layer's MaskVolume
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;
1406
- // Re-init only the active layer's MaskVolume
1407
- this.protectedData.maskData.volumes[activeLayer] = new MaskVolume(w, h, d, 1);
1408
- // Phase 6 Task 6.6: Clear undo/redo stack for this layer (volume too large to snapshot)
1409
- this.undoManager.clearLayer(activeLayer);
1410
- // Phase 3 Task 3.2: Notify external that this layer's volume was cleared
1411
- this.annotationCallbacks.onLayerVolumeCleared(activeLayer);
1412
- }
1413
- // Invalidate reusable slice buffer
1414
- this.invalidateSliceBuffer();
1415
- // Reload all layers to canvas (restores other layers' visuals)
1416
- this.reloadMasksFromVolume();
1417
- }
1418
- /**
1419
- * Reset the draw and display canvases layout after mouse pan.
1420
- * If no params in, then center the draw and display canvases.
1421
- * @param l number, Offset to the left
1422
- * @param t number, Offset to the top
1423
- */
1424
- resetPaintAreaUIPosition(l, t) {
1425
- if (l && t) {
1426
- this.protectedData.canvases.displayCanvas.style.left =
1427
- this.protectedData.canvases.drawingCanvas.style.left = l + "px";
1428
- this.protectedData.canvases.displayCanvas.style.top =
1429
- this.protectedData.canvases.drawingCanvas.style.top = t + "px";
1430
- }
1431
- else {
1432
- this.protectedData.canvases.displayCanvas.style.left =
1433
- this.protectedData.canvases.drawingCanvas.style.left = "";
1434
- this.protectedData.canvases.displayCanvas.style.top =
1435
- this.protectedData.canvases.drawingCanvas.style.top = "";
1436
- this.protectedData.mainAreaContainer.style.justifyContent = "center";
1437
- this.protectedData.mainAreaContainer.style.alignItems = "center";
639
+ return this.state.nrrd_states.view.showContrast;
640
+ }
641
+ // ═══════════════════════════════════════════════════════════════════════════
642
+ // 7. Delegated LayerChannelManager
643
+ // ═══════════════════════════════════════════════════════════════════════════
644
+ setActiveLayer(layerId) { this.layerChannelManager.setActiveLayer(layerId); }
645
+ getActiveLayer() { return this.layerChannelManager.getActiveLayer(); }
646
+ setActiveChannel(channel) { this.layerChannelManager.setActiveChannel(channel); }
647
+ getActiveChannel() { return this.layerChannelManager.getActiveChannel(); }
648
+ setActiveSphereType(type) { this.layerChannelManager.setActiveSphereType(type); }
649
+ getActiveSphereType() { return this.layerChannelManager.getActiveSphereType(); }
650
+ setLayerVisible(layerId, visible) { this.layerChannelManager.setLayerVisible(layerId, visible); }
651
+ isLayerVisible(layerId) { return this.layerChannelManager.isLayerVisible(layerId); }
652
+ setChannelVisible(layerId, channel, visible) { this.layerChannelManager.setChannelVisible(layerId, channel, visible); }
653
+ isChannelVisible(layerId, channel) { return this.layerChannelManager.isChannelVisible(layerId, channel); }
654
+ getLayerVisibility() { return this.layerChannelManager.getLayerVisibility(); }
655
+ getChannelVisibility() { return this.layerChannelManager.getChannelVisibility(); }
656
+ hasLayerData(layerId) { return this.layerChannelManager.hasLayerData(layerId); }
657
+ setChannelColor(layerId, channel, color) { this.layerChannelManager.setChannelColor(layerId, channel, color); }
658
+ getChannelColor(layerId, channel) { return this.layerChannelManager.getChannelColor(layerId, channel); }
659
+ getChannelHexColor(layerId, channel) { return this.layerChannelManager.getChannelHexColor(layerId, channel); }
660
+ getChannelCssColor(layerId, channel) { return this.layerChannelManager.getChannelCssColor(layerId, channel); }
661
+ setChannelColors(layerId, colorMap) { this.layerChannelManager.setChannelColors(layerId, colorMap); }
662
+ setAllLayersChannelColor(channel, color) { this.layerChannelManager.setAllLayersChannelColor(channel, color); }
663
+ resetChannelColors(layerId, channel) { this.layerChannelManager.resetChannelColors(layerId, channel); }
664
+ // ═══════════════════════════════════════════════════════════════════════════
665
+ // 8. Delegated — SliceRenderPipeline
666
+ // ═══════════════════════════════════════════════════════════════════════════
667
+ updateOriginAndChangedWH() { this.sliceRenderPipeline.updateOriginAndChangedWH(); }
668
+ setSyncsliceNum() { this.sliceRenderPipeline.setSyncsliceNum(); }
669
+ resetPaintAreaUIPosition(l, t) { this.sliceRenderPipeline.resetPaintAreaUIPosition(l, t); }
670
+ resetLayerCanvas() { this.sliceRenderPipeline.resetLayerCanvas(); }
671
+ redrawMianPreOnDisplayCanvas() { this.sliceRenderPipeline.redrawMianPreOnDisplayCanvas(); }
672
+ resizePaintArea(factor) { this.sliceRenderPipeline.resizePaintArea(factor); }
673
+ flipDisplayImageByAxis() { this.sliceRenderPipeline.flipDisplayImageByAxis(); }
674
+ setEmptyCanvasSize(axis) { this.sliceRenderPipeline.setEmptyCanvasSize(axis); }
675
+ redrawDisplayCanvas() { this.sliceRenderPipeline.redrawDisplayCanvas(); }
676
+ reloadMasksFromVolume() { this.sliceRenderPipeline.reloadMasksFromVolume(); }
677
+ // ═══════════════════════════════════════════════════════════════════════════
678
+ // 9. Delegated — DataLoader
679
+ // ═══════════════════════════════════════════════════════════════════════════
680
+ setAllSlices(allSlices) { this.dataLoader.setAllSlices(allSlices); }
681
+ setMasksData(masksData, loadingBar) { this.dataLoader.setMasksData(masksData, loadingBar); }
682
+ setMasksFromNIfTI(layerVoxels, loadingBar) { this.dataLoader.setMasksFromNIfTI(layerVoxels, loadingBar); }
683
+ // ═══════════════════════════════════════════════════════════════════════════
684
+ // 10. Sphere Orchestration
685
+ // ═══════════════════════════════════════════════════════════════════════════
686
+ setCalculateDistanceSphere(x, y, sliceIndex, cal_position) {
687
+ this.state.nrrd_states.sphere.sphereRadius = 5;
688
+ const steps = sliceIndex - this.state.nrrd_states.view.currentSliceIndex;
689
+ this.setSliceMoving(steps * this.state.protectedData.displaySlices.length);
690
+ const mouseX = x * this.state.nrrd_states.view.sizeFactor;
691
+ const mouseY = y * this.state.nrrd_states.view.sizeFactor;
692
+ this.state.nrrd_states.sphere.sphereOrigin[this.state.protectedData.axis] = [
693
+ mouseX, mouseY, sliceIndex,
694
+ ];
695
+ this.drawCore.crosshairTool.setUpSphereOrigins(mouseX, mouseY, sliceIndex);
696
+ const originCopy = JSON.parse(JSON.stringify(this.state.nrrd_states.sphere.sphereOrigin));
697
+ switch (cal_position) {
698
+ case "tumour":
699
+ this.state.nrrd_states.sphere.tumourSphereOrigin = originCopy;
700
+ break;
701
+ case "skin":
702
+ this.state.nrrd_states.sphere.skinSphereOrigin = originCopy;
703
+ break;
704
+ case "nipple":
705
+ this.state.nrrd_states.sphere.nippleSphereOrigin = originCopy;
706
+ break;
707
+ case "ribcage":
708
+ this.state.nrrd_states.sphere.ribSphereOrigin = originCopy;
709
+ break;
1438
710
  }
711
+ this.drawCore.drawCalculatorSphere(this.state.nrrd_states.sphere.sphereRadius);
712
+ this.drawCore.sphereTool.writeAllCalculatorSpheresToVolume();
713
+ this.drawCore.sphereTool.refreshSphereCanvas();
1439
714
  }
1440
715
  /**
1441
- * Clear masks on drawingCanvas layers.
716
+ * Refresh sphere canvas from sphereMaskVolume for the current slice/axis.
1442
717
  */
1443
- resetLayerCanvas() {
1444
- this.protectedData.canvases.drawingCanvasLayerMaster.width =
1445
- this.protectedData.canvases.drawingCanvasLayerMaster.width;
1446
- for (const [, target] of this.protectedData.layerTargets) {
1447
- target.canvas.width = target.canvas.width;
1448
- }
718
+ refreshSphereOverlay() {
719
+ this.drawCore.refreshSphereOverlay();
1449
720
  }
1450
721
  /**
1451
722
  * 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
723
  */
1459
724
  enterSphereMode() {
1460
725
  var _a;
1461
- // Disable left-click drag for slice navigation
1462
726
  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);
727
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool('sphere');
728
+ const w = this.state.nrrd_states.view.changedWidth;
729
+ const h = this.state.nrrd_states.view.changedHeight;
730
+ for (const layerId of this.state.nrrd_states.image.layers) {
731
+ const target = this.state.protectedData.layerTargets.get(layerId);
1470
732
  if (target) {
1471
733
  target.ctx.clearRect(0, 0, target.canvas.width, target.canvas.height);
1472
734
  }
1473
735
  }
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)
736
+ this.state.protectedData.ctxes.drawingLayerMasterCtx.clearRect(0, 0, w, h);
1477
737
  this.refreshSphereOverlay();
1478
738
  }
1479
739
  /**
1480
740
  * 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
741
  */
1488
742
  exitSphereMode() {
1489
743
  var _a;
1490
- // Restore left-click drag for slice navigation
1491
744
  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
745
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool('pencil');
746
+ this.state.protectedData.ctxes.drawingSphereCtx.clearRect(0, 0, this.state.protectedData.canvases.drawingSphereCanvas.width, this.state.protectedData.canvases.drawingSphereCanvas.height);
1497
747
  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
748
  this.reloadMasksFromVolume();
1502
749
  }
1503
- redrawMianPreOnDisplayCanvas() {
1504
- var _a;
1505
- this.protectedData.canvases.displayCanvas.width =
1506
- this.protectedData.canvases.displayCanvas.width;
1507
- this.protectedData.canvases.displayCanvas.height =
1508
- this.protectedData.canvases.displayCanvas.height;
1509
- this.protectedData.canvases.originCanvas.width =
1510
- this.protectedData.canvases.originCanvas.width;
1511
- if (this.protectedData.mainPreSlices) {
1512
- this.protectedData.mainPreSlices.repaint.call(this.protectedData.mainPreSlices);
1513
- this.flipDisplayImageByAxis();
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);
1516
- }
1517
- }
1518
- /**
1519
- * Resize the draw and display canvas size based on the input size factor number.
1520
- * @param factor number
1521
- */
1522
- resizePaintArea(factor) {
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;
1527
- // Always clear display/drawing/origin canvases (needed for contrast updates)
1528
- this.protectedData.canvases.originCanvas.width =
1529
- this.protectedData.canvases.originCanvas.width;
1530
- this.protectedData.canvases.displayCanvas.width =
1531
- this.protectedData.canvases.displayCanvas.width;
1532
- this.protectedData.canvases.drawingCanvas.width =
1533
- this.protectedData.canvases.drawingCanvas.width;
1534
- if (sizeChanged) {
1535
- // Only clear and resize layer canvases when size actually changes.
1536
- // Skipping this avoids the expensive reloadMasksFromVolume() call
1537
- // during contrast toggle (where size stays the same).
1538
- this.resetLayerCanvas();
1539
- this.nrrd_states.view.changedWidth = newWidth;
1540
- this.nrrd_states.view.changedHeight = newHeight;
1541
- this.protectedData.canvases.displayCanvas.width = newWidth;
1542
- this.protectedData.canvases.displayCanvas.height = newHeight;
1543
- this.protectedData.canvases.drawingCanvas.width = newWidth;
1544
- this.protectedData.canvases.drawingCanvas.height = newHeight;
1545
- this.protectedData.canvases.drawingCanvasLayerMaster.width = newWidth;
1546
- this.protectedData.canvases.drawingCanvasLayerMaster.height = newHeight;
1547
- for (const [, target] of this.protectedData.layerTargets) {
1548
- target.canvas.width = newWidth;
1549
- target.canvas.height = newHeight;
1550
- }
1551
- }
1552
- this.redrawDisplayCanvas();
1553
- if (sizeChanged) {
1554
- // Phase 3: Reload masks from MaskVolume only when canvas size changed
1555
- this.reloadMasksFromVolume();
1556
- }
1557
- else {
1558
- // Size unchanged (e.g. contrast toggle): layer canvases still have
1559
- // valid data, just recomposite to master for the start() render loop.
1560
- this.compositeAllLayers();
1561
- }
1562
- // Refresh sphere overlay from volume after resize/contrast change
1563
- this.refreshSphereOverlay();
1564
- }
1565
- /**
1566
- * Phase 3: Reload all mask layers from MaskVolume using buffer reuse
1567
- * Replaces the old reloadMaskToLayer approach
1568
- */
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
- }
1575
- const axis = this.protectedData.axis;
1576
- let sliceIndex = this.nrrd_states.view.currentSliceIndex;
1577
- // Clamp sliceIndex to valid range for current axis
1578
- // (currentSliceIndex may not be updated yet when switching axes)
1579
- try {
1580
- const vol = this.getVolumeForLayer(this.nrrd_states.image.layers[0]);
1581
- const dims = vol.getDimensions();
1582
- const maxSlice = axis === "x" ? dims.width : axis === "y" ? dims.height : dims.depth;
1583
- if (sliceIndex >= maxSlice)
1584
- sliceIndex = maxSlice - 1;
1585
- if (sliceIndex < 0)
1586
- sliceIndex = 0;
1587
- }
1588
- catch ( /* volume not ready */_a) { /* volume not ready */ }
1589
- // Get a single reusable buffer shared across all layer renders
1590
- const buffer = this.getOrCreateSliceBuffer(axis);
1591
- if (!buffer)
1592
- return;
1593
- const w = this.nrrd_states.view.changedWidth;
1594
- const h = this.nrrd_states.view.changedHeight;
1595
- // Clear and render each layer using the shared buffer
1596
- for (const layerId of this.nrrd_states.image.layers) {
1597
- const target = this.protectedData.layerTargets.get(layerId);
1598
- if (!target)
1599
- continue;
1600
- target.ctx.clearRect(0, 0, w, h);
1601
- this.renderSliceToCanvas(layerId, axis, sliceIndex, buffer, target.ctx, w, h);
1602
- }
1603
- // Composite all layers to master canvas
1604
- this.compositeAllLayers();
1605
- }
1606
- // compositeAllLayers() is inherited from CommToolsData
1607
- /**
1608
- * flip the canvas to a correct position.
1609
- * This is because the slice canvas from threejs is not in a correct 2D postion.
1610
- * Thus, everytime when we redraw the display canvas, we need to flip to draw the origin canvas from threejs.
1611
- * Under different axis(sagittal, Axial, Coronal), the flip orientation is different.
1612
- */
1613
- flipDisplayImageByAxis() {
1614
- var _a, _b, _c, _d, _e, _f;
1615
- if (this.protectedData.axis === "x") {
1616
- (_a = this.protectedData.ctxes.displayCtx) === null || _a === void 0 ? void 0 : _a.scale(-1, -1);
1617
- (_b = this.protectedData.ctxes.displayCtx) === null || _b === void 0 ? void 0 : _b.translate(-this.nrrd_states.view.changedWidth, -this.nrrd_states.view.changedHeight);
1618
- }
1619
- else if (this.protectedData.axis === "z") {
1620
- (_c = this.protectedData.ctxes.displayCtx) === null || _c === void 0 ? void 0 : _c.scale(1, -1);
1621
- (_d = this.protectedData.ctxes.displayCtx) === null || _d === void 0 ? void 0 : _d.translate(0, -this.nrrd_states.view.changedHeight);
1622
- }
1623
- else if (this.protectedData.axis === "y") {
1624
- (_e = this.protectedData.ctxes.displayCtx) === null || _e === void 0 ? void 0 : _e.scale(1, -1);
1625
- (_f = this.protectedData.ctxes.displayCtx) === null || _f === void 0 ? void 0 : _f.translate(0, -this.nrrd_states.view.changedHeight);
1626
- }
1627
- }
1628
- clearDictionary(dic) {
1629
- for (var key in dic) {
1630
- delete dic[key];
1631
- }
1632
- }
1633
- /**
1634
- * Set the empty canvas width and height based on the axis (pixel distance not the mm), to reduce duplicate codes.
1635
- *
1636
- * @param axis
1637
- */
1638
- setEmptyCanvasSize(axis) {
1639
- switch (!!axis ? axis : this.protectedData.axis) {
1640
- case "x":
1641
- this.protectedData.canvases.emptyCanvas.width =
1642
- this.nrrd_states.image.nrrd_z_pixel;
1643
- this.protectedData.canvases.emptyCanvas.height =
1644
- this.nrrd_states.image.nrrd_y_pixel;
1645
- break;
1646
- case "y":
1647
- this.protectedData.canvases.emptyCanvas.width =
1648
- this.nrrd_states.image.nrrd_x_pixel;
1649
- this.protectedData.canvases.emptyCanvas.height =
1650
- this.nrrd_states.image.nrrd_z_pixel;
1651
- break;
1652
- case "z":
1653
- this.protectedData.canvases.emptyCanvas.width =
1654
- this.nrrd_states.image.nrrd_x_pixel;
1655
- this.protectedData.canvases.emptyCanvas.height =
1656
- this.nrrd_states.image.nrrd_y_pixel;
1657
- break;
1658
- }
1659
- }
1660
- /******************************** redraw display canvas ***************************************/
1661
- /**
1662
- * Redraw current contrast image to display canvas.
1663
- * It is more related to change the contrast slice image's window width or center.
1664
- */
1665
- redrawDisplayCanvas() {
1666
- var _a, _b, _c;
1667
- this.dragOperator.updateCurrentContrastSlice();
1668
- this.protectedData.canvases.displayCanvas.width =
1669
- this.protectedData.canvases.displayCanvas.width;
1670
- this.protectedData.canvases.displayCanvas.height =
1671
- this.protectedData.canvases.displayCanvas.height;
1672
- this.protectedData.canvases.originCanvas.width =
1673
- this.protectedData.canvases.originCanvas.width;
1674
- if (this.protectedData.currentShowingSlice) {
1675
- this.protectedData.currentShowingSlice.repaint.call(this.protectedData.currentShowingSlice);
1676
- (_a = this.protectedData.ctxes.displayCtx) === null || _a === void 0 ? void 0 : _a.save();
1677
- this.flipDisplayImageByAxis();
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);
1679
- (_c = this.protectedData.ctxes.displayCtx) === null || _c === void 0 ? void 0 : _c.restore();
1680
- }
750
+ // ═══════════════════════════════════════════════════════════════════════════
751
+ // 11. Clear / Reset
752
+ // ═══════════════════════════════════════════════════════════════════════════
753
+ clearActiveLayer() {
754
+ if (this.state.nrrd_states.image.dimensions.length === 3) {
755
+ const [w, h, d] = this.state.nrrd_states.image.dimensions;
756
+ const activeLayer = this.state.gui_states.layerChannel.layer;
757
+ this.state.protectedData.maskData.volumes[activeLayer] = new MaskVolume(w, h, d, 1);
758
+ this.drawCore.undoManager.clearLayer(activeLayer);
759
+ this.state.annotationCallbacks.onLayerVolumeCleared(activeLayer);
760
+ }
761
+ this.drawCore.renderer.invalidateSliceBuffer();
762
+ this.reloadMasksFromVolume();
1681
763
  }
1682
- /**
1683
- * Config mouse slice wheel event.
1684
- */
764
+ reset() {
765
+ this.state.protectedData.allSlicesArray.length = 0;
766
+ this.state.protectedData.displaySlices.length = 0;
767
+ this.drawCore.undoManager.clearAll();
768
+ this.state.protectedData.maskData.volumes = this.state.nrrd_states.image.layers.reduce((acc, id) => {
769
+ acc[id] = new MaskVolume(1, 1, 1, 1);
770
+ return acc;
771
+ }, {});
772
+ this.state.nrrd_states.sphere.sphereMaskVolume = null;
773
+ this.drawCore.renderer.invalidateSliceBuffer();
774
+ this.clearDictionary(this.state.protectedData.skipSlicesDic);
775
+ this.state.protectedData.canvases.displayCanvas.style.left =
776
+ this.state.protectedData.canvases.drawingCanvas.style.left = "";
777
+ this.state.protectedData.canvases.displayCanvas.style.top =
778
+ this.state.protectedData.canvases.drawingCanvas.style.top = "";
779
+ this.state.protectedData.backUpDisplaySlices.length = 0;
780
+ this.state.protectedData.mainPreSlices = undefined;
781
+ this.state.protectedData.currentShowingSlice = undefined;
782
+ this.sliceRenderPipeline.resetInitState();
783
+ this.state.protectedData.axis = "z";
784
+ this.state.nrrd_states.view.sizeFactor = this.state.baseCanvasesSize;
785
+ this.state.gui_states.viewConfig.mainAreaSize = this.state.baseCanvasesSize;
786
+ this.resetLayerCanvas();
787
+ this.state.protectedData.canvases.drawingCanvas.width =
788
+ this.state.protectedData.canvases.drawingCanvas.width;
789
+ this.state.protectedData.canvases.displayCanvas.width =
790
+ this.state.protectedData.canvases.displayCanvas.width;
791
+ this.state.nrrd_states.sphere.tumourSphereOrigin = null;
792
+ this.state.nrrd_states.sphere.ribSphereOrigin = null;
793
+ this.state.nrrd_states.sphere.skinSphereOrigin = null;
794
+ this.state.nrrd_states.sphere.nippleSphereOrigin = null;
795
+ }
796
+ // ═══════════════════════════════════════════════════════════════════════════
797
+ // 12. Internal — Input Events
798
+ // ═══════════════════════════════════════════════════════════════════════════
1685
799
  configMouseSliceWheel() {
1686
800
  const handleMouseZoomSliceWheelMove = (e) => {
1687
801
  var _a;
1688
- if ((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
802
+ if ((_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
1689
803
  return;
1690
804
  }
1691
805
  e.preventDefault();
@@ -1698,23 +812,53 @@ export class NrrdTools extends DrawToolCore {
1698
812
  };
1699
813
  return handleMouseZoomSliceWheelMove;
1700
814
  }
1701
- /**
1702
- * Update mouse wheel event.
1703
- */
1704
815
  updateMouseWheelEvent() {
1705
- this.protectedData.canvases.drawingCanvas.removeEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
1706
- switch (this._keyboardSettings.mouseWheel) {
816
+ this.state.protectedData.canvases.drawingCanvas.removeEventListener("wheel", this.drawCore.drawingPrameters.handleMouseZoomSliceWheel);
817
+ switch (this.state.keyboardSettings.mouseWheel) {
1707
818
  case "Scroll:Zoom":
1708
- this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseZoomWheel();
819
+ this.drawCore.drawingPrameters.handleMouseZoomSliceWheel = this.drawCore.configMouseZoomWheel();
1709
820
  break;
1710
821
  case "Scroll:Slice":
1711
- this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseSliceWheel();
822
+ this.drawCore.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseSliceWheel();
1712
823
  break;
1713
824
  default:
1714
- this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseZoomWheel();
825
+ this.drawCore.drawingPrameters.handleMouseZoomSliceWheel = this.drawCore.configMouseZoomWheel();
1715
826
  break;
1716
827
  }
1717
- this.protectedData.canvases.drawingCanvas.addEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
828
+ this.state.protectedData.canvases.drawingCanvas.addEventListener("wheel", this.drawCore.drawingPrameters.handleMouseZoomSliceWheel);
829
+ }
830
+ // ═══════════════════════════════════════════════════════════════════════════
831
+ // 13. Internal — Utility
832
+ // ═══════════════════════════════════════════════════════════════════════════
833
+ setIsDrawFalse(target) {
834
+ this.preTimer = setTimeout(() => {
835
+ this.state.protectedData.isDrawing = false;
836
+ if (this.preTimer) {
837
+ window.clearTimeout(this.preTimer);
838
+ this.preTimer = undefined;
839
+ }
840
+ }, target);
841
+ }
842
+ setShowInMainArea() {
843
+ this.state.nrrd_states.view.showContrast = true;
844
+ }
845
+ clearDictionary(dic) {
846
+ for (var key in dic) {
847
+ delete dic[key];
848
+ }
849
+ }
850
+ /**
851
+ * Get the DrawToolCore's start() render loop callback.
852
+ * This is called by DragSliceTool's requestAnimationFrame loop.
853
+ */
854
+ get start() {
855
+ return this.drawCore.start;
856
+ }
857
+ /**
858
+ * Expose drawCalculatorSphereOnEachViews for external use.
859
+ */
860
+ drawCalculatorSphereOnEachViews(axis) {
861
+ this.drawCore.drawCalculatorSphereOnEachViews(axis);
1718
862
  }
1719
863
  }
1720
864
  //# sourceMappingURL=NrrdTools.js.map