copper3d 3.0.1 → 3.1.0

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