cloudmr-ux 4.8.6 → 4.8.8

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.
@@ -849,7 +849,15 @@ Niivue.prototype.groupLabelsFromSelection = function (sourceLabels = [], targetL
849
849
  };
850
850
 
851
851
  Niivue.prototype.resetScene = function () {
852
- this.scene.pan2Dxyzmm = [0, 0, 0, 1]
852
+ // Reset 2D pan + zoom
853
+ this.scene.pan2Dxyzmm = [0, 0, 0, 1];
854
+ // Reset 3D camera to Niivue defaults (azimuth=110, elevation=10, scale=1)
855
+ if (typeof this.setRenderAzimuthElevation === "function") {
856
+ this.setRenderAzimuthElevation(110, 10);
857
+ }
858
+ if (typeof this.setScale === "function") {
859
+ this.setScale(1.0);
860
+ }
853
861
  this.drawScene();
854
862
  }
855
863
 
@@ -871,19 +879,19 @@ Niivue.prototype.recenter = function () {
871
879
 
872
880
 
873
881
  Niivue.prototype.resetZoom = function () {
874
- // this.scene.pan2Dxyzmm[0] = 0;
875
- // this.scene.pan2Dxyzmm[1] = 0;
876
- // this.scene.pan2Dxyzmm[2] = 0;
877
-
882
+ // Reset 2D zoom (pan stays, only zoom component adjusted)
878
883
  const zoom = 1;
879
-
880
- const zoomChange = this.scene.pan2Dxyzmm[3] - zoom
884
+ const zoomChange = this.scene.pan2Dxyzmm[3] - zoom;
881
885
  this.scene.pan2Dxyzmm[3] = zoom;
882
- const mm = this.frac2mm(this.scene.crosshairPos)
883
- this.scene.pan2Dxyzmm[0] += zoomChange * mm[0]
884
- this.scene.pan2Dxyzmm[1] += zoomChange * mm[1]
885
- this.scene.pan2Dxyzmm[2] += zoomChange * mm[2]
886
- this.drawScene()
886
+ const mm = this.frac2mm(this.scene.crosshairPos);
887
+ this.scene.pan2Dxyzmm[0] += zoomChange * mm[0];
888
+ this.scene.pan2Dxyzmm[1] += zoomChange * mm[1];
889
+ this.scene.pan2Dxyzmm[2] += zoomChange * mm[2];
890
+ // Reset 3D zoom to default scale
891
+ if (typeof this.setScale === "function") {
892
+ this.setScale(1.0);
893
+ }
894
+ this.drawScene();
887
895
  }
888
896
 
889
897
  Niivue.prototype.setCenteredZoom = function (zoom) {
@@ -1528,6 +1536,10 @@ function clickedVoxelToolKind(nv) {
1528
1536
  function cloudMrTryReopenDraftOnClick(nv) {
1529
1537
  if (nv._cloudMrShapeDraftActive || nv._cloudMrPenDraftActive) return;
1530
1538
  if (!isClickWithoutDrag(nv.uiData)) return;
1539
+ // Never reopen a draft when the eraser is the active tool — the user is
1540
+ // trying to erase, not re-select. The eraser sets penValue to 0 with
1541
+ // drawing enabled; that combination uniquely identifies the eraser here.
1542
+ if (nv.opts.drawingEnabled && nv.opts.penValue === 0) return;
1531
1543
 
1532
1544
  const kind = clickedVoxelToolKind(nv);
1533
1545
  if (kind === 1) {
@@ -440,11 +440,18 @@ export function capturePolylineDraftFromClick(nv) {
440
440
  return null;
441
441
  var dx = nv.back.dims[1];
442
442
  var dy = nv.back.dims[2];
443
- // Decode stored linear indices back to [x, y, z] triples.
443
+ // Decode stored linear indices back to [x, y, z] triples, but only include
444
+ // voxels that are still present in the current bitmap. Registered indices
445
+ // that were cleared by the eraser must not be restored — otherwise re-opening
446
+ // the draft would silently undo the user's erasure.
444
447
  var strokeVoxels = [];
448
+ var presentIndices = new Set();
445
449
  var x1 = Infinity, y1 = Infinity, z1 = Infinity;
446
450
  var x2 = -Infinity, y2 = -Infinity, z2 = -Infinity;
447
451
  entry.voxelIndices.forEach(function (idx) {
452
+ if (nv.drawBitmap[idx] === 0)
453
+ return; // already erased — skip
454
+ presentIndices.add(idx);
448
455
  var z = Math.floor(idx / (dx * dy));
449
456
  var rem = idx - z * dx * dy;
450
457
  var y = Math.floor(rem / dx);
@@ -463,7 +470,9 @@ export function capturePolylineDraftFromClick(nv) {
463
470
  if (z > z2)
464
471
  z2 = z;
465
472
  });
466
- var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, entry.voxelIndices);
473
+ if (strokeVoxels.length === 0)
474
+ return null;
475
+ var baseBitmap = eraseClusterFromBitmap(nv.drawBitmap, presentIndices);
467
476
  return {
468
477
  kind: "freehand",
469
478
  baseBitmap: baseBitmap,
@@ -44,6 +44,7 @@ export function notifyPolylineChange(nv, count) {
44
44
  export function resetPolylineState(nv) {
45
45
  nv._cloudMrPolylineVertices = [];
46
46
  nv._cloudMrPolylineBaseBitmap = null;
47
+ nv._cloudMrPolylinePrevBaseBitmap = null;
47
48
  nv._cloudMrPolylineSessionStartBitmap = null;
48
49
  nv._cloudMrPolylineAxCorSag = -1;
49
50
  nv._cloudMrLastPolylineClickMs = null;
@@ -90,12 +91,20 @@ export function addPolylineVertex(nv) {
90
91
  now - nv._cloudMrLastPolylineClickMs <= DOUBLE_CLICK_MS;
91
92
  nv._cloudMrLastPolylineClickMs = now;
92
93
  if (isDoubleClick) {
93
- // Don't add this click as a vertex signal close instead.
94
+ // The first click of the double-click already placed an extra vertex.
95
+ // Roll it back using the snapshot saved before that click, then close.
96
+ if (nv._cloudMrPolylinePrevBaseBitmap) {
97
+ verts.pop();
98
+ nv._cloudMrPolylineBaseBitmap = nv._cloudMrPolylinePrevBaseBitmap;
99
+ nv.drawBitmap.set(nv._cloudMrPolylinePrevBaseBitmap);
100
+ nv.refreshDrawing(false, false);
101
+ }
94
102
  return POLYLINE_CLOSE;
95
103
  }
96
104
  if (verts.length === 0) {
97
105
  nv._cloudMrPolylineSessionStartBitmap = nv.drawBitmap.slice();
98
106
  nv._cloudMrPolylineBaseBitmap = nv._cloudMrPolylineSessionStartBitmap.slice();
107
+ nv._cloudMrPolylinePrevBaseBitmap = null;
99
108
  nv._cloudMrPolylineAxCorSag = axCorSagFromMouse(nv);
100
109
  nv.drawPenAxCorSag = nv._cloudMrPolylineAxCorSag;
101
110
  verts.push(pt);
@@ -108,6 +117,8 @@ export function addPolylineVertex(nv) {
108
117
  return false;
109
118
  }
110
119
  nv.drawPenAxCorSag = nv._cloudMrPolylineAxCorSag;
120
+ // Save the pre-segment bitmap so a subsequent double-click can roll back.
121
+ nv._cloudMrPolylinePrevBaseBitmap = nv._cloudMrPolylineBaseBitmap;
111
122
  nv.drawPenLine(pt, prev, penValue);
112
123
  verts.push(pt);
113
124
  nv._cloudMrPolylineBaseBitmap = nv.drawBitmap.slice();
@@ -10,12 +10,71 @@ var __assign = (this && this.__assign) || function () {
10
10
  return __assign.apply(this, arguments);
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import { useEffect, useState } from "react";
13
14
  import "./tk-dual-range.css";
14
15
  var clamp = function (v, lo, hi) { return Math.max(lo, Math.min(hi, v)); };
16
+ // Display REAL-space values; use scientific notation for small non-zero values
17
+ // so they don't display as "0.000". Use type="text" because type="number" can
18
+ // show "0" for very small values instead of scientific notation.
19
+ function fmt(v, precision) {
20
+ if (!Number.isFinite(v))
21
+ return "";
22
+ return v !== 0 && Math.abs(v) < 0.01
23
+ ? Number(v).toExponential(precision)
24
+ : v.toFixed(precision);
25
+ }
26
+ function parse(s) {
27
+ var n = Number(s);
28
+ return Number.isFinite(n) ? n : NaN;
29
+ }
30
+ /**
31
+ * A text input that holds a local string draft while the user is typing,
32
+ * committing only on blur or Enter. This lets the user type multi-digit and
33
+ * decimal values without mid-entry validation snapping the field.
34
+ * Shows a small red "Out of range" message when the committed value falls
35
+ * outside [lo, hi].
36
+ */
37
+ function DeferredInput(_a) {
38
+ var committed = _a.committed, onCommit = _a.onCommit, lo = _a.lo, hi = _a.hi, className = _a.className;
39
+ var _b = useState(committed), draft = _b[0], setDraft = _b[1];
40
+ var _c = useState(false), focused = _c[0], setFocused = _c[1];
41
+ var _d = useState(null), error = _d[0], setError = _d[1];
42
+ // Sync external changes into the field only when the user is not typing.
43
+ useEffect(function () {
44
+ if (!focused)
45
+ setDraft(committed);
46
+ }, [committed, focused]);
47
+ var commit = function (value) {
48
+ var n = Number(value);
49
+ if (!Number.isFinite(n)) {
50
+ setError("\"".concat(value, "\" is not a valid number"));
51
+ }
52
+ else if (n < lo || n > hi) {
53
+ setError("".concat(value.trim(), " is out of range"));
54
+ }
55
+ else {
56
+ setError(null);
57
+ }
58
+ onCommit(value);
59
+ // After commit the parent re-formats, so sync back to avoid a stale draft.
60
+ setDraft(committed);
61
+ };
62
+ return (_jsxs("div", __assign({ className: "tkdr__input-wrap" }, { children: [_jsx("input", { className: "".concat(className !== null && className !== void 0 ? className : "").concat(error ? " tkdr__num--error" : ""), type: "text", inputMode: "decimal", value: draft, onChange: function (e) { return setDraft(e.target.value); }, onFocus: function () {
63
+ setFocused(true);
64
+ setError(null);
65
+ }, onBlur: function (e) {
66
+ setFocused(false);
67
+ commit(e.target.value);
68
+ }, onKeyDown: function (e) {
69
+ if (e.key === "Enter") {
70
+ e.target.blur();
71
+ }
72
+ } }), error && _jsx("span", __assign({ className: "tkdr__error" }, { children: error }))] })));
73
+ }
15
74
  export default function TKDualRange(_a) {
16
75
  var _b;
17
76
  var _c = _a.name, name = _c === void 0 ? "Values" : _c, minDomain = _a.minDomain, maxDomain = _a.maxDomain, valueLow = _a.valueLow, valueHigh = _a.valueHigh, onChangeLow = _a.onChangeLow, onChangeHigh = _a.onChangeHigh, _d = _a.transform, transform = _d === void 0 ? function (x) { return x; } : _d, _e = _a.inverse, inverse = _e === void 0 ? function (y) { return y; } : _e, step = _a.step, _f = _a.precision, precision = _f === void 0 ? 3 : _f, _g = _a.accentColor, accentColor = _g === void 0 ? "#580f8b" : _g;
18
- // Map domain & current values into RENDER space (like TestKarts)
77
+ // Map domain & current values into RENDER space
19
78
  var tMin = transform(minDomain);
20
79
  var tMax = transform(maxDomain);
21
80
  var tLow = transform(valueLow);
@@ -23,7 +82,7 @@ export default function TKDualRange(_a) {
23
82
  var span = Math.max(1e-12, tMax - tMin);
24
83
  var pct = function (t) { return ((t - tMin) / span) * 100; };
25
84
  var s = step !== null && step !== void 0 ? step : Math.max(span * 0.001, Number.EPSILON);
26
- // Keep ends from crossing; clamp in REAL space against the other end
85
+ // Keep ends from crossing; clamp in REAL space against the other end.
27
86
  var handleLowRender = function (nextRender) {
28
87
  var nextReal = clamp(inverse(nextRender), minDomain, valueHigh);
29
88
  onChangeLow(nextReal);
@@ -32,37 +91,13 @@ export default function TKDualRange(_a) {
32
91
  var nextReal = clamp(inverse(nextRender), valueLow, maxDomain);
33
92
  onChangeHigh(nextReal);
34
93
  };
35
- // Display REAL-space values (matching the color bar); use scientific notation for
36
- // small non-zero values so they don't display as "0.000". Use type="text" because
37
- // type="number" can show "0" for very small values instead of scientific notation.
38
- var fmt = function (v) {
39
- return Number.isFinite(v)
40
- ? v !== 0 && Math.abs(v) < 0.01
41
- ? Number(v).toExponential(precision)
42
- : v.toFixed(precision)
43
- : "";
44
- };
45
- var parse = function (s) {
46
- var n = Number(s);
47
- return Number.isFinite(n) ? n : NaN;
48
- };
49
- return (_jsxs("div", __assign({ className: "tkdr" }, { children: [_jsxs("div", __assign({ className: "tkdr__row tkdr__row--ends" }, { children: [_jsxs("div", __assign({ className: "tkdr__group" }, { children: [_jsx("span", __assign({ className: "tkdr__hint" }, { children: "Min" })), _jsx("input", { className: "tkdr__num", type: "text", inputMode: "decimal", value: fmt(valueLow), onChange: function (e) {
50
- var n = parse(e.target.value);
51
- if (!Number.isFinite(n))
52
- return;
53
- onChangeLow(clamp(n, minDomain, valueHigh));
54
- }, onBlur: function (e) {
55
- var n = parse(e.target.value);
94
+ return (_jsxs("div", __assign({ className: "tkdr" }, { children: [_jsxs("div", __assign({ className: "tkdr__row tkdr__row--ends" }, { children: [_jsxs("div", __assign({ className: "tkdr__group" }, { children: [_jsx("span", __assign({ className: "tkdr__hint" }, { children: "Min" })), _jsx(DeferredInput, { className: "tkdr__num", committed: fmt(valueLow, precision), lo: minDomain, hi: valueHigh, onCommit: function (raw) {
95
+ var n = parse(raw);
56
96
  if (!Number.isFinite(n))
57
97
  return;
58
98
  onChangeLow(clamp(n, minDomain, valueHigh));
59
- } })] })), _jsxs("div", __assign({ className: "tkdr__group" }, { children: [_jsx("span", __assign({ className: "tkdr__hint" }, { children: "Max" })), _jsx("input", { className: "tkdr__num", type: "text", inputMode: "decimal", value: fmt(valueHigh), onChange: function (e) {
60
- var n = parse(e.target.value);
61
- if (!Number.isFinite(n))
62
- return;
63
- onChangeHigh(clamp(n, valueLow, maxDomain));
64
- }, onBlur: function (e) {
65
- var n = parse(e.target.value);
99
+ } })] })), _jsxs("div", __assign({ className: "tkdr__group" }, { children: [_jsx("span", __assign({ className: "tkdr__hint" }, { children: "Max" })), _jsx(DeferredInput, { className: "tkdr__num", committed: fmt(valueHigh, precision), lo: valueLow, hi: maxDomain, onCommit: function (raw) {
100
+ var n = parse(raw);
66
101
  if (!Number.isFinite(n))
67
102
  return;
68
103
  onChangeHigh(clamp(n, valueLow, maxDomain));
@@ -140,3 +140,25 @@
140
140
  font-weight: 400;
141
141
  font-family: 'Inter', 'Roboto', 'Helvetica', 'Arial', sans-serif;
142
142
  }
143
+
144
+ /* wrapper that stacks the input + error text */
145
+ .tkdr__input-wrap {
146
+ display: flex;
147
+ flex-direction: column;
148
+ align-items: flex-start;
149
+ gap: 2px;
150
+ }
151
+
152
+ /* red outline when the value is invalid */
153
+ .tkdr__num--error {
154
+ border-color: #d32f2f;
155
+ outline-color: #d32f2f;
156
+ }
157
+
158
+ /* small red validation message */
159
+ .tkdr__error {
160
+ font-size: 0.7rem;
161
+ color: #d32f2f;
162
+ line-height: 1;
163
+ white-space: nowrap;
164
+ }
@@ -1469,7 +1469,15 @@ Niivue.prototype.groupLabelsFromSelection = function (
1469
1469
  };
1470
1470
 
1471
1471
  Niivue.prototype.resetScene = function () {
1472
+ // Reset 2D pan + zoom
1472
1473
  this.scene.pan2Dxyzmm = [0, 0, 0, 1];
1474
+ // Reset 3D camera to Niivue defaults (azimuth=110, elevation=10, scale=1)
1475
+ if (typeof this.setRenderAzimuthElevation === "function") {
1476
+ this.setRenderAzimuthElevation(110, 10);
1477
+ }
1478
+ if (typeof this.setScale === "function") {
1479
+ this.setScale(1.0);
1480
+ }
1473
1481
  this.drawScene();
1474
1482
  };
1475
1483
 
@@ -1490,18 +1498,18 @@ Niivue.prototype.recenter = function () {
1490
1498
  };
1491
1499
 
1492
1500
  Niivue.prototype.resetZoom = function () {
1493
- // this.scene.pan2Dxyzmm[0] = 0;
1494
- // this.scene.pan2Dxyzmm[1] = 0;
1495
- // this.scene.pan2Dxyzmm[2] = 0;
1496
-
1501
+ // Reset 2D zoom (pan stays, only zoom component adjusted)
1497
1502
  const zoom = 1;
1498
-
1499
1503
  const zoomChange = this.scene.pan2Dxyzmm[3] - zoom;
1500
1504
  this.scene.pan2Dxyzmm[3] = zoom;
1501
1505
  const mm = this.frac2mm(this.scene.crosshairPos);
1502
1506
  this.scene.pan2Dxyzmm[0] += zoomChange * mm[0];
1503
1507
  this.scene.pan2Dxyzmm[1] += zoomChange * mm[1];
1504
1508
  this.scene.pan2Dxyzmm[2] += zoomChange * mm[2];
1509
+ // Reset 3D zoom to default scale
1510
+ if (typeof this.setScale === "function") {
1511
+ this.setScale(1.0);
1512
+ }
1505
1513
  this.drawScene();
1506
1514
  };
1507
1515
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmr-ux",
3
- "version": "4.8.6",
3
+ "version": "4.8.8",
4
4
  "author": "erosmontin@gmail.com",
5
5
  "license": "MIT",
6
6
  "repository": "erosmontin/cloudmr-ux",