A 3D visualisation package based on three.js — multiple scenes, NRRD/DICOM image loading, and a full medical image segmentation annotation engine.
https://copper3d-visualisation.readthedocs.io/en/latest/
Old: https://www.npmjs.com/package/copper3d_visualisation Very old: https://www.npmjs.com/package/gltfloader-plugin-test
Pick model with Gltfloader Copper3d_examples
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];
}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.
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);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);// 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);// Register once after initialization
const callbackId = scene.addPreRenderCallbackFunction(nrrdTools.start);
// Unregister on teardown
scene.removePreRenderCallbackFunction(callbackId);nrrdTools.drag({
showNumber: true,
getSliceNum: (sliceIndex, contrastIndex) => updateUI(sliceIndex),
});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 });
},
});nrrdTools.enableContrastDragEvents((step, towards) => {
console.log(`Contrast: ${towards} ${step}`);
});| 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 internally// 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');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(); // everythingPer-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();
});| 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());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)| 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 |
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;Special thanks to Duke University dataset for providing the MRI data.