cloudmr-ux 4.2.9 → 4.3.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/README.md CHANGED
@@ -19,6 +19,5 @@ npx parcel public/index.html
19
19
  that's it!
20
20
 
21
21
  ## License
22
- MIT ©
23
- [erosmontin](https://github.com/erosmontin)
22
+ The code is released under the MIT License
24
23
 
@@ -0,0 +1,72 @@
1
+ import React from "react";
2
+ /**
3
+ * Everything the component needs to drive the three slice sliders.
4
+ *
5
+ * The parent should get `mins`, `maxs`, and `mms` from Niivue's
6
+ * `onLocationChange` callback so the sliders stay in sync with scroll/click.
7
+ *
8
+ * The `nv` instance is typed `any` so `cloudmr-ux` doesn't need to take a
9
+ * hard dependency on `@niivue/niivue` — any version of the Niivue object works
10
+ * as long as it exposes `scene.crosshairPos`, `mm2frac`, `frac2mm`,
11
+ * `volumes[0].getImageMetadata()`, and `drawScene()`.
12
+ */
13
+ export interface NiivueSlicePositionProps {
14
+ /** The Niivue instance. */
15
+ nv: any;
16
+ /**
17
+ * World-space bounding-box minimums [xMin, yMin, zMin] in mm.
18
+ * Comes from Niivue's `onLocationChange` data.
19
+ */
20
+ mins: number[];
21
+ /**
22
+ * World-space bounding-box maximums [xMax, yMax, zMax] in mm.
23
+ * Comes from Niivue's `onLocationChange` data.
24
+ */
25
+ maxs: number[];
26
+ /**
27
+ * Current crosshair position [x, y, z] in mm.
28
+ * Comes from Niivue's `onLocationChange` data.
29
+ */
30
+ mms: number[];
31
+ /**
32
+ * Heading displayed above the sliders.
33
+ * @default "Slice Position"
34
+ */
35
+ title?: string;
36
+ /**
37
+ * CSS accent color for all three range inputs.
38
+ * @default "#580f8b"
39
+ */
40
+ accentColor?: string;
41
+ style?: React.CSSProperties;
42
+ className?: string;
43
+ }
44
+ /**
45
+ * **NiivueSlicePosition**
46
+ *
47
+ * A reusable "Slice Position" control panel that drives a Niivue viewer.
48
+ * Renders three labeled sliders — X, Y, and Z — each paired with an editable
49
+ * number field. All sliders snap to exact voxel centres so they stay in sync
50
+ * with Niivue's own scroll behaviour.
51
+ *
52
+ * ### Wiring it up
53
+ *
54
+ * ```tsx
55
+ * // In the parent that owns the Niivue instance:
56
+ * const [mms, setMms] = useState([0, 0, 0]);
57
+ * const [mins, setMins] = useState([0, 0, 0]);
58
+ * const [maxs, setMaxs] = useState([1, 1, 1]);
59
+ *
60
+ * // Give these to Niivue so it calls back on every crosshair move:
61
+ * nv.opts.onLocationChange = (data) => {
62
+ * setMms([data.mm[0], data.mm[1], data.mm[2]]);
63
+ * setMins([data.vox[0]?.min ?? 0, data.vox[1]?.min ?? 0, data.vox[2]?.min ?? 0]);
64
+ * setMaxs([data.vox[0]?.max ?? 1, data.vox[1]?.max ?? 1, data.vox[2]?.max ?? 1]);
65
+ * };
66
+ *
67
+ * // Then render:
68
+ * <NiivueSlicePosition nv={nv} mins={mins} maxs={maxs} mms={mms} />
69
+ * ```
70
+ */
71
+ export declare function NiivueSlicePosition({ nv, mins, maxs, mms, title, accentColor, style, className, }: NiivueSlicePositionProps): import("react/jsx-runtime").JSX.Element;
72
+ export default NiivueSlicePosition;
@@ -0,0 +1,255 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import React from "react";
14
+ import { Card, CardContent } from "@mui/material";
15
+ import CmrLabel from "../label/Label";
16
+ // ─── Helpers (pure, no React) ─────────────────────────────────────────────────
17
+ var safeSpan = function (min, max) { return Math.max(1e-9, max - min); };
18
+ var clamp = function (v, lo, hi) { return Math.max(lo, Math.min(hi, v)); };
19
+ var round3 = function (v) { return Math.round(v * 1000) / 1000; };
20
+ // ─── Component ────────────────────────────────────────────────────────────────
21
+ /**
22
+ * **NiivueSlicePosition**
23
+ *
24
+ * A reusable "Slice Position" control panel that drives a Niivue viewer.
25
+ * Renders three labeled sliders — X, Y, and Z — each paired with an editable
26
+ * number field. All sliders snap to exact voxel centres so they stay in sync
27
+ * with Niivue's own scroll behaviour.
28
+ *
29
+ * ### Wiring it up
30
+ *
31
+ * ```tsx
32
+ * // In the parent that owns the Niivue instance:
33
+ * const [mms, setMms] = useState([0, 0, 0]);
34
+ * const [mins, setMins] = useState([0, 0, 0]);
35
+ * const [maxs, setMaxs] = useState([1, 1, 1]);
36
+ *
37
+ * // Give these to Niivue so it calls back on every crosshair move:
38
+ * nv.opts.onLocationChange = (data) => {
39
+ * setMms([data.mm[0], data.mm[1], data.mm[2]]);
40
+ * setMins([data.vox[0]?.min ?? 0, data.vox[1]?.min ?? 0, data.vox[2]?.min ?? 0]);
41
+ * setMaxs([data.vox[0]?.max ?? 1, data.vox[1]?.max ?? 1, data.vox[2]?.max ?? 1]);
42
+ * };
43
+ *
44
+ * // Then render:
45
+ * <NiivueSlicePosition nv={nv} mins={mins} maxs={maxs} mms={mms} />
46
+ * ```
47
+ */
48
+ export function NiivueSlicePosition(_a) {
49
+ var _b, _c, _d, _e, _f;
50
+ var nv = _a.nv, mins = _a.mins, maxs = _a.maxs, mms = _a.mms, _g = _a.title, title = _g === void 0 ? "Slice Position" : _g, _h = _a.accentColor, accentColor = _h === void 0 ? "#580f8b" : _h, style = _a.style, className = _a.className;
51
+ // ── Derive voxel grid from the loaded volume ─────────────────────────────
52
+ var vol = (_b = nv === null || nv === void 0 ? void 0 : nv.volumes) === null || _b === void 0 ? void 0 : _b[0];
53
+ var meta = (_c = vol === null || vol === void 0 ? void 0 : vol.getImageMetadata) === null || _c === void 0 ? void 0 : _c.call(vol);
54
+ var nx = Math.max(1, (_d = meta === null || meta === void 0 ? void 0 : meta.nx) !== null && _d !== void 0 ? _d : 1);
55
+ var ny = Math.max(1, (_e = meta === null || meta === void 0 ? void 0 : meta.ny) !== null && _e !== void 0 ? _e : 1);
56
+ var nz = Math.max(1, (_f = meta === null || meta === void 0 ? void 0 : meta.nz) !== null && _f !== void 0 ? _f : 1);
57
+ // ── Slider bounds for X and Y ────────────────────────────────────────────
58
+ var spanX = safeSpan(mins[0], maxs[0]);
59
+ var spanY = safeSpan(mins[1], maxs[1]);
60
+ var stepX = nx > 1 ? spanX / nx : spanX * 0.01;
61
+ var stepY = ny > 1 ? spanY / ny : spanY * 0.01;
62
+ var sliderMinX = nx > 1 ? mins[0] + 0.5 * stepX : mins[0];
63
+ var sliderMaxX = nx > 1 ? maxs[0] - 0.5 * stepX : maxs[0];
64
+ var sliderMinY = ny > 1 ? mins[1] + 0.5 * stepY : mins[1];
65
+ var sliderMaxY = ny > 1 ? maxs[1] - 0.5 * stepY : maxs[1];
66
+ // ── Slider bounds for Z (uses actual Niivue slice centres) ───────────────
67
+ var zAtStart;
68
+ var zAtEnd;
69
+ if (nz <= 1) {
70
+ zAtStart = mins[2];
71
+ zAtEnd = maxs[2];
72
+ }
73
+ else {
74
+ try {
75
+ var cx = nv.scene.crosshairPos[0];
76
+ var cy = nv.scene.crosshairPos[1];
77
+ zAtStart = nv.frac2mm([cx, cy, 0.5 / nz])[2];
78
+ zAtEnd = nv.frac2mm([cx, cy, (nz - 0.5) / nz])[2];
79
+ }
80
+ catch (_j) {
81
+ var s = safeSpan(mins[2], maxs[2]) / nz;
82
+ zAtStart = mins[2] + 0.5 * s;
83
+ zAtEnd = maxs[2] - 0.5 * s;
84
+ }
85
+ }
86
+ var sliderMinZ = Math.min(zAtStart, zAtEnd);
87
+ var sliderMaxZ = Math.max(zAtStart, zAtEnd);
88
+ var stepZ = nz > 1
89
+ ? Math.abs(zAtEnd - zAtStart) / (nz - 1)
90
+ : Math.max(1e-9, Math.abs(sliderMaxZ - sliderMinZ) * 0.01);
91
+ // ── Fractional helpers ───────────────────────────────────────────────────
92
+ var ratioAxis = function (val, axis) {
93
+ return (val - mins[axis]) / safeSpan(mins[axis], maxs[axis]);
94
+ };
95
+ var mmToFrac = function (x, y, z) {
96
+ try {
97
+ return nv.mm2frac([x, y, z]);
98
+ }
99
+ catch (_a) {
100
+ return [ratioAxis(x, 0), ratioAxis(y, 1), ratioAxis(z, 2)];
101
+ }
102
+ };
103
+ /** Snap a mm value to the nearest voxel centre on the given axis. */
104
+ var snapToVoxel = function (mm, axis) {
105
+ var n = axis === 0 ? nx : axis === 1 ? ny : nz;
106
+ var mm3 = [mmsRef.current[0], mmsRef.current[1], mmsRef.current[2]];
107
+ mm3[axis] = mm;
108
+ var frac;
109
+ try {
110
+ frac = nv.mm2frac(mm3);
111
+ }
112
+ catch (_a) {
113
+ frac = [
114
+ ratioAxis(mmsRef.current[0], 0),
115
+ ratioAxis(mmsRef.current[1], 1),
116
+ ratioAxis(mmsRef.current[2], 2),
117
+ ];
118
+ frac[axis] = ratioAxis(mm, axis);
119
+ }
120
+ var idx = Math.round(frac[axis] * n - 0.5);
121
+ var fracSnapped = n > 1 ? (clamp(idx, 0, n - 1) + 0.5) / n : 0.5;
122
+ frac[axis] = fracSnapped;
123
+ try {
124
+ return nv.frac2mm(frac)[axis];
125
+ }
126
+ catch (_b) {
127
+ return mins[axis] + fracSnapped * safeSpan(mins[axis], maxs[axis]);
128
+ }
129
+ };
130
+ // ── Local slider state (mirrors mms; synced by useEffect) ────────────────
131
+ var _k = React.useState(round3(mms[0])), xVal = _k[0], setXVal = _k[1];
132
+ var _l = React.useState(round3(mms[1])), yVal = _l[0], setYVal = _l[1];
133
+ var _m = React.useState(round3(mms[2])), zVal = _m[0], setZVal = _m[1];
134
+ // Keep a ref so snapToVoxel can read the *latest* values without stale closure
135
+ var mmsRef = React.useRef([xVal, yVal, zVal]);
136
+ React.useEffect(function () {
137
+ mmsRef.current = [xVal, yVal, zVal];
138
+ }, [xVal, yVal, zVal]);
139
+ // Sync from Niivue (e.g. mouse scroll, click)
140
+ React.useEffect(function () {
141
+ var fmt = function (v) { return (Number.isFinite(v) ? round3(v) : 0); };
142
+ setXVal(fmt(mms[0]));
143
+ setYVal(fmt(mms[1]));
144
+ setZVal(fmt(mms[2]));
145
+ }, [mms]);
146
+ // ── Apply handlers ────────────────────────────────────────────────────────
147
+ var applyX = function (val) {
148
+ var v = clamp(snapToVoxel(val, 0), sliderMinX, sliderMaxX);
149
+ setXVal(v);
150
+ nv.scene.crosshairPos = mmToFrac(v, mmsRef.current[1], mmsRef.current[2]);
151
+ nv.drawScene();
152
+ };
153
+ var applyY = function (val) {
154
+ var v = clamp(snapToVoxel(val, 1), sliderMinY, sliderMaxY);
155
+ setYVal(v);
156
+ nv.scene.crosshairPos = mmToFrac(mmsRef.current[0], v, mmsRef.current[2]);
157
+ nv.drawScene();
158
+ };
159
+ var applyZBySliceIndex = function (kRaw) {
160
+ if (nz <= 1) {
161
+ var cx_1 = nv.scene.crosshairPos[0];
162
+ var cy_1 = nv.scene.crosshairPos[1];
163
+ nv.scene.crosshairPos = [cx_1, cy_1, 0.5];
164
+ nv.drawScene();
165
+ try {
166
+ setZVal(round3(nv.frac2mm([cx_1, cy_1, 0.5])[2]));
167
+ }
168
+ catch ( /* ignore */_a) { /* ignore */ }
169
+ return;
170
+ }
171
+ var k = clamp(Math.round(kRaw), 0, nz - 1);
172
+ var cx = nv.scene.crosshairPos[0];
173
+ var cy = nv.scene.crosshairPos[1];
174
+ var fz = (k + 0.5) / nz;
175
+ nv.scene.crosshairPos = [cx, cy, fz];
176
+ nv.drawScene();
177
+ try {
178
+ setZVal(round3(nv.frac2mm([cx, cy, fz])[2]));
179
+ }
180
+ catch (_b) {
181
+ var spanLen = zAtEnd - zAtStart;
182
+ setZVal(round3(zAtStart + (k * spanLen) / Math.max(1, nz - 1)));
183
+ }
184
+ };
185
+ var applyZ = function (val) {
186
+ if (!Number.isFinite(val))
187
+ return;
188
+ if (nz <= 1) {
189
+ var v = clamp(snapToVoxel(val, 2), sliderMinZ, sliderMaxZ);
190
+ setZVal(round3(v));
191
+ nv.scene.crosshairPos = mmToFrac(mmsRef.current[0], mmsRef.current[1], v);
192
+ nv.drawScene();
193
+ return;
194
+ }
195
+ var fracZ;
196
+ try {
197
+ fracZ = nv.mm2frac([mmsRef.current[0], mmsRef.current[1], val])[2];
198
+ }
199
+ catch (_a) {
200
+ fracZ = ratioAxis(val, 2);
201
+ }
202
+ applyZBySliceIndex(Math.round(fracZ * nz - 0.5));
203
+ };
204
+ // Current Z as a discrete slice index (for the integer-step Z range input)
205
+ var zSliceIndex = 0;
206
+ if (nz > 1) {
207
+ var fracZ = void 0;
208
+ try {
209
+ fracZ = nv.mm2frac([xVal, yVal, zVal])[2];
210
+ }
211
+ catch (_o) {
212
+ fracZ = ratioAxis(zVal, 2);
213
+ }
214
+ zSliceIndex = clamp(Math.round(fracZ * nz - 0.5), 0, nz - 1);
215
+ }
216
+ // ── Shared styles ─────────────────────────────────────────────────────────
217
+ var inputStyle = {
218
+ width: 100,
219
+ padding: "4px 6px",
220
+ borderRadius: 6,
221
+ border: "1px solid #ccc",
222
+ fontSize: "0.9rem"
223
+ };
224
+ var rowStyle = {
225
+ display: "flex",
226
+ alignItems: "center",
227
+ gap: 10,
228
+ marginBottom: 6
229
+ };
230
+ var sliderStyle = {
231
+ width: "100%",
232
+ accentColor: accentColor
233
+ };
234
+ // ── Render ────────────────────────────────────────────────────────────────
235
+ return (_jsxs("div", __assign({ style: style, className: className }, { children: [title !== "" && (_jsx("div", __assign({ className: "title", style: { width: "100%" } }, { children: title }))), _jsx(Card, __assign({ variant: "outlined", sx: { mb: 2, borderTopLeftRadius: 0, borderTopRightRadius: 0 } }, { children: _jsx(CardContent, { children: _jsxs("div", __assign({ style: { display: "flex", flexDirection: "column" } }, { children: [_jsxs("div", __assign({ style: { marginBottom: 20 } }, { children: [_jsxs("div", __assign({ style: rowStyle }, { children: [_jsx(CmrLabel, { children: "X:" }), _jsx("input", { type: "number", value: xVal.toFixed(3), min: sliderMinX, max: sliderMaxX, step: stepX, style: inputStyle, onChange: function (e) {
236
+ var next = Number(e.target.value);
237
+ if (Number.isFinite(next))
238
+ applyX(next);
239
+ }, onBlur: function (e) {
240
+ applyX(clamp(Number(e.target.value), sliderMinX, sliderMaxX));
241
+ } })] })), _jsx("input", { id: "xSlice", type: "range", min: sliderMinX, max: sliderMaxX, step: stepX, value: clamp(xVal, sliderMinX, sliderMaxX), style: sliderStyle, onChange: function (e) { return applyX(Number(e.target.value)); } })] })), _jsxs("div", __assign({ style: { marginBottom: 20 } }, { children: [_jsxs("div", __assign({ style: rowStyle }, { children: [_jsx(CmrLabel, { children: "Y:" }), _jsx("input", { type: "number", value: yVal.toFixed(3), min: sliderMinY, max: sliderMaxY, step: stepY, style: inputStyle, onChange: function (e) {
242
+ var next = Number(e.target.value);
243
+ if (Number.isFinite(next))
244
+ applyY(next);
245
+ }, onBlur: function (e) {
246
+ applyY(clamp(Number(e.target.value), sliderMinY, sliderMaxY));
247
+ } })] })), _jsx("input", { id: "ySlice", type: "range", min: sliderMinY, max: sliderMaxY, step: stepY, value: clamp(yVal, sliderMinY, sliderMaxY), style: sliderStyle, onChange: function (e) { return applyY(Number(e.target.value)); } })] })), _jsxs("div", { children: [_jsxs("div", __assign({ style: rowStyle }, { children: [_jsx(CmrLabel, { children: "Z:" }), _jsx("input", { type: "number", value: zVal.toFixed(3), min: sliderMinZ, max: sliderMaxZ, step: stepZ, style: inputStyle, onChange: function (e) {
248
+ var next = Number(e.target.value);
249
+ if (Number.isFinite(next))
250
+ applyZ(next);
251
+ }, onBlur: function (e) {
252
+ applyZ(clamp(Number(e.target.value), sliderMinZ, sliderMaxZ));
253
+ } })] })), _jsx("input", { id: "zSlice", type: "range", min: 0, max: Math.max(0, nz - 1), step: 1, value: zSliceIndex, style: sliderStyle, onChange: function (e) { return applyZBySliceIndex(Number(e.target.value)); } })] })] })) }) }))] })));
254
+ }
255
+ export default NiivueSlicePosition;
@@ -48,16 +48,31 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
48
48
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
49
49
  import { CmrTable, CMRUpload, CmrConfirmation } from "../../../index";
50
50
  import { useState } from "react";
51
- import { Tooltip, IconButton } from "@mui/material";
51
+ import { Tooltip, IconButton, } from "@mui/material";
52
52
  import { getEndpoints } from "../../config/AppConfig";
53
53
  import { useAppDispatch, useAppSelector } from "../../store/hooks";
54
54
  import Box from "@mui/material/Box";
55
55
  import VisibilityIcon from "@mui/icons-material/Visibility";
56
56
  import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
57
- import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
58
- import { faObjectGroup, faObjectUngroup, faDownload, faTrash, } from "@fortawesome/free-solid-svg-icons";
57
+ import GetAppIcon from "@mui/icons-material/GetApp";
58
+ import DeleteIcon from "@mui/icons-material/Delete";
59
+ import { Icon as WpIcon, group as wpGroup, ungroup as wpUngroup } from "@wordpress/icons";
59
60
  import { getPipelineROI } from "../../features/rois/resultActionCreation";
60
61
  import { AuthenticatedHttpClient } from "../../common/utilities/AuthenticatedRequests";
62
+ /** Default merged ROI label in NiivuePatcher `groupLabelsInto` — keep in sync with patcher */
63
+ var GROUP_TARGET_LABEL = 7;
64
+ /** Toolbar action icons: `action.active` → rgba(0, 0, 0, 0.54) in default MUI light theme */
65
+ var ROI_TOOLBAR_ICON_SIZE_PX = 24;
66
+ var ROI_TOOLBAR_ICON_BUTTON_SX = {
67
+ color: "action.active",
68
+ "&.Mui-disabled": {
69
+ color: function (theme) { return theme.palette.action.disabled; }
70
+ }
71
+ };
72
+ var ROI_TOOLBAR_MUI_ICON_SX = {
73
+ fontSize: ROI_TOOLBAR_ICON_SIZE_PX,
74
+ color: "inherit"
75
+ };
61
76
  export var ROITable = function (props) {
62
77
  // const rois:ROI[] = useAppSelector(state=>{
63
78
  // return (state.roi.rois[props.pipelineID]==undefined)?[]:state.roi.rois[props.pipelineID];
@@ -163,6 +178,15 @@ export var ROITable = function (props) {
163
178
  setWarningMessage(message);
164
179
  setWarningVisible(true);
165
180
  };
181
+ var selectedNums = selectedData.map(function (v) { return Number(v); });
182
+ var uniqueSelected = new Set(selectedNums);
183
+ var canGroupSelection = selectedNums.length >= 2 && uniqueSelected.size >= 2;
184
+ var groupButtonDisabled = selectedData.length > 0 && !canGroupSelection;
185
+ var groupTooltip = selectedData.length === 0
186
+ ? "Group selected ROIs"
187
+ : uniqueSelected.size < 2 || selectedNums.length < 2
188
+ ? "Select at least two different ROIs to group"
189
+ : "Group selected ROIs";
166
190
  return (_jsxs(Box, __assign({ style: props.style }, { children: [_jsx(CmrTable
167
191
  // sx={{
168
192
  // borderBottomLeftRadius: 0,
@@ -192,15 +216,28 @@ export var ROITable = function (props) {
192
216
  border: "1px solid rgba(0, 0, 0, 0.12)",
193
217
  borderTop: "none",
194
218
  borderRadius: "0 0 4px 4px"
195
- } }, { children: [_jsx(Tooltip, __assign({ title: "Group" }, { children: _jsx(IconButton, __assign({ onClick: function () {
196
- props.nv.groupLabelsInto(selectedData.map(function (value) { return Number(value); }));
197
- props.nv.drawScene();
198
- props.resampleImage();
199
- } }, { children: _jsx(FontAwesomeIcon, { icon: faObjectGroup, style: { fontSize: "16px" } }) })) })), _jsx(Tooltip, __assign({ title: "Ungroup" }, { children: _jsx(IconButton, __assign({ onClick: function () {
219
+ } }, { children: [_jsx(Tooltip, __assign({ title: groupTooltip }, { children: _jsx("span", { children: _jsx(IconButton, __assign({ disabled: groupButtonDisabled, sx: ROI_TOOLBAR_ICON_BUTTON_SX, onClick: function () {
220
+ if (selectedData.length === 0) {
221
+ warnEmptySelection("Please select an ROI to group");
222
+ return;
223
+ }
224
+ if (!canGroupSelection) {
225
+ warnEmptySelection("Please select at least two different ROIs to group");
226
+ return;
227
+ }
228
+ if (typeof props.nv.groupLabelsFromSelection === "function") {
229
+ props.nv.groupLabelsFromSelection(selectedNums, GROUP_TARGET_LABEL);
230
+ }
231
+ else {
232
+ props.nv.groupLabelsInto(selectedNums, GROUP_TARGET_LABEL);
233
+ }
234
+ props.nv.drawScene();
235
+ props.resampleImage();
236
+ } }, { children: _jsx(WpIcon, { icon: wpGroup, size: ROI_TOOLBAR_ICON_SIZE_PX, fill: "currentColor" }) })) }) })), _jsx(Tooltip, __assign({ title: "Ungroup" }, { children: _jsx(IconButton, __assign({ sx: ROI_TOOLBAR_ICON_BUTTON_SX, onClick: function () {
200
237
  props.nv.ungroup();
201
238
  props.nv.drawScene();
202
239
  props.resampleImage();
203
- } }, { children: _jsx(FontAwesomeIcon, { icon: faObjectUngroup, style: { fontSize: "16px" } }) })) })), _jsx(Tooltip, __assign({ title: "Download" }, { children: _jsx(IconButton, __assign({ onClick: function () { return __awaiter(void 0, void 0, void 0, function () {
240
+ } }, { children: _jsx(WpIcon, { icon: wpUngroup, size: 16, fill: "currentColor" }) })) })), _jsx(Tooltip, __assign({ title: "Download" }, { children: _jsx(IconButton, __assign({ sx: ROI_TOOLBAR_ICON_BUTTON_SX, onClick: function () { return __awaiter(void 0, void 0, void 0, function () {
204
241
  var fileName, selectedLabels, _i, selectedData_1, label;
205
242
  return __generator(this, function (_a) {
206
243
  switch (_a.label) {
@@ -223,11 +260,11 @@ export var ROITable = function (props) {
223
260
  return [2 /*return*/];
224
261
  }
225
262
  });
226
- }); } }, { children: _jsx(FontAwesomeIcon, { icon: faDownload, style: { fontSize: "16px" } }) })) })), _jsx(Tooltip, __assign({ title: "Delete" }, { children: _jsx(IconButton, __assign({ onClick: function () {
263
+ }); } }, { children: _jsx(GetAppIcon, { sx: ROI_TOOLBAR_MUI_ICON_SX }) })) })), _jsx(Tooltip, __assign({ title: "Delete" }, { children: _jsx(IconButton, __assign({ sx: ROI_TOOLBAR_ICON_BUTTON_SX, onClick: function () {
227
264
  props.nv.deleteDrawingByLabel(selectedData.map(function (value) { return Number(value); }));
228
265
  props.resampleImage();
229
266
  props.nv.drawScene();
230
- } }, { children: _jsx(FontAwesomeIcon, { icon: faTrash, style: { fontSize: "16px" } }) })) })), _jsx(CMRUpload, { changeNameAfterUpload: false, color: "primary", onUploaded: function (res, file) { }, uploadHandler: function (file) { return __awaiter(void 0, void 0, void 0, function () {
267
+ } }, { children: _jsx(DeleteIcon, { sx: ROI_TOOLBAR_MUI_ICON_SX }) })) })), _jsx(CMRUpload, { changeNameAfterUpload: false, color: "primary", onUploaded: function (res, file) { }, uploadHandler: function (file) { return __awaiter(void 0, void 0, void 0, function () {
231
268
  var filename, response;
232
269
  return __generator(this, function (_a) {
233
270
  switch (_a.label) {
@@ -1051,56 +1051,92 @@ export default function NiiVueport(props) {
1051
1051
  });
1052
1052
  });
1053
1053
  };
1054
- var unzipAndRenderDrawingLayer = function (accessURL) { return __awaiter(_this, void 0, void 0, function () {
1055
- var response, blob, zip, fileInfo, content, info_1, niiFilePath, niiDrawing, base64;
1056
- return __generator(this, function (_a) {
1057
- switch (_a.label) {
1058
- case 0: return [4 /*yield*/, fetch(accessURL)];
1059
- case 1:
1060
- response = _a.sent();
1061
- // Check if the request was successful
1062
- if (!response.ok) {
1063
- props.warn("Failed to load the requested ROI Layer");
1064
- return [2 /*return*/];
1065
- }
1066
- return [4 /*yield*/, response.blob()];
1067
- case 2:
1068
- blob = _a.sent();
1069
- zip = new JSZip();
1070
- return [4 /*yield*/, zip.loadAsync(blob)];
1071
- case 3:
1072
- _a.sent();
1073
- fileInfo = zip.file("info.json");
1074
- if (!fileInfo) return [3 /*break*/, 8];
1075
- return [4 /*yield*/, fileInfo.async("string")];
1076
- case 4:
1077
- content = _a.sent();
1078
- info_1 = JSON.parse(content);
1079
- niiFilePath = info_1.data[0].filename;
1080
- niiDrawing = zip.file(niiFilePath);
1081
- if (!niiDrawing) return [3 /*break*/, 6];
1082
- return [4 /*yield*/, niiDrawing.async("base64")];
1083
- case 5:
1084
- base64 = _a.sent();
1085
- console.log(niiFilePath);
1086
- nv.loadDrawingFromBase64(niiFilePath, base64).then(function () {
1087
- setLabelMapping(info_1.data[0].labelMapping);
1088
- resampleImage(info_1.data[0].labelMapping);
1089
- });
1090
- return [3 /*break*/, 7];
1091
- case 6:
1092
- console.log("".concat(niiFilePath, " not found in the ZIP file."));
1093
- props.warn("".concat(niiFilePath, " not found in the ZIP file."));
1094
- return [2 /*return*/, null];
1095
- case 7: return [3 /*break*/, 9];
1096
- case 8:
1097
- console.log("info.json not found in the ZIP file.");
1098
- props.warn("info.json not found in the ZIP file.");
1099
- return [2 /*return*/, null];
1100
- case 9: return [2 /*return*/];
1101
- }
1054
+ /** mergeIntoExisting: true for ROI file upload; false when opening a saved ROI from the list */
1055
+ var unzipAndRenderDrawingLayer = function (accessURL, mergeIntoExisting) {
1056
+ if (mergeIntoExisting === void 0) { mergeIntoExisting = false; }
1057
+ return __awaiter(_this, void 0, void 0, function () {
1058
+ var response, blob, zip, fileInfo, content, info, niiFilePath, importedLabels, niiDrawing, base64, mergeResult, merged, _i, _a, _b, oldL, newId, oldN, newN, alias;
1059
+ var _c, _d;
1060
+ return __generator(this, function (_e) {
1061
+ switch (_e.label) {
1062
+ case 0: return [4 /*yield*/, fetch(accessURL)];
1063
+ case 1:
1064
+ response = _e.sent();
1065
+ // Check if the request was successful
1066
+ if (!response.ok) {
1067
+ props.warn("Failed to load the requested ROI Layer");
1068
+ return [2 /*return*/];
1069
+ }
1070
+ return [4 /*yield*/, response.blob()];
1071
+ case 2:
1072
+ blob = _e.sent();
1073
+ zip = new JSZip();
1074
+ return [4 /*yield*/, zip.loadAsync(blob)];
1075
+ case 3:
1076
+ _e.sent();
1077
+ fileInfo = zip.file("info.json");
1078
+ if (!fileInfo) return [3 /*break*/, 12];
1079
+ return [4 /*yield*/, fileInfo.async("string")];
1080
+ case 4:
1081
+ content = _e.sent();
1082
+ info = JSON.parse(content);
1083
+ niiFilePath = info.data[0].filename;
1084
+ importedLabels = info.data[0].labelMapping || {};
1085
+ niiDrawing = zip.file(niiFilePath);
1086
+ if (!niiDrawing) return [3 /*break*/, 10];
1087
+ return [4 /*yield*/, niiDrawing.async("base64")];
1088
+ case 5:
1089
+ base64 = _e.sent();
1090
+ console.log(niiFilePath);
1091
+ if (!(mergeIntoExisting &&
1092
+ typeof nv.mergeDrawingFromBase64 === "function")) return [3 /*break*/, 7];
1093
+ return [4 /*yield*/, nv.mergeDrawingFromBase64(niiFilePath, base64)];
1094
+ case 6:
1095
+ mergeResult = _e.sent();
1096
+ if (!mergeResult || !mergeResult.ok) {
1097
+ props.warn("Failed to merge ROI upload (invalid file or size mismatch).");
1098
+ return [2 /*return*/, null];
1099
+ }
1100
+ if (mergeResult.importLabelRemap === null) {
1101
+ setLabelMapping(importedLabels);
1102
+ resampleImage(importedLabels);
1103
+ }
1104
+ else {
1105
+ merged = __assign({}, labelMapping);
1106
+ for (_i = 0, _a = Object.entries(mergeResult.importLabelRemap); _i < _a.length; _i++) {
1107
+ _b = _a[_i], oldL = _b[0], newId = _b[1];
1108
+ oldN = Number(oldL);
1109
+ newN = Number(newId);
1110
+ alias = oldN === newN
1111
+ ? ((_d = (_c = importedLabels[oldL]) !== null && _c !== void 0 ? _c : importedLabels[String(oldL)]) !== null && _d !== void 0 ? _d : String(newId))
1112
+ : String(newId);
1113
+ merged[String(newId)] = alias;
1114
+ }
1115
+ setLabelMapping(merged);
1116
+ resampleImage(merged);
1117
+ }
1118
+ return [3 /*break*/, 9];
1119
+ case 7: return [4 /*yield*/, nv.loadDrawingFromBase64(niiFilePath, base64)];
1120
+ case 8:
1121
+ _e.sent();
1122
+ setLabelMapping(importedLabels);
1123
+ resampleImage(importedLabels);
1124
+ _e.label = 9;
1125
+ case 9: return [3 /*break*/, 11];
1126
+ case 10:
1127
+ console.log("".concat(niiFilePath, " not found in the ZIP file."));
1128
+ props.warn("".concat(niiFilePath, " not found in the ZIP file."));
1129
+ return [2 /*return*/, null];
1130
+ case 11: return [3 /*break*/, 13];
1131
+ case 12:
1132
+ console.log("info.json not found in the ZIP file.");
1133
+ props.warn("info.json not found in the ZIP file.");
1134
+ return [2 /*return*/, null];
1135
+ case 13: return [2 /*return*/];
1136
+ }
1137
+ });
1102
1138
  });
1103
- }); };
1139
+ };
1104
1140
  // This is a small fix that prevents the selected roi index from jumping to
1105
1141
  // the newest saved roi after user has performed a roi reselection during
1106
1142
  // roi saving
@@ -1124,7 +1160,7 @@ export default function NiiVueport(props) {
1124
1160
  var unpackROI = function (accessURL) { return __awaiter(_this, void 0, void 0, function () {
1125
1161
  return __generator(this, function (_a) {
1126
1162
  switch (_a.label) {
1127
- case 0: return [4 /*yield*/, unzipAndRenderDrawingLayer(accessURL)];
1163
+ case 0: return [4 /*yield*/, unzipAndRenderDrawingLayer(accessURL, true)];
1128
1164
  case 1:
1129
1165
  _a.sent();
1130
1166
  setDrawingChanged(false);
@@ -1391,9 +1391,13 @@ Niivue.prototype.groupLabelsInto = function (
1391
1391
  sourceLabels = [0],
1392
1392
  targetLabel = 7,
1393
1393
  ) {
1394
+ const overlayIndex = new Set(bitmapOverlay.map((t) => t[0]));
1394
1395
  for (let i = 0; i < this.drawBitmap.length; i++) {
1395
1396
  if (sourceLabels.indexOf(this.drawBitmap[i]) >= 0) {
1396
- bitmapOverlay.push([i, this.drawBitmap[i]]);
1397
+ if (!overlayIndex.has(i)) {
1398
+ bitmapOverlay.push([i, this.drawBitmap[i]]);
1399
+ overlayIndex.add(i);
1400
+ }
1397
1401
  this.drawBitmap[i] = targetLabel;
1398
1402
  }
1399
1403
  }
@@ -1408,6 +1412,62 @@ Niivue.prototype.ungroup = function () {
1408
1412
  this.refreshDrawing(false);
1409
1413
  };
1410
1414
 
1415
+ /**
1416
+ * Smart group from ROI table selection.
1417
+ * - Extend: selection includes merged label(s) >= targetLabel → mask those voxels, ungroup once,
1418
+ * merge into max(merged ids in selection) (add to group 7 or 8, etc.).
1419
+ * - New merge (primitives only): if no merged id >= targetLabel in the volume yet, merge into targetLabel (7);
1420
+ * else merge into maxMergedInVolume + 1 (second disjoint group → 8).
1421
+ */
1422
+ Niivue.prototype.groupLabelsFromSelection = function (
1423
+ sourceLabels = [],
1424
+ targetLabel = 7,
1425
+ ) {
1426
+ if (!this.drawBitmap || !sourceLabels || sourceLabels.length < 2) {
1427
+ return;
1428
+ }
1429
+ const labelSet = new Set(sourceLabels);
1430
+ if (labelSet.size < 2) {
1431
+ return;
1432
+ }
1433
+ const MERGE_THRESHOLD = targetLabel;
1434
+ const mergedInSelection = sourceLabels.filter((l) => l >= MERGE_THRESHOLD);
1435
+ if (mergedInSelection.length > 0) {
1436
+ const mergeTarget = Math.max(...mergedInSelection);
1437
+ const n = this.drawBitmap.length;
1438
+ const mask = new Uint8Array(n);
1439
+ for (let i = 0; i < n; i++) {
1440
+ if (labelSet.has(this.drawBitmap[i])) {
1441
+ mask[i] = 1;
1442
+ }
1443
+ }
1444
+ this.ungroup();
1445
+ const mergeSet = new Set();
1446
+ for (let i = 0; i < n; i++) {
1447
+ if (mask[i] && this.drawBitmap[i] !== 0) {
1448
+ mergeSet.add(this.drawBitmap[i]);
1449
+ }
1450
+ }
1451
+ const toMerge = Array.from(mergeSet).sort((a, b) => a - b);
1452
+ if (toMerge.length === 0) {
1453
+ this.refreshDrawing(false);
1454
+ return;
1455
+ }
1456
+ this.groupLabelsInto(toMerge, mergeTarget);
1457
+ return;
1458
+ }
1459
+ let sceneMaxMerged = 0;
1460
+ for (let j = 0; j < this.drawBitmap.length; j++) {
1461
+ const v = this.drawBitmap[j];
1462
+ if (v >= MERGE_THRESHOLD) {
1463
+ sceneMaxMerged = Math.max(sceneMaxMerged, v);
1464
+ }
1465
+ }
1466
+ const newTarget =
1467
+ sceneMaxMerged >= MERGE_THRESHOLD ? sceneMaxMerged + 1 : MERGE_THRESHOLD;
1468
+ this.groupLabelsInto(sourceLabels, newTarget);
1469
+ };
1470
+
1411
1471
  Niivue.prototype.resetScene = function () {
1412
1472
  this.scene.pan2Dxyzmm = [0, 0, 0, 1];
1413
1473
  this.drawScene();
@@ -1499,6 +1559,193 @@ Niivue.prototype.loadDrawingFromBase64 = async function (fnm, base64) {
1499
1559
  return base64 !== undefined;
1500
1560
  };
1501
1561
 
1562
+ /**
1563
+ * Merge an uploaded/loaded drawing into the current drawBitmap instead of replacing it.
1564
+ * Non-zero import voxels overwrite the current bitmap (same as drawing a new ROI on top).
1565
+ * Import label IDs are reused when not already on the canvas; otherwise remapped to a free ID.
1566
+ */
1567
+ Niivue.prototype.mergeDrawingFromBase64 = async function (fnm, base64) {
1568
+ const result = { ok: false, importLabelRemap: null };
1569
+ if (!base64) {
1570
+ return result;
1571
+ }
1572
+ try {
1573
+ if (!this.back) {
1574
+ throw new Error("back undefined");
1575
+ }
1576
+ const drawingBitmap = NVImage.loadFromBase64({ name: fnm, base64 });
1577
+ if (!drawingBitmap || !drawingBitmap.hdr || !drawingBitmap.hdr.dims) {
1578
+ return result;
1579
+ }
1580
+ const dims = drawingBitmap.hdr.dims;
1581
+ if (
1582
+ dims[1] !== this.back.hdr.dims[1] ||
1583
+ dims[2] !== this.back.hdr.dims[2] ||
1584
+ dims[3] !== this.back.hdr.dims[3]
1585
+ ) {
1586
+ console.warn(
1587
+ "mergeDrawingFromBase64: drawing dimensions do not match background",
1588
+ );
1589
+ return result;
1590
+ }
1591
+ const vx = dims[1] * dims[2] * dims[3];
1592
+ const perm = drawingBitmap.permRAS;
1593
+ const layout = [0, 0, 0];
1594
+ for (let i = 0; i < 3; i++) {
1595
+ for (let j = 0; j < 3; j++) {
1596
+ if (Math.abs(perm[i]) - 1 !== j) {
1597
+ continue;
1598
+ }
1599
+ layout[j] = i * Math.sign(perm[i]);
1600
+ }
1601
+ }
1602
+ let stride = 1;
1603
+ const instride = [1, 1, 1];
1604
+ const inflip = [false, false, false];
1605
+ for (let i = 0; i < layout.length; i++) {
1606
+ for (let j = 0; j < layout.length; j++) {
1607
+ const a = Math.abs(layout[j]);
1608
+ if (a !== i) {
1609
+ continue;
1610
+ }
1611
+ instride[j] = stride;
1612
+ if (layout[j] < 0 || Object.is(layout[j], -0)) {
1613
+ inflip[j] = true;
1614
+ }
1615
+ stride *= dims[j + 1];
1616
+ }
1617
+ }
1618
+ const nvRange = (start, end, step) => {
1619
+ const o = [];
1620
+ if (step > 0) {
1621
+ for (let i = start; i <= end; i += step) {
1622
+ o.push(i);
1623
+ }
1624
+ } else {
1625
+ for (let i = start; i >= end; i += step) {
1626
+ o.push(i);
1627
+ }
1628
+ }
1629
+ return o;
1630
+ };
1631
+ let xlut = nvRange(0, dims[1] - 1, 1);
1632
+ if (inflip[0]) {
1633
+ xlut = nvRange(dims[1] - 1, 0, -1);
1634
+ }
1635
+ for (let i = 0; i < dims[1]; i++) {
1636
+ xlut[i] *= instride[0];
1637
+ }
1638
+ let ylut = nvRange(0, dims[2] - 1, 1);
1639
+ if (inflip[1]) {
1640
+ ylut = nvRange(dims[2] - 1, 0, -1);
1641
+ }
1642
+ for (let i = 0; i < dims[2]; i++) {
1643
+ ylut[i] *= instride[1];
1644
+ }
1645
+ let zlut = nvRange(0, dims[3] - 1, 1);
1646
+ if (inflip[2]) {
1647
+ zlut = nvRange(dims[3] - 1, 0, -1);
1648
+ }
1649
+ for (let i = 0; i < dims[3]; i++) {
1650
+ zlut[i] *= instride[2];
1651
+ }
1652
+
1653
+ const inVs = drawingBitmap.img;
1654
+ const importFlat = new Uint8Array(vx);
1655
+ let ji = 0;
1656
+ for (let z = 0; z < dims[3]; z++) {
1657
+ for (let y = 0; y < dims[2]; y++) {
1658
+ for (let x = 0; x < dims[1]; x++) {
1659
+ importFlat[xlut[x] + ylut[y] + zlut[z]] = inVs[ji];
1660
+ ji++;
1661
+ }
1662
+ }
1663
+ }
1664
+
1665
+ const hasExistingDrawing =
1666
+ this.drawBitmap &&
1667
+ this.drawBitmap.length === vx &&
1668
+ this.drawBitmap.some((v) => v !== 0);
1669
+
1670
+ if (this.drawBitmap && this.drawBitmap.length !== vx) {
1671
+ console.warn(
1672
+ "mergeDrawingFromBase64: drawBitmap size mismatch; replacing drawing",
1673
+ );
1674
+ this.loadDrawing(drawingBitmap);
1675
+ result.ok = true;
1676
+ result.importLabelRemap = null;
1677
+ return result;
1678
+ }
1679
+
1680
+ if (!hasExistingDrawing) {
1681
+ this.loadDrawing(drawingBitmap);
1682
+ result.ok = true;
1683
+ result.importLabelRemap = null;
1684
+ return result;
1685
+ }
1686
+
1687
+ const usedOnCanvas = new Set();
1688
+ for (let i = 0; i < this.drawBitmap.length; i++) {
1689
+ const v = this.drawBitmap[i];
1690
+ if (v !== 0) {
1691
+ usedOnCanvas.add(v);
1692
+ }
1693
+ }
1694
+
1695
+ const seen = new Set();
1696
+ for (let i = 0; i < importFlat.length; i++) {
1697
+ const v = importFlat[i];
1698
+ if (v !== 0) {
1699
+ seen.add(v);
1700
+ }
1701
+ }
1702
+ const sorted = Array.from(seen).sort((a, b) => a - b);
1703
+ const importLabelRemap = {};
1704
+ let nextSynthetic = 0;
1705
+ for (const v of usedOnCanvas) {
1706
+ if (v > nextSynthetic) {
1707
+ nextSynthetic = v;
1708
+ }
1709
+ }
1710
+ nextSynthetic += 1;
1711
+
1712
+ for (let s = 0; s < sorted.length; s++) {
1713
+ const oldL = sorted[s];
1714
+ if (!usedOnCanvas.has(oldL)) {
1715
+ importLabelRemap[oldL] = oldL;
1716
+ usedOnCanvas.add(oldL);
1717
+ } else {
1718
+ while (usedOnCanvas.has(nextSynthetic)) {
1719
+ nextSynthetic++;
1720
+ }
1721
+ importLabelRemap[oldL] = nextSynthetic;
1722
+ usedOnCanvas.add(nextSynthetic);
1723
+ nextSynthetic++;
1724
+ }
1725
+ }
1726
+
1727
+ for (let i = 0; i < vx; i++) {
1728
+ const v = importFlat[i];
1729
+ if (v === 0) {
1730
+ continue;
1731
+ }
1732
+ // Upload wins overlaps (matches pen drawing on top of another ROI)
1733
+ this.drawBitmap[i] = importLabelRemap[v];
1734
+ }
1735
+
1736
+ this.drawAddUndoBitmap();
1737
+ this.refreshDrawing(false);
1738
+ this.drawScene();
1739
+ result.ok = true;
1740
+ result.importLabelRemap = importLabelRemap;
1741
+ return result;
1742
+ } catch (err) {
1743
+ console.error(err);
1744
+ console.error("mergeDrawingFromBase64() failed to load " + fnm);
1745
+ return result;
1746
+ }
1747
+ };
1748
+
1502
1749
  // not included in public docs
1503
1750
  // show text labels for L/R, A/P, I/S dimensions
1504
1751
  Niivue.prototype.drawSliceOrientationText = function (
@@ -18,6 +18,7 @@ import { DrawToolkit } from "./DrawToolKit";
18
18
  // import GUI from "lil-gui";
19
19
  import "./Toolbar.scss";
20
20
  import TKDualRange from "../../../../../CmrComponents/tk-dualrange/TKDualRange";
21
+ import { CmrLabel } from "../../../../../index";
21
22
  function toRatio(val, min, max) {
22
23
  return (val - min) / (max - min);
23
24
  }
@@ -173,7 +174,7 @@ export function NiivuePanel(props) {
173
174
  alignItems: "center",
174
175
  gap: 10,
175
176
  marginBottom: 6
176
- } }, { children: [_jsx("label", __assign({ htmlFor: "xSlice" }, { children: "X:" })), _jsx("input", { type: "number", value: xVal, min: mins[0], max: maxs[0], step: 1, onChange: function (e) {
177
+ } }, { children: [_jsx(CmrLabel, __assign({ style: { fontSize: "0.9rem" } }, { children: "X:" })), _jsx("input", { type: "number", value: xVal, min: mins[0], max: maxs[0], step: 1, onChange: function (e) {
177
178
  var next = Number(e.target.value);
178
179
  if (!Number.isFinite(next))
179
180
  return;
@@ -227,7 +228,7 @@ export function NiivuePanel(props) {
227
228
  alignItems: "center",
228
229
  gap: 10,
229
230
  marginBottom: 6
230
- } }, { children: [_jsx("label", __assign({ htmlFor: "zSlice" }, { children: "Z:" })), _jsx("input", { type: "number", value: zVal.toFixed(3), min: mins[2], max: maxs[2], step: 0.001, onChange: function (e) {
231
+ } }, { children: [_jsx(CmrLabel, __assign({ style: { fontSize: "0.9rem" } }, { children: "Z:" })), _jsx("input", { type: "number", value: zVal.toFixed(3), min: mins[2], max: maxs[2], step: 0.001, onChange: function (e) {
231
232
  var next = Number(e.target.value);
232
233
  if (!Number.isFinite(next))
233
234
  return;
package/dist/index.d.ts CHANGED
@@ -21,6 +21,8 @@ export { DualSlider } from "./CmrComponents/double-slider/DualSlider";
21
21
  export { Slider } from "./CmrComponents/gui-slider/Slider";
22
22
  export { InvertibleDualSlider } from "./CmrComponents/double-slider/InvertibleDualSlider";
23
23
  export type { LambdaFile } from "./CmrComponents/upload/Upload";
24
+ export { NiivueSlicePosition } from "./CmrComponents/niivue-slice-position/NiivueSlicePosition";
25
+ export type { NiivueSlicePositionProps } from "./CmrComponents/niivue-slice-position/NiivueSlicePosition";
24
26
  import type { FC } from "react";
25
27
  import type { CmrTableProps } from "./CmrTable/CmrTable";
26
28
  export declare const CmrTable: FC<CmrTableProps>;
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ export { default as CmrTooltip } from "./CmrComponents/tooltip/Tooltip";
22
22
  export { DualSlider } from "./CmrComponents/double-slider/DualSlider";
23
23
  export { Slider } from "./CmrComponents/gui-slider/Slider";
24
24
  export { InvertibleDualSlider } from "./CmrComponents/double-slider/InvertibleDualSlider";
25
+ export { NiivueSlicePosition } from "./CmrComponents/niivue-slice-position/NiivueSlicePosition";
25
26
  import CmrTableComponent from "./CmrTable/CmrTable";
26
27
  export var CmrTable = CmrTableComponent;
27
28
  export * from "./core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmr-ux",
3
- "version": "4.2.9",
3
+ "version": "4.3.1",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",
@@ -297,6 +297,7 @@
297
297
  "@fortawesome/fontawesome-svg-core": "^6.7.2",
298
298
  "@fortawesome/free-solid-svg-icons": "^6.7.2",
299
299
  "@fortawesome/react-fontawesome": "^0.2.2",
300
+ "@wordpress/icons": "^11.8.0",
300
301
  "@mui/icons-material": "^5.14.1",
301
302
  "@mui/material": "^5.14.2",
302
303
  "@mui/x-data-grid": "^6.10.1",