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.
Files changed (111) hide show
  1. package/README.md +1056 -5
  2. package/dist/Scene/copperScene.d.ts +3 -0
  3. package/dist/Scene/copperScene.js +13 -0
  4. package/dist/Scene/copperScene.js.map +1 -1
  5. package/dist/Utils/segmentation/CommToolsData.d.ts +109 -6
  6. package/dist/Utils/segmentation/CommToolsData.js +314 -50
  7. package/dist/Utils/segmentation/CommToolsData.js.map +1 -1
  8. package/dist/Utils/segmentation/DragOperator.d.ts +13 -7
  9. package/dist/Utils/segmentation/DragOperator.js +51 -165
  10. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  11. package/dist/Utils/segmentation/DrawToolCore.d.ts +57 -76
  12. package/dist/Utils/segmentation/DrawToolCore.js +368 -878
  13. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  14. package/dist/Utils/segmentation/NrrdTools.d.ts +277 -15
  15. package/dist/Utils/segmentation/NrrdTools.js +594 -232
  16. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  17. package/dist/Utils/segmentation/core/MaskVolume.d.ts +477 -0
  18. package/dist/Utils/segmentation/core/MaskVolume.js +1238 -0
  19. package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -0
  20. package/dist/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
  21. package/dist/Utils/segmentation/core/MigrationUtils.js +163 -0
  22. package/dist/Utils/segmentation/core/MigrationUtils.js.map +1 -0
  23. package/dist/Utils/segmentation/core/UndoManager.d.ts +47 -0
  24. package/dist/Utils/segmentation/core/UndoManager.js +86 -0
  25. package/dist/Utils/segmentation/core/UndoManager.js.map +1 -0
  26. package/dist/Utils/segmentation/core/index.d.ts +76 -0
  27. package/dist/Utils/segmentation/core/index.js +75 -0
  28. package/dist/Utils/segmentation/core/index.js.map +1 -0
  29. package/dist/Utils/segmentation/core/types.d.ts +106 -0
  30. package/dist/Utils/segmentation/core/types.js +98 -0
  31. package/dist/Utils/segmentation/core/types.js.map +1 -0
  32. package/dist/Utils/segmentation/coreTools/coreType.d.ts +55 -36
  33. package/dist/Utils/segmentation/coreTools/coreType.js.map +1 -1
  34. package/dist/Utils/segmentation/coreTools/gui.d.ts +6 -16
  35. package/dist/Utils/segmentation/coreTools/gui.js +23 -27
  36. package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
  37. package/dist/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
  38. package/dist/Utils/segmentation/eventRouter/EventRouter.js +369 -0
  39. package/dist/Utils/segmentation/eventRouter/EventRouter.js.map +1 -0
  40. package/dist/Utils/segmentation/eventRouter/index.d.ts +7 -0
  41. package/dist/Utils/segmentation/eventRouter/index.js +7 -0
  42. package/dist/Utils/segmentation/eventRouter/index.js.map +1 -0
  43. package/dist/Utils/segmentation/eventRouter/types.d.ts +70 -0
  44. package/dist/Utils/segmentation/eventRouter/types.js +7 -0
  45. package/dist/Utils/segmentation/eventRouter/types.js.map +1 -0
  46. package/dist/Utils/segmentation/tools/BaseTool.d.ts +25 -0
  47. package/dist/Utils/segmentation/tools/BaseTool.js +18 -0
  48. package/dist/Utils/segmentation/tools/BaseTool.js.map +1 -0
  49. package/dist/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
  50. package/dist/Utils/segmentation/tools/ContrastTool.js +118 -0
  51. package/dist/Utils/segmentation/tools/ContrastTool.js.map +1 -0
  52. package/dist/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
  53. package/dist/Utils/segmentation/tools/CrosshairTool.js +157 -0
  54. package/dist/Utils/segmentation/tools/CrosshairTool.js.map +1 -0
  55. package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
  56. package/dist/Utils/segmentation/tools/DragSliceTool.js +170 -0
  57. package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -0
  58. package/dist/Utils/segmentation/tools/EraserTool.d.ts +16 -0
  59. package/dist/Utils/segmentation/tools/EraserTool.js +92 -0
  60. package/dist/Utils/segmentation/tools/EraserTool.js.map +1 -0
  61. package/dist/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
  62. package/dist/Utils/segmentation/tools/ImageStoreHelper.js +174 -0
  63. package/dist/Utils/segmentation/tools/ImageStoreHelper.js.map +1 -0
  64. package/dist/Utils/segmentation/tools/SphereTool.d.ts +38 -0
  65. package/dist/Utils/segmentation/tools/SphereTool.js +193 -0
  66. package/dist/Utils/segmentation/tools/SphereTool.js.map +1 -0
  67. package/dist/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
  68. package/dist/Utils/segmentation/tools/ZoomTool.js +59 -0
  69. package/dist/Utils/segmentation/tools/ZoomTool.js.map +1 -0
  70. package/dist/Utils/segmentation/tools/index.d.ts +22 -0
  71. package/dist/Utils/segmentation/tools/index.js +23 -0
  72. package/dist/Utils/segmentation/tools/index.js.map +1 -0
  73. package/dist/bundle.esm.js +4740 -1979
  74. package/dist/bundle.umd.js +4743 -1978
  75. package/dist/index.d.ts +5 -3
  76. package/dist/index.js +4 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/types/Renderer/copperRenderer.d.ts +1 -1
  79. package/dist/types/Renderer/copperRendererOnDemond.d.ts +1 -1
  80. package/dist/types/Scene/copperScene.d.ts +3 -0
  81. package/dist/types/Utils/segmentation/CommToolsData.d.ts +109 -6
  82. package/dist/types/Utils/segmentation/DragOperator.d.ts +13 -7
  83. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +57 -76
  84. package/dist/types/Utils/segmentation/NrrdTools.d.ts +277 -15
  85. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +477 -0
  86. package/dist/types/Utils/segmentation/core/MigrationUtils.d.ts +97 -0
  87. package/dist/types/Utils/segmentation/core/UndoManager.d.ts +47 -0
  88. package/dist/types/Utils/segmentation/core/index.d.ts +76 -0
  89. package/dist/types/Utils/segmentation/core/types.d.ts +106 -0
  90. package/dist/types/Utils/segmentation/coreTools/coreType.d.ts +55 -36
  91. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +6 -16
  92. package/dist/types/Utils/segmentation/eventRouter/EventRouter.d.ts +144 -0
  93. package/dist/types/Utils/segmentation/eventRouter/index.d.ts +7 -0
  94. package/dist/types/Utils/segmentation/eventRouter/types.d.ts +70 -0
  95. package/dist/types/Utils/segmentation/tools/BaseTool.d.ts +25 -0
  96. package/dist/types/Utils/segmentation/tools/ContrastTool.d.ts +26 -0
  97. package/dist/types/Utils/segmentation/tools/CrosshairTool.d.ts +30 -0
  98. package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +44 -0
  99. package/dist/types/Utils/segmentation/tools/EraserTool.d.ts +16 -0
  100. package/dist/types/Utils/segmentation/tools/ImageStoreHelper.d.ts +65 -0
  101. package/dist/types/Utils/segmentation/tools/SphereTool.d.ts +38 -0
  102. package/dist/types/Utils/segmentation/tools/ZoomTool.d.ts +20 -0
  103. package/dist/types/Utils/segmentation/tools/index.d.ts +22 -0
  104. package/dist/types/index.d.ts +5 -3
  105. package/dist/types/types/types.d.ts +3 -3
  106. package/dist/types/types.d.ts +3 -3
  107. package/package.json +1 -1
  108. package/dist/Utils/segmentation/coreTools/archive.d.ts +0 -1
  109. package/dist/Utils/segmentation/coreTools/archive.js +0 -23
  110. package/dist/Utils/segmentation/coreTools/archive.js.map +0 -1
  111. 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