@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.
- package/es/aics-image-viewer/components/App/index.js +1 -0
- package/es/aics-image-viewer/components/BottomPanel/index.js +48 -19
- package/es/aics-image-viewer/components/BottomPanel/styles.css +16 -7
- package/es/aics-image-viewer/components/CellViewerCanvasWrapper/index.js +39 -19
- package/es/aics-image-viewer/components/ControlPanel/index.js +2 -0
- package/es/aics-image-viewer/components/dimension_sliders/AxisClipSliders.js +148 -0
- package/es/aics-image-viewer/components/dimension_sliders/RotationSliders.js +167 -0
- package/es/aics-image-viewer/components/dimension_sliders/SliderRows.js +167 -0
- package/es/aics-image-viewer/components/{AxisClipSliders → dimension_sliders}/styles.css +7 -1
- package/es/aics-image-viewer/components/shared/NumericInput/index.js +4 -2
- package/es/aics-image-viewer/components/shared/SliderRow/index.js +2 -0
- package/es/aics-image-viewer/components/shared/SliderRow/styles.css +2 -1
- package/es/aics-image-viewer/shared/constants.js +1 -1
- package/es/aics-image-viewer/shared/utils/camera.js +122 -0
- package/es/aics-image-viewer/shared/utils/test/camera.test.js +127 -0
- package/es/aics-image-viewer/shared/utils/test/urlParsing.test.js +23 -702
- package/es/aics-image-viewer/shared/utils/urlParsing.js +14 -828
- package/es/aics-image-viewer/state/deserialize.js +437 -0
- package/es/aics-image-viewer/state/serialize.js +145 -0
- package/es/aics-image-viewer/state/test/deserialize.test.js +432 -0
- package/es/aics-image-viewer/state/test/serialize.test.js +97 -0
- package/es/aics-image-viewer/state/test/test_data.js +168 -0
- package/es/aics-image-viewer/state/types.js +265 -1
- package/package.json +2 -1
- package/type-declarations/aics-image-viewer/components/App/types.d.ts +1 -1
- package/type-declarations/aics-image-viewer/components/BottomPanel/index.d.ts +6 -3
- package/type-declarations/aics-image-viewer/components/CellViewerCanvasWrapper/index.d.ts +1 -0
- package/type-declarations/aics-image-viewer/components/{AxisClipSliders/index.d.ts → dimension_sliders/AxisClipSliders.d.ts} +2 -3
- package/type-declarations/aics-image-viewer/components/dimension_sliders/RotationSliders.d.ts +6 -0
- package/type-declarations/aics-image-viewer/components/dimension_sliders/SliderRows.d.ts +31 -0
- package/type-declarations/aics-image-viewer/components/shared/NumericInput/index.d.ts +1 -0
- package/type-declarations/aics-image-viewer/components/shared/SliderRow/index.d.ts +3 -1
- package/type-declarations/aics-image-viewer/shared/utils/camera.d.ts +19 -0
- package/type-declarations/aics-image-viewer/shared/utils/urlParsing.d.ts +1 -307
- package/type-declarations/aics-image-viewer/state/deserialize.d.ts +70 -0
- package/type-declarations/aics-image-viewer/state/serialize.d.ts +20 -0
- package/type-declarations/aics-image-viewer/state/types.d.ts +216 -0
- 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:
|
|
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", {
|
|
@@ -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 = [
|
|
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
|
+
});
|