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.
Files changed (39) hide show
  1. package/dist/Renderer/baseRenderer.js +5 -3
  2. package/dist/Renderer/baseRenderer.js.map +1 -1
  3. package/dist/Scene/baseScene.js +18 -4
  4. package/dist/Scene/baseScene.js.map +1 -1
  5. package/dist/Scene/copperScene.d.ts +1 -0
  6. package/dist/Scene/copperScene.js +17 -4
  7. package/dist/Scene/copperScene.js.map +1 -1
  8. package/dist/Utils/segmentation/DragOperator.d.ts +2 -1
  9. package/dist/Utils/segmentation/DragOperator.js +3 -1
  10. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  11. package/dist/Utils/segmentation/NrrdTools.d.ts +3 -1
  12. package/dist/Utils/segmentation/NrrdTools.js +46 -2
  13. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  14. package/dist/Utils/segmentation/core/GaussianSmoother.d.ts +55 -0
  15. package/dist/Utils/segmentation/core/GaussianSmoother.js +210 -0
  16. package/dist/Utils/segmentation/core/GaussianSmoother.js.map +1 -0
  17. package/dist/Utils/segmentation/core/MaskVolume.d.ts +9 -0
  18. package/dist/Utils/segmentation/core/MaskVolume.js +11 -0
  19. package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -1
  20. package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +0 -4
  21. package/dist/Utils/segmentation/tools/DragSliceTool.js +2 -19
  22. package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -1
  23. package/dist/Utils/segmentation/tools/SliceRenderPipeline.js +8 -1
  24. package/dist/Utils/segmentation/tools/SliceRenderPipeline.js.map +1 -1
  25. package/dist/Utils/segmentation/tools/ToolHost.d.ts +1 -1
  26. package/dist/bundle.esm.js +321 -36
  27. package/dist/bundle.umd.js +321 -35
  28. package/dist/index.d.ts +3 -2
  29. package/dist/index.js +3 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/types/Scene/copperScene.d.ts +1 -0
  32. package/dist/types/Utils/segmentation/DragOperator.d.ts +2 -1
  33. package/dist/types/Utils/segmentation/NrrdTools.d.ts +3 -1
  34. package/dist/types/Utils/segmentation/core/GaussianSmoother.d.ts +55 -0
  35. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +9 -0
  36. package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +0 -4
  37. package/dist/types/Utils/segmentation/tools/ToolHost.d.ts +1 -1
  38. package/dist/types/index.d.ts +3 -2
  39. package/package.json +1 -1
@@ -49812,8 +49812,8 @@ void main() {
49812
49812
  this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
49813
49813
  };
49814
49814
  this.renderer = renderer;
49815
- this.ambientLight = new AmbientLight(0x202020, 0.3);
49816
- this.directionalLight = new DirectionalLight(0xffffff, 0.3);
49815
+ this.ambientLight = new AmbientLight(0x606060, 0.8);
49816
+ this.directionalLight = new DirectionalLight(0xffffff, 1.0);
49817
49817
  if (!(opt === null || opt === void 0 ? void 0 : opt.alpha)) {
49818
49818
  this.vignette = createBackground({
49819
49819
  aspect: this.container.clientWidth / this.container.clientHeight,
@@ -49897,17 +49897,31 @@ void main() {
49897
49897
  this.scene.add(obj);
49898
49898
  }
49899
49899
  addLights() {
49900
- const hemiLight = new HemisphereLight();
49900
+ // Hemisphere light: sky/ground for broad ambient fill
49901
+ const hemiLight = new HemisphereLight(0xddeeff, 0x0f0e0d, 0.6);
49901
49902
  hemiLight.name = "hemi_light";
49902
49903
  this.scene.add(hemiLight);
49904
+ // Main key light — front-right, slightly above
49903
49905
  this.ambientLight.name = "ambient_light";
49904
49906
  this.directionalLight.name = "main_light";
49905
- this.directionalLight.position.set(0.5, 0, 0.866);
49907
+ this.directionalLight.position.set(0.5, 0.5, 0.866);
49906
49908
  this.camera.add(this.ambientLight);
49907
49909
  this.camera.add(this.directionalLight);
49910
+ // Fill light — opposite side to soften shadows
49911
+ const fillLight = new DirectionalLight(0xffffff, 0.4);
49912
+ fillLight.name = "fill_light";
49913
+ fillLight.position.set(-0.5, -0.2, -0.866);
49914
+ this.camera.add(fillLight);
49915
+ // Top light — illuminates the top surface that key/fill lights miss
49916
+ const topLight = new DirectionalLight(0xffffff, 0.5);
49917
+ topLight.name = "top_light";
49918
+ topLight.position.set(0, 1, 0.2);
49919
+ this.camera.add(topLight);
49908
49920
  this.lights.push(this.ambientLight);
49909
49921
  this.lights.push(this.directionalLight);
49910
49922
  this.lights.push(hemiLight);
49923
+ this.lights.push(fillLight);
49924
+ this.lights.push(topLight);
49911
49925
  }
49912
49926
  removeLights() {
49913
49927
  if (this.lights) {
@@ -50611,6 +50625,8 @@ void main() {
50611
50625
  }
50612
50626
  // this.renderer.useLegacyLights = true;
50613
50627
  this.renderer.outputColorSpace = SRGBColorSpace;
50628
+ this.renderer.toneMapping = ACESFilmicToneMapping;
50629
+ this.renderer.toneMappingExposure = 1.8;
50614
50630
  this.gui = null;
50615
50631
  this.stats = new Stats$1();
50616
50632
  this.pmremGenerator = new PMREMGenerator(this.renderer);
@@ -50636,9 +50652,9 @@ void main() {
50636
50652
  grid: false,
50637
50653
  // Lights
50638
50654
  addLights: true,
50639
- exposure: 1.0,
50640
- ambientIntensity: 0.3,
50641
- ambientColor: 0x202020,
50655
+ exposure: 1.8,
50656
+ ambientIntensity: 0.8,
50657
+ ambientColor: 0x606060,
50642
50658
  directIntensity: 0.8 * Math.PI,
50643
50659
  directColor: 0xffffff,
50644
50660
  bgColor1: "#5454ad",
@@ -59435,12 +59451,25 @@ void main() {
59435
59451
  const loader = copperGltfLoader(this.renderer);
59436
59452
  loader.load(url, (glb) => {
59437
59453
  const content = glb.scene;
59454
+ // Enhance PBR materials for better visual quality
59455
+ if ((opts === null || opts === void 0 ? void 0 : opts.enhanceMaterial) !== false) {
59456
+ content.traverse((child) => {
59457
+ if (child.isMesh) {
59458
+ const mesh = child;
59459
+ const mat = mesh.material;
59460
+ if (mat.isMeshStandardMaterial) {
59461
+ mat.roughness = Math.min(mat.roughness, 0.35);
59462
+ mat.metalness = Math.max(mat.metalness, 0.05);
59463
+ mat.envMapIntensity = 1.2;
59464
+ mat.side = DoubleSide;
59465
+ mat.needsUpdate = true;
59466
+ }
59467
+ }
59468
+ });
59469
+ }
59438
59470
  this.scene.add(content);
59439
59471
  !!callback && callback(content);
59440
- }, // called when loading is in progresses
59441
- (xhr) => { },
59442
- // called when loading has errors
59443
- (error) => {
59472
+ }, (xhr) => { }, (error) => {
59444
59473
  console.log("An error happened: ", error);
59445
59474
  });
59446
59475
  }
@@ -71395,6 +71424,17 @@ void main() {
71395
71424
  getChannels() {
71396
71425
  return this.numChannels;
71397
71426
  }
71427
+ /**
71428
+ * Return the number of bytes in one complete z-slice.
71429
+ *
71430
+ * Equal to `width × height × channels`. Useful for direct buffer
71431
+ * index arithmetic in performance-critical code paths.
71432
+ *
71433
+ * @returns Bytes per z-slice.
71434
+ */
71435
+ getBytesPerSlice() {
71436
+ return this.bytesPerSlice;
71437
+ }
71398
71438
  /**
71399
71439
  * Return total memory used by the backing buffer, in bytes.
71400
71440
  *
@@ -73212,8 +73252,8 @@ void main() {
73212
73252
  }
73213
73253
  }
73214
73254
  }
73215
- // Composite all layers to master canvas
73216
- this.compositeAllLayers();
73255
+ // Composite all layers to master canvas (uses per-layer alpha)
73256
+ this.callbacks.compositeAllLayers();
73217
73257
  }
73218
73258
  // Refresh sphere overlay from volume for the new slice
73219
73259
  if (this.ctx.gui_states.mode.sphere) {
@@ -73222,23 +73262,6 @@ void main() {
73222
73262
  view.switchSliceFlag = false;
73223
73263
  }
73224
73264
  }
73225
- /**
73226
- * Composite all visible layer canvases to the master display canvas.
73227
- */
73228
- compositeAllLayers() {
73229
- const masterCtx = this.ctx.protectedData.ctxes.drawingLayerMasterCtx;
73230
- const width = this.ctx.nrrd_states.view.changedWidth;
73231
- const height = this.ctx.nrrd_states.view.changedHeight;
73232
- masterCtx.clearRect(0, 0, width, height);
73233
- // Master stores full-alpha composite; globalAlpha applied in start() render loop.
73234
- for (const layerId of this.ctx.nrrd_states.image.layers) {
73235
- if (!this.ctx.gui_states.layerChannel.layerVisibility[layerId])
73236
- continue;
73237
- const target = this.ctx.protectedData.layerTargets.get(layerId);
73238
- if (target)
73239
- masterCtx.drawImage(target.canvas, 0, 0, width, height);
73240
- }
73241
- }
73242
73265
  // ===== Canvas Cleanup =====
73243
73266
  cleanCanvases(flag) {
73244
73267
  if (flag) {
@@ -73280,7 +73303,7 @@ void main() {
73280
73303
  }
73281
73304
 
73282
73305
  class DragOperator {
73283
- constructor(container, nrrd_sates, gui_states, protectedData, drawingPrameters, setSyncsliceNum, setIsDrawFalse, flipDisplayImageByAxis, setEmptyCanvasSize, getOrCreateSliceBuffer, renderSliceToCanvas) {
73306
+ constructor(container, nrrd_sates, gui_states, protectedData, drawingPrameters, setSyncsliceNum, setIsDrawFalse, flipDisplayImageByAxis, setEmptyCanvasSize, getOrCreateSliceBuffer, renderSliceToCanvas, compositeAllLayers) {
73284
73307
  this.dragPrameters = {
73285
73308
  move: 0,
73286
73309
  y: 0,
@@ -73316,6 +73339,7 @@ void main() {
73316
73339
  this.setEmptyCanvasSize = setEmptyCanvasSize;
73317
73340
  this.getOrCreateSliceBuffer = getOrCreateSliceBuffer;
73318
73341
  this.renderSliceToCanvas = renderSliceToCanvas;
73342
+ this.compositeAllLayers = compositeAllLayers;
73319
73343
  this.showDragNumberDiv = createShowSliceNumberDiv();
73320
73344
  this.init();
73321
73345
  }
@@ -73341,6 +73365,7 @@ void main() {
73341
73365
  setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
73342
73366
  getOrCreateSliceBuffer: (axis) => this.getOrCreateSliceBuffer(axis),
73343
73367
  renderSliceToCanvas: (layer, axis, sliceIndex, buffer, targetCtx, w, h) => this.renderSliceToCanvas(layer, axis, sliceIndex, buffer, targetCtx, w, h),
73368
+ compositeAllLayers: () => this.compositeAllLayers(),
73344
73369
  refreshSphereOverlay: () => { var _a; return (_a = this.refreshSphereOverlayCb) === null || _a === void 0 ? void 0 : _a.call(this); },
73345
73370
  }, this.showDragNumberDiv, dragEffectCanvases);
73346
73371
  }
@@ -76468,6 +76493,216 @@ void main() {
76468
76493
  }
76469
76494
  }
76470
76495
 
76496
+ /**
76497
+ * GaussianSmoother — 3D Gaussian smoothing for segmentation masks.
76498
+ *
76499
+ * Applies a separable 3D Gaussian blur to a single label channel within a
76500
+ * MaskVolume, then thresholds the result back to a binary mask. This smooths
76501
+ * jagged edges and fills small holes in segmentation annotations.
76502
+ *
76503
+ * Pure, stateless utility — no DOM/Canvas/GUI dependencies.
76504
+ *
76505
+ * Performance: uses direct typed-array access (bypassing getVoxel/setVoxel
76506
+ * boundary checks) and branch-free convolution for the interior region.
76507
+ */
76508
+ class GaussianSmoother {
76509
+ /**
76510
+ * Smooth a single label channel in-place using separable 3D Gaussian blur.
76511
+ *
76512
+ * Steps:
76513
+ * 1. Extract — create a Float32Array with 1.0 where voxel === channel, 0.0 elsewhere.
76514
+ * 2. Blur — apply separable Gaussian (X → Y → Z) on the float buffer.
76515
+ * 3. Threshold — binarize at 0.5.
76516
+ * 4. Write back — overwrite/erase voxels according to the thresholded result.
76517
+ *
76518
+ * @param volume The MaskVolume to operate on (modified in-place).
76519
+ * @param channel The label value to smooth (must be > 0).
76520
+ * @param sigma Base Gaussian sigma in voxel units (default 1.0).
76521
+ * @param spacing Optional voxel spacing [sx, sy, sz]. When provided, per-axis
76522
+ * sigma is computed as `sigma / spacing[axis]` so that the
76523
+ * physical smoothing radius is isotropic.
76524
+ */
76525
+ static gaussianSmooth3D(volume, channel, sigma = 1.0, spacing) {
76526
+ const dims = volume.getDimensions();
76527
+ const { width, height, depth } = dims;
76528
+ const totalVoxels = width * height * depth;
76529
+ // Direct access to underlying buffer — bypasses getVoxel/setVoxel
76530
+ // boundary checks for ~10x speedup in hot loops.
76531
+ const rawData = volume.getRawData();
76532
+ const channels = volume.getChannels();
76533
+ const bytesPerSlice = volume.getBytesPerSlice();
76534
+ const rowStride = width * channels;
76535
+ // Compute per-axis sigma (normalized to voxel units)
76536
+ const sigmaX = spacing ? sigma / spacing[0] : sigma;
76537
+ const sigmaY = spacing ? sigma / spacing[1] : sigma;
76538
+ const sigmaZ = spacing ? sigma / spacing[2] : sigma;
76539
+ // 1. Extract binary float buffer — direct array access
76540
+ const buffer = new Float32Array(totalVoxels);
76541
+ for (let z = 0; z < depth; z++) {
76542
+ const zOffset = z * bytesPerSlice;
76543
+ for (let y = 0; y < height; y++) {
76544
+ const zyOffset = zOffset + y * rowStride;
76545
+ for (let x = 0; x < width; x++) {
76546
+ const bufIdx = z * height * width + y * width + x;
76547
+ buffer[bufIdx] = rawData[zyOffset + x * channels] === channel ? 1.0 : 0.0;
76548
+ }
76549
+ }
76550
+ }
76551
+ // 2. Separable Gaussian blur: X → Y → Z
76552
+ const kernelX = GaussianSmoother.generateKernel1D(sigmaX);
76553
+ const kernelY = GaussianSmoother.generateKernel1D(sigmaY);
76554
+ const kernelZ = GaussianSmoother.generateKernel1D(sigmaZ);
76555
+ GaussianSmoother.convolve1D(buffer, width, height, depth, 0, kernelX); // X axis
76556
+ GaussianSmoother.convolve1D(buffer, width, height, depth, 1, kernelY); // Y axis
76557
+ GaussianSmoother.convolve1D(buffer, width, height, depth, 2, kernelZ); // Z axis
76558
+ // 3 & 4. Threshold and write back — direct array access
76559
+ for (let z = 0; z < depth; z++) {
76560
+ const zOffset = z * bytesPerSlice;
76561
+ for (let y = 0; y < height; y++) {
76562
+ const zyOffset = zOffset + y * rowStride;
76563
+ for (let x = 0; x < width; x++) {
76564
+ const bufIdx = z * height * width + y * width + x;
76565
+ const smoothed = buffer[bufIdx] >= 0.5 ? 1 : 0;
76566
+ const rawIdx = zyOffset + x * channels;
76567
+ const original = rawData[rawIdx];
76568
+ if (smoothed === 1 && original !== channel) {
76569
+ // Smoothed region expands into this voxel — overwrite
76570
+ rawData[rawIdx] = channel;
76571
+ }
76572
+ else if (smoothed === 0 && original === channel) {
76573
+ // Smoothed region retreats — erase
76574
+ rawData[rawIdx] = 0;
76575
+ }
76576
+ // Otherwise leave unchanged
76577
+ }
76578
+ }
76579
+ }
76580
+ }
76581
+ /**
76582
+ * Generate a normalized 1D Gaussian kernel truncated at ±3σ.
76583
+ *
76584
+ * @param sigma Standard deviation (in voxels). Must be > 0.
76585
+ * @returns Normalized Float32Array of odd length.
76586
+ */
76587
+ static generateKernel1D(sigma) {
76588
+ if (sigma <= 0) {
76589
+ return new Float32Array([1.0]);
76590
+ }
76591
+ const radius = Math.ceil(sigma * 3);
76592
+ const size = 2 * radius + 1;
76593
+ const kernel = new Float32Array(size);
76594
+ const twoSigmaSq = 2 * sigma * sigma;
76595
+ let sum = 0;
76596
+ for (let i = 0; i < size; i++) {
76597
+ const x = i - radius;
76598
+ kernel[i] = Math.exp(-(x * x) / twoSigmaSq);
76599
+ sum += kernel[i];
76600
+ }
76601
+ // Normalize
76602
+ for (let i = 0; i < size; i++) {
76603
+ kernel[i] /= sum;
76604
+ }
76605
+ return kernel;
76606
+ }
76607
+ /**
76608
+ * In-place single-axis convolution with zero-padding at boundaries.
76609
+ *
76610
+ * Uses a 3-segment approach: left boundary (with bounds check),
76611
+ * middle interior (branch-free, ~95% of work), right boundary
76612
+ * (with bounds check). This eliminates per-element branching
76613
+ * in the hot inner loop.
76614
+ *
76615
+ * @param data Flat float buffer (width × height × depth).
76616
+ * @param width X dimension.
76617
+ * @param height Y dimension.
76618
+ * @param depth Z dimension.
76619
+ * @param axis 0 = X, 1 = Y, 2 = Z.
76620
+ * @param kernel 1D convolution kernel (odd length).
76621
+ */
76622
+ static convolve1D(data, width, height, depth, axis, kernel) {
76623
+ const kLen = kernel.length;
76624
+ const radius = (kLen - 1) / 2;
76625
+ // Determine the dimension length along the convolution axis
76626
+ // and the stride between consecutive elements along that axis
76627
+ let axisLen;
76628
+ let stride;
76629
+ let outerCount;
76630
+ if (axis === 0) {
76631
+ // X axis: stride = 1, iterate over all (y, z) lines
76632
+ axisLen = width;
76633
+ stride = 1;
76634
+ outerCount = height * depth;
76635
+ }
76636
+ else if (axis === 1) {
76637
+ // Y axis: stride = width, iterate over all (x, z) lines
76638
+ axisLen = height;
76639
+ stride = width;
76640
+ outerCount = width * depth;
76641
+ }
76642
+ else {
76643
+ // Z axis: stride = width * height, iterate over all (x, y) lines
76644
+ axisLen = depth;
76645
+ stride = width * height;
76646
+ outerCount = width * height;
76647
+ }
76648
+ // Precompute middle segment bounds
76649
+ const midStart = radius;
76650
+ const midEnd = axisLen - radius;
76651
+ // Temporary line buffer to avoid read-after-write issues
76652
+ const line = new Float32Array(axisLen);
76653
+ for (let outer = 0; outer < outerCount; outer++) {
76654
+ // Compute the starting index for this line
76655
+ let lineStart;
76656
+ if (axis === 0) {
76657
+ lineStart = outer * width;
76658
+ }
76659
+ else if (axis === 1) {
76660
+ const z = Math.floor(outer / width);
76661
+ const x = outer % width;
76662
+ lineStart = z * width * height + x;
76663
+ }
76664
+ else {
76665
+ lineStart = outer;
76666
+ }
76667
+ // Read the line
76668
+ for (let i = 0; i < axisLen; i++) {
76669
+ line[i] = data[lineStart + i * stride];
76670
+ }
76671
+ // Left boundary segment (i = 0 .. radius-1): lower bound check
76672
+ for (let i = 0; i < midStart; i++) {
76673
+ let sum = 0;
76674
+ for (let k = 0; k < kLen; k++) {
76675
+ const j = i + k - radius;
76676
+ if (j >= 0) {
76677
+ sum += line[j] * kernel[k];
76678
+ }
76679
+ }
76680
+ data[lineStart + i * stride] = sum;
76681
+ }
76682
+ // Middle segment (i = radius .. axisLen-radius-1): NO bounds check
76683
+ for (let i = midStart; i < midEnd; i++) {
76684
+ let sum = 0;
76685
+ const lineOffset = i - radius;
76686
+ for (let k = 0; k < kLen; k++) {
76687
+ sum += line[lineOffset + k] * kernel[k];
76688
+ }
76689
+ data[lineStart + i * stride] = sum;
76690
+ }
76691
+ // Right boundary segment (i = axisLen-radius .. axisLen-1): upper bound check
76692
+ for (let i = midEnd; i < axisLen; i++) {
76693
+ let sum = 0;
76694
+ for (let k = 0; k < kLen; k++) {
76695
+ const j = i + k - radius;
76696
+ if (j < axisLen) {
76697
+ sum += line[j] * kernel[k];
76698
+ }
76699
+ }
76700
+ data[lineStart + i * stride] = sum;
76701
+ }
76702
+ }
76703
+ }
76704
+ }
76705
+
76471
76706
  /**
76472
76707
  * Manages layer/channel state: active selection, visibility, and channel colors.
76473
76708
  *
@@ -76984,6 +77219,8 @@ void main() {
76984
77219
  * Then update the changedWidth and changedHeight based on the sizeFactor.
76985
77220
  */
76986
77221
  updateOriginAndChangedWH() {
77222
+ const prevW = this.ctx.nrrd_states.image.originWidth;
77223
+ const prevH = this.ctx.nrrd_states.image.originHeight;
76987
77224
  this.ctx.nrrd_states.image.originWidth =
76988
77225
  this.ctx.protectedData.canvases.originCanvas.width;
76989
77226
  this.ctx.nrrd_states.image.originHeight =
@@ -76992,7 +77229,12 @@ void main() {
76992
77229
  // Setting them here would defeat the sizeChanged detection in resizePaintArea,
76993
77230
  // causing canvas elements to keep stale dimensions after axis switches.
76994
77231
  this.resizePaintArea(this.ctx.nrrd_states.view.sizeFactor);
76995
- this.resetPaintAreaUIPosition();
77232
+ // Only re-center when origin dimensions changed (e.g. axis switch).
77233
+ // Contrast toggle doesn't change origin size, so skip to preserve user's pan/zoom.
77234
+ if (prevW !== this.ctx.nrrd_states.image.originWidth ||
77235
+ prevH !== this.ctx.nrrd_states.image.originHeight) {
77236
+ this.resetPaintAreaUIPosition();
77237
+ }
76996
77238
  }
76997
77239
  /**
76998
77240
  * Keep all contrast slice index to same.
@@ -77206,7 +77448,7 @@ void main() {
77206
77448
  // Wire RenderingUtils' setEmptyCanvasSize callback
77207
77449
  this.drawCore.renderer.setEmptyCanvasSize = (axis) => this.setEmptyCanvasSize(axis);
77208
77450
  this.init();
77209
- 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));
77451
+ 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));
77210
77452
  // Inject EventRouter into DragOperator for centralized event handling
77211
77453
  if (this.drawCore.eventRouter) {
77212
77454
  this.dragOperator.setEventRouter(this.drawCore.eventRouter);
@@ -77525,7 +77767,8 @@ void main() {
77525
77767
  getPencilColor() {
77526
77768
  return this.state.gui_states.drawing.color;
77527
77769
  }
77528
- executeAction(action) {
77770
+ executeAction(action, opts) {
77771
+ var _a;
77529
77772
  switch (action) {
77530
77773
  case "undo":
77531
77774
  this.undo();
@@ -77563,6 +77806,48 @@ void main() {
77563
77806
  enableDownload(config);
77564
77807
  break;
77565
77808
  }
77809
+ case "gaussianSmooth": {
77810
+ const layerId = this.state.gui_states.layerChannel.layer;
77811
+ const channel = this.state.gui_states.layerChannel.activeChannel;
77812
+ const volume = this.drawCore.renderer.getVolumeForLayer(layerId);
77813
+ if (volume && channel > 0) {
77814
+ const dims = volume.getDimensions();
77815
+ // 1. Snapshot all Z-slices before mutation (undo support)
77816
+ const deltas = [];
77817
+ for (let z = 0; z < dims.depth; z++) {
77818
+ const sliceData = volume.getSliceUint8(z, "z").data;
77819
+ const hasChannel = sliceData.some((v) => v === channel);
77820
+ if (hasChannel) {
77821
+ deltas.push({
77822
+ layerId,
77823
+ axis: "z",
77824
+ sliceIndex: z,
77825
+ oldSlice: sliceData.slice(),
77826
+ newSlice: new Uint8Array(0), // placeholder, filled after smoothing
77827
+ });
77828
+ }
77829
+ }
77830
+ // 2. Apply smoothing with anisotropic spacing
77831
+ const spacing = this.state.nrrd_states.image.voxelSpacing;
77832
+ const spacingTuple = spacing.length >= 3 ? [spacing[0], spacing[1], spacing[2]] : undefined;
77833
+ const sigma = (_a = opts === null || opts === void 0 ? void 0 : opts.sigma) !== null && _a !== void 0 ? _a : 1.0;
77834
+ GaussianSmoother.gaussianSmooth3D(volume, channel, sigma, spacingTuple);
77835
+ // 3. Capture newSlice data after smoothing and push undo group
77836
+ for (const delta of deltas) {
77837
+ delta.newSlice = volume.getSliceUint8(delta.sliceIndex, "z").data.slice();
77838
+ }
77839
+ if (deltas.length > 0) {
77840
+ this.drawCore.undoManager.pushGroup(deltas);
77841
+ }
77842
+ // 4. Notify backend for each changed slice
77843
+ for (const delta of deltas) {
77844
+ const { data: sliceData, width, height } = volume.getSliceUint8(delta.sliceIndex, "z");
77845
+ this.state.annotationCallbacks.onMaskChanged(sliceData, layerId, channel, delta.sliceIndex, "z", width, height, false);
77846
+ }
77847
+ this.reloadMasksFromVolume();
77848
+ }
77849
+ break;
77850
+ }
77566
77851
  }
77567
77852
  }
77568
77853
  // ═══════════════════════════════════════════════════════════════════════════
@@ -78277,13 +78562,14 @@ void main() {
78277
78562
  }
78278
78563
 
78279
78564
  // import * as kiwrious from "copper3d_plugin_heart_k";
78280
- const REVISION = "v3.2.0-beta";
78565
+ const REVISION = "v3.3.0-beta";
78281
78566
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
78282
78567
 
78283
78568
  exports.CHANNEL_COLORS = CHANNEL_COLORS;
78284
78569
  exports.CHANNEL_HEX_COLORS = CHANNEL_HEX_COLORS;
78285
78570
  exports.CameraViewPoint = CameraViewPoint;
78286
78571
  exports.Copper3dTrackballControls = Copper3dTrackballControls;
78572
+ exports.GaussianSmoother = GaussianSmoother;
78287
78573
  exports.MeshNodeTool = MeshNodeTool;
78288
78574
  exports.NrrdTools = NrrdTools;
78289
78575
  exports.REVISION = REVISION;
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ import { createTexture2D_NRRD } from "./Utils/texture2d";
13
13
  import { configKiwriousHeart } from "./Utils/kiwrious/configKiwrious";
14
14
  import kiwrious from "./Utils/kiwrious/configKiwrious";
15
15
  import { NrrdTools } from "./Utils/segmentation/NrrdTools";
16
+ import { GaussianSmoother } from "./Utils/segmentation/core/GaussianSmoother";
16
17
  import { Copper3dTrackballControls } from "./Controls/Copper3dTrackballControls";
17
18
  import { MeshNodeTool } from "./Utils/MeshNodeTool";
18
19
  import { removeGuiFolderChilden } from "./Utils/segmentation/coreTools/gui";
@@ -24,6 +25,6 @@ import type { ToolMode, IAnnotationCallbacks } from "./Utils/segmentation/core/t
24
25
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
25
26
  import type { LayerId, ChannelValue } from "./Utils/segmentation/core/index";
26
27
  import "./css/style.css";
27
- export declare const REVISION = "v3.2.0-beta";
28
- export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, };
28
+ export declare const REVISION = "v3.3.0-beta";
29
+ export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, GaussianSmoother, };
29
30
  export type { positionType, screenPosType, optsType, nrrdMeshesType, nrrdSliceType, SensorDecodedValue_kiwrious, SensorReadResult_kiwrious, HeartRateResult_kiwrious, loadingBarType, IPaintImage, exportPaintImageType, IOptVTKLoader, ICommXYZ, IGUIStates, IGuiParameterSettings, INrrdStates, NrrdState, GuiState, IGuiMeta, ToolMode, IAnnotationCallbacks, LayerId, ChannelValue, };
package/dist/index.js CHANGED
@@ -14,13 +14,14 @@ import { createTexture2D_NRRD } from "./Utils/texture2d";
14
14
  import { configKiwriousHeart } from "./Utils/kiwrious/configKiwrious";
15
15
  import kiwrious from "./Utils/kiwrious/configKiwrious";
16
16
  import { NrrdTools } from "./Utils/segmentation/NrrdTools";
17
+ import { GaussianSmoother } from "./Utils/segmentation/core/GaussianSmoother";
17
18
  // Phase 7: Segmentation Module - Unified exports
18
19
  import { Copper3dTrackballControls } from "./Controls/Copper3dTrackballControls";
19
20
  import { MeshNodeTool } from "./Utils/MeshNodeTool";
20
21
  import { removeGuiFolderChilden } from "./Utils/segmentation/coreTools/gui";
21
22
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
22
23
  import "./css/style.css";
23
- export const REVISION = "v3.2.0-beta";
24
+ export const REVISION = "v3.3.0-beta";
24
25
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
25
- export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, };
26
+ export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, GaussianSmoother, };
26
27
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EACL,eAAe,EAGf,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAY,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,QAAQ,MAAM,iCAAiC,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,iDAAiD;AAEjD,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAiB5E,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAG3G,OAAO,iBAAiB,CAAC;AAEzB,MAAM,CAAC,MAAM,QAAQ,GAAG,aAAa,CAAC;AAEtC,OAAO,CAAC,GAAG,CACT,mCAAmC,QAAQ,EAAE,EAC7C,8CAA8C,EAC9C,8CAA8C,CAC/C,CAAC;AAEF,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,SAAS,EACT,OAAO,EACP,yBAAyB,EACzB,oBAAoB,EACpB,YAAY,EACZ,QAAQ,EACR,sBAAsB,EACtB,cAAc,EACd,kBAAkB,EAClB,SAAS,EACT,SAAS,GACV,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EACL,eAAe,EAGf,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAY,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,QAAQ,MAAM,iCAAiC,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAC9E,iDAAiD;AAEjD,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAiB5E,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAG3G,OAAO,iBAAiB,CAAC;AAEzB,MAAM,CAAC,MAAM,QAAQ,GAAG,aAAa,CAAC;AAEtC,OAAO,CAAC,GAAG,CACT,mCAAmC,QAAQ,EAAE,EAC7C,8CAA8C,EAC9C,8CAA8C,CAC/C,CAAC;AAEF,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,SAAS,EACT,OAAO,EACP,yBAAyB,EACzB,oBAAoB,EACpB,YAAY,EACZ,QAAQ,EACR,sBAAsB,EACtB,cAAc,EACd,kBAAkB,EAClB,SAAS,EACT,SAAS,EACT,gBAAgB,GACjB,CAAC"}
@@ -18,6 +18,7 @@ export declare class copperScene extends baseScene {
18
18
  loadGltf(url: string, callback?: (content: THREE.Group) => void): void;
19
19
  loadPureGLB(url: string, callback?: (mesh: THREE.Group) => void, opts?: {
20
20
  color: string;
21
+ enhanceMaterial?: boolean;
21
22
  }): void;
22
23
  loadVtk(url: string): void;
23
24
  loadVtks(models: Array<vtkModels>): void;
@@ -18,9 +18,10 @@ export declare class DragOperator {
18
18
  private setEmptyCanvasSize;
19
19
  private getOrCreateSliceBuffer;
20
20
  private renderSliceToCanvas;
21
+ private compositeAllLayers;
21
22
  private refreshSphereOverlayCb;
22
23
  private eventRouter;
23
- constructor(container: HTMLElement, nrrd_sates: NrrdState, gui_states: GuiState, protectedData: IProtected, drawingPrameters: IDrawingEvents, setSyncsliceNum: () => void, setIsDrawFalse: (target: number) => void, flipDisplayImageByAxis: () => void, setEmptyCanvasSize: (axis?: "x" | "y" | "z") => void, getOrCreateSliceBuffer: (axis: "x" | "y" | "z") => ImageData | null, renderSliceToCanvas: (layer: string, axis: "x" | "y" | "z", sliceIndex: number, buffer: ImageData, targetCtx: CanvasRenderingContext2D, scaledWidth: number, scaledHeight: number) => void);
24
+ constructor(container: HTMLElement, nrrd_sates: NrrdState, gui_states: GuiState, protectedData: IProtected, drawingPrameters: IDrawingEvents, setSyncsliceNum: () => void, setIsDrawFalse: (target: number) => void, flipDisplayImageByAxis: () => void, setEmptyCanvasSize: (axis?: "x" | "y" | "z") => void, getOrCreateSliceBuffer: (axis: "x" | "y" | "z") => ImageData | null, renderSliceToCanvas: (layer: string, axis: "x" | "y" | "z", sliceIndex: number, buffer: ImageData, targetCtx: CanvasRenderingContext2D, scaledWidth: number, scaledHeight: number) => void, compositeAllLayers: () => void);
24
25
  private init;
25
26
  setShowDragNumberDiv(sliceIndexContainer: HTMLDivElement): void;
26
27
  /**
@@ -88,7 +88,9 @@ export declare class NrrdTools {
88
88
  getSliderMeta(key: string): IGuiMeta | null;
89
89
  setPencilColor(hex: string): void;
90
90
  getPencilColor(): string;
91
- executeAction(action: "undo" | "redo" | "clearActiveSliceMask" | "clearActiveLayerMask" | "resetZoom" | "downloadCurrentMask"): void;
91
+ executeAction(action: "undo" | "redo" | "clearActiveSliceMask" | "clearActiveLayerMask" | "resetZoom" | "downloadCurrentMask" | "gaussianSmooth", opts?: {
92
+ sigma?: number;
93
+ }): void;
92
94
  undo(): void;
93
95
  redo(): void;
94
96
  enterKeyboardConfig(): void;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * GaussianSmoother — 3D Gaussian smoothing for segmentation masks.
3
+ *
4
+ * Applies a separable 3D Gaussian blur to a single label channel within a
5
+ * MaskVolume, then thresholds the result back to a binary mask. This smooths
6
+ * jagged edges and fills small holes in segmentation annotations.
7
+ *
8
+ * Pure, stateless utility — no DOM/Canvas/GUI dependencies.
9
+ *
10
+ * Performance: uses direct typed-array access (bypassing getVoxel/setVoxel
11
+ * boundary checks) and branch-free convolution for the interior region.
12
+ */
13
+ import type { MaskVolume } from './MaskVolume';
14
+ export declare class GaussianSmoother {
15
+ /**
16
+ * Smooth a single label channel in-place using separable 3D Gaussian blur.
17
+ *
18
+ * Steps:
19
+ * 1. Extract — create a Float32Array with 1.0 where voxel === channel, 0.0 elsewhere.
20
+ * 2. Blur — apply separable Gaussian (X → Y → Z) on the float buffer.
21
+ * 3. Threshold — binarize at 0.5.
22
+ * 4. Write back — overwrite/erase voxels according to the thresholded result.
23
+ *
24
+ * @param volume The MaskVolume to operate on (modified in-place).
25
+ * @param channel The label value to smooth (must be > 0).
26
+ * @param sigma Base Gaussian sigma in voxel units (default 1.0).
27
+ * @param spacing Optional voxel spacing [sx, sy, sz]. When provided, per-axis
28
+ * sigma is computed as `sigma / spacing[axis]` so that the
29
+ * physical smoothing radius is isotropic.
30
+ */
31
+ static gaussianSmooth3D(volume: MaskVolume, channel: number, sigma?: number, spacing?: [number, number, number]): void;
32
+ /**
33
+ * Generate a normalized 1D Gaussian kernel truncated at ±3σ.
34
+ *
35
+ * @param sigma Standard deviation (in voxels). Must be > 0.
36
+ * @returns Normalized Float32Array of odd length.
37
+ */
38
+ static generateKernel1D(sigma: number): Float32Array;
39
+ /**
40
+ * In-place single-axis convolution with zero-padding at boundaries.
41
+ *
42
+ * Uses a 3-segment approach: left boundary (with bounds check),
43
+ * middle interior (branch-free, ~95% of work), right boundary
44
+ * (with bounds check). This eliminates per-element branching
45
+ * in the hot inner loop.
46
+ *
47
+ * @param data Flat float buffer (width × height × depth).
48
+ * @param width X dimension.
49
+ * @param height Y dimension.
50
+ * @param depth Z dimension.
51
+ * @param axis 0 = X, 1 = Y, 2 = Z.
52
+ * @param kernel 1D convolution kernel (odd length).
53
+ */
54
+ private static convolve1D;
55
+ }
@@ -250,6 +250,15 @@ export declare class MaskVolume {
250
250
  * @returns Channel count (≥ 1).
251
251
  */
252
252
  getChannels(): number;
253
+ /**
254
+ * Return the number of bytes in one complete z-slice.
255
+ *
256
+ * Equal to `width × height × channels`. Useful for direct buffer
257
+ * index arithmetic in performance-critical code paths.
258
+ *
259
+ * @returns Bytes per z-slice.
260
+ */
261
+ getBytesPerSlice(): number;
253
262
  /**
254
263
  * Return total memory used by the backing buffer, in bytes.
255
264
  *
@@ -26,10 +26,6 @@ export declare class DragSliceTool extends BaseTool {
26
26
  setShowDragNumberDiv(div: HTMLDivElement): void;
27
27
  updateIndex(move: number): void;
28
28
  private drawDragSlice;
29
- /**
30
- * Composite all visible layer canvases to the master display canvas.
31
- */
32
- private compositeAllLayers;
33
29
  private cleanCanvases;
34
30
  updateShowNumDiv(contrastNum: number): void;
35
31
  updateCurrentContrastSlice(): any;