react-os-shell 0.3.18 → 0.3.20

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.
@@ -514,16 +514,17 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
514
514
  const [showHint, setShowHint] = useState(true);
515
515
  const [measureEnabled, setMeasureEnabled] = useState(false);
516
516
  const [measureMode, setMeasureMode] = useState("horizontal");
517
- const [measureAutocad, setMeasureAutocad] = useState(true);
518
517
  const [measureDistance, setMeasureDistance] = useState(null);
518
+ const [measureFixedDist, setMeasureFixedDist] = useState(null);
519
+ const [measureFixedInput, setMeasureFixedInput] = useState("");
519
520
  const measureModeRef = useRef(measureMode);
520
- const measureAutocadRef = useRef(measureAutocad);
521
+ const measureFixedDistRef = useRef(measureFixedDist);
521
522
  useEffect(() => {
522
523
  measureModeRef.current = measureMode;
523
524
  }, [measureMode]);
524
525
  useEffect(() => {
525
- measureAutocadRef.current = measureAutocad;
526
- }, [measureAutocad]);
526
+ measureFixedDistRef.current = measureFixedDist;
527
+ }, [measureFixedDist]);
527
528
  const measureRedrawRef = useRef(null);
528
529
  const measureRef = useRef(null);
529
530
  useEffect(() => {
@@ -692,14 +693,23 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
692
693
  lineEl.setAttribute("stroke-linecap", "round");
693
694
  lineEl.style.display = "none";
694
695
  svg.appendChild(lineEl);
696
+ const fixedLineEl = document.createElementNS("http://www.w3.org/2000/svg", "line");
697
+ fixedLineEl.setAttribute("stroke", "#ff8800");
698
+ fixedLineEl.setAttribute("stroke-width", "1.5");
699
+ fixedLineEl.setAttribute("stroke-linecap", "round");
700
+ fixedLineEl.style.display = "none";
701
+ svg.appendChild(fixedLineEl);
695
702
  const snapEl = document.createElement("div");
696
703
  snapEl.style.cssText = `position:absolute;width:14px;height:14px;border:2px solid #ff8800;background:rgba(255,255,255,0.7);transform:translate(-50%,-50%) rotate(45deg);box-sizing:border-box;display:none;`;
697
704
  overlay.appendChild(snapEl);
698
705
  measureRef.current = {
699
706
  picks: [],
707
+ rawSecondClick: null,
700
708
  overlay,
701
709
  svg,
702
710
  line: lineEl,
711
+ fixedLine: fixedLineEl,
712
+ fixedLabel: null,
703
713
  refLine: refLineEl,
704
714
  extLineA,
705
715
  extLineB,
@@ -743,11 +753,25 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
743
753
  }
744
754
  })();
745
755
  const SNAP_PX = 12;
756
+ const segSegIntersect = (A, B) => {
757
+ const x1 = A.ax, y1 = A.ay, x2 = A.bx, y2 = A.by;
758
+ const x3 = B.ax, y3 = B.ay, x4 = B.bx, y4 = B.by;
759
+ const d = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
760
+ if (Math.abs(d) < 1e-12) return null;
761
+ const t = ((x3 - x1) * (y4 - y3) - (y3 - y1) * (x4 - x3)) / d;
762
+ const u = ((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) / d;
763
+ const eps = 1e-6;
764
+ if (t < -eps || t > 1 + eps || u < -eps || u > 1 + eps) return null;
765
+ return { x: x1 + t * (x2 - x1), y: y1 + t * (y2 - y1) };
766
+ };
746
767
  const findSnap = (cx, cy) => {
747
768
  const s = measureRef.current;
748
769
  if (!s || !ready) return null;
749
770
  let best = null;
750
771
  let bestD2 = SNAP_PX * SNAP_PX;
772
+ const NEARBY_PX = SNAP_PX * 3;
773
+ const nearbyD2 = NEARBY_PX * NEARBY_PX;
774
+ const nearby = [];
751
775
  for (const seg of s.segments) {
752
776
  const ap = pxFromScene(seg.ax, seg.ay);
753
777
  const bp = pxFromScene(seg.bx, seg.by);
@@ -755,35 +779,53 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
755
779
  const llen = Math.sqrt(ldx * ldx + ldy * ldy) || 1;
756
780
  const segDir = { dx: ldx / llen, dy: ldy / llen };
757
781
  let dx = cx - ap.x, dy = cy - ap.y;
758
- let d2 = dx * dx + dy * dy;
759
- if (d2 < bestD2) {
782
+ let dEpA2 = dx * dx + dy * dy;
783
+ if (dEpA2 < bestD2) {
760
784
  best = { sx: seg.ax, sy: seg.ay, type: "endpoint", dir: segDir };
761
- bestD2 = d2;
785
+ bestD2 = dEpA2;
762
786
  }
763
787
  dx = cx - bp.x;
764
788
  dy = cy - bp.y;
765
- d2 = dx * dx + dy * dy;
766
- if (d2 < bestD2) {
789
+ let dEpB2 = dx * dx + dy * dy;
790
+ if (dEpB2 < bestD2) {
767
791
  best = { sx: seg.bx, sy: seg.by, type: "endpoint", dir: segDir };
768
- bestD2 = d2;
792
+ bestD2 = dEpB2;
769
793
  }
770
794
  const sdx = bp.x - ap.x, sdy = bp.y - ap.y;
771
795
  const len2 = sdx * sdx + sdy * sdy;
796
+ let dLine2 = Infinity;
772
797
  if (len2 > 0) {
773
798
  const t = ((cx - ap.x) * sdx + (cy - ap.y) * sdy) / len2;
774
799
  if (t > 0 && t < 1) {
775
800
  const px = ap.x + t * sdx, py = ap.y + t * sdy;
776
801
  dx = cx - px;
777
802
  dy = cy - py;
778
- d2 = dx * dx + dy * dy;
779
- if (d2 < bestD2) {
803
+ dLine2 = dx * dx + dy * dy;
804
+ if (dLine2 < bestD2) {
780
805
  const sx = seg.ax + t * (seg.bx - seg.ax);
781
806
  const sy = seg.ay + t * (seg.by - seg.ay);
782
807
  best = { sx, sy, type: "line", dir: segDir };
783
- bestD2 = d2;
808
+ bestD2 = dLine2;
784
809
  }
785
810
  }
786
811
  }
812
+ if (dEpA2 < nearbyD2 || dEpB2 < nearbyD2 || dLine2 < nearbyD2) {
813
+ nearby.push(seg);
814
+ }
815
+ }
816
+ for (let i = 0; i < nearby.length; i++) {
817
+ for (let j = i + 1; j < nearby.length; j++) {
818
+ const ix = segSegIntersect(nearby[i], nearby[j]);
819
+ if (!ix) continue;
820
+ const p = pxFromScene(ix.x, ix.y);
821
+ const dx = cx - p.x, dy = cy - p.y;
822
+ const d2 = dx * dx + dy * dy;
823
+ if (d2 >= SNAP_PX * SNAP_PX) continue;
824
+ if (!best || best.type !== "intersection" || d2 < bestD2) {
825
+ best = { sx: ix.x, sy: ix.y, type: "intersection" };
826
+ bestD2 = d2;
827
+ }
828
+ }
787
829
  }
788
830
  return best;
789
831
  };
@@ -807,24 +849,48 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
807
849
  el.style.left = `${p.x}px`;
808
850
  el.style.top = `${p.y}px`;
809
851
  };
810
- const computeRenderedEnds = () => {
852
+ const reconcileLockedPick = () => {
853
+ const s = measureRef.current;
854
+ if (!s || !s.rawSecondClick || s.picks.length < 2) return;
855
+ const a = s.picks[0];
856
+ const raw = s.rawSecondClick;
857
+ const fixed = measureFixedDistRef.current;
858
+ const mode = measureModeRef.current;
859
+ if (fixed !== null && Number.isFinite(fixed) && (mode === "horizontal" || mode === "vertical")) {
860
+ if (mode === "horizontal") {
861
+ const sign = raw.x >= a.x ? 1 : -1;
862
+ s.picks[1] = { x: a.x + sign * Math.abs(fixed), y: raw.y };
863
+ } else {
864
+ const sign = raw.y >= a.y ? 1 : -1;
865
+ s.picks[1] = { x: raw.x, y: a.y + sign * Math.abs(fixed) };
866
+ }
867
+ } else {
868
+ s.picks[1] = { x: raw.x, y: raw.y };
869
+ }
870
+ };
871
+ const computeMainDimEnds = () => {
811
872
  const s = measureRef.current;
812
873
  const a = s.picks[0];
813
874
  const b = s.picks[1];
814
875
  const mode = measureModeRef.current;
815
- if (mode === "horizontal") {
816
- return { from: a, to: { x: b.x, y: a.y } };
876
+ const fixed = measureFixedDistRef.current;
877
+ if (fixed !== null && Number.isFinite(fixed) && mode === "horizontal") {
878
+ return { from: { x: b.x, y: a.y }, to: { x: b.x, y: b.y } };
817
879
  }
818
- if (mode === "vertical") {
819
- return { from: a, to: { x: a.x, y: b.y } };
880
+ if (fixed !== null && Number.isFinite(fixed) && mode === "vertical") {
881
+ return { from: { x: a.x, y: b.y }, to: { x: b.x, y: b.y } };
820
882
  }
883
+ if (mode === "horizontal") return { from: a, to: { x: b.x, y: a.y } };
884
+ if (mode === "vertical") return { from: a, to: { x: a.x, y: b.y } };
821
885
  return { from: a, to: b };
822
886
  };
823
887
  const updateOverlay = () => {
824
888
  const s = measureRef.current;
825
889
  if (!s) return;
890
+ reconcileLockedPick();
826
891
  const mode = measureModeRef.current;
827
- const autocad = measureAutocadRef.current;
892
+ const fixed = measureFixedDistRef.current;
893
+ const fixedActive = fixed !== null && Number.isFinite(fixed) && (mode === "horizontal" || mode === "vertical");
828
894
  if (s.markers[0]) positionMarker(s.markers[0], s.picks[0].x, s.picks[0].y);
829
895
  if (s.markers[1]) positionMarker(s.markers[1], s.picks[1].x, s.picks[1].y);
830
896
  const refDir = mode === "horizontal" ? { dx: 1, dy: 0 } : mode === "vertical" ? { dx: 0, dy: 1 } : null;
@@ -852,24 +918,43 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
852
918
  }
853
919
  if (s.picks.length === 2) {
854
920
  const a = s.picks[0], b = s.picks[1];
855
- const ends = computeRenderedEnds();
921
+ const ends = computeMainDimEnds();
856
922
  const fp = pxFromScene(ends.from.x, ends.from.y);
857
923
  const tp = pxFromScene(ends.to.x, ends.to.y);
858
924
  s.line.setAttribute("x1", String(fp.x));
859
925
  s.line.setAttribute("y1", String(fp.y));
860
926
  s.line.setAttribute("x2", String(tp.x));
861
927
  s.line.setAttribute("y2", String(tp.y));
862
- s.line.style.display = "";
863
- if (autocad) {
864
- s.line.setAttribute("marker-start", "url(#dxf-measure-arrow)");
865
- s.line.setAttribute("marker-end", "url(#dxf-measure-arrow)");
928
+ s.line.setAttribute("marker-start", "url(#dxf-measure-arrow)");
929
+ s.line.setAttribute("marker-end", "url(#dxf-measure-arrow)");
930
+ const mainLen = Math.hypot(tp.x - fp.x, tp.y - fp.y);
931
+ s.line.style.display = mainLen > 0.5 ? "" : "none";
932
+ if (fixedActive && s.fixedLine) {
933
+ const r = mode === "horizontal" ? { x: b.x, y: a.y } : { x: a.x, y: b.y };
934
+ const ap = pxFromScene(a.x, a.y);
935
+ const rp = pxFromScene(r.x, r.y);
936
+ s.fixedLine.setAttribute("x1", String(ap.x));
937
+ s.fixedLine.setAttribute("y1", String(ap.y));
938
+ s.fixedLine.setAttribute("x2", String(rp.x));
939
+ s.fixedLine.setAttribute("y2", String(rp.y));
940
+ s.fixedLine.setAttribute("marker-start", "url(#dxf-measure-arrow)");
941
+ s.fixedLine.setAttribute("marker-end", "url(#dxf-measure-arrow)");
942
+ s.fixedLine.style.display = "";
943
+ const fLabel = ensureFixedLabel();
944
+ fLabel.textContent = formatMeasureDistance(Math.abs(fixed));
945
+ const fcx = (ap.x + rp.x) / 2;
946
+ const fcy = (ap.y + rp.y) / 2;
947
+ fLabel.style.left = `${fcx}px`;
948
+ fLabel.style.top = `${fcy}px`;
949
+ const w = canvas.clientWidth, h = canvas.clientHeight;
950
+ fLabel.style.display = fcx < 0 || fcy < 0 || fcx > w || fcy > h ? "none" : "";
866
951
  } else {
867
- s.line.removeAttribute("marker-start");
868
- s.line.removeAttribute("marker-end");
952
+ if (s.fixedLine) s.fixedLine.style.display = "none";
953
+ if (s.fixedLabel) s.fixedLabel.style.display = "none";
869
954
  }
870
955
  const extA = s.extLineA;
871
956
  const extB = s.extLineB;
872
- if (autocad && mode === "horizontal" && Math.abs(b.y - a.y) > 1e-9) {
957
+ if (!fixedActive && mode === "horizontal" && Math.abs(b.y - a.y) > 1e-9) {
873
958
  const p = pxFromScene(b.x, a.y);
874
959
  const q = pxFromScene(b.x, b.y);
875
960
  extB.setAttribute("x1", String(p.x));
@@ -878,7 +963,7 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
878
963
  extB.setAttribute("y2", String(q.y));
879
964
  extB.style.display = "";
880
965
  extA.style.display = "none";
881
- } else if (autocad && mode === "vertical" && Math.abs(b.x - a.x) > 1e-9) {
966
+ } else if (!fixedActive && mode === "vertical" && Math.abs(b.x - a.x) > 1e-9) {
882
967
  const p = pxFromScene(a.x, b.y);
883
968
  const q = pxFromScene(b.x, b.y);
884
969
  extB.setAttribute("x1", String(p.x));
@@ -898,13 +983,25 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
898
983
  s.label.style.top = `${cy}px`;
899
984
  const w = canvas.clientWidth, h = canvas.clientHeight;
900
985
  s.label.style.display = cx < 0 || cy < 0 || cx > w || cy > h ? "none" : "";
986
+ if (mainLen <= 0.5) s.label.style.display = "none";
901
987
  }
902
988
  } else {
903
989
  s.line.style.display = "none";
990
+ if (s.fixedLine) s.fixedLine.style.display = "none";
991
+ if (s.fixedLabel) s.fixedLabel.style.display = "none";
904
992
  if (s.extLineA) s.extLineA.style.display = "none";
905
993
  if (s.extLineB) s.extLineB.style.display = "none";
906
994
  }
907
995
  };
996
+ const ensureFixedLabel = () => {
997
+ const s = measureRef.current;
998
+ if (s.fixedLabel) return s.fixedLabel;
999
+ const el = document.createElement("div");
1000
+ el.style.cssText = `position:absolute;left:-9999px;top:-9999px;transform:translate(-50%,-50%);padding:2px 6px;font-size:11px;font-weight:600;font-family:system-ui,-apple-system,sans-serif;background:rgba(255,136,0,0.75);color:#fff;border-radius:4px;white-space:nowrap;box-shadow:0 1px 4px rgba(0,0,0,0.25);pointer-events:none;`;
1001
+ overlay.appendChild(el);
1002
+ s.fixedLabel = el;
1003
+ return el;
1004
+ };
908
1005
  const DRAG_TOL = 4;
909
1006
  const DRAG_TIME = 350;
910
1007
  let downX = 0, downY = 0, downTime = 0, downActive = false, dragging = false;
@@ -954,11 +1051,20 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
954
1051
  const recomputeLabel = () => {
955
1052
  const s = measureRef.current;
956
1053
  if (!s || s.picks.length !== 2) return;
1054
+ reconcileLockedPick();
957
1055
  const a = s.picks[0], b = s.picks[1];
958
1056
  const mode = measureModeRef.current;
1057
+ const fixed = measureFixedDistRef.current;
1058
+ const fixedActive = fixed !== null && Number.isFinite(fixed) && (mode === "horizontal" || mode === "vertical");
959
1059
  let dist;
960
1060
  let suffix = "";
961
- if (mode === "horizontal") {
1061
+ if (fixedActive && mode === "horizontal") {
1062
+ dist = Math.abs(b.y - a.y);
1063
+ suffix = " \u2195";
1064
+ } else if (fixedActive && mode === "vertical") {
1065
+ dist = Math.abs(b.x - a.x);
1066
+ suffix = " \u2194";
1067
+ } else if (mode === "horizontal") {
962
1068
  dist = Math.abs(b.x - a.x);
963
1069
  suffix = " \u2194";
964
1070
  } else if (mode === "vertical") {
@@ -984,16 +1090,23 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
984
1090
  for (const m of s.markers) m.parentElement?.removeChild(m);
985
1091
  s.markers = [];
986
1092
  s.picks = [];
1093
+ s.rawSecondClick = null;
987
1094
  s.line.style.display = "none";
1095
+ if (s.fixedLine) s.fixedLine.style.display = "none";
1096
+ if (s.fixedLabel) s.fixedLabel.style.display = "none";
988
1097
  if (s.refLine) s.refLine.style.display = "none";
989
1098
  if (s.extLineA) s.extLineA.style.display = "none";
990
1099
  if (s.extLineB) s.extLineB.style.display = "none";
991
1100
  if (s.label) s.label.style.opacity = "0";
992
1101
  setMeasureDistance(null);
993
1102
  }
994
- s.picks.push({ x: p.x, y: p.y });
995
- s.markers.push(makeMarker());
996
- if (s.picks.length === 2) {
1103
+ if (s.picks.length === 0) {
1104
+ s.picks.push({ x: p.x, y: p.y });
1105
+ s.markers.push(makeMarker());
1106
+ } else {
1107
+ s.rawSecondClick = { x: p.x, y: p.y };
1108
+ s.picks.push({ x: p.x, y: p.y });
1109
+ s.markers.push(makeMarker());
997
1110
  recomputeLabel();
998
1111
  }
999
1112
  updateOverlay();
@@ -1039,7 +1152,7 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1039
1152
  }, [measureEnabled, loading, error]);
1040
1153
  useEffect(() => {
1041
1154
  measureRedrawRef.current?.();
1042
- }, [measureMode, measureAutocad]);
1155
+ }, [measureMode, measureFixedDist]);
1043
1156
  const toggleLayer = (name) => {
1044
1157
  setLayers((prev) => prev.map((l) => {
1045
1158
  if (l.name !== name) return l;
@@ -1130,21 +1243,6 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1130
1243
  }
1131
1244
  ),
1132
1245
  measureEnabled && /* @__PURE__ */ jsxs(Fragment, { children: [
1133
- /* @__PURE__ */ jsx(
1134
- "button",
1135
- {
1136
- onClick: () => {
1137
- setMeasureAutocad((prev) => {
1138
- const next = !prev;
1139
- if (next && measureMode === "point") setMeasureMode("horizontal");
1140
- return next;
1141
- });
1142
- },
1143
- className: btn + (measureAutocad ? " bg-orange-100 text-orange-700" : ""),
1144
- title: measureAutocad ? "AutoCAD dim style is ON \u2014 extension lines + arrows" : "AutoCAD dim style is OFF \u2014 plain line. Click to turn on (also switches to H if you're in Point).",
1145
- children: "\u22A5"
1146
- }
1147
- ),
1148
1246
  /* @__PURE__ */ jsxs("div", { className: "flex items-stretch h-7 rounded border border-gray-200 overflow-hidden text-[11px] font-semibold", children: [
1149
1247
  /* @__PURE__ */ jsx(
1150
1248
  "button",
@@ -1173,6 +1271,41 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1173
1271
  children: "V"
1174
1272
  }
1175
1273
  )
1274
+ ] }),
1275
+ (measureMode === "horizontal" || measureMode === "vertical") && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1276
+ /* @__PURE__ */ jsx(
1277
+ "input",
1278
+ {
1279
+ type: "number",
1280
+ inputMode: "decimal",
1281
+ step: "any",
1282
+ value: measureFixedInput,
1283
+ placeholder: measureMode === "horizontal" ? "fix \u0394x" : "fix \u0394y",
1284
+ onChange: (e) => {
1285
+ const raw = e.target.value;
1286
+ setMeasureFixedInput(raw);
1287
+ if (raw.trim() === "") setMeasureFixedDist(null);
1288
+ else {
1289
+ const n = parseFloat(raw);
1290
+ setMeasureFixedDist(Number.isFinite(n) && n !== 0 ? n : null);
1291
+ }
1292
+ },
1293
+ className: "h-7 w-20 px-1.5 text-[11px] font-mono rounded border border-gray-200 bg-white text-gray-700 focus:outline-none focus:border-orange-400",
1294
+ title: "Lock the second pick's axis coord to first pick + this value (mm). The reported measurement becomes the perpendicular distance."
1295
+ }
1296
+ ),
1297
+ measureFixedDist !== null && /* @__PURE__ */ jsx(
1298
+ "button",
1299
+ {
1300
+ onClick: () => {
1301
+ setMeasureFixedDist(null);
1302
+ setMeasureFixedInput("");
1303
+ },
1304
+ className: "text-gray-400 hover:text-gray-600 text-[11px] px-1",
1305
+ title: "Clear fixed distance",
1306
+ children: "\xD7"
1307
+ }
1308
+ )
1176
1309
  ] })
1177
1310
  ] }),
1178
1311
  measureEnabled && measureDistance !== null && /* @__PURE__ */ jsxs(
@@ -2546,5 +2679,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2546
2679
  }
2547
2680
 
2548
2681
  export { Preview, setPdfPreview };
2549
- //# sourceMappingURL=chunk-HADXZNYO.js.map
2550
- //# sourceMappingURL=chunk-HADXZNYO.js.map
2682
+ //# sourceMappingURL=chunk-SYPEBLWY.js.map
2683
+ //# sourceMappingURL=chunk-SYPEBLWY.js.map