copper3d 3.0.2 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -1028
- package/dist/Utils/segmentation/CanvasState.d.ts +31 -0
- package/dist/Utils/segmentation/CanvasState.js +168 -0
- package/dist/Utils/segmentation/CanvasState.js.map +1 -0
- package/dist/Utils/segmentation/DragOperator.d.ts +1 -1
- package/dist/Utils/segmentation/DragOperator.js +2 -4
- package/dist/Utils/segmentation/DragOperator.js.map +1 -1
- package/dist/Utils/segmentation/DrawToolCore.d.ts +49 -27
- package/dist/Utils/segmentation/DrawToolCore.js +275 -344
- package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
- package/dist/Utils/segmentation/NrrdTools.d.ts +92 -467
- package/dist/Utils/segmentation/NrrdTools.js +499 -1341
- package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
- package/dist/Utils/segmentation/RenderingUtils.d.ts +73 -0
- package/dist/Utils/segmentation/RenderingUtils.js +186 -0
- package/dist/Utils/segmentation/RenderingUtils.js.map +1 -0
- package/dist/Utils/segmentation/core/MaskVolume.d.ts +1 -1
- package/dist/Utils/segmentation/core/MaskVolume.js +1 -1
- package/dist/Utils/segmentation/core/index.d.ts +1 -1
- package/dist/Utils/segmentation/core/index.js.map +1 -1
- package/dist/Utils/segmentation/core/types.d.ts +386 -0
- package/dist/Utils/segmentation/core/types.js +1 -0
- package/dist/Utils/segmentation/core/types.js.map +1 -1
- package/dist/Utils/segmentation/coreTools/GuiState.d.ts +1 -1
- package/dist/Utils/segmentation/coreTools/GuiState.js +1 -0
- package/dist/Utils/segmentation/coreTools/GuiState.js.map +1 -1
- package/dist/Utils/segmentation/coreTools/NrrdState.d.ts +1 -1
- package/dist/Utils/segmentation/coreTools/divControlTools.d.ts +1 -1
- package/dist/Utils/segmentation/coreTools/gui.d.ts +1 -1
- package/dist/Utils/segmentation/tools/BaseTool.d.ts +1 -1
- package/dist/Utils/segmentation/tools/ContrastTool.d.ts +3 -6
- package/dist/Utils/segmentation/tools/ContrastTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/CrosshairTool.d.ts +11 -1
- package/dist/Utils/segmentation/tools/CrosshairTool.js +25 -0
- package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/DataLoader.d.ts +33 -0
- package/dist/Utils/segmentation/tools/DataLoader.js +159 -0
- package/dist/Utils/segmentation/tools/DataLoader.js.map +1 -0
- package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +3 -12
- package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/DrawingTool.d.ts +18 -26
- package/dist/Utils/segmentation/tools/DrawingTool.js +48 -0
- package/dist/Utils/segmentation/tools/DrawingTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +2 -5
- package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -1
- package/dist/Utils/segmentation/tools/LayerChannelManager.d.ts +56 -0
- package/dist/Utils/segmentation/tools/LayerChannelManager.js +188 -0
- package/dist/Utils/segmentation/tools/LayerChannelManager.js.map +1 -0
- package/dist/Utils/segmentation/tools/PanTool.d.ts +2 -8
- package/dist/Utils/segmentation/tools/PanTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/SliceRenderPipeline.d.ts +80 -0
- package/dist/Utils/segmentation/tools/SliceRenderPipeline.js +355 -0
- package/dist/Utils/segmentation/tools/SliceRenderPipeline.js.map +1 -0
- package/dist/Utils/segmentation/tools/SphereTool.d.ts +25 -19
- package/dist/Utils/segmentation/tools/SphereTool.js +61 -7
- package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/ToolHost.d.ts +74 -0
- package/dist/Utils/segmentation/tools/ToolHost.js +10 -0
- package/dist/Utils/segmentation/tools/ToolHost.js.map +1 -0
- package/dist/Utils/segmentation/tools/ZoomTool.d.ts +2 -6
- package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/index.d.ts +5 -7
- package/dist/Utils/segmentation/tools/index.js +6 -0
- package/dist/Utils/segmentation/tools/index.js.map +1 -1
- package/dist/Utils/workers/reformatSaveDataWorker.d.ts +1 -1
- package/dist/bundle.esm.js +2146 -2359
- package/dist/bundle.umd.js +2146 -2359
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/types/Utils/segmentation/CanvasState.d.ts +31 -0
- package/dist/types/Utils/segmentation/DragOperator.d.ts +1 -1
- package/dist/types/Utils/segmentation/DrawToolCore.d.ts +49 -27
- package/dist/types/Utils/segmentation/NrrdTools.d.ts +92 -467
- package/dist/types/Utils/segmentation/RenderingUtils.d.ts +73 -0
- package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +1 -1
- package/dist/types/Utils/segmentation/core/index.d.ts +1 -1
- package/dist/types/Utils/segmentation/core/types.d.ts +386 -0
- package/dist/types/Utils/segmentation/coreTools/GuiState.d.ts +1 -1
- package/dist/types/Utils/segmentation/coreTools/NrrdState.d.ts +1 -1
- package/dist/types/Utils/segmentation/coreTools/divControlTools.d.ts +1 -1
- package/dist/types/Utils/segmentation/coreTools/gui.d.ts +1 -1
- package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +1 -1
- package/dist/types/Utils/segmentation/tools/ContrastTool.d.ts +3 -6
- package/dist/types/Utils/segmentation/tools/CrosshairTool.d.ts +11 -1
- package/dist/types/Utils/segmentation/tools/DataLoader.d.ts +33 -0
- package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +3 -12
- package/dist/types/Utils/segmentation/tools/DrawingTool.d.ts +18 -26
- package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +2 -5
- package/dist/types/Utils/segmentation/tools/LayerChannelManager.d.ts +56 -0
- package/dist/types/Utils/segmentation/tools/PanTool.d.ts +2 -8
- package/dist/types/Utils/segmentation/tools/SliceRenderPipeline.d.ts +80 -0
- package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +25 -19
- package/dist/types/Utils/segmentation/tools/ToolHost.d.ts +74 -0
- package/dist/types/Utils/segmentation/tools/ZoomTool.d.ts +2 -6
- package/dist/types/Utils/segmentation/tools/index.d.ts +5 -7
- package/dist/types/Utils/workers/reformatSaveDataWorker.d.ts +1 -1
- package/dist/types/index.d.ts +3 -3
- package/package.json +1 -1
- package/dist/Utils/segmentation/CommToolsData.d.ts +0 -192
- package/dist/Utils/segmentation/CommToolsData.js +0 -474
- package/dist/Utils/segmentation/CommToolsData.js.map +0 -1
- package/dist/Utils/segmentation/coreTools/coreType.d.ts +0 -389
- package/dist/Utils/segmentation/coreTools/coreType.js +0 -3
- package/dist/Utils/segmentation/coreTools/coreType.js.map +0 -1
- package/dist/types/Utils/segmentation/CommToolsData.d.ts +0 -192
- package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +0 -389
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
|
|
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
|
-
###
|
|
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
|
-
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### Basic Usage
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
**Load demo**
|
|
29
30
|
|
|
30
31
|
```ts
|
|
31
32
|
import * as Copper from "copper3d";
|
|
32
33
|
import { getCurrentInstance, onMounted } from "vue";
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
let appRenderer;
|
|
35
36
|
onMounted(() => {
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
scene.createDemoMesh();
|
|
40
|
+
appRenderer.getCurrentScene().createDemoMesh();
|
|
42
41
|
appRenderer.animate();
|
|
43
42
|
});
|
|
44
43
|
```
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
**Options**
|
|
47
46
|
|
|
48
47
|
```ts
|
|
49
48
|
appRenderer = new Copper.copperRenderer(bg, { guiOpen: true });
|
|
50
49
|
```
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
**Multiple scenes with glTF**
|
|
53
52
|
|
|
54
53
|
```ts
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
62
|
+
appRenderer.setCurrentScene(scene);
|
|
78
63
|
}
|
|
79
64
|
}
|
|
80
65
|
```
|
|
81
66
|
|
|
82
|
-
|
|
67
|
+
**View data structure**
|
|
83
68
|
|
|
84
69
|
```ts
|
|
85
|
-
|
|
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`
|
|
99
|
-
|
|
100
|
-
---
|
|
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.
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
107
|
nrrdTools.reset();
|
|
132
|
-
nrrdTools.setAllSlices(allSlices);
|
|
108
|
+
nrrdTools.setAllSlices(allSlices); // allSlices from Copper scene loader
|
|
133
109
|
|
|
134
|
-
|
|
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,580 +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 |
|
|
163
|
-
| `options.layers` | `string[]` | `["layer1","layer2","layer3"]` | Named layers to create
|
|
164
|
-
|
|
165
|
-
#### Example: Single-layer (minimal)
|
|
131
|
+
| `container` | `HTMLDivElement` | required | Host DOM element for all annotation canvases |
|
|
132
|
+
| `options.layers` | `string[]` | `["layer1","layer2","layer3"]` | Named layers to create |
|
|
166
133
|
|
|
167
134
|
```typescript
|
|
168
|
-
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
#### Example: Custom layer set
|
|
172
|
-
|
|
173
|
-
```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
139
|
|
|
186
|
-
|
|
140
|
+
// Optional: show current slice index in a panel
|
|
141
|
+
nrrdTools.setDisplaySliceIndexPanel(document.getElementById('slice-panel') as HTMLDivElement);
|
|
187
142
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
nrrdTools.
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
#### Optional GUI (dat.GUI)
|
|
194
|
-
|
|
195
|
-
If your project uses `dat.GUI`, wire up the built-in control panel:
|
|
196
|
-
|
|
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
|
|
153
|
+
// Reset state then load new slices
|
|
214
154
|
nrrdTools.reset();
|
|
215
|
-
|
|
216
|
-
// allSlices is the array of decoded NRRD slice objects returned by Copper's loader
|
|
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
|
-
|
|
225
|
-
If the user has previously saved annotations, reload them into the volumes:
|
|
226
156
|
|
|
227
|
-
|
|
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
163
|
|
|
237
|
-
With
|
|
238
|
-
|
|
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
|
|
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
|
-
####
|
|
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,
|
|
306
|
-
getSliceNum: (sliceIndex, contrastIndex) =>
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
notifyBackendLayerCleared(layerId);
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
// Called when the 3D sphere annotation tool places a sphere (sphere mode)
|
|
340
|
-
//
|
|
341
|
-
// sphereOrigin: [mouseX, mouseY, sliceIndex] — center on z-axis view
|
|
342
|
-
// sphereRadius: radius in pixels (1-50)
|
|
343
|
-
//
|
|
344
|
-
// Note: Sphere data does NOT write to layer MaskVolume.
|
|
345
|
-
// It is rendered as a visual overlay via the dedicated sphereMaskVolume.
|
|
346
|
-
getSphereData: (sphereOrigin: number[], sphereRadius: number) => {
|
|
347
|
-
console.log('Sphere placed at', sphereOrigin, 'radius', sphereRadius);
|
|
348
|
-
sendSphereToBackend({ sphereOrigin, sphereRadius });
|
|
349
|
-
},
|
|
350
|
-
|
|
351
|
-
// Called when the calculator sphere positions are updated (calculator mode)
|
|
352
|
-
//
|
|
353
|
-
// Each sphere type maps to a specific channel on layer1:
|
|
354
|
-
// tumour → channel 1
|
|
355
|
-
// ribcage → channel 3
|
|
356
|
-
// skin → channel 4
|
|
357
|
-
// nipple → channel 2
|
|
358
|
-
//
|
|
359
|
-
// Origins are ICommXYZ: { x: [mx, my, slice], y: [...], z: [...] }
|
|
360
|
-
// representing sphere center coordinates on all 3 axis views.
|
|
361
|
-
// null if that sphere type has not been placed yet.
|
|
362
|
-
getCalculateSpherePositionsData: (
|
|
363
|
-
tumourOrigin, skinOrigin, ribOrigin, nippleOrigin, axis
|
|
364
|
-
) => {
|
|
365
|
-
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 });
|
|
366
205
|
},
|
|
367
206
|
});
|
|
368
207
|
```
|
|
369
208
|
|
|
370
|
-
####
|
|
371
|
-
|
|
372
|
-
The SphereTool provides unified sphere placement with 4 sphere types controlled by `gui_states.mode.activeSphereType`. The active type determines which origin and color are used. The old separate "Calculator" mode has been merged into SphereTool.
|
|
373
|
-
|
|
374
|
-
##### Sphere Types & Channel Mapping
|
|
375
|
-
|
|
376
|
-
Each sphere type maps to a specific channel on layer1. Switch between types using `gui_states.mode.activeSphereType`:
|
|
377
|
-
|
|
378
|
-
| Sphere Type | Channel | Default Color | `activeSphereType` value |
|
|
379
|
-
|-------------|---------|---------------|---------------------|
|
|
380
|
-
| tumour | 1 | `#10b981` (Emerald) | `"tumour"` (default) |
|
|
381
|
-
| nipple | 2 | `#f43f5e` (Rose) | `"nipple"` |
|
|
382
|
-
| ribcage | 3 | `#3b82f6` (Blue) | `"ribcage"` |
|
|
383
|
-
| skin | 4 | `#fbbf24` (Amber) | `"skin"` |
|
|
384
|
-
|
|
385
|
-
These mappings are exported as `SPHERE_CHANNEL_MAP` and `SPHERE_LABELS` from `tools/SphereTool.ts`. (`SPHERE_COLORS` was removed — colors are now derived dynamically from each volume's `colorMap`.)
|
|
386
|
-
|
|
387
|
-
##### Switching Sphere Type
|
|
388
|
-
|
|
389
|
-
Use the public API — do NOT mutate `gui_states.mode.activeSphereType` directly, as `setActiveSphereType()` also updates brush/fill color as a side-effect:
|
|
209
|
+
#### `enableContrastDragEvents()` — Window/Level
|
|
390
210
|
|
|
391
211
|
```typescript
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
nrrdTools.setActiveSphereType('tumour'); // channel 1, Emerald #10b981
|
|
395
|
-
nrrdTools.setActiveSphereType('skin'); // channel 4, Amber #fbbf24
|
|
396
|
-
nrrdTools.setActiveSphereType('ribcage'); // channel 3, Blue #3b82f6
|
|
397
|
-
|
|
398
|
-
// Read current type
|
|
399
|
-
const type = nrrdTools.getActiveSphereType();
|
|
400
|
-
// → 'tumour' | 'skin' | 'nipple' | 'ribcage'
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
##### Interaction Flow
|
|
404
|
-
|
|
405
|
-
```
|
|
406
|
-
Sphere mode activated (gui_states.mode.sphere = true, keyboard shortcut: 'q'):
|
|
407
|
-
├─ Shift key DISABLED (no draw mode)
|
|
408
|
-
├─ Crosshair toggle allowed (S key)
|
|
409
|
-
│
|
|
410
|
-
├─ Left-click DOWN → record origin for current activeSphereType type,
|
|
411
|
-
│ show preview, bind sphere wheel
|
|
412
|
-
├─ Scroll wheel (while holding) → adjust radius [1, 50]
|
|
413
|
-
└─ Left-click UP → write all placed spheres to volume,
|
|
414
|
-
fire getSphere + getCalculateSpherePositions callbacks,
|
|
415
|
-
restore zoom/slice wheel
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
##### SphereMaskVolume
|
|
419
|
-
|
|
420
|
-
Sphere 3D data is stored in a dedicated `MaskVolume` (`nrrd_states.sphereMaskVolume`), separate from the layer draw mask volumes. This prevents sphere overlay data from polluting layer1's annotations.
|
|
421
|
-
|
|
422
|
-
- **Created** in `setAllSlices()` (same dimensions as CT volume)
|
|
423
|
-
- **Cleared** in `reset()` (when switching cases)
|
|
424
|
-
|
|
425
|
-
> **Note**: Currently sphere data does NOT write to layer1's MaskVolume. The channel mapping and `sphereMaskVolume` are reserved for future integration.
|
|
426
|
-
|
|
427
|
-
##### Scenario: Sphere mode with AI backend (distance calculation)
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
nrrdTools.draw({
|
|
431
|
-
// Called on every sphere placement — receives all 4 sphere origins
|
|
432
|
-
getCalculateSpherePositionsData: (tumour, skin, rib, nipple, axis) => {
|
|
433
|
-
// Each origin is { x: [mx, my, slice], y: [...], z: [...] } or null
|
|
434
|
-
if (tumour && skin && rib && nipple) {
|
|
435
|
-
aiBackend.runSegmentation({ tumour, skin, rib, nipple, axis });
|
|
436
|
-
}
|
|
437
|
-
},
|
|
438
|
-
// Also called — receives the current sphere's origin and radius
|
|
439
|
-
getSphereData: (origin, radius) => {
|
|
440
|
-
console.log('Sphere placed at', origin, 'radius', radius);
|
|
441
|
-
},
|
|
212
|
+
nrrdTools.enableContrastDragEvents((step, towards) => {
|
|
213
|
+
console.log(`Contrast: ${towards} ${step}`);
|
|
442
214
|
});
|
|
443
215
|
```
|
|
444
216
|
|
|
445
|
-
|
|
217
|
+
#### SphereTool — Sphere Types & Channel Mapping
|
|
446
218
|
|
|
447
|
-
|
|
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"` |
|
|
448
225
|
|
|
449
226
|
```typescript
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
type: 'tumour' | 'skin' | 'nipple' | 'ribcage';
|
|
453
|
-
x: number; // X in unscaled image space
|
|
454
|
-
y: number; // Y in unscaled image space
|
|
455
|
-
sliceIndex: number; // Target slice index
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Place each sphere programmatically
|
|
459
|
-
function applySphereFromBackend(nrrdTools: Copper.NrrdTools, spheres: BackendSphereData[]) {
|
|
460
|
-
for (const s of spheres) {
|
|
461
|
-
nrrdTools.setCalculateDistanceSphere(s.x, s.y, s.sliceIndex, s.type);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Example: place a tumour sphere at (120, 95) on slice 42
|
|
466
|
-
nrrdTools.setCalculateDistanceSphere(120, 95, 42, 'tumour');
|
|
227
|
+
nrrdTools.setActiveSphereType('nipple'); // also updates brush/fill color
|
|
228
|
+
const type = nrrdTools.getActiveSphereType(); // → 'tumour' | 'skin' | 'nipple' | 'ribcage'
|
|
467
229
|
```
|
|
468
230
|
|
|
469
|
-
|
|
470
|
-
1. Sets `sphereRadius = 5` and navigates to the target slice
|
|
471
|
-
2. Records `sphereOrigin` on all 3 axes (via `crosshairTool.setUpSphereOrigins`)
|
|
472
|
-
3. Deep-copies the origin into the type-specific field (e.g., `tumourSphereOrigin`)
|
|
473
|
-
4. Draws the sphere preview on canvas (`drawCalculatorSphere`)
|
|
474
|
-
5. Writes all placed spheres to `sphereMaskVolume` (`writeAllCalculatorSpheresToVolume`)
|
|
475
|
-
6. Re-renders the sphere overlay (`refreshSphereCanvas`)
|
|
476
|
-
|
|
477
|
-
> **Note**: Coordinates (`x`, `y`) are in **unscaled** image space. The method automatically applies `sizeFactor` scaling internally.
|
|
478
|
-
|
|
479
|
-
#### 5.4 `enableContrastDragEvents()` — Windowing
|
|
480
|
-
|
|
481
|
-
Enable Ctrl+drag to adjust window/level (brightness/contrast):
|
|
231
|
+
**Programmatic sphere placement (backend → frontend):**
|
|
482
232
|
|
|
483
233
|
```typescript
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
console.log(`Contrast adjusted: ${towards} ${step}`);
|
|
488
|
-
});
|
|
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
|
|
489
237
|
```
|
|
490
238
|
|
|
491
239
|
---
|
|
492
240
|
|
|
493
241
|
### 6. Layer & Channel Management
|
|
494
242
|
|
|
495
|
-
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).
|
|
496
|
-
|
|
497
|
-
#### 6.1 Active Layer & Channel
|
|
498
|
-
|
|
499
243
|
```typescript
|
|
500
|
-
//
|
|
244
|
+
// Active layer / channel
|
|
501
245
|
nrrdTools.setActiveLayer('layer2');
|
|
246
|
+
nrrdTools.setActiveChannel(3);
|
|
247
|
+
const layer = nrrdTools.getActiveLayer();
|
|
248
|
+
const channel = nrrdTools.getActiveChannel();
|
|
502
249
|
|
|
503
|
-
//
|
|
504
|
-
nrrdTools.setActiveChannel(3); // channel 3 = blue by default
|
|
505
|
-
|
|
506
|
-
// Query current state
|
|
507
|
-
const currentLayer = nrrdTools.getActiveLayer(); // → 'layer2'
|
|
508
|
-
const currentChannel = nrrdTools.getActiveChannel(); // → 3
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
#### 6.2 Layer Visibility
|
|
512
|
-
|
|
513
|
-
Toggle layers on/off in the composite canvas:
|
|
514
|
-
|
|
515
|
-
```typescript
|
|
516
|
-
// Hide layer2 (e.g., edema) while keeping layer1 visible
|
|
250
|
+
// Layer visibility
|
|
517
251
|
nrrdTools.setLayerVisible('layer2', false);
|
|
518
|
-
nrrdTools.
|
|
519
|
-
|
|
520
|
-
// Query visibility
|
|
521
|
-
const layer2Visible = nrrdTools.isLayerVisible('layer2'); // → false
|
|
522
|
-
|
|
523
|
-
// Get all at once
|
|
524
|
-
const visibilityMap = nrrdTools.getLayerVisibility();
|
|
525
|
-
// → { layer1: true, layer2: false, layer3: true, layer4: true }
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
#### Scenario: Eye-button toggle in UI
|
|
529
|
-
|
|
530
|
-
```typescript
|
|
531
|
-
function onToggleLayerEye(layerId: string) {
|
|
532
|
-
const current = nrrdTools.isLayerVisible(layerId);
|
|
533
|
-
nrrdTools.setLayerVisible(layerId, !current);
|
|
534
|
-
}
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
#### 6.3 Channel Visibility
|
|
538
|
-
|
|
539
|
-
Each layer can independently show/hide its channels:
|
|
252
|
+
const visible = nrrdTools.isLayerVisible('layer2');
|
|
253
|
+
const visMap = nrrdTools.getLayerVisibility(); // { layer1: true, layer2: false, ... }
|
|
540
254
|
|
|
541
|
-
|
|
542
|
-
// Hide channel 2 in layer1 (e.g., secondary annotation color)
|
|
255
|
+
// Channel visibility (per layer)
|
|
543
256
|
nrrdTools.setChannelVisible('layer1', 2, false);
|
|
544
|
-
|
|
545
|
-
// Show it again
|
|
546
|
-
nrrdTools.setChannelVisible('layer1', 2, true);
|
|
547
|
-
|
|
548
|
-
// Query
|
|
549
|
-
const ch2Visible = nrrdTools.isChannelVisible('layer1', 2); // → true
|
|
550
|
-
|
|
551
|
-
// Get all channel visibility for all layers
|
|
552
257
|
const allChannelVis = nrrdTools.getChannelVisibility();
|
|
553
|
-
// → { layer1: { 1: true, 2: false, 3: true, ... }, layer2: { 1: true, ... }, ... }
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
#### Scenario: Showing only the selected channel
|
|
557
258
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
for (let ch = 1; ch <= totalChannels; ch++) {
|
|
561
|
-
nrrdTools.setChannelVisible(layerId, ch, ch === targetChannel);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Show only channel 1 in layer1
|
|
566
|
-
isolateChannel('layer1', 1);
|
|
567
|
-
|
|
568
|
-
// Restore all
|
|
569
|
-
for (let ch = 1; ch <= 8; ch++) {
|
|
570
|
-
nrrdTools.setChannelVisible('layer1', ch, true);
|
|
571
|
-
}
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
#### 6.4 Checking if a layer has annotations
|
|
575
|
-
|
|
576
|
-
```typescript
|
|
577
|
-
if (nrrdTools.hasLayerData('layer2')) {
|
|
578
|
-
console.log('layer2 has annotated voxels — saving...');
|
|
579
|
-
await saveLayer('layer2');
|
|
580
|
-
} else {
|
|
581
|
-
console.log('layer2 is empty, skipping save');
|
|
582
|
-
}
|
|
259
|
+
// Check if a layer has annotations
|
|
260
|
+
if (nrrdTools.hasLayerData('layer1')) await saveLayer('layer1');
|
|
583
261
|
```
|
|
584
262
|
|
|
585
263
|
---
|
|
586
264
|
|
|
587
265
|
### 7. Channel Color Customization
|
|
588
266
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
#### Default Channel Colors
|
|
267
|
+
**Default colors:**
|
|
592
268
|
|
|
593
269
|
| Channel | Color | Hex |
|
|
594
270
|
|---------|-------|-----|
|
|
595
|
-
| 1 | Emerald
|
|
596
|
-
| 2 | Rose
|
|
597
|
-
| 3 | Blue
|
|
598
|
-
| 4 | Amber
|
|
599
|
-
| 5 | Fuchsia
|
|
600
|
-
| 6 | Cyan
|
|
601
|
-
| 7 | Orange
|
|
602
|
-
| 8 | Violet
|
|
603
|
-
|
|
604
|
-
#### 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` |
|
|
605
279
|
|
|
606
280
|
```typescript
|
|
607
|
-
// RGBAColor: { r, g, b, a } —
|
|
608
|
-
nrrdTools.setChannelColor('layer1', 3, { r: 255, g: 128, b: 0, a: 255 });
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
#### 7.2 Batch-set multiple channel colors (recommended)
|
|
612
|
-
|
|
613
|
-
One `reloadMasksFromVolume()` call instead of N calls — better performance:
|
|
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 });
|
|
614
283
|
|
|
615
|
-
|
|
284
|
+
// Batch-set (one reloadMasksFromVolume call — better performance)
|
|
616
285
|
nrrdTools.setChannelColors('layer1', {
|
|
617
|
-
1: { r: 255, g:
|
|
618
|
-
2: { r:
|
|
619
|
-
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 },
|
|
620
288
|
});
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
#### 7.3 Apply the same color to all layers
|
|
624
289
|
|
|
625
|
-
|
|
626
|
-
// Paint all layers' channel 1 in the same tumour color
|
|
290
|
+
// Apply same channel color across all layers
|
|
627
291
|
nrrdTools.setAllLayersChannelColor(1, { r: 0, g: 220, b: 100, a: 255 });
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
#### 7.4 Read current colors
|
|
631
|
-
|
|
632
|
-
```typescript
|
|
633
|
-
const rgba = nrrdTools.getChannelColor('layer2', 3);
|
|
634
|
-
// → { r: 255, g: 128, b: 0, a: 255 }
|
|
635
|
-
|
|
636
|
-
const hex = nrrdTools.getChannelHexColor('layer2', 3);
|
|
637
|
-
// → "#ff8000" — suitable for canvas fillStyle
|
|
638
|
-
|
|
639
|
-
const css = nrrdTools.getChannelCssColor('layer2', 3);
|
|
640
|
-
// → "rgba(255,128,0,1.00)" — suitable for Vue :style binding
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
#### 7.5 Reset colors
|
|
644
|
-
|
|
645
|
-
```typescript
|
|
646
|
-
// Reset one channel in one layer
|
|
647
|
-
nrrdTools.resetChannelColors('layer1', 3);
|
|
648
|
-
|
|
649
|
-
// Reset all channels in one layer
|
|
650
|
-
nrrdTools.resetChannelColors('layer1');
|
|
651
|
-
|
|
652
|
-
// Reset everything across all layers
|
|
653
|
-
nrrdTools.resetChannelColors();
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
#### Scenario: Color picker integration
|
|
657
|
-
|
|
658
|
-
```typescript
|
|
659
|
-
// User picks a new color in the UI color picker
|
|
660
|
-
function onColorPicked(hexColor: string) {
|
|
661
|
-
// Parse hex to RGBA (simple implementation)
|
|
662
|
-
const r = parseInt(hexColor.slice(1, 3), 16);
|
|
663
|
-
const g = parseInt(hexColor.slice(3, 5), 16);
|
|
664
|
-
const b = parseInt(hexColor.slice(5, 7), 16);
|
|
665
292
|
|
|
666
|
-
|
|
667
|
-
|
|
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)"
|
|
668
297
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
#### Scenario: Vue UI reactivity after color change
|
|
674
|
-
|
|
675
|
-
After calling `setChannelColor()`, the canvas re-renders automatically.
|
|
676
|
-
But Vue computed properties that show the color need a manual nudge:
|
|
677
|
-
|
|
678
|
-
```typescript
|
|
679
|
-
// In your composable or component
|
|
680
|
-
const colorVersion = ref(0);
|
|
681
|
-
|
|
682
|
-
const channelCssColor = computed(() => {
|
|
683
|
-
colorVersion.value; // subscribe to version
|
|
684
|
-
return nrrdTools.value?.getChannelCssColor(activeLayer.value, activeChannel.value) ?? '#00ff00';
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
function refreshColors() {
|
|
688
|
-
colorVersion.value++; // forces recompute
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// After setting color:
|
|
692
|
-
nrrdTools.value.setChannelColor('layer1', 2, { r: 255, g: 0, b: 0, a: 255 });
|
|
693
|
-
refreshColors();
|
|
298
|
+
// Reset
|
|
299
|
+
nrrdTools.resetChannelColors('layer1', 3); // one channel
|
|
300
|
+
nrrdTools.resetChannelColors('layer1'); // all channels in layer
|
|
301
|
+
nrrdTools.resetChannelColors(); // everything
|
|
694
302
|
```
|
|
695
303
|
|
|
696
304
|
---
|
|
697
305
|
|
|
698
306
|
### 8. Undo / Redo
|
|
699
307
|
|
|
700
|
-
|
|
308
|
+
Per-layer undo stack (max 50 entries). Every completed stroke pushes a delta snapshot.
|
|
701
309
|
|
|
702
310
|
```typescript
|
|
703
|
-
// Undo the last stroke on the active layer
|
|
704
311
|
nrrdTools.undo();
|
|
705
|
-
|
|
706
|
-
// Redo the last undone stroke
|
|
707
312
|
nrrdTools.redo();
|
|
708
|
-
```
|
|
709
313
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
```typescript
|
|
314
|
+
// Or via keyboard
|
|
713
315
|
window.addEventListener('keydown', (e) => {
|
|
714
316
|
if (e.ctrlKey && e.key === 'z') nrrdTools.undo();
|
|
715
317
|
if (e.ctrlKey && e.key === 'y') nrrdTools.redo();
|
|
716
318
|
});
|
|
717
319
|
```
|
|
718
320
|
|
|
719
|
-
> The built-in keyboard system also handles undo/redo (default: `z` and `y` without Ctrl).
|
|
720
|
-
> See [Keyboard Shortcuts](#9-keyboard-shortcuts) for configuring this.
|
|
721
|
-
|
|
722
321
|
---
|
|
723
322
|
|
|
724
323
|
### 9. Keyboard Shortcuts
|
|
725
324
|
|
|
726
|
-
#### Default bindings
|
|
727
|
-
|
|
728
325
|
| Action | Default Key |
|
|
729
326
|
|--------|-------------|
|
|
730
327
|
| Draw mode | `Shift` (hold) |
|
|
@@ -735,523 +332,43 @@ window.addEventListener('keydown', (e) => {
|
|
|
735
332
|
| Sphere mode | `q` |
|
|
736
333
|
| Mouse wheel | Zoom |
|
|
737
334
|
|
|
738
|
-
#### Reading current settings
|
|
739
|
-
|
|
740
335
|
```typescript
|
|
336
|
+
nrrdTools.setKeyboardSettings({ undo: 'u', mouseWheel: 'Scroll:Slice' });
|
|
741
337
|
const settings = nrrdTools.getKeyboardSettings();
|
|
742
|
-
console.log(settings);
|
|
743
|
-
// {
|
|
744
|
-
// draw: 'Shift',
|
|
745
|
-
// undo: 'z',
|
|
746
|
-
// redo: 'y',
|
|
747
|
-
// contrast: ['Control', 'Meta'],
|
|
748
|
-
// crosshair: 's',
|
|
749
|
-
// mouseWheel: 'Scroll:Zoom'
|
|
750
|
-
// }
|
|
751
|
-
```
|
|
752
338
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
nrrdTools.setKeyboardSettings({
|
|
757
|
-
undo: 'u', // Remap undo to 'u'
|
|
758
|
-
mouseWheel: 'Scroll:Slice', // Mouse wheel scrolls slices instead of zooming
|
|
759
|
-
});
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
#### Scenario: Form input focus — suppressing shortcuts
|
|
763
|
-
|
|
764
|
-
When a user types in an input field, you don't want tool shortcuts firing:
|
|
765
|
-
|
|
766
|
-
```typescript
|
|
767
|
-
inputElement.addEventListener('focus', () => {
|
|
768
|
-
nrrdTools.enterKeyboardConfig(); // Suppress all tool shortcuts
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
inputElement.addEventListener('blur', () => {
|
|
772
|
-
nrrdTools.exitKeyboardConfig(); // Restore shortcuts
|
|
773
|
-
});
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
#### Enabling / disabling contrast shortcut independently
|
|
777
|
-
|
|
778
|
-
```typescript
|
|
779
|
-
// Disable only the contrast (window/level) keyboard shortcut
|
|
780
|
-
nrrdTools.setContrastShortcutEnabled(false);
|
|
781
|
-
|
|
782
|
-
// Check status
|
|
783
|
-
const isEnabled = nrrdTools.isContrastShortcutEnabled(); // → false
|
|
784
|
-
|
|
785
|
-
// Re-enable
|
|
786
|
-
nrrdTools.setContrastShortcutEnabled(true);
|
|
339
|
+
// Suppress shortcuts while a form input is focused
|
|
340
|
+
inputEl.addEventListener('focus', () => nrrdTools.enterKeyboardConfig());
|
|
341
|
+
inputEl.addEventListener('blur', () => nrrdTools.exitKeyboardConfig());
|
|
787
342
|
```
|
|
788
343
|
|
|
789
344
|
---
|
|
790
345
|
|
|
791
|
-
### 10.
|
|
792
|
-
|
|
793
|
-
#### Canvas size scaling
|
|
794
|
-
|
|
795
|
-
Set the base display size multiplier (1–8). Larger values use more GPU memory but give sharper annotations:
|
|
796
|
-
|
|
797
|
-
```typescript
|
|
798
|
-
nrrdTools.setBaseDrawDisplayCanvasesSize(2); // 2× base resolution
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
#### Reading image metadata
|
|
802
|
-
|
|
803
|
-
```typescript
|
|
804
|
-
// Voxel dimensions [width, height, depth]
|
|
805
|
-
const dims = nrrdTools.getCurrentImageDimension();
|
|
806
|
-
// → [512, 512, 256]
|
|
807
|
-
|
|
808
|
-
// Physical spacing (mm per voxel, from NRRD header)
|
|
809
|
-
const spacing = nrrdTools.getVoxelSpacing();
|
|
810
|
-
// → [0.488, 0.488, 1.0]
|
|
811
|
-
|
|
812
|
-
// World-space origin
|
|
813
|
-
const origin = nrrdTools.getSpaceOrigin();
|
|
814
|
-
// → [-125.0, -125.0, -127.5]
|
|
815
|
-
|
|
816
|
-
// Max slice count per axis
|
|
817
|
-
const maxSlices = nrrdTools.getMaxSliceNum();
|
|
818
|
-
// → [512, 512, 256] (one per axis)
|
|
819
|
-
|
|
820
|
-
// Current viewing state
|
|
821
|
-
const { currentSliceIndex, contrastIndex } = nrrdTools.getCurrentSlicesNumAndContrastNum();
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
#### Accessing internal canvases
|
|
825
|
-
|
|
826
|
-
```typescript
|
|
827
|
-
// The topmost interactive canvas (mouse/pen events target here)
|
|
828
|
-
const drawingCanvas = nrrdTools.getDrawingCanvas();
|
|
829
|
-
|
|
830
|
-
// The inner main-area container div (dynamically created by NrrdTools, NOT the original container passed to constructor)
|
|
831
|
-
const container = nrrdTools.getContainer();
|
|
832
|
-
|
|
833
|
-
// Raw NrrdState (all internal state fields, grouped by semantic sub-objects)
|
|
834
|
-
const states = nrrdTools.getNrrdToolsSettings();
|
|
835
|
-
console.log(states.image.layers); // ["layer1", "layer2", ...]
|
|
836
|
-
console.log(states.image.dimensions); // [512, 512, 256]
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
#### Accessing full mask data
|
|
840
|
-
|
|
841
|
-
```typescript
|
|
842
|
-
// All MaskVolume data structures
|
|
843
|
-
const maskData = nrrdTools.getMaskData();
|
|
844
|
-
// maskData.volumes is Record<string, MaskVolume>
|
|
845
|
-
```
|
|
846
|
-
|
|
847
|
-
---
|
|
848
|
-
|
|
849
|
-
### 11. Reading State & Diagnostics
|
|
850
|
-
|
|
851
|
-
```typescript
|
|
852
|
-
// Full snapshot of current state
|
|
853
|
-
const state = nrrdTools.getNrrdToolsSettings();
|
|
854
|
-
|
|
855
|
-
// Active layer / channel
|
|
856
|
-
const layer = nrrdTools.getActiveLayer();
|
|
857
|
-
const channel = nrrdTools.getActiveChannel();
|
|
858
|
-
|
|
859
|
-
// Visibility maps
|
|
860
|
-
const layerVis = nrrdTools.getLayerVisibility();
|
|
861
|
-
const channelVis = nrrdTools.getChannelVisibility();
|
|
862
|
-
|
|
863
|
-
// Check if any real annotation exists in a layer
|
|
864
|
-
const hasTumour = nrrdTools.hasLayerData('layer1');
|
|
865
|
-
|
|
866
|
-
// Get current channel color as hex
|
|
867
|
-
const hex = nrrdTools.getChannelHexColor('layer1', 1); // → "#10b981"
|
|
868
|
-
|
|
869
|
-
// Keyboard settings
|
|
870
|
-
const keys = nrrdTools.getKeyboardSettings();
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
---
|
|
874
|
-
|
|
875
|
-
### 12. Advanced Scenarios
|
|
876
|
-
|
|
877
|
-
#### Scenario A: Complete multi-layer initialization from scratch
|
|
878
|
-
|
|
879
|
-
```typescript
|
|
880
|
-
async function initAnnotationTool(container: HTMLDivElement, allSlices: any[]) {
|
|
881
|
-
// Create tool with 4 layers
|
|
882
|
-
const nrrdTools = new Copper.NrrdTools(container, {
|
|
883
|
-
layers: ['layer1', 'layer2', 'layer3', 'layer4']
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
// Attach GUI
|
|
887
|
-
nrrdTools.setupGUI(gui as any);
|
|
888
|
-
|
|
889
|
-
// Attach contrast drag
|
|
890
|
-
nrrdTools.enableContrastDragEvents((step, towards) => {
|
|
891
|
-
console.log('Windowing:', towards, step);
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
// Load image volume
|
|
895
|
-
nrrdTools.reset();
|
|
896
|
-
nrrdTools.setAllSlices(allSlices);
|
|
897
|
-
|
|
898
|
-
// Register callbacks
|
|
899
|
-
nrrdTools.drag({ getSliceNum: (idx) => updateSliceUI(idx) });
|
|
900
|
-
|
|
901
|
-
nrrdTools.draw({
|
|
902
|
-
getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, w, h, clearFlag) => {
|
|
903
|
-
syncSliceToBackend({ sliceData, layerId, channelId, sliceIndex, axis, clearFlag });
|
|
904
|
-
},
|
|
905
|
-
onClearLayerVolume: (layerId) => {
|
|
906
|
-
notifyBackendCleared(layerId);
|
|
907
|
-
},
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
// Connect to render loop
|
|
911
|
-
scene.addPreRenderCallbackFunction(nrrdTools.start);
|
|
912
|
-
|
|
913
|
-
return nrrdTools;
|
|
914
|
-
}
|
|
915
|
-
```
|
|
916
|
-
|
|
917
|
-
#### Scenario B: Load existing annotations and apply custom color scheme
|
|
918
|
-
|
|
919
|
-
```typescript
|
|
920
|
-
async function loadAndColorCase(nrrdTools: Copper.NrrdTools, caseId: string) {
|
|
921
|
-
// 1. Load existing NIfTI masks
|
|
922
|
-
const masks = await fetchCaseMasks(caseId);
|
|
923
|
-
const layerVoxels = new Map<string, Uint8Array>();
|
|
924
|
-
for (const [layerId, data] of Object.entries(masks)) {
|
|
925
|
-
layerVoxels.set(layerId, data);
|
|
926
|
-
}
|
|
927
|
-
nrrdTools.setMasksFromNIfTI(layerVoxels);
|
|
928
|
-
|
|
929
|
-
// 2. Apply per-layer color schemes
|
|
930
|
-
nrrdTools.setChannelColors('layer1', {
|
|
931
|
-
1: { r: 0, g: 200, b: 80, a: 255 }, // Tumour core — green
|
|
932
|
-
2: { r: 255, g: 200, b: 0, a: 255 }, // Tumour ring — yellow
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
nrrdTools.setChannelColors('layer2', {
|
|
936
|
-
1: { r: 255, g: 60, b: 60, a: 200 }, // Edema — semi-transparent red
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
// 3. Set initial active state
|
|
940
|
-
nrrdTools.setActiveLayer('layer1');
|
|
941
|
-
nrrdTools.setActiveChannel(1);
|
|
942
|
-
}
|
|
943
|
-
```
|
|
944
|
-
|
|
945
|
-
#### Scenario C: Switching cases — full reset
|
|
946
|
-
|
|
947
|
-
```typescript
|
|
948
|
-
async function switchCase(nrrdTools: Copper.NrrdTools, newCaseData: ICaseData) {
|
|
949
|
-
// Reset annotation volumes
|
|
950
|
-
nrrdTools.reset();
|
|
951
|
-
|
|
952
|
-
// Load new image slices (from Copper's scene loader result)
|
|
953
|
-
nrrdTools.setAllSlices(newCaseData.slices);
|
|
954
|
-
|
|
955
|
-
// Reset colors to defaults
|
|
956
|
-
nrrdTools.resetChannelColors();
|
|
957
|
-
|
|
958
|
-
// Make all layers and channels visible
|
|
959
|
-
for (const layerId of ['layer1', 'layer2', 'layer3', 'layer4']) {
|
|
960
|
-
nrrdTools.setLayerVisible(layerId, true);
|
|
961
|
-
for (let ch = 1; ch <= 8; ch++) {
|
|
962
|
-
nrrdTools.setChannelVisible(layerId, ch, true);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// Start fresh on layer1, channel1
|
|
967
|
-
nrrdTools.setActiveLayer('layer1');
|
|
968
|
-
nrrdTools.setActiveChannel(1);
|
|
969
|
-
|
|
970
|
-
// Load existing masks if available
|
|
971
|
-
if (newCaseData.hasExistingMasks) {
|
|
972
|
-
nrrdTools.setMasksFromNIfTI(newCaseData.layerVoxels);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
```
|
|
976
|
-
|
|
977
|
-
#### Scenario D: Save workflow with dirty-layer detection
|
|
346
|
+
### 10. Clearing Annotations
|
|
978
347
|
|
|
979
348
|
```typescript
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
for (const layerId of layers) {
|
|
984
|
-
if (!nrrdTools.hasLayerData(layerId)) {
|
|
985
|
-
console.log(`${layerId} is empty — initializing blank NIfTI on backend`);
|
|
986
|
-
await initBlankLayerOnBackend(caseId, layerId);
|
|
987
|
-
} else {
|
|
988
|
-
console.log(`${layerId} has data — saving...`);
|
|
989
|
-
await saveLayerToBackend(caseId, layerId);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
#### Scenario E: AI segmentation result — write back to volume
|
|
996
|
-
|
|
997
|
-
After receiving a segmentation result from a backend AI model, write it directly into a layer:
|
|
998
|
-
|
|
999
|
-
```typescript
|
|
1000
|
-
async function applyAIResult(nrrdTools: Copper.NrrdTools, layerId: string) {
|
|
1001
|
-
// Fetch AI-generated NIfTI from backend
|
|
1002
|
-
const response = await fetch(`/api/ai-result/${layerId}`);
|
|
1003
|
-
const buffer = await response.arrayBuffer();
|
|
1004
|
-
const voxels = new Uint8Array(buffer);
|
|
1005
|
-
|
|
1006
|
-
// Write directly into the target layer volume
|
|
1007
|
-
const layerVoxels = new Map<string, Uint8Array>([[layerId, voxels]]);
|
|
1008
|
-
nrrdTools.setMasksFromNIfTI(layerVoxels);
|
|
1009
|
-
|
|
1010
|
-
// Optionally switch to that layer so user sees the result
|
|
1011
|
-
nrrdTools.setActiveLayer(layerId);
|
|
1012
|
-
}
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
#### Scenario F: Dynamic keyboard remap from user settings
|
|
1016
|
-
|
|
1017
|
-
```typescript
|
|
1018
|
-
interface UserPreferences {
|
|
1019
|
-
undoKey: string;
|
|
1020
|
-
redoKey: string;
|
|
1021
|
-
sliceScrollMode: 'zoom' | 'slice';
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function applyUserKeyboardPreferences(nrrdTools: Copper.NrrdTools, prefs: UserPreferences) {
|
|
1025
|
-
nrrdTools.setKeyboardSettings({
|
|
1026
|
-
undo: prefs.undoKey,
|
|
1027
|
-
redo: prefs.redoKey,
|
|
1028
|
-
mouseWheel: prefs.sliceScrollMode === 'slice' ? 'Scroll:Slice' : 'Scroll:Zoom',
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// Example: Apply on preference change
|
|
1033
|
-
applyUserKeyboardPreferences(nrrdTools, {
|
|
1034
|
-
undoKey: 'u',
|
|
1035
|
-
redoKey: 'r',
|
|
1036
|
-
sliceScrollMode: 'slice',
|
|
1037
|
-
});
|
|
1038
|
-
```
|
|
1039
|
-
|
|
1040
|
-
#### Scenario G: Read layer colors to build a color legend
|
|
1041
|
-
|
|
1042
|
-
```typescript
|
|
1043
|
-
function buildColorLegend(nrrdTools: Copper.NrrdTools, layerId: string) {
|
|
1044
|
-
const legend = [];
|
|
1045
|
-
for (let ch = 1; ch <= 8; ch++) {
|
|
1046
|
-
legend.push({
|
|
1047
|
-
channel: ch,
|
|
1048
|
-
cssColor: nrrdTools.getChannelCssColor(layerId, ch),
|
|
1049
|
-
hexColor: nrrdTools.getChannelHexColor(layerId, ch),
|
|
1050
|
-
rgba: nrrdTools.getChannelColor(layerId, ch),
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
return legend;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// → [
|
|
1057
|
-
// { channel: 1, cssColor: 'rgba(16,185,129,1.00)', hexColor: '#10b981', rgba: { r:16, g:185, b:129, a:255 } },
|
|
1058
|
-
// { channel: 2, cssColor: 'rgba(244,63,94,1.00)', hexColor: '#f43f5e', rgba: { r:244, g:63, b:94, a:255 } },
|
|
1059
|
-
// ...
|
|
1060
|
-
// ]
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
#### Scenario H: Clearing annotations
|
|
1064
|
-
|
|
1065
|
-
There are different levels of clearing data depending on the use case:
|
|
1066
|
-
|
|
1067
|
-
```typescript
|
|
1068
|
-
// 1. Clear ALL data across ALL layers comprehensively (typically used when switching cases)
|
|
1069
|
-
// This clears all volumes, undo histories, UI canvases, index parameters, and sphere data globally.
|
|
1070
|
-
nrrdTools.reset();
|
|
1071
|
-
|
|
1072
|
-
// 2. Clear all annotations on the currently active layer across its entire 3D volume
|
|
1073
|
-
// This clears the active layer's MaskVolume, clears its undo/redo history, and re-renders the canvas.
|
|
1074
|
-
// It also triggers the `onClearLayerVolume` callback so you can notify the backend.
|
|
1075
|
-
nrrdTools.clearActiveLayer();
|
|
1076
|
-
|
|
1077
|
-
// 3. Clear the mask ONLY on the currently viewed 2D slice for the active layer
|
|
1078
|
-
// This removes annotations specifically on the current slice index and orientation.
|
|
1079
|
-
// It records an undo operation to allow rollback.
|
|
1080
|
-
nrrdTools.clearActiveSlice();
|
|
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)
|
|
1081
352
|
```
|
|
1082
353
|
|
|
1083
354
|
---
|
|
1084
355
|
|
|
1085
|
-
###
|
|
1086
|
-
|
|
1087
|
-
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.
|
|
1088
|
-
|
|
1089
|
-
#### LeftPanel (creator)
|
|
1090
|
-
|
|
1091
|
-
```vue
|
|
1092
|
-
<script setup lang="ts">
|
|
1093
|
-
import * as Copper from 'copper3d';
|
|
1094
|
-
import emitter from '@/plugins/custom-emitter';
|
|
1095
|
-
|
|
1096
|
-
let nrrdTools: Copper.NrrdTools | undefined;
|
|
1097
|
-
|
|
1098
|
-
onMounted(() => {
|
|
1099
|
-
nrrdTools = new Copper.NrrdTools(canvasContainer.value as HTMLDivElement, {
|
|
1100
|
-
layers: ['layer1', 'layer2', 'layer3', 'layer4']
|
|
1101
|
-
});
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
// After all NRRD files are loaded by the Copper scene:
|
|
1105
|
-
function onAllImagesLoaded(allSlices: any[]) {
|
|
1106
|
-
nrrdTools!.reset();
|
|
1107
|
-
nrrdTools!.setAllSlices(allSlices);
|
|
1108
|
-
|
|
1109
|
-
nrrdTools!.drag({ getSliceNum: (idx) => emit('sliceChanged', idx) });
|
|
1110
|
-
|
|
1111
|
-
nrrdTools!.draw({
|
|
1112
|
-
getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, w, h, clearFlag) => {
|
|
1113
|
-
emit('maskDataUpdated', { sliceData, layerId, channelId, sliceIndex, axis, w, h, clearFlag });
|
|
1114
|
-
},
|
|
1115
|
-
onClearLayerVolume: (layerId) => emit('layerCleared', layerId),
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
scene!.addPreRenderCallbackFunction(nrrdTools!.start);
|
|
1119
|
-
|
|
1120
|
-
// Broadcast to all other components
|
|
1121
|
-
emitter.emit('Core:NrrdTools', nrrdTools!);
|
|
1122
|
-
emitter.emit('Segmentation:FinishLoadAllCaseImages');
|
|
1123
|
-
}
|
|
1124
|
-
</script>
|
|
1125
|
-
```
|
|
1126
|
-
|
|
1127
|
-
#### Annotation Control Panel (consumer)
|
|
1128
|
-
|
|
1129
|
-
```vue
|
|
1130
|
-
<script setup lang="ts">
|
|
1131
|
-
import * as Copper from 'copper3d';
|
|
1132
|
-
import emitter from '@/plugins/custom-emitter';
|
|
1133
|
-
|
|
1134
|
-
const nrrdTools = ref<Copper.NrrdTools>();
|
|
1135
|
-
|
|
1136
|
-
onMounted(() => {
|
|
1137
|
-
emitter.on('Core:NrrdTools', (tools: Copper.NrrdTools) => {
|
|
1138
|
-
nrrdTools.value = tools;
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1141
|
-
emitter.on('Segmentation:FinishLoadAllCaseImages', () => {
|
|
1142
|
-
// Safe to call all APIs now — MaskVolume is initialized
|
|
1143
|
-
syncStateFromTools();
|
|
1144
|
-
});
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
function syncStateFromTools() {
|
|
1148
|
-
if (!nrrdTools.value) return;
|
|
1149
|
-
activeLayer.value = nrrdTools.value.getActiveLayer();
|
|
1150
|
-
activeChannel.value = nrrdTools.value.getActiveChannel();
|
|
1151
|
-
layerVisibility.value = nrrdTools.value.getLayerVisibility();
|
|
1152
|
-
channelVisibility.value = nrrdTools.value.getChannelVisibility();
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
function onChannelColorPicked(hex: string) {
|
|
1156
|
-
if (!nrrdTools.value) return;
|
|
1157
|
-
const r = parseInt(hex.slice(1,3), 16);
|
|
1158
|
-
const g = parseInt(hex.slice(3,5), 16);
|
|
1159
|
-
const b = parseInt(hex.slice(5,7), 16);
|
|
1160
|
-
nrrdTools.value.setChannelColor(activeLayer.value, activeChannel.value, { r, g, b, a: 255 });
|
|
1161
|
-
refreshColors(); // bump colorVersion to re-trigger computed
|
|
1162
|
-
}
|
|
1163
|
-
</script>
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
---
|
|
1167
|
-
|
|
1168
|
-
### 14. Type Reference
|
|
1169
|
-
|
|
1170
|
-
```typescript
|
|
1171
|
-
// Color type used by all channel color APIs
|
|
1172
|
-
interface RGBAColor {
|
|
1173
|
-
r: number; // 0-255
|
|
1174
|
-
g: number; // 0-255
|
|
1175
|
-
b: number; // 0-255
|
|
1176
|
-
a: number; // 0-255 (255 = fully opaque)
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
// Batch color map for setChannelColors()
|
|
1180
|
-
type ChannelColorMap = Record<number, RGBAColor>; // key = channel number 1-8
|
|
1181
|
-
|
|
1182
|
-
// draw() options
|
|
1183
|
-
interface IDrawOpts {
|
|
1184
|
-
getMaskData?: (
|
|
1185
|
-
sliceData: Uint8Array,
|
|
1186
|
-
layerId: string,
|
|
1187
|
-
channelId: number,
|
|
1188
|
-
sliceIndex: number,
|
|
1189
|
-
axis: 'x' | 'y' | 'z',
|
|
1190
|
-
width: number,
|
|
1191
|
-
height: number,
|
|
1192
|
-
clearFlag?: boolean
|
|
1193
|
-
) => void;
|
|
1194
|
-
onClearLayerVolume?: (layerId: string) => void;
|
|
1195
|
-
getSphereData?: (sphereOrigin: number[], sphereRadius: number) => void;
|
|
1196
|
-
getCalculateSpherePositionsData?: (
|
|
1197
|
-
tumourOrigin: ICommXYZ | null,
|
|
1198
|
-
skinOrigin: ICommXYZ | null,
|
|
1199
|
-
ribOrigin: ICommXYZ | null,
|
|
1200
|
-
nippleOrigin: ICommXYZ | null,
|
|
1201
|
-
axis: 'x' | 'y' | 'z'
|
|
1202
|
-
) => void;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// drag() options
|
|
1206
|
-
interface IDragOpts {
|
|
1207
|
-
showNumber?: boolean;
|
|
1208
|
-
getSliceNum?: (sliceIndex: number, contrastIndex: number) => void;
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
// Keyboard settings
|
|
1212
|
-
interface IKeyBoardSettings {
|
|
1213
|
-
draw: string;
|
|
1214
|
-
undo: string;
|
|
1215
|
-
redo: string;
|
|
1216
|
-
contrast: string[]; // always an array, e.g. ["Control", "Meta"]
|
|
1217
|
-
crosshair: string;
|
|
1218
|
-
sphere: string;
|
|
1219
|
-
mouseWheel: 'Scroll:Zoom' | 'Scroll:Slice';
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// 3D coordinate (used in sphere position callbacks)
|
|
1223
|
-
interface ICommXYZ {
|
|
1224
|
-
x: number;
|
|
1225
|
-
y: number;
|
|
1226
|
-
z: number;
|
|
1227
|
-
}
|
|
1228
|
-
```
|
|
1229
|
-
|
|
1230
|
-
#### Common type aliases
|
|
1231
|
-
|
|
1232
|
-
```typescript
|
|
1233
|
-
type LayerId = 'layer1' | 'layer2' | 'layer3' | 'layer4'; // or any string
|
|
1234
|
-
type ChannelValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
1235
|
-
```
|
|
1236
|
-
|
|
1237
|
-
---
|
|
1238
|
-
|
|
1239
|
-
### API Summary
|
|
356
|
+
### 11. API Summary
|
|
1240
357
|
|
|
1241
358
|
| Category | Method | Description |
|
|
1242
359
|
|----------|--------|-------------|
|
|
1243
360
|
| **Constructor** | `new NrrdTools(container, { layers })` | Create instance with optional layer config |
|
|
1244
361
|
| **Setup** | `drag(opts?)` | Enable slice-drag navigation |
|
|
1245
362
|
| | `draw(opts?)` | Bind annotation callbacks |
|
|
1246
|
-
| | `setupGUI(gui)` | Connect dat.GUI panel |
|
|
363
|
+
| | `setupGUI(gui)` | Connect dat.GUI / lil-gui panel |
|
|
1247
364
|
| | `enableContrastDragEvents(cb)` | Enable Ctrl+drag windowing |
|
|
1248
365
|
| | `setDisplaySliceIndexPanel(el)` | Show slice index in a panel |
|
|
1249
|
-
| | `setBaseDrawDisplayCanvasesSize(n)` | Set canvas resolution multiplier (1
|
|
1250
|
-
| **Data** | `reset()` | Reset all volumes, undo histories, canvases,
|
|
1251
|
-
| | `clearActiveLayer()` | Clear
|
|
1252
|
-
| | `clearActiveSlice()` | Clear
|
|
366
|
+
| | `setBaseDrawDisplayCanvasesSize(n)` | Set canvas resolution multiplier (1–8) |
|
|
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) |
|
|
1253
370
|
| | `setAllSlices(slices)` | Load NRRD slices, init MaskVolumes |
|
|
1254
|
-
| | `setMasksFromNIfTI(map)` | Load saved NIfTI voxel data |
|
|
371
|
+
| | `setMasksFromNIfTI(map, bar?)` | Load saved NIfTI voxel data |
|
|
1255
372
|
| **Render** | `start` | Frame callback — pass to render loop |
|
|
1256
373
|
| **Layer** | `setActiveLayer(id)` | Switch drawing target layer |
|
|
1257
374
|
| | `getActiveLayer()` | Read current layer |
|
|
@@ -1259,8 +376,9 @@ type ChannelValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
|
1259
376
|
| | `isLayerVisible(id)` | Query layer visibility |
|
|
1260
377
|
| | `getLayerVisibility()` | All layer visibility map |
|
|
1261
378
|
| | `hasLayerData(id)` | Check if layer has non-zero voxels |
|
|
1262
|
-
| **Sphere** | `setActiveSphereType(type)` | Set active sphere type
|
|
379
|
+
| **Sphere** | `setActiveSphereType(type)` | Set active sphere type, updates brush color |
|
|
1263
380
|
| | `getActiveSphereType()` | Read current sphere type |
|
|
381
|
+
| | `setCalculateDistanceSphere(x, y, slice, type)` | Programmatically place a calculator sphere |
|
|
1264
382
|
| **Channel** | `setActiveChannel(ch)` | Switch drawing target channel |
|
|
1265
383
|
| | `getActiveChannel()` | Read current channel |
|
|
1266
384
|
| | `setChannelVisible(id, ch, bool)` | Toggle channel visibility in a layer |
|
|
@@ -1273,45 +391,82 @@ type ChannelValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
|
1273
391
|
| | `getChannelHexColor(id, ch)` | Read Hex string |
|
|
1274
392
|
| | `getChannelCssColor(id, ch)` | Read CSS rgba() string |
|
|
1275
393
|
| | `resetChannelColors(id?, ch?)` | Reset to defaults |
|
|
1276
|
-
| **Tool Mode** | `setMode(mode)` | Switch tool
|
|
394
|
+
| **Tool Mode** | `setMode(mode)` | Switch tool: `"pencil"` / `"brush"` / `"eraser"` / `"sphere"` / `"calculator"` |
|
|
1277
395
|
| | `getMode()` | Read current tool mode |
|
|
1278
|
-
| | `isCalculatorActive()` | Check if calculator (distance) mode is active |
|
|
1279
396
|
| **Drawing** | `setOpacity(value)` | Set mask overlay opacity [0.1, 1] |
|
|
1280
397
|
| | `getOpacity()` | Read current opacity |
|
|
1281
|
-
| | `setBrushSize(size)` | Set brush/eraser size [5, 50]
|
|
398
|
+
| | `setBrushSize(size)` | Set brush/eraser size [5, 50] |
|
|
1282
399
|
| | `getBrushSize()` | Read current brush size |
|
|
1283
|
-
| | `
|
|
1284
|
-
| | `
|
|
1285
|
-
| **Contrast** | `setWindowHigh(value)` | Set window high (image contrast), call `finishWindowAdjustment()` after drag |
|
|
1286
|
-
| | `setWindowLow(value)` | Set window low (image center) |
|
|
400
|
+
| **Contrast** | `setWindowHigh(value)` | Set window high |
|
|
401
|
+
| | `setWindowLow(value)` | Set window low |
|
|
1287
402
|
| | `finishWindowAdjustment()` | Repaint all contrast slices after drag ends |
|
|
1288
|
-
| | `
|
|
1289
|
-
| **Actions** | `executeAction(action)` | Run named action: `"undo"`, `"redo"`, `"clearActiveSliceMask"`, `"clearActiveLayerMask"`, `"resetZoom"`, `"downloadCurrentMask"` |
|
|
403
|
+
| **Actions** | `executeAction(action)` | Run: `"undo"` / `"redo"` / `"clearActiveSliceMask"` / `"clearActiveLayerMask"` / `"resetZoom"` / `"downloadCurrentMask"` |
|
|
1290
404
|
| **Navigation** | `setSliceOrientation(axis)` | Switch viewing axis `"x"` / `"y"` / `"z"` |
|
|
1291
|
-
| | `
|
|
1292
|
-
| **History** | `undo()` | Undo last stroke |
|
|
1293
|
-
| | `redo()` | Redo last undone stroke |
|
|
405
|
+
| **History** | `undo()` / `redo()` | Undo / redo last stroke |
|
|
1294
406
|
| **Keyboard** | `setKeyboardSettings(partial)` | Remap shortcuts |
|
|
1295
407
|
| | `getKeyboardSettings()` | Read current bindings |
|
|
1296
|
-
| | `enterKeyboardConfig()` | Suppress
|
|
1297
|
-
| | `exitKeyboardConfig()` | Restore shortcuts |
|
|
408
|
+
| | `enterKeyboardConfig()` / `exitKeyboardConfig()` | Suppress / restore shortcuts |
|
|
1298
409
|
| | `setContrastShortcutEnabled(bool)` | Enable/disable contrast key |
|
|
1299
|
-
| | `isContrastShortcutEnabled()` | Query contrast key state |
|
|
1300
410
|
| **Inspect** | `getCurrentImageDimension()` | `[w, h, d]` voxel dims |
|
|
1301
411
|
| | `getVoxelSpacing()` | Physical mm spacing |
|
|
1302
412
|
| | `getSpaceOrigin()` | World-space origin |
|
|
1303
413
|
| | `getMaxSliceNum()` | Max slice index per axis |
|
|
1304
414
|
| | `getCurrentSlicesNumAndContrastNum()` | Current slice & contrast index |
|
|
1305
415
|
| | `getMaskData()` | Raw `IMaskData` object |
|
|
1306
|
-
| | `getNrrdToolsSettings()` | Full `NrrdState` snapshot
|
|
416
|
+
| | `getNrrdToolsSettings()` | Full `NrrdState` snapshot |
|
|
1307
417
|
| | `getContainer()` | Host `HTMLElement` |
|
|
1308
418
|
| | `getDrawingCanvas()` | Top-layer `HTMLCanvasElement` |
|
|
1309
419
|
|
|
420
|
+
---
|
|
1310
421
|
|
|
1311
|
-
|
|
422
|
+
### 12. Type Reference
|
|
423
|
+
|
|
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
|
+
}
|
|
1312
443
|
|
|
1313
|
-
|
|
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
|
|
1314
468
|
|
|
469
|
+
Special thanks to [Duke University dataset](https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=70226903) for providing the MRI data.
|
|
1315
470
|
|
|
1316
471
|
---
|
|
1317
472
|
|
|
@@ -1324,4 +479,4 @@ Special thanks to [Duke University dataset](https://wiki.cancerimagingarchive.ne
|
|
|
1324
479
|
[nrrd_example]: https://img.shields.io/badge/Nrrd__Segmentation__tool-example-orange
|
|
1325
480
|
[nrrd_example-url]: https://abi-web-apps.github.io/NRRD_Segmentation_Tool/
|
|
1326
481
|
[heart_example-url]: https://uoa-heart-mechanics-research.github.io/medtech-heart/model-heart
|
|
1327
|
-
[heart_example]: https://img.shields.io/badge/Medtech%20Heart-example-brightgreen
|
|
482
|
+
[heart_example]: https://img.shields.io/badge/Medtech%20Heart-example-brightgreen
|