copper3d 3.2.0 → 3.3.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/dist/Renderer/baseRenderer.js +5 -3
- package/dist/Renderer/baseRenderer.js.map +1 -1
- package/dist/Scene/baseScene.js +18 -4
- package/dist/Scene/baseScene.js.map +1 -1
- package/dist/Scene/copperScene.d.ts +1 -0
- package/dist/Scene/copperScene.js +17 -4
- package/dist/Scene/copperScene.js.map +1 -1
- package/dist/Utils/segmentation/DragOperator.d.ts +2 -1
- package/dist/Utils/segmentation/DragOperator.js +3 -1
- package/dist/Utils/segmentation/DragOperator.js.map +1 -1
- package/dist/Utils/segmentation/NrrdTools.d.ts +3 -1
- package/dist/Utils/segmentation/NrrdTools.js +46 -2
- package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
- package/dist/Utils/segmentation/core/GaussianSmoother.d.ts +55 -0
- package/dist/Utils/segmentation/core/GaussianSmoother.js +210 -0
- package/dist/Utils/segmentation/core/GaussianSmoother.js.map +1 -0
- package/dist/Utils/segmentation/core/MaskVolume.d.ts +9 -0
- package/dist/Utils/segmentation/core/MaskVolume.js +11 -0
- package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -1
- package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +0 -4
- package/dist/Utils/segmentation/tools/DragSliceTool.js +2 -19
- package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -1
- package/dist/Utils/segmentation/tools/SliceRenderPipeline.js +8 -1
- package/dist/Utils/segmentation/tools/SliceRenderPipeline.js.map +1 -1
- package/dist/Utils/segmentation/tools/ToolHost.d.ts +1 -1
- package/dist/bundle.esm.js +321 -36
- package/dist/bundle.umd.js +321 -35
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/types/Scene/copperScene.d.ts +1 -0
- package/dist/types/Utils/segmentation/DragOperator.d.ts +2 -1
- package/dist/types/Utils/segmentation/NrrdTools.d.ts +3 -1
- package/dist/types/Utils/segmentation/core/GaussianSmoother.d.ts +55 -0
- package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +9 -0
- package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +0 -4
- package/dist/types/Utils/segmentation/tools/ToolHost.d.ts +1 -1
- package/dist/types/index.d.ts +3 -2
- package/package.json +1 -1
package/dist/bundle.esm.js
CHANGED
|
@@ -49804,8 +49804,8 @@ class baseScene extends commonScene {
|
|
|
49804
49804
|
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
|
|
49805
49805
|
};
|
|
49806
49806
|
this.renderer = renderer;
|
|
49807
|
-
this.ambientLight = new AmbientLight(
|
|
49808
|
-
this.directionalLight = new DirectionalLight(0xffffff, 0
|
|
49807
|
+
this.ambientLight = new AmbientLight(0x606060, 0.8);
|
|
49808
|
+
this.directionalLight = new DirectionalLight(0xffffff, 1.0);
|
|
49809
49809
|
if (!(opt === null || opt === void 0 ? void 0 : opt.alpha)) {
|
|
49810
49810
|
this.vignette = createBackground({
|
|
49811
49811
|
aspect: this.container.clientWidth / this.container.clientHeight,
|
|
@@ -49889,17 +49889,31 @@ class baseScene extends commonScene {
|
|
|
49889
49889
|
this.scene.add(obj);
|
|
49890
49890
|
}
|
|
49891
49891
|
addLights() {
|
|
49892
|
-
|
|
49892
|
+
// Hemisphere light: sky/ground for broad ambient fill
|
|
49893
|
+
const hemiLight = new HemisphereLight(0xddeeff, 0x0f0e0d, 0.6);
|
|
49893
49894
|
hemiLight.name = "hemi_light";
|
|
49894
49895
|
this.scene.add(hemiLight);
|
|
49896
|
+
// Main key light — front-right, slightly above
|
|
49895
49897
|
this.ambientLight.name = "ambient_light";
|
|
49896
49898
|
this.directionalLight.name = "main_light";
|
|
49897
|
-
this.directionalLight.position.set(0.5, 0, 0.866);
|
|
49899
|
+
this.directionalLight.position.set(0.5, 0.5, 0.866);
|
|
49898
49900
|
this.camera.add(this.ambientLight);
|
|
49899
49901
|
this.camera.add(this.directionalLight);
|
|
49902
|
+
// Fill light — opposite side to soften shadows
|
|
49903
|
+
const fillLight = new DirectionalLight(0xffffff, 0.4);
|
|
49904
|
+
fillLight.name = "fill_light";
|
|
49905
|
+
fillLight.position.set(-0.5, -0.2, -0.866);
|
|
49906
|
+
this.camera.add(fillLight);
|
|
49907
|
+
// Top light — illuminates the top surface that key/fill lights miss
|
|
49908
|
+
const topLight = new DirectionalLight(0xffffff, 0.5);
|
|
49909
|
+
topLight.name = "top_light";
|
|
49910
|
+
topLight.position.set(0, 1, 0.2);
|
|
49911
|
+
this.camera.add(topLight);
|
|
49900
49912
|
this.lights.push(this.ambientLight);
|
|
49901
49913
|
this.lights.push(this.directionalLight);
|
|
49902
49914
|
this.lights.push(hemiLight);
|
|
49915
|
+
this.lights.push(fillLight);
|
|
49916
|
+
this.lights.push(topLight);
|
|
49903
49917
|
}
|
|
49904
49918
|
removeLights() {
|
|
49905
49919
|
if (this.lights) {
|
|
@@ -50603,6 +50617,8 @@ class baseRenderer {
|
|
|
50603
50617
|
}
|
|
50604
50618
|
// this.renderer.useLegacyLights = true;
|
|
50605
50619
|
this.renderer.outputColorSpace = SRGBColorSpace;
|
|
50620
|
+
this.renderer.toneMapping = ACESFilmicToneMapping;
|
|
50621
|
+
this.renderer.toneMappingExposure = 1.8;
|
|
50606
50622
|
this.gui = null;
|
|
50607
50623
|
this.stats = new Stats$1();
|
|
50608
50624
|
this.pmremGenerator = new PMREMGenerator(this.renderer);
|
|
@@ -50628,9 +50644,9 @@ class baseRenderer {
|
|
|
50628
50644
|
grid: false,
|
|
50629
50645
|
// Lights
|
|
50630
50646
|
addLights: true,
|
|
50631
|
-
exposure: 1.
|
|
50632
|
-
ambientIntensity: 0.
|
|
50633
|
-
ambientColor:
|
|
50647
|
+
exposure: 1.8,
|
|
50648
|
+
ambientIntensity: 0.8,
|
|
50649
|
+
ambientColor: 0x606060,
|
|
50634
50650
|
directIntensity: 0.8 * Math.PI,
|
|
50635
50651
|
directColor: 0xffffff,
|
|
50636
50652
|
bgColor1: "#5454ad",
|
|
@@ -59427,12 +59443,25 @@ class copperScene extends baseScene {
|
|
|
59427
59443
|
const loader = copperGltfLoader(this.renderer);
|
|
59428
59444
|
loader.load(url, (glb) => {
|
|
59429
59445
|
const content = glb.scene;
|
|
59446
|
+
// Enhance PBR materials for better visual quality
|
|
59447
|
+
if ((opts === null || opts === void 0 ? void 0 : opts.enhanceMaterial) !== false) {
|
|
59448
|
+
content.traverse((child) => {
|
|
59449
|
+
if (child.isMesh) {
|
|
59450
|
+
const mesh = child;
|
|
59451
|
+
const mat = mesh.material;
|
|
59452
|
+
if (mat.isMeshStandardMaterial) {
|
|
59453
|
+
mat.roughness = Math.min(mat.roughness, 0.35);
|
|
59454
|
+
mat.metalness = Math.max(mat.metalness, 0.05);
|
|
59455
|
+
mat.envMapIntensity = 1.2;
|
|
59456
|
+
mat.side = DoubleSide;
|
|
59457
|
+
mat.needsUpdate = true;
|
|
59458
|
+
}
|
|
59459
|
+
}
|
|
59460
|
+
});
|
|
59461
|
+
}
|
|
59430
59462
|
this.scene.add(content);
|
|
59431
59463
|
!!callback && callback(content);
|
|
59432
|
-
},
|
|
59433
|
-
(xhr) => { },
|
|
59434
|
-
// called when loading has errors
|
|
59435
|
-
(error) => {
|
|
59464
|
+
}, (xhr) => { }, (error) => {
|
|
59436
59465
|
console.log("An error happened: ", error);
|
|
59437
59466
|
});
|
|
59438
59467
|
}
|
|
@@ -71387,6 +71416,17 @@ class MaskVolume {
|
|
|
71387
71416
|
getChannels() {
|
|
71388
71417
|
return this.numChannels;
|
|
71389
71418
|
}
|
|
71419
|
+
/**
|
|
71420
|
+
* Return the number of bytes in one complete z-slice.
|
|
71421
|
+
*
|
|
71422
|
+
* Equal to `width × height × channels`. Useful for direct buffer
|
|
71423
|
+
* index arithmetic in performance-critical code paths.
|
|
71424
|
+
*
|
|
71425
|
+
* @returns Bytes per z-slice.
|
|
71426
|
+
*/
|
|
71427
|
+
getBytesPerSlice() {
|
|
71428
|
+
return this.bytesPerSlice;
|
|
71429
|
+
}
|
|
71390
71430
|
/**
|
|
71391
71431
|
* Return total memory used by the backing buffer, in bytes.
|
|
71392
71432
|
*
|
|
@@ -73204,8 +73244,8 @@ class DragSliceTool extends BaseTool {
|
|
|
73204
73244
|
}
|
|
73205
73245
|
}
|
|
73206
73246
|
}
|
|
73207
|
-
// Composite all layers to master canvas
|
|
73208
|
-
this.compositeAllLayers();
|
|
73247
|
+
// Composite all layers to master canvas (uses per-layer alpha)
|
|
73248
|
+
this.callbacks.compositeAllLayers();
|
|
73209
73249
|
}
|
|
73210
73250
|
// Refresh sphere overlay from volume for the new slice
|
|
73211
73251
|
if (this.ctx.gui_states.mode.sphere) {
|
|
@@ -73214,23 +73254,6 @@ class DragSliceTool extends BaseTool {
|
|
|
73214
73254
|
view.switchSliceFlag = false;
|
|
73215
73255
|
}
|
|
73216
73256
|
}
|
|
73217
|
-
/**
|
|
73218
|
-
* Composite all visible layer canvases to the master display canvas.
|
|
73219
|
-
*/
|
|
73220
|
-
compositeAllLayers() {
|
|
73221
|
-
const masterCtx = this.ctx.protectedData.ctxes.drawingLayerMasterCtx;
|
|
73222
|
-
const width = this.ctx.nrrd_states.view.changedWidth;
|
|
73223
|
-
const height = this.ctx.nrrd_states.view.changedHeight;
|
|
73224
|
-
masterCtx.clearRect(0, 0, width, height);
|
|
73225
|
-
// Master stores full-alpha composite; globalAlpha applied in start() render loop.
|
|
73226
|
-
for (const layerId of this.ctx.nrrd_states.image.layers) {
|
|
73227
|
-
if (!this.ctx.gui_states.layerChannel.layerVisibility[layerId])
|
|
73228
|
-
continue;
|
|
73229
|
-
const target = this.ctx.protectedData.layerTargets.get(layerId);
|
|
73230
|
-
if (target)
|
|
73231
|
-
masterCtx.drawImage(target.canvas, 0, 0, width, height);
|
|
73232
|
-
}
|
|
73233
|
-
}
|
|
73234
73257
|
// ===== Canvas Cleanup =====
|
|
73235
73258
|
cleanCanvases(flag) {
|
|
73236
73259
|
if (flag) {
|
|
@@ -73272,7 +73295,7 @@ class DragSliceTool extends BaseTool {
|
|
|
73272
73295
|
}
|
|
73273
73296
|
|
|
73274
73297
|
class DragOperator {
|
|
73275
|
-
constructor(container, nrrd_sates, gui_states, protectedData, drawingPrameters, setSyncsliceNum, setIsDrawFalse, flipDisplayImageByAxis, setEmptyCanvasSize, getOrCreateSliceBuffer, renderSliceToCanvas) {
|
|
73298
|
+
constructor(container, nrrd_sates, gui_states, protectedData, drawingPrameters, setSyncsliceNum, setIsDrawFalse, flipDisplayImageByAxis, setEmptyCanvasSize, getOrCreateSliceBuffer, renderSliceToCanvas, compositeAllLayers) {
|
|
73276
73299
|
this.dragPrameters = {
|
|
73277
73300
|
move: 0,
|
|
73278
73301
|
y: 0,
|
|
@@ -73308,6 +73331,7 @@ class DragOperator {
|
|
|
73308
73331
|
this.setEmptyCanvasSize = setEmptyCanvasSize;
|
|
73309
73332
|
this.getOrCreateSliceBuffer = getOrCreateSliceBuffer;
|
|
73310
73333
|
this.renderSliceToCanvas = renderSliceToCanvas;
|
|
73334
|
+
this.compositeAllLayers = compositeAllLayers;
|
|
73311
73335
|
this.showDragNumberDiv = createShowSliceNumberDiv();
|
|
73312
73336
|
this.init();
|
|
73313
73337
|
}
|
|
@@ -73333,6 +73357,7 @@ class DragOperator {
|
|
|
73333
73357
|
setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
|
|
73334
73358
|
getOrCreateSliceBuffer: (axis) => this.getOrCreateSliceBuffer(axis),
|
|
73335
73359
|
renderSliceToCanvas: (layer, axis, sliceIndex, buffer, targetCtx, w, h) => this.renderSliceToCanvas(layer, axis, sliceIndex, buffer, targetCtx, w, h),
|
|
73360
|
+
compositeAllLayers: () => this.compositeAllLayers(),
|
|
73336
73361
|
refreshSphereOverlay: () => { var _a; return (_a = this.refreshSphereOverlayCb) === null || _a === void 0 ? void 0 : _a.call(this); },
|
|
73337
73362
|
}, this.showDragNumberDiv, dragEffectCanvases);
|
|
73338
73363
|
}
|
|
@@ -76460,6 +76485,216 @@ class CanvasState {
|
|
|
76460
76485
|
}
|
|
76461
76486
|
}
|
|
76462
76487
|
|
|
76488
|
+
/**
|
|
76489
|
+
* GaussianSmoother — 3D Gaussian smoothing for segmentation masks.
|
|
76490
|
+
*
|
|
76491
|
+
* Applies a separable 3D Gaussian blur to a single label channel within a
|
|
76492
|
+
* MaskVolume, then thresholds the result back to a binary mask. This smooths
|
|
76493
|
+
* jagged edges and fills small holes in segmentation annotations.
|
|
76494
|
+
*
|
|
76495
|
+
* Pure, stateless utility — no DOM/Canvas/GUI dependencies.
|
|
76496
|
+
*
|
|
76497
|
+
* Performance: uses direct typed-array access (bypassing getVoxel/setVoxel
|
|
76498
|
+
* boundary checks) and branch-free convolution for the interior region.
|
|
76499
|
+
*/
|
|
76500
|
+
class GaussianSmoother {
|
|
76501
|
+
/**
|
|
76502
|
+
* Smooth a single label channel in-place using separable 3D Gaussian blur.
|
|
76503
|
+
*
|
|
76504
|
+
* Steps:
|
|
76505
|
+
* 1. Extract — create a Float32Array with 1.0 where voxel === channel, 0.0 elsewhere.
|
|
76506
|
+
* 2. Blur — apply separable Gaussian (X → Y → Z) on the float buffer.
|
|
76507
|
+
* 3. Threshold — binarize at 0.5.
|
|
76508
|
+
* 4. Write back — overwrite/erase voxels according to the thresholded result.
|
|
76509
|
+
*
|
|
76510
|
+
* @param volume The MaskVolume to operate on (modified in-place).
|
|
76511
|
+
* @param channel The label value to smooth (must be > 0).
|
|
76512
|
+
* @param sigma Base Gaussian sigma in voxel units (default 1.0).
|
|
76513
|
+
* @param spacing Optional voxel spacing [sx, sy, sz]. When provided, per-axis
|
|
76514
|
+
* sigma is computed as `sigma / spacing[axis]` so that the
|
|
76515
|
+
* physical smoothing radius is isotropic.
|
|
76516
|
+
*/
|
|
76517
|
+
static gaussianSmooth3D(volume, channel, sigma = 1.0, spacing) {
|
|
76518
|
+
const dims = volume.getDimensions();
|
|
76519
|
+
const { width, height, depth } = dims;
|
|
76520
|
+
const totalVoxels = width * height * depth;
|
|
76521
|
+
// Direct access to underlying buffer — bypasses getVoxel/setVoxel
|
|
76522
|
+
// boundary checks for ~10x speedup in hot loops.
|
|
76523
|
+
const rawData = volume.getRawData();
|
|
76524
|
+
const channels = volume.getChannels();
|
|
76525
|
+
const bytesPerSlice = volume.getBytesPerSlice();
|
|
76526
|
+
const rowStride = width * channels;
|
|
76527
|
+
// Compute per-axis sigma (normalized to voxel units)
|
|
76528
|
+
const sigmaX = spacing ? sigma / spacing[0] : sigma;
|
|
76529
|
+
const sigmaY = spacing ? sigma / spacing[1] : sigma;
|
|
76530
|
+
const sigmaZ = spacing ? sigma / spacing[2] : sigma;
|
|
76531
|
+
// 1. Extract binary float buffer — direct array access
|
|
76532
|
+
const buffer = new Float32Array(totalVoxels);
|
|
76533
|
+
for (let z = 0; z < depth; z++) {
|
|
76534
|
+
const zOffset = z * bytesPerSlice;
|
|
76535
|
+
for (let y = 0; y < height; y++) {
|
|
76536
|
+
const zyOffset = zOffset + y * rowStride;
|
|
76537
|
+
for (let x = 0; x < width; x++) {
|
|
76538
|
+
const bufIdx = z * height * width + y * width + x;
|
|
76539
|
+
buffer[bufIdx] = rawData[zyOffset + x * channels] === channel ? 1.0 : 0.0;
|
|
76540
|
+
}
|
|
76541
|
+
}
|
|
76542
|
+
}
|
|
76543
|
+
// 2. Separable Gaussian blur: X → Y → Z
|
|
76544
|
+
const kernelX = GaussianSmoother.generateKernel1D(sigmaX);
|
|
76545
|
+
const kernelY = GaussianSmoother.generateKernel1D(sigmaY);
|
|
76546
|
+
const kernelZ = GaussianSmoother.generateKernel1D(sigmaZ);
|
|
76547
|
+
GaussianSmoother.convolve1D(buffer, width, height, depth, 0, kernelX); // X axis
|
|
76548
|
+
GaussianSmoother.convolve1D(buffer, width, height, depth, 1, kernelY); // Y axis
|
|
76549
|
+
GaussianSmoother.convolve1D(buffer, width, height, depth, 2, kernelZ); // Z axis
|
|
76550
|
+
// 3 & 4. Threshold and write back — direct array access
|
|
76551
|
+
for (let z = 0; z < depth; z++) {
|
|
76552
|
+
const zOffset = z * bytesPerSlice;
|
|
76553
|
+
for (let y = 0; y < height; y++) {
|
|
76554
|
+
const zyOffset = zOffset + y * rowStride;
|
|
76555
|
+
for (let x = 0; x < width; x++) {
|
|
76556
|
+
const bufIdx = z * height * width + y * width + x;
|
|
76557
|
+
const smoothed = buffer[bufIdx] >= 0.5 ? 1 : 0;
|
|
76558
|
+
const rawIdx = zyOffset + x * channels;
|
|
76559
|
+
const original = rawData[rawIdx];
|
|
76560
|
+
if (smoothed === 1 && original !== channel) {
|
|
76561
|
+
// Smoothed region expands into this voxel — overwrite
|
|
76562
|
+
rawData[rawIdx] = channel;
|
|
76563
|
+
}
|
|
76564
|
+
else if (smoothed === 0 && original === channel) {
|
|
76565
|
+
// Smoothed region retreats — erase
|
|
76566
|
+
rawData[rawIdx] = 0;
|
|
76567
|
+
}
|
|
76568
|
+
// Otherwise leave unchanged
|
|
76569
|
+
}
|
|
76570
|
+
}
|
|
76571
|
+
}
|
|
76572
|
+
}
|
|
76573
|
+
/**
|
|
76574
|
+
* Generate a normalized 1D Gaussian kernel truncated at ±3σ.
|
|
76575
|
+
*
|
|
76576
|
+
* @param sigma Standard deviation (in voxels). Must be > 0.
|
|
76577
|
+
* @returns Normalized Float32Array of odd length.
|
|
76578
|
+
*/
|
|
76579
|
+
static generateKernel1D(sigma) {
|
|
76580
|
+
if (sigma <= 0) {
|
|
76581
|
+
return new Float32Array([1.0]);
|
|
76582
|
+
}
|
|
76583
|
+
const radius = Math.ceil(sigma * 3);
|
|
76584
|
+
const size = 2 * radius + 1;
|
|
76585
|
+
const kernel = new Float32Array(size);
|
|
76586
|
+
const twoSigmaSq = 2 * sigma * sigma;
|
|
76587
|
+
let sum = 0;
|
|
76588
|
+
for (let i = 0; i < size; i++) {
|
|
76589
|
+
const x = i - radius;
|
|
76590
|
+
kernel[i] = Math.exp(-(x * x) / twoSigmaSq);
|
|
76591
|
+
sum += kernel[i];
|
|
76592
|
+
}
|
|
76593
|
+
// Normalize
|
|
76594
|
+
for (let i = 0; i < size; i++) {
|
|
76595
|
+
kernel[i] /= sum;
|
|
76596
|
+
}
|
|
76597
|
+
return kernel;
|
|
76598
|
+
}
|
|
76599
|
+
/**
|
|
76600
|
+
* In-place single-axis convolution with zero-padding at boundaries.
|
|
76601
|
+
*
|
|
76602
|
+
* Uses a 3-segment approach: left boundary (with bounds check),
|
|
76603
|
+
* middle interior (branch-free, ~95% of work), right boundary
|
|
76604
|
+
* (with bounds check). This eliminates per-element branching
|
|
76605
|
+
* in the hot inner loop.
|
|
76606
|
+
*
|
|
76607
|
+
* @param data Flat float buffer (width × height × depth).
|
|
76608
|
+
* @param width X dimension.
|
|
76609
|
+
* @param height Y dimension.
|
|
76610
|
+
* @param depth Z dimension.
|
|
76611
|
+
* @param axis 0 = X, 1 = Y, 2 = Z.
|
|
76612
|
+
* @param kernel 1D convolution kernel (odd length).
|
|
76613
|
+
*/
|
|
76614
|
+
static convolve1D(data, width, height, depth, axis, kernel) {
|
|
76615
|
+
const kLen = kernel.length;
|
|
76616
|
+
const radius = (kLen - 1) / 2;
|
|
76617
|
+
// Determine the dimension length along the convolution axis
|
|
76618
|
+
// and the stride between consecutive elements along that axis
|
|
76619
|
+
let axisLen;
|
|
76620
|
+
let stride;
|
|
76621
|
+
let outerCount;
|
|
76622
|
+
if (axis === 0) {
|
|
76623
|
+
// X axis: stride = 1, iterate over all (y, z) lines
|
|
76624
|
+
axisLen = width;
|
|
76625
|
+
stride = 1;
|
|
76626
|
+
outerCount = height * depth;
|
|
76627
|
+
}
|
|
76628
|
+
else if (axis === 1) {
|
|
76629
|
+
// Y axis: stride = width, iterate over all (x, z) lines
|
|
76630
|
+
axisLen = height;
|
|
76631
|
+
stride = width;
|
|
76632
|
+
outerCount = width * depth;
|
|
76633
|
+
}
|
|
76634
|
+
else {
|
|
76635
|
+
// Z axis: stride = width * height, iterate over all (x, y) lines
|
|
76636
|
+
axisLen = depth;
|
|
76637
|
+
stride = width * height;
|
|
76638
|
+
outerCount = width * height;
|
|
76639
|
+
}
|
|
76640
|
+
// Precompute middle segment bounds
|
|
76641
|
+
const midStart = radius;
|
|
76642
|
+
const midEnd = axisLen - radius;
|
|
76643
|
+
// Temporary line buffer to avoid read-after-write issues
|
|
76644
|
+
const line = new Float32Array(axisLen);
|
|
76645
|
+
for (let outer = 0; outer < outerCount; outer++) {
|
|
76646
|
+
// Compute the starting index for this line
|
|
76647
|
+
let lineStart;
|
|
76648
|
+
if (axis === 0) {
|
|
76649
|
+
lineStart = outer * width;
|
|
76650
|
+
}
|
|
76651
|
+
else if (axis === 1) {
|
|
76652
|
+
const z = Math.floor(outer / width);
|
|
76653
|
+
const x = outer % width;
|
|
76654
|
+
lineStart = z * width * height + x;
|
|
76655
|
+
}
|
|
76656
|
+
else {
|
|
76657
|
+
lineStart = outer;
|
|
76658
|
+
}
|
|
76659
|
+
// Read the line
|
|
76660
|
+
for (let i = 0; i < axisLen; i++) {
|
|
76661
|
+
line[i] = data[lineStart + i * stride];
|
|
76662
|
+
}
|
|
76663
|
+
// Left boundary segment (i = 0 .. radius-1): lower bound check
|
|
76664
|
+
for (let i = 0; i < midStart; i++) {
|
|
76665
|
+
let sum = 0;
|
|
76666
|
+
for (let k = 0; k < kLen; k++) {
|
|
76667
|
+
const j = i + k - radius;
|
|
76668
|
+
if (j >= 0) {
|
|
76669
|
+
sum += line[j] * kernel[k];
|
|
76670
|
+
}
|
|
76671
|
+
}
|
|
76672
|
+
data[lineStart + i * stride] = sum;
|
|
76673
|
+
}
|
|
76674
|
+
// Middle segment (i = radius .. axisLen-radius-1): NO bounds check
|
|
76675
|
+
for (let i = midStart; i < midEnd; i++) {
|
|
76676
|
+
let sum = 0;
|
|
76677
|
+
const lineOffset = i - radius;
|
|
76678
|
+
for (let k = 0; k < kLen; k++) {
|
|
76679
|
+
sum += line[lineOffset + k] * kernel[k];
|
|
76680
|
+
}
|
|
76681
|
+
data[lineStart + i * stride] = sum;
|
|
76682
|
+
}
|
|
76683
|
+
// Right boundary segment (i = axisLen-radius .. axisLen-1): upper bound check
|
|
76684
|
+
for (let i = midEnd; i < axisLen; i++) {
|
|
76685
|
+
let sum = 0;
|
|
76686
|
+
for (let k = 0; k < kLen; k++) {
|
|
76687
|
+
const j = i + k - radius;
|
|
76688
|
+
if (j < axisLen) {
|
|
76689
|
+
sum += line[j] * kernel[k];
|
|
76690
|
+
}
|
|
76691
|
+
}
|
|
76692
|
+
data[lineStart + i * stride] = sum;
|
|
76693
|
+
}
|
|
76694
|
+
}
|
|
76695
|
+
}
|
|
76696
|
+
}
|
|
76697
|
+
|
|
76463
76698
|
/**
|
|
76464
76699
|
* Manages layer/channel state: active selection, visibility, and channel colors.
|
|
76465
76700
|
*
|
|
@@ -76976,6 +77211,8 @@ class SliceRenderPipeline extends BaseTool {
|
|
|
76976
77211
|
* Then update the changedWidth and changedHeight based on the sizeFactor.
|
|
76977
77212
|
*/
|
|
76978
77213
|
updateOriginAndChangedWH() {
|
|
77214
|
+
const prevW = this.ctx.nrrd_states.image.originWidth;
|
|
77215
|
+
const prevH = this.ctx.nrrd_states.image.originHeight;
|
|
76979
77216
|
this.ctx.nrrd_states.image.originWidth =
|
|
76980
77217
|
this.ctx.protectedData.canvases.originCanvas.width;
|
|
76981
77218
|
this.ctx.nrrd_states.image.originHeight =
|
|
@@ -76984,7 +77221,12 @@ class SliceRenderPipeline extends BaseTool {
|
|
|
76984
77221
|
// Setting them here would defeat the sizeChanged detection in resizePaintArea,
|
|
76985
77222
|
// causing canvas elements to keep stale dimensions after axis switches.
|
|
76986
77223
|
this.resizePaintArea(this.ctx.nrrd_states.view.sizeFactor);
|
|
76987
|
-
|
|
77224
|
+
// Only re-center when origin dimensions changed (e.g. axis switch).
|
|
77225
|
+
// Contrast toggle doesn't change origin size, so skip to preserve user's pan/zoom.
|
|
77226
|
+
if (prevW !== this.ctx.nrrd_states.image.originWidth ||
|
|
77227
|
+
prevH !== this.ctx.nrrd_states.image.originHeight) {
|
|
77228
|
+
this.resetPaintAreaUIPosition();
|
|
77229
|
+
}
|
|
76988
77230
|
}
|
|
76989
77231
|
/**
|
|
76990
77232
|
* Keep all contrast slice index to same.
|
|
@@ -77198,7 +77440,7 @@ class NrrdTools {
|
|
|
77198
77440
|
// Wire RenderingUtils' setEmptyCanvasSize callback
|
|
77199
77441
|
this.drawCore.renderer.setEmptyCanvasSize = (axis) => this.setEmptyCanvasSize(axis);
|
|
77200
77442
|
this.init();
|
|
77201
|
-
this.dragOperator = new DragOperator(this.container, this.state.nrrd_states, this.state.gui_states, this.state.protectedData, this.drawCore.drawingPrameters, this.setSyncsliceNum.bind(this), this.setIsDrawFalse.bind(this), this.flipDisplayImageByAxis.bind(this), this.setEmptyCanvasSize.bind(this), this.drawCore.renderer.getOrCreateSliceBuffer.bind(this.drawCore.renderer), this.drawCore.renderer.renderSliceToCanvas.bind(this.drawCore.renderer));
|
|
77443
|
+
this.dragOperator = new DragOperator(this.container, this.state.nrrd_states, this.state.gui_states, this.state.protectedData, this.drawCore.drawingPrameters, this.setSyncsliceNum.bind(this), this.setIsDrawFalse.bind(this), this.flipDisplayImageByAxis.bind(this), this.setEmptyCanvasSize.bind(this), this.drawCore.renderer.getOrCreateSliceBuffer.bind(this.drawCore.renderer), this.drawCore.renderer.renderSliceToCanvas.bind(this.drawCore.renderer), this.drawCore.renderer.compositeAllLayers.bind(this.drawCore.renderer));
|
|
77202
77444
|
// Inject EventRouter into DragOperator for centralized event handling
|
|
77203
77445
|
if (this.drawCore.eventRouter) {
|
|
77204
77446
|
this.dragOperator.setEventRouter(this.drawCore.eventRouter);
|
|
@@ -77517,7 +77759,8 @@ class NrrdTools {
|
|
|
77517
77759
|
getPencilColor() {
|
|
77518
77760
|
return this.state.gui_states.drawing.color;
|
|
77519
77761
|
}
|
|
77520
|
-
executeAction(action) {
|
|
77762
|
+
executeAction(action, opts) {
|
|
77763
|
+
var _a;
|
|
77521
77764
|
switch (action) {
|
|
77522
77765
|
case "undo":
|
|
77523
77766
|
this.undo();
|
|
@@ -77555,6 +77798,48 @@ class NrrdTools {
|
|
|
77555
77798
|
enableDownload(config);
|
|
77556
77799
|
break;
|
|
77557
77800
|
}
|
|
77801
|
+
case "gaussianSmooth": {
|
|
77802
|
+
const layerId = this.state.gui_states.layerChannel.layer;
|
|
77803
|
+
const channel = this.state.gui_states.layerChannel.activeChannel;
|
|
77804
|
+
const volume = this.drawCore.renderer.getVolumeForLayer(layerId);
|
|
77805
|
+
if (volume && channel > 0) {
|
|
77806
|
+
const dims = volume.getDimensions();
|
|
77807
|
+
// 1. Snapshot all Z-slices before mutation (undo support)
|
|
77808
|
+
const deltas = [];
|
|
77809
|
+
for (let z = 0; z < dims.depth; z++) {
|
|
77810
|
+
const sliceData = volume.getSliceUint8(z, "z").data;
|
|
77811
|
+
const hasChannel = sliceData.some((v) => v === channel);
|
|
77812
|
+
if (hasChannel) {
|
|
77813
|
+
deltas.push({
|
|
77814
|
+
layerId,
|
|
77815
|
+
axis: "z",
|
|
77816
|
+
sliceIndex: z,
|
|
77817
|
+
oldSlice: sliceData.slice(),
|
|
77818
|
+
newSlice: new Uint8Array(0), // placeholder, filled after smoothing
|
|
77819
|
+
});
|
|
77820
|
+
}
|
|
77821
|
+
}
|
|
77822
|
+
// 2. Apply smoothing with anisotropic spacing
|
|
77823
|
+
const spacing = this.state.nrrd_states.image.voxelSpacing;
|
|
77824
|
+
const spacingTuple = spacing.length >= 3 ? [spacing[0], spacing[1], spacing[2]] : undefined;
|
|
77825
|
+
const sigma = (_a = opts === null || opts === void 0 ? void 0 : opts.sigma) !== null && _a !== void 0 ? _a : 1.0;
|
|
77826
|
+
GaussianSmoother.gaussianSmooth3D(volume, channel, sigma, spacingTuple);
|
|
77827
|
+
// 3. Capture newSlice data after smoothing and push undo group
|
|
77828
|
+
for (const delta of deltas) {
|
|
77829
|
+
delta.newSlice = volume.getSliceUint8(delta.sliceIndex, "z").data.slice();
|
|
77830
|
+
}
|
|
77831
|
+
if (deltas.length > 0) {
|
|
77832
|
+
this.drawCore.undoManager.pushGroup(deltas);
|
|
77833
|
+
}
|
|
77834
|
+
// 4. Notify backend for each changed slice
|
|
77835
|
+
for (const delta of deltas) {
|
|
77836
|
+
const { data: sliceData, width, height } = volume.getSliceUint8(delta.sliceIndex, "z");
|
|
77837
|
+
this.state.annotationCallbacks.onMaskChanged(sliceData, layerId, channel, delta.sliceIndex, "z", width, height, false);
|
|
77838
|
+
}
|
|
77839
|
+
this.reloadMasksFromVolume();
|
|
77840
|
+
}
|
|
77841
|
+
break;
|
|
77842
|
+
}
|
|
77558
77843
|
}
|
|
77559
77844
|
}
|
|
77560
77845
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -78269,7 +78554,7 @@ function evaluateElement(element, weights) {
|
|
|
78269
78554
|
}
|
|
78270
78555
|
|
|
78271
78556
|
// import * as kiwrious from "copper3d_plugin_heart_k";
|
|
78272
|
-
const REVISION = "v3.
|
|
78557
|
+
const REVISION = "v3.3.0-beta";
|
|
78273
78558
|
console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
|
|
78274
78559
|
|
|
78275
|
-
export { CHANNEL_COLORS, CHANNEL_HEX_COLORS, CameraViewPoint, Copper3dTrackballControls, MeshNodeTool, NrrdTools, REVISION, addBoxHelper, addLabelToScene, configKiwriousHeart, convert3DPostoScreenPos, convertScreenPosto3DPos, copperMScene, copperMSceneRenderer, copperRenderer, copperRendererOnDemond, copperScene, copperSceneOnDemond, createTexture2D_NRRD, fullScreenListenner, kiwrious, loading, removeGuiFolderChilden, rgbaToCss, rgbaToHex, setHDRFilePath, throttle };
|
|
78560
|
+
export { CHANNEL_COLORS, CHANNEL_HEX_COLORS, CameraViewPoint, Copper3dTrackballControls, GaussianSmoother, MeshNodeTool, NrrdTools, REVISION, addBoxHelper, addLabelToScene, configKiwriousHeart, convert3DPostoScreenPos, convertScreenPosto3DPos, copperMScene, copperMSceneRenderer, copperRenderer, copperRendererOnDemond, copperScene, copperSceneOnDemond, createTexture2D_NRRD, fullScreenListenner, kiwrious, loading, removeGuiFolderChilden, rgbaToCss, rgbaToHex, setHDRFilePath, throttle };
|