@aics/vole-app 2.14.0 → 3.1.1

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 (81) hide show
  1. package/es/Globals.d.js +0 -0
  2. package/es/aics-image-viewer/components/App/ChannelUpdater.js +21 -3
  3. package/es/aics-image-viewer/components/App/index.js +184 -198
  4. package/es/aics-image-viewer/components/CellViewerCanvasWrapper/index.js +10 -9
  5. package/es/aics-image-viewer/components/ChannelsWidget.js +35 -22
  6. package/es/aics-image-viewer/components/ChannelsWidgetRow/index.js +26 -52
  7. package/es/aics-image-viewer/components/ChannelsWidgetRow/styles.css +0 -6
  8. package/es/aics-image-viewer/components/ColorPicker.js +1 -1
  9. package/es/aics-image-viewer/components/ControlPanel/index.js +5 -4
  10. package/es/aics-image-viewer/components/CustomizeWidget.js +12 -8
  11. package/es/aics-image-viewer/components/ErrorAlert/index.js +4 -4
  12. package/es/aics-image-viewer/components/GlobalVolumeControls.js +15 -12
  13. package/es/aics-image-viewer/components/StyleProvider/index.js +4 -2
  14. package/es/aics-image-viewer/components/TfEditor/index.js +304 -154
  15. package/es/aics-image-viewer/components/TfEditor/styles.css +53 -31
  16. package/es/aics-image-viewer/components/Toolbar/DownloadButton.js +1 -1
  17. package/es/aics-image-viewer/components/Toolbar/index.js +18 -16
  18. package/es/aics-image-viewer/components/shared/SharedCheckBox.js +1 -1
  19. package/es/aics-image-viewer/components/shared/SliderRow/index.js +8 -3
  20. package/es/aics-image-viewer/components/shared/SliderRow/styles.css +7 -0
  21. package/es/aics-image-viewer/components/shared/SmarterSlider.js +1 -1
  22. package/es/aics-image-viewer/components/shared/ViewerIcon.js +1 -1
  23. package/es/aics-image-viewer/components/useVolume.js +59 -58
  24. package/es/aics-image-viewer/shared/constants.js +2 -2
  25. package/es/aics-image-viewer/shared/utils/controlPointsToLut.js +57 -24
  26. package/es/aics-image-viewer/shared/utils/sceneStore.js +5 -3
  27. package/es/aics-image-viewer/shared/utils/storage.js +238 -0
  28. package/es/aics-image-viewer/shared/utils/test/storage.test.js +264 -0
  29. package/es/aics-image-viewer/shared/utils/test/urlParsing.test.js +240 -108
  30. package/es/aics-image-viewer/shared/utils/test/viewerChannelSettings.test.js +1 -1
  31. package/es/aics-image-viewer/shared/utils/urlParsing.js +278 -153
  32. package/es/aics-image-viewer/shared/utils/viewerState.js +4 -24
  33. package/es/aics-image-viewer/state/reset.js +111 -0
  34. package/es/aics-image-viewer/state/store.js +90 -0
  35. package/es/aics-image-viewer/state/subscribers.js +153 -0
  36. package/es/aics-image-viewer/state/test/reset.test.js +247 -0
  37. package/es/aics-image-viewer/state/util.js +71 -0
  38. package/es/index.js +3 -3
  39. package/package.json +18 -11
  40. package/type-declarations/aics-image-viewer/components/App/ChannelUpdater.d.ts +2 -4
  41. package/type-declarations/aics-image-viewer/components/App/types.d.ts +3 -3
  42. package/type-declarations/aics-image-viewer/components/AxisClipSliders/index.d.ts +6 -6
  43. package/type-declarations/aics-image-viewer/components/CellViewerCanvasWrapper/index.d.ts +5 -14
  44. package/type-declarations/aics-image-viewer/components/ChannelsWidget.d.ts +3 -6
  45. package/type-declarations/aics-image-viewer/components/ChannelsWidgetRow/index.d.ts +3 -6
  46. package/type-declarations/aics-image-viewer/components/ColorPicker.d.ts +1 -1
  47. package/type-declarations/aics-image-viewer/components/ControlPanel/index.d.ts +6 -7
  48. package/type-declarations/aics-image-viewer/components/CustomizeWidget.d.ts +2 -8
  49. package/type-declarations/aics-image-viewer/components/ErrorAlert/index.d.ts +4 -0
  50. package/type-declarations/aics-image-viewer/components/GlobalVolumeControls.d.ts +2 -9
  51. package/type-declarations/aics-image-viewer/components/MetadataViewer/index.d.ts +1 -1
  52. package/type-declarations/aics-image-viewer/components/StyleProvider/index.d.ts +1 -1
  53. package/type-declarations/aics-image-viewer/components/TfEditor/index.d.ts +10 -3
  54. package/type-declarations/aics-image-viewer/components/Toolbar/index.d.ts +3 -12
  55. package/type-declarations/aics-image-viewer/components/shared/SliderRow/index.d.ts +2 -1
  56. package/type-declarations/aics-image-viewer/components/shared/SmarterSlider.d.ts +1 -1
  57. package/type-declarations/aics-image-viewer/components/shared/ViewerIcon.d.ts +1 -1
  58. package/type-declarations/aics-image-viewer/components/useVolume.d.ts +7 -4
  59. package/type-declarations/aics-image-viewer/shared/constants.d.ts +5 -5
  60. package/type-declarations/aics-image-viewer/shared/types.d.ts +1 -1
  61. package/type-declarations/aics-image-viewer/shared/utils/controlPointsToLut.d.ts +9 -5
  62. package/type-declarations/aics-image-viewer/shared/utils/firebase/index.d.ts +1 -1
  63. package/type-declarations/aics-image-viewer/shared/utils/hooks.d.ts +1 -1
  64. package/type-declarations/aics-image-viewer/shared/utils/playControls.d.ts +1 -1
  65. package/type-declarations/aics-image-viewer/shared/utils/sceneStore.d.ts +8 -4
  66. package/type-declarations/aics-image-viewer/shared/utils/storage.d.ts +15 -0
  67. package/type-declarations/aics-image-viewer/shared/utils/urlParsing.d.ts +94 -31
  68. package/type-declarations/aics-image-viewer/shared/utils/viewerChannelSettings.d.ts +39 -1
  69. package/type-declarations/aics-image-viewer/shared/utils/viewerState.d.ts +3 -6
  70. package/type-declarations/aics-image-viewer/state/reset.d.ts +29 -0
  71. package/type-declarations/aics-image-viewer/state/store.d.ts +30 -0
  72. package/type-declarations/aics-image-viewer/state/subscribers.d.ts +4 -0
  73. package/type-declarations/aics-image-viewer/state/types.d.ts +46 -0
  74. package/type-declarations/aics-image-viewer/state/util.d.ts +8 -0
  75. package/type-declarations/index.d.ts +4 -4
  76. package/es/aics-image-viewer/components/ViewerStateProvider/ResetStateProvider.js +0 -163
  77. package/es/aics-image-viewer/components/ViewerStateProvider/index.js +0 -259
  78. package/type-declarations/aics-image-viewer/components/ViewerStateProvider/ResetStateProvider.d.ts +0 -34
  79. package/type-declarations/aics-image-viewer/components/ViewerStateProvider/index.d.ts +0 -33
  80. package/type-declarations/aics-image-viewer/components/ViewerStateProvider/types.d.ts +0 -107
  81. /package/es/aics-image-viewer/{components/ViewerStateProvider → state}/types.js +0 -0
@@ -15,7 +15,8 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
15
15
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
16
16
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
17
17
  import { Lut } from "@aics/vole-core";
18
- import { Button, Checkbox, InputNumber, Tooltip } from "antd";
18
+ import { PushpinFilled, PushpinOutlined } from "@ant-design/icons";
19
+ import { Button, Checkbox, Dropdown, InputNumber, Tooltip } from "antd";
19
20
  import * as d3 from "d3";
20
21
  import "nouislider/distribute/nouislider.css";
21
22
  import React, { useCallback, useMemo, useRef, useState } from "react";
@@ -25,6 +26,7 @@ import { colorArrayToObject, colorArrayToString, colorObjectToArray } from "../.
25
26
  import { controlPointsToRamp, rampToControlPoints } from "../../shared/utils/controlPointsToLut";
26
27
  import { useRefWithSetter } from "../../shared/utils/hooks";
27
28
  import SliderRow from "../shared/SliderRow";
29
+ import ViewerIcon from "../shared/ViewerIcon";
28
30
  import "./styles.css";
29
31
 
30
32
  /**The color picker opens next to control points like a context menu. This constant gives it a bit of space. */
@@ -50,11 +52,12 @@ var TFEDITOR_MARGINS = {
50
52
  left: 25
51
53
  };
52
54
  var MOUSE_EVENT_BUTTONS_PRIMARY = 1;
53
- var TfEditorRampSliderHandle = /*#__PURE__*/function (TfEditorRampSliderHandle) {
54
- TfEditorRampSliderHandle["Min"] = "min";
55
- TfEditorRampSliderHandle["Max"] = "max";
56
- return TfEditorRampSliderHandle;
57
- }(TfEditorRampSliderHandle || {});
55
+ var TfEditorSliderHandle = /*#__PURE__*/function (TfEditorSliderHandle) {
56
+ TfEditorSliderHandle["Min"] = "min";
57
+ TfEditorSliderHandle["Max"] = "max";
58
+ TfEditorSliderHandle["Isosurface"] = "surf";
59
+ return TfEditorSliderHandle;
60
+ }(TfEditorSliderHandle || {});
58
61
  var TF_GENERATORS = {
59
62
  autoXF: function autoXF(histo) {
60
63
  // Currently unused. min and max are the first and last bins whose values are >=10% that of the max bin
@@ -88,6 +91,7 @@ var TF_GENERATORS = {
88
91
  }
89
92
  };
90
93
 
94
+ // RAMP SLIDER HANDLE
91
95
  // *---*
92
96
  // | |
93
97
  // | |
@@ -108,18 +112,34 @@ var sliderHandleSymbol = {
108
112
  context.closePath();
109
113
  }
110
114
  };
111
- function binToAbsolute(value, histogram) {
112
- return histogram.getValueFromBinIndex(value);
113
- }
114
- function absoluteToBin(value, histogram) {
115
- return histogram.findFractionalBinOfValue(value);
116
- }
117
- function controlPointToAbsolute(cp, histogram) {
118
- // the x value of the control point is in the range [0, 255]
119
- // because of the way the histogram is generated
120
- // (see LUT_ENTRIES and the fact that we use Uint8Array)
121
- return binToAbsolute(cp.x, histogram);
122
- }
115
+
116
+ // ISOVALUE SLIDER HANDLE
117
+ // / \ / \
118
+ // \ \/ /
119
+ // \ /
120
+ // *--*
121
+ // start ^ ^ end
122
+ var LINE_WIDTH = 2;
123
+ // the "base" is the 5-sided area where both lines meet - think of a square with side length
124
+ // `LINE_WIDTH`, rotated 45deg, with the bottom corner cut off where the slider line meets the handle
125
+ var BASE_AREA = Math.pow(LINE_WIDTH, 2) * 0.75;
126
+ var BASE_HEIGHT = (Math.SQRT2 - 0.5) * LINE_WIDTH;
127
+ // how much the base "sticks out" horizontally on either side of the slider line
128
+ var BASE_MARGIN = (Math.SQRT2 - 1) * 0.5 * LINE_WIDTH;
129
+ var isovalueHandleSymbol = {
130
+ draw: function draw(context, size) {
131
+ var linesArea = Math.max(size - BASE_AREA, 0);
132
+ var margin = BASE_MARGIN + linesArea / (2 * Math.SQRT2 * LINE_WIDTH);
133
+ context.moveTo(1, 0);
134
+ context.lineTo(margin + 1, -margin);
135
+ context.lineTo(margin + 1 - Math.SQRT2, -margin - Math.SQRT2);
136
+ context.lineTo(0, -BASE_HEIGHT);
137
+ context.lineTo(-margin - 1 + Math.SQRT2, -margin - Math.SQRT2);
138
+ context.lineTo(-margin - 1, -margin);
139
+ context.lineTo(-1, 0);
140
+ context.closePath();
141
+ }
142
+ };
123
143
 
124
144
  /** For when all control points are outside the plot's range: just fill the plot with the settings from 1 point */
125
145
  var coverRangeWithPoint = function coverRangeWithPoint(point, plotMin, plotMax) {
@@ -263,9 +283,9 @@ var TfEditor = function TfEditor(props) {
263
283
  _useState4 = _slicedToArray(_useState3, 2),
264
284
  draggedPointIdx = _useState4[0],
265
285
  _setDraggedPointIdx = _useState4[1];
266
- var _setCPs = useCallback(function (p) {
286
+ var _setCPs = useCallback(function (controlPoints) {
267
287
  return changeChannelSetting({
268
- controlPoints: p
288
+ controlPoints: controlPoints
269
289
  });
270
290
  }, [changeChannelSetting]);
271
291
  var setRamp = useCallback(function (ramp) {
@@ -274,6 +294,12 @@ var TfEditor = function TfEditor(props) {
274
294
  });
275
295
  }, [changeChannelSetting]);
276
296
 
297
+ // The temporary isovalue used while dragging the isovalue slider, which we only want to update on slider release
298
+ var _useState5 = useState(0),
299
+ _useState6 = _slicedToArray(_useState5, 2),
300
+ draggedIsovalue = _useState6[0],
301
+ setDraggedIsovalue = _useState6[1];
302
+
277
303
  // these bits of state need their freshest, most up-to-date values available in mouse event handlers. make refs!
278
304
  var _useRefWithSetter = useRefWithSetter(_setCPs, props.controlPoints),
279
305
  _useRefWithSetter2 = _slicedToArray(_useRefWithSetter, 2),
@@ -286,10 +312,10 @@ var TfEditor = function TfEditor(props) {
286
312
 
287
313
  // Either `null` when the control panel is closed, or an x offset into the plot to position the color picker.
288
314
  // Positive: offset right from the left edge of the plot; negative: offset left from the right edge of the plot.
289
- var _useState5 = useState(null),
290
- _useState6 = _slicedToArray(_useState5, 2),
291
- colorPickerPosition = _useState6[0],
292
- setColorPickerPosition = _useState6[1];
315
+ var _useState7 = useState(null),
316
+ _useState8 = _slicedToArray(_useState7, 2),
317
+ colorPickerPosition = _useState8[0],
318
+ setColorPickerPosition = _useState8[1];
293
319
  var lastColorRef = useRef(TFEDITOR_DEFAULT_COLOR);
294
320
  var svgRef = useRef(null); // need access to SVG element to measure mouse position
295
321
 
@@ -301,22 +327,16 @@ var TfEditor = function TfEditor(props) {
301
327
  var xScale = useMemo(function () {
302
328
  return d3.scaleLinear().domain([plotMin, plotMax]).range([0, innerWidth]);
303
329
  }, [innerWidth, plotMin, plotMax]);
304
- var plotMinU8 = useMemo(function () {
305
- return absoluteToBin(plotMin, histogram);
306
- }, [plotMin, histogram]);
307
- var plotMaxU8 = useMemo(function () {
308
- return absoluteToBin(plotMax, histogram);
309
- }, [plotMax, histogram]);
310
330
  var yScale = useMemo(function () {
311
331
  return d3.scaleLinear().domain([0, 1]).range([innerHeight, 0]);
312
332
  }, [innerHeight]);
313
- var mouseEventToControlPointValues = function mouseEventToControlPointValues(event) {
333
+ var mouseEventToPlotValues = function mouseEventToPlotValues(event) {
314
334
  var _svgRef$current$getBo, _svgRef$current;
315
335
  var svgRect = (_svgRef$current$getBo = (_svgRef$current = svgRef.current) === null || _svgRef$current === void 0 ? void 0 : _svgRef$current.getBoundingClientRect()) !== null && _svgRef$current$getBo !== void 0 ? _svgRef$current$getBo : {
316
336
  x: 0,
317
337
  y: 0
318
338
  };
319
- return [absoluteToBin(xScale.invert(clamp(event.clientX - svgRect.x - TFEDITOR_MARGINS.left, 0, innerWidth)), histogram), yScale.invert(clamp(event.clientY - svgRect.y - TFEDITOR_MARGINS.top, 0, innerHeight))];
339
+ return [xScale.invert(clamp(event.clientX - svgRect.x - TFEDITOR_MARGINS.left, 0, innerWidth)), yScale.invert(clamp(event.clientY - svgRect.y - TFEDITOR_MARGINS.top, 0, innerHeight))];
320
340
  };
321
341
  var dragControlPoint = function dragControlPoint(draggedIdx, x, opacity) {
322
342
  var newControlPoints = _toConsumableArray(controlPointsRef.current);
@@ -341,13 +361,18 @@ var TfEditor = function TfEditor(props) {
341
361
  }
342
362
  setControlPoints(newControlPoints);
343
363
  };
344
- var dragRampSlider = function dragRampSlider(handle, x) {
345
- if (handle === TfEditorRampSliderHandle.Min) {
364
+ var dragSlider = function dragSlider(handle, x) {
365
+ if (handle === TfEditorSliderHandle.Min) {
366
+ // dragging the min slider
346
367
  var max = props.ramp[1];
347
368
  setRamp([Math.min(x, max), max]);
348
- } else {
369
+ } else if (handle === TfEditorSliderHandle.Max) {
370
+ // dragging the max slider
349
371
  var min = props.ramp[0];
350
372
  setRamp([min, Math.max(x, min)]);
373
+ } else {
374
+ // dragging the isosurface slider
375
+ setDraggedIsovalue(x);
351
376
  }
352
377
  };
353
378
  var handlePlotPointerDown = function handlePlotPointerDown(event) {
@@ -355,10 +380,10 @@ var TfEditor = function TfEditor(props) {
355
380
  // Advanced mode - we're either creating a new control point or selecting/dragging an existing one
356
381
  if (draggedPointIdxRef.current === null && event.button === 0) {
357
382
  // this click is not on an existing point - create a new one
358
- var _mouseEventToControlP = mouseEventToControlPointValues(event),
359
- _mouseEventToControlP2 = _slicedToArray(_mouseEventToControlP, 2),
360
- x = _mouseEventToControlP2[0],
361
- opacity = _mouseEventToControlP2[1];
383
+ var _mouseEventToPlotValu = mouseEventToPlotValues(event),
384
+ _mouseEventToPlotValu2 = _slicedToArray(_mouseEventToPlotValu, 2),
385
+ x = _mouseEventToPlotValu2[0],
386
+ opacity = _mouseEventToPlotValu2[1];
362
387
  var point = {
363
388
  x: x,
364
389
  opacity: opacity,
@@ -398,20 +423,25 @@ var TfEditor = function TfEditor(props) {
398
423
  }
399
424
  event.stopPropagation();
400
425
  event.preventDefault();
401
- var _mouseEventToControlP3 = mouseEventToControlPointValues(event),
402
- _mouseEventToControlP4 = _slicedToArray(_mouseEventToControlP3, 2),
403
- x = _mouseEventToControlP4[0],
404
- opacity = _mouseEventToControlP4[1];
426
+ var _mouseEventToPlotValu3 = mouseEventToPlotValues(event),
427
+ _mouseEventToPlotValu4 = _slicedToArray(_mouseEventToPlotValu3, 2),
428
+ x = _mouseEventToPlotValu4[0],
429
+ opacity = _mouseEventToPlotValu4[1];
405
430
 
406
431
  // `draggedPointIdxRef` may either be a number (control point index) or a string (ramp slider handle).
407
432
  // The result of this check should always be the same as `props.useControlPoints`, but this narrows the type for TS
408
433
  if (typeof draggedPointIdxRef.current === "number") {
409
434
  dragControlPoint(draggedPointIdxRef.current, x, opacity);
410
435
  } else {
411
- dragRampSlider(draggedPointIdxRef.current, x);
436
+ dragSlider(draggedPointIdxRef.current, x);
412
437
  }
413
438
  };
414
439
  var handleDragEnd = function handleDragEnd(event) {
440
+ if (draggedPointIdx === TfEditorSliderHandle.Isosurface) {
441
+ changeChannelSetting({
442
+ isovalue: draggedIsovalue
443
+ });
444
+ }
415
445
  setDraggedPointIdx(null);
416
446
  event.currentTarget.releasePointerCapture(event.pointerId);
417
447
  };
@@ -444,25 +474,29 @@ var TfEditor = function TfEditor(props) {
444
474
  };
445
475
  var controlPointsToRender = useMemo(function () {
446
476
  var points = props.useControlPoints ? props.controlPoints.slice() : rampToControlPoints(props.ramp);
447
- return fitControlPointsToRange(points, plotMinU8, plotMaxU8);
448
- }, [props.controlPoints, props.ramp, props.useControlPoints, plotMinU8, plotMaxU8]);
477
+ return fitControlPointsToRange(points, plotMin, plotMax);
478
+ }, [props.controlPoints, props.ramp, props.useControlPoints, plotMin, plotMax]);
449
479
 
450
480
  /** d3-generated svg data string representing both the line between points and the region filled with gradient */
451
481
  var areaPath = useMemo(function () {
452
482
  var _areaGenerator;
453
483
  var areaGenerator = d3.area().x(function (d) {
454
- return xScale(controlPointToAbsolute(d, histogram));
484
+ return xScale(d.x);
455
485
  }).y0(function (d) {
456
486
  return yScale(d.opacity);
457
487
  }).y1(innerHeight).curve(d3.curveLinear);
458
488
  return (_areaGenerator = areaGenerator(controlPointsToRender)) !== null && _areaGenerator !== void 0 ? _areaGenerator : undefined;
459
- }, [controlPointsToRender, xScale, yScale, innerHeight, histogram]);
489
+ }, [controlPointsToRender, xScale, yScale, innerHeight]);
460
490
 
461
491
  /** d3-generated svg data string representing the "basic mode" min/max slider handles */
462
492
  var sliderHandlePath = useMemo(function () {
463
493
  var _d3$symbol$type$size;
464
494
  return (_d3$symbol$type$size = d3.symbol().type(sliderHandleSymbol).size(80)()) !== null && _d3$symbol$type$size !== void 0 ? _d3$symbol$type$size : undefined;
465
495
  }, []);
496
+ var isovalueHandlePath = useMemo(function () {
497
+ var _d3$symbol$type$size2;
498
+ return (_d3$symbol$type$size2 = d3.symbol().type(isovalueHandleSymbol).size(21)()) !== null && _d3$symbol$type$size2 !== void 0 ? _d3$symbol$type$size2 : undefined;
499
+ }, []);
466
500
 
467
501
  // The below `useCallback`s are used as "ref callbacks" - passed as the `ref` prop of SVG elements in order to render
468
502
  // these elements' content using D3. They are called when the ref'd component mounts and unmounts, and whenever their
@@ -504,6 +538,8 @@ var TfEditor = function TfEditor(props) {
504
538
  if (numBins < 1) {
505
539
  return;
506
540
  }
541
+ var plotMinU8 = histogram.findFractionalBinOfValue(plotMin);
542
+ var plotMaxU8 = histogram.findFractionalBinOfValue(plotMax);
507
543
  var _getHistogramBinLengt = getHistogramBinLengths(histogram),
508
544
  binLengths = _getHistogramBinLengt.binLengths,
509
545
  max = _getHistogramBinLengt.max;
@@ -516,100 +552,113 @@ var TfEditor = function TfEditor(props) {
516
552
  .data(binLengthsToRender) // bind the histogram bins to this selection
517
553
  .join("rect") // ensure we have exactly as many bound `rect` elements in the DOM as we have histogram bins
518
554
  .attr("class", "bar").attr("width", barWidth).attr("x", function (_len, idx) {
519
- return xScale(binToAbsolute(idx + start, histogram));
555
+ return xScale(histogram.getValueFromBinIndex(idx + start));
520
556
  }) // set position and height from data
521
557
  .attr("y", function (len) {
522
558
  return binScale(len);
523
559
  }).attr("height", function (len) {
524
560
  return innerHeight - binScale(len);
525
561
  });
526
- }, [xScale, histogram, innerWidth, innerHeight, plotMinU8, plotMaxU8]);
562
+ }, [xScale, histogram, innerWidth, innerHeight, plotMin, plotMax]);
527
563
  var applyTFGenerator = useCallback(function (generator) {
528
564
  setSelectedPointIdx(null);
529
565
  lastColorRef.current = TFEDITOR_DEFAULT_COLOR;
530
566
  var lut = TF_GENERATORS[generator](histogram);
567
+ var valueIndexedControlPoints = lut.controlPoints.map(function (cp) {
568
+ return _objectSpread(_objectSpread({}, cp), {}, {
569
+ x: histogram.getValueFromBinIndex(cp.x)
570
+ });
571
+ });
531
572
  if (props.useControlPoints) {
532
- setControlPoints(lut.controlPoints.map(function (cp) {
573
+ setControlPoints(valueIndexedControlPoints.map(function (cp) {
533
574
  return _objectSpread(_objectSpread({}, cp), {}, {
534
575
  color: TFEDITOR_DEFAULT_COLOR
535
576
  });
536
577
  }));
537
578
  } else {
538
- setRamp(controlPointsToRamp(lut.controlPoints));
579
+ setRamp(controlPointsToRamp(valueIndexedControlPoints));
539
580
  }
540
581
  }, [histogram, props.useControlPoints, setControlPoints, setRamp]);
541
- var createTFGeneratorButton = function createTFGeneratorButton(generator, name, description) {
542
- return /*#__PURE__*/React.createElement(Tooltip, {
543
- title: description,
544
- placement: "top"
545
- }, /*#__PURE__*/React.createElement(Button, {
546
- size: "small",
547
- onClick: function onClick() {
548
- return applyTFGenerator(generator);
549
- }
550
- }, name));
551
- };
552
-
553
- // create one svg circle element for each control point
554
- var controlPointCircles = props.useControlPoints ? props.controlPoints.filter(function (cp) {
555
- return plotMinU8 <= cp.x && cp.x <= plotMaxU8;
556
- }) // filter out-of-range points
557
- .map(function (cp, i) {
558
- return /*#__PURE__*/React.createElement("circle", {
559
- key: i,
560
- className: i === selectedPointIdx ? "selected" : "",
561
- cx: xScale(controlPointToAbsolute(cp, histogram)),
562
- cy: yScale(cp.opacity),
563
- style: {
564
- fill: colorArrayToString(cp.color)
565
- },
566
- r: 5,
567
- onPointerDown: function onPointerDown() {
568
- return setDraggedPointIdx(i);
569
- },
570
- onContextMenu: handleControlPointContextMenu
582
+ var controlPointCircles = null;
583
+ if (props.volumeEnabled && props.useControlPoints) {
584
+ // create one svg circle element for each control point
585
+ controlPointCircles = props.controlPoints.filter(function (cp) {
586
+ return plotMin <= cp.x && cp.x <= plotMax;
587
+ }) // filter out-of-range points
588
+ .map(function (cp, i) {
589
+ return /*#__PURE__*/React.createElement("circle", {
590
+ key: i,
591
+ className: i === selectedPointIdx ? "selected" : "",
592
+ cx: xScale(cp.x),
593
+ cy: yScale(cp.opacity),
594
+ style: {
595
+ fill: colorArrayToString(cp.color)
596
+ },
597
+ r: 5,
598
+ onPointerDown: function onPointerDown() {
599
+ return setDraggedPointIdx(i);
600
+ },
601
+ onContextMenu: handleControlPointContextMenu
602
+ });
571
603
  });
572
- }) : null;
604
+ }
573
605
  // move selected control point to the end so it's drawn last and not occluded by other nearby points
574
606
  if (controlPointCircles !== null && selectedPointIdx !== null) {
575
607
  controlPointCircles.push(controlPointCircles.splice(selectedPointIdx, 1)[0]);
576
608
  }
577
- var viewerModeString = props.useControlPoints ? "advanced" : "basic";
578
- return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
579
- className: "button-row"
580
- }, createTFGeneratorButton("auto98XF", "Default", "Ramp from 50th percentile to 98th"), createTFGeneratorButton("auto2XF", "IJ Auto", "Emulates ImageJ's \"auto\" button"), createTFGeneratorButton("resetXF", "Auto 1", "Ramp over the full data range (0% to 100%)"), createTFGeneratorButton("bestFitXF", "Auto 2", "Ramp over the middle 80% of data"), /*#__PURE__*/React.createElement(Checkbox, {
581
- checked: props.useControlPoints,
582
- onChange: function onChange(e) {
609
+ var dragClass = draggedPointIdx === null ? "" : typeof draggedPointIdx === "number" ? " dragging-point" : " dragging-slider";
610
+ var isovalue = draggedPointIdx === TfEditorSliderHandle.Isosurface ? draggedIsovalue : props.isovalue;
611
+ var useRamp = props.volumeEnabled && !props.useControlPoints;
612
+ var _props$channelData = props.channelData,
613
+ rawMin = _props$channelData.rawMin,
614
+ rawMax = _props$channelData.rawMax;
615
+ return /*#__PURE__*/React.createElement("div", {
616
+ className: "tf-editor"
617
+ }, /*#__PURE__*/React.createElement("div", {
618
+ className: "tf-editor-control-row"
619
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Button, {
620
+ size: "small",
621
+ onClick: function onClick() {
583
622
  return changeChannelSetting({
584
- useControlPoints: e.target.checked
623
+ plotMin: rawMin,
624
+ plotMax: rawMax
585
625
  });
586
- },
587
- style: {
588
- marginLeft: "auto"
589
626
  }
590
- }, "Advanced")), !props.useControlPoints && /*#__PURE__*/React.createElement("div", {
591
- className: "tf-editor-control-row ramp-row"
592
- }, "Levels min/max", /*#__PURE__*/React.createElement(InputNumber, {
593
- value: binToAbsolute(props.ramp[0], histogram),
627
+ }, "Fit to data"), /*#__PURE__*/React.createElement(Button, {
628
+ size: "small",
629
+ onClick: function onClick() {
630
+ return changeChannelSetting({
631
+ plotMin: typeRange.min,
632
+ plotMax: typeRange.max
633
+ });
634
+ }
635
+ }, "Full range")), /*#__PURE__*/React.createElement("div", null, "Plot min/max", /*#__PURE__*/React.createElement(InputNumber, {
636
+ value: plotMin,
594
637
  onChange: function onChange(v) {
595
- return v !== null && setRamp([absoluteToBin(v, histogram), props.ramp[1]]);
638
+ return v !== null && changeChannelSetting({
639
+ plotMin: v,
640
+ plotMax: Math.max(v + 1, plotMax)
641
+ });
596
642
  },
597
643
  formatter: numberFormatter,
598
644
  min: typeRange.min,
599
- max: Math.min(binToAbsolute(props.ramp[1], histogram), typeRange.max),
645
+ max: typeRange.max - 1,
600
646
  size: "small",
601
647
  controls: false
602
648
  }), /*#__PURE__*/React.createElement(InputNumber, {
603
- value: binToAbsolute(props.ramp[1], histogram),
649
+ value: plotMax,
604
650
  onChange: function onChange(v) {
605
- return v !== null && setRamp([props.ramp[0], absoluteToBin(v, histogram)]);
651
+ return v !== null && changeChannelSetting({
652
+ plotMax: v,
653
+ plotMin: Math.min(v - 1, plotMin)
654
+ });
606
655
  },
607
656
  formatter: numberFormatter,
608
- min: Math.max(typeRange.min, binToAbsolute(props.ramp[0], histogram)),
657
+ min: typeRange.min + 1,
609
658
  max: typeRange.max,
610
659
  size: "small",
611
660
  controls: false
612
- })), colorPickerPosition !== null && /*#__PURE__*/React.createElement("div", {
661
+ }))), colorPickerPosition !== null && /*#__PURE__*/React.createElement("div", {
613
662
  className: "tf-editor-popover"
614
663
  }, /*#__PURE__*/React.createElement("div", {
615
664
  className: "tf-editor-cover",
@@ -623,7 +672,7 @@ var TfEditor = function TfEditor(props) {
623
672
  onChange: handleChangeColor,
624
673
  disableAlpha: true
625
674
  }))), /*#__PURE__*/React.createElement("svg", {
626
- className: "tf-editor-svg ".concat(viewerModeString).concat(draggedPointIdx !== null ? " dragging" : ""),
675
+ className: "tf-editor-svg".concat(dragClass),
627
676
  ref: svgRef,
628
677
  width: props.width,
629
678
  height: props.height,
@@ -637,7 +686,7 @@ var TfEditor = function TfEditor(props) {
637
686
  transform: "translate(".concat(TFEDITOR_MARGINS.left, ",").concat(TFEDITOR_MARGINS.top, ")")
638
687
  }, /*#__PURE__*/React.createElement("g", {
639
688
  ref: histogramRef
640
- }), /*#__PURE__*/React.createElement("path", {
689
+ }), props.volumeEnabled && /*#__PURE__*/React.createElement("path", {
641
690
  className: "line",
642
691
  fill: "url(#tfGradient-".concat(props.id, ")"),
643
692
  d: areaPath
@@ -648,109 +697,210 @@ var TfEditor = function TfEditor(props) {
648
697
  }), /*#__PURE__*/React.createElement("g", {
649
698
  ref: yAxisRef,
650
699
  className: "axis"
651
- }), controlPointCircles, !props.useControlPoints && /*#__PURE__*/React.createElement("g", {
652
- className: "ramp-sliders"
653
- }, plotMinU8 <= props.ramp[0] && props.ramp[0] <= plotMaxU8 && /*#__PURE__*/React.createElement("g", {
654
- transform: "translate(".concat(xScale(binToAbsolute(props.ramp[0], histogram)), ")")
700
+ }), controlPointCircles, props.volumeEnabled && !props.useControlPoints && /*#__PURE__*/React.createElement("g", {
701
+ className: "sliders sliders-ramp"
702
+ }, plotMin <= props.ramp[0] && props.ramp[0] <= plotMax && /*#__PURE__*/React.createElement("g", {
703
+ transform: "translate(".concat(xScale(props.ramp[0]), ")")
655
704
  }, /*#__PURE__*/React.createElement("line", {
656
705
  y1: innerHeight,
657
706
  strokeDasharray: "5,5",
658
707
  strokeWidth: 2
659
708
  }), /*#__PURE__*/React.createElement("line", {
660
- className: "ramp-slider-click-target",
709
+ className: "slider-click-target",
661
710
  y1: innerHeight,
662
711
  strokeWidth: 6,
663
712
  onPointerDown: function onPointerDown() {
664
- return setDraggedPointIdx(TfEditorRampSliderHandle.Min);
713
+ return setDraggedPointIdx(TfEditorSliderHandle.Min);
665
714
  }
666
715
  }), /*#__PURE__*/React.createElement("path", {
667
716
  d: sliderHandlePath,
668
717
  transform: "translate(0,".concat(innerHeight, ") rotate(180)"),
669
718
  onPointerDown: function onPointerDown() {
670
- return setDraggedPointIdx(TfEditorRampSliderHandle.Min);
719
+ return setDraggedPointIdx(TfEditorSliderHandle.Min);
671
720
  }
672
- })), plotMinU8 <= props.ramp[1] && props.ramp[1] <= plotMaxU8 && /*#__PURE__*/React.createElement("g", {
673
- transform: "translate(".concat(xScale(binToAbsolute(props.ramp[1], histogram)), ")")
721
+ })), plotMin <= props.ramp[1] && props.ramp[1] <= plotMax && /*#__PURE__*/React.createElement("g", {
722
+ transform: "translate(".concat(xScale(props.ramp[1]), ")")
674
723
  }, /*#__PURE__*/React.createElement("line", {
675
724
  y1: innerHeight,
676
725
  strokeDasharray: "5,5",
677
726
  strokeWidth: 2
678
727
  }), /*#__PURE__*/React.createElement("line", {
679
- className: "ramp-slider-click-target",
728
+ className: "slider-click-target",
680
729
  y1: innerHeight,
681
730
  strokeWidth: 6,
682
731
  onPointerDown: function onPointerDown() {
683
- return setDraggedPointIdx(TfEditorRampSliderHandle.Max);
732
+ return setDraggedPointIdx(TfEditorSliderHandle.Max);
684
733
  }
685
734
  }), /*#__PURE__*/React.createElement("path", {
686
735
  d: sliderHandlePath,
687
736
  onPointerDown: function onPointerDown() {
688
- return setDraggedPointIdx(TfEditorRampSliderHandle.Max);
737
+ return setDraggedPointIdx(TfEditorSliderHandle.Max);
689
738
  }
690
- }))))), /*#__PURE__*/React.createElement("div", {
691
- className: "tf-editor-control-row plot-range-row"
692
- }, "Plot min/max", /*#__PURE__*/React.createElement(InputNumber, {
693
- value: plotMin,
739
+ }))), props.isosurfaceEnabled && plotMin <= isovalue && isovalue <= plotMax && /*#__PURE__*/React.createElement("g", {
740
+ className: "sliders sliders-isovalue",
741
+ transform: "translate(".concat(xScale(isovalue), ")")
742
+ }, /*#__PURE__*/React.createElement("line", {
743
+ className: "slider-isosurface",
744
+ y1: innerHeight,
745
+ strokeDasharray: "4,4",
746
+ strokeWidth: 2
747
+ }), /*#__PURE__*/React.createElement("line", {
748
+ className: "slider-click-target",
749
+ y1: innerHeight,
750
+ strokeWidth: 6,
751
+ onPointerDown: function onPointerDown() {
752
+ setDraggedIsovalue(props.isovalue);
753
+ setDraggedPointIdx(TfEditorSliderHandle.Isosurface);
754
+ }
755
+ }), /*#__PURE__*/React.createElement("path", {
756
+ d: isovalueHandlePath,
757
+ onPointerDown: function onPointerDown() {
758
+ return setDraggedPointIdx(TfEditorSliderHandle.Isosurface);
759
+ }
760
+ }), /*#__PURE__*/React.createElement("path", {
761
+ d: isovalueHandlePath,
762
+ transform: "translate(0,".concat(innerHeight, ") rotate(180)"),
763
+ onPointerDown: function onPointerDown() {
764
+ return setDraggedPointIdx(TfEditorSliderHandle.Isosurface);
765
+ }
766
+ })))), props.volumeEnabled && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h4", null, "Volume settings"), /*#__PURE__*/React.createElement("div", {
767
+ className: "tf-editor-control-row"
768
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Dropdown, {
769
+ trigger: ["click"],
770
+ menu: {
771
+ items: [{
772
+ key: "auto98XF",
773
+ label: "Default (50% - 98%)"
774
+ }, {
775
+ key: "auto2XF",
776
+ label: "ImageJ Auto"
777
+ }, {
778
+ key: "resetXF",
779
+ label: "Auto 1 (0% - 100%)"
780
+ }, {
781
+ key: "bestFitXF",
782
+ label: "Auto 2 (10% - 90%)"
783
+ }],
784
+ onClick: function onClick(_ref5) {
785
+ var key = _ref5.key;
786
+ return applyTFGenerator(key);
787
+ }
788
+ }
789
+ }, /*#__PURE__*/React.createElement(Button, {
790
+ size: "small"
791
+ }, /*#__PURE__*/React.createElement("span", {
792
+ style: {
793
+ display: "flex",
794
+ flexDirection: "row",
795
+ alignItems: "center",
796
+ gap: "4px"
797
+ }
798
+ }, "Apply ramp", /*#__PURE__*/React.createElement(ViewerIcon, {
799
+ type: "dropdownArrow",
800
+ style: {
801
+ fontSize: "14px"
802
+ }
803
+ }))))), /*#__PURE__*/React.createElement("div", null, useRamp && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", null, "Levels min/max"), /*#__PURE__*/React.createElement(InputNumber, {
804
+ value: props.ramp[0],
694
805
  onChange: function onChange(v) {
695
- return v !== null && changeChannelSetting({
696
- plotMin: v,
697
- plotMax: Math.max(v + 1, plotMax)
698
- });
806
+ return v !== null && setRamp([v, props.ramp[1]]);
699
807
  },
700
808
  formatter: numberFormatter,
701
809
  min: typeRange.min,
702
- max: typeRange.max - 1,
810
+ max: Math.min(props.ramp[1], typeRange.max),
703
811
  size: "small",
704
812
  controls: false
705
813
  }), /*#__PURE__*/React.createElement(InputNumber, {
706
- value: plotMax,
814
+ value: props.ramp[1],
707
815
  onChange: function onChange(v) {
708
- return v !== null && changeChannelSetting({
709
- plotMax: v,
710
- plotMin: Math.min(v - 1, plotMin)
711
- });
816
+ return v !== null && setRamp([props.ramp[0], v]);
712
817
  },
713
818
  formatter: numberFormatter,
714
- min: typeRange.min + 1,
819
+ min: Math.max(typeRange.min, props.ramp[0]),
715
820
  max: typeRange.max,
716
821
  size: "small",
717
822
  controls: false
718
- }), /*#__PURE__*/React.createElement(Button, {
719
- size: "small",
720
- style: {
721
- marginLeft: "12px"
722
- },
723
- onClick: function onClick() {
724
- return changeChannelSetting({
725
- plotMin: props.channelData.rawMin,
726
- plotMax: props.channelData.rawMax
727
- });
728
- }
729
- }, "Fit to data"), /*#__PURE__*/React.createElement(Button, {
730
- size: "small",
823
+ }), /*#__PURE__*/React.createElement(Tooltip, {
824
+ title: "".concat(props.keepIntensityRange ? "Do not keep" : "Keep", " intensity threshold values when switching volumes.")
825
+ }, /*#__PURE__*/React.createElement(Button, {
731
826
  onClick: function onClick() {
732
- return changeChannelSetting({
733
- plotMin: typeRange.min,
734
- plotMax: typeRange.max
827
+ return props.changeChannelSetting({
828
+ keepIntensityRange: !props.keepIntensityRange
735
829
  });
830
+ },
831
+ style: {
832
+ height: 24,
833
+ padding: "0 4px"
834
+ },
835
+ type: props.keepIntensityRange ? "default" : "text"
836
+ }, /*#__PURE__*/React.createElement("span", {
837
+ style: {
838
+ margin: 0
736
839
  }
737
- }, "Full range")), /*#__PURE__*/React.createElement(SliderRow, {
738
- label: /*#__PURE__*/React.createElement(Checkbox, {
840
+ }, props.keepIntensityRange ? /*#__PURE__*/React.createElement(PushpinFilled, null) : /*#__PURE__*/React.createElement(PushpinOutlined, null))))))), /*#__PURE__*/React.createElement(SliderRow, {
841
+ label: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Checkbox, {
842
+ checked: props.useControlPoints,
843
+ onChange: function onChange(e) {
844
+ return changeChannelSetting({
845
+ useControlPoints: e.target.checked
846
+ });
847
+ }
848
+ }, "Advanced"), /*#__PURE__*/React.createElement(Checkbox, {
739
849
  checked: props.colorizeEnabled,
740
850
  onChange: function onChange(e) {
741
851
  return changeChannelSetting({
742
852
  colorizeEnabled: e.target.checked
743
853
  });
854
+ },
855
+ style: {
856
+ marginLeft: "5px"
744
857
  }
745
- }, "Colorize"),
858
+ }, "Colorize")),
746
859
  max: 1,
747
860
  start: props.colorizeAlpha,
748
- onUpdate: function onUpdate(values) {
861
+ onUpdate: function onUpdate(_ref6) {
862
+ var _ref7 = _slicedToArray(_ref6, 1),
863
+ colorizeAlpha = _ref7[0];
749
864
  return changeChannelSetting({
750
- colorizeAlpha: values[0]
865
+ colorizeAlpha: colorizeAlpha
751
866
  });
752
867
  },
753
868
  hideSlider: !props.colorizeEnabled
754
- }));
869
+ })), props.isosurfaceEnabled && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h4", null, "Isosurface settings"), /*#__PURE__*/React.createElement("div", {
870
+ className: "tf-editor-control-row"
871
+ }, /*#__PURE__*/React.createElement("div", null, "Isovalue", /*#__PURE__*/React.createElement(InputNumber, {
872
+ value: props.isovalue,
873
+ onChange: function onChange(isovalue) {
874
+ return isovalue !== null && changeChannelSetting({
875
+ isovalue: isovalue
876
+ });
877
+ },
878
+ formatter: numberFormatter,
879
+ min: typeRange.min,
880
+ max: typeRange.max,
881
+ size: "small",
882
+ controls: false
883
+ })), /*#__PURE__*/React.createElement("div", null, "Export surface", /*#__PURE__*/React.createElement(Button, {
884
+ size: "small",
885
+ onClick: function onClick() {
886
+ return props.saveIsosurface("GLTF");
887
+ }
888
+ }, "GLTF"), /*#__PURE__*/React.createElement(Button, {
889
+ size: "small",
890
+ onClick: function onClick() {
891
+ return props.saveIsosurface("STL");
892
+ }
893
+ }, "STL"))), /*#__PURE__*/React.createElement(SliderRow, {
894
+ label: "Opacity",
895
+ max: 1.0,
896
+ start: props.opacity,
897
+ onUpdate: function onUpdate(_ref8) {
898
+ var _ref9 = _slicedToArray(_ref8, 1),
899
+ opacity = _ref9[0];
900
+ return changeChannelSetting({
901
+ opacity: opacity
902
+ });
903
+ }
904
+ })));
755
905
  };
756
906
  export default TfEditor;