@aics/vole-app 3.3.1 → 3.4.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 (38) hide show
  1. package/es/aics-image-viewer/components/App/index.js +1 -0
  2. package/es/aics-image-viewer/components/BottomPanel/index.js +48 -19
  3. package/es/aics-image-viewer/components/BottomPanel/styles.css +16 -7
  4. package/es/aics-image-viewer/components/CellViewerCanvasWrapper/index.js +39 -19
  5. package/es/aics-image-viewer/components/ControlPanel/index.js +2 -0
  6. package/es/aics-image-viewer/components/dimension_sliders/AxisClipSliders.js +148 -0
  7. package/es/aics-image-viewer/components/dimension_sliders/RotationSliders.js +167 -0
  8. package/es/aics-image-viewer/components/dimension_sliders/SliderRows.js +167 -0
  9. package/es/aics-image-viewer/components/{AxisClipSliders → dimension_sliders}/styles.css +7 -1
  10. package/es/aics-image-viewer/components/shared/NumericInput/index.js +4 -2
  11. package/es/aics-image-viewer/components/shared/SliderRow/index.js +2 -0
  12. package/es/aics-image-viewer/components/shared/SliderRow/styles.css +2 -1
  13. package/es/aics-image-viewer/shared/constants.js +1 -1
  14. package/es/aics-image-viewer/shared/utils/camera.js +122 -0
  15. package/es/aics-image-viewer/shared/utils/test/camera.test.js +127 -0
  16. package/es/aics-image-viewer/shared/utils/test/urlParsing.test.js +23 -702
  17. package/es/aics-image-viewer/shared/utils/urlParsing.js +14 -828
  18. package/es/aics-image-viewer/state/deserialize.js +437 -0
  19. package/es/aics-image-viewer/state/serialize.js +145 -0
  20. package/es/aics-image-viewer/state/test/deserialize.test.js +432 -0
  21. package/es/aics-image-viewer/state/test/serialize.test.js +97 -0
  22. package/es/aics-image-viewer/state/test/test_data.js +168 -0
  23. package/es/aics-image-viewer/state/types.js +265 -1
  24. package/package.json +2 -1
  25. package/type-declarations/aics-image-viewer/components/App/types.d.ts +1 -1
  26. package/type-declarations/aics-image-viewer/components/BottomPanel/index.d.ts +6 -3
  27. package/type-declarations/aics-image-viewer/components/CellViewerCanvasWrapper/index.d.ts +1 -0
  28. package/type-declarations/aics-image-viewer/components/{AxisClipSliders/index.d.ts → dimension_sliders/AxisClipSliders.d.ts} +2 -3
  29. package/type-declarations/aics-image-viewer/components/dimension_sliders/RotationSliders.d.ts +6 -0
  30. package/type-declarations/aics-image-viewer/components/dimension_sliders/SliderRows.d.ts +31 -0
  31. package/type-declarations/aics-image-viewer/components/shared/NumericInput/index.d.ts +1 -0
  32. package/type-declarations/aics-image-viewer/components/shared/SliderRow/index.d.ts +3 -1
  33. package/type-declarations/aics-image-viewer/shared/utils/camera.d.ts +19 -0
  34. package/type-declarations/aics-image-viewer/shared/utils/urlParsing.d.ts +1 -307
  35. package/type-declarations/aics-image-viewer/state/deserialize.d.ts +70 -0
  36. package/type-declarations/aics-image-viewer/state/serialize.d.ts +20 -0
  37. package/type-declarations/aics-image-viewer/state/types.d.ts +216 -0
  38. package/es/aics-image-viewer/components/AxisClipSliders/index.js +0 -295
@@ -0,0 +1,167 @@
1
+ function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
2
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
3
+ 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; } }
4
+ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
5
+ function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
6
+ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
7
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
8
+ function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
9
+ function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
10
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
11
+ import { CaretRightOutlined, PauseOutlined } from "@ant-design/icons";
12
+ import { Button, Tooltip } from "antd";
13
+ import React, { useCallback, useState } from "react";
14
+ import NumericInput from "../shared/NumericInput";
15
+ import SmarterSlider from "../shared/SmarterSlider";
16
+ import "./styles.css";
17
+ /** A single slider row, with a slider, one or two spinbox inputs, and a max value. */
18
+ export var DimensionSliderRow = function DimensionSliderRow(_ref) {
19
+ var label = _ref.label,
20
+ val = _ref.val,
21
+ _ref$valReadout = _ref.valReadout,
22
+ valReadout = _ref$valReadout === void 0 ? val : _ref$valReadout,
23
+ min = _ref.min,
24
+ max = _ref.max,
25
+ hideMax = _ref.hideMax,
26
+ unitSymbol = _ref.unitSymbol,
27
+ onSlide = _ref.onSlide,
28
+ _ref$onChange = _ref.onChange,
29
+ _onChange = _ref$onChange === void 0 ? onSlide : _ref$onChange,
30
+ onStart = _ref.onStart,
31
+ onEnd = _ref.onEnd;
32
+ return /*#__PURE__*/React.createElement("span", {
33
+ className: "axis-slider-container"
34
+ }, /*#__PURE__*/React.createElement("span", {
35
+ className: "slider-name"
36
+ }, label), max <= min ? /*#__PURE__*/React.createElement("i", null, "No values to adjust") : /*#__PURE__*/React.createElement("span", {
37
+ className: "axis-slider"
38
+ }, /*#__PURE__*/React.createElement(SmarterSlider, {
39
+ className: val.length <= 1 ? "slider-single-handle" : "",
40
+ connect: true,
41
+ range: {
42
+ min: min,
43
+ max: max
44
+ },
45
+ start: val,
46
+ step: 1,
47
+ margin: 1,
48
+ behaviour: "drag",
49
+ pips: {
50
+ mode: "positions",
51
+ values: [25, 50, 75],
52
+ density: 25,
53
+ format: {
54
+ // remove labels from pips
55
+ to: function to() {
56
+ return "";
57
+ }
58
+ }
59
+ }
60
+ // round slider output to nearest slice; assume any string inputs represent ints
61
+ ,
62
+ format: {
63
+ to: Math.round,
64
+ from: parseInt
65
+ },
66
+ onSlide: onSlide,
67
+ onChange: _onChange,
68
+ onStart: onStart,
69
+ onEnd: onEnd
70
+ })), max > min && /*#__PURE__*/React.createElement("span", {
71
+ className: "slider-values"
72
+ }, /*#__PURE__*/React.createElement(NumericInput, {
73
+ min: min,
74
+ max: max,
75
+ value: valReadout[0],
76
+ onChange: function onChange(value) {
77
+ return _onChange === null || _onChange === void 0 ? void 0 : _onChange([value].concat(_toConsumableArray(val.slice(1))));
78
+ },
79
+ unitSymbol: unitSymbol
80
+ }), val.length > 1 && /*#__PURE__*/React.createElement(React.Fragment, null, " , ", /*#__PURE__*/React.createElement(NumericInput, {
81
+ min: min,
82
+ max: max,
83
+ value: valReadout[1],
84
+ onChange: function onChange(value) {
85
+ return _onChange === null || _onChange === void 0 ? void 0 : _onChange([val[0], value].concat(_toConsumableArray(val.slice(2))));
86
+ },
87
+ unitSymbol: unitSymbol
88
+ })), !hideMax && /*#__PURE__*/React.createElement(React.Fragment, null, " / ", max)));
89
+ };
90
+
91
+ /** Extremely thin wrapper around `SliderRow` for adjusting 0-indexed values that the user should see as 1-indexed. */
92
+ export var IndexSliderRow = function IndexSliderRow(props) {
93
+ return /*#__PURE__*/React.createElement(DimensionSliderRow, {
94
+ label: props.label,
95
+ val: [props.val + 1],
96
+ valReadout: props.valReadout === undefined ? undefined : [props.valReadout + 1],
97
+ min: 1,
98
+ max: props.max,
99
+ onSlide: function onSlide(_ref2) {
100
+ var _props$onSlide;
101
+ var _ref3 = _slicedToArray(_ref2, 1),
102
+ value = _ref3[0];
103
+ return (_props$onSlide = props.onSlide) === null || _props$onSlide === void 0 ? void 0 : _props$onSlide.call(props, value - 1);
104
+ },
105
+ onChange: function onChange(_ref4) {
106
+ var _props$onChange;
107
+ var _ref5 = _slicedToArray(_ref4, 1),
108
+ value = _ref5[0];
109
+ return (_props$onChange = props.onChange) === null || _props$onChange === void 0 ? void 0 : _props$onChange.call(props, value - 1);
110
+ },
111
+ onStart: props.onStart,
112
+ onEnd: props.onEnd
113
+ });
114
+ };
115
+
116
+ /** Wrapper around `SliderRow` that adds a play button and accounts for the case where not all of an axis is loaded. */
117
+ export var PlaySliderRow = function PlaySliderRow(props) {
118
+ var onChange = props.onChange,
119
+ onStart = props.onStart,
120
+ onEnd = props.onEnd;
121
+ // In partially-loaded axes, stores the displayed value of the slider while the user is sliding it
122
+ var _useState = useState(props.val),
123
+ _useState2 = _slicedToArray(_useState, 2),
124
+ valReadout = _useState2[0],
125
+ setValReadout = _useState2[1];
126
+ // Tracks when the user is sliding the slider and `valReadout` may have to sub in for props
127
+ var _useState3 = useState(false),
128
+ _useState4 = _slicedToArray(_useState3, 2),
129
+ sliderHeld = _useState4[0],
130
+ setSliderHeld = _useState4[1];
131
+ var wrappedOnChange = useCallback(function (val) {
132
+ return onChange === null || onChange === void 0 ? void 0 : onChange(val);
133
+ }, [onChange]);
134
+ var wrappedSetValReadout = useCallback(function (val) {
135
+ return setValReadout(val);
136
+ }, []);
137
+ var wrappedOnStart = useCallback(function () {
138
+ setValReadout(props.val);
139
+ setSliderHeld(true);
140
+ onStart === null || onStart === void 0 || onStart();
141
+ }, [onStart, props.val]);
142
+ var wrappedOnEnd = useCallback(function () {
143
+ setSliderHeld(false);
144
+ onEnd === null || onEnd === void 0 || onEnd();
145
+ }, [onEnd]);
146
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(IndexSliderRow, {
147
+ label: props.label,
148
+ val: props.val,
149
+ valReadout: props.updateWhileSliding || !sliderHeld ? undefined : valReadout,
150
+ max: props.max,
151
+ onSlide: props.updateWhileSliding ? wrappedOnChange : wrappedSetValReadout,
152
+ onChange: props.updateWhileSliding ? undefined : wrappedOnChange,
153
+ onStart: wrappedOnStart,
154
+ onEnd: wrappedOnEnd
155
+ }), props.max > 1 && /*#__PURE__*/React.createElement(Tooltip, {
156
+ placement: "top",
157
+ title: "Play through sequence",
158
+ trigger: ["hover", "focus"]
159
+ }, /*#__PURE__*/React.createElement(Button, {
160
+ className: "slider-play-button",
161
+ onClick: function onClick() {
162
+ return props.onTogglePlayback(!props.playing);
163
+ },
164
+ icon: props.playing ? /*#__PURE__*/React.createElement(PauseOutlined, null) : /*#__PURE__*/React.createElement(CaretRightOutlined, null),
165
+ "aria-label": (props.playing ? "Pause " : "Play ") + props.label
166
+ })));
167
+ };
@@ -1,5 +1,5 @@
1
1
  .clip-sliders{
2
- --w-group-title:50px;
2
+ --w-group-title:60px;
3
3
  --w-axis-slider:480px;
4
4
  --w-name:10px;
5
5
  --w-values-3d:160px;
@@ -93,8 +93,14 @@
93
93
  .clip-sliders .slider-group .slider-group-title{
94
94
  flex:0 0 var(--w-group-title);
95
95
  margin:0;
96
+ font-family:var(--font-family);
97
+ color:var(--color-text-header);
96
98
  }
97
99
 
100
+ .clip-sliders .slider-group .slider-group-title.group-title-extra{
101
+ flex-basis:calc(var(--w-group-title) + var(--w-name) + 14px);
102
+ }
103
+
98
104
  .clip-sliders .slider-group .slider-group-rows{
99
105
  flex:1 1 auto;
100
106
  }
@@ -4,8 +4,8 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
4
4
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5
5
  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; } }
6
6
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
7
- import React from "react";
8
7
  import { CaretDownOutlined, CaretUpOutlined } from "@ant-design/icons";
8
+ import React from "react";
9
9
  import { useRefWithSetter } from "../../../shared/utils/hooks";
10
10
  import "./styles.css";
11
11
  /**
@@ -27,6 +27,8 @@ var NumericInput = function NumericInput(_ref) {
27
27
  disabled = _ref$disabled === void 0 ? false : _ref$disabled,
28
28
  _ref$className = _ref.className,
29
29
  className = _ref$className === void 0 ? "" : _ref$className,
30
+ _ref$unitSymbol = _ref.unitSymbol,
31
+ unitSymbol = _ref$unitSymbol === void 0 ? "" : _ref$unitSymbol,
30
32
  onChange = _ref.onChange;
31
33
  // While the input has focus, allow invalid input, just don't call `onChange` with it
32
34
  var _React$useState = React.useState(false),
@@ -100,7 +102,7 @@ var NumericInput = function NumericInput(_ref) {
100
102
  className: fullClassName,
101
103
  onKeyDown: onKeyDown
102
104
  }, /*#__PURE__*/React.createElement("input", {
103
- value: hasFocus ? textContent : displayedValue,
105
+ value: hasFocus ? textContent : displayedValue + unitSymbol,
104
106
  step: step,
105
107
  min: min,
106
108
  max: max,
@@ -23,10 +23,12 @@ var SliderRow = function SliderRow(props) {
23
23
  max: props.max
24
24
  },
25
25
  start: props.start,
26
+ disabled: props.disabled,
26
27
  connect: true,
27
28
  tooltips: true,
28
29
  behaviour: "drag",
29
30
  format: props.formatInteger ? INTEGER_FORMATTER : undefined,
31
+ onSlide: props.onSlide,
30
32
  onUpdate: props.onUpdate,
31
33
  onChange: props.onChange
32
34
  })), props.children && /*#__PURE__*/React.createElement("div", {
@@ -21,6 +21,7 @@
21
21
  align-items:center;
22
22
  }
23
23
 
24
- .viewer-control-row .control .control-slider{
24
+ .viewer-control-row .control .control-slider,
25
+ .viewer-control-row .control .control-extra:first-child{
25
26
  flex-grow:1;
26
27
  }
@@ -13,7 +13,7 @@ export var
13
13
  BACKGROUND_COLOR_DEFAULT = [0, 0, 0],
14
14
  BOUNDING_BOX_COLOR_DEFAULT = [255, 255, 255],
15
15
  AXIS_MARGIN_DEFAULT = [16, 16],
16
- SCALE_BAR_MARGIN_DEFAULT = [120, 12],
16
+ SCALE_BAR_MARGIN_DEFAULT = [220, 12],
17
17
  // These settings were chosen to work well with most AICS microscopy pipeline images.
18
18
  // These numbers mean: remap the bottom LUT_MIN_PERCENTILE fraction of pixels to zero intensity,
19
19
  // and linearly increase intensity up to the LUT_MAX_PERCENTILE fraction of pixels.
@@ -0,0 +1,122 @@
1
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
5
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
6
+ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
7
+ import { mat3, quat, vec3 } from "gl-matrix";
8
+ import React from "react";
9
+
10
+ /**
11
+ * Converts a `vec3` to a `[number, number, number]`.
12
+ *
13
+ * Technically, these are already equivalent types for all the math we do here, but the `vec3` type also includes any
14
+ * other indexable collection of `number`s, so this keeps the type checker happy.
15
+ */
16
+ var toArray = function toArray(v) {
17
+ return [v[0], v[1], v[2]];
18
+ };
19
+ var vecToTarget = function vecToTarget(state) {
20
+ return vec3.subtract(vec3.create(), state.target, state.position);
21
+ };
22
+
23
+ /** Transpose a 3x3 matrix */
24
+ var transpose = function transpose(matrix) {
25
+ var out = mat3.create();
26
+ mat3.transpose(out, matrix);
27
+ return out;
28
+ };
29
+
30
+ /** Multiply every vector in `state` by `matrix` */
31
+ export var applyMatrix = function applyMatrix(state, matrix) {
32
+ var position = toArray(vec3.transformMat3(vec3.create(), state.position, matrix));
33
+ var up = toArray(vec3.transformMat3(vec3.create(), state.up, matrix));
34
+ var target = toArray(vec3.transformMat3(vec3.create(), state.target, matrix));
35
+ return {
36
+ position: position,
37
+ up: up,
38
+ target: target
39
+ };
40
+ };
41
+ /** Orthonormal camera basis derived from `state`. */
42
+ var getBasis = function getBasis(state) {
43
+ var toTarget = vecToTarget(state);
44
+ var distance = vec3.length(toTarget);
45
+ var forward = vec3.scale(vec3.create(), toTarget, 1 / distance);
46
+ var right = vec3.cross(vec3.create(), forward, state.up);
47
+ vec3.normalize(right, right);
48
+ var up = vec3.cross(vec3.create(), right, forward);
49
+ return {
50
+ forward: forward,
51
+ right: right,
52
+ up: up,
53
+ distance: distance
54
+ };
55
+ };
56
+
57
+ /** Creates a XYZ rotation matrix with the given angles */
58
+ export var rotationMatrix = function rotationMatrix(x, y, z) {
59
+ var rotationQuat = quat.create();
60
+ quat.rotateX(rotationQuat, rotationQuat, x);
61
+ quat.rotateY(rotationQuat, rotationQuat, y);
62
+ quat.rotateZ(rotationQuat, rotationQuat, z);
63
+ var rotation = mat3.fromQuat(mat3.create(), rotationQuat);
64
+ return rotation;
65
+ };
66
+
67
+ /** Extract Tait-Bryan angles from a `CameraState` */
68
+ export var getRotationAngles = function getRotationAngles(state) {
69
+ var _getBasis = getBasis(state),
70
+ forward = _getBasis.forward,
71
+ right = _getBasis.right,
72
+ up = _getBasis.up;
73
+ var sy = Math.max(-1, Math.min(1, -forward[0]));
74
+ var y = Math.asin(sy);
75
+ if (Math.abs(sy) < 1 - 1e-6) {
76
+ return {
77
+ x: Math.atan2(forward[1], -forward[2]),
78
+ y: y,
79
+ z: Math.atan2(-up[0], right[0])
80
+ };
81
+ }
82
+ // Gimbal lock: y = ±90°, fold combined rotation into x.
83
+ return {
84
+ x: Math.atan2(up[2], up[1]),
85
+ y: y,
86
+ z: 0
87
+ };
88
+ };
89
+
90
+ /**
91
+ * Constructs a new `CameraState` equivalent to rotating `state` about the origin such that the camera returns to its
92
+ * default *orientation* (looking towards -Z, +Y up), though not necessarily its default *position*.
93
+ */
94
+ export var defaultOrientedCamera = function defaultOrientedCamera(state) {
95
+ var distance = vec3.length(vecToTarget(state));
96
+ // Rotate `target` about the origin, then derive `position` from `target`
97
+ var _getRotationAngles = getRotationAngles(state),
98
+ x = _getRotationAngles.x,
99
+ y = _getRotationAngles.y,
100
+ z = _getRotationAngles.z;
101
+ var matrix = transpose(rotationMatrix(x, y, z));
102
+ var target = toArray(vec3.transformMat3(vec3.create(), state.target, matrix));
103
+ var position = toArray(vec3.add(vec3.create(), target, vec3.fromValues(0, 0, distance)));
104
+ return {
105
+ target: target,
106
+ position: position,
107
+ up: [0, 1, 0]
108
+ };
109
+ };
110
+
111
+ /** Creates a callback that performs some action on the camera, by applying `transform` to the current camera state. */
112
+ export var useCameraCallback = function useCameraCallback(transform, view3d) {
113
+ return React.useCallback(function () {
114
+ var state = view3d.getCameraState();
115
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
116
+ args[_key] = arguments[_key];
117
+ }
118
+ var nextState = transform.apply(void 0, [state].concat(args));
119
+ view3d.setCameraState(nextState);
120
+ return _objectSpread(_objectSpread({}, state), nextState);
121
+ }, [view3d, transform]);
122
+ };
@@ -0,0 +1,127 @@
1
+ function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
2
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
3
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
4
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5
+ 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; } }
6
+ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
7
+ import { describe, expect, it } from "@jest/globals";
8
+ import { mat3 } from "gl-matrix";
9
+ import { applyMatrix, defaultOrientedCamera, getRotationAngles, rotationMatrix } from "../camera";
10
+ var expectCameraStateToBe = function expectCameraStateToBe(actual, expected, precision) {
11
+ console.log(actual.up, expected.up);
12
+ var _actual$position = _slicedToArray(actual.position, 3),
13
+ apx = _actual$position[0],
14
+ apy = _actual$position[1],
15
+ apz = _actual$position[2];
16
+ var _expected$position = _slicedToArray(expected.position, 3),
17
+ epx = _expected$position[0],
18
+ epy = _expected$position[1],
19
+ epz = _expected$position[2];
20
+ expect(apx).toBeCloseTo(epx, precision);
21
+ expect(apy).toBeCloseTo(epy, precision);
22
+ expect(apz).toBeCloseTo(epz, precision);
23
+ var _actual$target = _slicedToArray(actual.target, 3),
24
+ atx = _actual$target[0],
25
+ aty = _actual$target[1],
26
+ atz = _actual$target[2];
27
+ var _expected$target = _slicedToArray(expected.target, 3),
28
+ etx = _expected$target[0],
29
+ ety = _expected$target[1],
30
+ etz = _expected$target[2];
31
+ expect(atx).toBeCloseTo(etx, precision);
32
+ expect(aty).toBeCloseTo(ety, precision);
33
+ expect(atz).toBeCloseTo(etz, precision);
34
+ var _actual$up = _slicedToArray(actual.up, 3),
35
+ aux = _actual$up[0],
36
+ auy = _actual$up[1],
37
+ auz = _actual$up[2];
38
+ var _expected$up = _slicedToArray(expected.up, 3),
39
+ eux = _expected$up[0],
40
+ euy = _expected$up[1],
41
+ euz = _expected$up[2];
42
+ expect(aux).toBeCloseTo(eux, precision);
43
+ expect(auy).toBeCloseTo(euy, precision);
44
+ expect(auz).toBeCloseTo(euz, precision);
45
+ };
46
+ describe("applyMatrix", function () {
47
+ it("multiplies every vector in a `CameraState` by a matrix", function () {
48
+ var INPUT = {
49
+ position: [3, 0, 0],
50
+ up: [0, 1, 0],
51
+ target: [0, 0, 1]
52
+ };
53
+ var EXPECTED = {
54
+ position: [0, 0, 3],
55
+ up: [0, 2, 0],
56
+ target: [3, 0, 0]
57
+ };
58
+ var actual = applyMatrix(INPUT, mat3.fromValues(0, 0, 1, 0, 2, 0, 3, 0, 0));
59
+ expectCameraStateToBe(actual, EXPECTED);
60
+ });
61
+ it("properly handles the matrix in column-major order", function () {
62
+ var INPUT = {
63
+ position: [7, 11, 3],
64
+ up: [0, 0, 1],
65
+ target: [2, 3, 5]
66
+ };
67
+ var EXPECTED = {
68
+ position: [38, 31, 57],
69
+ up: [3, 2, 1],
70
+ target: [23, 17, 20]
71
+ };
72
+ var actual = applyMatrix(INPUT, mat3.fromValues(1, 2, 3, 2, 1, 3, 3, 2, 1));
73
+ expectCameraStateToBe(actual, EXPECTED);
74
+ });
75
+ });
76
+ describe("defaultOrientedCamera", function () {
77
+ var testDefaultOrientedCamera = function testDefaultOrientedCamera(input, position, target) {
78
+ var result = defaultOrientedCamera(input);
79
+ expectCameraStateToBe(result, {
80
+ position: position,
81
+ target: target,
82
+ up: [0, 1, 0]
83
+ });
84
+ };
85
+ it("returns the camera to its default orientation (towards -Z, with +Y up)", function () {
86
+ testDefaultOrientedCamera({
87
+ position: [Math.SQRT2 / 2, 0, Math.SQRT2 / 2],
88
+ target: [0, 0, 0],
89
+ up: [-1, 0, 0]
90
+ }, [0, 0, 1], [0, 0, 0]);
91
+ });
92
+ it("preserves the distance from `position` to `target`", function () {
93
+ testDefaultOrientedCamera({
94
+ position: [0, 1, 1],
95
+ target: [0, 0, 0],
96
+ up: [-1, 0, 0]
97
+ }, [0, 0, Math.SQRT2], [0, 0, 0]);
98
+ });
99
+ it("rotates both `target` and `position` about the origin", function () {
100
+ testDefaultOrientedCamera({
101
+ position: [-1, -2, 1],
102
+ target: [-1, -1, 1],
103
+ up: [-1, 0, 0]
104
+ }, [1, 1, 2], [1, 1, 1]);
105
+ });
106
+ });
107
+ describe("getRotationAngles", function () {
108
+ it("is the inverse of applying a rotation matrix to a default-oriented camera when -PI/2 < Y < PI/2", function () {
109
+ var testRotationAnglesInverse = function testRotationAnglesInverse(x, y, z, target) {
110
+ var state = {
111
+ position: [target[0], target[1], target[2] - 1],
112
+ target: target,
113
+ up: [0, 1, 0]
114
+ };
115
+ var rotated = applyMatrix(defaultOrientedCamera(state), rotationMatrix(x, y, z));
116
+ var result = getRotationAngles(rotated);
117
+ expect(result.x).toBeCloseTo(x);
118
+ expect(result.y).toBeCloseTo(y);
119
+ expect(result.z).toBeCloseTo(z);
120
+ };
121
+ testRotationAnglesInverse(Math.PI / 2, 0, 0, [0, 0, 0]);
122
+ testRotationAnglesInverse(Math.PI / 2, 0, 0, [1, 2, 3]);
123
+ testRotationAnglesInverse(Math.PI / 3, Math.PI / 3, Math.PI / 3, [3, 3, 3]);
124
+ testRotationAnglesInverse(Math.PI, Math.PI / 4, -Math.PI, [3, 3, 3]);
125
+ testRotationAnglesInverse(1, -0.8, 3, [0.5, 0.8, 1.2]);
126
+ });
127
+ });