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.
- package/README.md +197 -875
- package/dist/Utils/segmentation/CanvasState.d.ts +31 -0
- package/dist/Utils/segmentation/CanvasState.js +167 -0
- package/dist/Utils/segmentation/CanvasState.js.map +1 -0
- package/dist/Utils/segmentation/DragOperator.d.ts +10 -2
- package/dist/Utils/segmentation/DragOperator.js +16 -8
- package/dist/Utils/segmentation/DragOperator.js.map +1 -1
- package/dist/Utils/segmentation/DrawToolCore.d.ts +64 -35
- package/dist/Utils/segmentation/DrawToolCore.js +332 -584
- package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
- package/dist/Utils/segmentation/NrrdTools.d.ts +117 -353
- package/dist/Utils/segmentation/NrrdTools.js +622 -1081
- package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
- package/dist/Utils/segmentation/RenderingUtils.d.ts +73 -0
- package/dist/Utils/segmentation/RenderingUtils.js +180 -0
- package/dist/Utils/segmentation/RenderingUtils.js.map +1 -0
- package/dist/Utils/segmentation/core/MaskVolume.js +23 -2
- package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -1
- package/dist/Utils/segmentation/core/UndoManager.d.ts +1 -1
- package/dist/Utils/segmentation/core/UndoManager.js +1 -1
- package/dist/Utils/segmentation/core/index.d.ts +1 -3
- package/dist/Utils/segmentation/core/index.js +0 -1
- package/dist/Utils/segmentation/core/index.js.map +1 -1
- package/dist/Utils/segmentation/core/types.d.ts +384 -0
- package/dist/Utils/segmentation/core/types.js +25 -24
- package/dist/Utils/segmentation/core/types.js.map +1 -1
- package/dist/Utils/segmentation/coreTools/GuiState.d.ts +22 -0
- package/dist/Utils/segmentation/coreTools/GuiState.js +48 -0
- package/dist/Utils/segmentation/coreTools/GuiState.js.map +1 -0
- package/dist/Utils/segmentation/coreTools/NrrdState.d.ts +25 -0
- package/dist/Utils/segmentation/coreTools/NrrdState.js +85 -0
- package/dist/Utils/segmentation/coreTools/NrrdState.js.map +1 -0
- package/dist/Utils/segmentation/coreTools/divControlTools.d.ts +1 -1
- package/dist/Utils/segmentation/coreTools/divControlTools.js +1 -1
- package/dist/Utils/segmentation/coreTools/divControlTools.js.map +1 -1
- package/dist/Utils/segmentation/coreTools/gui.d.ts +12 -12
- package/dist/Utils/segmentation/coreTools/gui.js +81 -107
- package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
- package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +2 -1
- package/dist/Utils/segmentation/eventRouter/EventRouter.js +15 -10
- package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -1
- package/dist/Utils/segmentation/eventRouter/types.d.ts +3 -3
- package/dist/Utils/segmentation/tools/BaseTool.d.ts +8 -5
- package/dist/Utils/segmentation/tools/BaseTool.js +1 -2
- package/dist/Utils/segmentation/tools/BaseTool.js.map +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 +50 -25
- 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 -10
- package/dist/Utils/segmentation/tools/DragSliceTool.js +63 -53
- package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/DrawingTool.d.ts +85 -0
- package/dist/Utils/segmentation/tools/DrawingTool.js +270 -0
- package/dist/Utils/segmentation/tools/DrawingTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/EraserTool.js +8 -8
- package/dist/Utils/segmentation/tools/EraserTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +13 -33
- package/dist/Utils/segmentation/tools/ImageStoreHelper.js +39 -82
- package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -1
- package/dist/Utils/segmentation/tools/LayerChannelManager.d.ts +52 -0
- package/dist/Utils/segmentation/tools/LayerChannelManager.js +175 -0
- package/dist/Utils/segmentation/tools/LayerChannelManager.js.map +1 -0
- package/dist/Utils/segmentation/tools/PanTool.d.ts +47 -0
- package/dist/Utils/segmentation/tools/PanTool.js +90 -0
- package/dist/Utils/segmentation/tools/PanTool.js.map +1 -0
- 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 +223 -10
- package/dist/Utils/segmentation/tools/SphereTool.js +512 -47
- 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 +3 -3
- package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/index.d.ts +8 -6
- package/dist/Utils/segmentation/tools/index.js +10 -1
- package/dist/Utils/segmentation/tools/index.js.map +1 -1
- package/dist/Utils/workers/reformatSaveDataWorker.d.ts +1 -1
- package/dist/bundle.esm.js +3729 -3017
- package/dist/bundle.umd.js +3729 -3017
- package/dist/index.d.ts +6 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/Utils/segmentation/CanvasState.d.ts +31 -0
- package/dist/types/Utils/segmentation/DragOperator.d.ts +10 -2
- package/dist/types/Utils/segmentation/DrawToolCore.d.ts +64 -35
- package/dist/types/Utils/segmentation/NrrdTools.d.ts +117 -353
- package/dist/types/Utils/segmentation/RenderingUtils.d.ts +73 -0
- package/dist/types/Utils/segmentation/core/UndoManager.d.ts +1 -1
- package/dist/types/Utils/segmentation/core/index.d.ts +1 -3
- package/dist/types/Utils/segmentation/core/types.d.ts +384 -0
- package/dist/types/Utils/segmentation/coreTools/GuiState.d.ts +22 -0
- package/dist/types/Utils/segmentation/coreTools/NrrdState.d.ts +25 -0
- package/dist/types/Utils/segmentation/coreTools/divControlTools.d.ts +1 -1
- package/dist/types/Utils/segmentation/coreTools/gui.d.ts +12 -12
- package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +2 -1
- package/dist/types/Utils/segmentation/eventRouter/types.d.ts +3 -3
- package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +8 -5
- 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 -10
- package/dist/types/Utils/segmentation/tools/DrawingTool.d.ts +85 -0
- package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +13 -33
- package/dist/types/Utils/segmentation/tools/LayerChannelManager.d.ts +52 -0
- package/dist/types/Utils/segmentation/tools/PanTool.d.ts +47 -0
- package/dist/types/Utils/segmentation/tools/SliceRenderPipeline.d.ts +80 -0
- package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +223 -10
- 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 +8 -6
- package/dist/types/Utils/workers/reformatSaveDataWorker.d.ts +1 -1
- package/dist/types/index.d.ts +6 -3
- package/package.json +1 -1
- package/dist/Utils/segmentation/CommToolsData.d.ts +0 -177
- package/dist/Utils/segmentation/CommToolsData.js +0 -548
- package/dist/Utils/segmentation/CommToolsData.js.map +0 -1
- package/dist/Utils/segmentation/core/MigrationUtils.d.ts +0 -97
- package/dist/Utils/segmentation/core/MigrationUtils.js +0 -163
- package/dist/Utils/segmentation/core/MigrationUtils.js.map +0 -1
- package/dist/Utils/segmentation/coreTools/coreType.d.ts +0 -363
- 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 -177
- package/dist/types/Utils/segmentation/core/MigrationUtils.d.ts +0 -97
- 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
|
|
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`
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
131
|
-
nrrdTools.
|
|
132
|
-
nrrdTools.setAllSlices(allSlices); // allSlices from Copper scene loader
|
|
107
|
+
nrrdTools.reset();
|
|
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,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 |
|
|
163
|
-
| `options.layers` | `string[]` | `["layer1","layer2","layer3"]` | Named layers to create
|
|
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
|
-
//
|
|
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
|
-
|
|
140
|
+
// Optional: show current slice index in a panel
|
|
141
|
+
nrrdTools.setDisplaySliceIndexPanel(document.getElementById('slice-panel') as HTMLDivElement);
|
|
196
142
|
|
|
197
|
-
|
|
198
|
-
import GUI from 'lil-gui';
|
|
199
|
-
|
|
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
|
|
214
|
-
nrrdTools.
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
const loadingBar = { value: 0 };
|
|
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
|
|
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
|
-
####
|
|
354
|
-
|
|
355
|
-
Enable Ctrl+drag to adjust window/level (brightness/contrast):
|
|
209
|
+
#### `enableContrastDragEvents()` — Window/Level
|
|
356
210
|
|
|
357
211
|
```typescript
|
|
358
|
-
nrrdTools.enableContrastDragEvents((step
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
391
|
-
nrrdTools.
|
|
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
|
-
|
|
231
|
+
**Programmatic sphere placement (backend → frontend):**
|
|
403
232
|
|
|
404
233
|
```typescript
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
239
|
+
---
|
|
412
240
|
|
|
413
|
-
|
|
241
|
+
### 6. Layer & Channel Management
|
|
414
242
|
|
|
415
243
|
```typescript
|
|
416
|
-
//
|
|
417
|
-
nrrdTools.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
nrrdTools.
|
|
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
|
-
//
|
|
423
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
440
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
#### Default Channel Colors
|
|
267
|
+
**Default colors:**
|
|
466
268
|
|
|
467
269
|
| Channel | Color | Hex |
|
|
468
270
|
|---------|-------|-----|
|
|
469
|
-
| 1 |
|
|
470
|
-
| 2 |
|
|
471
|
-
| 3 | Blue
|
|
472
|
-
| 4 |
|
|
473
|
-
| 5 |
|
|
474
|
-
| 6 | Cyan
|
|
475
|
-
| 7 | Orange
|
|
476
|
-
| 8 |
|
|
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 } —
|
|
482
|
-
nrrdTools.setChannelColor('layer1', 3, { r: 255, g: 128, b: 0, a: 255 });
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
```typescript
|
|
284
|
+
// Batch-set (one reloadMasksFromVolume call — better performance)
|
|
490
285
|
nrrdTools.setChannelColors('layer1', {
|
|
491
|
-
1: { r: 255, g:
|
|
492
|
-
2: { r:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1102
|
-
| **Data** | `
|
|
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) |
|
|
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
|
-
| **
|
|
1125
|
-
| | `
|
|
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
|
|
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 `
|
|
416
|
+
| | `getNrrdToolsSettings()` | Full `NrrdState` snapshot |
|
|
1139
417
|
| | `getContainer()` | Host `HTMLElement` |
|
|
1140
418
|
| | `getDrawingCanvas()` | Top-layer `HTMLCanvasElement` |
|
|
1141
419
|
|
|
1142
420
|
---
|
|
1143
421
|
|
|
1144
|
-
|
|
422
|
+
### 12. Type Reference
|
|
1145
423
|
|
|
1146
|
-
|
|
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
|