react-os-shell 0.2.46 → 0.2.47

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.
@@ -410,6 +410,10 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
410
410
  const [layers, setLayers] = useState([]);
411
411
  const [showLayers, setShowLayers] = useState(false);
412
412
  const [showHint, setShowHint] = useState(true);
413
+ const [measureEnabled, setMeasureEnabled] = useState(false);
414
+ const [measureMode, setMeasureMode] = useState("point");
415
+ const [measureDistance, setMeasureDistance] = useState(null);
416
+ const measureRef = useRef(null);
413
417
  useEffect(() => {
414
418
  let cancelled = false;
415
419
  let viewer = null;
@@ -512,6 +516,348 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
512
516
  const t = setTimeout(() => setShowHint(false), 5e3);
513
517
  return () => clearTimeout(t);
514
518
  }, [showHint, loading]);
519
+ useEffect(() => {
520
+ const v = viewerRef.current;
521
+ if (loading || error || !v) return;
522
+ const scene = v.GetScene?.();
523
+ const camera = v.GetCamera?.();
524
+ const canvas = v.GetCanvas?.();
525
+ if (!scene || !camera || !canvas || !containerRef.current) return;
526
+ const teardown = () => {
527
+ const s = measureRef.current;
528
+ if (!s) return;
529
+ if (s.overlay && s.overlay.parentElement) s.overlay.parentElement.removeChild(s.overlay);
530
+ measureRef.current = null;
531
+ };
532
+ if (!measureEnabled) {
533
+ teardown();
534
+ setMeasureDistance(null);
535
+ return;
536
+ }
537
+ const overlay = document.createElement("div");
538
+ overlay.style.cssText = "position:absolute;inset:0;pointer-events:none;z-index:5;";
539
+ containerRef.current.appendChild(overlay);
540
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
541
+ svg.setAttribute("style", "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;");
542
+ overlay.appendChild(svg);
543
+ const refLineEl = document.createElementNS("http://www.w3.org/2000/svg", "line");
544
+ refLineEl.setAttribute("stroke", "#ff8800");
545
+ refLineEl.setAttribute("stroke-width", "1");
546
+ refLineEl.setAttribute("stroke-dasharray", "6,4");
547
+ refLineEl.setAttribute("opacity", "0.55");
548
+ refLineEl.style.display = "none";
549
+ svg.appendChild(refLineEl);
550
+ const lineEl = document.createElementNS("http://www.w3.org/2000/svg", "line");
551
+ lineEl.setAttribute("stroke", "#ff8800");
552
+ lineEl.setAttribute("stroke-width", "1.5");
553
+ lineEl.setAttribute("stroke-linecap", "round");
554
+ lineEl.style.display = "none";
555
+ svg.appendChild(lineEl);
556
+ const snapEl = document.createElement("div");
557
+ 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;`;
558
+ overlay.appendChild(snapEl);
559
+ measureRef.current = {
560
+ picks: [],
561
+ lineDir: null,
562
+ overlay,
563
+ svg,
564
+ line: lineEl,
565
+ refLine: refLineEl,
566
+ markers: [],
567
+ label: null,
568
+ snap: snapEl,
569
+ segments: []
570
+ };
571
+ let THREE = null;
572
+ let ready = false;
573
+ const pxFromScene = (sx, sy) => {
574
+ if (!THREE) return { x: 0, y: 0 };
575
+ const v3 = new THREE.Vector3(sx, sy, 0).project(camera);
576
+ const w = canvas.clientWidth, h = canvas.clientHeight;
577
+ return { x: (v3.x + 1) / 2 * w, y: (-v3.y + 1) / 2 * h };
578
+ };
579
+ (async () => {
580
+ try {
581
+ THREE = await import(
582
+ /* @vite-ignore */
583
+ 'three'
584
+ );
585
+ const segs = measureRef.current?.segments;
586
+ if (!segs) return;
587
+ const va = new THREE.Vector3(), vb = new THREE.Vector3();
588
+ scene.traverse((obj) => {
589
+ if (!obj?.isLineSegments) return;
590
+ const pos = obj.geometry?.attributes?.position;
591
+ if (!pos) return;
592
+ const arr = pos.array;
593
+ obj.updateMatrixWorld?.();
594
+ const m = obj.matrixWorld;
595
+ for (let i = 0; i < arr.length; i += 6) {
596
+ va.set(arr[i], arr[i + 1], arr[i + 2]).applyMatrix4(m);
597
+ vb.set(arr[i + 3], arr[i + 4], arr[i + 5]).applyMatrix4(m);
598
+ segs.push({ ax: va.x, ay: va.y, bx: vb.x, by: vb.y });
599
+ }
600
+ });
601
+ ready = true;
602
+ } catch {
603
+ }
604
+ })();
605
+ const SNAP_PX = 12;
606
+ const findSnap = (cx, cy) => {
607
+ const s = measureRef.current;
608
+ if (!s || !ready) return null;
609
+ let best = null;
610
+ let bestD2 = SNAP_PX * SNAP_PX;
611
+ for (const seg of s.segments) {
612
+ const ap = pxFromScene(seg.ax, seg.ay);
613
+ const bp = pxFromScene(seg.bx, seg.by);
614
+ const ldx = seg.bx - seg.ax, ldy = seg.by - seg.ay;
615
+ const llen = Math.sqrt(ldx * ldx + ldy * ldy) || 1;
616
+ const segDir = { dx: ldx / llen, dy: ldy / llen };
617
+ let dx = cx - ap.x, dy = cy - ap.y;
618
+ let d2 = dx * dx + dy * dy;
619
+ if (d2 < bestD2) {
620
+ best = { sx: seg.ax, sy: seg.ay, type: "endpoint", dir: segDir };
621
+ bestD2 = d2;
622
+ }
623
+ dx = cx - bp.x;
624
+ dy = cy - bp.y;
625
+ d2 = dx * dx + dy * dy;
626
+ if (d2 < bestD2) {
627
+ best = { sx: seg.bx, sy: seg.by, type: "endpoint", dir: segDir };
628
+ bestD2 = d2;
629
+ }
630
+ const sdx = bp.x - ap.x, sdy = bp.y - ap.y;
631
+ const len2 = sdx * sdx + sdy * sdy;
632
+ if (len2 > 0) {
633
+ const t = ((cx - ap.x) * sdx + (cy - ap.y) * sdy) / len2;
634
+ if (t > 0 && t < 1) {
635
+ const px = ap.x + t * sdx, py = ap.y + t * sdy;
636
+ dx = cx - px;
637
+ dy = cy - py;
638
+ d2 = dx * dx + dy * dy;
639
+ if (d2 < bestD2) {
640
+ const sx = seg.ax + t * (seg.bx - seg.ax);
641
+ const sy = seg.ay + t * (seg.by - seg.ay);
642
+ best = { sx, sy, type: "line", dir: segDir };
643
+ bestD2 = d2;
644
+ }
645
+ }
646
+ }
647
+ }
648
+ return best;
649
+ };
650
+ const makeMarker = () => {
651
+ const el = document.createElement("div");
652
+ el.style.cssText = `position:absolute;width:10px;height:10px;border-radius:50%;background:#ff8800;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,0.25);transform:translate(-50%,-50%);pointer-events:none;`;
653
+ overlay.appendChild(el);
654
+ return el;
655
+ };
656
+ const ensureLabel = () => {
657
+ const s = measureRef.current;
658
+ if (s.label) return s.label;
659
+ const el = document.createElement("div");
660
+ el.style.cssText = `position:absolute;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.95);color:#fff;border-radius:4px;white-space:nowrap;box-shadow:0 1px 4px rgba(0,0,0,0.25);pointer-events:none;`;
661
+ overlay.appendChild(el);
662
+ s.label = el;
663
+ return el;
664
+ };
665
+ const positionMarker = (el, x, y) => {
666
+ const p = pxFromScene(x, y);
667
+ el.style.left = `${p.x}px`;
668
+ el.style.top = `${p.y}px`;
669
+ };
670
+ const computeRenderedEnds = () => {
671
+ const s = measureRef.current;
672
+ const a = s.picks[0];
673
+ let b = s.picks[1];
674
+ if (measureMode === "perp" && s.lineDir) {
675
+ const d = s.lineDir;
676
+ const ax = a.x, ay = a.y;
677
+ const t = (b.x - ax) * d.dx + (b.y - ay) * d.dy;
678
+ const fx = ax + d.dx * t;
679
+ const fy = ay + d.dy * t;
680
+ return { from: { x: fx, y: fy }, to: { x: b.x, y: b.y } };
681
+ }
682
+ return { from: a, to: b };
683
+ };
684
+ const updateOverlay = () => {
685
+ const s = measureRef.current;
686
+ if (!s) return;
687
+ if (s.markers[0]) positionMarker(s.markers[0], s.picks[0].x, s.picks[0].y);
688
+ if (s.markers[1]) positionMarker(s.markers[1], s.picks[1].x, s.picks[1].y);
689
+ if (measureMode === "perp" && s.picks.length >= 1 && s.lineDir && s.refLine) {
690
+ const a = s.picks[0];
691
+ const w = canvas.clientWidth, h = canvas.clientHeight;
692
+ const screenSpan = Math.hypot(w, h) * 4;
693
+ const ap = pxFromScene(a.x, a.y);
694
+ const probe = pxFromScene(a.x + s.lineDir.dx, a.y + s.lineDir.dy);
695
+ const pxLen = Math.hypot(probe.x - ap.x, probe.y - ap.y) || 1;
696
+ const sceneStep = screenSpan / pxLen;
697
+ const x0 = a.x - s.lineDir.dx * sceneStep;
698
+ const y0 = a.y - s.lineDir.dy * sceneStep;
699
+ const x1 = a.x + s.lineDir.dx * sceneStep;
700
+ const y1 = a.y + s.lineDir.dy * sceneStep;
701
+ const p0 = pxFromScene(x0, y0);
702
+ const p1 = pxFromScene(x1, y1);
703
+ s.refLine.setAttribute("x1", String(p0.x));
704
+ s.refLine.setAttribute("y1", String(p0.y));
705
+ s.refLine.setAttribute("x2", String(p1.x));
706
+ s.refLine.setAttribute("y2", String(p1.y));
707
+ s.refLine.style.display = "";
708
+ } else if (s.refLine) {
709
+ s.refLine.style.display = "none";
710
+ }
711
+ if (s.picks.length === 2) {
712
+ const ends = computeRenderedEnds();
713
+ const fp = pxFromScene(ends.from.x, ends.from.y);
714
+ const tp = pxFromScene(ends.to.x, ends.to.y);
715
+ s.line.setAttribute("x1", String(fp.x));
716
+ s.line.setAttribute("y1", String(fp.y));
717
+ s.line.setAttribute("x2", String(tp.x));
718
+ s.line.setAttribute("y2", String(tp.y));
719
+ s.line.style.display = "";
720
+ if (s.label) {
721
+ s.label.style.left = `${(fp.x + tp.x) / 2}px`;
722
+ s.label.style.top = `${(fp.y + tp.y) / 2}px`;
723
+ }
724
+ } else {
725
+ s.line.style.display = "none";
726
+ }
727
+ };
728
+ const DRAG_TOL = 4;
729
+ const DRAG_TIME = 350;
730
+ let downX = 0, downY = 0, downTime = 0, downActive = false, dragging = false;
731
+ let lastSnap = null;
732
+ const handlePointerDown = (ev) => {
733
+ const d = ev?.detail;
734
+ if (!d || d.domEvent?.button !== 0) return;
735
+ downX = d.canvasCoord.x;
736
+ downY = d.canvasCoord.y;
737
+ downTime = performance.now();
738
+ dragging = false;
739
+ downActive = true;
740
+ };
741
+ const handlePointerMove = (ev) => {
742
+ const rect = canvas.getBoundingClientRect();
743
+ const cx = ev.clientX - rect.left;
744
+ const cy = ev.clientY - rect.top;
745
+ if (downActive) {
746
+ if ((cx - downX) ** 2 + (cy - downY) ** 2 > DRAG_TOL * DRAG_TOL) dragging = true;
747
+ }
748
+ const s = measureRef.current;
749
+ if (!s || !s.snap) return;
750
+ if (downActive && dragging) {
751
+ s.snap.style.display = "none";
752
+ return;
753
+ }
754
+ lastSnap = findSnap(cx, cy);
755
+ if (lastSnap) {
756
+ const p = pxFromScene(lastSnap.sx, lastSnap.sy);
757
+ s.snap.style.left = `${p.x}px`;
758
+ s.snap.style.top = `${p.y}px`;
759
+ s.snap.style.display = "";
760
+ } else {
761
+ s.snap.style.display = "none";
762
+ }
763
+ };
764
+ const handlePointerUp = (ev) => {
765
+ if (!downActive) return;
766
+ downActive = false;
767
+ const elapsed = performance.now() - downTime;
768
+ if (dragging || elapsed > DRAG_TIME) return;
769
+ const raw = ev?.detail?.position;
770
+ if (!raw) return;
771
+ const useSnap = lastSnap && Math.hypot(downX - canvas.getBoundingClientRect().width, 0) >= 0;
772
+ const picked = useSnap && lastSnap ? { x: lastSnap.sx, y: lastSnap.sy, snapType: lastSnap.type, lineDir: lastSnap.dir } : { x: raw.x, y: raw.y, lineDir: void 0 };
773
+ doPick(picked);
774
+ };
775
+ const doPick = (p) => {
776
+ const s = measureRef.current;
777
+ if (!s) return;
778
+ if (s.picks.length === 2) {
779
+ for (const m of s.markers) m.parentElement?.removeChild(m);
780
+ s.markers = [];
781
+ s.picks = [];
782
+ s.lineDir = null;
783
+ s.line.style.display = "none";
784
+ if (s.refLine) s.refLine.style.display = "none";
785
+ if (s.label) s.label.style.opacity = "0";
786
+ setMeasureDistance(null);
787
+ }
788
+ if (measureMode === "perp" && s.picks.length === 0) {
789
+ if (!p.lineDir) {
790
+ const label = ensureLabel();
791
+ label.style.opacity = "1";
792
+ label.style.left = `${pxFromScene(p.x, p.y).x}px`;
793
+ label.style.top = `${pxFromScene(p.x, p.y).y - 18}px`;
794
+ label.textContent = "\u22A5: snap to a line or corner first";
795
+ setTimeout(() => {
796
+ if (s.label && s.picks.length === 0) s.label.style.opacity = "0";
797
+ }, 1500);
798
+ return;
799
+ }
800
+ s.lineDir = p.lineDir;
801
+ }
802
+ s.picks.push({ x: p.x, y: p.y });
803
+ s.markers.push(makeMarker());
804
+ if (s.picks.length === 2) {
805
+ const a = s.picks[0], b = s.picks[1];
806
+ let dist;
807
+ let suffix = "";
808
+ if (measureMode === "perp" && s.lineDir) {
809
+ const dx = b.x - a.x, dy = b.y - a.y;
810
+ dist = Math.abs(dx * s.lineDir.dy - dy * s.lineDir.dx);
811
+ suffix = " \u22A5";
812
+ } else {
813
+ const dx = b.x - a.x, dy = b.y - a.y;
814
+ dist = Math.sqrt(dx * dx + dy * dy);
815
+ }
816
+ setMeasureDistance(dist);
817
+ const label = ensureLabel();
818
+ label.style.opacity = "1";
819
+ label.textContent = `${formatMeasureDistance(dist)}${suffix}`;
820
+ }
821
+ updateOverlay();
822
+ };
823
+ const onKeyDown = (ev) => {
824
+ if (ev.key === "Escape") setMeasureEnabled(false);
825
+ };
826
+ canvas.style.cursor = "crosshair";
827
+ try {
828
+ v.Subscribe?.("pointerdown", handlePointerDown);
829
+ } catch {
830
+ }
831
+ try {
832
+ v.Subscribe?.("pointerup", handlePointerUp);
833
+ } catch {
834
+ }
835
+ try {
836
+ v.Subscribe?.("viewChanged", updateOverlay);
837
+ } catch {
838
+ }
839
+ canvas.addEventListener("pointermove", handlePointerMove);
840
+ window.addEventListener("keydown", onKeyDown);
841
+ return () => {
842
+ canvas.style.cursor = "";
843
+ try {
844
+ v.Unsubscribe?.("pointerdown", handlePointerDown);
845
+ } catch {
846
+ }
847
+ try {
848
+ v.Unsubscribe?.("pointerup", handlePointerUp);
849
+ } catch {
850
+ }
851
+ try {
852
+ v.Unsubscribe?.("viewChanged", updateOverlay);
853
+ } catch {
854
+ }
855
+ canvas.removeEventListener("pointermove", handlePointerMove);
856
+ window.removeEventListener("keydown", onKeyDown);
857
+ teardown();
858
+ setMeasureDistance(null);
859
+ };
860
+ }, [measureEnabled, measureMode, loading, error]);
515
861
  const toggleLayer = (name) => {
516
862
  setLayers((prev) => prev.map((l) => {
517
863
  if (l.name !== name) return l;
@@ -586,6 +932,45 @@ function DxfPanel({ url, filename, onDownload, onEmail }) {
586
932
  }
587
933
  ),
588
934
  /* @__PURE__ */ jsx("button", { onClick: () => setShowHint((s) => !s), className: btn, title: "How to navigate", children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" }) }) }),
935
+ /* @__PURE__ */ jsxs(
936
+ "button",
937
+ {
938
+ onClick: () => setMeasureEnabled((m) => !m),
939
+ className: btn + (measureEnabled ? " bg-gray-200" : ""),
940
+ title: measureEnabled ? "Stop measuring (Esc)" : "Measure distance \u2014 click two points on the drawing",
941
+ children: [
942
+ /* @__PURE__ */ jsxs("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
943
+ /* @__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" }),
944
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.25 18.75h19.5" })
945
+ ] }),
946
+ "Measure"
947
+ ]
948
+ }
949
+ ),
950
+ measureEnabled && /* @__PURE__ */ jsxs("div", { className: "flex items-stretch h-7 rounded border border-gray-200 overflow-hidden text-[11px] font-semibold", children: [
951
+ /* @__PURE__ */ jsx(
952
+ "button",
953
+ {
954
+ onClick: () => setMeasureMode("point"),
955
+ className: `px-2 transition-colors ${measureMode === "point" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
956
+ title: "Point \u2014 straight-line distance between two picks",
957
+ children: "Point"
958
+ }
959
+ ),
960
+ /* @__PURE__ */ jsx(
961
+ "button",
962
+ {
963
+ onClick: () => setMeasureMode("perp"),
964
+ className: `px-2 transition-colors ${measureMode === "perp" ? "bg-orange-500 text-white" : "bg-white text-gray-600 hover:bg-gray-50"}`,
965
+ title: "Perpendicular \u2014 click on a line first, then pick a point. Reports the perpendicular distance.",
966
+ children: "\u22A5"
967
+ }
968
+ )
969
+ ] }),
970
+ measureEnabled && measureDistance !== null && /* @__PURE__ */ jsxs("div", { className: "px-2 py-1 text-[11px] font-mono font-semibold text-orange-600 bg-orange-50 border border-orange-200 rounded whitespace-nowrap", title: measureMode === "perp" ? "Perpendicular distance from second pick to first line" : "Straight-line distance between the two picked points", children: [
971
+ formatMeasureDistance(measureDistance),
972
+ measureMode === "perp" ? " \u22A5" : ""
973
+ ] }),
589
974
  /* @__PURE__ */ jsx("button", { onClick: handleResetView, className: btn, title: "Fit drawing to view", children: "Fit" }),
590
975
  /* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
591
976
  /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }),
@@ -1909,5 +2294,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
1909
2294
  }
1910
2295
 
1911
2296
  export { Preview, setPdfPreview };
1912
- //# sourceMappingURL=chunk-QBH7KERS.js.map
1913
- //# sourceMappingURL=chunk-QBH7KERS.js.map
2297
+ //# sourceMappingURL=chunk-G6WVMTJU.js.map
2298
+ //# sourceMappingURL=chunk-G6WVMTJU.js.map