copper3d 3.0.2 → 3.1.1

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 (106) 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 +168 -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 +92 -467
  12. package/dist/Utils/segmentation/NrrdTools.js +499 -1341
  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 +186 -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 +386 -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/GuiState.js +1 -0
  26. package/dist/Utils/segmentation/coreTools/GuiState.js.map +1 -1
  27. package/dist/Utils/segmentation/coreTools/NrrdState.d.ts +1 -1
  28. package/dist/Utils/segmentation/coreTools/divControlTools.d.ts +1 -1
  29. package/dist/Utils/segmentation/coreTools/gui.d.ts +1 -1
  30. package/dist/Utils/segmentation/tools/BaseTool.d.ts +1 -1
  31. package/dist/Utils/segmentation/tools/ContrastTool.d.ts +3 -6
  32. package/dist/Utils/segmentation/tools/ContrastTool.js.map +1 -1
  33. package/dist/Utils/segmentation/tools/CrosshairTool.d.ts +11 -1
  34. package/dist/Utils/segmentation/tools/CrosshairTool.js +25 -0
  35. package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -1
  36. package/dist/Utils/segmentation/tools/DataLoader.d.ts +33 -0
  37. package/dist/Utils/segmentation/tools/DataLoader.js +159 -0
  38. package/dist/Utils/segmentation/tools/DataLoader.js.map +1 -0
  39. package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +3 -12
  40. package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -1
  41. package/dist/Utils/segmentation/tools/DrawingTool.d.ts +18 -26
  42. package/dist/Utils/segmentation/tools/DrawingTool.js +48 -0
  43. package/dist/Utils/segmentation/tools/DrawingTool.js.map +1 -1
  44. package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +2 -5
  45. package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -1
  46. package/dist/Utils/segmentation/tools/LayerChannelManager.d.ts +56 -0
  47. package/dist/Utils/segmentation/tools/LayerChannelManager.js +188 -0
  48. package/dist/Utils/segmentation/tools/LayerChannelManager.js.map +1 -0
  49. package/dist/Utils/segmentation/tools/PanTool.d.ts +2 -8
  50. package/dist/Utils/segmentation/tools/PanTool.js.map +1 -1
  51. package/dist/Utils/segmentation/tools/SliceRenderPipeline.d.ts +80 -0
  52. package/dist/Utils/segmentation/tools/SliceRenderPipeline.js +355 -0
  53. package/dist/Utils/segmentation/tools/SliceRenderPipeline.js.map +1 -0
  54. package/dist/Utils/segmentation/tools/SphereTool.d.ts +25 -19
  55. package/dist/Utils/segmentation/tools/SphereTool.js +61 -7
  56. package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -1
  57. package/dist/Utils/segmentation/tools/ToolHost.d.ts +74 -0
  58. package/dist/Utils/segmentation/tools/ToolHost.js +10 -0
  59. package/dist/Utils/segmentation/tools/ToolHost.js.map +1 -0
  60. package/dist/Utils/segmentation/tools/ZoomTool.d.ts +2 -6
  61. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -1
  62. package/dist/Utils/segmentation/tools/index.d.ts +5 -7
  63. package/dist/Utils/segmentation/tools/index.js +6 -0
  64. package/dist/Utils/segmentation/tools/index.js.map +1 -1
  65. package/dist/Utils/workers/reformatSaveDataWorker.d.ts +1 -1
  66. package/dist/bundle.esm.js +2146 -2359
  67. package/dist/bundle.umd.js +2146 -2359
  68. package/dist/index.d.ts +3 -3
  69. package/dist/index.js +1 -1
  70. package/dist/types/Utils/segmentation/CanvasState.d.ts +31 -0
  71. package/dist/types/Utils/segmentation/DragOperator.d.ts +1 -1
  72. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +49 -27
  73. package/dist/types/Utils/segmentation/NrrdTools.d.ts +92 -467
  74. package/dist/types/Utils/segmentation/RenderingUtils.d.ts +73 -0
  75. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +1 -1
  76. package/dist/types/Utils/segmentation/core/index.d.ts +1 -1
  77. package/dist/types/Utils/segmentation/core/types.d.ts +386 -0
  78. package/dist/types/Utils/segmentation/coreTools/GuiState.d.ts +1 -1
  79. package/dist/types/Utils/segmentation/coreTools/NrrdState.d.ts +1 -1
  80. package/dist/types/Utils/segmentation/coreTools/divControlTools.d.ts +1 -1
  81. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +1 -1
  82. package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +1 -1
  83. package/dist/types/Utils/segmentation/tools/ContrastTool.d.ts +3 -6
  84. package/dist/types/Utils/segmentation/tools/CrosshairTool.d.ts +11 -1
  85. package/dist/types/Utils/segmentation/tools/DataLoader.d.ts +33 -0
  86. package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +3 -12
  87. package/dist/types/Utils/segmentation/tools/DrawingTool.d.ts +18 -26
  88. package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +2 -5
  89. package/dist/types/Utils/segmentation/tools/LayerChannelManager.d.ts +56 -0
  90. package/dist/types/Utils/segmentation/tools/PanTool.d.ts +2 -8
  91. package/dist/types/Utils/segmentation/tools/SliceRenderPipeline.d.ts +80 -0
  92. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +25 -19
  93. package/dist/types/Utils/segmentation/tools/ToolHost.d.ts +74 -0
  94. package/dist/types/Utils/segmentation/tools/ZoomTool.d.ts +2 -6
  95. package/dist/types/Utils/segmentation/tools/index.d.ts +5 -7
  96. package/dist/types/Utils/workers/reformatSaveDataWorker.d.ts +1 -1
  97. package/dist/types/index.d.ts +3 -3
  98. package/package.json +1 -1
  99. package/dist/Utils/segmentation/CommToolsData.d.ts +0 -192
  100. package/dist/Utils/segmentation/CommToolsData.js +0 -474
  101. package/dist/Utils/segmentation/CommToolsData.js.map +0 -1
  102. package/dist/Utils/segmentation/coreTools/coreType.d.ts +0 -389
  103. package/dist/Utils/segmentation/coreTools/coreType.js +0 -3
  104. package/dist/Utils/segmentation/coreTools/coreType.js.map +0 -1
  105. package/dist/types/Utils/segmentation/CommToolsData.d.ts +0 -192
  106. 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,56 +296,46 @@ 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
- var _a, _b, _c, _d, _e, _f, _g, _h;
300
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
338
301
  if (!this.guiParameterSettings)
339
302
  return null;
303
+ // Per-layer alpha: read directly from layerOpacity state
304
+ if (key === "layerAlpha") {
305
+ const activeLayer = this.state.gui_states.layerChannel.layer;
306
+ return {
307
+ min: 0.1,
308
+ max: 1,
309
+ step: 0.01,
310
+ value: (_b = (_a = this.state.gui_states.layerChannel.layerOpacity) === null || _a === void 0 ? void 0 : _a[activeLayer]) !== null && _b !== void 0 ? _b : 1.0,
311
+ };
312
+ }
340
313
  const setting = this.guiParameterSettings[key];
341
314
  if (!setting)
342
315
  return null;
343
316
  let value;
344
317
  if (key === "windowHigh") {
345
- value = (_b = (_a = setting.value) === null || _a === void 0 ? void 0 : _a.windowHigh) !== null && _b !== void 0 ? _b : 0;
318
+ value = (_d = (_c = setting.value) === null || _c === void 0 ? void 0 : _c.windowHigh) !== null && _d !== void 0 ? _d : 0;
346
319
  }
347
320
  else if (key === "windowLow") {
348
- value = (_d = (_c = setting.value) === null || _c === void 0 ? void 0 : _c.windowLow) !== null && _d !== void 0 ? _d : 0;
321
+ value = (_f = (_e = setting.value) === null || _e === void 0 ? void 0 : _e.windowLow) !== null && _f !== void 0 ? _f : 0;
349
322
  }
350
323
  else {
351
- value = (_e = this.gui_states.drawing[key]) !== null && _e !== void 0 ? _e : 0;
324
+ value = (_g = this.state.gui_states.drawing[key]) !== null && _g !== void 0 ? _g : 0;
352
325
  }
353
326
  return {
354
- min: (_f = setting.min) !== null && _f !== void 0 ? _f : 0,
355
- max: (_g = setting.max) !== null && _g !== void 0 ? _g : 100,
356
- step: (_h = setting.step) !== null && _h !== void 0 ? _h : 1,
327
+ min: (_h = setting.min) !== null && _h !== void 0 ? _h : 0,
328
+ max: (_j = setting.max) !== null && _j !== void 0 ? _j : 100,
329
+ step: (_k = setting.step) !== null && _k !== void 0 ? _k : 1,
357
330
  value,
358
331
  };
359
332
  }
360
- // ── Phase 1: GUI API — Color & Action Methods ──────────────────────────
361
- /**
362
- * Set the pencil stroke color.
363
- */
364
333
  setPencilColor(hex) {
365
- this.gui_states.drawing.color = hex;
334
+ this.state.gui_states.drawing.color = hex;
366
335
  }
367
- /**
368
- * Get the current pencil stroke color.
369
- */
370
336
  getPencilColor() {
371
- return this.gui_states.drawing.color;
337
+ return this.state.gui_states.drawing.color;
372
338
  }
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
339
  executeAction(action) {
382
340
  switch (action) {
383
341
  case "undo":
@@ -387,811 +345,237 @@ export class NrrdTools extends DrawToolCore {
387
345
  this.redo();
388
346
  break;
389
347
  case "clearActiveSliceMask":
390
- this.clearActiveSlice();
348
+ this.drawCore.clearActiveSlice();
391
349
  break;
392
350
  case "clearActiveLayerMask": {
393
351
  const text = "Are you sure remove annotations on All slice?";
394
352
  if (confirm(text) === true) {
395
- this.nrrd_states.flags.clearAllFlag = true;
396
- this.clearActiveSlice();
353
+ this.state.nrrd_states.flags.clearAllFlag = true;
354
+ this.drawCore.clearActiveSlice();
397
355
  this.clearActiveLayer();
398
356
  }
399
- this.nrrd_states.flags.clearAllFlag = false;
357
+ this.state.nrrd_states.flags.clearAllFlag = false;
400
358
  break;
401
359
  }
402
360
  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);
361
+ this.state.nrrd_states.view.sizeFactor = this.state.baseCanvasesSize;
362
+ this.state.gui_states.viewConfig.mainAreaSize = this.state.baseCanvasesSize;
363
+ this.resizePaintArea(this.state.nrrd_states.view.sizeFactor);
406
364
  this.resetPaintAreaUIPosition();
407
365
  break;
408
366
  case "downloadCurrentMask": {
409
367
  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,
368
+ axis: this.state.protectedData.axis,
369
+ currentSliceIndex: this.state.nrrd_states.view.currentSliceIndex,
370
+ drawingCanvas: this.state.protectedData.canvases.drawingCanvas,
371
+ originWidth: this.state.nrrd_states.image.originWidth,
372
+ originHeight: this.state.nrrd_states.image.originHeight,
415
373
  };
416
374
  enableDownload(config);
417
375
  break;
418
376
  }
419
377
  }
420
378
  }
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
- */
379
+ // ═══════════════════════════════════════════════════════════════════════════
380
+ // 4. Public API Keyboard & History
381
+ // ═══════════════════════════════════════════════════════════════════════════
658
382
  undo() {
659
- this.undoLastPainting();
383
+ this.drawCore.undoLastPainting();
660
384
  }
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
385
  redo() {
674
- this.redoLastPainting();
386
+ this.drawCore.redoLastPainting();
675
387
  }
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
388
  enterKeyboardConfig() {
691
- this._configKeyBoard = true;
389
+ this.state.configKeyBoard = true;
692
390
  }
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
391
  exitKeyboardConfig() {
702
- this._configKeyBoard = false;
392
+ this.state.configKeyBoard = false;
703
393
  }
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
394
  setContrastShortcutEnabled(enabled) {
726
395
  var _a;
727
- (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.setContrastEnabled(enabled);
396
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setContrastEnabled(enabled);
728
397
  }
729
- /**
730
- * Returns whether the contrast shortcut is currently enabled.
731
- */
732
398
  isContrastShortcutEnabled() {
733
399
  var _a, _b;
734
- return (_b = (_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isContrastEnabled()) !== null && _b !== void 0 ? _b : true;
400
+ return (_b = (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.isContrastEnabled()) !== null && _b !== void 0 ? _b : true;
735
401
  }
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
402
  setKeyboardSettings(settings) {
760
403
  var _a;
761
404
  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);
405
+ && settings.mouseWheel !== this.state.keyboardSettings.mouseWheel;
406
+ Object.assign(this.state.keyboardSettings, settings);
407
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setKeyboardSettings(settings);
765
408
  if (mouseWheelChanged) {
766
409
  this.updateMouseWheelEvent();
767
410
  }
768
411
  }
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
412
  getKeyboardSettings() {
784
- return Object.assign({}, this._keyboardSettings);
413
+ return Object.assign({}, this.state.keyboardSettings);
785
414
  }
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();
415
+ // ═══════════════════════════════════════════════════════════════════════════
416
+ // 5. Public API View Control (drag, zoom, pan, slice navigation)
417
+ // ═══════════════════════════════════════════════════════════════════════════
418
+ drag(opts) {
419
+ this.dragOperator.drag(opts);
794
420
  }
795
421
  /**
796
- *
797
- * entry function
798
- * * {
799
- * x:slice,
800
- * y:slice,
801
- * z:slice
802
- * }
803
- *
804
- * @param allSlices - all nrrd contrast slices
805
-
422
+ * Core drawing entry point.
806
423
  */
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
- }
424
+ draw(opts) {
425
+ this.drawCore.draw(opts);
897
426
  }
898
427
  /**
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
428
+ * Set the Draw Display Canvas base size
906
429
  */
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");
430
+ setBaseDrawDisplayCanvasesSize(size) {
431
+ if (size > 8) {
432
+ this.state.baseCanvasesSize = 8;
935
433
  }
936
- catch (error) {
937
- console.error("Error loading NIfTI masks:", error);
434
+ else if (size < 1 || typeof size !== "number") {
435
+ this.state.baseCanvasesSize = 1;
938
436
  }
939
- finally {
940
- if (loadingBar) {
941
- loadingBar.loadingContainer.style.display = "none";
942
- }
437
+ else {
438
+ this.state.baseCanvasesSize = size;
943
439
  }
944
440
  }
945
- setShowInMainArea() {
946
- this.nrrd_states.view.showContrast = true;
947
- }
948
- getCurrentImageDimension() {
949
- return this.nrrd_states.image.dimensions;
950
- }
951
- getVoxelSpacing() {
952
- return this.nrrd_states.image.voxelSpacing;
441
+ setDisplaySliceIndexPanel(panel) {
442
+ this.dragOperator.setShowDragNumberDiv(panel);
953
443
  }
954
- getSpaceOrigin() {
955
- return this.nrrd_states.image.spaceOrigin;
444
+ enableContrastDragEvents(callback) {
445
+ this.drawCore.setupConrastEvents(callback);
956
446
  }
957
- getMaskData() {
958
- return this.protectedData.maskData;
447
+ setEraserUrls(urls) {
448
+ this.drawCore.setEraserUrls(urls);
959
449
  }
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();
450
+ setPencilIconUrls(urls) {
451
+ this.drawCore.setPencilIconUrls(urls);
1009
452
  }
1010
453
  /**
1011
454
  * Switch all contrast slices' orientation
1012
- * @param {string} aixs:"x" | "y" | "z"
1013
- * */
455
+ */
1014
456
  setSliceOrientation(axisTo) {
1015
457
  var _a;
1016
458
  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;
459
+ if (((_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.isCrosshairEnabled()) || this.state.gui_states.mode.sphere) {
460
+ if (this.state.protectedData.axis === "z") {
461
+ this.state.cursorPage.z.index = this.state.nrrd_states.view.currentSliceIndex;
462
+ this.state.cursorPage.z.cursorPageX = this.state.nrrd_states.interaction.cursorPageX;
463
+ this.state.cursorPage.z.cursorPageY = this.state.nrrd_states.interaction.cursorPageY;
1022
464
  }
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;
465
+ else if (this.state.protectedData.axis === "x") {
466
+ this.state.cursorPage.x.index = this.state.nrrd_states.view.currentSliceIndex;
467
+ this.state.cursorPage.x.cursorPageX = this.state.nrrd_states.interaction.cursorPageX;
468
+ this.state.cursorPage.x.cursorPageY = this.state.nrrd_states.interaction.cursorPageY;
1027
469
  }
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;
470
+ else if (this.state.protectedData.axis === "y") {
471
+ this.state.cursorPage.y.index = this.state.nrrd_states.view.currentSliceIndex;
472
+ this.state.cursorPage.y.cursorPageX = this.state.nrrd_states.interaction.cursorPageX;
473
+ this.state.cursorPage.y.cursorPageY = this.state.nrrd_states.interaction.cursorPageY;
1032
474
  }
1033
475
  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);
476
+ if (this.state.nrrd_states.interaction.isCursorSelect && !this.state.cursorPage.z.updated) {
477
+ if (this.state.protectedData.axis === "x") {
478
+ convetObj = this.drawCore.convertCursorPoint("x", "z", this.state.cursorPage.x.cursorPageX, this.state.cursorPage.x.cursorPageY, this.state.cursorPage.x.index);
1038
479
  }
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);
480
+ if (this.state.protectedData.axis === "y") {
481
+ convetObj = this.drawCore.convertCursorPoint("y", "z", this.state.cursorPage.y.cursorPageX, this.state.cursorPage.y.cursorPageY, this.state.cursorPage.y.index);
1042
482
  }
1043
483
  }
1044
484
  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;
485
+ this.state.nrrd_states.view.currentSliceIndex = this.state.cursorPage.z.index;
486
+ this.state.nrrd_states.view.preSliceIndex =
487
+ this.state.cursorPage.z.index * this.state.nrrd_states.image.ratios.z;
488
+ this.state.nrrd_states.interaction.cursorPageX = this.state.cursorPage.z.cursorPageX;
489
+ this.state.nrrd_states.interaction.cursorPageY = this.state.cursorPage.z.cursorPageY;
1051
490
  }
1052
491
  }
1053
492
  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);
493
+ if (this.state.nrrd_states.interaction.isCursorSelect && !this.state.cursorPage.x.updated) {
494
+ if (this.state.protectedData.axis === "z") {
495
+ convetObj = this.drawCore.convertCursorPoint("z", "x", this.state.cursorPage.z.cursorPageX, this.state.cursorPage.z.cursorPageY, this.state.cursorPage.z.index);
1058
496
  }
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);
497
+ if (this.state.protectedData.axis === "y") {
498
+ convetObj = this.drawCore.convertCursorPoint("y", "x", this.state.cursorPage.y.cursorPageX, this.state.cursorPage.y.cursorPageY, this.state.cursorPage.y.index);
1062
499
  }
1063
500
  }
1064
501
  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;
502
+ this.state.nrrd_states.view.currentSliceIndex = this.state.cursorPage.x.index;
503
+ this.state.nrrd_states.view.preSliceIndex =
504
+ this.state.cursorPage.x.index * this.state.nrrd_states.image.ratios.x;
505
+ this.state.nrrd_states.interaction.cursorPageX = this.state.cursorPage.x.cursorPageX;
506
+ this.state.nrrd_states.interaction.cursorPageY = this.state.cursorPage.x.cursorPageY;
1071
507
  }
1072
508
  }
1073
509
  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);
510
+ if (this.state.nrrd_states.interaction.isCursorSelect && !this.state.cursorPage.y.updated) {
511
+ if (this.state.protectedData.axis === "z") {
512
+ convetObj = this.drawCore.convertCursorPoint("z", "y", this.state.cursorPage.z.cursorPageX, this.state.cursorPage.z.cursorPageY, this.state.cursorPage.z.index);
1078
513
  }
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);
514
+ if (this.state.protectedData.axis === "x") {
515
+ convetObj = this.drawCore.convertCursorPoint("x", "y", this.state.cursorPage.x.cursorPageX, this.state.cursorPage.x.cursorPageY, this.state.cursorPage.x.index);
1082
516
  }
1083
517
  }
1084
518
  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;
519
+ this.state.nrrd_states.view.currentSliceIndex = this.state.cursorPage.y.index;
520
+ this.state.nrrd_states.view.preSliceIndex =
521
+ this.state.cursorPage.y.index * this.state.nrrd_states.image.ratios.y;
522
+ this.state.nrrd_states.interaction.cursorPageX = this.state.cursorPage.y.cursorPageX;
523
+ this.state.nrrd_states.interaction.cursorPageY = this.state.cursorPage.y.cursorPageY;
1091
524
  }
1092
525
  }
1093
526
  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;
527
+ this.state.nrrd_states.view.currentSliceIndex = convetObj.currentNewSliceIndex;
528
+ this.state.nrrd_states.view.preSliceIndex = convetObj.preSliceIndex;
529
+ this.state.nrrd_states.interaction.cursorPageX = convetObj.convertCursorNumX;
530
+ this.state.nrrd_states.interaction.cursorPageY = convetObj.convertCursorNumY;
1099
531
  convetObj = undefined;
1100
532
  switch (axisTo) {
1101
533
  case "x":
1102
- this.cursorPage.x.updated = true;
534
+ this.state.cursorPage.x.updated = true;
1103
535
  break;
1104
536
  case "y":
1105
- this.cursorPage.y.updated = true;
537
+ this.state.cursorPage.y.updated = true;
1106
538
  break;
1107
539
  case "z":
1108
- this.cursorPage.z.updated = true;
540
+ this.state.cursorPage.z.updated = true;
1109
541
  break;
1110
542
  }
1111
543
  }
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;
544
+ if (this.state.cursorPage.x.updated &&
545
+ this.state.cursorPage.y.updated &&
546
+ this.state.cursorPage.z.updated) {
547
+ this.state.nrrd_states.interaction.isCursorSelect = false;
1117
548
  }
1118
549
  }
1119
- this.protectedData.axis = axisTo;
1120
- this.resetDisplaySlicesStatus();
550
+ this.state.protectedData.axis = axisTo;
551
+ this.sliceRenderPipeline.resetDisplaySlicesStatus();
1121
552
  }
1122
553
  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;
554
+ this.state.protectedData.skipSlicesDic[index] =
555
+ this.state.protectedData.backUpDisplaySlices[index];
556
+ if (index >= this.state.protectedData.displaySlices.length) {
557
+ this.state.nrrd_states.view.contrastNum = this.state.protectedData.displaySlices.length;
1127
558
  }
1128
559
  else {
1129
- this.nrrd_states.view.contrastNum = index;
560
+ this.state.nrrd_states.view.contrastNum = index;
1130
561
  }
1131
- this.resetDisplaySlicesStatus();
562
+ this.sliceRenderPipeline.resetDisplaySlicesStatus();
1132
563
  }
1133
564
  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;
565
+ this.state.protectedData.skipSlicesDic[index] = undefined;
566
+ this.state.nrrd_states.view.contrastNum = 0;
567
+ this.sliceRenderPipeline.resetDisplaySlicesStatus();
1182
568
  }
1183
569
  setSliceMoving(step) {
1184
- if (this.protectedData.mainPreSlices) {
1185
- // Accumulate steps so no keydown events are lost
570
+ if (this.state.protectedData.mainPreSlices) {
1186
571
  this._pendingSliceStep += step;
1187
- // RAF throttle: render at most once per frame, but apply ALL accumulated steps
1188
572
  if (this._sliceRAFId !== null)
1189
573
  return;
1190
574
  this._sliceRAFId = requestAnimationFrame(() => {
1191
575
  this._sliceRAFId = null;
1192
576
  const totalStep = this._pendingSliceStep;
1193
577
  this._pendingSliceStep = 0;
1194
- this.protectedData.isDrawing = true;
578
+ this.state.protectedData.isDrawing = true;
1195
579
  this.setSyncsliceNum();
1196
580
  this.dragOperator.updateIndex(totalStep);
1197
581
  this.setIsDrawFalse(1000);
@@ -1199,493 +583,237 @@ export class NrrdTools extends DrawToolCore {
1199
583
  }
1200
584
  }
1201
585
  setMainAreaSize(factor) {
1202
- this.nrrd_states.view.sizeFactor = factor;
1203
- if (this.nrrd_states.view.sizeFactor >= 8) {
1204
- this.nrrd_states.view.sizeFactor = 8;
586
+ this.state.nrrd_states.view.sizeFactor = factor;
587
+ if (this.state.nrrd_states.view.sizeFactor >= 8) {
588
+ this.state.nrrd_states.view.sizeFactor = 8;
1205
589
  }
1206
- else if (this.nrrd_states.view.sizeFactor <= 1) {
1207
- this.nrrd_states.view.sizeFactor = 1;
590
+ else if (this.state.nrrd_states.view.sizeFactor <= 1) {
591
+ this.state.nrrd_states.view.sizeFactor = 1;
1208
592
  }
1209
- this.resizePaintArea(this.nrrd_states.view.sizeFactor);
593
+ this.resizePaintArea(this.state.nrrd_states.view.sizeFactor);
1210
594
  this.resetPaintAreaUIPosition();
1211
- // this.setIsDrawFalse(1000);
595
+ }
596
+ switchAllSlicesArrayData(allSlices) {
597
+ this.state.protectedData.allSlicesArray.length = 0;
598
+ this.state.protectedData.allSlicesArray = [...allSlices];
599
+ this.sliceRenderPipeline.resetDisplaySlicesStatus();
600
+ }
601
+ appendLoadingbar(loadingbar) {
602
+ this.state.protectedData.mainAreaContainer.appendChild(loadingbar);
603
+ }
604
+ // ═══════════════════════════════════════════════════════════════════════════
605
+ // 6. Public API — Data Getters
606
+ // ═══════════════════════════════════════════════════════════════════════════
607
+ getCurrentImageDimension() {
608
+ return this.state.nrrd_states.image.dimensions;
609
+ }
610
+ getVoxelSpacing() {
611
+ return this.state.nrrd_states.image.voxelSpacing;
612
+ }
613
+ getSpaceOrigin() {
614
+ return this.state.nrrd_states.image.spaceOrigin;
615
+ }
616
+ getMaskData() {
617
+ return this.state.protectedData.maskData;
1212
618
  }
1213
619
  getContainer() {
1214
- return this.protectedData.mainAreaContainer;
620
+ return this.state.protectedData.mainAreaContainer;
1215
621
  }
1216
622
  getDrawingCanvas() {
1217
- return this.protectedData.canvases.drawingCanvas;
623
+ return this.state.protectedData.canvases.drawingCanvas;
1218
624
  }
1219
625
  getNrrdToolsSettings() {
1220
- return this.nrrd_states;
626
+ return this.state.nrrd_states;
1221
627
  }
1222
628
  getMaxSliceNum() {
1223
- if (this.nrrd_states.view.showContrast) {
629
+ if (this.state.nrrd_states.view.showContrast) {
1224
630
  return [
1225
- this.nrrd_states.view.maxIndex,
1226
- this.nrrd_states.view.maxIndex * this.protectedData.displaySlices.length,
631
+ this.state.nrrd_states.view.maxIndex,
632
+ this.state.nrrd_states.view.maxIndex * this.state.protectedData.displaySlices.length,
1227
633
  ];
1228
634
  }
1229
635
  else {
1230
- return [this.nrrd_states.view.maxIndex];
636
+ return [this.state.nrrd_states.view.maxIndex];
1231
637
  }
1232
638
  }
1233
639
  getCurrentSlicesNumAndContrastNum() {
1234
640
  return {
1235
- currentSliceIndex: this.nrrd_states.view.currentSliceIndex,
1236
- contrastIndex: this.nrrd_states.view.contrastNum,
641
+ currentSliceIndex: this.state.nrrd_states.view.currentSliceIndex,
642
+ contrastIndex: this.state.nrrd_states.view.contrastNum,
1237
643
  };
1238
644
  }
1239
645
  getCurrentSliceIndex() {
1240
- return Math.ceil(this.protectedData.mainPreSlices.index / this.nrrd_states.image.RSARatio);
646
+ return Math.ceil(this.state.protectedData.mainPreSlices.index / this.state.nrrd_states.image.RSARatio);
1241
647
  }
1242
648
  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";
649
+ return this.state.nrrd_states.view.showContrast;
650
+ }
651
+ // ═══════════════════════════════════════════════════════════════════════════
652
+ // 7. Delegated LayerChannelManager
653
+ // ═══════════════════════════════════════════════════════════════════════════
654
+ setActiveLayer(layerId) { this.layerChannelManager.setActiveLayer(layerId); }
655
+ getActiveLayer() { return this.layerChannelManager.getActiveLayer(); }
656
+ setActiveChannel(channel) { this.layerChannelManager.setActiveChannel(channel); }
657
+ getActiveChannel() { return this.layerChannelManager.getActiveChannel(); }
658
+ setActiveSphereType(type) { this.layerChannelManager.setActiveSphereType(type); }
659
+ getActiveSphereType() { return this.layerChannelManager.getActiveSphereType(); }
660
+ setLayerVisible(layerId, visible) { this.layerChannelManager.setLayerVisible(layerId, visible); }
661
+ isLayerVisible(layerId) { return this.layerChannelManager.isLayerVisible(layerId); }
662
+ setChannelVisible(layerId, channel, visible) { this.layerChannelManager.setChannelVisible(layerId, channel, visible); }
663
+ isChannelVisible(layerId, channel) { return this.layerChannelManager.isChannelVisible(layerId, channel); }
664
+ getLayerVisibility() { return this.layerChannelManager.getLayerVisibility(); }
665
+ getChannelVisibility() { return this.layerChannelManager.getChannelVisibility(); }
666
+ hasLayerData(layerId) { return this.layerChannelManager.hasLayerData(layerId); }
667
+ setChannelColor(layerId, channel, color) { this.layerChannelManager.setChannelColor(layerId, channel, color); }
668
+ getChannelColor(layerId, channel) { return this.layerChannelManager.getChannelColor(layerId, channel); }
669
+ getChannelHexColor(layerId, channel) { return this.layerChannelManager.getChannelHexColor(layerId, channel); }
670
+ getChannelCssColor(layerId, channel) { return this.layerChannelManager.getChannelCssColor(layerId, channel); }
671
+ setChannelColors(layerId, colorMap) { this.layerChannelManager.setChannelColors(layerId, colorMap); }
672
+ setAllLayersChannelColor(channel, color) { this.layerChannelManager.setAllLayersChannelColor(channel, color); }
673
+ resetChannelColors(layerId, channel) { this.layerChannelManager.resetChannelColors(layerId, channel); }
674
+ // Per-layer opacity
675
+ setLayerOpacity(layerId, opacity) { this.layerChannelManager.setLayerOpacity(layerId, opacity); }
676
+ getLayerOpacity(layerId) { return this.layerChannelManager.getLayerOpacity(layerId); }
677
+ getLayerOpacityMap() { return this.layerChannelManager.getLayerOpacityMap(); }
678
+ // ═══════════════════════════════════════════════════════════════════════════
679
+ // 8. Delegated — SliceRenderPipeline
680
+ // ═══════════════════════════════════════════════════════════════════════════
681
+ updateOriginAndChangedWH() { this.sliceRenderPipeline.updateOriginAndChangedWH(); }
682
+ setSyncsliceNum() { this.sliceRenderPipeline.setSyncsliceNum(); }
683
+ resetPaintAreaUIPosition(l, t) { this.sliceRenderPipeline.resetPaintAreaUIPosition(l, t); }
684
+ resetLayerCanvas() { this.sliceRenderPipeline.resetLayerCanvas(); }
685
+ redrawMianPreOnDisplayCanvas() { this.sliceRenderPipeline.redrawMianPreOnDisplayCanvas(); }
686
+ resizePaintArea(factor) { this.sliceRenderPipeline.resizePaintArea(factor); }
687
+ flipDisplayImageByAxis() { this.sliceRenderPipeline.flipDisplayImageByAxis(); }
688
+ setEmptyCanvasSize(axis) { this.sliceRenderPipeline.setEmptyCanvasSize(axis); }
689
+ redrawDisplayCanvas() { this.sliceRenderPipeline.redrawDisplayCanvas(); }
690
+ reloadMasksFromVolume() { this.sliceRenderPipeline.reloadMasksFromVolume(); }
691
+ // ═══════════════════════════════════════════════════════════════════════════
692
+ // 9. Delegated — DataLoader
693
+ // ═══════════════════════════════════════════════════════════════════════════
694
+ setAllSlices(allSlices) { this.dataLoader.setAllSlices(allSlices); }
695
+ setMasksData(masksData, loadingBar) { this.dataLoader.setMasksData(masksData, loadingBar); }
696
+ setMasksFromNIfTI(layerVoxels, loadingBar) { this.dataLoader.setMasksFromNIfTI(layerVoxels, loadingBar); }
697
+ // ═══════════════════════════════════════════════════════════════════════════
698
+ // 10. Sphere Orchestration
699
+ // ═══════════════════════════════════════════════════════════════════════════
700
+ setCalculateDistanceSphere(x, y, sliceIndex, cal_position) {
701
+ this.state.nrrd_states.sphere.sphereRadius = 5;
702
+ const steps = sliceIndex - this.state.nrrd_states.view.currentSliceIndex;
703
+ this.setSliceMoving(steps * this.state.protectedData.displaySlices.length);
704
+ const mouseX = x * this.state.nrrd_states.view.sizeFactor;
705
+ const mouseY = y * this.state.nrrd_states.view.sizeFactor;
706
+ this.state.nrrd_states.sphere.sphereOrigin[this.state.protectedData.axis] = [
707
+ mouseX, mouseY, sliceIndex,
708
+ ];
709
+ this.drawCore.crosshairTool.setUpSphereOrigins(mouseX, mouseY, sliceIndex);
710
+ const originCopy = JSON.parse(JSON.stringify(this.state.nrrd_states.sphere.sphereOrigin));
711
+ switch (cal_position) {
712
+ case "tumour":
713
+ this.state.nrrd_states.sphere.tumourSphereOrigin = originCopy;
714
+ break;
715
+ case "skin":
716
+ this.state.nrrd_states.sphere.skinSphereOrigin = originCopy;
717
+ break;
718
+ case "nipple":
719
+ this.state.nrrd_states.sphere.nippleSphereOrigin = originCopy;
720
+ break;
721
+ case "ribcage":
722
+ this.state.nrrd_states.sphere.ribSphereOrigin = originCopy;
723
+ break;
1438
724
  }
725
+ this.drawCore.drawCalculatorSphere(this.state.nrrd_states.sphere.sphereRadius);
726
+ this.drawCore.sphereTool.writeAllCalculatorSpheresToVolume();
727
+ this.drawCore.sphereTool.refreshSphereCanvas();
1439
728
  }
1440
729
  /**
1441
- * Clear masks on drawingCanvas layers.
730
+ * Refresh sphere canvas from sphereMaskVolume for the current slice/axis.
1442
731
  */
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
- }
732
+ refreshSphereOverlay() {
733
+ this.drawCore.refreshSphereOverlay();
1449
734
  }
1450
735
  /**
1451
736
  * 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
737
  */
1459
738
  enterSphereMode() {
1460
739
  var _a;
1461
- // Disable left-click drag for slice navigation
1462
740
  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);
741
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool('sphere');
742
+ const w = this.state.nrrd_states.view.changedWidth;
743
+ const h = this.state.nrrd_states.view.changedHeight;
744
+ for (const layerId of this.state.nrrd_states.image.layers) {
745
+ const target = this.state.protectedData.layerTargets.get(layerId);
1470
746
  if (target) {
1471
747
  target.ctx.clearRect(0, 0, target.canvas.width, target.canvas.height);
1472
748
  }
1473
749
  }
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)
750
+ this.state.protectedData.ctxes.drawingLayerMasterCtx.clearRect(0, 0, w, h);
1477
751
  this.refreshSphereOverlay();
1478
752
  }
1479
753
  /**
1480
754
  * 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
755
  */
1488
756
  exitSphereMode() {
1489
757
  var _a;
1490
- // Restore left-click drag for slice navigation
1491
758
  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
759
+ (_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.setGuiTool('pencil');
760
+ this.state.protectedData.ctxes.drawingSphereCtx.clearRect(0, 0, this.state.protectedData.canvases.drawingSphereCanvas.width, this.state.protectedData.canvases.drawingSphereCanvas.height);
1497
761
  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
762
  this.reloadMasksFromVolume();
1502
763
  }
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
- }
764
+ // ═══════════════════════════════════════════════════════════════════════════
765
+ // 11. Clear / Reset
766
+ // ═══════════════════════════════════════════════════════════════════════════
767
+ clearActiveLayer() {
768
+ if (this.state.nrrd_states.image.dimensions.length === 3) {
769
+ const [w, h, d] = this.state.nrrd_states.image.dimensions;
770
+ const activeLayer = this.state.gui_states.layerChannel.layer;
771
+ this.state.protectedData.maskData.volumes[activeLayer] = new MaskVolume(w, h, d, 1);
772
+ this.drawCore.undoManager.clearLayer(activeLayer);
773
+ this.state.annotationCallbacks.onLayerVolumeCleared(activeLayer);
774
+ }
775
+ this.drawCore.renderer.invalidateSliceBuffer();
776
+ this.reloadMasksFromVolume();
1681
777
  }
1682
- /**
1683
- * Config mouse slice wheel event.
1684
- */
778
+ reset() {
779
+ this.state.protectedData.allSlicesArray.length = 0;
780
+ this.state.protectedData.displaySlices.length = 0;
781
+ this.drawCore.undoManager.clearAll();
782
+ this.state.protectedData.maskData.volumes = this.state.nrrd_states.image.layers.reduce((acc, id) => {
783
+ acc[id] = new MaskVolume(1, 1, 1, 1);
784
+ return acc;
785
+ }, {});
786
+ this.state.nrrd_states.sphere.sphereMaskVolume = null;
787
+ this.drawCore.renderer.invalidateSliceBuffer();
788
+ this.clearDictionary(this.state.protectedData.skipSlicesDic);
789
+ this.state.protectedData.canvases.displayCanvas.style.left =
790
+ this.state.protectedData.canvases.drawingCanvas.style.left = "";
791
+ this.state.protectedData.canvases.displayCanvas.style.top =
792
+ this.state.protectedData.canvases.drawingCanvas.style.top = "";
793
+ this.state.protectedData.backUpDisplaySlices.length = 0;
794
+ this.state.protectedData.mainPreSlices = undefined;
795
+ this.state.protectedData.currentShowingSlice = undefined;
796
+ this.sliceRenderPipeline.resetInitState();
797
+ this.state.protectedData.axis = "z";
798
+ this.state.nrrd_states.view.sizeFactor = this.state.baseCanvasesSize;
799
+ this.state.gui_states.viewConfig.mainAreaSize = this.state.baseCanvasesSize;
800
+ this.resetLayerCanvas();
801
+ this.state.protectedData.canvases.drawingCanvas.width =
802
+ this.state.protectedData.canvases.drawingCanvas.width;
803
+ this.state.protectedData.canvases.displayCanvas.width =
804
+ this.state.protectedData.canvases.displayCanvas.width;
805
+ this.state.nrrd_states.sphere.tumourSphereOrigin = null;
806
+ this.state.nrrd_states.sphere.ribSphereOrigin = null;
807
+ this.state.nrrd_states.sphere.skinSphereOrigin = null;
808
+ this.state.nrrd_states.sphere.nippleSphereOrigin = null;
809
+ }
810
+ // ═══════════════════════════════════════════════════════════════════════════
811
+ // 12. Internal — Input Events
812
+ // ═══════════════════════════════════════════════════════════════════════════
1685
813
  configMouseSliceWheel() {
1686
814
  const handleMouseZoomSliceWheelMove = (e) => {
1687
815
  var _a;
1688
- if ((_a = this.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
816
+ if ((_a = this.drawCore.eventRouter) === null || _a === void 0 ? void 0 : _a.isShiftHeld()) {
1689
817
  return;
1690
818
  }
1691
819
  e.preventDefault();
@@ -1698,23 +826,53 @@ export class NrrdTools extends DrawToolCore {
1698
826
  };
1699
827
  return handleMouseZoomSliceWheelMove;
1700
828
  }
1701
- /**
1702
- * Update mouse wheel event.
1703
- */
1704
829
  updateMouseWheelEvent() {
1705
- this.protectedData.canvases.drawingCanvas.removeEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
1706
- switch (this._keyboardSettings.mouseWheel) {
830
+ this.state.protectedData.canvases.drawingCanvas.removeEventListener("wheel", this.drawCore.drawingPrameters.handleMouseZoomSliceWheel);
831
+ switch (this.state.keyboardSettings.mouseWheel) {
1707
832
  case "Scroll:Zoom":
1708
- this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseZoomWheel();
833
+ this.drawCore.drawingPrameters.handleMouseZoomSliceWheel = this.drawCore.configMouseZoomWheel();
1709
834
  break;
1710
835
  case "Scroll:Slice":
1711
- this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseSliceWheel();
836
+ this.drawCore.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseSliceWheel();
1712
837
  break;
1713
838
  default:
1714
- this.drawingPrameters.handleMouseZoomSliceWheel = this.configMouseZoomWheel();
839
+ this.drawCore.drawingPrameters.handleMouseZoomSliceWheel = this.drawCore.configMouseZoomWheel();
1715
840
  break;
1716
841
  }
1717
- this.protectedData.canvases.drawingCanvas.addEventListener("wheel", this.drawingPrameters.handleMouseZoomSliceWheel);
842
+ this.state.protectedData.canvases.drawingCanvas.addEventListener("wheel", this.drawCore.drawingPrameters.handleMouseZoomSliceWheel);
843
+ }
844
+ // ═══════════════════════════════════════════════════════════════════════════
845
+ // 13. Internal — Utility
846
+ // ═══════════════════════════════════════════════════════════════════════════
847
+ setIsDrawFalse(target) {
848
+ this.preTimer = setTimeout(() => {
849
+ this.state.protectedData.isDrawing = false;
850
+ if (this.preTimer) {
851
+ window.clearTimeout(this.preTimer);
852
+ this.preTimer = undefined;
853
+ }
854
+ }, target);
855
+ }
856
+ setShowInMainArea() {
857
+ this.state.nrrd_states.view.showContrast = true;
858
+ }
859
+ clearDictionary(dic) {
860
+ for (var key in dic) {
861
+ delete dic[key];
862
+ }
863
+ }
864
+ /**
865
+ * Get the DrawToolCore's start() render loop callback.
866
+ * This is called by DragSliceTool's requestAnimationFrame loop.
867
+ */
868
+ get start() {
869
+ return this.drawCore.start;
870
+ }
871
+ /**
872
+ * Expose drawCalculatorSphereOnEachViews for external use.
873
+ */
874
+ drawCalculatorSphereOnEachViews(axis) {
875
+ this.drawCore.drawCalculatorSphereOnEachViews(axis);
1718
876
  }
1719
877
  }
1720
878
  //# sourceMappingURL=NrrdTools.js.map