copper3d
v3.1.1
Published
A 3d visualisation package base on threejs provides multiple scenes and Nrrd image load funtion.
Maintainers
Readme
copper3d
A 3D visualisation package based on three.js — multiple scenes, NRRD/DICOM image loading, and a full medical image segmentation annotation engine.
Documentation
https://copper3d-visualisation.readthedocs.io/en/latest/
Previous versions
Old: https://www.npmjs.com/package/copper3d_visualisation Very old: https://www.npmjs.com/package/gltfloader-plugin-test
Examples
Pick model with Gltfloader Copper3d_examples
Basic Usage
Load demo
import * as Copper from "copper3d";
import { getCurrentInstance, onMounted } from "vue";
let appRenderer;
onMounted(() => {
const { $refs } = (getCurrentInstance() as any).proxy;
const bg: HTMLDivElement = $refs.classfy;
appRenderer = new Copper.copperRenderer(bg);
appRenderer.getCurrentScene().createDemoMesh();
appRenderer.animate();
});Options
appRenderer = new Copper.copperRenderer(bg, { guiOpen: true });Multiple scenes with glTF
function loadModel(url: string, name: string) {
let scene = appRenderer.getSceneByName(name);
if (!scene) {
scene = appRenderer.createScene(name);
appRenderer.setCurrentScene(scene);
scene.loadViewUrl("/noInfarct_view.json");
scene.loadGltf(url);
} else {
appRenderer.setCurrentScene(scene);
}
}View data structure
CameraViewPoint {
nearPlane: number = 0.1;
farPlane: number = 2000.0;
eyePosition: Array<number> = [0.0, 0.0, 0.0];
targetPosition: Array<number> = [0.0, 0.0, 0.0];
upVector: Array<number> = [0.0, 1.0, 0.0];
}NrrdTools Usage Guide
Copper3D
NrrdTools— Medical Image Segmentation Annotation Engine
NrrdTools manages multi-layer mask volumes, a layered canvas pipeline, drawing tools, undo/redo history, channel color customization, and keyboard shortcuts on top of a Three.js medical image viewer.
Internal Architecture:
NrrdToolsis a Facade using composition (no inheritance). It composes:
CanvasState— unified state container (nrrd_states, gui_states, protectedData, callbacks, keyboardSettings)DrawToolCore— tool orchestration and event routingRenderingUtils— slice extraction and canvas compositing helpersLayerChannelManager— layer/channel/sphere-type management and color customizationSliceRenderPipeline— slice setup, canvas rendering, mask reload, canvas flipDataLoader— NRRD slice loading, NIfTI voxel loadingThe old inheritance chain (
NrrdTools → DrawToolCore → CommToolsData) has been fully replaced. All modules communicate viaToolContext(shared state). The public API below is unchanged.
1. Quick Start
import * as Copper from 'copper3d';
const container = document.getElementById('viewer') as HTMLDivElement;
const nrrdTools = new Copper.NrrdTools(container);
nrrdTools.reset();
nrrdTools.setAllSlices(allSlices); // allSlices from Copper scene loader
nrrdTools.drag({ getSliceNum: (index) => console.log('Slice:', index) });
nrrdTools.draw({
getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, width, height, clearFlag) => {
// Called after every stroke, undo, redo — sync to backend here
}
});
scene.addPreRenderCallbackFunction(nrrdTools.start);2. Constructor & Initialization
new Copper.NrrdTools(container: HTMLDivElement, options?: { layers?: string[] })| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| container | HTMLDivElement | required | Host DOM element for all annotation canvases |
| options.layers | string[] | ["layer1","layer2","layer3"] | Named layers to create |
// Custom layer set
const nrrdTools = new Copper.NrrdTools(container, {
layers: ['layer1', 'layer2', 'layer3', 'layer4']
});
// Optional: show current slice index in a panel
nrrdTools.setDisplaySliceIndexPanel(document.getElementById('slice-panel') as HTMLDivElement);
// Optional: connect dat.GUI / lil-gui
import GUI from 'lil-gui';
nrrdTools.setupGUI(new GUI() as any);3. Data Loading
// Reset state then load new slices
nrrdTools.reset();
nrrdTools.setAllSlices(allSlices);
// Load existing NIfTI mask data
const layerVoxels = new Map<string, Uint8Array>([
['layer1', layer1Uint8Array],
['layer2', layer2Uint8Array],
]);
nrrdTools.setMasksFromNIfTI(layerVoxels);
// With loading progress bar
const loadingBar = { value: 0 };
nrrdTools.setMasksFromNIfTI(layerVoxels, loadingBar);4. Render Loop Integration
// Register once after initialization
const callbackId = scene.addPreRenderCallbackFunction(nrrdTools.start);
// Unregister on teardown
scene.removePreRenderCallbackFunction(callbackId);5. Drawing Setup
drag() — Slice navigation
nrrdTools.drag({
showNumber: true,
getSliceNum: (sliceIndex, contrastIndex) => updateUI(sliceIndex),
});draw() — Annotation callbacks
nrrdTools.draw({
getMaskData: (sliceData, layerId, channelId, sliceIndex, axis, width, height, clearFlag?) => {
sendSliceToBackend({ sliceData, layerId, channelId, sliceIndex, axis, width, height, clearFlag });
},
onClearLayerVolume: (layerId) => notifyBackendLayerCleared(layerId),
getSphereData: (sphereOrigin, sphereRadius) => sendSphereToBackend({ sphereOrigin, sphereRadius }),
getCalculateSpherePositionsData: (tumour, skin, rib, nipple, axis) => {
if (tumour && skin && rib && nipple) aiBackend.runSegmentation({ tumour, skin, rib, nipple, axis });
},
});enableContrastDragEvents() — Window/Level
nrrdTools.enableContrastDragEvents((step, towards) => {
console.log(`Contrast: ${towards} ${step}`);
});SphereTool — Sphere Types & Channel Mapping
| Sphere Type | Channel | Default Color | activeSphereType value |
|-------------|---------|---------------|--------------------------|
| tumour | 1 | #10b981 | "tumour" (default) |
| nipple | 2 | #f43f5e | "nipple" |
| ribcage | 3 | #3b82f6 | "ribcage" |
| skin | 4 | #fbbf24 | "skin" |
nrrdTools.setActiveSphereType('nipple'); // also updates brush/fill color
const type = nrrdTools.getActiveSphereType(); // → 'tumour' | 'skin' | 'nipple' | 'ribcage'Programmatic sphere placement (backend → frontend):
// Replicates full click flow internally — no user interaction needed
nrrdTools.setCalculateDistanceSphere(120, 95, 42, 'tumour');
// Coordinates are in unscaled image space; sizeFactor is applied internally6. Layer & Channel Management
// Active layer / channel
nrrdTools.setActiveLayer('layer2');
nrrdTools.setActiveChannel(3);
const layer = nrrdTools.getActiveLayer();
const channel = nrrdTools.getActiveChannel();
// Layer visibility
nrrdTools.setLayerVisible('layer2', false);
const visible = nrrdTools.isLayerVisible('layer2');
const visMap = nrrdTools.getLayerVisibility(); // { layer1: true, layer2: false, ... }
// Channel visibility (per layer)
nrrdTools.setChannelVisible('layer1', 2, false);
const allChannelVis = nrrdTools.getChannelVisibility();
// Check if a layer has annotations
if (nrrdTools.hasLayerData('layer1')) await saveLayer('layer1');7. Channel Color Customization
Default colors:
| Channel | Color | Hex |
|---------|-------|-----|
| 1 | Emerald | #10b981 |
| 2 | Rose | #f43f5e |
| 3 | Blue | #3b82f6 |
| 4 | Amber | #fbbf24 |
| 5 | Fuchsia | #d946ef |
| 6 | Cyan | #06b6d4 |
| 7 | Orange | #f97316 |
| 8 | Violet | #8b5cf6 |
// Set one channel color (RGBAColor: { r, g, b, a } — 0-255)
nrrdTools.setChannelColor('layer1', 3, { r: 255, g: 128, b: 0, a: 255 });
// Batch-set (one reloadMasksFromVolume call — better performance)
nrrdTools.setChannelColors('layer1', {
1: { r: 255, g: 80, b: 80, a: 255 },
2: { r: 80, g: 180, b: 255, a: 255 },
});
// Apply same channel color across all layers
nrrdTools.setAllLayersChannelColor(1, { r: 0, g: 220, b: 100, a: 255 });
// Read colors
const rgba = nrrdTools.getChannelColor('layer1', 3);
const hex = nrrdTools.getChannelHexColor('layer1', 3); // → "#ff8000"
const css = nrrdTools.getChannelCssColor('layer1', 3); // → "rgba(255,128,0,1.00)"
// Reset
nrrdTools.resetChannelColors('layer1', 3); // one channel
nrrdTools.resetChannelColors('layer1'); // all channels in layer
nrrdTools.resetChannelColors(); // everything8. Undo / Redo
Per-layer undo stack (max 50 entries). Every completed stroke pushes a delta snapshot.
nrrdTools.undo();
nrrdTools.redo();
// Or via keyboard
window.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'z') nrrdTools.undo();
if (e.ctrlKey && e.key === 'y') nrrdTools.redo();
});9. Keyboard Shortcuts
| Action | Default Key |
|--------|-------------|
| Draw mode | Shift (hold) |
| Undo | z |
| Redo | y |
| Contrast adjust | Ctrl / Meta (hold) |
| Crosshair | s |
| Sphere mode | q |
| Mouse wheel | Zoom |
nrrdTools.setKeyboardSettings({ undo: 'u', mouseWheel: 'Scroll:Slice' });
const settings = nrrdTools.getKeyboardSettings();
// Suppress shortcuts while a form input is focused
inputEl.addEventListener('focus', () => nrrdTools.enterKeyboardConfig());
inputEl.addEventListener('blur', () => nrrdTools.exitKeyboardConfig());10. Clearing Annotations
nrrdTools.reset(); // Reset ALL layers, volumes, undo histories, canvases (use when switching cases)
nrrdTools.clearActiveLayer(); // Clear active layer's entire 3D volume + undo history, fire onClearLayerVolume
nrrdTools.clearActiveSlice(); // Clear only the currently viewed 2D slice (undoable)11. API Summary
| Category | Method | Description |
|----------|--------|-------------|
| Constructor | new NrrdTools(container, { layers }) | Create instance with optional layer config |
| Setup | drag(opts?) | Enable slice-drag navigation |
| | draw(opts?) | Bind annotation callbacks |
| | setupGUI(gui) | Connect dat.GUI / lil-gui panel |
| | enableContrastDragEvents(cb) | Enable Ctrl+drag windowing |
| | setDisplaySliceIndexPanel(el) | Show slice index in a panel |
| | setBaseDrawDisplayCanvasesSize(n) | Set canvas resolution multiplier (1–8) |
| Data | reset() | Reset all volumes, undo histories, canvases, sphere data |
| | clearActiveLayer() | Clear active layer volume + undo history |
| | clearActiveSlice() | Clear current slice (undoable) |
| | setAllSlices(slices) | Load NRRD slices, init MaskVolumes |
| | setMasksFromNIfTI(map, bar?) | Load saved NIfTI voxel data |
| Render | start | Frame callback — pass to render loop |
| Layer | setActiveLayer(id) | Switch drawing target layer |
| | getActiveLayer() | Read current layer |
| | setLayerVisible(id, bool) | Toggle layer in composite view |
| | isLayerVisible(id) | Query layer visibility |
| | getLayerVisibility() | All layer visibility map |
| | hasLayerData(id) | Check if layer has non-zero voxels |
| Sphere | setActiveSphereType(type) | Set active sphere type, updates brush color |
| | getActiveSphereType() | Read current sphere type |
| | setCalculateDistanceSphere(x, y, slice, type) | Programmatically place a calculator sphere |
| Channel | setActiveChannel(ch) | Switch drawing target channel |
| | getActiveChannel() | Read current channel |
| | setChannelVisible(id, ch, bool) | Toggle channel visibility in a layer |
| | isChannelVisible(id, ch) | Query channel visibility |
| | getChannelVisibility() | All channel visibility map |
| Color | setChannelColor(id, ch, rgba) | Set one channel color in one layer |
| | setChannelColors(id, map) | Batch-set colors in one layer |
| | setAllLayersChannelColor(ch, rgba) | Set one channel color across all layers |
| | getChannelColor(id, ch) | Read RGBA |
| | getChannelHexColor(id, ch) | Read Hex string |
| | getChannelCssColor(id, ch) | Read CSS rgba() string |
| | resetChannelColors(id?, ch?) | Reset to defaults |
| Tool Mode | setMode(mode) | Switch tool: "pencil" / "brush" / "eraser" / "sphere" / "calculator" |
| | getMode() | Read current tool mode |
| Drawing | setOpacity(value) | Set mask overlay opacity [0.1, 1] |
| | getOpacity() | Read current opacity |
| | setBrushSize(size) | Set brush/eraser size [5, 50] |
| | getBrushSize() | Read current brush size |
| Contrast | setWindowHigh(value) | Set window high |
| | setWindowLow(value) | Set window low |
| | finishWindowAdjustment() | Repaint all contrast slices after drag ends |
| Actions | executeAction(action) | Run: "undo" / "redo" / "clearActiveSliceMask" / "clearActiveLayerMask" / "resetZoom" / "downloadCurrentMask" |
| Navigation | setSliceOrientation(axis) | Switch viewing axis "x" / "y" / "z" |
| History | undo() / redo() | Undo / redo last stroke |
| Keyboard | setKeyboardSettings(partial) | Remap shortcuts |
| | getKeyboardSettings() | Read current bindings |
| | enterKeyboardConfig() / exitKeyboardConfig() | Suppress / restore shortcuts |
| | setContrastShortcutEnabled(bool) | Enable/disable contrast key |
| Inspect | getCurrentImageDimension() | [w, h, d] voxel dims |
| | getVoxelSpacing() | Physical mm spacing |
| | getSpaceOrigin() | World-space origin |
| | getMaxSliceNum() | Max slice index per axis |
| | getCurrentSlicesNumAndContrastNum() | Current slice & contrast index |
| | getMaskData() | Raw IMaskData object |
| | getNrrdToolsSettings() | Full NrrdState snapshot |
| | getContainer() | Host HTMLElement |
| | getDrawingCanvas() | Top-layer HTMLCanvasElement |
12. Type Reference
interface RGBAColor { r: number; g: number; b: number; a: number; } // 0-255
type ChannelColorMap = Record<number, RGBAColor>; // key = channel 1-8
interface IDrawOpts {
getMaskData?: (
sliceData: Uint8Array, layerId: string, channelId: number,
sliceIndex: number, axis: 'x' | 'y' | 'z',
width: number, height: number, clearFlag?: boolean
) => void;
onClearLayerVolume?: (layerId: string) => void;
getSphereData?: (sphereOrigin: number[], sphereRadius: number) => void;
getCalculateSpherePositionsData?: (
tumourOrigin: ICommXYZ | null, skinOrigin: ICommXYZ | null,
ribOrigin: ICommXYZ | null, nippleOrigin: ICommXYZ | null,
axis: 'x' | 'y' | 'z'
) => void;
}
interface IDragOpts {
showNumber?: boolean;
getSliceNum?: (sliceIndex: number, contrastIndex: number) => void;
}
interface IKeyBoardSettings {
draw: string;
undo: string;
redo: string;
contrast: string[];
crosshair: string;
sphere: string;
mouseWheel: 'Scroll:Zoom' | 'Scroll:Slice';
}
interface ICommXYZ { x: number; y: number; z: number; }
type LayerId = 'layer1' | 'layer2' | 'layer3' | 'layer4'; // or any string
type ChannelValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;Acknowledgements
Special thanks to Duke University dataset for providing the MRI data.
