react-os-shell 2.3.0 → 2.5.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.
Files changed (51) hide show
  1. package/dist/Browser-PH57GK7S.js +7 -0
  2. package/dist/{Browser-SP6PGPN5.js.map → Browser-PH57GK7S.js.map} +1 -1
  3. package/dist/{Calculator-XGXSRMF2.js → Calculator-H7HMALL3.js} +4 -4
  4. package/dist/{Calculator-XGXSRMF2.js.map → Calculator-H7HMALL3.js.map} +1 -1
  5. package/dist/{CurrencyConverter-SVYA7ZGT.js → CurrencyConverter-576WXG7B.js} +4 -4
  6. package/dist/{CurrencyConverter-SVYA7ZGT.js.map → CurrencyConverter-576WXG7B.js.map} +1 -1
  7. package/dist/{Documents-QXF5BF3N.js → Documents-3SXOEFIQ.js} +4 -4
  8. package/dist/{Documents-QXF5BF3N.js.map → Documents-3SXOEFIQ.js.map} +1 -1
  9. package/dist/Files-YIYWFGHQ.js +13 -0
  10. package/dist/{Files-JUO4Q7WI.js.map → Files-YIYWFGHQ.js.map} +1 -1
  11. package/dist/{Notepad-EQ5YTHYQ.js → Notepad-BZBRXG43.js} +4 -4
  12. package/dist/{Notepad-EQ5YTHYQ.js.map → Notepad-BZBRXG43.js.map} +1 -1
  13. package/dist/{PomodoroTimer-L62PEYPP.js → PomodoroTimer-PQ7FXSKY.js} +4 -4
  14. package/dist/{PomodoroTimer-L62PEYPP.js.map → PomodoroTimer-PQ7FXSKY.js.map} +1 -1
  15. package/dist/Preview-L5ABFUSO.js +9 -0
  16. package/dist/{Preview-FHEOYNOR.js.map → Preview-L5ABFUSO.js.map} +1 -1
  17. package/dist/Spreadsheet-WDMPVUDW.js +7 -0
  18. package/dist/{Spreadsheet-Q2YHXKP4.js.map → Spreadsheet-WDMPVUDW.js.map} +1 -1
  19. package/dist/{Stock-EDXEPZFF.js → Stock-NQFKY52J.js} +4 -4
  20. package/dist/{Stock-EDXEPZFF.js.map → Stock-NQFKY52J.js.map} +1 -1
  21. package/dist/{Weather-T2EVJ5NE.js → Weather-KH5A7AZ3.js} +4 -4
  22. package/dist/{Weather-T2EVJ5NE.js.map → Weather-KH5A7AZ3.js.map} +1 -1
  23. package/dist/{WorldClock-JN7YJN3B.js → WorldClock-VUMFYV5V.js} +4 -4
  24. package/dist/{WorldClock-JN7YJN3B.js.map → WorldClock-VUMFYV5V.js.map} +1 -1
  25. package/dist/apps/index.js +19 -19
  26. package/dist/{chunk-FISSBIJU.js → chunk-6YJFK6H3.js} +4 -4
  27. package/dist/{chunk-FISSBIJU.js.map → chunk-6YJFK6H3.js.map} +1 -1
  28. package/dist/{chunk-KGDRIEI4.js → chunk-7CXMEEUA.js} +5 -5
  29. package/dist/{chunk-KGDRIEI4.js.map → chunk-7CXMEEUA.js.map} +1 -1
  30. package/dist/{chunk-G5VOT4ER.js → chunk-FPOVTUH2.js} +4 -4
  31. package/dist/{chunk-G5VOT4ER.js.map → chunk-FPOVTUH2.js.map} +1 -1
  32. package/dist/{chunk-IBNOPYS4.js → chunk-QMX4QCJG.js} +4 -4
  33. package/dist/{chunk-IBNOPYS4.js.map → chunk-QMX4QCJG.js.map} +1 -1
  34. package/dist/{chunk-7YHB7JZ3.js → chunk-QQ3K3EMW.js} +6 -6
  35. package/dist/{chunk-7YHB7JZ3.js.map → chunk-QQ3K3EMW.js.map} +1 -1
  36. package/dist/{chunk-QAHF22KW.js → chunk-TJ6N7SI5.js} +24 -3
  37. package/dist/chunk-TJ6N7SI5.js.map +1 -0
  38. package/dist/{chunk-4LQVJQ3S.js → chunk-UFTJG6IM.js} +682 -127
  39. package/dist/chunk-UFTJG6IM.js.map +1 -0
  40. package/dist/{chunk-5V6OCGI6.js → chunk-YVIW5GPB.js} +3 -3
  41. package/dist/{chunk-5V6OCGI6.js.map → chunk-YVIW5GPB.js.map} +1 -1
  42. package/dist/index.d.ts +13 -3
  43. package/dist/index.js +25 -11
  44. package/dist/index.js.map +1 -1
  45. package/package.json +1 -1
  46. package/dist/Browser-SP6PGPN5.js +0 -7
  47. package/dist/Files-JUO4Q7WI.js +0 -13
  48. package/dist/Preview-FHEOYNOR.js +0 -9
  49. package/dist/Spreadsheet-Q2YHXKP4.js +0 -7
  50. package/dist/chunk-4LQVJQ3S.js.map +0 -1
  51. package/dist/chunk-QAHF22KW.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { ImageAnnotator_default } from './chunk-KUIPWCTJ.js';
2
2
  import { toast_default } from './chunk-WIJ45SYD.js';
3
- import { AboutApp } from './chunk-7YHB7JZ3.js';
4
- import { WindowTitle, getActiveModalId } from './chunk-QAHF22KW.js';
3
+ import { AboutApp } from './chunk-QQ3K3EMW.js';
4
+ import { WindowTitle, registerModalEscapeInterceptor, getActiveModalId } from './chunk-TJ6N7SI5.js';
5
5
  import { createContext, useRef, useEffect, useState, useContext } from 'react';
6
6
  import { createPortal } from 'react-dom';
7
7
  import * as pdfjsLib from 'pdfjs-dist';
@@ -263,8 +263,65 @@ function ConvertingPanel({ filename, message }) {
263
263
  ] });
264
264
  }
265
265
  var ZOOM_PRESETS = [50, 75, 100, 125, 150, 200, 300, 400];
266
+ var TEXT_LAYER_CSS = `
267
+ .preview-pdf-textlayer {
268
+ position: absolute;
269
+ inset: 0;
270
+ overflow: clip;
271
+ line-height: 1;
272
+ text-align: initial;
273
+ text-size-adjust: none;
274
+ forced-color-adjust: none;
275
+ transform-origin: 0 0;
276
+ caret-color: CanvasText;
277
+ --min-font-size: 1;
278
+ --text-scale-factor: calc(var(--total-scale-factor, 1) * var(--min-font-size));
279
+ --min-font-size-inv: calc(1 / var(--min-font-size));
280
+ }
281
+ .preview-pdf-textlayer :is(span, br) {
282
+ color: transparent;
283
+ position: absolute;
284
+ white-space: pre;
285
+ cursor: text;
286
+ transform-origin: 0% 0%;
287
+ }
288
+ .preview-pdf-textlayer > :not(.markedContent),
289
+ .preview-pdf-textlayer .markedContent span:not(.markedContent) {
290
+ z-index: 1;
291
+ --font-height: 0;
292
+ font-size: calc(var(--text-scale-factor) * var(--font-height));
293
+ --scale-x: 1;
294
+ --rotate: 0deg;
295
+ transform: rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv));
296
+ }
297
+ .preview-pdf-textlayer .markedContent {
298
+ display: contents;
299
+ }
300
+ .preview-pdf-textlayer span[role="img"] {
301
+ user-select: none;
302
+ cursor: default;
303
+ }
304
+ .preview-pdf-textlayer ::selection {
305
+ background: rgba(0, 80, 255, 0.25);
306
+ }
307
+ .preview-pdf-textlayer br::selection {
308
+ background: transparent;
309
+ }
310
+ .preview-pdf-textlayer .endOfContent {
311
+ display: block;
312
+ position: absolute;
313
+ inset: 100% 0 0;
314
+ z-index: 0;
315
+ cursor: default;
316
+ user-select: none;
317
+ }
318
+ .preview-pdf-textlayer.selecting .endOfContent {
319
+ top: 0;
320
+ }
321
+ `;
266
322
  function PdfPanel({ url, filename, onDownload, onEmail }) {
267
323
  const canvasRef = useRef(null);
324
+ const textLayerRef = useRef(null);
268
325
  const containerRef = useRef(null);
269
326
  const [pdf, setPdf] = useState(null);
270
327
  const [page, setPage] = useState(1);
@@ -310,6 +367,7 @@ function PdfPanel({ url, filename, onDownload, onEmail }) {
310
367
  if (!pdf || !canvasRef.current) return;
311
368
  let cancelled = false;
312
369
  let task = null;
370
+ let textLayer = null;
313
371
  pdf.getPage(page).then((p) => {
314
372
  if (cancelled || !canvasRef.current) return;
315
373
  const viewport = p.getViewport({ scale });
@@ -322,12 +380,35 @@ function PdfPanel({ url, filename, onDownload, onEmail }) {
322
380
  task = p.render({ canvas, canvasContext: ctx, viewport });
323
381
  task.promise.catch(() => {
324
382
  });
383
+ const textEl = textLayerRef.current;
384
+ if (textEl) {
385
+ textEl.replaceChildren();
386
+ textEl.style.setProperty("--total-scale-factor", String(viewport.scale));
387
+ textLayer = new pdfjsLib.TextLayer({
388
+ textContentSource: p.streamTextContent(),
389
+ container: textEl,
390
+ viewport
391
+ });
392
+ textLayer.render().then(() => {
393
+ if (cancelled) return;
394
+ const end = document.createElement("div");
395
+ end.className = "endOfContent";
396
+ textEl.append(end);
397
+ }).catch(() => {
398
+ });
399
+ }
325
400
  });
326
401
  return () => {
327
402
  cancelled = true;
328
403
  task?.cancel();
404
+ textLayer?.cancel();
329
405
  };
330
406
  }, [pdf, page, scale]);
407
+ useEffect(() => {
408
+ const up = () => textLayerRef.current?.classList.remove("selecting");
409
+ window.addEventListener("pointerup", up);
410
+ return () => window.removeEventListener("pointerup", up);
411
+ }, []);
331
412
  const handlePrint = () => {
332
413
  if (!pdf) return;
333
414
  const win = window.open("", "_blank");
@@ -500,7 +581,20 @@ function PdfPanel({ url, filename, onDownload, onEmail }) {
500
581
  tabIndex: 0,
501
582
  onWheel: onWheelPage,
502
583
  onKeyDown: onKeyPage,
503
- children: loading ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-gray-400 text-sm", children: "Loading PDF..." }) : /* @__PURE__ */ jsx("div", { className: "min-h-full flex items-center justify-center p-4", children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "shadow-lg rounded" }) })
584
+ children: loading ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-gray-400 text-sm", children: "Loading PDF..." }) : /* @__PURE__ */ jsxs("div", { className: "min-h-full flex items-center justify-center p-4", children: [
585
+ /* @__PURE__ */ jsx("style", { children: TEXT_LAYER_CSS }),
586
+ /* @__PURE__ */ jsxs("div", { className: "relative shadow-lg rounded", children: [
587
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "block rounded" }),
588
+ /* @__PURE__ */ jsx(
589
+ "div",
590
+ {
591
+ ref: textLayerRef,
592
+ className: "preview-pdf-textlayer",
593
+ onPointerDown: (e) => e.currentTarget.classList.add("selecting")
594
+ }
595
+ )
596
+ ] })
597
+ ] })
504
598
  }
505
599
  )
506
600
  ] });
@@ -510,6 +604,61 @@ var DEFAULT_DXF_FONTS = [
510
604
  "https://cdn.jsdelivr.net/gh/vagran/dxf-viewer-example-src@master/src/assets/fonts/NotoSansDisplay-SemiCondensedLightItalic.ttf",
511
605
  "https://cdn.jsdelivr.net/gh/vagran/dxf-viewer-example-src@master/src/assets/fonts/NanumGothic-Regular.ttf"
512
606
  ];
607
+ var DXF_EDIT_COMMANDS = {
608
+ l: "LINE",
609
+ line: "LINE",
610
+ pl: "PLINE",
611
+ pline: "PLINE",
612
+ ex: "EXTEND",
613
+ extend: "EXTEND",
614
+ tr: "TRIM",
615
+ trim: "TRIM",
616
+ e: "ERASE",
617
+ erase: "ERASE",
618
+ m: "MOVE",
619
+ move: "MOVE",
620
+ co: "COPY",
621
+ cp: "COPY",
622
+ copy: "COPY",
623
+ o: "OFFSET",
624
+ offset: "OFFSET",
625
+ f: "FILLET",
626
+ fillet: "FILLET",
627
+ cha: "CHAMFER",
628
+ chamfer: "CHAMFER",
629
+ x: "EXPLODE",
630
+ explode: "EXPLODE",
631
+ mi: "MIRROR",
632
+ mirror: "MIRROR",
633
+ ro: "ROTATE",
634
+ rotate: "ROTATE",
635
+ sc: "SCALE",
636
+ scale: "SCALE",
637
+ s: "STRETCH",
638
+ stretch: "STRETCH",
639
+ ar: "ARRAY",
640
+ array: "ARRAY",
641
+ rec: "RECTANG",
642
+ rectang: "RECTANG",
643
+ rectangle: "RECTANG",
644
+ c: "CIRCLE",
645
+ circle: "CIRCLE",
646
+ a: "ARC",
647
+ arc: "ARC",
648
+ el: "ELLIPSE",
649
+ ellipse: "ELLIPSE",
650
+ t: "MTEXT",
651
+ mt: "MTEXT",
652
+ mtext: "MTEXT",
653
+ text: "TEXT",
654
+ b: "BLOCK",
655
+ block: "BLOCK",
656
+ i: "INSERT",
657
+ insert: "INSERT",
658
+ ha: "HATCH",
659
+ hatch: "HATCH"
660
+ };
661
+ var DXF_CMD_HELP = "Commands: DI distance \xB7 DIM/DLI linear dim \xB7 H / V force axis \xB7 <number> lock \u0394 \xB7 U undo pick \xB7 Z fit \xB7 LA layers \xB7 Esc exit";
513
662
  function DxfPanel({ url, filename, onDownload, onEmail }) {
514
663
  const containerRef = useRef(null);
515
664
  const viewerRef = useRef(null);
@@ -519,8 +668,9 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
519
668
  const [showLayers, setShowLayers] = useState(false);
520
669
  const [showHint, setShowHint] = useState(true);
521
670
  const [measureEnabled, setMeasureEnabled] = useState(false);
522
- const [measureMode, setMeasureMode] = useState("horizontal");
671
+ const [measureMode, setMeasureMode] = useState("auto");
523
672
  const [measureDistance, setMeasureDistance] = useState(null);
673
+ const [measureResolved, setMeasureResolved] = useState(null);
524
674
  const [measureFixedDist, setMeasureFixedDist] = useState(null);
525
675
  const [measureFixedInput, setMeasureFixedInput] = useState("");
526
676
  const measureModeRef = useRef(measureMode);
@@ -532,6 +682,13 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
532
682
  measureFixedDistRef.current = measureFixedDist;
533
683
  }, [measureFixedDist]);
534
684
  const measureRedrawRef = useRef(null);
685
+ const measureResetRef = useRef(null);
686
+ const measureUndoRef = useRef(null);
687
+ const [cmdValue, setCmdValue] = useState("");
688
+ const [cmdEcho, setCmdEcho] = useState(null);
689
+ const lastCmdRef = useRef("");
690
+ const cmdInputRef = useRef(null);
691
+ const rootRef = useRef(null);
535
692
  const measureRef = useRef(null);
536
693
  useEffect(() => {
537
694
  let cancelled = false;
@@ -635,6 +792,7 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
635
792
  const t = setTimeout(() => setShowHint(false), 5e3);
636
793
  return () => clearTimeout(t);
637
794
  }, [showHint, loading]);
795
+ const layersKey = layers.map((l) => l.visible ? "1" : "0").join("");
638
796
  useEffect(() => {
639
797
  const v = viewerRef.current;
640
798
  if (loading || error || !v) return;
@@ -651,6 +809,7 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
651
809
  if (!measureEnabled) {
652
810
  teardown();
653
811
  setMeasureDistance(null);
812
+ setMeasureResolved(null);
654
813
  return;
655
814
  }
656
815
  const overlay = document.createElement("div");
@@ -674,13 +833,18 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
674
833
  defs.appendChild(marker);
675
834
  svg.appendChild(defs);
676
835
  overlay.appendChild(svg);
677
- const refLineEl = document.createElementNS("http://www.w3.org/2000/svg", "line");
678
- refLineEl.setAttribute("stroke", "#ff8800");
679
- refLineEl.setAttribute("stroke-width", "1");
680
- refLineEl.setAttribute("stroke-dasharray", "6,4");
681
- refLineEl.setAttribute("opacity", "0.55");
682
- refLineEl.style.display = "none";
683
- svg.appendChild(refLineEl);
836
+ const mkRefLine = () => {
837
+ const el = document.createElementNS("http://www.w3.org/2000/svg", "line");
838
+ el.setAttribute("stroke", "#ff8800");
839
+ el.setAttribute("stroke-width", "1");
840
+ el.setAttribute("stroke-dasharray", "6,4");
841
+ el.setAttribute("opacity", "0.55");
842
+ el.style.display = "none";
843
+ svg.appendChild(el);
844
+ return el;
845
+ };
846
+ const refLineEl = mkRefLine();
847
+ const refLineVEl = mkRefLine();
684
848
  const extLineA = document.createElementNS("http://www.w3.org/2000/svg", "line");
685
849
  extLineA.setAttribute("stroke", "#ff8800");
686
850
  extLineA.setAttribute("stroke-width", "1");
@@ -756,10 +920,44 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
756
920
  poly.setAttribute("stroke-linejoin", "round");
757
921
  g.appendChild(poly);
758
922
  });
923
+ const snapGlyphMidpoint = mkSnapGlyph((g) => {
924
+ const poly = document.createElementNS(SVGNS, "polygon");
925
+ poly.setAttribute("points", "11,3 19,18 3,18");
926
+ poly.setAttribute("fill", "rgba(255,255,255,0.9)");
927
+ poly.setAttribute("stroke", "#ff8800");
928
+ poly.setAttribute("stroke-width", "1.8");
929
+ poly.setAttribute("stroke-linejoin", "round");
930
+ g.appendChild(poly);
931
+ });
932
+ const snapGlyphNode = mkSnapGlyph((g) => {
933
+ const c = document.createElementNS(SVGNS, "circle");
934
+ c.setAttribute("cx", "11");
935
+ c.setAttribute("cy", "11");
936
+ c.setAttribute("r", "7");
937
+ c.setAttribute("fill", "rgba(255,255,255,0.9)");
938
+ c.setAttribute("stroke", "#ff8800");
939
+ c.setAttribute("stroke-width", "1.8");
940
+ g.appendChild(c);
941
+ const mkLn = (x1, y1, x2, y2) => {
942
+ const ln = document.createElementNS(SVGNS, "line");
943
+ ln.setAttribute("x1", String(x1));
944
+ ln.setAttribute("y1", String(y1));
945
+ ln.setAttribute("x2", String(x2));
946
+ ln.setAttribute("y2", String(y2));
947
+ ln.setAttribute("stroke", "#ff8800");
948
+ ln.setAttribute("stroke-width", "1.6");
949
+ ln.setAttribute("stroke-linecap", "round");
950
+ return ln;
951
+ };
952
+ g.appendChild(mkLn(6.5, 6.5, 15.5, 15.5));
953
+ g.appendChild(mkLn(15.5, 6.5, 6.5, 15.5));
954
+ });
759
955
  const setSnapGlyph = (type) => {
760
956
  snapGlyphEndpoint.style.display = type === "endpoint" ? "" : "none";
761
957
  snapGlyphIntersection.style.display = type === "intersection" ? "" : "none";
762
958
  snapGlyphLine.style.display = type === "line" ? "" : "none";
959
+ snapGlyphMidpoint.style.display = type === "midpoint" ? "" : "none";
960
+ snapGlyphNode.style.display = type === "node" ? "" : "none";
763
961
  };
764
962
  overlay.appendChild(snapEl);
765
963
  measureRef.current = {
@@ -771,12 +969,14 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
771
969
  fixedLine: fixedLineEl,
772
970
  fixedLabel: null,
773
971
  refLine: refLineEl,
972
+ refLineV: refLineVEl,
774
973
  extLineA,
775
974
  extLineB,
776
975
  markers: [],
777
976
  label: null,
778
977
  snap: snapEl,
779
- segments: []
978
+ segs: null,
979
+ nodes: null
780
980
  };
781
981
  let THREE = null;
782
982
  let ready = false;
@@ -786,28 +986,82 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
786
986
  const w = canvas.clientWidth, h = canvas.clientHeight;
787
987
  return { x: (v3.x + 1) / 2 * w, y: (-v3.y + 1) / 2 * h };
788
988
  };
989
+ const MAX_SNAP_SEGS = 4e5;
789
990
  (async () => {
790
991
  try {
791
992
  THREE = await import(
792
993
  /* @vite-ignore */
793
994
  'three'
794
995
  );
795
- const segs = measureRef.current?.segments;
796
- if (!segs) return;
797
- const va = new THREE.Vector3(), vb = new THREE.Vector3();
798
- scene.traverse((obj) => {
799
- if (!obj?.isLineSegments) return;
800
- const pos = obj.geometry?.attributes?.position;
996
+ const st = measureRef.current;
997
+ if (!st) return;
998
+ const segXY = [];
999
+ const nodeXY = [];
1000
+ let truncated = false;
1001
+ const instanceXforms = (g) => {
1002
+ const t0 = g?.attributes?.instanceTransform0;
1003
+ const t1 = g?.attributes?.instanceTransform1;
1004
+ if (t0 && t1) {
1005
+ const out = [];
1006
+ const n = Math.min(t0.count, t1.count);
1007
+ for (let k = 0; k < n; k++) {
1008
+ out.push([t0.getX(k), t0.getY(k), t0.getZ(k), t1.getX(k), t1.getY(k), t1.getZ(k)]);
1009
+ }
1010
+ return out;
1011
+ }
1012
+ const tp = g?.attributes?.instanceTransform;
1013
+ if (tp) {
1014
+ const out = [];
1015
+ for (let k = 0; k < tp.count; k++) out.push([1, 0, tp.getX(k), 0, 1, tp.getY(k)]);
1016
+ return out;
1017
+ }
1018
+ return [[1, 0, 0, 0, 1, 0]];
1019
+ };
1020
+ const visit = (obj) => {
1021
+ const isSeg = !!obj?.isLineSegments;
1022
+ const isPts = !!obj?.isPoints;
1023
+ if (!isSeg && !isPts) return;
1024
+ const g = obj.geometry;
1025
+ const pos = g?.attributes?.position;
801
1026
  if (!pos) return;
802
- const arr = pos.array;
803
- obj.updateMatrixWorld?.();
804
- const m = obj.matrixWorld;
805
- for (let i = 0; i < arr.length; i += 6) {
806
- va.set(arr[i], arr[i + 1], arr[i + 2]).applyMatrix4(m);
807
- vb.set(arr[i + 3], arr[i + 4], arr[i + 5]).applyMatrix4(m);
808
- segs.push({ ax: va.x, ay: va.y, bx: vb.x, by: vb.y });
1027
+ const xfs = instanceXforms(g);
1028
+ if (isSeg) {
1029
+ const idx = g.index;
1030
+ const pairCount = Math.floor((idx ? idx.count : pos.count) / 2);
1031
+ for (let p = 0; p < pairCount; p++) {
1032
+ const ia = idx ? idx.getX(2 * p) : 2 * p;
1033
+ const ib = idx ? idx.getX(2 * p + 1) : 2 * p + 1;
1034
+ const ax = pos.getX(ia), ay = pos.getY(ia);
1035
+ const bx = pos.getX(ib), by = pos.getY(ib);
1036
+ if (!Number.isFinite(ax + ay + bx + by)) continue;
1037
+ for (const m of xfs) {
1038
+ if (segXY.length >= MAX_SNAP_SEGS * 4) {
1039
+ truncated = true;
1040
+ return;
1041
+ }
1042
+ const tax = m[0] * ax + m[1] * ay + m[2], tay = m[3] * ax + m[4] * ay + m[5];
1043
+ const tbx = m[0] * bx + m[1] * by + m[2], tby = m[3] * bx + m[4] * by + m[5];
1044
+ if (tax === tbx && tay === tby) continue;
1045
+ segXY.push(tax, tay, tbx, tby);
1046
+ }
1047
+ }
1048
+ } else {
1049
+ for (let i = 0; i < pos.count; i++) {
1050
+ const x = pos.getX(i), y = pos.getY(i);
1051
+ if (!Number.isFinite(x + y)) continue;
1052
+ for (const m of xfs) {
1053
+ nodeXY.push(m[0] * x + m[1] * y + m[2], m[3] * x + m[4] * y + m[5]);
1054
+ }
1055
+ }
809
1056
  }
810
- });
1057
+ };
1058
+ if (typeof scene.traverseVisible === "function") scene.traverseVisible(visit);
1059
+ else scene.traverse(visit);
1060
+ st.segs = new Float64Array(segXY);
1061
+ st.nodes = new Float64Array(nodeXY);
1062
+ if (truncated) {
1063
+ console.warn(`[Preview] DXF snap cache truncated at ${MAX_SNAP_SEGS} segments \u2014 snapping may miss some geometry.`);
1064
+ }
811
1065
  ready = true;
812
1066
  } catch {
813
1067
  }
@@ -824,54 +1078,92 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
824
1078
  };
825
1079
  const findSnap = (cx, cy) => {
826
1080
  const s = measureRef.current;
827
- if (!s || !ready) return null;
828
- let best = null;
829
- let bestD2 = SNAP_PX * SNAP_PX;
830
- const candidateD2 = SNAP_PX * SNAP_PX;
1081
+ if (!s || !ready || !s.segs) return null;
1082
+ const o = pxFromScene(0, 0);
1083
+ const ex = pxFromScene(1, 0);
1084
+ const ey = pxFromScene(0, 1);
1085
+ const mxx = ex.x - o.x, mxy = ex.y - o.y;
1086
+ const myx = ey.x - o.x, myy = ey.y - o.y;
1087
+ const det = mxx * myy - myx * mxy;
1088
+ if (!det) return null;
1089
+ const csx = ((cx - o.x) * myy - (cy - o.y) * myx) / det;
1090
+ const csy = ((cy - o.y) * mxx - (cx - o.x) * mxy) / det;
1091
+ const rx = SNAP_PX / (Math.hypot(mxx, mxy) || 1);
1092
+ const ry = SNAP_PX / (Math.hypot(myx, myy) || 1);
1093
+ const minX = csx - rx, maxX = csx + rx, minY = csy - ry, maxY = csy + ry;
1094
+ const T2 = SNAP_PX * SNAP_PX;
1095
+ let bestPt = null, dPt = T2;
1096
+ let bestMid = null, dMid = T2;
1097
+ let bestLine = null, dLine = T2;
1098
+ let bestX = null, dX = T2;
831
1099
  const cand = [];
832
- for (const seg of s.segments) {
833
- const ap = pxFromScene(seg.ax, seg.ay);
834
- const bp = pxFromScene(seg.bx, seg.by);
835
- const apx = ap.x, apy = ap.y, bpx = bp.x, bpy = bp.y;
836
- const rejectX = apx < cx - SNAP_PX && bpx < cx - SNAP_PX || apx > cx + SNAP_PX && bpx > cx + SNAP_PX;
837
- const rejectY = apy < cy - SNAP_PX && bpy < cy - SNAP_PX || apy > cy + SNAP_PX && bpy > cy + SNAP_PX;
838
- if (rejectX || rejectY) continue;
839
- const ldx = seg.bx - seg.ax, ldy = seg.by - seg.ay;
840
- const llen = Math.sqrt(ldx * ldx + ldy * ldy) || 1;
841
- const segDir = { dx: ldx / llen, dy: ldy / llen };
1100
+ const segs = s.segs;
1101
+ for (let i = 0; i < segs.length; i += 4) {
1102
+ const ax = segs[i], ay = segs[i + 1], bx = segs[i + 2], by = segs[i + 3];
1103
+ if (ax < minX && bx < minX || ax > maxX && bx > maxX || ay < minY && by < minY || ay > maxY && by > maxY) continue;
1104
+ const apx = o.x + mxx * ax + myx * ay, apy = o.y + mxy * ax + myy * ay;
1105
+ const bpx = o.x + mxx * bx + myx * by, bpy = o.y + mxy * bx + myy * by;
1106
+ let near = false;
842
1107
  let dx = cx - apx, dy = cy - apy;
843
- const dEpA2 = dx * dx + dy * dy;
844
- if (dEpA2 < bestD2) {
845
- best = { sx: seg.ax, sy: seg.ay, type: "endpoint", dir: segDir };
846
- bestD2 = dEpA2;
1108
+ let d2 = dx * dx + dy * dy;
1109
+ if (d2 < T2) {
1110
+ near = true;
1111
+ if (d2 < dPt) {
1112
+ dPt = d2;
1113
+ bestPt = { sx: ax, sy: ay, type: "endpoint" };
1114
+ }
847
1115
  }
848
1116
  dx = cx - bpx;
849
1117
  dy = cy - bpy;
850
- const dEpB2 = dx * dx + dy * dy;
851
- if (dEpB2 < bestD2) {
852
- best = { sx: seg.bx, sy: seg.by, type: "endpoint", dir: segDir };
853
- bestD2 = dEpB2;
1118
+ d2 = dx * dx + dy * dy;
1119
+ if (d2 < T2) {
1120
+ near = true;
1121
+ if (d2 < dPt) {
1122
+ dPt = d2;
1123
+ bestPt = { sx: bx, sy: by, type: "endpoint" };
1124
+ }
1125
+ }
1126
+ dx = cx - (apx + bpx) / 2;
1127
+ dy = cy - (apy + bpy) / 2;
1128
+ d2 = dx * dx + dy * dy;
1129
+ if (d2 < T2) {
1130
+ near = true;
1131
+ if (d2 < dMid) {
1132
+ dMid = d2;
1133
+ bestMid = { sx: (ax + bx) / 2, sy: (ay + by) / 2, type: "midpoint" };
1134
+ }
854
1135
  }
855
1136
  const sdx = bpx - apx, sdy = bpy - apy;
856
1137
  const len2 = sdx * sdx + sdy * sdy;
857
- let dLine2 = Infinity;
858
1138
  if (len2 > 0) {
859
1139
  const t = ((cx - apx) * sdx + (cy - apy) * sdy) / len2;
860
1140
  if (t > 0 && t < 1) {
861
- const px = apx + t * sdx, py = apy + t * sdy;
862
- dx = cx - px;
863
- dy = cy - py;
864
- dLine2 = dx * dx + dy * dy;
865
- if (dLine2 < bestD2) {
866
- const sx = seg.ax + t * (seg.bx - seg.ax);
867
- const sy = seg.ay + t * (seg.by - seg.ay);
868
- best = { sx, sy, type: "line", dir: segDir };
869
- bestD2 = dLine2;
1141
+ dx = cx - (apx + t * sdx);
1142
+ dy = cy - (apy + t * sdy);
1143
+ d2 = dx * dx + dy * dy;
1144
+ if (d2 < T2) {
1145
+ near = true;
1146
+ if (d2 < dLine) {
1147
+ dLine = d2;
1148
+ bestLine = { sx: ax + t * (bx - ax), sy: ay + t * (by - ay), type: "line" };
1149
+ }
870
1150
  }
871
1151
  }
872
1152
  }
873
- if (dEpA2 < candidateD2 || dEpB2 < candidateD2 || dLine2 < candidateD2) {
874
- cand.push({ seg, apx, apy, bpx, bpy });
1153
+ if (near) cand.push({ ax, ay, bx, by, apx, apy, bpx, bpy });
1154
+ }
1155
+ const nodes = s.nodes;
1156
+ if (nodes) {
1157
+ for (let i = 0; i < nodes.length; i += 2) {
1158
+ const x = nodes[i], y = nodes[i + 1];
1159
+ if (x < minX || x > maxX || y < minY || y > maxY) continue;
1160
+ const dx = cx - (o.x + mxx * x + myx * y);
1161
+ const dy = cy - (o.y + mxy * x + myy * y);
1162
+ const d2 = dx * dx + dy * dy;
1163
+ if (d2 < dPt) {
1164
+ dPt = d2;
1165
+ bestPt = { sx: x, sy: y, type: "node" };
1166
+ }
875
1167
  }
876
1168
  }
877
1169
  for (let i = 0; i < cand.length; i++) {
@@ -882,16 +1174,13 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
882
1174
  if (!ix) continue;
883
1175
  const dx = cx - ix.px, dy = cy - ix.py;
884
1176
  const d2 = dx * dx + dy * dy;
885
- if (d2 >= SNAP_PX * SNAP_PX) continue;
886
- if (!best || best.type !== "intersection" || d2 < bestD2) {
887
- const sx = A.seg.ax + ix.t * (A.seg.bx - A.seg.ax);
888
- const sy = A.seg.ay + ix.t * (A.seg.by - A.seg.ay);
889
- best = { sx, sy, type: "intersection" };
890
- bestD2 = d2;
891
- }
1177
+ if (d2 >= T2 || d2 >= dX) continue;
1178
+ dX = d2;
1179
+ bestX = { sx: A.ax + ix.t * (A.bx - A.ax), sy: A.ay + ix.t * (A.by - A.ay), type: "intersection" };
892
1180
  }
893
1181
  }
894
- return best;
1182
+ const top = bestX && bestPt ? dX < dPt - 1 ? bestX : bestPt : bestX ?? bestPt;
1183
+ return top ?? bestMid ?? bestLine;
895
1184
  };
896
1185
  const makeMarker = () => {
897
1186
  const el = document.createElement("div");
@@ -932,16 +1221,23 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
932
1221
  s.picks[1] = { x: raw.x, y: raw.y };
933
1222
  }
934
1223
  };
1224
+ const resolveMode = (a, b) => {
1225
+ const mode = measureModeRef.current;
1226
+ if (mode !== "auto") return mode;
1227
+ if (!a || !b) return "horizontal";
1228
+ return Math.abs(b.x - a.x) >= Math.abs(b.y - a.y) ? "horizontal" : "vertical";
1229
+ };
935
1230
  const computeMainDimEnds = () => {
936
1231
  const s = measureRef.current;
937
1232
  const a = s.picks[0];
938
1233
  const b = s.picks[1];
939
- const mode = measureModeRef.current;
1234
+ const mode = resolveMode(a, b);
1235
+ const rawMode = measureModeRef.current;
940
1236
  const fixed = measureFixedDistRef.current;
941
- if (fixed !== null && Number.isFinite(fixed) && mode === "horizontal") {
1237
+ if (fixed !== null && Number.isFinite(fixed) && rawMode === "horizontal") {
942
1238
  return { from: { x: b.x, y: a.y }, to: { x: b.x, y: b.y } };
943
1239
  }
944
- if (fixed !== null && Number.isFinite(fixed) && mode === "vertical") {
1240
+ if (fixed !== null && Number.isFinite(fixed) && rawMode === "vertical") {
945
1241
  return { from: { x: a.x, y: b.y }, to: { x: b.x, y: b.y } };
946
1242
  }
947
1243
  if (mode === "horizontal") return { from: a, to: { x: b.x, y: a.y } };
@@ -952,36 +1248,49 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
952
1248
  const s = measureRef.current;
953
1249
  if (!s) return;
954
1250
  reconcileLockedPick();
955
- const mode = measureModeRef.current;
1251
+ const rawMode = measureModeRef.current;
956
1252
  const fixed = measureFixedDistRef.current;
957
- const fixedActive = fixed !== null && Number.isFinite(fixed) && (mode === "horizontal" || mode === "vertical");
1253
+ const fixedActive = fixed !== null && Number.isFinite(fixed) && (rawMode === "horizontal" || rawMode === "vertical");
958
1254
  if (s.markers[0]) positionMarker(s.markers[0], s.picks[0].x, s.picks[0].y);
959
1255
  if (s.markers[1]) positionMarker(s.markers[1], s.picks[1].x, s.picks[1].y);
960
- const refDir = mode === "horizontal" ? { dx: 1, dy: 0 } : mode === "vertical" ? { dx: 0, dy: 1 } : null;
961
- if (refDir && s.picks.length >= 1 && s.refLine) {
1256
+ const drawRefLine = (el, dir, visible) => {
1257
+ if (!el) return;
1258
+ if (!visible || s.picks.length < 1) {
1259
+ el.style.display = "none";
1260
+ return;
1261
+ }
962
1262
  const a = s.picks[0];
963
1263
  const w = canvas.clientWidth, h = canvas.clientHeight;
964
1264
  const screenSpan = Math.hypot(w, h) * 4;
965
1265
  const ap = pxFromScene(a.x, a.y);
966
- const probe = pxFromScene(a.x + refDir.dx, a.y + refDir.dy);
1266
+ const probe = pxFromScene(a.x + dir.dx, a.y + dir.dy);
967
1267
  const pxLen = Math.hypot(probe.x - ap.x, probe.y - ap.y) || 1;
968
1268
  const sceneStep = screenSpan / pxLen;
969
- const x0 = a.x - refDir.dx * sceneStep;
970
- const y0 = a.y - refDir.dy * sceneStep;
971
- const x1 = a.x + refDir.dx * sceneStep;
972
- const y1 = a.y + refDir.dy * sceneStep;
973
- const p0 = pxFromScene(x0, y0);
974
- const p1 = pxFromScene(x1, y1);
975
- s.refLine.setAttribute("x1", String(p0.x));
976
- s.refLine.setAttribute("y1", String(p0.y));
977
- s.refLine.setAttribute("x2", String(p1.x));
978
- s.refLine.setAttribute("y2", String(p1.y));
979
- s.refLine.style.display = "";
980
- } else if (s.refLine) {
981
- s.refLine.style.display = "none";
1269
+ const p0 = pxFromScene(a.x - dir.dx * sceneStep, a.y - dir.dy * sceneStep);
1270
+ const p1 = pxFromScene(a.x + dir.dx * sceneStep, a.y + dir.dy * sceneStep);
1271
+ el.setAttribute("x1", String(p0.x));
1272
+ el.setAttribute("y1", String(p0.y));
1273
+ el.setAttribute("x2", String(p1.x));
1274
+ el.setAttribute("y2", String(p1.y));
1275
+ el.style.display = "";
1276
+ };
1277
+ let showH = rawMode === "horizontal";
1278
+ let showV = rawMode === "vertical";
1279
+ if (rawMode === "auto") {
1280
+ if (s.picks.length >= 2) {
1281
+ const r = resolveMode(s.picks[0], s.picks[1]);
1282
+ showH = r === "horizontal";
1283
+ showV = r === "vertical";
1284
+ } else {
1285
+ showH = true;
1286
+ showV = true;
1287
+ }
982
1288
  }
1289
+ drawRefLine(s.refLine, { dx: 1, dy: 0 }, showH);
1290
+ drawRefLine(s.refLineV, { dx: 0, dy: 1 }, showV);
983
1291
  if (s.picks.length === 2) {
984
1292
  const a = s.picks[0], b = s.picks[1];
1293
+ const mode = resolveMode(a, b);
985
1294
  const ends = computeMainDimEnds();
986
1295
  const fp = pxFromScene(ends.from.x, ends.from.y);
987
1296
  const tp = pxFromScene(ends.to.x, ends.to.y);
@@ -1118,28 +1427,46 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1118
1427
  if (!s || s.picks.length !== 2) return;
1119
1428
  reconcileLockedPick();
1120
1429
  const a = s.picks[0], b = s.picks[1];
1121
- const mode = measureModeRef.current;
1430
+ const rawMode = measureModeRef.current;
1431
+ const mode = resolveMode(a, b);
1122
1432
  const fixed = measureFixedDistRef.current;
1123
- const fixedActive = fixed !== null && Number.isFinite(fixed) && (mode === "horizontal" || mode === "vertical");
1433
+ const fixedActive = fixed !== null && Number.isFinite(fixed) && (rawMode === "horizontal" || rawMode === "vertical");
1124
1434
  let dist;
1125
1435
  let suffix = "";
1126
- if (fixedActive && mode === "horizontal") {
1127
- dist = Math.abs(b.y - a.y);
1436
+ let echo;
1437
+ let shown = mode;
1438
+ const adx = Math.abs(b.x - a.x), ady = Math.abs(b.y - a.y);
1439
+ if (fixedActive && rawMode === "horizontal") {
1440
+ dist = ady;
1128
1441
  suffix = " \u2195";
1129
- } else if (fixedActive && mode === "vertical") {
1130
- dist = Math.abs(b.x - a.x);
1442
+ shown = "vertical";
1443
+ echo = `\u0394Y = ${formatMeasureDistance(ady)} with \u0394X locked to ${formatMeasureDistance(Math.abs(fixed))}`;
1444
+ } else if (fixedActive && rawMode === "vertical") {
1445
+ dist = adx;
1131
1446
  suffix = " \u2194";
1447
+ shown = "horizontal";
1448
+ echo = `\u0394X = ${formatMeasureDistance(adx)} with \u0394Y locked to ${formatMeasureDistance(Math.abs(fixed))}`;
1132
1449
  } else if (mode === "horizontal") {
1133
- dist = Math.abs(b.x - a.x);
1450
+ dist = adx;
1134
1451
  suffix = " \u2194";
1452
+ echo = `Linear dimension = ${formatMeasureDistance(adx)} (\u0394X)`;
1135
1453
  } else if (mode === "vertical") {
1136
- dist = Math.abs(b.y - a.y);
1454
+ dist = ady;
1137
1455
  suffix = " \u2195";
1456
+ echo = `Linear dimension = ${formatMeasureDistance(ady)} (\u0394Y)`;
1138
1457
  } else {
1139
- const dx = b.x - a.x, dy = b.y - a.y;
1140
- dist = Math.sqrt(dx * dx + dy * dy);
1458
+ dist = Math.hypot(b.x - a.x, b.y - a.y);
1459
+ echo = `Distance = ${formatMeasureDistance(dist)} \u0394X = ${formatMeasureDistance(adx)} \u0394Y = ${formatMeasureDistance(ady)}`;
1460
+ }
1461
+ if (!Number.isFinite(dist)) {
1462
+ setMeasureDistance(null);
1463
+ setMeasureResolved(null);
1464
+ if (s.label) s.label.style.opacity = "0";
1465
+ return;
1141
1466
  }
1142
1467
  setMeasureDistance(dist);
1468
+ setMeasureResolved(shown);
1469
+ setCmdEcho(echo);
1143
1470
  const label = ensureLabel();
1144
1471
  label.style.opacity = "1";
1145
1472
  label.textContent = `${formatMeasureDistance(dist)}${suffix}`;
@@ -1148,23 +1475,42 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1148
1475
  recomputeLabel();
1149
1476
  updateOverlay();
1150
1477
  };
1478
+ const hideDimVisuals = (s) => {
1479
+ s.line.style.display = "none";
1480
+ if (s.fixedLine) s.fixedLine.style.display = "none";
1481
+ if (s.fixedLabel) s.fixedLabel.style.display = "none";
1482
+ if (s.extLineA) s.extLineA.style.display = "none";
1483
+ if (s.extLineB) s.extLineB.style.display = "none";
1484
+ if (s.label) s.label.style.opacity = "0";
1485
+ setMeasureDistance(null);
1486
+ setMeasureResolved(null);
1487
+ };
1488
+ const resetPicks = () => {
1489
+ const s = measureRef.current;
1490
+ if (!s) return;
1491
+ for (const m of s.markers) m.parentElement?.removeChild(m);
1492
+ s.markers = [];
1493
+ s.picks = [];
1494
+ s.rawSecondClick = null;
1495
+ if (s.refLine) s.refLine.style.display = "none";
1496
+ if (s.refLineV) s.refLineV.style.display = "none";
1497
+ hideDimVisuals(s);
1498
+ };
1499
+ measureResetRef.current = resetPicks;
1500
+ measureUndoRef.current = () => {
1501
+ const s = measureRef.current;
1502
+ if (!s || s.picks.length === 0) return;
1503
+ s.picks.pop();
1504
+ const m = s.markers.pop();
1505
+ m?.parentElement?.removeChild(m);
1506
+ s.rawSecondClick = null;
1507
+ hideDimVisuals(s);
1508
+ updateOverlay();
1509
+ };
1151
1510
  const doPick = (p) => {
1152
1511
  const s = measureRef.current;
1153
1512
  if (!s) return;
1154
- if (s.picks.length === 2) {
1155
- for (const m of s.markers) m.parentElement?.removeChild(m);
1156
- s.markers = [];
1157
- s.picks = [];
1158
- s.rawSecondClick = null;
1159
- s.line.style.display = "none";
1160
- if (s.fixedLine) s.fixedLine.style.display = "none";
1161
- if (s.fixedLabel) s.fixedLabel.style.display = "none";
1162
- if (s.refLine) s.refLine.style.display = "none";
1163
- if (s.extLineA) s.extLineA.style.display = "none";
1164
- if (s.extLineB) s.extLineB.style.display = "none";
1165
- if (s.label) s.label.style.opacity = "0";
1166
- setMeasureDistance(null);
1167
- }
1513
+ if (s.picks.length === 2) resetPicks();
1168
1514
  if (s.picks.length === 0) {
1169
1515
  s.picks.push({ x: p.x, y: p.y });
1170
1516
  s.markers.push(makeMarker());
@@ -1211,10 +1557,13 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1211
1557
  canvas.removeEventListener("pointermove", handlePointerMove);
1212
1558
  window.removeEventListener("keydown", onKeyDown);
1213
1559
  measureRedrawRef.current = null;
1560
+ measureResetRef.current = null;
1561
+ measureUndoRef.current = null;
1214
1562
  teardown();
1215
1563
  setMeasureDistance(null);
1564
+ setMeasureResolved(null);
1216
1565
  };
1217
- }, [measureEnabled, loading, error]);
1566
+ }, [measureEnabled, loading, error, layersKey]);
1218
1567
  useEffect(() => {
1219
1568
  measureRedrawRef.current?.();
1220
1569
  }, [measureMode, measureFixedDist]);
@@ -1264,12 +1613,170 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1264
1613
  } catch {
1265
1614
  }
1266
1615
  };
1616
+ const runCommand = (raw) => {
1617
+ const exec = raw.trim() || lastCmdRef.current;
1618
+ setCmdValue("");
1619
+ if (!exec) return;
1620
+ const cmd = exec.toLowerCase().split(/\s+/)[0];
1621
+ const remember = () => {
1622
+ lastCmdRef.current = exec;
1623
+ };
1624
+ const startMeasure = (mode, echo) => {
1625
+ setMeasureEnabled(true);
1626
+ setMeasureMode(mode);
1627
+ measureResetRef.current?.();
1628
+ setCmdEcho(echo);
1629
+ remember();
1630
+ };
1631
+ if (/^-?(\d+\.?\d*|\.\d+)$/.test(cmd)) {
1632
+ if (measureEnabled && (measureMode === "horizontal" || measureMode === "vertical")) {
1633
+ const n = parseFloat(cmd);
1634
+ if (n) {
1635
+ setMeasureFixedInput(cmd);
1636
+ setMeasureFixedDist(n);
1637
+ setCmdEcho(`\u0394${measureMode === "horizontal" ? "X" : "Y"} locked to ${formatMeasureDistance(Math.abs(n))} \u2014 the label shows the perpendicular distance`);
1638
+ } else {
1639
+ setMeasureFixedInput("");
1640
+ setMeasureFixedDist(null);
1641
+ setCmdEcho("Fixed distance cleared.");
1642
+ }
1643
+ } else {
1644
+ setCmdEcho("Fixed distances apply in H or V mode \u2014 type H or V first.");
1645
+ }
1646
+ return;
1647
+ }
1648
+ switch (cmd) {
1649
+ case "di":
1650
+ case "dist":
1651
+ case "mea":
1652
+ case "measuregeom":
1653
+ startMeasure("point", "DIST \u2014 click two points; straight-line distance (Esc to exit)");
1654
+ break;
1655
+ case "dim":
1656
+ case "dli":
1657
+ case "dimlin":
1658
+ case "dimlinear":
1659
+ startMeasure("auto", "DIMLINEAR \u2014 click two points; measures \u0394X or \u0394Y, whichever is larger (H / V to force)");
1660
+ break;
1661
+ // H / V / AUTO switch the axis without dropping existing picks —
1662
+ // same as clicking the pill.
1663
+ case "h":
1664
+ case "hor":
1665
+ case "horizontal":
1666
+ setMeasureEnabled(true);
1667
+ setMeasureMode("horizontal");
1668
+ setCmdEcho("Horizontal \u2014 \u0394X between the two picks");
1669
+ remember();
1670
+ break;
1671
+ case "v":
1672
+ case "ver":
1673
+ case "vertical":
1674
+ setMeasureEnabled(true);
1675
+ setMeasureMode("vertical");
1676
+ setCmdEcho("Vertical \u2014 \u0394Y between the two picks");
1677
+ remember();
1678
+ break;
1679
+ case "au":
1680
+ case "auto":
1681
+ setMeasureEnabled(true);
1682
+ setMeasureMode("auto");
1683
+ setCmdEcho("Auto (DIMLINEAR) \u2014 measures along the dominant axis of the two picks");
1684
+ remember();
1685
+ break;
1686
+ case "measure":
1687
+ setMeasureEnabled(!measureEnabled);
1688
+ setCmdEcho(measureEnabled ? "Measure off." : "Measure on \u2014 click two points.");
1689
+ remember();
1690
+ break;
1691
+ case "u":
1692
+ case "undo":
1693
+ measureUndoRef.current?.();
1694
+ setCmdEcho("Last pick removed.");
1695
+ remember();
1696
+ break;
1697
+ case "z":
1698
+ case "ze":
1699
+ case "zoom":
1700
+ case "fit":
1701
+ case "zoomextents":
1702
+ handleResetView();
1703
+ setCmdEcho("Zoom extents.");
1704
+ remember();
1705
+ break;
1706
+ case "la":
1707
+ case "layer":
1708
+ case "layers":
1709
+ setShowLayers((s) => !s);
1710
+ setCmdEcho("Layer panel toggled.");
1711
+ remember();
1712
+ break;
1713
+ case "off":
1714
+ case "clear":
1715
+ setMeasureFixedDist(null);
1716
+ setMeasureFixedInput("");
1717
+ measureResetRef.current?.();
1718
+ setCmdEcho("Measurement cleared.");
1719
+ remember();
1720
+ break;
1721
+ case "?":
1722
+ case "help":
1723
+ setCmdEcho(DXF_CMD_HELP);
1724
+ break;
1725
+ default: {
1726
+ const editCmd = DXF_EDIT_COMMANDS[cmd];
1727
+ if (editCmd) setCmdEcho(`${editCmd} is a drawing command \u2014 Preview is a read-only viewer. Measuring: DI, DIM, H, V (? for help)`);
1728
+ else setCmdEcho(`Unknown command "${cmd.toUpperCase()}" \u2014 try DI, DIM, H, V, Z, LA (? for help)`);
1729
+ }
1730
+ }
1731
+ };
1732
+ useEffect(() => {
1733
+ const onKey = (e) => {
1734
+ if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return;
1735
+ if (e.key.length !== 1 || e.key === " ") return;
1736
+ const t = e.target;
1737
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
1738
+ const root = rootRef.current;
1739
+ if (!root) return;
1740
+ const myModal = root.closest("[data-modal-id]");
1741
+ if (myModal && getActiveModalId() !== myModal.dataset.modalId) return;
1742
+ cmdInputRef.current?.focus();
1743
+ };
1744
+ window.addEventListener("keydown", onKey);
1745
+ return () => window.removeEventListener("keydown", onKey);
1746
+ }, []);
1747
+ const cmdValueRef = useRef(cmdValue);
1748
+ const measureEnabledRef = useRef(measureEnabled);
1749
+ useEffect(() => {
1750
+ cmdValueRef.current = cmdValue;
1751
+ }, [cmdValue]);
1752
+ useEffect(() => {
1753
+ measureEnabledRef.current = measureEnabled;
1754
+ }, [measureEnabled]);
1755
+ useEffect(() => {
1756
+ return registerModalEscapeInterceptor(() => {
1757
+ const root = rootRef.current;
1758
+ if (!root) return false;
1759
+ const myModal = root.closest("[data-modal-id]");
1760
+ if (!myModal || getActiveModalId() !== myModal.dataset.modalId) return false;
1761
+ if (cmdValueRef.current) {
1762
+ setCmdValue("");
1763
+ setCmdEcho("*Cancel*");
1764
+ return true;
1765
+ }
1766
+ if (measureEnabledRef.current) {
1767
+ setMeasureEnabled(false);
1768
+ setCmdEcho("Measure off.");
1769
+ return true;
1770
+ }
1771
+ return false;
1772
+ });
1773
+ }, []);
1267
1774
  const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
1268
1775
  const colorHex = (n) => {
1269
1776
  if (typeof n !== "number") return "#999";
1270
1777
  return "#" + n.toString(16).padStart(6, "0");
1271
1778
  };
1272
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
1779
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: "flex flex-col h-full", children: [
1273
1780
  /* @__PURE__ */ jsxs(PanelActions, { children: [
1274
1781
  /* @__PURE__ */ jsxs(
1275
1782
  "button",
@@ -1297,7 +1804,7 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1297
1804
  {
1298
1805
  onClick: () => setMeasureEnabled((m) => !m),
1299
1806
  className: btn + (measureEnabled ? " bg-gray-200" : ""),
1300
- title: measureEnabled ? "Stop measuring (Esc)" : "Measure distance \u2014 click two points on the drawing",
1807
+ title: measureEnabled ? "Stop measuring (Esc)" : "Measure distance \u2014 click two points on the drawing, or type DI / DIM below",
1301
1808
  children: [
1302
1809
  /* @__PURE__ */ jsxs("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
1303
1810
  /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 14.25l6-6 6 6 4.5-4.5M9.75 8.25v3M12.75 11.25v3M15.75 14.25v3" }),
@@ -1312,10 +1819,10 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1312
1819
  /* @__PURE__ */ jsx(
1313
1820
  "button",
1314
1821
  {
1315
- onClick: () => setMeasureMode("point"),
1316
- className: `px-2 transition-colors ${measureMode === "point" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
1317
- title: "Point \u2014 straight-line (Euclidean) distance between two picks",
1318
- children: "Point"
1822
+ onClick: () => setMeasureMode("auto"),
1823
+ className: `px-2 transition-colors ${measureMode === "auto" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
1824
+ title: "Auto \u2014 AutoCAD DIMLINEAR: measures \u0394X or \u0394Y, whichever is larger between the two picks",
1825
+ children: "Auto"
1319
1826
  }
1320
1827
  ),
1321
1828
  /* @__PURE__ */ jsx(
@@ -1335,6 +1842,15 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1335
1842
  title: "Vertical \u2014 distance along the Y axis between two picks (AutoCAD DIMLINEAR vertical)",
1336
1843
  children: "V"
1337
1844
  }
1845
+ ),
1846
+ /* @__PURE__ */ jsx(
1847
+ "button",
1848
+ {
1849
+ onClick: () => setMeasureMode("point"),
1850
+ className: `px-2 transition-colors ${measureMode === "point" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
1851
+ title: "Point \u2014 straight-line (Euclidean) distance between two picks (AutoCAD DIST)",
1852
+ children: "Point"
1853
+ }
1338
1854
  )
1339
1855
  ] }),
1340
1856
  (measureMode === "horizontal" || measureMode === "vertical") && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -1377,10 +1893,10 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1377
1893
  "div",
1378
1894
  {
1379
1895
  className: "px-2 py-1 text-[11px] font-mono font-semibold text-orange-600 bg-orange-50 border border-orange-200 rounded whitespace-nowrap",
1380
- title: measureMode === "horizontal" ? "Horizontal distance (\u0394x) between the two picked points" : measureMode === "vertical" ? "Vertical distance (\u0394y) between the two picked points" : "Straight-line distance between the two picked points",
1896
+ title: measureResolved === "horizontal" ? `Horizontal distance (\u0394x) between the two picked points${measureMode === "auto" ? " \u2014 Auto resolved to the X axis" : ""}` : measureResolved === "vertical" ? `Vertical distance (\u0394y) between the two picked points${measureMode === "auto" ? " \u2014 Auto resolved to the Y axis" : ""}` : "Straight-line distance between the two picked points",
1381
1897
  children: [
1382
1898
  formatMeasureDistance(measureDistance),
1383
- measureMode === "horizontal" ? " \u2194" : measureMode === "vertical" ? " \u2195" : ""
1899
+ measureResolved === "horizontal" ? " \u2194" : measureResolved === "vertical" ? " \u2195" : ""
1384
1900
  ]
1385
1901
  }
1386
1902
  ),
@@ -1436,10 +1952,50 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
1436
1952
  "Scroll to zoom"
1437
1953
  ] }),
1438
1954
  /* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
1439
- /* @__PURE__ */ jsx("span", { children: "Fit to reset" })
1955
+ /* @__PURE__ */ jsx("span", { children: "Fit to reset" }),
1956
+ /* @__PURE__ */ jsx("span", { className: "text-white/40", children: "\u2022" }),
1957
+ /* @__PURE__ */ jsxs("span", { children: [
1958
+ "Type ",
1959
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold", children: "DI" }),
1960
+ " / ",
1961
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold", children: "DIM" }),
1962
+ " to measure"
1963
+ ] })
1440
1964
  ] }),
1441
1965
  loading && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-white/80 text-sm text-gray-500", children: "Loading drawing\u2026" }),
1442
1966
  error && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center text-sm text-red-600 px-6 text-center", children: error })
1967
+ ] }),
1968
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-t border-gray-200 bg-gray-50", children: [
1969
+ cmdEcho && /* @__PURE__ */ jsx("div", { className: "px-2.5 pt-1 text-[11px] font-mono text-gray-500 truncate", title: cmdEcho, children: cmdEcho }),
1970
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-2.5 h-7", children: [
1971
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-mono font-semibold text-gray-400 select-none", children: ">" }),
1972
+ /* @__PURE__ */ jsx(
1973
+ "input",
1974
+ {
1975
+ ref: cmdInputRef,
1976
+ value: cmdValue,
1977
+ onChange: (e) => setCmdValue(e.target.value),
1978
+ onKeyDown: (e) => {
1979
+ if (e.key === "Enter" || e.key === " ") {
1980
+ e.preventDefault();
1981
+ runCommand(cmdValue);
1982
+ } else if (e.key === "Escape") {
1983
+ if (cmdValue) {
1984
+ e.stopPropagation();
1985
+ setCmdValue("");
1986
+ setCmdEcho("*Cancel*");
1987
+ } else {
1988
+ e.target.blur();
1989
+ }
1990
+ }
1991
+ },
1992
+ placeholder: "Type a command \u2014 DI, DIM, H, V, Z, LA (? for help)",
1993
+ spellCheck: false,
1994
+ autoComplete: "off",
1995
+ className: "flex-1 min-w-0 bg-transparent text-[11px] font-mono text-gray-700 focus:outline-none placeholder:text-gray-300"
1996
+ }
1997
+ )
1998
+ ] })
1443
1999
  ] })
1444
2000
  ] });
1445
2001
  }
@@ -1463,8 +2019,7 @@ function hexToRgb(OV, hex) {
1463
2019
  return new OV.RGBColor(n >> 16 & 255, n >> 8 & 255, n & 255);
1464
2020
  }
1465
2021
  function formatMeasureDistance(mm) {
1466
- if (mm >= 1e3) return `${(mm / 1e3).toFixed(2)} m`;
1467
- if (mm >= 10) return `${mm.toFixed(1)} mm`;
2022
+ if (mm >= 1e3) return `${(mm / 1e3).toFixed(3)} m`;
1468
2023
  return `${mm.toFixed(2)} mm`;
1469
2024
  }
1470
2025
  function StepPanel({ url, filename, onDownload, onEmail }) {
@@ -2744,5 +3299,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2744
3299
  }
2745
3300
 
2746
3301
  export { Preview, setPdfPreview };
2747
- //# sourceMappingURL=chunk-4LQVJQ3S.js.map
2748
- //# sourceMappingURL=chunk-4LQVJQ3S.js.map
3302
+ //# sourceMappingURL=chunk-UFTJG6IM.js.map
3303
+ //# sourceMappingURL=chunk-UFTJG6IM.js.map