copper3d 2.2.3 → 3.0.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 +1056 -5
- package/dist/Scene/copperScene.d.ts +3 -0
- package/dist/Scene/copperScene.js +13 -0
- package/dist/Scene/copperScene.js.map +1 -1
- package/dist/Utils/segmentation/CommToolsData.d.ts +109 -6
- package/dist/Utils/segmentation/CommToolsData.js +314 -50
- package/dist/Utils/segmentation/CommToolsData.js.map +1 -1
- package/dist/Utils/segmentation/DragOperator.d.ts +13 -7
- package/dist/Utils/segmentation/DragOperator.js +51 -165
- package/dist/Utils/segmentation/DragOperator.js.map +1 -1
- package/dist/Utils/segmentation/DrawToolCore.d.ts +57 -76
- package/dist/Utils/segmentation/DrawToolCore.js +368 -878
- package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
- package/dist/Utils/segmentation/NrrdTools.d.ts +277 -15
- package/dist/Utils/segmentation/NrrdTools.js +594 -232
- package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
- package/dist/Utils/segmentation/core/MaskVolume.d.ts +477 -0
- package/dist/Utils/segmentation/core/MaskVolume.js +1238 -0
- package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -0
- package/dist/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
- package/dist/Utils/segmentation/core/MigrationUtils.js +163 -0
- package/dist/Utils/segmentation/core/MigrationUtils.js.map +1 -0
- package/dist/Utils/segmentation/core/UndoManager.d.ts +47 -0
- package/dist/Utils/segmentation/core/UndoManager.js +86 -0
- package/dist/Utils/segmentation/core/UndoManager.js.map +1 -0
- package/dist/Utils/segmentation/core/index.d.ts +76 -0
- package/dist/Utils/segmentation/core/index.js +75 -0
- package/dist/Utils/segmentation/core/index.js.map +1 -0
- package/dist/Utils/segmentation/core/types.d.ts +106 -0
- package/dist/Utils/segmentation/core/types.js +98 -0
- package/dist/Utils/segmentation/core/types.js.map +1 -0
- package/dist/Utils/segmentation/coreTools/coreType.d.ts +55 -36
- package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
- package/dist/Utils/segmentation/coreTools/gui.d.ts +6 -16
- package/dist/Utils/segmentation/coreTools/gui.js +23 -27
- package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
- package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
- package/dist/Utils/segmentation/eventRouter/EventRouter.js +369 -0
- package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -0
- package/dist/Utils/segmentation/eventRouter/index.d.ts +7 -0
- package/dist/Utils/segmentation/eventRouter/index.js +7 -0
- package/dist/Utils/segmentation/eventRouter/index.js.map +1 -0
- package/dist/Utils/segmentation/eventRouter/types.d.ts +70 -0
- package/dist/Utils/segmentation/eventRouter/types.js +7 -0
- package/dist/Utils/segmentation/eventRouter/types.js.map +1 -0
- package/dist/Utils/segmentation/tools/BaseTool.d.ts +25 -0
- package/dist/Utils/segmentation/tools/BaseTool.js +18 -0
- package/dist/Utils/segmentation/tools/BaseTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
- package/dist/Utils/segmentation/tools/ContrastTool.js +118 -0
- package/dist/Utils/segmentation/tools/ContrastTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
- package/dist/Utils/segmentation/tools/CrosshairTool.js +157 -0
- package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
- package/dist/Utils/segmentation/tools/DragSliceTool.js +170 -0
- package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/EraserTool.d.ts +16 -0
- package/dist/Utils/segmentation/tools/EraserTool.js +92 -0
- package/dist/Utils/segmentation/tools/EraserTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
- package/dist/Utils/segmentation/tools/ImageStoreHelper.js +174 -0
- package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -0
- package/dist/Utils/segmentation/tools/SphereTool.d.ts +38 -0
- package/dist/Utils/segmentation/tools/SphereTool.js +193 -0
- package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
- package/dist/Utils/segmentation/tools/ZoomTool.js +59 -0
- package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -0
- package/dist/Utils/segmentation/tools/index.d.ts +22 -0
- package/dist/Utils/segmentation/tools/index.js +23 -0
- package/dist/Utils/segmentation/tools/index.js.map +1 -0
- package/dist/bundle.esm.js +4740 -1979
- package/dist/bundle.umd.js +4743 -1978
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/types/Renderer/copperRenderer.d.ts +1 -1
- package/dist/types/Renderer/copperRendererOnDemond.d.ts +1 -1
- package/dist/types/Scene/copperScene.d.ts +3 -0
- package/dist/types/Utils/segmentation/CommToolsData.d.ts +109 -6
- package/dist/types/Utils/segmentation/DragOperator.d.ts +13 -7
- package/dist/types/Utils/segmentation/DrawToolCore.d.ts +57 -76
- package/dist/types/Utils/segmentation/NrrdTools.d.ts +277 -15
- package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +477 -0
- package/dist/types/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
- package/dist/types/Utils/segmentation/core/UndoManager.d.ts +47 -0
- package/dist/types/Utils/segmentation/core/index.d.ts +76 -0
- package/dist/types/Utils/segmentation/core/types.d.ts +106 -0
- package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +55 -36
- package/dist/types/Utils/segmentation/coreTools/gui.d.ts +6 -16
- package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
- package/dist/types/Utils/segmentation/eventRouter/index.d.ts +7 -0
- package/dist/types/Utils/segmentation/eventRouter/types.d.ts +70 -0
- package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +25 -0
- package/dist/types/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
- package/dist/types/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
- package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
- package/dist/types/Utils/segmentation/tools/EraserTool.d.ts +16 -0
- package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
- package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +38 -0
- package/dist/types/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
- package/dist/types/Utils/segmentation/tools/index.d.ts +22 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/types/types.d.ts +3 -3
- package/dist/types/types.d.ts +3 -3
- package/package.json +1 -1
- package/dist/Utils/segmentation/coreTools/archive.d.ts +0 -1
- package/dist/Utils/segmentation/coreTools/archive.js +0 -23
- package/dist/Utils/segmentation/coreTools/archive.js.map +0 -1
- package/dist/types/Utils/segmentation/coreTools/archive.d.ts +0 -1
package/README.md
CHANGED
|
@@ -91,6 +91,1061 @@ function loadModel(url, name) {
|
|
|
91
91
|
}
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
## NrrdTools Usage Guide
|
|
95
|
+
|
|
96
|
+
> Copper3D `NrrdTools` — Medical Image Segmentation Annotation Engine
|
|
97
|
+
|
|
98
|
+
`NrrdTools` is the core annotation engine of Copper3D. It manages multi-layer mask volumes, a layered canvas pipeline, drawing tools, undo/redo history, channel color customization, and keyboard shortcuts — all on top of a Three.js medical image viewer.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### Table of Contents
|
|
103
|
+
|
|
104
|
+
1. [Quick Start](#1-quick-start)
|
|
105
|
+
2. [Constructor & Initialization](#2-constructor--initialization)
|
|
106
|
+
3. [Data Loading](#3-data-loading)
|
|
107
|
+
4. [Render Loop Integration](#4-render-loop-integration)
|
|
108
|
+
5. [Drawing Setup](#5-drawing-setup)
|
|
109
|
+
6. [Layer & Channel Management](#6-layer--channel-management)
|
|
110
|
+
7. [Channel Color Customization](#7-channel-color-customization)
|
|
111
|
+
8. [Undo / Redo](#8-undo--redo)
|
|
112
|
+
9. [Keyboard Shortcuts](#9-keyboard-shortcuts)
|
|
113
|
+
10. [Display & Canvas Control](#10-display--canvas-control)
|
|
114
|
+
11. [Reading State & Diagnostics](#11-reading-state--diagnostics)
|
|
115
|
+
12. [Advanced Scenarios](#12-advanced-scenarios)
|
|
116
|
+
13. [Vue 3 Integration Pattern](#13-vue-3-integration-pattern)
|
|
117
|
+
14. [Type Reference](#14-type-reference)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### 1. Quick Start
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import * as Copper from 'copper3d';
|
|
125
|
+
|
|
126
|
+
// 1. Mount the tool on a container div
|
|
127
|
+
const container = document.getElementById('viewer') as HTMLDivElement;
|
|
128
|
+
const nrrdTools = new Copper.NrrdTools(container);
|
|
129
|
+
|
|
130
|
+
// 2. After NRRD images are loaded via Copper scene:
|
|
131
|
+
nrrdTools.clear();
|
|
132
|
+
nrrdTools.setAllSlices(allSlices); // allSlices from Copper scene loader
|
|
133
|
+
|
|
134
|
+
// 3. Hook drawing callbacks
|
|
135
|
+
nrrdTools.drag({ getSliceNum: (index, contrastIndex) => {
|
|
136
|
+
console.log('Slice changed to:', index);
|
|
137
|
+
}});
|
|
138
|
+
|
|
139
|
+
nrrdTools.draw({
|
|
140
|
+
getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, width, height, clearFlag) => {
|
|
141
|
+
// Called after every stroke, undo, redo — sync to backend here
|
|
142
|
+
console.log(`Layer ${layerId}, channel ${channelId}, slice ${sliceIndex} on ${axis}-axis`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 4. Register with the Copper render loop
|
|
147
|
+
scene.addPreRenderCallbackFunction(nrrdTools.start);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### 2. Constructor & Initialization
|
|
153
|
+
|
|
154
|
+
#### Signature
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
new Copper.NrrdTools(container: HTMLDivElement, options?: { layers?: string[] })
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Parameter | Type | Default | Description |
|
|
161
|
+
|-----------|------|---------|-------------|
|
|
162
|
+
| `container` | `HTMLDivElement` | required | The DOM element that will host all annotation canvases |
|
|
163
|
+
| `options.layers` | `string[]` | `["layer1","layer2","layer3"]` | Named layers to create. Each layer gets its own `MaskVolume` and canvas |
|
|
164
|
+
|
|
165
|
+
#### Example: Single-layer (minimal)
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const nrrdTools = new Copper.NrrdTools(document.getElementById('viewer') as HTMLDivElement);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Example: Custom layer set
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// For a 4-layer segmentation workflow:
|
|
175
|
+
// layer1 = Tumour, layer2 = Edema, layer3 = Necrosis, layer4 = Enhancement
|
|
176
|
+
const nrrdTools = new Copper.NrrdTools(container, {
|
|
177
|
+
layers: ['layer1', 'layer2', 'layer3', 'layer4']
|
|
178
|
+
});
|
|
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
|
+
|
|
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);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### 3. Data Loading
|
|
206
|
+
|
|
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
|
+
```typescript
|
|
213
|
+
// Reset state from previous case
|
|
214
|
+
nrrdTools.clear();
|
|
215
|
+
|
|
216
|
+
// allSlices is the array of decoded NRRD slice objects returned by Copper's loader
|
|
217
|
+
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
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// layerVoxels maps each layer ID to a Uint8Array of raw NIfTI voxels
|
|
229
|
+
const layerVoxels = new Map<string, Uint8Array>([
|
|
230
|
+
['layer1', layer1Uint8Array],
|
|
231
|
+
['layer2', layer2Uint8Array],
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
nrrdTools.setMasksFromNIfTI(layerVoxels);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
With a loading progress bar:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const loadingBar = { value: 0 }; // must be a reactive object with .value
|
|
241
|
+
nrrdTools.setMasksFromNIfTI(layerVoxels, loadingBar);
|
|
242
|
+
```
|
|
243
|
+
|
|
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
|
+
---
|
|
269
|
+
|
|
270
|
+
### 4. Render Loop Integration
|
|
271
|
+
|
|
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
|
+
```typescript
|
|
277
|
+
// Register once after initialization
|
|
278
|
+
const callbackId = scene.addPreRenderCallbackFunction(nrrdTools.start);
|
|
279
|
+
|
|
280
|
+
// Unregister on teardown (e.g., Vue component unmount)
|
|
281
|
+
scene.removePreRenderCallbackFunction(callbackId);
|
|
282
|
+
```
|
|
283
|
+
|
|
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
|
+
---
|
|
296
|
+
|
|
297
|
+
### 5. Drawing Setup
|
|
298
|
+
|
|
299
|
+
#### 5.1 `drag()` — Slice navigation
|
|
300
|
+
|
|
301
|
+
Enables dragging to scroll through CT/MRI slices. Call once before adding to the render loop.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
nrrdTools.drag({
|
|
305
|
+
showNumber: true, // optional: show slice number overlay
|
|
306
|
+
getSliceNum: (sliceIndex, contrastIndex) => { // optional: callback on slice change
|
|
307
|
+
console.log('Now viewing slice:', sliceIndex);
|
|
308
|
+
updateUISliceDisplay(sliceIndex);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### 5.2 `draw()` — Annotation callbacks
|
|
314
|
+
|
|
315
|
+
Hooks into the drawing events. The `getMaskData` callback is the primary integration point for syncing annotations to a backend.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
nrrdTools.draw({
|
|
319
|
+
// Called after every draw stroke, undo, redo, or clear
|
|
320
|
+
getMaskData: (
|
|
321
|
+
sliceData: Uint8Array, // Raw voxel data for this slice (label values 0-8)
|
|
322
|
+
layerId: string, // Which layer was modified, e.g. "layer1"
|
|
323
|
+
channelId: number, // Active channel (1-8)
|
|
324
|
+
sliceIndex: number, // Index along the current axis
|
|
325
|
+
axis: 'x' | 'y' | 'z', // Current viewing axis
|
|
326
|
+
width: number, // Slice width in voxels
|
|
327
|
+
height: number, // Slice height in voxels
|
|
328
|
+
clearFlag?: boolean // true if the user cleared the slice
|
|
329
|
+
) => {
|
|
330
|
+
sendSliceToBackend({ sliceData, layerId, channelId, sliceIndex, axis, width, height, clearFlag });
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
// Called when the user clears an entire layer volume
|
|
334
|
+
onClearLayerVolume: (layerId: string) => {
|
|
335
|
+
console.log(`Layer ${layerId} fully cleared`);
|
|
336
|
+
notifyBackendLayerCleared(layerId);
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
// Called when the 3D sphere annotation tool places a sphere
|
|
340
|
+
getSphereData: (sphereOrigin: number[], sphereRadius: number) => {
|
|
341
|
+
console.log('Sphere placed at', sphereOrigin, 'radius', sphereRadius);
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
// Called when the calculate-sphere-positions tool fires
|
|
345
|
+
getCalculateSpherePositionsData: (
|
|
346
|
+
tumourOrigin, skinOrigin, ribOrigin, nippleOrigin, axis
|
|
347
|
+
) => {
|
|
348
|
+
runAISegmentation({ tumourOrigin, skinOrigin, ribOrigin, nippleOrigin, axis });
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### 5.3 `enableContrastDragEvents()` — Windowing
|
|
354
|
+
|
|
355
|
+
Enable Ctrl+drag to adjust window/level (brightness/contrast):
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
nrrdTools.enableContrastDragEvents((step: number, towards: 'horizental' | 'vertical') => {
|
|
359
|
+
// step: magnitude of drag movement
|
|
360
|
+
// towards: direction of drag
|
|
361
|
+
console.log(`Contrast adjusted: ${towards} ${step}`);
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
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
|
|
386
|
+
|
|
387
|
+
Toggle layers on/off in the composite canvas:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// Hide layer2 (e.g., edema) while keeping layer1 visible
|
|
391
|
+
nrrdTools.setLayerVisible('layer2', false);
|
|
392
|
+
nrrdTools.setLayerVisible('layer1', true);
|
|
393
|
+
|
|
394
|
+
// Query visibility
|
|
395
|
+
const layer2Visible = nrrdTools.isLayerVisible('layer2'); // → false
|
|
396
|
+
|
|
397
|
+
// Get all at once
|
|
398
|
+
const visibilityMap = nrrdTools.getLayerVisibility();
|
|
399
|
+
// → { layer1: true, layer2: false, layer3: true, layer4: true }
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
#### Scenario: Eye-button toggle in UI
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
function onToggleLayerEye(layerId: string) {
|
|
406
|
+
const current = nrrdTools.isLayerVisible(layerId);
|
|
407
|
+
nrrdTools.setLayerVisible(layerId, !current);
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
#### 6.3 Channel Visibility
|
|
412
|
+
|
|
413
|
+
Each layer can independently show/hide its channels:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// Hide channel 2 in layer1 (e.g., secondary annotation color)
|
|
417
|
+
nrrdTools.setChannelVisible('layer1', 2, false);
|
|
418
|
+
|
|
419
|
+
// Show it again
|
|
420
|
+
nrrdTools.setChannelVisible('layer1', 2, true);
|
|
421
|
+
|
|
422
|
+
// Query
|
|
423
|
+
const ch2Visible = nrrdTools.isChannelVisible('layer1', 2); // → true
|
|
424
|
+
|
|
425
|
+
// Get all channel visibility for all layers
|
|
426
|
+
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
|
+
|
|
439
|
+
// Show only channel 1 in layer1
|
|
440
|
+
isolateChannel('layer1', 1);
|
|
441
|
+
|
|
442
|
+
// Restore all
|
|
443
|
+
for (let ch = 1; ch <= 8; ch++) {
|
|
444
|
+
nrrdTools.setChannelVisible('layer1', ch, true);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### 6.4 Checking if a layer has annotations
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
if (nrrdTools.hasLayerData('layer2')) {
|
|
452
|
+
console.log('layer2 has annotated voxels — saving...');
|
|
453
|
+
await saveLayer('layer2');
|
|
454
|
+
} else {
|
|
455
|
+
console.log('layer2 is empty, skipping save');
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
### 7. Channel Color Customization
|
|
462
|
+
|
|
463
|
+
Each layer has its own independent color map. Changing channel 3's color in `layer1` does **not** affect `layer2`.
|
|
464
|
+
|
|
465
|
+
#### Default Channel Colors
|
|
466
|
+
|
|
467
|
+
| Channel | Color | Hex |
|
|
468
|
+
|---------|-------|-----|
|
|
469
|
+
| 1 | Green (Tumour) | `#00ff00` |
|
|
470
|
+
| 2 | Red (Edema) | `#ff0000` |
|
|
471
|
+
| 3 | Blue (Necrosis) | `#0000ff` |
|
|
472
|
+
| 4 | Yellow (Enhancement) | `#ffff00` |
|
|
473
|
+
| 5 | Magenta (Vessel) | `#ff00ff` |
|
|
474
|
+
| 6 | Cyan (Additional) | `#00ffff` |
|
|
475
|
+
| 7 | Orange (Auxiliary) | `#ff8000` |
|
|
476
|
+
| 8 | Purple (Extended) | `#8000ff` |
|
|
477
|
+
|
|
478
|
+
#### 7.1 Set a single channel color
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// RGBAColor: { r, g, b, a } — all values 0-255
|
|
482
|
+
nrrdTools.setChannelColor('layer1', 3, { r: 255, g: 128, b: 0, a: 255 }); // → orange
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
#### 7.2 Batch-set multiple channel colors (recommended)
|
|
486
|
+
|
|
487
|
+
One `reloadMasksFromVolume()` call instead of N calls — better performance:
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
nrrdTools.setChannelColors('layer1', {
|
|
491
|
+
1: { r: 255, g: 80, b: 80, a: 255 }, // Soft red
|
|
492
|
+
2: { r: 80, g: 180, b: 255, a: 255 }, // Sky blue
|
|
493
|
+
3: { r: 255, g: 230, b: 50, a: 255 }, // Golden yellow
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### 7.3 Apply the same color to all layers
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// Paint all layers' channel 1 in the same tumour color
|
|
501
|
+
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
|
+
|
|
543
|
+
nrrdTools.setChannelColor(activeLayer, activeChannel, { r, g, b, a: 255 });
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
#### Scenario: Vue UI reactivity after color change
|
|
548
|
+
|
|
549
|
+
After calling `setChannelColor()`, the canvas re-renders automatically.
|
|
550
|
+
But Vue computed properties that show the color need a manual nudge:
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
// In your composable or component
|
|
554
|
+
const colorVersion = ref(0);
|
|
555
|
+
|
|
556
|
+
const channelCssColor = computed(() => {
|
|
557
|
+
colorVersion.value; // subscribe to version
|
|
558
|
+
return nrrdTools.value?.getChannelCssColor(activeLayer.value, activeChannel.value) ?? '#00ff00';
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
function refreshColors() {
|
|
562
|
+
colorVersion.value++; // forces recompute
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// After setting color:
|
|
566
|
+
nrrdTools.value.setChannelColor('layer1', 2, { r: 255, g: 0, b: 0, a: 255 });
|
|
567
|
+
refreshColors();
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
### 8. Undo / Redo
|
|
573
|
+
|
|
574
|
+
NrrdTools maintains a per-layer undo stack (max 50 entries). Every completed brush stroke pushes a delta snapshot.
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
// Undo the last stroke on the active layer
|
|
578
|
+
nrrdTools.undo();
|
|
579
|
+
|
|
580
|
+
// Redo the last undone stroke
|
|
581
|
+
nrrdTools.redo();
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
#### Scenario: Keyboard shortcut binding
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
window.addEventListener('keydown', (e) => {
|
|
588
|
+
if (e.ctrlKey && e.key === 'z') nrrdTools.undo();
|
|
589
|
+
if (e.ctrlKey && e.key === 'y') nrrdTools.redo();
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
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
|
+
---
|
|
597
|
+
|
|
598
|
+
### 9. Keyboard Shortcuts
|
|
599
|
+
|
|
600
|
+
#### Default bindings
|
|
601
|
+
|
|
602
|
+
| Action | Default Key |
|
|
603
|
+
|--------|-------------|
|
|
604
|
+
| Draw mode | `Shift` (hold) |
|
|
605
|
+
| Undo | `z` |
|
|
606
|
+
| Redo | `y` |
|
|
607
|
+
| Contrast adjust | `Ctrl` / `Meta` (hold) |
|
|
608
|
+
| Crosshair | `s` |
|
|
609
|
+
| Mouse wheel | Zoom |
|
|
610
|
+
|
|
611
|
+
#### Reading current settings
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
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
|
+
|
|
626
|
+
#### Customizing bindings
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
nrrdTools.setKeyboardSettings({
|
|
630
|
+
undo: 'u', // Remap undo to 'u'
|
|
631
|
+
mouseWheel: 'Scroll:Slice', // Mouse wheel scrolls slices instead of zooming
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
#### Scenario: Form input focus — suppressing shortcuts
|
|
636
|
+
|
|
637
|
+
When a user types in an input field, you don't want tool shortcuts firing:
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
inputElement.addEventListener('focus', () => {
|
|
641
|
+
nrrdTools.enterKeyboardConfig(); // Suppress all tool shortcuts
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
inputElement.addEventListener('blur', () => {
|
|
645
|
+
nrrdTools.exitKeyboardConfig(); // Restore shortcuts
|
|
646
|
+
});
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
#### Enabling / disabling contrast shortcut independently
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
// Disable only the contrast (window/level) keyboard shortcut
|
|
653
|
+
nrrdTools.setContrastShortcutEnabled(false);
|
|
654
|
+
|
|
655
|
+
// Check status
|
|
656
|
+
const isEnabled = nrrdTools.isContrastShortcutEnabled(); // → false
|
|
657
|
+
|
|
658
|
+
// Re-enable
|
|
659
|
+
nrrdTools.setContrastShortcutEnabled(true);
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
### 10. Display & Canvas Control
|
|
665
|
+
|
|
666
|
+
#### Canvas size scaling
|
|
667
|
+
|
|
668
|
+
Set the base display size multiplier (1–8). Larger values use more GPU memory but give sharper annotations:
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
nrrdTools.setBaseDrawDisplayCanvasesSize(2); // 2× base resolution
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
#### Reading image metadata
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
// Voxel dimensions [width, height, depth]
|
|
678
|
+
const dims = nrrdTools.getCurrentImageDimension();
|
|
679
|
+
// → [512, 512, 256]
|
|
680
|
+
|
|
681
|
+
// Physical spacing (mm per voxel, from NRRD header)
|
|
682
|
+
const spacing = nrrdTools.getVoxelSpacing();
|
|
683
|
+
// → [0.488, 0.488, 1.0]
|
|
684
|
+
|
|
685
|
+
// World-space origin
|
|
686
|
+
const origin = nrrdTools.getSpaceOrigin();
|
|
687
|
+
// → [-125.0, -125.0, -127.5]
|
|
688
|
+
|
|
689
|
+
// Max slice count per axis
|
|
690
|
+
const maxSlices = nrrdTools.getMaxSliceNum();
|
|
691
|
+
// → [512, 512, 256] (one per axis)
|
|
692
|
+
|
|
693
|
+
// Current viewing state
|
|
694
|
+
const { currentIndex, contrastIndex } = nrrdTools.getCurrentSlicesNumAndContrastNum();
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
#### Accessing internal canvases
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
// The topmost interactive canvas (mouse/pen events target here)
|
|
701
|
+
const drawingCanvas = nrrdTools.getDrawingCanvas();
|
|
702
|
+
|
|
703
|
+
// The container element passed to the constructor
|
|
704
|
+
const container = nrrdTools.getContainer();
|
|
705
|
+
|
|
706
|
+
// Raw NrrdStates (all internal state fields)
|
|
707
|
+
const states = nrrdTools.getNrrdToolsSettings();
|
|
708
|
+
console.log(states.layers); // ["layer1", "layer2", ...]
|
|
709
|
+
console.log(states.dimensions); // [512, 512, 256]
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
#### Accessing full mask data
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
// All MaskVolume data structures
|
|
716
|
+
const maskData = nrrdTools.getMaskData();
|
|
717
|
+
// maskData.volumes is Record<string, MaskVolume>
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
### 11. Reading State & Diagnostics
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
// Full snapshot of current state
|
|
726
|
+
const state = nrrdTools.getNrrdToolsSettings();
|
|
727
|
+
|
|
728
|
+
// Active layer / channel
|
|
729
|
+
const layer = nrrdTools.getActiveLayer();
|
|
730
|
+
const channel = nrrdTools.getActiveChannel();
|
|
731
|
+
|
|
732
|
+
// Visibility maps
|
|
733
|
+
const layerVis = nrrdTools.getLayerVisibility();
|
|
734
|
+
const channelVis = nrrdTools.getChannelVisibility();
|
|
735
|
+
|
|
736
|
+
// Check if any real annotation exists in a layer
|
|
737
|
+
const hasTumour = nrrdTools.hasLayerData('layer1');
|
|
738
|
+
|
|
739
|
+
// Get current channel color as hex
|
|
740
|
+
const hex = nrrdTools.getChannelHexColor('layer1', 1); // → "#00ff00"
|
|
741
|
+
|
|
742
|
+
// Keyboard settings
|
|
743
|
+
const keys = nrrdTools.getKeyboardSettings();
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
### 12. Advanced Scenarios
|
|
749
|
+
|
|
750
|
+
#### Scenario A: Complete multi-layer initialization from scratch
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
async function initAnnotationTool(container: HTMLDivElement, allSlices: any[]) {
|
|
754
|
+
// Create tool with 4 layers
|
|
755
|
+
const nrrdTools = new Copper.NrrdTools(container, {
|
|
756
|
+
layers: ['layer1', 'layer2', 'layer3', 'layer4']
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Attach GUI
|
|
760
|
+
nrrdTools.setupGUI(gui as any);
|
|
761
|
+
|
|
762
|
+
// Attach contrast drag
|
|
763
|
+
nrrdTools.enableContrastDragEvents((step, towards) => {
|
|
764
|
+
console.log('Windowing:', towards, step);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Load image volume
|
|
768
|
+
nrrdTools.clear();
|
|
769
|
+
nrrdTools.setAllSlices(allSlices);
|
|
770
|
+
|
|
771
|
+
// Register callbacks
|
|
772
|
+
nrrdTools.drag({ getSliceNum: (idx) => updateSliceUI(idx) });
|
|
773
|
+
|
|
774
|
+
nrrdTools.draw({
|
|
775
|
+
getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, w, h, clearFlag) => {
|
|
776
|
+
syncSliceToBackend({ sliceData, layerId, channelId, sliceIndex, axis, clearFlag });
|
|
777
|
+
},
|
|
778
|
+
onClearLayerVolume: (layerId) => {
|
|
779
|
+
notifyBackendCleared(layerId);
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// Connect to render loop
|
|
784
|
+
scene.addPreRenderCallbackFunction(nrrdTools.start);
|
|
785
|
+
|
|
786
|
+
return nrrdTools;
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
#### Scenario B: Load existing annotations and apply custom color scheme
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
async function loadAndColorCase(nrrdTools: Copper.NrrdTools, caseId: string) {
|
|
794
|
+
// 1. Load existing NIfTI masks
|
|
795
|
+
const masks = await fetchCaseMasks(caseId);
|
|
796
|
+
const layerVoxels = new Map<string, Uint8Array>();
|
|
797
|
+
for (const [layerId, data] of Object.entries(masks)) {
|
|
798
|
+
layerVoxels.set(layerId, data);
|
|
799
|
+
}
|
|
800
|
+
nrrdTools.setMasksFromNIfTI(layerVoxels);
|
|
801
|
+
|
|
802
|
+
// 2. Apply per-layer color schemes
|
|
803
|
+
nrrdTools.setChannelColors('layer1', {
|
|
804
|
+
1: { r: 0, g: 200, b: 80, a: 255 }, // Tumour core — green
|
|
805
|
+
2: { r: 255, g: 200, b: 0, a: 255 }, // Tumour ring — yellow
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
nrrdTools.setChannelColors('layer2', {
|
|
809
|
+
1: { r: 255, g: 60, b: 60, a: 200 }, // Edema — semi-transparent red
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// 3. Set initial active state
|
|
813
|
+
nrrdTools.setActiveLayer('layer1');
|
|
814
|
+
nrrdTools.setActiveChannel(1);
|
|
815
|
+
}
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
#### Scenario C: Switching cases — full reset
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
async function switchCase(nrrdTools: Copper.NrrdTools, newCaseData: ICaseData) {
|
|
822
|
+
// Reset annotation volumes
|
|
823
|
+
nrrdTools.clear();
|
|
824
|
+
|
|
825
|
+
// Load new image slices (from Copper's scene loader result)
|
|
826
|
+
nrrdTools.setAllSlices(newCaseData.slices);
|
|
827
|
+
|
|
828
|
+
// Reset colors to defaults
|
|
829
|
+
nrrdTools.resetChannelColors();
|
|
830
|
+
|
|
831
|
+
// Make all layers and channels visible
|
|
832
|
+
for (const layerId of ['layer1', 'layer2', 'layer3', 'layer4']) {
|
|
833
|
+
nrrdTools.setLayerVisible(layerId, true);
|
|
834
|
+
for (let ch = 1; ch <= 8; ch++) {
|
|
835
|
+
nrrdTools.setChannelVisible(layerId, ch, true);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Start fresh on layer1, channel1
|
|
840
|
+
nrrdTools.setActiveLayer('layer1');
|
|
841
|
+
nrrdTools.setActiveChannel(1);
|
|
842
|
+
|
|
843
|
+
// Load existing masks if available
|
|
844
|
+
if (newCaseData.hasExistingMasks) {
|
|
845
|
+
nrrdTools.setMasksFromNIfTI(newCaseData.layerVoxels);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
#### Scenario D: Save workflow with dirty-layer detection
|
|
851
|
+
|
|
852
|
+
```typescript
|
|
853
|
+
async function onSave(nrrdTools: Copper.NrrdTools, caseId: string) {
|
|
854
|
+
const layers = ['layer1', 'layer2', 'layer3', 'layer4'];
|
|
855
|
+
|
|
856
|
+
for (const layerId of layers) {
|
|
857
|
+
if (!nrrdTools.hasLayerData(layerId)) {
|
|
858
|
+
console.log(`${layerId} is empty — initializing blank NIfTI on backend`);
|
|
859
|
+
await initBlankLayerOnBackend(caseId, layerId);
|
|
860
|
+
} else {
|
|
861
|
+
console.log(`${layerId} has data — saving...`);
|
|
862
|
+
await saveLayerToBackend(caseId, layerId);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
#### Scenario E: AI segmentation result — write back to volume
|
|
869
|
+
|
|
870
|
+
After receiving a segmentation result from a backend AI model, write it directly into a layer:
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
async function applyAIResult(nrrdTools: Copper.NrrdTools, layerId: string) {
|
|
874
|
+
// Fetch AI-generated NIfTI from backend
|
|
875
|
+
const response = await fetch(`/api/ai-result/${layerId}`);
|
|
876
|
+
const buffer = await response.arrayBuffer();
|
|
877
|
+
const voxels = new Uint8Array(buffer);
|
|
878
|
+
|
|
879
|
+
// Write directly into the target layer volume
|
|
880
|
+
const layerVoxels = new Map<string, Uint8Array>([[layerId, voxels]]);
|
|
881
|
+
nrrdTools.setMasksFromNIfTI(layerVoxels);
|
|
882
|
+
|
|
883
|
+
// Optionally switch to that layer so user sees the result
|
|
884
|
+
nrrdTools.setActiveLayer(layerId);
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
#### Scenario F: Dynamic keyboard remap from user settings
|
|
889
|
+
|
|
890
|
+
```typescript
|
|
891
|
+
interface UserPreferences {
|
|
892
|
+
undoKey: string;
|
|
893
|
+
redoKey: string;
|
|
894
|
+
sliceScrollMode: 'zoom' | 'slice';
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function applyUserKeyboardPreferences(nrrdTools: Copper.NrrdTools, prefs: UserPreferences) {
|
|
898
|
+
nrrdTools.setKeyboardSettings({
|
|
899
|
+
undo: prefs.undoKey,
|
|
900
|
+
redo: prefs.redoKey,
|
|
901
|
+
mouseWheel: prefs.sliceScrollMode === 'slice' ? 'Scroll:Slice' : 'Scroll:Zoom',
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Example: Apply on preference change
|
|
906
|
+
applyUserKeyboardPreferences(nrrdTools, {
|
|
907
|
+
undoKey: 'u',
|
|
908
|
+
redoKey: 'r',
|
|
909
|
+
sliceScrollMode: 'slice',
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
#### Scenario G: Read layer colors to build a color legend
|
|
914
|
+
|
|
915
|
+
```typescript
|
|
916
|
+
function buildColorLegend(nrrdTools: Copper.NrrdTools, layerId: string) {
|
|
917
|
+
const legend = [];
|
|
918
|
+
for (let ch = 1; ch <= 8; ch++) {
|
|
919
|
+
legend.push({
|
|
920
|
+
channel: ch,
|
|
921
|
+
cssColor: nrrdTools.getChannelCssColor(layerId, ch),
|
|
922
|
+
hexColor: nrrdTools.getChannelHexColor(layerId, ch),
|
|
923
|
+
rgba: nrrdTools.getChannelColor(layerId, ch),
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
return legend;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// → [
|
|
930
|
+
// { channel: 1, cssColor: 'rgba(0,255,0,1.00)', hexColor: '#00ff00', rgba: { r:0, g:255, b:0, a:255 } },
|
|
931
|
+
// { channel: 2, cssColor: 'rgba(255,0,0,1.00)', hexColor: '#ff0000', ... },
|
|
932
|
+
// ...
|
|
933
|
+
// ]
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
### 13. Vue 3 Integration Pattern
|
|
939
|
+
|
|
940
|
+
The recommended pattern in Vue 3 is to distribute the `NrrdTools` instance via a Vue event emitter after NRRD loading completes, so any descendant component can access it.
|
|
941
|
+
|
|
942
|
+
#### LeftPanel (creator)
|
|
943
|
+
|
|
944
|
+
```vue
|
|
945
|
+
<script setup lang="ts">
|
|
946
|
+
import * as Copper from 'copper3d';
|
|
947
|
+
import emitter from '@/plugins/custom-emitter';
|
|
948
|
+
|
|
949
|
+
let nrrdTools: Copper.NrrdTools | undefined;
|
|
950
|
+
|
|
951
|
+
onMounted(() => {
|
|
952
|
+
nrrdTools = new Copper.NrrdTools(canvasContainer.value as HTMLDivElement, {
|
|
953
|
+
layers: ['layer1', 'layer2', 'layer3', 'layer4']
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// After all NRRD files are loaded by the Copper scene:
|
|
958
|
+
function onAllImagesLoaded(allSlices: any[]) {
|
|
959
|
+
nrrdTools!.clear();
|
|
960
|
+
nrrdTools!.setAllSlices(allSlices);
|
|
961
|
+
|
|
962
|
+
nrrdTools!.drag({ getSliceNum: (idx) => emit('sliceChanged', idx) });
|
|
963
|
+
|
|
964
|
+
nrrdTools!.draw({
|
|
965
|
+
getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, w, h, clearFlag) => {
|
|
966
|
+
emit('maskDataUpdated', { sliceData, layerId, channelId, sliceIndex, axis, w, h, clearFlag });
|
|
967
|
+
},
|
|
968
|
+
onClearLayerVolume: (layerId) => emit('layerCleared', layerId),
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
scene!.addPreRenderCallbackFunction(nrrdTools!.start);
|
|
972
|
+
|
|
973
|
+
// Broadcast to all other components
|
|
974
|
+
emitter.emit('Core:NrrdTools', nrrdTools!);
|
|
975
|
+
emitter.emit('Segmentation:FinishLoadAllCaseImages');
|
|
976
|
+
}
|
|
977
|
+
</script>
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
#### Annotation Control Panel (consumer)
|
|
981
|
+
|
|
982
|
+
```vue
|
|
983
|
+
<script setup lang="ts">
|
|
984
|
+
import * as Copper from 'copper3d';
|
|
985
|
+
import emitter from '@/plugins/custom-emitter';
|
|
986
|
+
|
|
987
|
+
const nrrdTools = ref<Copper.NrrdTools>();
|
|
988
|
+
|
|
989
|
+
onMounted(() => {
|
|
990
|
+
emitter.on('Core:NrrdTools', (tools: Copper.NrrdTools) => {
|
|
991
|
+
nrrdTools.value = tools;
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
emitter.on('Segmentation:FinishLoadAllCaseImages', () => {
|
|
995
|
+
// Safe to call all APIs now — MaskVolume is initialized
|
|
996
|
+
syncStateFromTools();
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
function syncStateFromTools() {
|
|
1001
|
+
if (!nrrdTools.value) return;
|
|
1002
|
+
activeLayer.value = nrrdTools.value.getActiveLayer();
|
|
1003
|
+
activeChannel.value = nrrdTools.value.getActiveChannel();
|
|
1004
|
+
layerVisibility.value = nrrdTools.value.getLayerVisibility();
|
|
1005
|
+
channelVisibility.value = nrrdTools.value.getChannelVisibility();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function onChannelColorPicked(hex: string) {
|
|
1009
|
+
if (!nrrdTools.value) return;
|
|
1010
|
+
const r = parseInt(hex.slice(1,3), 16);
|
|
1011
|
+
const g = parseInt(hex.slice(3,5), 16);
|
|
1012
|
+
const b = parseInt(hex.slice(5,7), 16);
|
|
1013
|
+
nrrdTools.value.setChannelColor(activeLayer.value, activeChannel.value, { r, g, b, a: 255 });
|
|
1014
|
+
refreshColors(); // bump colorVersion to re-trigger computed
|
|
1015
|
+
}
|
|
1016
|
+
</script>
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
---
|
|
1020
|
+
|
|
1021
|
+
### 14. Type Reference
|
|
1022
|
+
|
|
1023
|
+
```typescript
|
|
1024
|
+
// Color type used by all channel color APIs
|
|
1025
|
+
interface RGBAColor {
|
|
1026
|
+
r: number; // 0-255
|
|
1027
|
+
g: number; // 0-255
|
|
1028
|
+
b: number; // 0-255
|
|
1029
|
+
a: number; // 0-255 (255 = fully opaque)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Batch color map for setChannelColors()
|
|
1033
|
+
type ChannelColorMap = Record<number, RGBAColor>; // key = channel number 1-8
|
|
1034
|
+
|
|
1035
|
+
// draw() options
|
|
1036
|
+
interface IDrawOpts {
|
|
1037
|
+
getMaskData?: (
|
|
1038
|
+
sliceData: Uint8Array,
|
|
1039
|
+
layerId: string,
|
|
1040
|
+
channelId: number,
|
|
1041
|
+
sliceIndex: number,
|
|
1042
|
+
axis: 'x' | 'y' | 'z',
|
|
1043
|
+
width: number,
|
|
1044
|
+
height: number,
|
|
1045
|
+
clearFlag?: boolean
|
|
1046
|
+
) => void;
|
|
1047
|
+
onClearLayerVolume?: (layerId: string) => void;
|
|
1048
|
+
getSphereData?: (sphereOrigin: number[], sphereRadius: number) => void;
|
|
1049
|
+
getCalculateSpherePositionsData?: (
|
|
1050
|
+
tumourOrigin: ICommXYZ | null,
|
|
1051
|
+
skinOrigin: ICommXYZ | null,
|
|
1052
|
+
ribOrigin: ICommXYZ | null,
|
|
1053
|
+
nippleOrigin: ICommXYZ | null,
|
|
1054
|
+
axis: 'x' | 'y' | 'z'
|
|
1055
|
+
) => void;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// drag() options
|
|
1059
|
+
interface IDragOpts {
|
|
1060
|
+
showNumber?: boolean;
|
|
1061
|
+
getSliceNum?: (sliceIndex: number, contrastIndex: number) => void;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Keyboard settings
|
|
1065
|
+
interface IKeyBoardSettings {
|
|
1066
|
+
draw: string;
|
|
1067
|
+
undo: string;
|
|
1068
|
+
redo: string;
|
|
1069
|
+
contrast: string | string[];
|
|
1070
|
+
crosshair: string;
|
|
1071
|
+
mouseWheel: 'Scroll:Zoom' | 'Scroll:Slice';
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// 3D coordinate (used in sphere position callbacks)
|
|
1075
|
+
interface ICommXYZ {
|
|
1076
|
+
x: number;
|
|
1077
|
+
y: number;
|
|
1078
|
+
z: number;
|
|
1079
|
+
}
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
#### Common type aliases
|
|
1083
|
+
|
|
1084
|
+
```typescript
|
|
1085
|
+
type LayerId = 'layer1' | 'layer2' | 'layer3' | 'layer4'; // or any string
|
|
1086
|
+
type ChannelValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
---
|
|
1090
|
+
|
|
1091
|
+
### API Summary
|
|
1092
|
+
|
|
1093
|
+
| Category | Method | Description |
|
|
1094
|
+
|----------|--------|-------------|
|
|
1095
|
+
| **Constructor** | `new NrrdTools(container, { layers })` | Create instance with optional layer config |
|
|
1096
|
+
| **Setup** | `drag(opts?)` | Enable slice-drag navigation |
|
|
1097
|
+
| | `draw(opts?)` | Bind annotation callbacks |
|
|
1098
|
+
| | `setupGUI(gui)` | Connect dat.GUI panel |
|
|
1099
|
+
| | `enableContrastDragEvents(cb)` | Enable Ctrl+drag windowing |
|
|
1100
|
+
| | `setDisplaySliceIndexPanel(el)` | Show slice index in a panel |
|
|
1101
|
+
| | `setBaseDrawDisplayCanvasesSize(n)` | Set canvas resolution multiplier (1-8) |
|
|
1102
|
+
| **Data** | `clear()` | Reset all volumes |
|
|
1103
|
+
| | `setAllSlices(slices)` | Load NRRD slices, init MaskVolumes |
|
|
1104
|
+
| | `setMasksFromNIfTI(map)` | Load saved NIfTI voxel data |
|
|
1105
|
+
| **Render** | `start` | Frame callback — pass to render loop |
|
|
1106
|
+
| **Layer** | `setActiveLayer(id)` | Switch drawing target layer |
|
|
1107
|
+
| | `getActiveLayer()` | Read current layer |
|
|
1108
|
+
| | `setLayerVisible(id, bool)` | Toggle layer in composite view |
|
|
1109
|
+
| | `isLayerVisible(id)` | Query layer visibility |
|
|
1110
|
+
| | `getLayerVisibility()` | All layer visibility map |
|
|
1111
|
+
| | `hasLayerData(id)` | Check if layer has non-zero voxels |
|
|
1112
|
+
| **Channel** | `setActiveChannel(ch)` | Switch drawing target channel |
|
|
1113
|
+
| | `getActiveChannel()` | Read current channel |
|
|
1114
|
+
| | `setChannelVisible(id, ch, bool)` | Toggle channel visibility in a layer |
|
|
1115
|
+
| | `isChannelVisible(id, ch)` | Query channel visibility |
|
|
1116
|
+
| | `getChannelVisibility()` | All channel visibility map |
|
|
1117
|
+
| **Color** | `setChannelColor(id, ch, rgba)` | Set one channel color in one layer |
|
|
1118
|
+
| | `setChannelColors(id, map)` | Batch-set colors in one layer |
|
|
1119
|
+
| | `setAllLayersChannelColor(ch, rgba)` | Set one channel color across all layers |
|
|
1120
|
+
| | `getChannelColor(id, ch)` | Read RGBA |
|
|
1121
|
+
| | `getChannelHexColor(id, ch)` | Read Hex string |
|
|
1122
|
+
| | `getChannelCssColor(id, ch)` | Read CSS rgba() string |
|
|
1123
|
+
| | `resetChannelColors(id?, ch?)` | Reset to defaults |
|
|
1124
|
+
| **History** | `undo()` | Undo last stroke |
|
|
1125
|
+
| | `redo()` | Redo last undone stroke |
|
|
1126
|
+
| **Keyboard** | `setKeyboardSettings(partial)` | Remap shortcuts |
|
|
1127
|
+
| | `getKeyboardSettings()` | Read current bindings |
|
|
1128
|
+
| | `enterKeyboardConfig()` | Suppress all shortcuts |
|
|
1129
|
+
| | `exitKeyboardConfig()` | Restore shortcuts |
|
|
1130
|
+
| | `setContrastShortcutEnabled(bool)` | Enable/disable contrast key |
|
|
1131
|
+
| | `isContrastShortcutEnabled()` | Query contrast key state |
|
|
1132
|
+
| **Inspect** | `getCurrentImageDimension()` | `[w, h, d]` voxel dims |
|
|
1133
|
+
| | `getVoxelSpacing()` | Physical mm spacing |
|
|
1134
|
+
| | `getSpaceOrigin()` | World-space origin |
|
|
1135
|
+
| | `getMaxSliceNum()` | Max slice index per axis |
|
|
1136
|
+
| | `getCurrentSlicesNumAndContrastNum()` | Current slice & contrast index |
|
|
1137
|
+
| | `getMaskData()` | Raw `IMaskData` object |
|
|
1138
|
+
| | `getNrrdToolsSettings()` | Full `INrrdStates` snapshot |
|
|
1139
|
+
| | `getContainer()` | Host `HTMLElement` |
|
|
1140
|
+
| | `getDrawingCanvas()` | Top-layer `HTMLCanvasElement` |
|
|
1141
|
+
|
|
1142
|
+
---
|
|
1143
|
+
|
|
1144
|
+
## Acknowledgements
|
|
1145
|
+
|
|
1146
|
+
Special thanks to [Duke University dataset](https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=70226903) provides these awsome MRI data!!!
|
|
1147
|
+
|
|
1148
|
+
|
|
94
1149
|
---
|
|
95
1150
|
|
|
96
1151
|
[npm]: https://img.shields.io/npm/v/copper3d
|
|
@@ -102,8 +1157,4 @@ function loadModel(url, name) {
|
|
|
102
1157
|
[nrrd_example]: https://img.shields.io/badge/Nrrd__Segmentation__tool-example-orange
|
|
103
1158
|
[nrrd_example-url]: https://abi-web-apps.github.io/NRRD_Segmentation_Tool/
|
|
104
1159
|
[heart_example-url]: https://uoa-heart-mechanics-research.github.io/medtech-heart/model-heart
|
|
105
|
-
[heart_example]: https://img.shields.io/badge/Medtech%20Heart-example-brightgreen
|
|
106
|
-
|
|
107
|
-
#### Acknowledgements
|
|
108
|
-
|
|
109
|
-
Special thanks to [Duke University dataset](https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=70226903) provides these awsome MRI data!!!
|
|
1160
|
+
[heart_example]: https://img.shields.io/badge/Medtech%20Heart-example-brightgreen
|