@xom11/whiteboard 0.7.0 → 0.10.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 (50) hide show
  1. package/README.md +51 -1
  2. package/dist/chunk-74VEEZBV.mjs +619 -0
  3. package/dist/chunk-74VEEZBV.mjs.map +1 -0
  4. package/dist/{chunk-BJX4YNA5.mjs → chunk-G7FR3AIV.mjs} +68 -12
  5. package/dist/chunk-G7FR3AIV.mjs.map +1 -0
  6. package/dist/{chunk-SHFOGORM.mjs → chunk-PDKKDZ4H.mjs} +4 -4
  7. package/dist/{chunk-SHFOGORM.mjs.map → chunk-PDKKDZ4H.mjs.map} +1 -1
  8. package/dist/chunk-PWIMZIB6.mjs +62 -0
  9. package/dist/chunk-PWIMZIB6.mjs.map +1 -0
  10. package/dist/{chunk-LPM4MM45.mjs → chunk-SBDMF4NQ.mjs} +3 -2
  11. package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
  12. package/dist/chunk-WQOABS6N.mjs +197 -0
  13. package/dist/chunk-WQOABS6N.mjs.map +1 -0
  14. package/dist/{chunk-3SSQKRRO.mjs → chunk-ZVN356JZ.mjs} +4 -4
  15. package/dist/{chunk-3SSQKRRO.mjs.map → chunk-ZVN356JZ.mjs.map} +1 -1
  16. package/dist/geometry-2d.js +344 -228
  17. package/dist/geometry-2d.js.map +1 -1
  18. package/dist/geometry-2d.mjs +2 -2
  19. package/dist/geometry-3d.d.mts +1 -1
  20. package/dist/geometry-3d.d.ts +1 -1
  21. package/dist/geometry-3d.js +3411 -1277
  22. package/dist/geometry-3d.js.map +1 -1
  23. package/dist/geometry-3d.mjs +3 -2
  24. package/dist/graph-2d.js +360 -66
  25. package/dist/graph-2d.js.map +1 -1
  26. package/dist/graph-2d.mjs +2 -2
  27. package/dist/{host-T2W6R6SO.mjs → host-DJETSFCG.mjs} +272 -223
  28. package/dist/host-DJETSFCG.mjs.map +1 -0
  29. package/dist/{host-2QGKMGCT.mjs → host-LZH2FZ2N.mjs} +3 -3
  30. package/dist/{host-2QGKMGCT.mjs.map → host-LZH2FZ2N.mjs.map} +1 -1
  31. package/dist/host-N6ACNJKI.mjs +3226 -0
  32. package/dist/host-N6ACNJKI.mjs.map +1 -0
  33. package/dist/index.d.mts +133 -6
  34. package/dist/index.d.ts +133 -6
  35. package/dist/index.js +5634 -1999
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +1231 -146
  38. package/dist/index.mjs.map +1 -1
  39. package/package.json +9 -6
  40. package/dist/chunk-BJX4YNA5.mjs.map +0 -1
  41. package/dist/chunk-DJTBZEAR.mjs +0 -25
  42. package/dist/chunk-DJTBZEAR.mjs.map +0 -1
  43. package/dist/chunk-HM7RIXJE.mjs +0 -331
  44. package/dist/chunk-HM7RIXJE.mjs.map +0 -1
  45. package/dist/chunk-HYXFHEDJ.mjs +0 -129
  46. package/dist/chunk-HYXFHEDJ.mjs.map +0 -1
  47. package/dist/chunk-LPM4MM45.mjs.map +0 -1
  48. package/dist/host-T2W6R6SO.mjs.map +0 -1
  49. package/dist/host-XUFON6CQ.mjs +0 -1422
  50. package/dist/host-XUFON6CQ.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  "use client";
2
- import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData } from './chunk-BJX4YNA5.mjs';
3
- import { useChordShortcut, MobileToolDrawer } from './chunk-LPM4MM45.mjs';
2
+ import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData, safeJsx } from './chunk-G7FR3AIV.mjs';
3
+ import { useChordShortcut, MobileToolDrawer } from './chunk-SBDMF4NQ.mjs';
4
4
  import { resolveAttrColors, paletteFor, themeLabel, themeAxis, themeGrid } from './chunk-HTBLO5JO.mjs';
5
5
  import { useIsMobile } from './chunk-P2AOIF7S.mjs';
6
6
  import { insertStampImage } from './chunk-C6SCVOMC.mjs';
@@ -259,8 +259,12 @@ function letterForGroup(g) {
259
259
  }
260
260
  function objKind(obj) {
261
261
  if (!obj) return "other";
262
+ const ec = typeof obj.elementClass === "number" ? obj.elementClass : null;
263
+ if (ec === 1) return "point";
264
+ if (ec === 2) return "line";
265
+ if (ec === 3) return "circle";
262
266
  const e = (obj.elType || obj.type || "").toString().toLowerCase();
263
- if (e === "point" || e === "glider" || e === "midpoint") return "point";
267
+ if (e === "point" || e === "glider" || e === "midpoint" || e === "intersection" || e === "otherintersection" || e === "reflection" || e === "mirrorpoint" || e === "mirrorelement" || e === "orthogonalprojection" || e === "parallelpoint") return "point";
264
268
  if (e === "line" || e === "segment" || e === "arrow" || e === "axis" || e === "normal" || e === "parallel" || e === "perpendicular" || e === "tangent" || e === "bisector" || e === "perpendicularsegment") return "line";
265
269
  if (e === "circle" || e === "circumcircle") return "circle";
266
270
  return "other";
@@ -282,7 +286,7 @@ function handleDown(ctx, e) {
282
286
  if (!sc) return;
283
287
  const [sx, sy] = sc;
284
288
  const hits2 = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y);
285
- const obj = hits2.find((o) => objKind(o) === "point") ?? hits2[0] ?? ctx.findNearestPoint(e, 12);
289
+ const obj = hits2.find((o) => objKind(o) === "point") ?? ctx.findNearestPoint(e, 12) ?? hits2[0];
286
290
  if (obj) {
287
291
  const shift = !!(e.shiftKey || e.altKey);
288
292
  ctx.toggleSelect(obj, shift);
@@ -320,14 +324,8 @@ function handleDown(ctx, e) {
320
324
  const tmp1 = ctx.boardRef.current.create("intersection", [a, b, 1], { visible: false, withLabel: false });
321
325
  const d0 = Math.hypot((tmp0.X?.() ?? 0) - x, (tmp0.Y?.() ?? 0) - y);
322
326
  const d1 = Math.hypot((tmp1.X?.() ?? 0) - x, (tmp1.Y?.() ?? 0) - y);
323
- try {
324
- ctx.boardRef.current.removeObject(tmp0);
325
- } catch {
326
- }
327
- try {
328
- ctx.boardRef.current.removeObject(tmp1);
329
- } catch {
330
- }
327
+ safeJsx("handlers.removeObject(intersect.tmp0)", () => ctx.boardRef.current.removeObject(tmp0));
328
+ safeJsx("handlers.removeObject(intersect.tmp1)", () => ctx.boardRef.current.removeObject(tmp1));
331
329
  const idx = d0 <= d1 ? 0 : 1;
332
330
  ctx.create("intersection", [aId, bId, idx], attrs);
333
331
  }
@@ -364,7 +362,7 @@ function handleDown(ctx, e) {
364
362
  })();
365
363
  if (ctx.pendingRef.current.length > 0 && ctx.boardRef.current) {
366
364
  const prev = ctx.pendingRef.current[ctx.pendingRef.current.length - 1];
367
- try {
365
+ safeJsx("handlers.createPreviewSegment", () => {
368
366
  const seg = ctx.boardRef.current.create("segment", [prev, pick2], {
369
367
  strokeColor: "#3b82f6",
370
368
  strokeWidth: 1.5,
@@ -374,8 +372,7 @@ function handleDown(ctx, e) {
374
372
  withLabel: false
375
373
  });
376
374
  ctx.previewSegRef.current.push(seg);
377
- } catch {
378
- }
375
+ });
379
376
  }
380
377
  ctx.pendingRef.current.push(pick2);
381
378
  ctx.setPendingCount(ctx.pendingRef.current.length);
@@ -486,10 +483,7 @@ function handleUp(ctx, e) {
486
483
  if (!sc2) return;
487
484
  const [ex, ey] = sc2;
488
485
  if (mq.rect) {
489
- try {
490
- ctx.boardRef.current?.removeObject(mq.rect);
491
- } catch {
492
- }
486
+ safeJsx("handlers.removeObject(marquee.rect)", () => ctx.boardRef.current?.removeObject(mq.rect));
493
487
  }
494
488
  if (Math.hypot(ex - mq.startSx, ey - mq.startSy) < 4) return;
495
489
  const x1 = Math.min(mq.startSx, ex), x2 = Math.max(mq.startSx, ex);
@@ -522,10 +516,7 @@ function handleUp(ctx, e) {
522
516
  }
523
517
  }
524
518
  ctx.setSelectionTick((tt) => tt + 1);
525
- try {
526
- board.update();
527
- } catch {
528
- }
519
+ safeJsx("handlers.board.update(marquee)", () => board.update());
529
520
  return;
530
521
  }
531
522
  if (t !== "move") return;
@@ -538,7 +529,7 @@ function handleUp(ctx, e) {
538
529
  const moved = Math.hypot(sx - start.sx, sy - start.sy);
539
530
  if (moved > 4) return;
540
531
  const hits = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y);
541
- const best = hits.find((o) => objKind(o) === "point") ?? hits[0] ?? ctx.findNearestPoint(e, 12);
532
+ const best = hits.find((o) => objKind(o) === "point") ?? ctx.findNearestPoint(e, 12) ?? hits[0];
542
533
  if (!best) {
543
534
  ctx.lastMoveClickRef.current = { obj: null, time: 0 };
544
535
  return;
@@ -572,12 +563,9 @@ function handleMove(ctx, e) {
572
563
  const [x2u, y2u] = ux2 && ux2.length >= 2 ? [ux2[0], ux2[1]] : toUsr(Math.max(startSx, sx), Math.max(startSy, sy));
573
564
  const rect = ctx.marqueeRef.current.rect;
574
565
  if (rect) {
575
- try {
576
- ctx.boardRef.current.removeObject(rect);
577
- } catch {
578
- }
566
+ safeJsx("handlers.removeObject(marquee.prevRect)", () => ctx.boardRef.current.removeObject(rect));
579
567
  }
580
- try {
568
+ safeJsx("handlers.createMarqueePolygon", () => {
581
569
  ctx.marqueeRef.current.rect = ctx.boardRef.current.create("polygon", [
582
570
  [x1u, y1u],
583
571
  [x2u, y1u],
@@ -592,8 +580,7 @@ function handleMove(ctx, e) {
592
580
  highlight: false,
593
581
  withLabel: false
594
582
  });
595
- } catch {
596
- }
583
+ });
597
584
  }
598
585
  return;
599
586
  }
@@ -603,14 +590,13 @@ function handleMove(ctx, e) {
603
590
  ctx.previewRafRef.current = requestAnimationFrame(() => {
604
591
  ctx.previewRafRef.current = null;
605
592
  if (!ctx.boardRef.current || !ctx.phantomRef.current) return;
606
- try {
593
+ safeJsx("handlers.phantomMove", () => {
607
594
  const coords = ctx.boardRef.current.getUsrCoordsOfMouse(e);
608
595
  const JXG = ctx.jxgRef.current;
609
596
  if (!JXG) return;
610
597
  ctx.phantomRef.current.setPositionDirectly(JXG.COORDS_BY_USER, [coords[0], coords[1]]);
611
598
  ctx.boardRef.current.update();
612
- } catch {
613
- }
599
+ });
614
600
  });
615
601
  }
616
602
  var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
@@ -622,6 +608,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
622
608
  const jxgRef = useRef(null);
623
609
  const axisObjsRef = useRef({});
624
610
  const creationLogRef = useRef([]);
611
+ const redoStackRef = useRef([]);
625
612
  const [tool, setTool] = useState("move");
626
613
  const toolRef = useRef("move");
627
614
  toolRef.current = tool;
@@ -665,19 +652,31 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
665
652
  const nextLocalId = useCallback(() => "j" + creationLogRef.current.length, []);
666
653
  const resolveArgs = useCallback((args) => {
667
654
  return args.map((a) => {
668
- if (typeof a === "string" && objMapRef.current.has(a)) {
669
- return objMapRef.current.get(a);
655
+ if (typeof a === "string") {
656
+ if (objMapRef.current.has(a)) return objMapRef.current.get(a);
657
+ const m = /^(.+):border:(\d+)$/.exec(a);
658
+ if (m) {
659
+ const poly = objMapRef.current.get(m[1]);
660
+ const idx = parseInt(m[2], 10);
661
+ if (poly && Array.isArray(poly.borders) && poly.borders[idx]) {
662
+ return poly.borders[idx];
663
+ }
664
+ }
670
665
  }
671
666
  return a;
672
667
  });
673
668
  }, []);
669
+ const pushCreationLog = useCallback((entry) => {
670
+ creationLogRef.current.push(entry);
671
+ redoStackRef.current = [];
672
+ }, []);
674
673
  const pushLog = useCallback(
675
674
  (id, type, args, attrs, obj) => {
676
- creationLogRef.current.push({ id, type, args, attrs });
675
+ pushCreationLog({ id, type, args, attrs });
677
676
  objMapRef.current.set(id, obj);
678
677
  setHistoryTick((t) => t + 1);
679
678
  },
680
- []
679
+ [pushCreationLog]
681
680
  );
682
681
  const create = useCallback(
683
682
  (type, args, attrs = {}) => {
@@ -692,15 +691,27 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
692
691
  [nextLocalId, resolveArgs, pushLog]
693
692
  );
694
693
  const localIdOf = useCallback((obj) => {
694
+ if (!obj) return null;
695
695
  for (const [id, o] of objMapRef.current.entries()) {
696
696
  if (o === obj) return id;
697
697
  }
698
+ for (const [id, o] of objMapRef.current.entries()) {
699
+ const borders = o?.borders;
700
+ if (Array.isArray(borders)) {
701
+ const idx = borders.indexOf(obj);
702
+ if (idx >= 0) return `${id}:border:${idx}`;
703
+ }
704
+ }
698
705
  return null;
699
706
  }, []);
700
707
  const snapshotObject = useCallback((obj, anchorScreen) => {
701
708
  const o = obj;
702
709
  const k = objKind(o);
703
710
  if (k !== "point" && k !== "line" && k !== "circle") return null;
711
+ for (const owner of objMapRef.current.values()) {
712
+ const borders = owner?.borders;
713
+ if (Array.isArray(borders) && borders.indexOf(o) >= 0) return null;
714
+ }
704
715
  const v = o.visProp ?? {};
705
716
  const showLabel = v.withlabel !== false;
706
717
  const showValue = valueLabelsRef.current.has(o);
@@ -760,16 +771,10 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
760
771
  if (patch.remove) {
761
772
  const vl = valueLabelsRef.current.get(o);
762
773
  if (vl) {
763
- try {
764
- boardRef.current.removeObject(vl);
765
- } catch {
766
- }
774
+ safeJsx("MiniBoard.removeObject(valueLabel)", () => boardRef.current.removeObject(vl));
767
775
  valueLabelsRef.current.delete(o);
768
776
  }
769
- try {
770
- boardRef.current.removeObject(o);
771
- } catch {
772
- }
777
+ safeJsx("MiniBoard.removeObject(target)", () => boardRef.current.removeObject(o));
773
778
  const board = boardRef.current;
774
779
  const aliveIds = /* @__PURE__ */ new Set();
775
780
  for (const [id, obj2] of objMapRef.current.entries()) {
@@ -794,7 +799,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
794
799
  const targetId = localIdOf(o);
795
800
  if (targetId) {
796
801
  const id = nextLocalId();
797
- creationLogRef.current.push({ id, type: "valueLabel", args: [targetId], attrs: {} });
802
+ pushCreationLog({ id, type: "valueLabel", args: [targetId], attrs: {} });
798
803
  objMapRef.current.set(id, txt);
799
804
  setHistoryTick((t) => t + 1);
800
805
  }
@@ -803,10 +808,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
803
808
  const txt = valueLabelsRef.current.get(o);
804
809
  valueLabelsRef.current.delete(o);
805
810
  if (txt) {
806
- try {
807
- boardRef.current.removeObject(txt);
808
- } catch {
809
- }
811
+ safeJsx("MiniBoard.removeObject(valueLabel.text)", () => boardRef.current.removeObject(txt));
810
812
  const txtId = localIdOf(txt);
811
813
  if (txtId) {
812
814
  creationLogRef.current = creationLogRef.current.filter((e) => e.id !== txtId);
@@ -817,10 +819,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
817
819
  }
818
820
  }
819
821
  if (patch.attrs) {
820
- try {
821
- o.setAttribute(patch.attrs);
822
- } catch {
823
- }
822
+ safeJsx("MiniBoard.setAttribute", () => o.setAttribute(patch.attrs));
824
823
  const id = localIdOf(o);
825
824
  if (id) {
826
825
  const entry = creationLogRef.current.find((e) => e.id === id);
@@ -828,19 +827,13 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
828
827
  setHistoryTick((t) => t + 1);
829
828
  }
830
829
  }
831
- try {
832
- boardRef.current.update();
833
- } catch {
834
- }
830
+ safeJsx("MiniBoard.board.update(mutate)", () => boardRef.current.update());
835
831
  }, [createValueLabelFor, localIdOf, nextLocalId]);
836
832
  const clearPreviewSegs = useCallback(() => {
837
833
  const b = boardRef.current;
838
834
  if (!b) return;
839
835
  for (const s of previewSegRef.current) {
840
- try {
841
- b.removeObject(s);
842
- } catch {
843
- }
836
+ safeJsx("MiniBoard.removeObject(previewSeg)", () => b.removeObject(s));
844
837
  }
845
838
  previewSegRef.current = [];
846
839
  }, []);
@@ -848,17 +841,11 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
848
841
  const b = boardRef.current;
849
842
  if (!b) return;
850
843
  if (previewShapeRef.current) {
851
- try {
852
- b.removeObject(previewShapeRef.current);
853
- } catch {
854
- }
844
+ safeJsx("MiniBoard.removeObject(previewShape)", () => b.removeObject(previewShapeRef.current));
855
845
  previewShapeRef.current = null;
856
846
  }
857
847
  if (phantomRef.current) {
858
- try {
859
- b.removeObject(phantomRef.current);
860
- } catch {
861
- }
848
+ safeJsx("MiniBoard.removeObject(phantom)", () => b.removeObject(phantomRef.current));
862
849
  phantomRef.current = null;
863
850
  }
864
851
  }, []);
@@ -870,7 +857,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
870
857
  }, [clearPreviewSegs, removePhantom]);
871
858
  const applySelectionStyle = useCallback((obj) => {
872
859
  if (!obj || selOriginalRef.current.has(obj)) return;
873
- try {
860
+ safeJsx("MiniBoard.applySelectionStyle", () => {
874
861
  const visProp = obj.visProp ?? {};
875
862
  selOriginalRef.current.set(obj, {
876
863
  strokeColor: visProp.strokecolor,
@@ -882,19 +869,17 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
882
869
  } else {
883
870
  obj.setAttribute({ strokeColor: "#06b6d4", strokeWidth: 3 });
884
871
  }
885
- } catch {
886
- }
872
+ });
887
873
  }, []);
888
874
  const restoreSelectionStyle = useCallback((obj) => {
889
875
  const orig = selOriginalRef.current.get(obj);
890
876
  if (!orig) return;
891
- try {
877
+ safeJsx("MiniBoard.restoreSelectionStyle", () => {
892
878
  const attrs = {};
893
879
  if (orig.strokeColor !== void 0) attrs.strokeColor = orig.strokeColor;
894
880
  if (orig.strokeWidth !== void 0) attrs.strokeWidth = orig.strokeWidth;
895
881
  obj.setAttribute(attrs);
896
- } catch {
897
- }
882
+ });
898
883
  selOriginalRef.current.delete(obj);
899
884
  }, []);
900
885
  const clearSelection = useCallback(() => {
@@ -903,10 +888,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
903
888
  }
904
889
  selectedSetRef.current.clear();
905
890
  setSelectionTick((t) => t + 1);
906
- try {
907
- boardRef.current?.update();
908
- } catch {
909
- }
891
+ safeJsx("MiniBoard.board.update(clearSelection)", () => boardRef.current?.update());
910
892
  }, [restoreSelectionStyle]);
911
893
  const toggleSelect = useCallback((obj, additive) => {
912
894
  if (!obj) return;
@@ -926,10 +908,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
926
908
  }
927
909
  }
928
910
  setSelectionTick((t) => t + 1);
929
- try {
930
- boardRef.current?.update();
931
- } catch {
932
- }
911
+ safeJsx("MiniBoard.board.update(toggleSelect)", () => boardRef.current?.update());
933
912
  }, [applySelectionStyle, restoreSelectionStyle]);
934
913
  const deleteSelected = useCallback(() => {
935
914
  const board = boardRef.current;
@@ -937,10 +916,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
937
916
  if (selectedSetRef.current.size === 0) return;
938
917
  for (const o of selectedSetRef.current) selOriginalRef.current.delete(o);
939
918
  for (const o of selectedSetRef.current) {
940
- try {
941
- board.removeObject(o);
942
- } catch {
943
- }
919
+ safeJsx("MiniBoard.removeObject(selected)", () => board.removeObject(o));
944
920
  }
945
921
  selectedSetRef.current.clear();
946
922
  const aliveIds = /* @__PURE__ */ new Set();
@@ -1013,10 +989,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1013
989
  const b = boardRef.current;
1014
990
  if (!b) return;
1015
991
  if (previewShapeRef.current) {
1016
- try {
1017
- b.removeObject(previewShapeRef.current);
1018
- } catch {
1019
- }
992
+ safeJsx("MiniBoard.removeObject(refreshPreview)", () => b.removeObject(previewShapeRef.current));
1020
993
  previewShapeRef.current = null;
1021
994
  }
1022
995
  const t = toolRef.current;
@@ -1135,7 +1108,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1135
1108
  }
1136
1109
  case "toggleLabel": {
1137
1110
  const obj = picks[0];
1138
- try {
1111
+ safeJsx("MiniBoard.toggleLabel", () => {
1139
1112
  if (obj.label) {
1140
1113
  const visible = obj.label.visProp.visible !== false;
1141
1114
  obj.label.setAttribute({ visible: !visible });
@@ -1144,23 +1117,21 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1144
1117
  obj.setAttribute({ withLabel: !cur });
1145
1118
  }
1146
1119
  boardRef.current.update();
1147
- } catch {
1148
- }
1120
+ });
1149
1121
  break;
1150
1122
  }
1151
1123
  case "toggleVisible": {
1152
1124
  const obj = picks[0];
1153
- try {
1125
+ safeJsx("MiniBoard.toggleVisible", () => {
1154
1126
  const visible = obj.visProp.visible !== false;
1155
1127
  obj.setAttribute({ visible: !visible });
1156
1128
  boardRef.current.update();
1157
- } catch {
1158
- }
1129
+ });
1159
1130
  break;
1160
1131
  }
1161
1132
  case "delete": {
1162
1133
  const obj = picks[0];
1163
- try {
1134
+ safeJsx("MiniBoard.deleteOne", () => {
1164
1135
  boardRef.current.removeObject(obj);
1165
1136
  const board = boardRef.current;
1166
1137
  const aliveIds = /* @__PURE__ */ new Set();
@@ -1175,8 +1146,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1175
1146
  if (!aliveIds.has(id)) objMapRef.current.delete(id);
1176
1147
  }
1177
1148
  setHistoryTick((t) => t + 1);
1178
- } catch {
1179
- }
1149
+ });
1180
1150
  break;
1181
1151
  }
1182
1152
  }
@@ -1211,7 +1181,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1211
1181
  }
1212
1182
  const stepId = nextLocalId();
1213
1183
  const stepObj = boardRef.current.create("transform", step.params, step.attrs);
1214
- creationLogRef.current.push({ id: stepId, type: "transform", args: stepLogArgs, attrs: step.attrs });
1184
+ pushCreationLog({ id: stepId, type: "transform", args: stepLogArgs, attrs: step.attrs });
1215
1185
  objMapRef.current.set(stepId, stepObj);
1216
1186
  transformObjs.push(stepObj);
1217
1187
  transformIds.push(stepId);
@@ -1225,7 +1195,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1225
1195
  const newName = srcName ? `${srcName}'` : nextLabel();
1226
1196
  const attrs = { name: newName, size: 3, color: "#0ea5e9", strokeColor: "#0ea5e9", fillColor: "#0ea5e9" };
1227
1197
  const obj = boardRef.current.create("point", [src, transformParent], attrs);
1228
- creationLogRef.current.push({ id, type: "point", args: [srcId ?? src, transformLogRef], attrs });
1198
+ pushCreationLog({ id, type: "point", args: [srcId ?? src, transformLogRef], attrs });
1229
1199
  objMapRef.current.set(id, obj);
1230
1200
  return obj;
1231
1201
  });
@@ -1256,6 +1226,45 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1256
1226
  }
1257
1227
  setHistoryTick((t) => t + 1);
1258
1228
  }, [create, flashWarn, localIdOf, nextLabel, nextLocalId]);
1229
+ const recreateFromLogEntry = useCallback((el) => {
1230
+ const board = boardRef.current;
1231
+ if (!board) return false;
1232
+ const idMap = objMapRef.current;
1233
+ const resolve = (a) => {
1234
+ if (typeof a === "string") {
1235
+ if (idMap.has(a)) return idMap.get(a);
1236
+ const m = /^(.+):border:(\d+)$/.exec(a);
1237
+ if (m) {
1238
+ const poly = idMap.get(m[1]);
1239
+ const idx = parseInt(m[2], 10);
1240
+ if (poly && Array.isArray(poly.borders) && poly.borders[idx]) {
1241
+ return poly.borders[idx];
1242
+ }
1243
+ }
1244
+ }
1245
+ if (Array.isArray(a)) return a.map(resolve);
1246
+ return a;
1247
+ };
1248
+ const resolved = el.args.map(resolve);
1249
+ try {
1250
+ if (el.type === "valueLabel") {
1251
+ const target = resolved[0];
1252
+ if (!target) return false;
1253
+ const txt = createValueLabelFor(target);
1254
+ if (!txt) return false;
1255
+ idMap.set(el.id, txt);
1256
+ valueLabelsRef.current.set(target, txt);
1257
+ return true;
1258
+ }
1259
+ const themedAttrs = resolveAttrColors({ ...el.attrs }, paletteFor(isDarkRef.current));
1260
+ const obj = board.create(el.type, resolved, themedAttrs);
1261
+ idMap.set(el.id, obj);
1262
+ return true;
1263
+ } catch (err) {
1264
+ console.warn("Recreate failed for", el.type, err);
1265
+ return false;
1266
+ }
1267
+ }, [createValueLabelFor]);
1259
1268
  const undoLast = useCallback(() => {
1260
1269
  const b = boardRef.current;
1261
1270
  if (!b) return;
@@ -1265,21 +1274,31 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1265
1274
  const obj = objMapRef.current.get(last.id);
1266
1275
  objMapRef.current.delete(last.id);
1267
1276
  if (obj) {
1268
- try {
1269
- b.removeObject(obj);
1270
- } catch {
1271
- }
1277
+ safeJsx("MiniBoard.removeObject(undo)", () => b.removeObject(obj));
1272
1278
  clearPending();
1279
+ redoStackRef.current.push(last);
1273
1280
  setHistoryTick((t) => t + 1);
1274
- try {
1275
- b.update();
1276
- } catch {
1277
- }
1281
+ safeJsx("MiniBoard.board.update(undo)", () => b.update());
1278
1282
  return;
1279
1283
  }
1280
1284
  }
1281
1285
  setHistoryTick((t) => t + 1);
1282
1286
  }, [clearPending]);
1287
+ const redoNext = useCallback(() => {
1288
+ const b = boardRef.current;
1289
+ if (!b) return;
1290
+ const entry = redoStackRef.current.pop();
1291
+ if (!entry) {
1292
+ setHistoryTick((t) => t + 1);
1293
+ return;
1294
+ }
1295
+ const ok = recreateFromLogEntry(entry);
1296
+ if (ok) {
1297
+ creationLogRef.current.push(entry);
1298
+ }
1299
+ setHistoryTick((t) => t + 1);
1300
+ safeJsx("MiniBoard.board.update(redo)", () => b.update());
1301
+ }, [recreateFromLogEntry]);
1283
1302
  useEffect(() => {
1284
1303
  const onKey = (e) => {
1285
1304
  const ae = document.activeElement;
@@ -1291,6 +1310,13 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1291
1310
  undoLastRef.current();
1292
1311
  return;
1293
1312
  }
1313
+ if ((e.metaKey || e.ctrlKey) && (e.key.toLowerCase() === "z" && e.shiftKey || e.key.toLowerCase() === "y" && !e.shiftKey)) {
1314
+ if (inField) return;
1315
+ e.preventDefault();
1316
+ e.stopPropagation();
1317
+ redoNextRef.current();
1318
+ return;
1319
+ }
1294
1320
  if (e.key === "Escape" && !inField) {
1295
1321
  if (pendingRef.current.length > 0) {
1296
1322
  e.preventDefault();
@@ -1337,16 +1363,14 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1337
1363
  if (!sc) return [];
1338
1364
  const [sx, sy] = sc;
1339
1365
  const list = [];
1340
- try {
1366
+ safeJsx("MiniBoard.objectsAt.loop", () => {
1341
1367
  const objs = b.objectsList || [];
1342
1368
  for (const o of objs) {
1343
- try {
1369
+ safeJsx("MiniBoard.objectsAt.hasPoint", () => {
1344
1370
  if (o.hasPoint && o.hasPoint(sx, sy)) list.push(o);
1345
- } catch {
1346
- }
1371
+ });
1347
1372
  }
1348
- } catch {
1349
- }
1373
+ });
1350
1374
  return list;
1351
1375
  }, [screenCoordsOf]);
1352
1376
  const findNearestPoint = useCallback((evt, tolPx = 12) => {
@@ -1356,24 +1380,23 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1356
1380
  if (!sc) return null;
1357
1381
  const [sx, sy] = sc;
1358
1382
  const tol2 = tolPx * tolPx;
1359
- let best = null;
1360
- try {
1383
+ const bestRef = { current: null };
1384
+ safeJsx("MiniBoard.findNearestPoint.loop", () => {
1361
1385
  const objs = b.objectsList || [];
1362
1386
  for (const o of objs) {
1363
- try {
1364
- if (objKind(o) !== "point") continue;
1387
+ safeJsx("MiniBoard.findNearestPoint.iter", () => {
1388
+ if (objKind(o) !== "point") return;
1365
1389
  const pc = o.coords?.scrCoords;
1366
- if (!pc) continue;
1390
+ if (!pc) return;
1367
1391
  const dx = pc[1] - sx;
1368
1392
  const dy = pc[2] - sy;
1369
1393
  const d2 = dx * dx + dy * dy;
1370
- if (d2 <= tol2 && (!best || d2 < best.d2)) best = { obj: o, d2 };
1371
- } catch {
1372
- }
1394
+ const cur = bestRef.current;
1395
+ if (d2 <= tol2 && (!cur || d2 < cur.d2)) bestRef.current = { obj: o, d2 };
1396
+ });
1373
1397
  }
1374
- } catch {
1375
- }
1376
- return best ? best.obj : null;
1398
+ });
1399
+ return bestRef.current ? bestRef.current.obj : null;
1377
1400
  }, [screenCoordsOf]);
1378
1401
  const promoteLabel = useCallback((o) => {
1379
1402
  if (!o) return o;
@@ -1381,31 +1404,25 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1381
1404
  if (t !== "text") return o;
1382
1405
  const b = boardRef.current;
1383
1406
  if (!b) return o;
1384
- try {
1407
+ const promoted = safeJsx("MiniBoard.promoteLabel", () => {
1385
1408
  for (const c of b.objectsList || []) {
1386
1409
  if (c.label === o) return c;
1387
1410
  }
1388
- } catch {
1389
- }
1390
- return o;
1411
+ return null;
1412
+ }, null);
1413
+ return promoted ?? o;
1391
1414
  }, []);
1392
1415
  const pendingTransformRef = useRef(null);
1393
1416
  const transformSubsRef = useRef(/* @__PURE__ */ new Set());
1394
1417
  const emitTransform = useCallback((info) => {
1395
1418
  transformSubsRef.current.forEach((cb) => {
1396
- try {
1397
- cb(info);
1398
- } catch {
1399
- }
1419
+ safeJsx("MiniBoard.emitTransform.cb", () => cb(info));
1400
1420
  });
1401
1421
  }, []);
1402
1422
  const selectSubsRef = useRef(/* @__PURE__ */ new Set());
1403
1423
  const emitSelect = useCallback((snap) => {
1404
1424
  selectSubsRef.current.forEach((cb) => {
1405
- try {
1406
- cb(snap);
1407
- } catch {
1408
- }
1425
+ safeJsx("MiniBoard.emitSelect.cb", () => cb(snap));
1409
1426
  });
1410
1427
  }, []);
1411
1428
  const moveDownRef = useRef(null);
@@ -1417,7 +1434,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1417
1434
  const JXG = (await import('jsxgraph')).default;
1418
1435
  if (cancelled || !containerRef.current) return;
1419
1436
  jxgRef.current = JXG;
1420
- try {
1437
+ safeJsx("MiniBoard.applyJxgOptions", () => {
1421
1438
  const opts = JXG.Options;
1422
1439
  if (opts) {
1423
1440
  opts.text = opts.text || {};
@@ -1430,8 +1447,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1430
1447
  opts.label.strokeColor = themeLabel(isDarkRef.current);
1431
1448
  opts.text.strokeColor = themeLabel(isDarkRef.current);
1432
1449
  }
1433
- } catch {
1434
- }
1450
+ });
1435
1451
  const board = JXG.JSXGraph.initBoard(containerId, {
1436
1452
  boundingbox: initialState?.bbox ?? [-10, 10, 10, -10],
1437
1453
  axis: false,
@@ -1452,43 +1468,20 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1452
1468
  });
1453
1469
  boardRef.current = board;
1454
1470
  if (initialState && initialState.elements.length > 0) {
1455
- const idMap = objMapRef.current;
1456
1471
  for (const el of initialState.elements) {
1457
- const resolved = el.args.map((a) => typeof a === "string" && idMap.has(a) ? idMap.get(a) : a);
1458
- try {
1459
- if (el.type === "valueLabel") {
1460
- const target = resolved[0];
1461
- if (target) {
1462
- const txt = createValueLabelFor(target);
1463
- if (txt) {
1464
- idMap.set(el.id, txt);
1465
- valueLabelsRef.current.set(target, txt);
1466
- }
1467
- }
1468
- continue;
1469
- }
1470
- const themedAttrs = resolveAttrColors({ ...el.attrs }, paletteFor(isDarkRef.current));
1471
- const obj = board.create(el.type, resolved, themedAttrs);
1472
- idMap.set(el.id, obj);
1473
- } catch (err) {
1474
- console.warn("Replay failed for", el.type, err);
1475
- }
1472
+ recreateFromLogEntry(el);
1476
1473
  }
1477
1474
  creationLogRef.current = [...initialState.elements];
1478
1475
  labelIdxRef.current = initialState.elements.filter((e) => e.type === "point").length;
1479
1476
  }
1480
1477
  if (showAxisRef.current) {
1481
- try {
1478
+ safeJsx("MiniBoard.initAxes", () => {
1482
1479
  axisObjsRef.current.x = board.create("axis", [[0, 0], [1, 0]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1483
1480
  axisObjsRef.current.y = board.create("axis", [[0, 0], [0, 1]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1484
- } catch {
1485
- }
1481
+ });
1486
1482
  }
1487
1483
  if (showGridRef.current) {
1488
- try {
1489
- board.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 });
1490
- } catch {
1491
- }
1484
+ safeJsx("MiniBoard.initGrid", () => board.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 }));
1492
1485
  }
1493
1486
  board.on("down", (e) => {
1494
1487
  const ctx = {
@@ -1639,6 +1632,8 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1639
1632
  setShowGrid: (b) => setShowGridRef.current(b),
1640
1633
  undo: () => undoLastRef.current(),
1641
1634
  canUndo: () => creationLogRef.current.length > 0,
1635
+ redo: () => redoNextRef.current(),
1636
+ canRedo: () => redoStackRef.current.length > 0,
1642
1637
  subscribe: (cb) => {
1643
1638
  subscribersRef.current.add(cb);
1644
1639
  return () => {
@@ -1651,15 +1646,14 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1651
1646
  const b = boardRef.current;
1652
1647
  if (!b) return [];
1653
1648
  const out = [];
1654
- try {
1649
+ safeJsx("MiniBoard.getAllPointNames", () => {
1655
1650
  const objs = b.objectsList || [];
1656
1651
  for (const o of objs) {
1657
1652
  if (objKind(o) === "point" && typeof o.name === "string" && o.name) {
1658
1653
  out.push(o.name);
1659
1654
  }
1660
1655
  }
1661
- } catch {
1662
- }
1656
+ });
1663
1657
  return out;
1664
1658
  },
1665
1659
  onSelect: (cb) => {
@@ -1720,10 +1714,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1720
1714
  previewRafRef.current = null;
1721
1715
  }
1722
1716
  if (boardRef.current && jxgRef.current) {
1723
- try {
1724
- jxgRef.current.JSXGraph.freeBoard(boardRef.current);
1725
- } catch {
1726
- }
1717
+ safeJsx("MiniBoard.freeBoard", () => jxgRef.current.JSXGraph.freeBoard(boardRef.current));
1727
1718
  boardRef.current = null;
1728
1719
  }
1729
1720
  };
@@ -1731,19 +1722,13 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1731
1722
  useEffect(() => {
1732
1723
  const b = boardRef.current;
1733
1724
  if (!b) return;
1734
- try {
1725
+ safeJsx("MiniBoard.toggleAxis", () => {
1735
1726
  if (axisObjsRef.current.x) {
1736
- try {
1737
- b.removeObject(axisObjsRef.current.x);
1738
- } catch {
1739
- }
1727
+ safeJsx("MiniBoard.removeObject(axisX)", () => b.removeObject(axisObjsRef.current.x));
1740
1728
  axisObjsRef.current.x = void 0;
1741
1729
  }
1742
1730
  if (axisObjsRef.current.y) {
1743
- try {
1744
- b.removeObject(axisObjsRef.current.y);
1745
- } catch {
1746
- }
1731
+ safeJsx("MiniBoard.removeObject(axisY)", () => b.removeObject(axisObjsRef.current.y));
1747
1732
  axisObjsRef.current.y = void 0;
1748
1733
  }
1749
1734
  if (showAxis) {
@@ -1751,28 +1736,23 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1751
1736
  axisObjsRef.current.y = b.create("axis", [[0, 0], [0, 1]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1752
1737
  }
1753
1738
  b.update();
1754
- } catch {
1755
- }
1739
+ });
1756
1740
  }, [showAxis]);
1757
1741
  useEffect(() => {
1758
1742
  const b = boardRef.current;
1759
1743
  if (!b) return;
1760
- try {
1744
+ safeJsx("MiniBoard.toggleGrid", () => {
1761
1745
  const objs = Object.values(b.objects || {});
1762
1746
  for (const o of objs) {
1763
1747
  if (o && (o.elType === "grid" || o.type === "grid" || o.visProp && o.visProp.type === "grid")) {
1764
- try {
1765
- b.removeObject(o);
1766
- } catch {
1767
- }
1748
+ safeJsx("MiniBoard.removeObject(grid)", () => b.removeObject(o));
1768
1749
  }
1769
1750
  }
1770
1751
  if (showGrid) {
1771
1752
  b.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 });
1772
1753
  }
1773
1754
  b.update();
1774
- } catch {
1775
- }
1755
+ });
1776
1756
  }, [showGrid]);
1777
1757
  const handleToolChange = useCallback((t) => {
1778
1758
  clearPending();
@@ -1780,10 +1760,9 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1780
1760
  setTool(t);
1781
1761
  const b = boardRef.current;
1782
1762
  if (b) {
1783
- try {
1763
+ safeJsx("MiniBoard.setPanForTool", () => {
1784
1764
  if (b.attr?.pan) b.attr.pan.enabled = t !== "select";
1785
- } catch {
1786
- }
1765
+ });
1787
1766
  }
1788
1767
  }, [clearPending]);
1789
1768
  const handleToolChangeRef = useRef(handleToolChange);
@@ -1791,10 +1770,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1791
1770
  const subscribersRef = useRef(/* @__PURE__ */ new Set());
1792
1771
  const notifySubscribers = useCallback(() => {
1793
1772
  subscribersRef.current.forEach((cb) => {
1794
- try {
1795
- cb();
1796
- } catch {
1797
- }
1773
+ safeJsx("MiniBoard.notifySubscriber.cb", () => cb());
1798
1774
  });
1799
1775
  }, []);
1800
1776
  useEffect(() => {
@@ -1802,6 +1778,8 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
1802
1778
  }, [tool, showAxis, showGrid, historyTick, notifySubscribers]);
1803
1779
  const undoLastRef = useRef(undoLast);
1804
1780
  undoLastRef.current = undoLast;
1781
+ const redoNextRef = useRef(redoNext);
1782
+ redoNextRef.current = redoNext;
1805
1783
  const clearPendingRef = useRef(clearPending);
1806
1784
  clearPendingRef.current = clearPending;
1807
1785
  const finalizeTransformCreateRef = useRef(finalizeTransformCreate);
@@ -1885,6 +1863,12 @@ function UndoIcon() {
1885
1863
  /* @__PURE__ */ jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
1886
1864
  ] });
1887
1865
  }
1866
+ function RedoIcon() {
1867
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1868
+ /* @__PURE__ */ jsx("polyline", { points: "21 7 21 13 15 13" }),
1869
+ /* @__PURE__ */ jsx("path", { d: "M20.49 13a9 9 0 1 1-2.13-9.36L21 7" })
1870
+ ] });
1871
+ }
1888
1872
  function AxisIcon() {
1889
1873
  return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
1890
1874
  /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
@@ -1929,7 +1913,7 @@ function useToolHoverTooltip() {
1929
1913
  return { hover, portalReady, showHover, hideHover };
1930
1914
  }
1931
1915
  function DesktopGeometryPanel(props) {
1932
- const { activeTool, onToolChange, showAxis, showGrid, onShowAxisChange, onShowGridChange, onUndo, canUndo, onClose, isDark, chordGroup } = props;
1916
+ const { activeTool, onToolChange, showAxis, showGrid, onShowAxisChange, onShowGridChange, onUndo, canUndo, onRedo, canRedo, onClose, isDark, chordGroup } = props;
1933
1917
  const grouped = useMemo(() => {
1934
1918
  return TOOLS.reduce((acc, t) => {
1935
1919
  var _a;
@@ -1978,9 +1962,23 @@ function DesktopGeometryPanel(props) {
1978
1962
  disabled: !canUndo,
1979
1963
  title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
1980
1964
  "aria-label": "Ho\xE0n t\xE1c",
1965
+ "data-testid": "undo-btn",
1981
1966
  className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
1982
1967
  children: /* @__PURE__ */ jsx(UndoIcon, {})
1983
1968
  }
1969
+ ),
1970
+ /* @__PURE__ */ jsx(
1971
+ "button",
1972
+ {
1973
+ type: "button",
1974
+ onClick: onRedo,
1975
+ disabled: !canRedo,
1976
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
1977
+ "aria-label": "L\xE0m l\u1EA1i",
1978
+ "data-testid": "redo-btn",
1979
+ className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
1980
+ children: /* @__PURE__ */ jsx(RedoIcon, {})
1981
+ }
1984
1982
  )
1985
1983
  ] }) }),
1986
1984
  groupKeys.map((group) => {
@@ -2101,6 +2099,8 @@ function MobileGeometryPanel(props) {
2101
2099
  onShowGridChange,
2102
2100
  onUndo,
2103
2101
  canUndo,
2102
+ onRedo,
2103
+ canRedo,
2104
2104
  isDark,
2105
2105
  drawerOpen,
2106
2106
  onDrawerClose
@@ -2149,6 +2149,13 @@ function MobileGeometryPanel(props) {
2149
2149
  icon: /* @__PURE__ */ jsx(UndoIcon, {}),
2150
2150
  onClick: onUndo,
2151
2151
  disabled: !canUndo
2152
+ },
2153
+ {
2154
+ label: "L\xE0m l\u1EA1i",
2155
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
2156
+ icon: /* @__PURE__ */ jsx(RedoIcon, {}),
2157
+ onClick: onRedo,
2158
+ disabled: !canRedo
2152
2159
  }
2153
2160
  ],
2154
2161
  groups,
@@ -2533,9 +2540,10 @@ var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel,
2533
2540
  return createPortal(node, document.body);
2534
2541
  };
2535
2542
  var GeometryEditorPanel = forwardRef(
2536
- function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark, isMobile = false, onOpenDrawer }, ref) {
2543
+ function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark, isMobile = false, onOpenDrawer, onUndo, onRedo, canUndo, canRedo }, ref) {
2537
2544
  const handleRef = useRef(null);
2538
2545
  const [ready, setReady] = useState(false);
2546
+ const [hasContent, setHasContent] = useState(false);
2539
2547
  const [propsPopover, setPropsPopover] = useState(null);
2540
2548
  const [transformPopover, setTransformPopover] = useState(null);
2541
2549
  const onStateChangeRef = useRef(onStateChange);
@@ -2544,13 +2552,16 @@ var GeometryEditorPanel = forwardRef(
2544
2552
  }, [onStateChange]);
2545
2553
  const emitState = useCallback(() => {
2546
2554
  const h = handleRef.current;
2555
+ if (!h) return;
2556
+ setHasContent(h.getCreationLog().length > 0);
2547
2557
  const cb = onStateChangeRef.current;
2548
- if (!h || !cb) return;
2558
+ if (!cb) return;
2549
2559
  cb({
2550
2560
  tool: h.getTool(),
2551
2561
  showAxis: h.getShowAxis(),
2552
2562
  showGrid: h.getShowGrid(),
2553
- canUndo: h.canUndo()
2563
+ canUndo: h.canUndo(),
2564
+ canRedo: h.canRedo()
2554
2565
  });
2555
2566
  }, []);
2556
2567
  const handleReady = useCallback((h) => {
@@ -2592,6 +2603,7 @@ var GeometryEditorPanel = forwardRef(
2592
2603
  setShowAxis: (b) => handleRef.current?.setShowAxis(b),
2593
2604
  setShowGrid: (b) => handleRef.current?.setShowGrid(b),
2594
2605
  undo: () => handleRef.current?.undo(),
2606
+ redo: () => handleRef.current?.redo(),
2595
2607
  insert: performInsert,
2596
2608
  hasContent: () => (handleRef.current?.getCreationLog().length ?? 0) > 0
2597
2609
  }), [performInsert]);
@@ -2641,17 +2653,46 @@ var GeometryEditorPanel = forwardRef(
2641
2653
  ] }),
2642
2654
  "D\u1EF1ng h\xECnh h\u1ECDc"
2643
2655
  ] }),
2644
- isMobile && /* @__PURE__ */ jsx(
2645
- "button",
2646
- {
2647
- type: "button",
2648
- onClick: handleInsert,
2649
- disabled: !ready,
2650
- "data-testid": "geometry-insert-btn-mobile",
2651
- className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
2652
- children: "Ch\xE8n"
2653
- }
2654
- ),
2656
+ isMobile && /* @__PURE__ */ jsxs(Fragment, { children: [
2657
+ /* @__PURE__ */ jsx(
2658
+ "button",
2659
+ {
2660
+ type: "button",
2661
+ onClick: onUndo,
2662
+ disabled: !canUndo,
2663
+ "aria-label": "Ho\xE0n t\xE1c",
2664
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
2665
+ "data-testid": "undo-btn-mobile",
2666
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
2667
+ children: /* @__PURE__ */ jsx(UndoIcon, {})
2668
+ }
2669
+ ),
2670
+ /* @__PURE__ */ jsx(
2671
+ "button",
2672
+ {
2673
+ type: "button",
2674
+ onClick: onRedo,
2675
+ disabled: !canRedo,
2676
+ "aria-label": "L\xE0m l\u1EA1i",
2677
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
2678
+ "data-testid": "redo-btn-mobile",
2679
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
2680
+ children: /* @__PURE__ */ jsx(RedoIcon, {})
2681
+ }
2682
+ ),
2683
+ /* @__PURE__ */ jsx(
2684
+ "button",
2685
+ {
2686
+ type: "button",
2687
+ onClick: handleInsert,
2688
+ disabled: !ready || !hasContent,
2689
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
2690
+ "data-testid": "geometry-insert-btn-mobile",
2691
+ className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
2692
+ children: "Ch\xE8n"
2693
+ }
2694
+ )
2695
+ ] }),
2655
2696
  /* @__PURE__ */ jsx("button", { onClick: onClose, "aria-label": "\u0110\xF3ng", className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15", children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2656
2697
  /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
2657
2698
  /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
@@ -2745,7 +2786,8 @@ var GeometryEditorPanel = forwardRef(
2745
2786
  "button",
2746
2787
  {
2747
2788
  onClick: handleInsert,
2748
- disabled: !ready,
2789
+ disabled: !ready || !hasContent,
2790
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
2749
2791
  "data-testid": "geometry-insert-btn",
2750
2792
  className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
2751
2793
  children: "Ch\xE8n"
@@ -2762,7 +2804,8 @@ var INITIAL_GEOM_STATE = {
2762
2804
  tool: "move",
2763
2805
  showAxis: false,
2764
2806
  showGrid: false,
2765
- canUndo: false
2807
+ canUndo: false,
2808
+ canRedo: false
2766
2809
  };
2767
2810
  var GeometryStampHost = forwardRef(
2768
2811
  function GeometryStampHost2({ api, editingElement, onClose, isDark }, ref) {
@@ -2828,6 +2871,8 @@ var GeometryStampHost = forwardRef(
2828
2871
  onShowGridChange: (b) => panelRef.current?.setShowGrid(b),
2829
2872
  onUndo: () => panelRef.current?.undo(),
2830
2873
  canUndo: geomState.canUndo,
2874
+ onRedo: () => panelRef.current?.redo(),
2875
+ canRedo: geomState.canRedo,
2831
2876
  onClose,
2832
2877
  isDark,
2833
2878
  isMobile,
@@ -2847,7 +2892,11 @@ var GeometryStampHost = forwardRef(
2847
2892
  withLeftPanel: !isMobile,
2848
2893
  isDark,
2849
2894
  isMobile,
2850
- onOpenDrawer: () => setDrawerOpen(true)
2895
+ onOpenDrawer: () => setDrawerOpen(true),
2896
+ onUndo: () => panelRef.current?.undo(),
2897
+ onRedo: () => panelRef.current?.redo(),
2898
+ canUndo: geomState.canUndo,
2899
+ canRedo: geomState.canRedo
2851
2900
  }
2852
2901
  )
2853
2902
  ] });
@@ -2855,5 +2904,5 @@ var GeometryStampHost = forwardRef(
2855
2904
  );
2856
2905
 
2857
2906
  export { GeometryStampHost };
2858
- //# sourceMappingURL=host-T2W6R6SO.mjs.map
2859
- //# sourceMappingURL=host-T2W6R6SO.mjs.map
2907
+ //# sourceMappingURL=host-DJETSFCG.mjs.map
2908
+ //# sourceMappingURL=host-DJETSFCG.mjs.map