copper3d 3.4.5 → 3.4.7

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 (31) hide show
  1. package/dist/Utils/segmentation/DrawToolCore.d.ts +19 -2
  2. package/dist/Utils/segmentation/DrawToolCore.js +58 -13
  3. package/dist/Utils/segmentation/DrawToolCore.js.map +1 -1
  4. package/dist/Utils/segmentation/NrrdTools.d.ts +0 -1
  5. package/dist/Utils/segmentation/NrrdTools.js +2 -7
  6. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  7. package/dist/Utils/segmentation/core/types.d.ts +0 -1
  8. package/dist/Utils/segmentation/core/types.js.map +1 -1
  9. package/dist/Utils/segmentation/coreTools/gui.d.ts +0 -1
  10. package/dist/Utils/segmentation/coreTools/gui.js +21 -29
  11. package/dist/Utils/segmentation/coreTools/gui.js.map +1 -1
  12. package/dist/Utils/segmentation/tools/DrawingTool.d.ts +18 -2
  13. package/dist/Utils/segmentation/tools/DrawingTool.js +60 -21
  14. package/dist/Utils/segmentation/tools/DrawingTool.js.map +1 -1
  15. package/dist/Utils/segmentation/tools/ToolHost.d.ts +2 -3
  16. package/dist/Utils/utils.d.ts +0 -1
  17. package/dist/Utils/utils.js +0 -80
  18. package/dist/Utils/utils.js.map +1 -1
  19. package/dist/bundle.esm.js +141 -149
  20. package/dist/bundle.umd.js +141 -149
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/types/Utils/segmentation/DrawToolCore.d.ts +19 -2
  24. package/dist/types/Utils/segmentation/NrrdTools.d.ts +0 -1
  25. package/dist/types/Utils/segmentation/core/types.d.ts +0 -1
  26. package/dist/types/Utils/segmentation/coreTools/gui.d.ts +0 -1
  27. package/dist/types/Utils/segmentation/tools/DrawingTool.d.ts +18 -2
  28. package/dist/types/Utils/segmentation/tools/ToolHost.d.ts +2 -3
  29. package/dist/types/Utils/utils.d.ts +0 -1
  30. package/dist/types/index.d.ts +1 -1
  31. package/package.json +1 -1
@@ -40614,86 +40614,6 @@ void main() {
40614
40614
  }
40615
40615
  return url;
40616
40616
  }
40617
- function switchEraserSize(size, urls) {
40618
- let url = "";
40619
- if (!!urls && urls.length > 0) {
40620
- if (size <= 3) {
40621
- url = `url(${urls[0]}) 3 3, crosshair`;
40622
- }
40623
- else if (3 < size && size <= 8) {
40624
- url = `url(${urls[1]}) 8 8, crosshair`;
40625
- }
40626
- else if (8 < size && size <= 13) {
40627
- url = `url(${urls[2]}) 13 13, crosshair`;
40628
- }
40629
- else if (13 < size && size <= 18) {
40630
- url = `url(${urls[3]}) 18 18, crosshair`;
40631
- }
40632
- else if (18 < size && size <= 23) {
40633
- url = `url(${urls[4]}) 23 23, crosshair`;
40634
- }
40635
- else if (23 < size && size <= 28) {
40636
- url = `url(${urls[5]}) 28 28, crosshair`;
40637
- }
40638
- else if (28 < size && size <= 33) {
40639
- url = `url(${urls[6]}) 33 33, crosshair`;
40640
- }
40641
- else if (33 < size && size <= 38) {
40642
- url = `url(${urls[7]}) 38 38, crosshair`;
40643
- }
40644
- else if (38 < size && size <= 43) {
40645
- url = `url(${urls[8]}) 43 43, crosshair`;
40646
- }
40647
- else if (43 < size && size <= 48) {
40648
- url = `url(${urls[9]}) 48 48, crosshair`;
40649
- }
40650
- else if (48 < size && size <= 53) {
40651
- url = `url(${urls[10]}) 52 52, crosshair`;
40652
- }
40653
- else {
40654
- url = `url(${urls[11]}) 52 52, crosshair`;
40655
- }
40656
- }
40657
- else {
40658
- if (size <= 3) {
40659
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_3.png) 3 3, crosshair`;
40660
- }
40661
- else if (3 < size && size <= 8) {
40662
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_8.png) 8 8, crosshair`;
40663
- }
40664
- else if (8 < size && size <= 13) {
40665
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_13.png) 13 13, crosshair`;
40666
- }
40667
- else if (13 < size && size <= 18) {
40668
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_18.png) 18 18, crosshair`;
40669
- }
40670
- else if (18 < size && size <= 23) {
40671
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_23.png) 23 23, crosshair`;
40672
- }
40673
- else if (23 < size && size <= 28) {
40674
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_28.png) 28 28, crosshair`;
40675
- }
40676
- else if (28 < size && size <= 33) {
40677
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_33.png) 33 33, crosshair`;
40678
- }
40679
- else if (33 < size && size <= 38) {
40680
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_38.png) 38 38, crosshair`;
40681
- }
40682
- else if (38 < size && size <= 43) {
40683
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_43.png) 43 43, crosshair`;
40684
- }
40685
- else if (43 < size && size <= 48) {
40686
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_48.png) 48 48, crosshair`;
40687
- }
40688
- else if (48 < size && size <= 53) {
40689
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_52.png) 52 52, crosshair`;
40690
- }
40691
- else {
40692
- url = `url(https://raw.githubusercontent.com/LinkunGao/copper3d-datasets/main/icons/eraser/circular-cursor_52.png) 52 52, crosshair`;
40693
- }
40694
- }
40695
- return url;
40696
- }
40697
40617
  function throttle(callback, wait) {
40698
40618
  let start = 0;
40699
40619
  return function (event) {
@@ -73010,10 +72930,7 @@ void main() {
73010
72930
  .name("BrushAndEraserSize")
73011
72931
  .min(5)
73012
72932
  .max(50)
73013
- .step(1)
73014
- .onChange(() => {
73015
- updateGuiBrushAndEraserSize();
73016
- });
72933
+ .step(1);
73017
72934
  actionsFolder.add(configs.gui_states.mode, "eraser").onChange((value) => {
73018
72935
  updateGuiEraserState();
73019
72936
  });
@@ -73141,37 +73058,33 @@ void main() {
73141
73058
  configs.gui_states.viewConfig.readyToUpdate = true;
73142
73059
  });
73143
73060
  actionsFolder.open();
73144
- const updateGuiBrushAndEraserSize = () => {
73145
- if (configs.gui_states.mode.eraser) {
73146
- configs.eraserUrls.length > 0
73147
- ? (configs.drawingCanvas.style.cursor = switchEraserSize(configs.gui_states.drawing.brushAndEraserSize, configs.eraserUrls))
73148
- : (configs.drawingCanvas.style.cursor = switchEraserSize(configs.gui_states.drawing.brushAndEraserSize));
73149
- }
73150
- };
73151
- const updatePencilState = () => {
73152
- if (configs.gui_states.mode.pencil) {
73153
- // add canvas brush circle move event listeners
73154
- configs.drawingCanvas.removeEventListener("mouseover", configs.drawingPrameters.handleOnDrawingBrushCricleMove);
73155
- configs.drawingCanvas.removeEventListener("mouseout", configs.drawingPrameters.handleOnDrawingBrushCricleMove);
73061
+ // Add/remove the hover-tracking listeners that feed the brush/eraser preview
73062
+ // ring (mouseOver position). Present for brush & eraser, removed for pencil.
73063
+ const setBrushHoverTracking = (enabled) => {
73064
+ const handler = configs.drawingPrameters.handleOnDrawingBrushCricleMove;
73065
+ if (enabled) {
73066
+ configs.drawingCanvas.addEventListener("mouseover", handler);
73067
+ configs.drawingCanvas.addEventListener("mouseout", handler);
73156
73068
  }
73157
73069
  else {
73158
- // add canvas brush circle move event listeners
73159
- configs.drawingCanvas.addEventListener("mouseover", configs.drawingPrameters.handleOnDrawingBrushCricleMove);
73160
- configs.drawingCanvas.addEventListener("mouseout", configs.drawingPrameters.handleOnDrawingBrushCricleMove);
73070
+ configs.drawingCanvas.removeEventListener("mouseover", handler);
73071
+ configs.drawingCanvas.removeEventListener("mouseout", handler);
73161
73072
  }
73073
+ };
73074
+ const updatePencilState = () => {
73075
+ // Pencil has no preview ring; brush keeps the hover-tracking listeners.
73076
+ setBrushHoverTracking(!configs.gui_states.mode.pencil);
73162
73077
  configs.drawingCanvas.style.cursor = configs.gui_states.viewConfig.defaultPaintCursor;
73163
73078
  };
73164
73079
  const updateGuiEraserState = () => {
73165
- // configs.gui_states.mode.eraser = value;
73080
+ // The eraser shows a dashed preview ring (rendered in DrawToolCore.start),
73081
+ // so it needs the same hover-tracking as the brush. The canvas keeps the
73082
+ // default paint cursor; the ring conveys size (resize via Shift+wheel).
73166
73083
  if (configs.gui_states.mode.eraser) {
73167
- configs.eraserUrls.length > 0
73168
- ? (configs.drawingCanvas.style.cursor = switchEraserSize(configs.gui_states.drawing.brushAndEraserSize, configs.eraserUrls))
73169
- : (configs.drawingCanvas.style.cursor = switchEraserSize(configs.gui_states.drawing.brushAndEraserSize));
73170
- }
73171
- else {
73172
- configs.drawingCanvas.style.cursor =
73173
- configs.gui_states.viewConfig.defaultPaintCursor;
73084
+ setBrushHoverTracking(true);
73174
73085
  }
73086
+ configs.drawingCanvas.style.cursor =
73087
+ configs.gui_states.viewConfig.defaultPaintCursor;
73175
73088
  };
73176
73089
  const updateGuiSphereState = () => {
73177
73090
  var _a, _b, _c, _d;
@@ -73227,7 +73140,6 @@ void main() {
73227
73140
  min: 5,
73228
73141
  max: 50,
73229
73142
  step: 1,
73230
- onChange: updateGuiBrushAndEraserSize,
73231
73143
  },
73232
73144
  eraser: {
73233
73145
  name: "Eraser",
@@ -74956,18 +74868,11 @@ void main() {
74956
74868
  this.drawingLines = [];
74957
74869
  this.isPainting = true;
74958
74870
  this.ctx.protectedData.isDrawing = true;
74959
- // Set cursor based on mode
74960
- if (this.ctx.gui_states.mode.eraser) {
74961
- const urls = this.callbacks.getEraserUrls();
74962
- this.ctx.protectedData.canvases.drawingCanvas.style.cursor =
74963
- urls.length > 0
74964
- ? switchEraserSize(this.ctx.gui_states.drawing.brushAndEraserSize, urls)
74965
- : switchEraserSize(this.ctx.gui_states.drawing.brushAndEraserSize);
74966
- }
74967
- else {
74968
- this.ctx.protectedData.canvases.drawingCanvas.style.cursor =
74969
- this.ctx.gui_states.viewConfig.defaultPaintCursor;
74970
- }
74871
+ // Brush and eraser both show their size via the dashed/solid preview ring
74872
+ // rendered in DrawToolCore.start(); the canvas keeps the default paint
74873
+ // cursor underneath. (The eraser previously swapped in a sized PNG cursor.)
74874
+ this.ctx.protectedData.canvases.drawingCanvas.style.cursor =
74875
+ this.ctx.gui_states.viewConfig.defaultPaintCursor;
74971
74876
  // Record draw start position
74972
74877
  this.ctx.nrrd_states.interaction.drawStartPos.x = e.offsetX;
74973
74878
  this.ctx.nrrd_states.interaction.drawStartPos.y = e.offsetY;
@@ -75043,9 +74948,15 @@ void main() {
75043
74948
  this.refreshLayerFromVolume();
75044
74949
  }
75045
74950
  else {
75046
- // Brush mode: voxels already written during mousemove
75047
- // Just re-render final state and composite
74951
+ // Brush mode: voxels already written directly into the MaskVolume
74952
+ // during mousemove, so no canvas→voxel bake (syncLayerSliceData) is
74953
+ // needed. But we MUST still notify the backend — unlike pencil/eraser
74954
+ // (which go through syncLayerSliceData) the brush path never fires
74955
+ // onMaskChanged, so brush strokes were silently never persisted via
74956
+ // useReplaceMask. Re-render the final state, then fire onMaskChanged
74957
+ // for the current slice (same pattern as SphereBrushTool.refreshDisplay).
75048
74958
  this.refreshLayerFromVolume();
74959
+ this.notifyMaskChanged();
75049
74960
  }
75050
74961
  }
75051
74962
  else {
@@ -75099,20 +75010,37 @@ void main() {
75099
75010
  return handler;
75100
75011
  }
75101
75012
  /**
75102
- * Render brush circle preview on the drawing context.
75013
+ * Render the size-preview ring for the brush / eraser on the drawing context.
75014
+ *
75015
+ * - Brush: solid outline in the current brush colour.
75016
+ * - Eraser: red dashed outline (matches the sphereEraser preview), so the
75017
+ * eraser no longer needs a sized PNG cursor — the ring tracks the live
75018
+ * `brushAndEraserSize` and is what the user resizes with Shift+wheel.
75019
+ *
75020
+ * Only the pencil has no preview ring (its stroke width is fixed).
75103
75021
  */
75104
- renderBrushPreview(ctx, width, height) {
75022
+ renderBrushPreview(ctx, _width, _height) {
75105
75023
  if (this.ctx.gui_states.mode.pencil ||
75106
- this.ctx.gui_states.mode.eraser ||
75107
75024
  !this.ctx.nrrd_states.interaction.mouseOver) {
75108
75025
  return;
75109
75026
  }
75110
- ctx.clearRect(0, 0, width, height);
75111
- ctx.fillStyle = this.ctx.gui_states.drawing.brushColor;
75027
+ const isEraser = this.ctx.gui_states.mode.eraser;
75028
+ // No clearRect here: DrawToolCore.start() clears drawingCtx once per frame
75029
+ // before compositing, and this ring is now drawn on top of the mask so it
75030
+ // stays visible during an active stroke (e.g. while Shift+wheel resizing).
75031
+ ctx.save();
75112
75032
  ctx.beginPath();
75113
75033
  ctx.arc(this.ctx.nrrd_states.interaction.mouseOverX, this.ctx.nrrd_states.interaction.mouseOverY, this.ctx.gui_states.drawing.brushAndEraserSize / 2 + 1, 0, Math.PI * 2);
75114
- ctx.strokeStyle = this.ctx.gui_states.drawing.brushColor;
75034
+ if (isEraser) {
75035
+ ctx.strokeStyle = "#ff4444";
75036
+ ctx.lineWidth = 2;
75037
+ ctx.setLineDash([4, 4]);
75038
+ }
75039
+ else {
75040
+ ctx.strokeStyle = this.ctx.gui_states.drawing.brushColor;
75041
+ }
75115
75042
  ctx.stroke();
75043
+ ctx.restore();
75116
75044
  }
75117
75045
  // ── Brush mode: direct voxel write ────────────────────────────
75118
75046
  /**
@@ -75303,6 +75231,30 @@ void main() {
75303
75231
  this.callbacks.compositeAllLayers();
75304
75232
  this.ctx.protectedData.mainPreSlices.mesh.material.map.needsUpdate = true;
75305
75233
  }
75234
+ /**
75235
+ * Notify the backend of the brush stroke by firing onMaskChanged for the
75236
+ * current slice. Brush writes voxels straight into the MaskVolume (no canvas
75237
+ * bake), so we read the authoritative slice back from the volume — mirrors
75238
+ * SphereBrushTool.refreshDisplay's notify path. Brush only ever touches the
75239
+ * current view slice (canvasToVoxel3D fixes the slice axis), so a single
75240
+ * slice notification is sufficient.
75241
+ */
75242
+ notifyMaskChanged() {
75243
+ if (this.ctx.nrrd_states.flags.loadingMaskData)
75244
+ return;
75245
+ try {
75246
+ const layer = this.ctx.gui_states.layerChannel.layer;
75247
+ const vol = this.callbacks.getVolumeForLayer(layer);
75248
+ const channel = this.ctx.gui_states.layerChannel.activeChannel || 1;
75249
+ const axis = this.ctx.protectedData.axis;
75250
+ const sliceIndex = this.ctx.nrrd_states.view.currentSliceIndex;
75251
+ const { data: sliceData, width, height } = vol.getSliceUint8(sliceIndex, axis);
75252
+ this.ctx.callbacks.onMaskChanged(sliceData, layer, channel, sliceIndex, axis, width, height, false);
75253
+ }
75254
+ catch (_a) {
75255
+ // Volume not ready — skip notification
75256
+ }
75257
+ }
75306
75258
  // ── Private helpers ────────────────────────────────────────
75307
75259
  /** Capture pre-draw slice snapshot for undo */
75308
75260
  capturePreDrawSnapshot() {
@@ -76229,7 +76181,6 @@ void main() {
76229
76181
  handleOnContrastMouseUp: (ev) => { },
76230
76182
  handleOnContrastMouseLeave: (ev) => { },
76231
76183
  };
76232
- this.eraserUrls = [];
76233
76184
  this.pencilUrls = [];
76234
76185
  this.undoManager = new UndoManager();
76235
76186
  /** Slice index recorded when paintOnCanvas() starts, guards stale-click */
@@ -76304,7 +76255,6 @@ void main() {
76304
76255
  filterDrawedImage: (axis, index) => this.renderer.filterDrawedImage(axis, index),
76305
76256
  getVolumeForLayer: (layer) => this.renderer.getVolumeForLayer(layer),
76306
76257
  pushUndoDelta: (delta) => this.undoManager.push(delta),
76307
- getEraserUrls: () => this.eraserUrls,
76308
76258
  renderSliceToCanvas: (layer, axis, sliceIndex, buffer, ctx, w, h) => this.renderer.renderSliceToCanvas(layer, axis, sliceIndex, buffer, ctx, w, h),
76309
76259
  getOrCreateSliceBuffer: (axis) => this.renderer.getOrCreateSliceBuffer(axis),
76310
76260
  });
@@ -76316,7 +76266,6 @@ void main() {
76316
76266
  getOrCreateSliceBuffer: (axis) => this.renderer.getOrCreateSliceBuffer(axis),
76317
76267
  setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
76318
76268
  reloadMasksFromVolume: () => this.reloadMasksFromVolume(),
76319
- getEraserUrls: () => this.eraserUrls,
76320
76269
  });
76321
76270
  }
76322
76271
  initDrawToolCore() {
@@ -76471,6 +76420,12 @@ void main() {
76471
76420
  });
76472
76421
  // Register wheel handler with EventRouter
76473
76422
  this.eventRouter.setWheelHandler((e) => {
76423
+ // Shift + wheel resizes the brush/eraser (like sphereBrush/sphereEraser
76424
+ // radius), independent of whether the left button is held. Consumes the
76425
+ // event so it never falls through to zoom/slice. Pencil is excluded even
76426
+ // though it also uses Shift — see tryBrushEraserResizeWheel.
76427
+ if (this.tryBrushEraserResizeWheel(e))
76428
+ return;
76474
76429
  if (this.activeWheelMode === 'zoom') {
76475
76430
  this.drawingPrameters.handleMouseZoomSliceWheel(e);
76476
76431
  }
@@ -76484,8 +76439,47 @@ void main() {
76484
76439
  // Bind all event listeners
76485
76440
  this.eventRouter.bindAll();
76486
76441
  }
76487
- setEraserUrls(urls) {
76488
- this.eraserUrls = urls;
76442
+ /**
76443
+ * Shift + mouse-wheel resize for the brush and eraser tools.
76444
+ *
76445
+ * Mirrors the sphereBrush/sphereEraser "scroll to change radius" UX so the
76446
+ * user no longer has to reach for the B&E Size slider mid-annotation. Active
76447
+ * whenever the EventRouter is in `draw` mode (i.e. Shift is held — with or
76448
+ * without the left button down) and the current tool is brush or eraser.
76449
+ *
76450
+ * Pencil is deliberately excluded: it also draws with Shift, but its stroke
76451
+ * width is fixed and unrelated to `brushAndEraserSize`. Sphere-family tools
76452
+ * keep their own wheel-radius handler (`activeWheelMode === 'sphereBrush'`).
76453
+ *
76454
+ * `gui_states.mode` is the source of truth for the active tool here, not
76455
+ * `eventRouter.getGuiTool()` — the latter is only updated for sphere tools.
76456
+ *
76457
+ * @returns true if the event was handled (and should not fall through to
76458
+ * the zoom/slice/sphere wheel handlers).
76459
+ */
76460
+ tryBrushEraserResizeWheel(e) {
76461
+ // `draw` mode means Shift is held and neither crosshair nor contrast is
76462
+ // active (EventRouter only enters it for drawing tools under those rules),
76463
+ // so it doubles as the "Shift held, not blocked" gate.
76464
+ if (this.eventRouter.getMode() !== 'draw')
76465
+ return false;
76466
+ const mode = this.state.gui_states.mode;
76467
+ // Brush = no flag set; eraser = mode.eraser. Exclude pencil and all
76468
+ // sphere-family tools.
76469
+ if (mode.pencil || mode.sphere || mode.sphereBrush || mode.sphereEraser) {
76470
+ return false;
76471
+ }
76472
+ e.preventDefault();
76473
+ const drawing = this.state.gui_states.drawing;
76474
+ // Step/bounds match the B&E Size slider config (min 5, max 50, step 1).
76475
+ const next = Math.max(5, Math.min(50, drawing.brushAndEraserSize + (e.deltaY < 0 ? 1 : -1)));
76476
+ if (next === drawing.brushAndEraserSize)
76477
+ return true;
76478
+ drawing.brushAndEraserSize = next;
76479
+ // Both brush and eraser draw a size-preview ring in start() that reads
76480
+ // brushAndEraserSize live, so the new size shows up on the next frame with
76481
+ // no extra work here.
76482
+ return true;
76489
76483
  }
76490
76484
  setPencilIconUrls(urls) {
76491
76485
  this.pencilUrls = urls;
@@ -76654,6 +76648,7 @@ void main() {
76654
76648
  this.state.protectedData.ctxes.drawingCtx.clearRect(0, 0, this.state.nrrd_states.view.changedWidth, this.state.nrrd_states.view.changedHeight);
76655
76649
  this.state.protectedData.ctxes.drawingCtx.globalAlpha =
76656
76650
  this.state.gui_states.drawing.globalAlpha;
76651
+ const currentMode = this.eventRouter.getMode();
76657
76652
  if (this.state.protectedData.isDrawing) {
76658
76653
  this.state.protectedData.ctxes.drawingLayerMasterCtx.lineCap = "round";
76659
76654
  this.state.protectedData.ctxes.drawingLayerMasterCtx.globalAlpha = 1;
@@ -76662,16 +76657,18 @@ void main() {
76662
76657
  target.ctx.globalAlpha = 1;
76663
76658
  }
76664
76659
  }
76665
- else {
76666
- const currentMode = this.eventRouter.getMode();
76667
- if (currentMode === 'draw') {
76668
- this.drawingTool.renderBrushPreview(this.state.protectedData.ctxes.drawingCtx, this.state.nrrd_states.view.changedWidth, this.state.nrrd_states.view.changedHeight);
76669
- }
76670
- else if (currentMode === 'crosshair' || this.eventRouter.isCrosshairEnabled()) {
76671
- this.crosshairTool.renderCrosshair(this.state.protectedData.ctxes.drawingCtx, this.state.nrrd_states.view.changedWidth, this.state.nrrd_states.view.changedHeight);
76672
- }
76660
+ else if (currentMode === 'crosshair' || this.eventRouter.isCrosshairEnabled()) {
76661
+ this.crosshairTool.renderCrosshair(this.state.protectedData.ctxes.drawingCtx, this.state.nrrd_states.view.changedWidth, this.state.nrrd_states.view.changedHeight);
76673
76662
  }
76674
76663
  this.state.protectedData.ctxes.drawingCtx.drawImage(this.state.protectedData.canvases.drawingCanvasLayerMaster, 0, 0);
76664
+ // Brush/eraser size ring is drawn AFTER the mask composite so it stays
76665
+ // visible during an active stroke (not just on hover) — e.g. while the
76666
+ // user resizes with Shift+wheel mid-paint. Gated on draw mode so it only
76667
+ // shows for brush/eraser with Shift held (renderBrushPreview itself
76668
+ // skips pencil and the no-hover case).
76669
+ if (currentMode === 'draw') {
76670
+ this.drawingTool.renderBrushPreview(this.state.protectedData.ctxes.drawingCtx);
76671
+ }
76675
76672
  if (this.state.gui_states.mode.sphere
76676
76673
  || this.state.gui_states.mode.sphereBrush
76677
76674
  || this.state.gui_states.mode.sphereEraser) {
@@ -78383,7 +78380,6 @@ void main() {
78383
78380
  nrrd_states: this.state.nrrd_states,
78384
78381
  drawingCanvas: this.state.protectedData.canvases.drawingCanvas,
78385
78382
  drawingPrameters: this.drawCore.drawingPrameters,
78386
- eraserUrls: this.drawCore.eraserUrls,
78387
78383
  pencilUrls: this.drawCore.pencilUrls,
78388
78384
  mainPreSlices: this.state.protectedData.mainPreSlices,
78389
78385
  protectedData: this.state.protectedData,
@@ -78416,7 +78412,6 @@ void main() {
78416
78412
  this.guiCallbacks = {
78417
78413
  updatePencilState: this.guiParameterSettings.pencil.onChange,
78418
78414
  updateEraserState: this.guiParameterSettings.eraser.onChange,
78419
- updateBrushAndEraserSize: this.guiParameterSettings.brushAndEraserSize.onChange,
78420
78415
  updateSphereState: this.guiParameterSettings.sphere.onChange,
78421
78416
  updateCalDistance: this.guiParameterSettings.activeSphereType.onChange,
78422
78417
  updateWindowHigh: this.guiParameterSettings.windowHigh.onChange,
@@ -78529,9 +78524,9 @@ void main() {
78529
78524
  return this.state.gui_states.drawing.globalAlpha;
78530
78525
  }
78531
78526
  setBrushSize(size) {
78532
- var _a;
78527
+ // No cursor side-effect needed: the brush/eraser preview ring reads this
78528
+ // value live each frame (see DrawingTool.renderBrushPreview).
78533
78529
  this.state.gui_states.drawing.brushAndEraserSize = Math.max(5, Math.min(50, size));
78534
- (_a = this.guiCallbacks) === null || _a === void 0 ? void 0 : _a.updateBrushAndEraserSize();
78535
78530
  }
78536
78531
  getBrushSize() {
78537
78532
  return this.state.gui_states.drawing.brushAndEraserSize;
@@ -78766,9 +78761,6 @@ void main() {
78766
78761
  enableContrastDragEvents(callback) {
78767
78762
  this.drawCore.setupConrastEvents(callback);
78768
78763
  }
78769
- setEraserUrls(urls) {
78770
- this.drawCore.setEraserUrls(urls);
78771
- }
78772
78764
  setPencilIconUrls(urls) {
78773
78765
  this.drawCore.setPencilIconUrls(urls);
78774
78766
  }
@@ -79458,7 +79450,7 @@ void main() {
79458
79450
  }
79459
79451
 
79460
79452
  // import * as kiwrious from "copper3d_plugin_heart_k";
79461
- const REVISION = "v3.4.5-beta";
79453
+ const REVISION = "v3.4.7-beta";
79462
79454
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
79463
79455
 
79464
79456
  exports.CHANNEL_COLORS = CHANNEL_COLORS;
package/dist/index.d.ts CHANGED
@@ -25,6 +25,6 @@ import type { ToolMode, IAnnotationCallbacks } from "./Utils/segmentation/core/t
25
25
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
26
26
  import type { LayerId, ChannelValue } from "./Utils/segmentation/core/index";
27
27
  import "./css/style.css";
28
- export declare const REVISION = "v3.4.5-beta";
28
+ export declare const REVISION = "v3.4.7-beta";
29
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, };
30
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
@@ -21,7 +21,7 @@ import { MeshNodeTool } from "./Utils/MeshNodeTool";
21
21
  import { removeGuiFolderChilden } from "./Utils/segmentation/coreTools/gui";
22
22
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
23
23
  import "./css/style.css";
24
- export const REVISION = "v3.4.5-beta";
24
+ export const REVISION = "v3.4.7-beta";
25
25
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
26
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, };
27
27
  //# sourceMappingURL=index.js.map
@@ -30,7 +30,6 @@ export declare class DrawToolCore {
30
30
  readonly renderer: RenderingUtils;
31
31
  drawingPrameters: IDrawingEvents;
32
32
  contrastEventPrameters: IContrastEvents;
33
- eraserUrls: string[];
34
33
  pencilUrls: string[];
35
34
  undoManager: UndoManager;
36
35
  eventRouter: EventRouter;
@@ -59,7 +58,25 @@ export declare class DrawToolCore {
59
58
  constructor(container: HTMLElement, state: CanvasState);
60
59
  private initTools;
61
60
  private initDrawToolCore;
62
- setEraserUrls(urls: string[]): void;
61
+ /**
62
+ * Shift + mouse-wheel resize for the brush and eraser tools.
63
+ *
64
+ * Mirrors the sphereBrush/sphereEraser "scroll to change radius" UX so the
65
+ * user no longer has to reach for the B&E Size slider mid-annotation. Active
66
+ * whenever the EventRouter is in `draw` mode (i.e. Shift is held — with or
67
+ * without the left button down) and the current tool is brush or eraser.
68
+ *
69
+ * Pencil is deliberately excluded: it also draws with Shift, but its stroke
70
+ * width is fixed and unrelated to `brushAndEraserSize`. Sphere-family tools
71
+ * keep their own wheel-radius handler (`activeWheelMode === 'sphereBrush'`).
72
+ *
73
+ * `gui_states.mode` is the source of truth for the active tool here, not
74
+ * `eventRouter.getGuiTool()` — the latter is only updated for sphere tools.
75
+ *
76
+ * @returns true if the event was handled (and should not fall through to
77
+ * the zoom/slice/sphere wheel handlers).
78
+ */
79
+ private tryBrushEraserResizeWheel;
63
80
  setPencilIconUrls(urls: string[]): void;
64
81
  private setCurrentLayer;
65
82
  draw(opts?: IDrawOpts): void;
@@ -110,7 +110,6 @@ export declare class NrrdTools {
110
110
  setBaseDrawDisplayCanvasesSize(size: number): void;
111
111
  setDisplaySliceIndexPanel(panel: HTMLDivElement): void;
112
112
  enableContrastDragEvents(callback: (step: number, towards: "horizental" | "vertical") => void): void;
113
- setEraserUrls(urls: string[]): void;
114
113
  setPencilIconUrls(urls: string[]): void;
115
114
  /**
116
115
  * Switch all contrast slices' orientation
@@ -287,7 +287,6 @@ export interface IGuiParameterSettings {
287
287
  min: number;
288
288
  max: number;
289
289
  step: number;
290
- onChange: () => void;
291
290
  };
292
291
  eraser: {
293
292
  name: "Eraser";
@@ -11,7 +11,6 @@ interface IConfigGUI {
11
11
  drawingCanvas: HTMLCanvasElement;
12
12
  drawingPrameters: IDrawingEvents;
13
13
  protectedData: IProtected;
14
- eraserUrls: string[];
15
14
  pencilUrls: string[];
16
15
  getVolumeForLayer: (layer: string) => any;
17
16
  mainPreSlices: any;
@@ -65,9 +65,16 @@ export declare class DrawingTool extends BaseTool {
65
65
  */
66
66
  createBrushTrackingHandler(): (e: MouseEvent) => void;
67
67
  /**
68
- * Render brush circle preview on the drawing context.
68
+ * Render the size-preview ring for the brush / eraser on the drawing context.
69
+ *
70
+ * - Brush: solid outline in the current brush colour.
71
+ * - Eraser: red dashed outline (matches the sphereEraser preview), so the
72
+ * eraser no longer needs a sized PNG cursor — the ring tracks the live
73
+ * `brushAndEraserSize` and is what the user resizes with Shift+wheel.
74
+ *
75
+ * Only the pencil has no preview ring (its stroke width is fixed).
69
76
  */
70
- renderBrushPreview(ctx: CanvasRenderingContext2D, width: number, height: number): void;
77
+ renderBrushPreview(ctx: CanvasRenderingContext2D, _width?: number, _height?: number): void;
71
78
  /**
72
79
  * Convert display (zoomed) coordinates to 3D voxel coordinates.
73
80
  * Same logic as SphereBrushTool.canvasToVoxelCenter.
@@ -94,6 +101,15 @@ export declare class DrawingTool extends BaseTool {
94
101
  * then composite all layers to master.
95
102
  */
96
103
  private refreshLayerFromVolume;
104
+ /**
105
+ * Notify the backend of the brush stroke by firing onMaskChanged for the
106
+ * current slice. Brush writes voxels straight into the MaskVolume (no canvas
107
+ * bake), so we read the authoritative slice back from the volume — mirrors
108
+ * SphereBrushTool.refreshDisplay's notify path. Brush only ever touches the
109
+ * current view slice (canvasToVoxel3D fixes the slice axis), so a single
110
+ * slice notification is sufficient.
111
+ */
112
+ private notifyMaskChanged;
97
113
  /** Capture pre-draw slice snapshot for undo */
98
114
  private capturePreDrawSnapshot;
99
115
  /** Push delta to UndoManager after drawing completes */
@@ -41,7 +41,6 @@ export interface ToolHost {
41
41
  } | undefined;
42
42
  pushUndoDelta(delta: MaskDelta): void;
43
43
  pushUndoGroup(deltas: MaskDelta[]): void;
44
- getEraserUrls(): string[];
45
44
  enableCrosshair(): void;
46
45
  setUpSphereOrigins(mouseX: number, mouseY: number, sliceIndex: number): void;
47
46
  zoomActionAfterDrawSphere(): void;
@@ -64,7 +63,7 @@ export type ZoomHostDeps = Pick<ToolHost, 'resetPaintAreaUIPosition' | 'resizePa
64
63
  /** SphereTool host dependencies */
65
64
  export type SphereHostDeps = Pick<ToolHost, 'setEmptyCanvasSize' | 'drawImageOnEmptyImage' | 'enableCrosshair' | 'setUpSphereOrigins'>;
66
65
  /** DrawingTool host dependencies */
67
- export type DrawingHostDeps = Pick<ToolHost, 'setCurrentLayer' | 'compositeAllLayers' | 'syncLayerSliceData' | 'filterDrawedImage' | 'getVolumeForLayer' | 'pushUndoDelta' | 'getEraserUrls' | 'renderSliceToCanvas' | 'getOrCreateSliceBuffer'>;
66
+ export type DrawingHostDeps = Pick<ToolHost, 'setCurrentLayer' | 'compositeAllLayers' | 'syncLayerSliceData' | 'filterDrawedImage' | 'getVolumeForLayer' | 'pushUndoDelta' | 'renderSliceToCanvas' | 'getOrCreateSliceBuffer'>;
68
67
  /** DragSliceTool host dependencies */
69
68
  export type DragSliceHostDeps = Pick<ToolHost, 'setSyncsliceNum' | 'setIsDrawFalse' | 'flipDisplayImageByAxis' | 'setEmptyCanvasSize' | 'getOrCreateSliceBuffer' | 'renderSliceToCanvas' | 'refreshSphereOverlay' | 'compositeAllLayers'>;
70
69
  /** LayerChannelManager host dependencies */
@@ -74,4 +73,4 @@ export type SliceRenderHostDeps = Pick<ToolHost, 'compositeAllLayers' | 'getOrCr
74
73
  /** DataLoader host dependencies */
75
74
  export type DataLoaderHostDeps = Pick<ToolHost, 'invalidateSliceBuffer' | 'setDisplaySlicesBaseOnAxis' | 'afterLoadSlice' | 'setEmptyCanvasSize' | 'syncLayerSliceData' | 'reloadMasksFromVolume' | 'resetZoom'>;
76
75
  /** SphereBrushTool host dependencies */
77
- export type SphereBrushHostDeps = Pick<ToolHost, 'getVolumeForLayer' | 'compositeAllLayers' | 'pushUndoGroup' | 'renderSliceToCanvas' | 'getOrCreateSliceBuffer' | 'setEmptyCanvasSize' | 'reloadMasksFromVolume' | 'getEraserUrls'>;
76
+ export type SphereBrushHostDeps = Pick<ToolHost, 'getVolumeForLayer' | 'compositeAllLayers' | 'pushUndoGroup' | 'renderSliceToCanvas' | 'getOrCreateSliceBuffer' | 'setEmptyCanvasSize' | 'reloadMasksFromVolume'>;
@@ -7,7 +7,6 @@ export declare function loading(loadingGif?: string): {
7
7
  progress: HTMLDivElement;
8
8
  };
9
9
  export declare function switchPencilIcon(icon: string, urls?: string[]): string;
10
- export declare function switchEraserSize(size: number, urls?: string[]): string;
11
10
  export declare function throttle(callback: (event: MouseEvent) => void, wait: number): (event: MouseEvent) => void;
12
11
  /**
13
12
  * Cubic-Lagrange basis function
@@ -25,6 +25,6 @@ import type { ToolMode, IAnnotationCallbacks } from "./Utils/segmentation/core/t
25
25
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
26
26
  import type { LayerId, ChannelValue } from "./Utils/segmentation/core/index";
27
27
  import "./css/style.css";
28
- export declare const REVISION = "v3.4.5-beta";
28
+ export declare const REVISION = "v3.4.7-beta";
29
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, };
30
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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "copper3d",
3
3
  "description": "A 3d visualisation package base on threejs provides multiple scenes and Nrrd image load funtion.",
4
- "version": "3.4.5",
4
+ "version": "3.4.7",
5
5
  "main": "dist/bundle.umd.js",
6
6
  "moudle": "dist/bundle.esm.js",
7
7
  "types": "dist/types/index.d.ts",