cloudmr-ux 4.2.9 → 4.3.0
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 +1 -2
- package/dist/CmrComponents/niivue-slice-position/NiivueSlicePosition.d.ts +67 -0
- package/dist/CmrComponents/niivue-slice-position/NiivueSlicePosition.js +254 -0
- package/dist/core/app/results/Rois.js +48 -11
- package/dist/core/common/components/NiivueTools/Niivue.js +86 -50
- package/dist/core/common/components/NiivueTools/NiivuePatcher.js +248 -1
- package/dist/core/common/components/NiivueTools/components/NiivuePanel.js +3 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
* CSS accent color for all three range inputs.
|
|
33
|
+
* @default "#580f8b"
|
|
34
|
+
*/
|
|
35
|
+
accentColor?: string;
|
|
36
|
+
style?: React.CSSProperties;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* **NiivueSlicePosition**
|
|
41
|
+
*
|
|
42
|
+
* A reusable "Slice Position" control panel that drives a Niivue viewer.
|
|
43
|
+
* Renders three labeled sliders — X, Y, and Z — each paired with an editable
|
|
44
|
+
* number field. All sliders snap to exact voxel centres so they stay in sync
|
|
45
|
+
* with Niivue's own scroll behaviour.
|
|
46
|
+
*
|
|
47
|
+
* ### Wiring it up
|
|
48
|
+
*
|
|
49
|
+
* ```tsx
|
|
50
|
+
* // In the parent that owns the Niivue instance:
|
|
51
|
+
* const [mms, setMms] = useState([0, 0, 0]);
|
|
52
|
+
* const [mins, setMins] = useState([0, 0, 0]);
|
|
53
|
+
* const [maxs, setMaxs] = useState([1, 1, 1]);
|
|
54
|
+
*
|
|
55
|
+
* // Give these to Niivue so it calls back on every crosshair move:
|
|
56
|
+
* nv.opts.onLocationChange = (data) => {
|
|
57
|
+
* setMms([data.mm[0], data.mm[1], data.mm[2]]);
|
|
58
|
+
* setMins([data.vox[0]?.min ?? 0, data.vox[1]?.min ?? 0, data.vox[2]?.min ?? 0]);
|
|
59
|
+
* setMaxs([data.vox[0]?.max ?? 1, data.vox[1]?.max ?? 1, data.vox[2]?.max ?? 1]);
|
|
60
|
+
* };
|
|
61
|
+
*
|
|
62
|
+
* // Then render:
|
|
63
|
+
* <NiivueSlicePosition nv={nv} mins={mins} maxs={maxs} mms={mms} />
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function NiivueSlicePosition({ nv, mins, maxs, mms, accentColor, style, className, }: NiivueSlicePositionProps): import("react/jsx-runtime").JSX.Element;
|
|
67
|
+
export default NiivueSlicePosition;
|
|
@@ -0,0 +1,254 @@
|
|
|
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 CmrLabel from "../label/Label";
|
|
15
|
+
// ─── Helpers (pure, no React) ─────────────────────────────────────────────────
|
|
16
|
+
var safeSpan = function (min, max) { return Math.max(1e-9, max - min); };
|
|
17
|
+
var clamp = function (v, lo, hi) { return Math.max(lo, Math.min(hi, v)); };
|
|
18
|
+
var round3 = function (v) { return Math.round(v * 1000) / 1000; };
|
|
19
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* **NiivueSlicePosition**
|
|
22
|
+
*
|
|
23
|
+
* A reusable "Slice Position" control panel that drives a Niivue viewer.
|
|
24
|
+
* Renders three labeled sliders — X, Y, and Z — each paired with an editable
|
|
25
|
+
* number field. All sliders snap to exact voxel centres so they stay in sync
|
|
26
|
+
* with Niivue's own scroll behaviour.
|
|
27
|
+
*
|
|
28
|
+
* ### Wiring it up
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* // In the parent that owns the Niivue instance:
|
|
32
|
+
* const [mms, setMms] = useState([0, 0, 0]);
|
|
33
|
+
* const [mins, setMins] = useState([0, 0, 0]);
|
|
34
|
+
* const [maxs, setMaxs] = useState([1, 1, 1]);
|
|
35
|
+
*
|
|
36
|
+
* // Give these to Niivue so it calls back on every crosshair move:
|
|
37
|
+
* nv.opts.onLocationChange = (data) => {
|
|
38
|
+
* setMms([data.mm[0], data.mm[1], data.mm[2]]);
|
|
39
|
+
* setMins([data.vox[0]?.min ?? 0, data.vox[1]?.min ?? 0, data.vox[2]?.min ?? 0]);
|
|
40
|
+
* setMaxs([data.vox[0]?.max ?? 1, data.vox[1]?.max ?? 1, data.vox[2]?.max ?? 1]);
|
|
41
|
+
* };
|
|
42
|
+
*
|
|
43
|
+
* // Then render:
|
|
44
|
+
* <NiivueSlicePosition nv={nv} mins={mins} maxs={maxs} mms={mms} />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function NiivueSlicePosition(_a) {
|
|
48
|
+
var _b, _c, _d, _e, _f;
|
|
49
|
+
var nv = _a.nv, mins = _a.mins, maxs = _a.maxs, mms = _a.mms, _g = _a.accentColor, accentColor = _g === void 0 ? "#580f8b" : _g, style = _a.style, className = _a.className;
|
|
50
|
+
// ── Derive voxel grid from the loaded volume ─────────────────────────────
|
|
51
|
+
var vol = (_b = nv === null || nv === void 0 ? void 0 : nv.volumes) === null || _b === void 0 ? void 0 : _b[0];
|
|
52
|
+
var meta = (_c = vol === null || vol === void 0 ? void 0 : vol.getImageMetadata) === null || _c === void 0 ? void 0 : _c.call(vol);
|
|
53
|
+
var nx = Math.max(1, (_d = meta === null || meta === void 0 ? void 0 : meta.nx) !== null && _d !== void 0 ? _d : 1);
|
|
54
|
+
var ny = Math.max(1, (_e = meta === null || meta === void 0 ? void 0 : meta.ny) !== null && _e !== void 0 ? _e : 1);
|
|
55
|
+
var nz = Math.max(1, (_f = meta === null || meta === void 0 ? void 0 : meta.nz) !== null && _f !== void 0 ? _f : 1);
|
|
56
|
+
// ── Slider bounds for X and Y ────────────────────────────────────────────
|
|
57
|
+
var spanX = safeSpan(mins[0], maxs[0]);
|
|
58
|
+
var spanY = safeSpan(mins[1], maxs[1]);
|
|
59
|
+
var stepX = nx > 1 ? spanX / nx : spanX * 0.01;
|
|
60
|
+
var stepY = ny > 1 ? spanY / ny : spanY * 0.01;
|
|
61
|
+
var sliderMinX = nx > 1 ? mins[0] + 0.5 * stepX : mins[0];
|
|
62
|
+
var sliderMaxX = nx > 1 ? maxs[0] - 0.5 * stepX : maxs[0];
|
|
63
|
+
var sliderMinY = ny > 1 ? mins[1] + 0.5 * stepY : mins[1];
|
|
64
|
+
var sliderMaxY = ny > 1 ? maxs[1] - 0.5 * stepY : maxs[1];
|
|
65
|
+
// ── Slider bounds for Z (uses actual Niivue slice centres) ───────────────
|
|
66
|
+
var zAtStart;
|
|
67
|
+
var zAtEnd;
|
|
68
|
+
if (nz <= 1) {
|
|
69
|
+
zAtStart = mins[2];
|
|
70
|
+
zAtEnd = maxs[2];
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
try {
|
|
74
|
+
var cx = nv.scene.crosshairPos[0];
|
|
75
|
+
var cy = nv.scene.crosshairPos[1];
|
|
76
|
+
zAtStart = nv.frac2mm([cx, cy, 0.5 / nz])[2];
|
|
77
|
+
zAtEnd = nv.frac2mm([cx, cy, (nz - 0.5) / nz])[2];
|
|
78
|
+
}
|
|
79
|
+
catch (_h) {
|
|
80
|
+
var s = safeSpan(mins[2], maxs[2]) / nz;
|
|
81
|
+
zAtStart = mins[2] + 0.5 * s;
|
|
82
|
+
zAtEnd = maxs[2] - 0.5 * s;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
var sliderMinZ = Math.min(zAtStart, zAtEnd);
|
|
86
|
+
var sliderMaxZ = Math.max(zAtStart, zAtEnd);
|
|
87
|
+
var stepZ = nz > 1
|
|
88
|
+
? Math.abs(zAtEnd - zAtStart) / (nz - 1)
|
|
89
|
+
: Math.max(1e-9, Math.abs(sliderMaxZ - sliderMinZ) * 0.01);
|
|
90
|
+
// ── Fractional helpers ───────────────────────────────────────────────────
|
|
91
|
+
var ratioAxis = function (val, axis) {
|
|
92
|
+
return (val - mins[axis]) / safeSpan(mins[axis], maxs[axis]);
|
|
93
|
+
};
|
|
94
|
+
var mmToFrac = function (x, y, z) {
|
|
95
|
+
try {
|
|
96
|
+
return nv.mm2frac([x, y, z]);
|
|
97
|
+
}
|
|
98
|
+
catch (_a) {
|
|
99
|
+
return [ratioAxis(x, 0), ratioAxis(y, 1), ratioAxis(z, 2)];
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
/** Snap a mm value to the nearest voxel centre on the given axis. */
|
|
103
|
+
var snapToVoxel = function (mm, axis) {
|
|
104
|
+
var n = axis === 0 ? nx : axis === 1 ? ny : nz;
|
|
105
|
+
var mm3 = [mmsRef.current[0], mmsRef.current[1], mmsRef.current[2]];
|
|
106
|
+
mm3[axis] = mm;
|
|
107
|
+
var frac;
|
|
108
|
+
try {
|
|
109
|
+
frac = nv.mm2frac(mm3);
|
|
110
|
+
}
|
|
111
|
+
catch (_a) {
|
|
112
|
+
frac = [
|
|
113
|
+
ratioAxis(mmsRef.current[0], 0),
|
|
114
|
+
ratioAxis(mmsRef.current[1], 1),
|
|
115
|
+
ratioAxis(mmsRef.current[2], 2),
|
|
116
|
+
];
|
|
117
|
+
frac[axis] = ratioAxis(mm, axis);
|
|
118
|
+
}
|
|
119
|
+
var idx = Math.round(frac[axis] * n - 0.5);
|
|
120
|
+
var fracSnapped = n > 1 ? (clamp(idx, 0, n - 1) + 0.5) / n : 0.5;
|
|
121
|
+
frac[axis] = fracSnapped;
|
|
122
|
+
try {
|
|
123
|
+
return nv.frac2mm(frac)[axis];
|
|
124
|
+
}
|
|
125
|
+
catch (_b) {
|
|
126
|
+
return mins[axis] + fracSnapped * safeSpan(mins[axis], maxs[axis]);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
// ── Local slider state (mirrors mms; synced by useEffect) ────────────────
|
|
130
|
+
var _j = React.useState(round3(mms[0])), xVal = _j[0], setXVal = _j[1];
|
|
131
|
+
var _k = React.useState(round3(mms[1])), yVal = _k[0], setYVal = _k[1];
|
|
132
|
+
var _l = React.useState(round3(mms[2])), zVal = _l[0], setZVal = _l[1];
|
|
133
|
+
// Keep a ref so snapToVoxel can read the *latest* values without stale closure
|
|
134
|
+
var mmsRef = React.useRef([xVal, yVal, zVal]);
|
|
135
|
+
React.useEffect(function () {
|
|
136
|
+
mmsRef.current = [xVal, yVal, zVal];
|
|
137
|
+
}, [xVal, yVal, zVal]);
|
|
138
|
+
// Sync from Niivue (e.g. mouse scroll, click)
|
|
139
|
+
React.useEffect(function () {
|
|
140
|
+
var fmt = function (v) { return (Number.isFinite(v) ? round3(v) : 0); };
|
|
141
|
+
setXVal(fmt(mms[0]));
|
|
142
|
+
setYVal(fmt(mms[1]));
|
|
143
|
+
setZVal(fmt(mms[2]));
|
|
144
|
+
}, [mms]);
|
|
145
|
+
// ── Apply handlers ────────────────────────────────────────────────────────
|
|
146
|
+
var applyX = function (val) {
|
|
147
|
+
var v = clamp(snapToVoxel(val, 0), sliderMinX, sliderMaxX);
|
|
148
|
+
setXVal(v);
|
|
149
|
+
nv.scene.crosshairPos = mmToFrac(v, mmsRef.current[1], mmsRef.current[2]);
|
|
150
|
+
nv.drawScene();
|
|
151
|
+
};
|
|
152
|
+
var applyY = function (val) {
|
|
153
|
+
var v = clamp(snapToVoxel(val, 1), sliderMinY, sliderMaxY);
|
|
154
|
+
setYVal(v);
|
|
155
|
+
nv.scene.crosshairPos = mmToFrac(mmsRef.current[0], v, mmsRef.current[2]);
|
|
156
|
+
nv.drawScene();
|
|
157
|
+
};
|
|
158
|
+
var applyZBySliceIndex = function (kRaw) {
|
|
159
|
+
if (nz <= 1) {
|
|
160
|
+
var cx_1 = nv.scene.crosshairPos[0];
|
|
161
|
+
var cy_1 = nv.scene.crosshairPos[1];
|
|
162
|
+
nv.scene.crosshairPos = [cx_1, cy_1, 0.5];
|
|
163
|
+
nv.drawScene();
|
|
164
|
+
try {
|
|
165
|
+
setZVal(round3(nv.frac2mm([cx_1, cy_1, 0.5])[2]));
|
|
166
|
+
}
|
|
167
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
var k = clamp(Math.round(kRaw), 0, nz - 1);
|
|
171
|
+
var cx = nv.scene.crosshairPos[0];
|
|
172
|
+
var cy = nv.scene.crosshairPos[1];
|
|
173
|
+
var fz = (k + 0.5) / nz;
|
|
174
|
+
nv.scene.crosshairPos = [cx, cy, fz];
|
|
175
|
+
nv.drawScene();
|
|
176
|
+
try {
|
|
177
|
+
setZVal(round3(nv.frac2mm([cx, cy, fz])[2]));
|
|
178
|
+
}
|
|
179
|
+
catch (_b) {
|
|
180
|
+
var spanLen = zAtEnd - zAtStart;
|
|
181
|
+
setZVal(round3(zAtStart + (k * spanLen) / Math.max(1, nz - 1)));
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var applyZ = function (val) {
|
|
185
|
+
if (!Number.isFinite(val))
|
|
186
|
+
return;
|
|
187
|
+
if (nz <= 1) {
|
|
188
|
+
var v = clamp(snapToVoxel(val, 2), sliderMinZ, sliderMaxZ);
|
|
189
|
+
setZVal(round3(v));
|
|
190
|
+
nv.scene.crosshairPos = mmToFrac(mmsRef.current[0], mmsRef.current[1], v);
|
|
191
|
+
nv.drawScene();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
var fracZ;
|
|
195
|
+
try {
|
|
196
|
+
fracZ = nv.mm2frac([mmsRef.current[0], mmsRef.current[1], val])[2];
|
|
197
|
+
}
|
|
198
|
+
catch (_a) {
|
|
199
|
+
fracZ = ratioAxis(val, 2);
|
|
200
|
+
}
|
|
201
|
+
applyZBySliceIndex(Math.round(fracZ * nz - 0.5));
|
|
202
|
+
};
|
|
203
|
+
// Current Z as a discrete slice index (for the integer-step Z range input)
|
|
204
|
+
var zSliceIndex = 0;
|
|
205
|
+
if (nz > 1) {
|
|
206
|
+
var fracZ = void 0;
|
|
207
|
+
try {
|
|
208
|
+
fracZ = nv.mm2frac([xVal, yVal, zVal])[2];
|
|
209
|
+
}
|
|
210
|
+
catch (_m) {
|
|
211
|
+
fracZ = ratioAxis(zVal, 2);
|
|
212
|
+
}
|
|
213
|
+
zSliceIndex = clamp(Math.round(fracZ * nz - 0.5), 0, nz - 1);
|
|
214
|
+
}
|
|
215
|
+
// ── Shared styles ─────────────────────────────────────────────────────────
|
|
216
|
+
var inputStyle = {
|
|
217
|
+
width: 100,
|
|
218
|
+
padding: "4px 6px",
|
|
219
|
+
borderRadius: 6,
|
|
220
|
+
border: "1px solid #ccc",
|
|
221
|
+
fontSize: "0.9rem"
|
|
222
|
+
};
|
|
223
|
+
var rowStyle = {
|
|
224
|
+
display: "flex",
|
|
225
|
+
alignItems: "center",
|
|
226
|
+
gap: 10,
|
|
227
|
+
marginBottom: 6
|
|
228
|
+
};
|
|
229
|
+
var sliderStyle = {
|
|
230
|
+
width: "100%",
|
|
231
|
+
accentColor: accentColor
|
|
232
|
+
};
|
|
233
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
234
|
+
return (_jsxs("div", __assign({ style: __assign({ display: "flex", flexDirection: "column" }, style), className: className }, { 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) {
|
|
235
|
+
var next = Number(e.target.value);
|
|
236
|
+
if (Number.isFinite(next))
|
|
237
|
+
applyX(next);
|
|
238
|
+
}, onBlur: function (e) {
|
|
239
|
+
applyX(clamp(Number(e.target.value), sliderMinX, sliderMaxX));
|
|
240
|
+
} })] })), _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) {
|
|
241
|
+
var next = Number(e.target.value);
|
|
242
|
+
if (Number.isFinite(next))
|
|
243
|
+
applyY(next);
|
|
244
|
+
}, onBlur: function (e) {
|
|
245
|
+
applyY(clamp(Number(e.target.value), sliderMinY, sliderMaxY));
|
|
246
|
+
} })] })), _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) {
|
|
247
|
+
var next = Number(e.target.value);
|
|
248
|
+
if (Number.isFinite(next))
|
|
249
|
+
applyZ(next);
|
|
250
|
+
}, onBlur: function (e) {
|
|
251
|
+
applyZ(clamp(Number(e.target.value), sliderMinZ, sliderMaxZ));
|
|
252
|
+
} })] })), _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)); } })] })] })));
|
|
253
|
+
}
|
|
254
|
+
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
|
|
58
|
-
import
|
|
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:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
3
|
+
"version": "4.3.0",
|
|
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",
|