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
@@ -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(0x202020, 0.3);
49808
- this.directionalLight = new DirectionalLight(0xffffff, 0.3);
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
- const hemiLight = new HemisphereLight();
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.0,
50632
- ambientIntensity: 0.3,
50633
- ambientColor: 0x202020,
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
- }, // called when loading is in progresses
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
- this.resetPaintAreaUIPosition();
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.2.0-beta";
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 };