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
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
  [![NRRD_Segmentation_Tool example][nrrd_example]][nrrd_example-url]
7
7
  [![MedTech Heart example][heart_example]][heart_example-url]
8
8
 
9
- A 3d visualisation package base on threejs provides multiple scenes and Nrrd image load funtion.
9
+ A 3D visualisation package based on three.js multiple scenes, NRRD/DICOM image loading, and a full medical image segmentation annotation engine.
10
10
 
11
- ### Documentation:
11
+ ### Documentation
12
12
 
13
13
  https://copper3d-visualisation.readthedocs.io/en/latest/
14
14
 
@@ -17,72 +17,57 @@ https://copper3d-visualisation.readthedocs.io/en/latest/
17
17
  Old: https://www.npmjs.com/package/copper3d_visualisation
18
18
  Very old: https://www.npmjs.com/package/gltfloader-plugin-test
19
19
 
20
- ### Example
20
+ ### Examples
21
21
 
22
22
  [Pick model with Gltfloader](https://linkungao.github.io/loadHumanModel_example/)
23
-
24
23
  [Copper3d_examples](https://linkungao.github.io/copper3d_examples)
25
24
 
26
- ### Useage
25
+ ---
26
+
27
+ ### Basic Usage
27
28
 
28
- - Load demo
29
+ **Load demo**
29
30
 
30
31
  ```ts
31
32
  import * as Copper from "copper3d";
32
33
  import { getCurrentInstance, onMounted } from "vue";
33
- let refs = null;
34
+
34
35
  let appRenderer;
35
36
  onMounted(() => {
36
- let { $refs } = (getCurrentInstance() as any).proxy;
37
- refs = $refs;
38
- const bg: HTMLDivElement = refs.classfy;
37
+ const { $refs } = (getCurrentInstance() as any).proxy;
38
+ const bg: HTMLDivElement = $refs.classfy;
39
39
  appRenderer = new Copper.copperRenderer(bg);
40
- const scene = appRenderer.getCurrentScene();
41
- scene.createDemoMesh();
40
+ appRenderer.getCurrentScene().createDemoMesh();
42
41
  appRenderer.animate();
43
42
  });
44
43
  ```
45
44
 
46
- - Add options (curently only control gui)
45
+ **Options**
47
46
 
48
47
  ```ts
49
48
  appRenderer = new Copper.copperRenderer(bg, { guiOpen: true });
50
49
  ```
51
50
 
52
- - Load multiple scenes with gltf-loader
51
+ **Multiple scenes with glTF**
53
52
 
54
53
  ```ts
55
- import * as Copper from "copper3d";
56
- import { getCurrentInstance, onMounted } from "vue";
57
-
58
- let refs = null;
59
- let appRenderer;
60
- onMounted(() => {
61
- let { $refs } = (getCurrentInstance() as any).proxy;
62
- refs = $refs;
63
- const bg: HTMLDivElement = refs.classfy;
64
- appRenderer = new Copper.copperRenderer(bg);
65
- appRenderer.animate();
66
- loadModel("/Healthy.glb", "health");
67
- });
68
-
69
- function loadModel(url, name) {
70
- let scene1 = appRenderer.getSceneByName(name);
71
- if (scene1 == undefined) {
72
- const scene1 = appRenderer.createScene(name);
73
- appRenderer.setCurrentScene(scene1);
74
- scene1.loadViewUrl("/noInfarct_view.json");
75
- scene1.loadGltf(url);
54
+ function loadModel(url: string, name: string) {
55
+ let scene = appRenderer.getSceneByName(name);
56
+ if (!scene) {
57
+ scene = appRenderer.createScene(name);
58
+ appRenderer.setCurrentScene(scene);
59
+ scene.loadViewUrl("/noInfarct_view.json");
60
+ scene.loadGltf(url);
76
61
  } else {
77
- appRenderer.setCurrentScene(scene1);
62
+ appRenderer.setCurrentScene(scene);
78
63
  }
79
64
  }
80
65
  ```
81
66
 
82
- #### Viewdata Structure
67
+ **View data structure**
83
68
 
84
69
  ```ts
85
- CameraViewPoint {
70
+ CameraViewPoint {
86
71
  nearPlane: number = 0.1;
87
72
  farPlane: number = 2000.0;
88
73
  eyePosition: Array<number> = [0.0, 0.0, 0.0];
@@ -91,30 +76,23 @@ function loadModel(url, name) {
91
76
  }
92
77
  ```
93
78
 
79
+ ---
80
+
94
81
  ## NrrdTools Usage Guide
95
82
 
96
83
  > Copper3D `NrrdTools` — Medical Image Segmentation Annotation Engine
97
84
 
98
- `NrrdTools` is the core annotation engine of Copper3D. It manages multi-layer mask volumes, a layered canvas pipeline, drawing tools, undo/redo history, channel color customization, and keyboard shortcuts — all on top of a Three.js medical image viewer.
85
+ `NrrdTools` manages multi-layer mask volumes, a layered canvas pipeline, drawing tools, undo/redo history, channel color customization, and keyboard shortcuts on top of a Three.js medical image viewer.
99
86
 
100
- ---
101
-
102
- ### Table of Contents
103
-
104
- 1. [Quick Start](#1-quick-start)
105
- 2. [Constructor & Initialization](#2-constructor--initialization)
106
- 3. [Data Loading](#3-data-loading)
107
- 4. [Render Loop Integration](#4-render-loop-integration)
108
- 5. [Drawing Setup](#5-drawing-setup)
109
- 6. [Layer & Channel Management](#6-layer--channel-management)
110
- 7. [Channel Color Customization](#7-channel-color-customization)
111
- 8. [Undo / Redo](#8-undo--redo)
112
- 9. [Keyboard Shortcuts](#9-keyboard-shortcuts)
113
- 10. [Display & Canvas Control](#10-display--canvas-control)
114
- 11. [Reading State & Diagnostics](#11-reading-state--diagnostics)
115
- 12. [Advanced Scenarios](#12-advanced-scenarios)
116
- 13. [Vue 3 Integration Pattern](#13-vue-3-integration-pattern)
117
- 14. [Type Reference](#14-type-reference)
87
+ > **Internal Architecture**: `NrrdTools` is a **Facade** using **composition** (no inheritance). It composes:
88
+ > - **`CanvasState`** — unified state container (nrrd_states, gui_states, protectedData, callbacks, keyboardSettings)
89
+ > - **`DrawToolCore`** — tool orchestration and event routing
90
+ > - **`RenderingUtils`** — slice extraction and canvas compositing helpers
91
+ > - **`LayerChannelManager`** — layer/channel/sphere-type management and color customization
92
+ > - **`SliceRenderPipeline`** — slice setup, canvas rendering, mask reload, canvas flip
93
+ > - **`DataLoader`** — NRRD slice loading, NIfTI voxel loading
94
+ >
95
+ > The old inheritance chain (`NrrdTools → DrawToolCore → CommToolsData`) has been fully replaced. All modules communicate via `ToolContext` (shared state). The public API below is unchanged.
118
96
 
119
97
  ---
120
98
 
@@ -123,27 +101,20 @@ function loadModel(url, name) {
123
101
  ```typescript
124
102
  import * as Copper from 'copper3d';
125
103
 
126
- // 1. Mount the tool on a container div
127
104
  const container = document.getElementById('viewer') as HTMLDivElement;
128
105
  const nrrdTools = new Copper.NrrdTools(container);
129
106
 
130
- // 2. After NRRD images are loaded via Copper scene:
131
- nrrdTools.clear();
132
- nrrdTools.setAllSlices(allSlices); // allSlices from Copper scene loader
107
+ nrrdTools.reset();
108
+ nrrdTools.setAllSlices(allSlices); // allSlices from Copper scene loader
133
109
 
134
- // 3. Hook drawing callbacks
135
- nrrdTools.drag({ getSliceNum: (index, contrastIndex) => {
136
- console.log('Slice changed to:', index);
137
- }});
110
+ nrrdTools.drag({ getSliceNum: (index) => console.log('Slice:', index) });
138
111
 
139
112
  nrrdTools.draw({
140
113
  getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, width, height, clearFlag) => {
141
114
  // Called after every stroke, undo, redo — sync to backend here
142
- console.log(`Layer ${layerId}, channel ${channelId}, slice ${sliceIndex} on ${axis}-axis`);
143
115
  }
144
116
  });
145
117
 
146
- // 4. Register with the Copper render loop
147
118
  scene.addPreRenderCallbackFunction(nrrdTools.start);
148
119
  ```
149
120
 
@@ -151,454 +122,206 @@ scene.addPreRenderCallbackFunction(nrrdTools.start);
151
122
 
152
123
  ### 2. Constructor & Initialization
153
124
 
154
- #### Signature
155
-
156
125
  ```typescript
157
126
  new Copper.NrrdTools(container: HTMLDivElement, options?: { layers?: string[] })
158
127
  ```
159
128
 
160
129
  | Parameter | Type | Default | Description |
161
130
  |-----------|------|---------|-------------|
162
- | `container` | `HTMLDivElement` | required | The DOM element that will host all annotation canvases |
163
- | `options.layers` | `string[]` | `["layer1","layer2","layer3"]` | Named layers to create. Each layer gets its own `MaskVolume` and canvas |
164
-
165
- #### Example: Single-layer (minimal)
166
-
167
- ```typescript
168
- const nrrdTools = new Copper.NrrdTools(document.getElementById('viewer') as HTMLDivElement);
169
- ```
170
-
171
- #### Example: Custom layer set
131
+ | `container` | `HTMLDivElement` | required | Host DOM element for all annotation canvases |
132
+ | `options.layers` | `string[]` | `["layer1","layer2","layer3"]` | Named layers to create |
172
133
 
173
134
  ```typescript
174
- // For a 4-layer segmentation workflow:
175
- // layer1 = Tumour, layer2 = Edema, layer3 = Necrosis, layer4 = Enhancement
135
+ // Custom layer set
176
136
  const nrrdTools = new Copper.NrrdTools(container, {
177
137
  layers: ['layer1', 'layer2', 'layer3', 'layer4']
178
138
  });
179
- ```
180
-
181
- > **Important**: The layer list you pass here must match what your backend and UI expect.
182
- > Adding or removing layers later requires re-instantiation.
183
-
184
- #### Optional display panel
185
-
186
- Attach a panel element to show current slice index in the viewer:
187
-
188
- ```typescript
189
- const slicePanel = document.getElementById('slice-index-panel') as HTMLDivElement;
190
- nrrdTools.setDisplaySliceIndexPanel(slicePanel);
191
- ```
192
-
193
- #### Optional GUI (dat.GUI)
194
139
 
195
- If your project uses `dat.GUI`, wire up the built-in control panel:
140
+ // Optional: show current slice index in a panel
141
+ nrrdTools.setDisplaySliceIndexPanel(document.getElementById('slice-panel') as HTMLDivElement);
196
142
 
197
- ```typescript
198
- import GUI from 'lil-gui'; // or 'dat.gui'
199
- const gui = new GUI();
200
- nrrdTools.setupGUI(gui as any);
143
+ // Optional: connect dat.GUI / lil-gui
144
+ import GUI from 'lil-gui';
145
+ nrrdTools.setupGUI(new GUI() as any);
201
146
  ```
202
147
 
203
148
  ---
204
149
 
205
150
  ### 3. Data Loading
206
151
 
207
- #### 3.1 Loading NRRD / NIfTI image slices
208
-
209
- This is the entry point that initializes all `MaskVolume` instances to the correct voxel dimensions.
210
- Must be called **after** Copper has loaded and decoded the NRRD files.
211
-
212
152
  ```typescript
213
- // Reset state from previous case
214
- nrrdTools.clear();
215
-
216
- // allSlices is the array of decoded NRRD slice objects returned by Copper's loader
153
+ // Reset state then load new slices
154
+ nrrdTools.reset();
217
155
  nrrdTools.setAllSlices(allSlices);
218
- ```
219
-
220
- > After `setAllSlices()` returns, you may safely call all layer/channel/color APIs.
221
- > Calling color APIs **before** `setAllSlices()` will silently fail (no MaskVolume exists yet).
222
-
223
- #### 3.2 Loading existing mask data (NIfTI)
224
156
 
225
- If the user has previously saved annotations, reload them into the volumes:
226
-
227
- ```typescript
228
- // layerVoxels maps each layer ID to a Uint8Array of raw NIfTI voxels
157
+ // Load existing NIfTI mask data
229
158
  const layerVoxels = new Map<string, Uint8Array>([
230
159
  ['layer1', layer1Uint8Array],
231
160
  ['layer2', layer2Uint8Array],
232
161
  ]);
233
-
234
162
  nrrdTools.setMasksFromNIfTI(layerVoxels);
235
- ```
236
-
237
- With a loading progress bar:
238
163
 
239
- ```typescript
240
- const loadingBar = { value: 0 }; // must be a reactive object with .value
164
+ // With loading progress bar
165
+ const loadingBar = { value: 0 };
241
166
  nrrdTools.setMasksFromNIfTI(layerVoxels, loadingBar);
242
167
  ```
243
168
 
244
- #### Scenario: Loading a saved case
245
-
246
- ```typescript
247
- async function loadCase(caseDetail: ICaseDetail) {
248
- const layerVoxels = new Map<string, Uint8Array>();
249
-
250
- // layer1 — always load from NIfTI if it exists
251
- if (Number(caseDetail.output.mask_layer1_nii_size) > 0) {
252
- const voxels = await fetchNiftiVoxels(caseDetail.output.mask_layer1_nii_path!);
253
- if (voxels) layerVoxels.set('layer1', voxels);
254
- }
255
-
256
- // layer2 — same pattern
257
- if (Number(caseDetail.output.mask_layer2_nii_size) > 0) {
258
- const voxels = await fetchNiftiVoxels(caseDetail.output.mask_layer2_nii_path!);
259
- if (voxels) layerVoxels.set('layer2', voxels);
260
- }
261
-
262
- if (layerVoxels.size > 0) {
263
- nrrdTools.setMasksFromNIfTI(layerVoxels);
264
- }
265
- }
266
- ```
267
-
268
169
  ---
269
170
 
270
171
  ### 4. Render Loop Integration
271
172
 
272
- `NrrdTools.start` is a function property that must be called every frame to refresh the annotation overlay on top of the Three.js rendered CT/MRI slice.
273
-
274
- #### With Copper scene
275
-
276
173
  ```typescript
277
174
  // Register once after initialization
278
175
  const callbackId = scene.addPreRenderCallbackFunction(nrrdTools.start);
279
176
 
280
- // Unregister on teardown (e.g., Vue component unmount)
177
+ // Unregister on teardown
281
178
  scene.removePreRenderCallbackFunction(callbackId);
282
179
  ```
283
180
 
284
- #### Manual render loop
285
-
286
- ```typescript
287
- function animate() {
288
- requestAnimationFrame(animate);
289
- renderer.render(scene, camera);
290
- nrrdTools.start();
291
- }
292
- animate();
293
- ```
294
-
295
181
  ---
296
182
 
297
183
  ### 5. Drawing Setup
298
184
 
299
- #### 5.1 `drag()` — Slice navigation
300
-
301
- Enables dragging to scroll through CT/MRI slices. Call once before adding to the render loop.
185
+ #### `drag()` — Slice navigation
302
186
 
303
187
  ```typescript
304
188
  nrrdTools.drag({
305
- showNumber: true, // optional: show slice number overlay
306
- getSliceNum: (sliceIndex, contrastIndex) => { // optional: callback on slice change
307
- console.log('Now viewing slice:', sliceIndex);
308
- updateUISliceDisplay(sliceIndex);
309
- }
189
+ showNumber: true,
190
+ getSliceNum: (sliceIndex, contrastIndex) => updateUI(sliceIndex),
310
191
  });
311
192
  ```
312
193
 
313
- #### 5.2 `draw()` — Annotation callbacks
314
-
315
- Hooks into the drawing events. The `getMaskData` callback is the primary integration point for syncing annotations to a backend.
194
+ #### `draw()` — Annotation callbacks
316
195
 
317
196
  ```typescript
318
197
  nrrdTools.draw({
319
- // Called after every draw stroke, undo, redo, or clear
320
- getMaskData: (
321
- sliceData: Uint8Array, // Raw voxel data for this slice (label values 0-8)
322
- layerId: string, // Which layer was modified, e.g. "layer1"
323
- channelId: number, // Active channel (1-8)
324
- sliceIndex: number, // Index along the current axis
325
- axis: 'x' | 'y' | 'z', // Current viewing axis
326
- width: number, // Slice width in voxels
327
- height: number, // Slice height in voxels
328
- clearFlag?: boolean // true if the user cleared the slice
329
- ) => {
198
+ getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, width, height, clearFlag?) => {
330
199
  sendSliceToBackend({ sliceData, layerId, channelId, sliceIndex, axis, width, height, clearFlag });
331
200
  },
332
-
333
- // Called when the user clears an entire layer volume
334
- onClearLayerVolume: (layerId: string) => {
335
- console.log(`Layer ${layerId} fully cleared`);
336
- notifyBackendLayerCleared(layerId);
337
- },
338
-
339
- // Called when the 3D sphere annotation tool places a sphere
340
- getSphereData: (sphereOrigin: number[], sphereRadius: number) => {
341
- console.log('Sphere placed at', sphereOrigin, 'radius', sphereRadius);
342
- },
343
-
344
- // Called when the calculate-sphere-positions tool fires
345
- getCalculateSpherePositionsData: (
346
- tumourOrigin, skinOrigin, ribOrigin, nippleOrigin, axis
347
- ) => {
348
- runAISegmentation({ tumourOrigin, skinOrigin, ribOrigin, nippleOrigin, axis });
201
+ onClearLayerVolume: (layerId) => notifyBackendLayerCleared(layerId),
202
+ getSphereData: (sphereOrigin, sphereRadius) => sendSphereToBackend({ sphereOrigin, sphereRadius }),
203
+ getCalculateSpherePositionsData: (tumour, skin, rib, nipple, axis) => {
204
+ if (tumour && skin && rib && nipple) aiBackend.runSegmentation({ tumour, skin, rib, nipple, axis });
349
205
  },
350
206
  });
351
207
  ```
352
208
 
353
- #### 5.3 `enableContrastDragEvents()` — Windowing
354
-
355
- Enable Ctrl+drag to adjust window/level (brightness/contrast):
209
+ #### `enableContrastDragEvents()` — Window/Level
356
210
 
357
211
  ```typescript
358
- nrrdTools.enableContrastDragEvents((step: number, towards: 'horizental' | 'vertical') => {
359
- // step: magnitude of drag movement
360
- // towards: direction of drag
361
- console.log(`Contrast adjusted: ${towards} ${step}`);
212
+ nrrdTools.enableContrastDragEvents((step, towards) => {
213
+ console.log(`Contrast: ${towards} ${step}`);
362
214
  });
363
215
  ```
364
216
 
365
- ---
366
-
367
- ### 6. Layer & Channel Management
368
-
369
- NrrdTools supports **multi-layer** annotation (e.g., tumour, edema, necrosis) and **multi-channel** within each layer (e.g., different anatomical structures painted in different colors).
370
-
371
- #### 6.1 Active Layer & Channel
372
-
373
- ```typescript
374
- // Switch the active layer (where new strokes go)
375
- nrrdTools.setActiveLayer('layer2');
376
-
377
- // Switch the active channel within the current layer
378
- nrrdTools.setActiveChannel(3); // channel 3 = blue by default
379
-
380
- // Query current state
381
- const currentLayer = nrrdTools.getActiveLayer(); // → 'layer2'
382
- const currentChannel = nrrdTools.getActiveChannel(); // → 3
383
- ```
384
-
385
- #### 6.2 Layer Visibility
217
+ #### SphereTool — Sphere Types & Channel Mapping
386
218
 
387
- Toggle layers on/off in the composite canvas:
219
+ | Sphere Type | Channel | Default Color | `activeSphereType` value |
220
+ |-------------|---------|---------------|--------------------------|
221
+ | tumour | 1 | `#10b981` | `"tumour"` (default) |
222
+ | nipple | 2 | `#f43f5e` | `"nipple"` |
223
+ | ribcage | 3 | `#3b82f6` | `"ribcage"` |
224
+ | skin | 4 | `#fbbf24` | `"skin"` |
388
225
 
389
226
  ```typescript
390
- // Hide layer2 (e.g., edema) while keeping layer1 visible
391
- nrrdTools.setLayerVisible('layer2', false);
392
- nrrdTools.setLayerVisible('layer1', true);
393
-
394
- // Query visibility
395
- const layer2Visible = nrrdTools.isLayerVisible('layer2'); // → false
396
-
397
- // Get all at once
398
- const visibilityMap = nrrdTools.getLayerVisibility();
399
- // → { layer1: true, layer2: false, layer3: true, layer4: true }
227
+ nrrdTools.setActiveSphereType('nipple'); // also updates brush/fill color
228
+ const type = nrrdTools.getActiveSphereType(); // → 'tumour' | 'skin' | 'nipple' | 'ribcage'
400
229
  ```
401
230
 
402
- #### Scenario: Eye-button toggle in UI
231
+ **Programmatic sphere placement (backend frontend):**
403
232
 
404
233
  ```typescript
405
- function onToggleLayerEye(layerId: string) {
406
- const current = nrrdTools.isLayerVisible(layerId);
407
- nrrdTools.setLayerVisible(layerId, !current);
408
- }
234
+ // Replicates full click flow internally — no user interaction needed
235
+ nrrdTools.setCalculateDistanceSphere(120, 95, 42, 'tumour');
236
+ // Coordinates are in unscaled image space; sizeFactor is applied internally
409
237
  ```
410
238
 
411
- #### 6.3 Channel Visibility
239
+ ---
412
240
 
413
- Each layer can independently show/hide its channels:
241
+ ### 6. Layer & Channel Management
414
242
 
415
243
  ```typescript
416
- // Hide channel 2 in layer1 (e.g., secondary annotation color)
417
- nrrdTools.setChannelVisible('layer1', 2, false);
418
-
419
- // Show it again
420
- nrrdTools.setChannelVisible('layer1', 2, true);
244
+ // Active layer / channel
245
+ nrrdTools.setActiveLayer('layer2');
246
+ nrrdTools.setActiveChannel(3);
247
+ const layer = nrrdTools.getActiveLayer();
248
+ const channel = nrrdTools.getActiveChannel();
421
249
 
422
- // Query
423
- const ch2Visible = nrrdTools.isChannelVisible('layer1', 2); // → true
250
+ // Layer visibility
251
+ nrrdTools.setLayerVisible('layer2', false);
252
+ const visible = nrrdTools.isLayerVisible('layer2');
253
+ const visMap = nrrdTools.getLayerVisibility(); // { layer1: true, layer2: false, ... }
424
254
 
425
- // Get all channel visibility for all layers
255
+ // Channel visibility (per layer)
256
+ nrrdTools.setChannelVisible('layer1', 2, false);
426
257
  const allChannelVis = nrrdTools.getChannelVisibility();
427
- // → { layer1: { 1: true, 2: false, 3: true, ... }, layer2: { 1: true, ... }, ... }
428
- ```
429
-
430
- #### Scenario: Showing only the selected channel
431
-
432
- ```typescript
433
- function isolateChannel(layerId: string, targetChannel: number, totalChannels = 8) {
434
- for (let ch = 1; ch <= totalChannels; ch++) {
435
- nrrdTools.setChannelVisible(layerId, ch, ch === targetChannel);
436
- }
437
- }
438
258
 
439
- // Show only channel 1 in layer1
440
- isolateChannel('layer1', 1);
441
-
442
- // Restore all
443
- for (let ch = 1; ch <= 8; ch++) {
444
- nrrdTools.setChannelVisible('layer1', ch, true);
445
- }
446
- ```
447
-
448
- #### 6.4 Checking if a layer has annotations
449
-
450
- ```typescript
451
- if (nrrdTools.hasLayerData('layer2')) {
452
- console.log('layer2 has annotated voxels — saving...');
453
- await saveLayer('layer2');
454
- } else {
455
- console.log('layer2 is empty, skipping save');
456
- }
259
+ // Check if a layer has annotations
260
+ if (nrrdTools.hasLayerData('layer1')) await saveLayer('layer1');
457
261
  ```
458
262
 
459
263
  ---
460
264
 
461
265
  ### 7. Channel Color Customization
462
266
 
463
- Each layer has its own independent color map. Changing channel 3's color in `layer1` does **not** affect `layer2`.
464
-
465
- #### Default Channel Colors
267
+ **Default colors:**
466
268
 
467
269
  | Channel | Color | Hex |
468
270
  |---------|-------|-----|
469
- | 1 | Green (Tumour) | `#00ff00` |
470
- | 2 | Red (Edema) | `#ff0000` |
471
- | 3 | Blue (Necrosis) | `#0000ff` |
472
- | 4 | Yellow (Enhancement) | `#ffff00` |
473
- | 5 | Magenta (Vessel) | `#ff00ff` |
474
- | 6 | Cyan (Additional) | `#00ffff` |
475
- | 7 | Orange (Auxiliary) | `#ff8000` |
476
- | 8 | Purple (Extended) | `#8000ff` |
477
-
478
- #### 7.1 Set a single channel color
271
+ | 1 | Emerald | `#10b981` |
272
+ | 2 | Rose | `#f43f5e` |
273
+ | 3 | Blue | `#3b82f6` |
274
+ | 4 | Amber | `#fbbf24` |
275
+ | 5 | Fuchsia | `#d946ef` |
276
+ | 6 | Cyan | `#06b6d4` |
277
+ | 7 | Orange | `#f97316` |
278
+ | 8 | Violet | `#8b5cf6` |
479
279
 
480
280
  ```typescript
481
- // RGBAColor: { r, g, b, a } — all values 0-255
482
- nrrdTools.setChannelColor('layer1', 3, { r: 255, g: 128, b: 0, a: 255 }); // → orange
483
- ```
484
-
485
- #### 7.2 Batch-set multiple channel colors (recommended)
281
+ // Set one channel color (RGBAColor: { r, g, b, a } — 0-255)
282
+ nrrdTools.setChannelColor('layer1', 3, { r: 255, g: 128, b: 0, a: 255 });
486
283
 
487
- One `reloadMasksFromVolume()` call instead of N calls — better performance:
488
-
489
- ```typescript
284
+ // Batch-set (one reloadMasksFromVolume call — better performance)
490
285
  nrrdTools.setChannelColors('layer1', {
491
- 1: { r: 255, g: 80, b: 80, a: 255 }, // Soft red
492
- 2: { r: 80, g: 180, b: 255, a: 255 }, // Sky blue
493
- 3: { r: 255, g: 230, b: 50, a: 255 }, // Golden yellow
286
+ 1: { r: 255, g: 80, b: 80, a: 255 },
287
+ 2: { r: 80, g: 180, b: 255, a: 255 },
494
288
  });
495
- ```
496
289
 
497
- #### 7.3 Apply the same color to all layers
498
-
499
- ```typescript
500
- // Paint all layers' channel 1 in the same tumour color
290
+ // Apply same channel color across all layers
501
291
  nrrdTools.setAllLayersChannelColor(1, { r: 0, g: 220, b: 100, a: 255 });
502
- ```
503
-
504
- #### 7.4 Read current colors
505
-
506
- ```typescript
507
- const rgba = nrrdTools.getChannelColor('layer2', 3);
508
- // → { r: 255, g: 128, b: 0, a: 255 }
509
-
510
- const hex = nrrdTools.getChannelHexColor('layer2', 3);
511
- // → "#ff8000" — suitable for canvas fillStyle
512
-
513
- const css = nrrdTools.getChannelCssColor('layer2', 3);
514
- // → "rgba(255,128,0,1.00)" — suitable for Vue :style binding
515
- ```
516
-
517
- #### 7.5 Reset colors
518
-
519
- ```typescript
520
- // Reset one channel in one layer
521
- nrrdTools.resetChannelColors('layer1', 3);
522
-
523
- // Reset all channels in one layer
524
- nrrdTools.resetChannelColors('layer1');
525
-
526
- // Reset everything across all layers
527
- nrrdTools.resetChannelColors();
528
- ```
529
-
530
- #### Scenario: Color picker integration
531
-
532
- ```typescript
533
- // User picks a new color in the UI color picker
534
- function onColorPicked(hexColor: string) {
535
- // Parse hex to RGBA (simple implementation)
536
- const r = parseInt(hexColor.slice(1, 3), 16);
537
- const g = parseInt(hexColor.slice(3, 5), 16);
538
- const b = parseInt(hexColor.slice(5, 7), 16);
539
-
540
- const activeLayer = nrrdTools.getActiveLayer();
541
- const activeChannel = nrrdTools.getActiveChannel();
542
292
 
543
- nrrdTools.setChannelColor(activeLayer, activeChannel, { r, g, b, a: 255 });
544
- }
545
- ```
546
-
547
- #### Scenario: Vue UI reactivity after color change
548
-
549
- After calling `setChannelColor()`, the canvas re-renders automatically.
550
- But Vue computed properties that show the color need a manual nudge:
551
-
552
- ```typescript
553
- // In your composable or component
554
- const colorVersion = ref(0);
293
+ // Read colors
294
+ const rgba = nrrdTools.getChannelColor('layer1', 3);
295
+ const hex = nrrdTools.getChannelHexColor('layer1', 3); // → "#ff8000"
296
+ const css = nrrdTools.getChannelCssColor('layer1', 3); // → "rgba(255,128,0,1.00)"
555
297
 
556
- const channelCssColor = computed(() => {
557
- colorVersion.value; // subscribe to version
558
- return nrrdTools.value?.getChannelCssColor(activeLayer.value, activeChannel.value) ?? '#00ff00';
559
- });
560
-
561
- function refreshColors() {
562
- colorVersion.value++; // forces recompute
563
- }
564
-
565
- // After setting color:
566
- nrrdTools.value.setChannelColor('layer1', 2, { r: 255, g: 0, b: 0, a: 255 });
567
- refreshColors();
298
+ // Reset
299
+ nrrdTools.resetChannelColors('layer1', 3); // one channel
300
+ nrrdTools.resetChannelColors('layer1'); // all channels in layer
301
+ nrrdTools.resetChannelColors(); // everything
568
302
  ```
569
303
 
570
304
  ---
571
305
 
572
306
  ### 8. Undo / Redo
573
307
 
574
- NrrdTools maintains a per-layer undo stack (max 50 entries). Every completed brush stroke pushes a delta snapshot.
308
+ Per-layer undo stack (max 50 entries). Every completed stroke pushes a delta snapshot.
575
309
 
576
310
  ```typescript
577
- // Undo the last stroke on the active layer
578
311
  nrrdTools.undo();
579
-
580
- // Redo the last undone stroke
581
312
  nrrdTools.redo();
582
- ```
583
313
 
584
- #### Scenario: Keyboard shortcut binding
585
-
586
- ```typescript
314
+ // Or via keyboard
587
315
  window.addEventListener('keydown', (e) => {
588
316
  if (e.ctrlKey && e.key === 'z') nrrdTools.undo();
589
317
  if (e.ctrlKey && e.key === 'y') nrrdTools.redo();
590
318
  });
591
319
  ```
592
320
 
593
- > The built-in keyboard system also handles undo/redo (default: `z` and `y` without Ctrl).
594
- > See [Keyboard Shortcuts](#9-keyboard-shortcuts) for configuring this.
595
-
596
321
  ---
597
322
 
598
323
  ### 9. Keyboard Shortcuts
599
324
 
600
- #### Default bindings
601
-
602
325
  | Action | Default Key |
603
326
  |--------|-------------|
604
327
  | Draw mode | `Shift` (hold) |
@@ -606,502 +329,46 @@ window.addEventListener('keydown', (e) => {
606
329
  | Redo | `y` |
607
330
  | Contrast adjust | `Ctrl` / `Meta` (hold) |
608
331
  | Crosshair | `s` |
332
+ | Sphere mode | `q` |
609
333
  | Mouse wheel | Zoom |
610
334
 
611
- #### Reading current settings
612
-
613
335
  ```typescript
336
+ nrrdTools.setKeyboardSettings({ undo: 'u', mouseWheel: 'Scroll:Slice' });
614
337
  const settings = nrrdTools.getKeyboardSettings();
615
- console.log(settings);
616
- // {
617
- // draw: 'Shift',
618
- // undo: 'z',
619
- // redo: 'y',
620
- // contrast: ['Control', 'Meta'],
621
- // crosshair: 's',
622
- // mouseWheel: 'Scroll:Zoom'
623
- // }
624
- ```
625
338
 
626
- #### Customizing bindings
627
-
628
- ```typescript
629
- nrrdTools.setKeyboardSettings({
630
- undo: 'u', // Remap undo to 'u'
631
- mouseWheel: 'Scroll:Slice', // Mouse wheel scrolls slices instead of zooming
632
- });
633
- ```
634
-
635
- #### Scenario: Form input focus — suppressing shortcuts
636
-
637
- When a user types in an input field, you don't want tool shortcuts firing:
638
-
639
- ```typescript
640
- inputElement.addEventListener('focus', () => {
641
- nrrdTools.enterKeyboardConfig(); // Suppress all tool shortcuts
642
- });
643
-
644
- inputElement.addEventListener('blur', () => {
645
- nrrdTools.exitKeyboardConfig(); // Restore shortcuts
646
- });
647
- ```
648
-
649
- #### Enabling / disabling contrast shortcut independently
650
-
651
- ```typescript
652
- // Disable only the contrast (window/level) keyboard shortcut
653
- nrrdTools.setContrastShortcutEnabled(false);
654
-
655
- // Check status
656
- const isEnabled = nrrdTools.isContrastShortcutEnabled(); // → false
657
-
658
- // Re-enable
659
- nrrdTools.setContrastShortcutEnabled(true);
339
+ // Suppress shortcuts while a form input is focused
340
+ inputEl.addEventListener('focus', () => nrrdTools.enterKeyboardConfig());
341
+ inputEl.addEventListener('blur', () => nrrdTools.exitKeyboardConfig());
660
342
  ```
661
343
 
662
344
  ---
663
345
 
664
- ### 10. Display & Canvas Control
665
-
666
- #### Canvas size scaling
667
-
668
- Set the base display size multiplier (1–8). Larger values use more GPU memory but give sharper annotations:
346
+ ### 10. Clearing Annotations
669
347
 
670
348
  ```typescript
671
- nrrdTools.setBaseDrawDisplayCanvasesSize(2); // base resolution
672
- ```
673
-
674
- #### Reading image metadata
675
-
676
- ```typescript
677
- // Voxel dimensions [width, height, depth]
678
- const dims = nrrdTools.getCurrentImageDimension();
679
- // → [512, 512, 256]
680
-
681
- // Physical spacing (mm per voxel, from NRRD header)
682
- const spacing = nrrdTools.getVoxelSpacing();
683
- // → [0.488, 0.488, 1.0]
684
-
685
- // World-space origin
686
- const origin = nrrdTools.getSpaceOrigin();
687
- // → [-125.0, -125.0, -127.5]
688
-
689
- // Max slice count per axis
690
- const maxSlices = nrrdTools.getMaxSliceNum();
691
- // → [512, 512, 256] (one per axis)
692
-
693
- // Current viewing state
694
- const { currentIndex, contrastIndex } = nrrdTools.getCurrentSlicesNumAndContrastNum();
695
- ```
696
-
697
- #### Accessing internal canvases
698
-
699
- ```typescript
700
- // The topmost interactive canvas (mouse/pen events target here)
701
- const drawingCanvas = nrrdTools.getDrawingCanvas();
702
-
703
- // The container element passed to the constructor
704
- const container = nrrdTools.getContainer();
705
-
706
- // Raw NrrdStates (all internal state fields)
707
- const states = nrrdTools.getNrrdToolsSettings();
708
- console.log(states.layers); // ["layer1", "layer2", ...]
709
- console.log(states.dimensions); // [512, 512, 256]
710
- ```
711
-
712
- #### Accessing full mask data
713
-
714
- ```typescript
715
- // All MaskVolume data structures
716
- const maskData = nrrdTools.getMaskData();
717
- // maskData.volumes is Record<string, MaskVolume>
718
- ```
719
-
720
- ---
721
-
722
- ### 11. Reading State & Diagnostics
723
-
724
- ```typescript
725
- // Full snapshot of current state
726
- const state = nrrdTools.getNrrdToolsSettings();
727
-
728
- // Active layer / channel
729
- const layer = nrrdTools.getActiveLayer();
730
- const channel = nrrdTools.getActiveChannel();
731
-
732
- // Visibility maps
733
- const layerVis = nrrdTools.getLayerVisibility();
734
- const channelVis = nrrdTools.getChannelVisibility();
735
-
736
- // Check if any real annotation exists in a layer
737
- const hasTumour = nrrdTools.hasLayerData('layer1');
738
-
739
- // Get current channel color as hex
740
- const hex = nrrdTools.getChannelHexColor('layer1', 1); // → "#00ff00"
741
-
742
- // Keyboard settings
743
- const keys = nrrdTools.getKeyboardSettings();
744
- ```
745
-
746
- ---
747
-
748
- ### 12. Advanced Scenarios
749
-
750
- #### Scenario A: Complete multi-layer initialization from scratch
751
-
752
- ```typescript
753
- async function initAnnotationTool(container: HTMLDivElement, allSlices: any[]) {
754
- // Create tool with 4 layers
755
- const nrrdTools = new Copper.NrrdTools(container, {
756
- layers: ['layer1', 'layer2', 'layer3', 'layer4']
757
- });
758
-
759
- // Attach GUI
760
- nrrdTools.setupGUI(gui as any);
761
-
762
- // Attach contrast drag
763
- nrrdTools.enableContrastDragEvents((step, towards) => {
764
- console.log('Windowing:', towards, step);
765
- });
766
-
767
- // Load image volume
768
- nrrdTools.clear();
769
- nrrdTools.setAllSlices(allSlices);
770
-
771
- // Register callbacks
772
- nrrdTools.drag({ getSliceNum: (idx) => updateSliceUI(idx) });
773
-
774
- nrrdTools.draw({
775
- getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, w, h, clearFlag) => {
776
- syncSliceToBackend({ sliceData, layerId, channelId, sliceIndex, axis, clearFlag });
777
- },
778
- onClearLayerVolume: (layerId) => {
779
- notifyBackendCleared(layerId);
780
- },
781
- });
782
-
783
- // Connect to render loop
784
- scene.addPreRenderCallbackFunction(nrrdTools.start);
785
-
786
- return nrrdTools;
787
- }
788
- ```
789
-
790
- #### Scenario B: Load existing annotations and apply custom color scheme
791
-
792
- ```typescript
793
- async function loadAndColorCase(nrrdTools: Copper.NrrdTools, caseId: string) {
794
- // 1. Load existing NIfTI masks
795
- const masks = await fetchCaseMasks(caseId);
796
- const layerVoxels = new Map<string, Uint8Array>();
797
- for (const [layerId, data] of Object.entries(masks)) {
798
- layerVoxels.set(layerId, data);
799
- }
800
- nrrdTools.setMasksFromNIfTI(layerVoxels);
801
-
802
- // 2. Apply per-layer color schemes
803
- nrrdTools.setChannelColors('layer1', {
804
- 1: { r: 0, g: 200, b: 80, a: 255 }, // Tumour core — green
805
- 2: { r: 255, g: 200, b: 0, a: 255 }, // Tumour ring — yellow
806
- });
807
-
808
- nrrdTools.setChannelColors('layer2', {
809
- 1: { r: 255, g: 60, b: 60, a: 200 }, // Edema — semi-transparent red
810
- });
811
-
812
- // 3. Set initial active state
813
- nrrdTools.setActiveLayer('layer1');
814
- nrrdTools.setActiveChannel(1);
815
- }
816
- ```
817
-
818
- #### Scenario C: Switching cases — full reset
819
-
820
- ```typescript
821
- async function switchCase(nrrdTools: Copper.NrrdTools, newCaseData: ICaseData) {
822
- // Reset annotation volumes
823
- nrrdTools.clear();
824
-
825
- // Load new image slices (from Copper's scene loader result)
826
- nrrdTools.setAllSlices(newCaseData.slices);
827
-
828
- // Reset colors to defaults
829
- nrrdTools.resetChannelColors();
830
-
831
- // Make all layers and channels visible
832
- for (const layerId of ['layer1', 'layer2', 'layer3', 'layer4']) {
833
- nrrdTools.setLayerVisible(layerId, true);
834
- for (let ch = 1; ch <= 8; ch++) {
835
- nrrdTools.setChannelVisible(layerId, ch, true);
836
- }
837
- }
838
-
839
- // Start fresh on layer1, channel1
840
- nrrdTools.setActiveLayer('layer1');
841
- nrrdTools.setActiveChannel(1);
842
-
843
- // Load existing masks if available
844
- if (newCaseData.hasExistingMasks) {
845
- nrrdTools.setMasksFromNIfTI(newCaseData.layerVoxels);
846
- }
847
- }
848
- ```
849
-
850
- #### Scenario D: Save workflow with dirty-layer detection
851
-
852
- ```typescript
853
- async function onSave(nrrdTools: Copper.NrrdTools, caseId: string) {
854
- const layers = ['layer1', 'layer2', 'layer3', 'layer4'];
855
-
856
- for (const layerId of layers) {
857
- if (!nrrdTools.hasLayerData(layerId)) {
858
- console.log(`${layerId} is empty — initializing blank NIfTI on backend`);
859
- await initBlankLayerOnBackend(caseId, layerId);
860
- } else {
861
- console.log(`${layerId} has data — saving...`);
862
- await saveLayerToBackend(caseId, layerId);
863
- }
864
- }
865
- }
866
- ```
867
-
868
- #### Scenario E: AI segmentation result — write back to volume
869
-
870
- After receiving a segmentation result from a backend AI model, write it directly into a layer:
871
-
872
- ```typescript
873
- async function applyAIResult(nrrdTools: Copper.NrrdTools, layerId: string) {
874
- // Fetch AI-generated NIfTI from backend
875
- const response = await fetch(`/api/ai-result/${layerId}`);
876
- const buffer = await response.arrayBuffer();
877
- const voxels = new Uint8Array(buffer);
878
-
879
- // Write directly into the target layer volume
880
- const layerVoxels = new Map<string, Uint8Array>([[layerId, voxels]]);
881
- nrrdTools.setMasksFromNIfTI(layerVoxels);
882
-
883
- // Optionally switch to that layer so user sees the result
884
- nrrdTools.setActiveLayer(layerId);
885
- }
886
- ```
887
-
888
- #### Scenario F: Dynamic keyboard remap from user settings
889
-
890
- ```typescript
891
- interface UserPreferences {
892
- undoKey: string;
893
- redoKey: string;
894
- sliceScrollMode: 'zoom' | 'slice';
895
- }
896
-
897
- function applyUserKeyboardPreferences(nrrdTools: Copper.NrrdTools, prefs: UserPreferences) {
898
- nrrdTools.setKeyboardSettings({
899
- undo: prefs.undoKey,
900
- redo: prefs.redoKey,
901
- mouseWheel: prefs.sliceScrollMode === 'slice' ? 'Scroll:Slice' : 'Scroll:Zoom',
902
- });
903
- }
904
-
905
- // Example: Apply on preference change
906
- applyUserKeyboardPreferences(nrrdTools, {
907
- undoKey: 'u',
908
- redoKey: 'r',
909
- sliceScrollMode: 'slice',
910
- });
911
- ```
912
-
913
- #### Scenario G: Read layer colors to build a color legend
914
-
915
- ```typescript
916
- function buildColorLegend(nrrdTools: Copper.NrrdTools, layerId: string) {
917
- const legend = [];
918
- for (let ch = 1; ch <= 8; ch++) {
919
- legend.push({
920
- channel: ch,
921
- cssColor: nrrdTools.getChannelCssColor(layerId, ch),
922
- hexColor: nrrdTools.getChannelHexColor(layerId, ch),
923
- rgba: nrrdTools.getChannelColor(layerId, ch),
924
- });
925
- }
926
- return legend;
927
- }
928
-
929
- // → [
930
- // { channel: 1, cssColor: 'rgba(0,255,0,1.00)', hexColor: '#00ff00', rgba: { r:0, g:255, b:0, a:255 } },
931
- // { channel: 2, cssColor: 'rgba(255,0,0,1.00)', hexColor: '#ff0000', ... },
932
- // ...
933
- // ]
934
- ```
935
-
936
- ---
937
-
938
- ### 13. Vue 3 Integration Pattern
939
-
940
- The recommended pattern in Vue 3 is to distribute the `NrrdTools` instance via a Vue event emitter after NRRD loading completes, so any descendant component can access it.
941
-
942
- #### LeftPanel (creator)
943
-
944
- ```vue
945
- <script setup lang="ts">
946
- import * as Copper from 'copper3d';
947
- import emitter from '@/plugins/custom-emitter';
948
-
949
- let nrrdTools: Copper.NrrdTools | undefined;
950
-
951
- onMounted(() => {
952
- nrrdTools = new Copper.NrrdTools(canvasContainer.value as HTMLDivElement, {
953
- layers: ['layer1', 'layer2', 'layer3', 'layer4']
954
- });
955
- });
956
-
957
- // After all NRRD files are loaded by the Copper scene:
958
- function onAllImagesLoaded(allSlices: any[]) {
959
- nrrdTools!.clear();
960
- nrrdTools!.setAllSlices(allSlices);
961
-
962
- nrrdTools!.drag({ getSliceNum: (idx) => emit('sliceChanged', idx) });
963
-
964
- nrrdTools!.draw({
965
- getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, w, h, clearFlag) => {
966
- emit('maskDataUpdated', { sliceData, layerId, channelId, sliceIndex, axis, w, h, clearFlag });
967
- },
968
- onClearLayerVolume: (layerId) => emit('layerCleared', layerId),
969
- });
970
-
971
- scene!.addPreRenderCallbackFunction(nrrdTools!.start);
972
-
973
- // Broadcast to all other components
974
- emitter.emit('Core:NrrdTools', nrrdTools!);
975
- emitter.emit('Segmentation:FinishLoadAllCaseImages');
976
- }
977
- </script>
978
- ```
979
-
980
- #### Annotation Control Panel (consumer)
981
-
982
- ```vue
983
- <script setup lang="ts">
984
- import * as Copper from 'copper3d';
985
- import emitter from '@/plugins/custom-emitter';
986
-
987
- const nrrdTools = ref<Copper.NrrdTools>();
988
-
989
- onMounted(() => {
990
- emitter.on('Core:NrrdTools', (tools: Copper.NrrdTools) => {
991
- nrrdTools.value = tools;
992
- });
993
-
994
- emitter.on('Segmentation:FinishLoadAllCaseImages', () => {
995
- // Safe to call all APIs now — MaskVolume is initialized
996
- syncStateFromTools();
997
- });
998
- });
999
-
1000
- function syncStateFromTools() {
1001
- if (!nrrdTools.value) return;
1002
- activeLayer.value = nrrdTools.value.getActiveLayer();
1003
- activeChannel.value = nrrdTools.value.getActiveChannel();
1004
- layerVisibility.value = nrrdTools.value.getLayerVisibility();
1005
- channelVisibility.value = nrrdTools.value.getChannelVisibility();
1006
- }
1007
-
1008
- function onChannelColorPicked(hex: string) {
1009
- if (!nrrdTools.value) return;
1010
- const r = parseInt(hex.slice(1,3), 16);
1011
- const g = parseInt(hex.slice(3,5), 16);
1012
- const b = parseInt(hex.slice(5,7), 16);
1013
- nrrdTools.value.setChannelColor(activeLayer.value, activeChannel.value, { r, g, b, a: 255 });
1014
- refreshColors(); // bump colorVersion to re-trigger computed
1015
- }
1016
- </script>
1017
- ```
1018
-
1019
- ---
1020
-
1021
- ### 14. Type Reference
1022
-
1023
- ```typescript
1024
- // Color type used by all channel color APIs
1025
- interface RGBAColor {
1026
- r: number; // 0-255
1027
- g: number; // 0-255
1028
- b: number; // 0-255
1029
- a: number; // 0-255 (255 = fully opaque)
1030
- }
1031
-
1032
- // Batch color map for setChannelColors()
1033
- type ChannelColorMap = Record<number, RGBAColor>; // key = channel number 1-8
1034
-
1035
- // draw() options
1036
- interface IDrawOpts {
1037
- getMaskData?: (
1038
- sliceData: Uint8Array,
1039
- layerId: string,
1040
- channelId: number,
1041
- sliceIndex: number,
1042
- axis: 'x' | 'y' | 'z',
1043
- width: number,
1044
- height: number,
1045
- clearFlag?: boolean
1046
- ) => void;
1047
- onClearLayerVolume?: (layerId: string) => void;
1048
- getSphereData?: (sphereOrigin: number[], sphereRadius: number) => void;
1049
- getCalculateSpherePositionsData?: (
1050
- tumourOrigin: ICommXYZ | null,
1051
- skinOrigin: ICommXYZ | null,
1052
- ribOrigin: ICommXYZ | null,
1053
- nippleOrigin: ICommXYZ | null,
1054
- axis: 'x' | 'y' | 'z'
1055
- ) => void;
1056
- }
1057
-
1058
- // drag() options
1059
- interface IDragOpts {
1060
- showNumber?: boolean;
1061
- getSliceNum?: (sliceIndex: number, contrastIndex: number) => void;
1062
- }
1063
-
1064
- // Keyboard settings
1065
- interface IKeyBoardSettings {
1066
- draw: string;
1067
- undo: string;
1068
- redo: string;
1069
- contrast: string | string[];
1070
- crosshair: string;
1071
- mouseWheel: 'Scroll:Zoom' | 'Scroll:Slice';
1072
- }
1073
-
1074
- // 3D coordinate (used in sphere position callbacks)
1075
- interface ICommXYZ {
1076
- x: number;
1077
- y: number;
1078
- z: number;
1079
- }
1080
- ```
1081
-
1082
- #### Common type aliases
1083
-
1084
- ```typescript
1085
- type LayerId = 'layer1' | 'layer2' | 'layer3' | 'layer4'; // or any string
1086
- type ChannelValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
349
+ nrrdTools.reset(); // Reset ALL layers, volumes, undo histories, canvases (use when switching cases)
350
+ nrrdTools.clearActiveLayer(); // Clear active layer's entire 3D volume + undo history, fire onClearLayerVolume
351
+ nrrdTools.clearActiveSlice(); // Clear only the currently viewed 2D slice (undoable)
1087
352
  ```
1088
353
 
1089
354
  ---
1090
355
 
1091
- ### API Summary
356
+ ### 11. API Summary
1092
357
 
1093
358
  | Category | Method | Description |
1094
359
  |----------|--------|-------------|
1095
360
  | **Constructor** | `new NrrdTools(container, { layers })` | Create instance with optional layer config |
1096
361
  | **Setup** | `drag(opts?)` | Enable slice-drag navigation |
1097
362
  | | `draw(opts?)` | Bind annotation callbacks |
1098
- | | `setupGUI(gui)` | Connect dat.GUI panel |
363
+ | | `setupGUI(gui)` | Connect dat.GUI / lil-gui panel |
1099
364
  | | `enableContrastDragEvents(cb)` | Enable Ctrl+drag windowing |
1100
365
  | | `setDisplaySliceIndexPanel(el)` | Show slice index in a panel |
1101
- | | `setBaseDrawDisplayCanvasesSize(n)` | Set canvas resolution multiplier (1-8) |
1102
- | **Data** | `clear()` | Reset all volumes |
366
+ | | `setBaseDrawDisplayCanvasesSize(n)` | Set canvas resolution multiplier (18) |
367
+ | **Data** | `reset()` | Reset all volumes, undo histories, canvases, sphere data |
368
+ | | `clearActiveLayer()` | Clear active layer volume + undo history |
369
+ | | `clearActiveSlice()` | Clear current slice (undoable) |
1103
370
  | | `setAllSlices(slices)` | Load NRRD slices, init MaskVolumes |
1104
- | | `setMasksFromNIfTI(map)` | Load saved NIfTI voxel data |
371
+ | | `setMasksFromNIfTI(map, bar?)` | Load saved NIfTI voxel data |
1105
372
  | **Render** | `start` | Frame callback — pass to render loop |
1106
373
  | **Layer** | `setActiveLayer(id)` | Switch drawing target layer |
1107
374
  | | `getActiveLayer()` | Read current layer |
@@ -1109,6 +376,9 @@ type ChannelValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
1109
376
  | | `isLayerVisible(id)` | Query layer visibility |
1110
377
  | | `getLayerVisibility()` | All layer visibility map |
1111
378
  | | `hasLayerData(id)` | Check if layer has non-zero voxels |
379
+ | **Sphere** | `setActiveSphereType(type)` | Set active sphere type, updates brush color |
380
+ | | `getActiveSphereType()` | Read current sphere type |
381
+ | | `setCalculateDistanceSphere(x, y, slice, type)` | Programmatically place a calculator sphere |
1112
382
  | **Channel** | `setActiveChannel(ch)` | Switch drawing target channel |
1113
383
  | | `getActiveChannel()` | Read current channel |
1114
384
  | | `setChannelVisible(id, ch, bool)` | Toggle channel visibility in a layer |
@@ -1121,30 +391,82 @@ type ChannelValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
1121
391
  | | `getChannelHexColor(id, ch)` | Read Hex string |
1122
392
  | | `getChannelCssColor(id, ch)` | Read CSS rgba() string |
1123
393
  | | `resetChannelColors(id?, ch?)` | Reset to defaults |
1124
- | **History** | `undo()` | Undo last stroke |
1125
- | | `redo()` | Redo last undone stroke |
394
+ | **Tool Mode** | `setMode(mode)` | Switch tool: `"pencil"` / `"brush"` / `"eraser"` / `"sphere"` / `"calculator"` |
395
+ | | `getMode()` | Read current tool mode |
396
+ | **Drawing** | `setOpacity(value)` | Set mask overlay opacity [0.1, 1] |
397
+ | | `getOpacity()` | Read current opacity |
398
+ | | `setBrushSize(size)` | Set brush/eraser size [5, 50] |
399
+ | | `getBrushSize()` | Read current brush size |
400
+ | **Contrast** | `setWindowHigh(value)` | Set window high |
401
+ | | `setWindowLow(value)` | Set window low |
402
+ | | `finishWindowAdjustment()` | Repaint all contrast slices after drag ends |
403
+ | **Actions** | `executeAction(action)` | Run: `"undo"` / `"redo"` / `"clearActiveSliceMask"` / `"clearActiveLayerMask"` / `"resetZoom"` / `"downloadCurrentMask"` |
404
+ | **Navigation** | `setSliceOrientation(axis)` | Switch viewing axis `"x"` / `"y"` / `"z"` |
405
+ | **History** | `undo()` / `redo()` | Undo / redo last stroke |
1126
406
  | **Keyboard** | `setKeyboardSettings(partial)` | Remap shortcuts |
1127
407
  | | `getKeyboardSettings()` | Read current bindings |
1128
- | | `enterKeyboardConfig()` | Suppress all shortcuts |
1129
- | | `exitKeyboardConfig()` | Restore shortcuts |
408
+ | | `enterKeyboardConfig()` / `exitKeyboardConfig()` | Suppress / restore shortcuts |
1130
409
  | | `setContrastShortcutEnabled(bool)` | Enable/disable contrast key |
1131
- | | `isContrastShortcutEnabled()` | Query contrast key state |
1132
410
  | **Inspect** | `getCurrentImageDimension()` | `[w, h, d]` voxel dims |
1133
411
  | | `getVoxelSpacing()` | Physical mm spacing |
1134
412
  | | `getSpaceOrigin()` | World-space origin |
1135
413
  | | `getMaxSliceNum()` | Max slice index per axis |
1136
414
  | | `getCurrentSlicesNumAndContrastNum()` | Current slice & contrast index |
1137
415
  | | `getMaskData()` | Raw `IMaskData` object |
1138
- | | `getNrrdToolsSettings()` | Full `INrrdStates` snapshot |
416
+ | | `getNrrdToolsSettings()` | Full `NrrdState` snapshot |
1139
417
  | | `getContainer()` | Host `HTMLElement` |
1140
418
  | | `getDrawingCanvas()` | Top-layer `HTMLCanvasElement` |
1141
419
 
1142
420
  ---
1143
421
 
1144
- ## Acknowledgements
422
+ ### 12. Type Reference
1145
423
 
1146
- Special thanks to [Duke University dataset](https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=70226903) provides these awsome MRI data!!!
424
+ ```typescript
425
+ interface RGBAColor { r: number; g: number; b: number; a: number; } // 0-255
426
+
427
+ type ChannelColorMap = Record<number, RGBAColor>; // key = channel 1-8
428
+
429
+ interface IDrawOpts {
430
+ getMaskData?: (
431
+ sliceData: Uint8Array, layerId: string, channelId: number,
432
+ sliceIndex: number, axis: 'x' | 'y' | 'z',
433
+ width: number, height: number, clearFlag?: boolean
434
+ ) => void;
435
+ onClearLayerVolume?: (layerId: string) => void;
436
+ getSphereData?: (sphereOrigin: number[], sphereRadius: number) => void;
437
+ getCalculateSpherePositionsData?: (
438
+ tumourOrigin: ICommXYZ | null, skinOrigin: ICommXYZ | null,
439
+ ribOrigin: ICommXYZ | null, nippleOrigin: ICommXYZ | null,
440
+ axis: 'x' | 'y' | 'z'
441
+ ) => void;
442
+ }
443
+
444
+ interface IDragOpts {
445
+ showNumber?: boolean;
446
+ getSliceNum?: (sliceIndex: number, contrastIndex: number) => void;
447
+ }
448
+
449
+ interface IKeyBoardSettings {
450
+ draw: string;
451
+ undo: string;
452
+ redo: string;
453
+ contrast: string[];
454
+ crosshair: string;
455
+ sphere: string;
456
+ mouseWheel: 'Scroll:Zoom' | 'Scroll:Slice';
457
+ }
458
+
459
+ interface ICommXYZ { x: number; y: number; z: number; }
460
+
461
+ type LayerId = 'layer1' | 'layer2' | 'layer3' | 'layer4'; // or any string
462
+ type ChannelValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
463
+ ```
464
+
465
+ ---
466
+
467
+ ## Acknowledgements
1147
468
 
469
+ Special thanks to [Duke University dataset](https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=70226903) for providing the MRI data.
1148
470
 
1149
471
  ---
1150
472
 
@@ -1157,4 +479,4 @@ Special thanks to [Duke University dataset](https://wiki.cancerimagingarchive.ne
1157
479
  [nrrd_example]: https://img.shields.io/badge/Nrrd__Segmentation__tool-example-orange
1158
480
  [nrrd_example-url]: https://abi-web-apps.github.io/NRRD_Segmentation_Tool/
1159
481
  [heart_example-url]: https://uoa-heart-mechanics-research.github.io/medtech-heart/model-heart
1160
- [heart_example]: https://img.shields.io/badge/Medtech%20Heart-example-brightgreen
482
+ [heart_example]: https://img.shields.io/badge/Medtech%20Heart-example-brightgreen