@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
@@ -116,7 +116,17 @@ function deserializeIntoBoard(board, serialized, options = {}) {
116
116
  const palette = options.palette ?? paletteFor(false);
117
117
  const idMap = /* @__PURE__ */ new Map();
118
118
  const resolve = (a) => {
119
- if (typeof a === "string" && idMap.has(a)) return idMap.get(a);
119
+ if (typeof a === "string") {
120
+ if (idMap.has(a)) return idMap.get(a);
121
+ const m = /^(.+):border:(\d+)$/.exec(a);
122
+ if (m) {
123
+ const poly = idMap.get(m[1]);
124
+ const idx = parseInt(m[2], 10);
125
+ if (poly && Array.isArray(poly.borders) && poly.borders[idx]) {
126
+ return poly.borders[idx];
127
+ }
128
+ }
129
+ }
120
130
  if (Array.isArray(a)) return a.map(resolve);
121
131
  return a;
122
132
  };
@@ -139,12 +149,59 @@ var init_serialize = __esm({
139
149
  }
140
150
  });
141
151
 
152
+ // src/stamps/shared/safeJsx.ts
153
+ function safeJsx(label, fn, fallback) {
154
+ try {
155
+ return fn();
156
+ } catch (err) {
157
+ if (isDev) {
158
+ console.warn("[whiteboard:jsxgraph]", label, err);
159
+ }
160
+ return fallback;
161
+ }
162
+ }
163
+ var isDev;
164
+ var init_safeJsx = __esm({
165
+ "src/stamps/shared/safeJsx.ts"() {
166
+ isDev = (() => {
167
+ try {
168
+ return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
169
+ } catch {
170
+ return false;
171
+ }
172
+ })();
173
+ }
174
+ });
175
+
142
176
  // src/stamps/geometry-2d/render.ts
177
+ function containerDimsForBbox(bbox) {
178
+ const [xmin, ymax, xmax, ymin] = bbox;
179
+ const w = Math.abs(xmax - xmin);
180
+ const h = Math.abs(ymax - ymin);
181
+ if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
182
+ return { width: FALLBACK_W, height: FALLBACK_H };
183
+ }
184
+ let width = w * PIXELS_PER_UNIT;
185
+ let height = h * PIXELS_PER_UNIT;
186
+ const maxAxis = Math.max(width, height);
187
+ if (maxAxis > MAX_DIM) {
188
+ const ratio = MAX_DIM / maxAxis;
189
+ width *= ratio;
190
+ height *= ratio;
191
+ }
192
+ const minAxis = Math.min(width, height);
193
+ if (minAxis < MIN_DIM) {
194
+ const ratio = MIN_DIM / minAxis;
195
+ width *= ratio;
196
+ height *= ratio;
197
+ }
198
+ return { width: Math.round(width), height: Math.round(height) };
199
+ }
143
200
  async function renderGeometrySvgFromState(jsonState) {
144
201
  const parsed = JSON.parse(jsonState);
145
202
  const palette = paletteFor(false);
146
203
  const JXG = (await import('jsxgraph')).default;
147
- try {
204
+ safeJsx("render.applyOptions", () => {
148
205
  const opts = JXG.Options;
149
206
  if (opts) {
150
207
  opts.text = opts.text || {};
@@ -161,12 +218,12 @@ async function renderGeometrySvgFromState(jsonState) {
161
218
  opts.grid = opts.grid || {};
162
219
  opts.grid.strokeColor = palette.grid;
163
220
  }
164
- } catch {
165
- }
221
+ });
222
+ const { width, height } = containerDimsForBbox(parsed.bbox);
166
223
  const container = document.createElement("div");
167
224
  const containerId = "jxg_offscreen_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
168
225
  container.id = containerId;
169
- container.style.cssText = "position:absolute;top:-99999px;left:-99999px;width:400px;height:300px;visibility:hidden;pointer-events:none;";
226
+ container.style.cssText = `position:absolute;top:-99999px;left:-99999px;width:${width}px;height:${height}px;visibility:hidden;pointer-events:none;`;
170
227
  document.body.appendChild(container);
171
228
  let board = null;
172
229
  try {
@@ -176,24 +233,30 @@ async function renderGeometrySvgFromState(jsonState) {
176
233
  grid: !!parsed.showGrid,
177
234
  showCopyright: false,
178
235
  showNavigation: false,
179
- keepAspectRatio: false
236
+ keepAspectRatio: true
180
237
  });
181
238
  deserializeIntoBoard(board, parsed, { palette });
182
239
  board.update();
183
240
  return renderGeometryToSvg(container);
184
241
  } finally {
185
- try {
242
+ safeJsx("render.freeBoard", () => {
186
243
  if (board) JXG.JSXGraph.freeBoard(board);
187
- } catch {
188
- }
244
+ });
189
245
  if (container.parentNode) container.parentNode.removeChild(container);
190
246
  }
191
247
  }
248
+ var PIXELS_PER_UNIT, MIN_DIM, MAX_DIM, FALLBACK_W, FALLBACK_H;
192
249
  var init_render = __esm({
193
250
  "src/stamps/geometry-2d/render.ts"() {
194
251
  init_renderInline();
195
252
  init_serialize();
196
253
  init_theme();
254
+ init_safeJsx();
255
+ PIXELS_PER_UNIT = 20;
256
+ MIN_DIM = 100;
257
+ MAX_DIM = 1200;
258
+ FALLBACK_W = 400;
259
+ FALLBACK_H = 300;
197
260
  }
198
261
  });
199
262
 
@@ -290,8 +353,12 @@ function letterForGroup(g) {
290
353
  }
291
354
  function objKind(obj) {
292
355
  if (!obj) return "other";
356
+ const ec = typeof obj.elementClass === "number" ? obj.elementClass : null;
357
+ if (ec === 1) return "point";
358
+ if (ec === 2) return "line";
359
+ if (ec === 3) return "circle";
293
360
  const e = (obj.elType || obj.type || "").toString().toLowerCase();
294
- if (e === "point" || e === "glider" || e === "midpoint") return "point";
361
+ if (e === "point" || e === "glider" || e === "midpoint" || e === "intersection" || e === "otherintersection" || e === "reflection" || e === "mirrorpoint" || e === "mirrorelement" || e === "orthogonalprojection" || e === "parallelpoint") return "point";
295
362
  if (e === "line" || e === "segment" || e === "arrow" || e === "axis" || e === "normal" || e === "parallel" || e === "perpendicular" || e === "tangent" || e === "bisector" || e === "perpendicularsegment") return "line";
296
363
  if (e === "circle" || e === "circumcircle") return "circle";
297
364
  return "other";
@@ -491,7 +558,7 @@ function handleDown(ctx, e) {
491
558
  if (!sc) return;
492
559
  const [sx, sy] = sc;
493
560
  const hits2 = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y);
494
- const obj = hits2.find((o) => objKind(o) === "point") ?? hits2[0] ?? ctx.findNearestPoint(e, 12);
561
+ const obj = hits2.find((o) => objKind(o) === "point") ?? ctx.findNearestPoint(e, 12) ?? hits2[0];
495
562
  if (obj) {
496
563
  const shift = !!(e.shiftKey || e.altKey);
497
564
  ctx.toggleSelect(obj, shift);
@@ -529,14 +596,8 @@ function handleDown(ctx, e) {
529
596
  const tmp1 = ctx.boardRef.current.create("intersection", [a, b, 1], { visible: false, withLabel: false });
530
597
  const d0 = Math.hypot((tmp0.X?.() ?? 0) - x, (tmp0.Y?.() ?? 0) - y);
531
598
  const d1 = Math.hypot((tmp1.X?.() ?? 0) - x, (tmp1.Y?.() ?? 0) - y);
532
- try {
533
- ctx.boardRef.current.removeObject(tmp0);
534
- } catch {
535
- }
536
- try {
537
- ctx.boardRef.current.removeObject(tmp1);
538
- } catch {
539
- }
599
+ safeJsx("handlers.removeObject(intersect.tmp0)", () => ctx.boardRef.current.removeObject(tmp0));
600
+ safeJsx("handlers.removeObject(intersect.tmp1)", () => ctx.boardRef.current.removeObject(tmp1));
540
601
  const idx = d0 <= d1 ? 0 : 1;
541
602
  ctx.create("intersection", [aId, bId, idx], attrs);
542
603
  }
@@ -573,7 +634,7 @@ function handleDown(ctx, e) {
573
634
  })();
574
635
  if (ctx.pendingRef.current.length > 0 && ctx.boardRef.current) {
575
636
  const prev = ctx.pendingRef.current[ctx.pendingRef.current.length - 1];
576
- try {
637
+ safeJsx("handlers.createPreviewSegment", () => {
577
638
  const seg = ctx.boardRef.current.create("segment", [prev, pick2], {
578
639
  strokeColor: "#3b82f6",
579
640
  strokeWidth: 1.5,
@@ -583,8 +644,7 @@ function handleDown(ctx, e) {
583
644
  withLabel: false
584
645
  });
585
646
  ctx.previewSegRef.current.push(seg);
586
- } catch {
587
- }
647
+ });
588
648
  }
589
649
  ctx.pendingRef.current.push(pick2);
590
650
  ctx.setPendingCount(ctx.pendingRef.current.length);
@@ -695,10 +755,7 @@ function handleUp(ctx, e) {
695
755
  if (!sc2) return;
696
756
  const [ex, ey] = sc2;
697
757
  if (mq.rect) {
698
- try {
699
- ctx.boardRef.current?.removeObject(mq.rect);
700
- } catch {
701
- }
758
+ safeJsx("handlers.removeObject(marquee.rect)", () => ctx.boardRef.current?.removeObject(mq.rect));
702
759
  }
703
760
  if (Math.hypot(ex - mq.startSx, ey - mq.startSy) < 4) return;
704
761
  const x1 = Math.min(mq.startSx, ex), x2 = Math.max(mq.startSx, ex);
@@ -731,10 +788,7 @@ function handleUp(ctx, e) {
731
788
  }
732
789
  }
733
790
  ctx.setSelectionTick((tt) => tt + 1);
734
- try {
735
- board.update();
736
- } catch {
737
- }
791
+ safeJsx("handlers.board.update(marquee)", () => board.update());
738
792
  return;
739
793
  }
740
794
  if (t !== "move") return;
@@ -747,7 +801,7 @@ function handleUp(ctx, e) {
747
801
  const moved = Math.hypot(sx - start.sx, sy - start.sy);
748
802
  if (moved > 4) return;
749
803
  const hits = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y);
750
- const best = hits.find((o) => objKind(o) === "point") ?? hits[0] ?? ctx.findNearestPoint(e, 12);
804
+ const best = hits.find((o) => objKind(o) === "point") ?? ctx.findNearestPoint(e, 12) ?? hits[0];
751
805
  if (!best) {
752
806
  ctx.lastMoveClickRef.current = { obj: null, time: 0 };
753
807
  return;
@@ -781,12 +835,9 @@ function handleMove(ctx, e) {
781
835
  const [x2u, y2u] = ux2 && ux2.length >= 2 ? [ux2[0], ux2[1]] : toUsr(Math.max(startSx, sx), Math.max(startSy, sy));
782
836
  const rect = ctx.marqueeRef.current.rect;
783
837
  if (rect) {
784
- try {
785
- ctx.boardRef.current.removeObject(rect);
786
- } catch {
787
- }
838
+ safeJsx("handlers.removeObject(marquee.prevRect)", () => ctx.boardRef.current.removeObject(rect));
788
839
  }
789
- try {
840
+ safeJsx("handlers.createMarqueePolygon", () => {
790
841
  ctx.marqueeRef.current.rect = ctx.boardRef.current.create("polygon", [
791
842
  [x1u, y1u],
792
843
  [x2u, y1u],
@@ -801,8 +852,7 @@ function handleMove(ctx, e) {
801
852
  highlight: false,
802
853
  withLabel: false
803
854
  });
804
- } catch {
805
- }
855
+ });
806
856
  }
807
857
  return;
808
858
  }
@@ -812,20 +862,20 @@ function handleMove(ctx, e) {
812
862
  ctx.previewRafRef.current = requestAnimationFrame(() => {
813
863
  ctx.previewRafRef.current = null;
814
864
  if (!ctx.boardRef.current || !ctx.phantomRef.current) return;
815
- try {
865
+ safeJsx("handlers.phantomMove", () => {
816
866
  const coords = ctx.boardRef.current.getUsrCoordsOfMouse(e);
817
867
  const JXG = ctx.jxgRef.current;
818
868
  if (!JXG) return;
819
869
  ctx.phantomRef.current.setPositionDirectly(JXG.COORDS_BY_USER, [coords[0], coords[1]]);
820
870
  ctx.boardRef.current.update();
821
- } catch {
822
- }
871
+ });
823
872
  });
824
873
  }
825
874
  var init_handlers = __esm({
826
875
  "src/stamps/geometry-2d/editor/handlers.ts"() {
827
876
  init_tools();
828
877
  init_transforms();
878
+ init_safeJsx();
829
879
  }
830
880
  });
831
881
  var JSXGraphMiniBoard;
@@ -836,6 +886,7 @@ var init_MiniBoard = __esm({
836
886
  init_tools();
837
887
  init_theme();
838
888
  init_handlers();
889
+ init_safeJsx();
839
890
  JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
840
891
  const isDarkRef = react.useRef(!!isDark);
841
892
  isDarkRef.current = !!isDark;
@@ -845,6 +896,7 @@ var init_MiniBoard = __esm({
845
896
  const jxgRef = react.useRef(null);
846
897
  const axisObjsRef = react.useRef({});
847
898
  const creationLogRef = react.useRef([]);
899
+ const redoStackRef = react.useRef([]);
848
900
  const [tool, setTool] = react.useState("move");
849
901
  const toolRef = react.useRef("move");
850
902
  toolRef.current = tool;
@@ -888,19 +940,31 @@ var init_MiniBoard = __esm({
888
940
  const nextLocalId = react.useCallback(() => "j" + creationLogRef.current.length, []);
889
941
  const resolveArgs = react.useCallback((args) => {
890
942
  return args.map((a) => {
891
- if (typeof a === "string" && objMapRef.current.has(a)) {
892
- return objMapRef.current.get(a);
943
+ if (typeof a === "string") {
944
+ if (objMapRef.current.has(a)) return objMapRef.current.get(a);
945
+ const m = /^(.+):border:(\d+)$/.exec(a);
946
+ if (m) {
947
+ const poly = objMapRef.current.get(m[1]);
948
+ const idx = parseInt(m[2], 10);
949
+ if (poly && Array.isArray(poly.borders) && poly.borders[idx]) {
950
+ return poly.borders[idx];
951
+ }
952
+ }
893
953
  }
894
954
  return a;
895
955
  });
896
956
  }, []);
957
+ const pushCreationLog = react.useCallback((entry) => {
958
+ creationLogRef.current.push(entry);
959
+ redoStackRef.current = [];
960
+ }, []);
897
961
  const pushLog = react.useCallback(
898
962
  (id, type, args, attrs, obj) => {
899
- creationLogRef.current.push({ id, type, args, attrs });
963
+ pushCreationLog({ id, type, args, attrs });
900
964
  objMapRef.current.set(id, obj);
901
965
  setHistoryTick((t) => t + 1);
902
966
  },
903
- []
967
+ [pushCreationLog]
904
968
  );
905
969
  const create = react.useCallback(
906
970
  (type, args, attrs = {}) => {
@@ -915,15 +979,27 @@ var init_MiniBoard = __esm({
915
979
  [nextLocalId, resolveArgs, pushLog]
916
980
  );
917
981
  const localIdOf = react.useCallback((obj) => {
982
+ if (!obj) return null;
918
983
  for (const [id, o] of objMapRef.current.entries()) {
919
984
  if (o === obj) return id;
920
985
  }
986
+ for (const [id, o] of objMapRef.current.entries()) {
987
+ const borders = o?.borders;
988
+ if (Array.isArray(borders)) {
989
+ const idx = borders.indexOf(obj);
990
+ if (idx >= 0) return `${id}:border:${idx}`;
991
+ }
992
+ }
921
993
  return null;
922
994
  }, []);
923
995
  const snapshotObject = react.useCallback((obj, anchorScreen) => {
924
996
  const o = obj;
925
997
  const k = objKind(o);
926
998
  if (k !== "point" && k !== "line" && k !== "circle") return null;
999
+ for (const owner of objMapRef.current.values()) {
1000
+ const borders = owner?.borders;
1001
+ if (Array.isArray(borders) && borders.indexOf(o) >= 0) return null;
1002
+ }
927
1003
  const v = o.visProp ?? {};
928
1004
  const showLabel = v.withlabel !== false;
929
1005
  const showValue = valueLabelsRef.current.has(o);
@@ -983,16 +1059,10 @@ var init_MiniBoard = __esm({
983
1059
  if (patch.remove) {
984
1060
  const vl = valueLabelsRef.current.get(o);
985
1061
  if (vl) {
986
- try {
987
- boardRef.current.removeObject(vl);
988
- } catch {
989
- }
1062
+ safeJsx("MiniBoard.removeObject(valueLabel)", () => boardRef.current.removeObject(vl));
990
1063
  valueLabelsRef.current.delete(o);
991
1064
  }
992
- try {
993
- boardRef.current.removeObject(o);
994
- } catch {
995
- }
1065
+ safeJsx("MiniBoard.removeObject(target)", () => boardRef.current.removeObject(o));
996
1066
  const board = boardRef.current;
997
1067
  const aliveIds = /* @__PURE__ */ new Set();
998
1068
  for (const [id, obj2] of objMapRef.current.entries()) {
@@ -1017,7 +1087,7 @@ var init_MiniBoard = __esm({
1017
1087
  const targetId = localIdOf(o);
1018
1088
  if (targetId) {
1019
1089
  const id = nextLocalId();
1020
- creationLogRef.current.push({ id, type: "valueLabel", args: [targetId], attrs: {} });
1090
+ pushCreationLog({ id, type: "valueLabel", args: [targetId], attrs: {} });
1021
1091
  objMapRef.current.set(id, txt);
1022
1092
  setHistoryTick((t) => t + 1);
1023
1093
  }
@@ -1026,10 +1096,7 @@ var init_MiniBoard = __esm({
1026
1096
  const txt = valueLabelsRef.current.get(o);
1027
1097
  valueLabelsRef.current.delete(o);
1028
1098
  if (txt) {
1029
- try {
1030
- boardRef.current.removeObject(txt);
1031
- } catch {
1032
- }
1099
+ safeJsx("MiniBoard.removeObject(valueLabel.text)", () => boardRef.current.removeObject(txt));
1033
1100
  const txtId = localIdOf(txt);
1034
1101
  if (txtId) {
1035
1102
  creationLogRef.current = creationLogRef.current.filter((e) => e.id !== txtId);
@@ -1040,10 +1107,7 @@ var init_MiniBoard = __esm({
1040
1107
  }
1041
1108
  }
1042
1109
  if (patch.attrs) {
1043
- try {
1044
- o.setAttribute(patch.attrs);
1045
- } catch {
1046
- }
1110
+ safeJsx("MiniBoard.setAttribute", () => o.setAttribute(patch.attrs));
1047
1111
  const id = localIdOf(o);
1048
1112
  if (id) {
1049
1113
  const entry = creationLogRef.current.find((e) => e.id === id);
@@ -1051,19 +1115,13 @@ var init_MiniBoard = __esm({
1051
1115
  setHistoryTick((t) => t + 1);
1052
1116
  }
1053
1117
  }
1054
- try {
1055
- boardRef.current.update();
1056
- } catch {
1057
- }
1118
+ safeJsx("MiniBoard.board.update(mutate)", () => boardRef.current.update());
1058
1119
  }, [createValueLabelFor, localIdOf, nextLocalId]);
1059
1120
  const clearPreviewSegs = react.useCallback(() => {
1060
1121
  const b = boardRef.current;
1061
1122
  if (!b) return;
1062
1123
  for (const s of previewSegRef.current) {
1063
- try {
1064
- b.removeObject(s);
1065
- } catch {
1066
- }
1124
+ safeJsx("MiniBoard.removeObject(previewSeg)", () => b.removeObject(s));
1067
1125
  }
1068
1126
  previewSegRef.current = [];
1069
1127
  }, []);
@@ -1071,17 +1129,11 @@ var init_MiniBoard = __esm({
1071
1129
  const b = boardRef.current;
1072
1130
  if (!b) return;
1073
1131
  if (previewShapeRef.current) {
1074
- try {
1075
- b.removeObject(previewShapeRef.current);
1076
- } catch {
1077
- }
1132
+ safeJsx("MiniBoard.removeObject(previewShape)", () => b.removeObject(previewShapeRef.current));
1078
1133
  previewShapeRef.current = null;
1079
1134
  }
1080
1135
  if (phantomRef.current) {
1081
- try {
1082
- b.removeObject(phantomRef.current);
1083
- } catch {
1084
- }
1136
+ safeJsx("MiniBoard.removeObject(phantom)", () => b.removeObject(phantomRef.current));
1085
1137
  phantomRef.current = null;
1086
1138
  }
1087
1139
  }, []);
@@ -1093,7 +1145,7 @@ var init_MiniBoard = __esm({
1093
1145
  }, [clearPreviewSegs, removePhantom]);
1094
1146
  const applySelectionStyle = react.useCallback((obj) => {
1095
1147
  if (!obj || selOriginalRef.current.has(obj)) return;
1096
- try {
1148
+ safeJsx("MiniBoard.applySelectionStyle", () => {
1097
1149
  const visProp = obj.visProp ?? {};
1098
1150
  selOriginalRef.current.set(obj, {
1099
1151
  strokeColor: visProp.strokecolor,
@@ -1105,19 +1157,17 @@ var init_MiniBoard = __esm({
1105
1157
  } else {
1106
1158
  obj.setAttribute({ strokeColor: "#06b6d4", strokeWidth: 3 });
1107
1159
  }
1108
- } catch {
1109
- }
1160
+ });
1110
1161
  }, []);
1111
1162
  const restoreSelectionStyle = react.useCallback((obj) => {
1112
1163
  const orig = selOriginalRef.current.get(obj);
1113
1164
  if (!orig) return;
1114
- try {
1165
+ safeJsx("MiniBoard.restoreSelectionStyle", () => {
1115
1166
  const attrs = {};
1116
1167
  if (orig.strokeColor !== void 0) attrs.strokeColor = orig.strokeColor;
1117
1168
  if (orig.strokeWidth !== void 0) attrs.strokeWidth = orig.strokeWidth;
1118
1169
  obj.setAttribute(attrs);
1119
- } catch {
1120
- }
1170
+ });
1121
1171
  selOriginalRef.current.delete(obj);
1122
1172
  }, []);
1123
1173
  const clearSelection = react.useCallback(() => {
@@ -1126,10 +1176,7 @@ var init_MiniBoard = __esm({
1126
1176
  }
1127
1177
  selectedSetRef.current.clear();
1128
1178
  setSelectionTick((t) => t + 1);
1129
- try {
1130
- boardRef.current?.update();
1131
- } catch {
1132
- }
1179
+ safeJsx("MiniBoard.board.update(clearSelection)", () => boardRef.current?.update());
1133
1180
  }, [restoreSelectionStyle]);
1134
1181
  const toggleSelect = react.useCallback((obj, additive) => {
1135
1182
  if (!obj) return;
@@ -1149,10 +1196,7 @@ var init_MiniBoard = __esm({
1149
1196
  }
1150
1197
  }
1151
1198
  setSelectionTick((t) => t + 1);
1152
- try {
1153
- boardRef.current?.update();
1154
- } catch {
1155
- }
1199
+ safeJsx("MiniBoard.board.update(toggleSelect)", () => boardRef.current?.update());
1156
1200
  }, [applySelectionStyle, restoreSelectionStyle]);
1157
1201
  const deleteSelected = react.useCallback(() => {
1158
1202
  const board = boardRef.current;
@@ -1160,10 +1204,7 @@ var init_MiniBoard = __esm({
1160
1204
  if (selectedSetRef.current.size === 0) return;
1161
1205
  for (const o of selectedSetRef.current) selOriginalRef.current.delete(o);
1162
1206
  for (const o of selectedSetRef.current) {
1163
- try {
1164
- board.removeObject(o);
1165
- } catch {
1166
- }
1207
+ safeJsx("MiniBoard.removeObject(selected)", () => board.removeObject(o));
1167
1208
  }
1168
1209
  selectedSetRef.current.clear();
1169
1210
  const aliveIds = /* @__PURE__ */ new Set();
@@ -1236,10 +1277,7 @@ var init_MiniBoard = __esm({
1236
1277
  const b = boardRef.current;
1237
1278
  if (!b) return;
1238
1279
  if (previewShapeRef.current) {
1239
- try {
1240
- b.removeObject(previewShapeRef.current);
1241
- } catch {
1242
- }
1280
+ safeJsx("MiniBoard.removeObject(refreshPreview)", () => b.removeObject(previewShapeRef.current));
1243
1281
  previewShapeRef.current = null;
1244
1282
  }
1245
1283
  const t = toolRef.current;
@@ -1358,7 +1396,7 @@ var init_MiniBoard = __esm({
1358
1396
  }
1359
1397
  case "toggleLabel": {
1360
1398
  const obj = picks[0];
1361
- try {
1399
+ safeJsx("MiniBoard.toggleLabel", () => {
1362
1400
  if (obj.label) {
1363
1401
  const visible = obj.label.visProp.visible !== false;
1364
1402
  obj.label.setAttribute({ visible: !visible });
@@ -1367,23 +1405,21 @@ var init_MiniBoard = __esm({
1367
1405
  obj.setAttribute({ withLabel: !cur });
1368
1406
  }
1369
1407
  boardRef.current.update();
1370
- } catch {
1371
- }
1408
+ });
1372
1409
  break;
1373
1410
  }
1374
1411
  case "toggleVisible": {
1375
1412
  const obj = picks[0];
1376
- try {
1413
+ safeJsx("MiniBoard.toggleVisible", () => {
1377
1414
  const visible = obj.visProp.visible !== false;
1378
1415
  obj.setAttribute({ visible: !visible });
1379
1416
  boardRef.current.update();
1380
- } catch {
1381
- }
1417
+ });
1382
1418
  break;
1383
1419
  }
1384
1420
  case "delete": {
1385
1421
  const obj = picks[0];
1386
- try {
1422
+ safeJsx("MiniBoard.deleteOne", () => {
1387
1423
  boardRef.current.removeObject(obj);
1388
1424
  const board = boardRef.current;
1389
1425
  const aliveIds = /* @__PURE__ */ new Set();
@@ -1398,8 +1434,7 @@ var init_MiniBoard = __esm({
1398
1434
  if (!aliveIds.has(id)) objMapRef.current.delete(id);
1399
1435
  }
1400
1436
  setHistoryTick((t) => t + 1);
1401
- } catch {
1402
- }
1437
+ });
1403
1438
  break;
1404
1439
  }
1405
1440
  }
@@ -1434,7 +1469,7 @@ var init_MiniBoard = __esm({
1434
1469
  }
1435
1470
  const stepId = nextLocalId();
1436
1471
  const stepObj = boardRef.current.create("transform", step.params, step.attrs);
1437
- creationLogRef.current.push({ id: stepId, type: "transform", args: stepLogArgs, attrs: step.attrs });
1472
+ pushCreationLog({ id: stepId, type: "transform", args: stepLogArgs, attrs: step.attrs });
1438
1473
  objMapRef.current.set(stepId, stepObj);
1439
1474
  transformObjs.push(stepObj);
1440
1475
  transformIds.push(stepId);
@@ -1448,7 +1483,7 @@ var init_MiniBoard = __esm({
1448
1483
  const newName = srcName ? `${srcName}'` : nextLabel();
1449
1484
  const attrs = { name: newName, size: 3, color: "#0ea5e9", strokeColor: "#0ea5e9", fillColor: "#0ea5e9" };
1450
1485
  const obj = boardRef.current.create("point", [src, transformParent], attrs);
1451
- creationLogRef.current.push({ id, type: "point", args: [srcId ?? src, transformLogRef], attrs });
1486
+ pushCreationLog({ id, type: "point", args: [srcId ?? src, transformLogRef], attrs });
1452
1487
  objMapRef.current.set(id, obj);
1453
1488
  return obj;
1454
1489
  });
@@ -1479,6 +1514,45 @@ var init_MiniBoard = __esm({
1479
1514
  }
1480
1515
  setHistoryTick((t) => t + 1);
1481
1516
  }, [create, flashWarn, localIdOf, nextLabel, nextLocalId]);
1517
+ const recreateFromLogEntry = react.useCallback((el) => {
1518
+ const board = boardRef.current;
1519
+ if (!board) return false;
1520
+ const idMap = objMapRef.current;
1521
+ const resolve = (a) => {
1522
+ if (typeof a === "string") {
1523
+ if (idMap.has(a)) return idMap.get(a);
1524
+ const m = /^(.+):border:(\d+)$/.exec(a);
1525
+ if (m) {
1526
+ const poly = idMap.get(m[1]);
1527
+ const idx = parseInt(m[2], 10);
1528
+ if (poly && Array.isArray(poly.borders) && poly.borders[idx]) {
1529
+ return poly.borders[idx];
1530
+ }
1531
+ }
1532
+ }
1533
+ if (Array.isArray(a)) return a.map(resolve);
1534
+ return a;
1535
+ };
1536
+ const resolved = el.args.map(resolve);
1537
+ try {
1538
+ if (el.type === "valueLabel") {
1539
+ const target = resolved[0];
1540
+ if (!target) return false;
1541
+ const txt = createValueLabelFor(target);
1542
+ if (!txt) return false;
1543
+ idMap.set(el.id, txt);
1544
+ valueLabelsRef.current.set(target, txt);
1545
+ return true;
1546
+ }
1547
+ const themedAttrs = resolveAttrColors({ ...el.attrs }, paletteFor(isDarkRef.current));
1548
+ const obj = board.create(el.type, resolved, themedAttrs);
1549
+ idMap.set(el.id, obj);
1550
+ return true;
1551
+ } catch (err) {
1552
+ console.warn("Recreate failed for", el.type, err);
1553
+ return false;
1554
+ }
1555
+ }, [createValueLabelFor]);
1482
1556
  const undoLast = react.useCallback(() => {
1483
1557
  const b = boardRef.current;
1484
1558
  if (!b) return;
@@ -1488,21 +1562,31 @@ var init_MiniBoard = __esm({
1488
1562
  const obj = objMapRef.current.get(last.id);
1489
1563
  objMapRef.current.delete(last.id);
1490
1564
  if (obj) {
1491
- try {
1492
- b.removeObject(obj);
1493
- } catch {
1494
- }
1565
+ safeJsx("MiniBoard.removeObject(undo)", () => b.removeObject(obj));
1495
1566
  clearPending();
1567
+ redoStackRef.current.push(last);
1496
1568
  setHistoryTick((t) => t + 1);
1497
- try {
1498
- b.update();
1499
- } catch {
1500
- }
1569
+ safeJsx("MiniBoard.board.update(undo)", () => b.update());
1501
1570
  return;
1502
1571
  }
1503
1572
  }
1504
1573
  setHistoryTick((t) => t + 1);
1505
1574
  }, [clearPending]);
1575
+ const redoNext = react.useCallback(() => {
1576
+ const b = boardRef.current;
1577
+ if (!b) return;
1578
+ const entry = redoStackRef.current.pop();
1579
+ if (!entry) {
1580
+ setHistoryTick((t) => t + 1);
1581
+ return;
1582
+ }
1583
+ const ok = recreateFromLogEntry(entry);
1584
+ if (ok) {
1585
+ creationLogRef.current.push(entry);
1586
+ }
1587
+ setHistoryTick((t) => t + 1);
1588
+ safeJsx("MiniBoard.board.update(redo)", () => b.update());
1589
+ }, [recreateFromLogEntry]);
1506
1590
  react.useEffect(() => {
1507
1591
  const onKey = (e) => {
1508
1592
  const ae = document.activeElement;
@@ -1514,6 +1598,13 @@ var init_MiniBoard = __esm({
1514
1598
  undoLastRef.current();
1515
1599
  return;
1516
1600
  }
1601
+ if ((e.metaKey || e.ctrlKey) && (e.key.toLowerCase() === "z" && e.shiftKey || e.key.toLowerCase() === "y" && !e.shiftKey)) {
1602
+ if (inField) return;
1603
+ e.preventDefault();
1604
+ e.stopPropagation();
1605
+ redoNextRef.current();
1606
+ return;
1607
+ }
1517
1608
  if (e.key === "Escape" && !inField) {
1518
1609
  if (pendingRef.current.length > 0) {
1519
1610
  e.preventDefault();
@@ -1560,16 +1651,14 @@ var init_MiniBoard = __esm({
1560
1651
  if (!sc) return [];
1561
1652
  const [sx, sy] = sc;
1562
1653
  const list = [];
1563
- try {
1654
+ safeJsx("MiniBoard.objectsAt.loop", () => {
1564
1655
  const objs = b.objectsList || [];
1565
1656
  for (const o of objs) {
1566
- try {
1657
+ safeJsx("MiniBoard.objectsAt.hasPoint", () => {
1567
1658
  if (o.hasPoint && o.hasPoint(sx, sy)) list.push(o);
1568
- } catch {
1569
- }
1659
+ });
1570
1660
  }
1571
- } catch {
1572
- }
1661
+ });
1573
1662
  return list;
1574
1663
  }, [screenCoordsOf]);
1575
1664
  const findNearestPoint = react.useCallback((evt, tolPx = 12) => {
@@ -1579,24 +1668,23 @@ var init_MiniBoard = __esm({
1579
1668
  if (!sc) return null;
1580
1669
  const [sx, sy] = sc;
1581
1670
  const tol2 = tolPx * tolPx;
1582
- let best = null;
1583
- try {
1671
+ const bestRef = { current: null };
1672
+ safeJsx("MiniBoard.findNearestPoint.loop", () => {
1584
1673
  const objs = b.objectsList || [];
1585
1674
  for (const o of objs) {
1586
- try {
1587
- if (objKind(o) !== "point") continue;
1675
+ safeJsx("MiniBoard.findNearestPoint.iter", () => {
1676
+ if (objKind(o) !== "point") return;
1588
1677
  const pc = o.coords?.scrCoords;
1589
- if (!pc) continue;
1678
+ if (!pc) return;
1590
1679
  const dx = pc[1] - sx;
1591
1680
  const dy = pc[2] - sy;
1592
1681
  const d2 = dx * dx + dy * dy;
1593
- if (d2 <= tol2 && (!best || d2 < best.d2)) best = { obj: o, d2 };
1594
- } catch {
1595
- }
1682
+ const cur = bestRef.current;
1683
+ if (d2 <= tol2 && (!cur || d2 < cur.d2)) bestRef.current = { obj: o, d2 };
1684
+ });
1596
1685
  }
1597
- } catch {
1598
- }
1599
- return best ? best.obj : null;
1686
+ });
1687
+ return bestRef.current ? bestRef.current.obj : null;
1600
1688
  }, [screenCoordsOf]);
1601
1689
  const promoteLabel = react.useCallback((o) => {
1602
1690
  if (!o) return o;
@@ -1604,31 +1692,25 @@ var init_MiniBoard = __esm({
1604
1692
  if (t !== "text") return o;
1605
1693
  const b = boardRef.current;
1606
1694
  if (!b) return o;
1607
- try {
1695
+ const promoted = safeJsx("MiniBoard.promoteLabel", () => {
1608
1696
  for (const c of b.objectsList || []) {
1609
1697
  if (c.label === o) return c;
1610
1698
  }
1611
- } catch {
1612
- }
1613
- return o;
1699
+ return null;
1700
+ }, null);
1701
+ return promoted ?? o;
1614
1702
  }, []);
1615
1703
  const pendingTransformRef = react.useRef(null);
1616
1704
  const transformSubsRef = react.useRef(/* @__PURE__ */ new Set());
1617
1705
  const emitTransform = react.useCallback((info) => {
1618
1706
  transformSubsRef.current.forEach((cb) => {
1619
- try {
1620
- cb(info);
1621
- } catch {
1622
- }
1707
+ safeJsx("MiniBoard.emitTransform.cb", () => cb(info));
1623
1708
  });
1624
1709
  }, []);
1625
1710
  const selectSubsRef = react.useRef(/* @__PURE__ */ new Set());
1626
1711
  const emitSelect = react.useCallback((snap) => {
1627
1712
  selectSubsRef.current.forEach((cb) => {
1628
- try {
1629
- cb(snap);
1630
- } catch {
1631
- }
1713
+ safeJsx("MiniBoard.emitSelect.cb", () => cb(snap));
1632
1714
  });
1633
1715
  }, []);
1634
1716
  const moveDownRef = react.useRef(null);
@@ -1640,7 +1722,7 @@ var init_MiniBoard = __esm({
1640
1722
  const JXG = (await import('jsxgraph')).default;
1641
1723
  if (cancelled || !containerRef.current) return;
1642
1724
  jxgRef.current = JXG;
1643
- try {
1725
+ safeJsx("MiniBoard.applyJxgOptions", () => {
1644
1726
  const opts = JXG.Options;
1645
1727
  if (opts) {
1646
1728
  opts.text = opts.text || {};
@@ -1653,8 +1735,7 @@ var init_MiniBoard = __esm({
1653
1735
  opts.label.strokeColor = themeLabel(isDarkRef.current);
1654
1736
  opts.text.strokeColor = themeLabel(isDarkRef.current);
1655
1737
  }
1656
- } catch {
1657
- }
1738
+ });
1658
1739
  const board = JXG.JSXGraph.initBoard(containerId, {
1659
1740
  boundingbox: initialState?.bbox ?? [-10, 10, 10, -10],
1660
1741
  axis: false,
@@ -1675,43 +1756,20 @@ var init_MiniBoard = __esm({
1675
1756
  });
1676
1757
  boardRef.current = board;
1677
1758
  if (initialState && initialState.elements.length > 0) {
1678
- const idMap = objMapRef.current;
1679
1759
  for (const el of initialState.elements) {
1680
- const resolved = el.args.map((a) => typeof a === "string" && idMap.has(a) ? idMap.get(a) : a);
1681
- try {
1682
- if (el.type === "valueLabel") {
1683
- const target = resolved[0];
1684
- if (target) {
1685
- const txt = createValueLabelFor(target);
1686
- if (txt) {
1687
- idMap.set(el.id, txt);
1688
- valueLabelsRef.current.set(target, txt);
1689
- }
1690
- }
1691
- continue;
1692
- }
1693
- const themedAttrs = resolveAttrColors({ ...el.attrs }, paletteFor(isDarkRef.current));
1694
- const obj = board.create(el.type, resolved, themedAttrs);
1695
- idMap.set(el.id, obj);
1696
- } catch (err) {
1697
- console.warn("Replay failed for", el.type, err);
1698
- }
1760
+ recreateFromLogEntry(el);
1699
1761
  }
1700
1762
  creationLogRef.current = [...initialState.elements];
1701
1763
  labelIdxRef.current = initialState.elements.filter((e) => e.type === "point").length;
1702
1764
  }
1703
1765
  if (showAxisRef.current) {
1704
- try {
1766
+ safeJsx("MiniBoard.initAxes", () => {
1705
1767
  axisObjsRef.current.x = board.create("axis", [[0, 0], [1, 0]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1706
1768
  axisObjsRef.current.y = board.create("axis", [[0, 0], [0, 1]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1707
- } catch {
1708
- }
1769
+ });
1709
1770
  }
1710
1771
  if (showGridRef.current) {
1711
- try {
1712
- board.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 });
1713
- } catch {
1714
- }
1772
+ safeJsx("MiniBoard.initGrid", () => board.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 }));
1715
1773
  }
1716
1774
  board.on("down", (e) => {
1717
1775
  const ctx = {
@@ -1862,6 +1920,8 @@ var init_MiniBoard = __esm({
1862
1920
  setShowGrid: (b) => setShowGridRef.current(b),
1863
1921
  undo: () => undoLastRef.current(),
1864
1922
  canUndo: () => creationLogRef.current.length > 0,
1923
+ redo: () => redoNextRef.current(),
1924
+ canRedo: () => redoStackRef.current.length > 0,
1865
1925
  subscribe: (cb) => {
1866
1926
  subscribersRef.current.add(cb);
1867
1927
  return () => {
@@ -1874,15 +1934,14 @@ var init_MiniBoard = __esm({
1874
1934
  const b = boardRef.current;
1875
1935
  if (!b) return [];
1876
1936
  const out = [];
1877
- try {
1937
+ safeJsx("MiniBoard.getAllPointNames", () => {
1878
1938
  const objs = b.objectsList || [];
1879
1939
  for (const o of objs) {
1880
1940
  if (objKind(o) === "point" && typeof o.name === "string" && o.name) {
1881
1941
  out.push(o.name);
1882
1942
  }
1883
1943
  }
1884
- } catch {
1885
- }
1944
+ });
1886
1945
  return out;
1887
1946
  },
1888
1947
  onSelect: (cb) => {
@@ -1943,10 +2002,7 @@ var init_MiniBoard = __esm({
1943
2002
  previewRafRef.current = null;
1944
2003
  }
1945
2004
  if (boardRef.current && jxgRef.current) {
1946
- try {
1947
- jxgRef.current.JSXGraph.freeBoard(boardRef.current);
1948
- } catch {
1949
- }
2005
+ safeJsx("MiniBoard.freeBoard", () => jxgRef.current.JSXGraph.freeBoard(boardRef.current));
1950
2006
  boardRef.current = null;
1951
2007
  }
1952
2008
  };
@@ -1954,19 +2010,13 @@ var init_MiniBoard = __esm({
1954
2010
  react.useEffect(() => {
1955
2011
  const b = boardRef.current;
1956
2012
  if (!b) return;
1957
- try {
2013
+ safeJsx("MiniBoard.toggleAxis", () => {
1958
2014
  if (axisObjsRef.current.x) {
1959
- try {
1960
- b.removeObject(axisObjsRef.current.x);
1961
- } catch {
1962
- }
2015
+ safeJsx("MiniBoard.removeObject(axisX)", () => b.removeObject(axisObjsRef.current.x));
1963
2016
  axisObjsRef.current.x = void 0;
1964
2017
  }
1965
2018
  if (axisObjsRef.current.y) {
1966
- try {
1967
- b.removeObject(axisObjsRef.current.y);
1968
- } catch {
1969
- }
2019
+ safeJsx("MiniBoard.removeObject(axisY)", () => b.removeObject(axisObjsRef.current.y));
1970
2020
  axisObjsRef.current.y = void 0;
1971
2021
  }
1972
2022
  if (showAxis) {
@@ -1974,28 +2024,23 @@ var init_MiniBoard = __esm({
1974
2024
  axisObjsRef.current.y = b.create("axis", [[0, 0], [0, 1]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1975
2025
  }
1976
2026
  b.update();
1977
- } catch {
1978
- }
2027
+ });
1979
2028
  }, [showAxis]);
1980
2029
  react.useEffect(() => {
1981
2030
  const b = boardRef.current;
1982
2031
  if (!b) return;
1983
- try {
2032
+ safeJsx("MiniBoard.toggleGrid", () => {
1984
2033
  const objs = Object.values(b.objects || {});
1985
2034
  for (const o of objs) {
1986
2035
  if (o && (o.elType === "grid" || o.type === "grid" || o.visProp && o.visProp.type === "grid")) {
1987
- try {
1988
- b.removeObject(o);
1989
- } catch {
1990
- }
2036
+ safeJsx("MiniBoard.removeObject(grid)", () => b.removeObject(o));
1991
2037
  }
1992
2038
  }
1993
2039
  if (showGrid) {
1994
2040
  b.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 });
1995
2041
  }
1996
2042
  b.update();
1997
- } catch {
1998
- }
2043
+ });
1999
2044
  }, [showGrid]);
2000
2045
  const handleToolChange = react.useCallback((t) => {
2001
2046
  clearPending();
@@ -2003,10 +2048,9 @@ var init_MiniBoard = __esm({
2003
2048
  setTool(t);
2004
2049
  const b = boardRef.current;
2005
2050
  if (b) {
2006
- try {
2051
+ safeJsx("MiniBoard.setPanForTool", () => {
2007
2052
  if (b.attr?.pan) b.attr.pan.enabled = t !== "select";
2008
- } catch {
2009
- }
2053
+ });
2010
2054
  }
2011
2055
  }, [clearPending]);
2012
2056
  const handleToolChangeRef = react.useRef(handleToolChange);
@@ -2014,10 +2058,7 @@ var init_MiniBoard = __esm({
2014
2058
  const subscribersRef = react.useRef(/* @__PURE__ */ new Set());
2015
2059
  const notifySubscribers = react.useCallback(() => {
2016
2060
  subscribersRef.current.forEach((cb) => {
2017
- try {
2018
- cb();
2019
- } catch {
2020
- }
2061
+ safeJsx("MiniBoard.notifySubscriber.cb", () => cb());
2021
2062
  });
2022
2063
  }, []);
2023
2064
  react.useEffect(() => {
@@ -2025,6 +2066,8 @@ var init_MiniBoard = __esm({
2025
2066
  }, [tool, showAxis, showGrid, historyTick, notifySubscribers]);
2026
2067
  const undoLastRef = react.useRef(undoLast);
2027
2068
  undoLastRef.current = undoLast;
2069
+ const redoNextRef = react.useRef(redoNext);
2070
+ redoNextRef.current = redoNext;
2028
2071
  const clearPendingRef = react.useRef(clearPending);
2029
2072
  clearPendingRef.current = clearPending;
2030
2073
  const finalizeTransformCreateRef = react.useRef(finalizeTransformCreate);
@@ -2135,6 +2178,7 @@ function MobileToolDrawer({
2135
2178
  disabled: a.disabled,
2136
2179
  "aria-label": a.label,
2137
2180
  title: a.title ?? a.label,
2181
+ "data-testid": a.testId,
2138
2182
  className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
2139
2183
  children: a.icon
2140
2184
  },
@@ -2240,6 +2284,12 @@ function UndoIcon() {
2240
2284
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
2241
2285
  ] });
2242
2286
  }
2287
+ function RedoIcon() {
2288
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2289
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "21 7 21 13 15 13" }),
2290
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20.49 13a9 9 0 1 1-2.13-9.36L21 7" })
2291
+ ] });
2292
+ }
2243
2293
  function AxisIcon() {
2244
2294
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
2245
2295
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
@@ -2284,7 +2334,7 @@ function useToolHoverTooltip() {
2284
2334
  return { hover, portalReady, showHover, hideHover };
2285
2335
  }
2286
2336
  function DesktopGeometryPanel(props) {
2287
- const { activeTool, onToolChange, showAxis, showGrid, onShowAxisChange, onShowGridChange, onUndo, canUndo, onClose, isDark, chordGroup } = props;
2337
+ const { activeTool, onToolChange, showAxis, showGrid, onShowAxisChange, onShowGridChange, onUndo, canUndo, onRedo, canRedo, onClose, isDark, chordGroup } = props;
2288
2338
  const grouped = react.useMemo(() => {
2289
2339
  return TOOLS.reduce((acc, t) => {
2290
2340
  var _a;
@@ -2333,9 +2383,23 @@ function DesktopGeometryPanel(props) {
2333
2383
  disabled: !canUndo,
2334
2384
  title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
2335
2385
  "aria-label": "Ho\xE0n t\xE1c",
2386
+ "data-testid": "undo-btn",
2336
2387
  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",
2337
2388
  children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {})
2338
2389
  }
2390
+ ),
2391
+ /* @__PURE__ */ jsxRuntime.jsx(
2392
+ "button",
2393
+ {
2394
+ type: "button",
2395
+ onClick: onRedo,
2396
+ disabled: !canRedo,
2397
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
2398
+ "aria-label": "L\xE0m l\u1EA1i",
2399
+ "data-testid": "redo-btn",
2400
+ 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",
2401
+ children: /* @__PURE__ */ jsxRuntime.jsx(RedoIcon, {})
2402
+ }
2339
2403
  )
2340
2404
  ] }) }),
2341
2405
  groupKeys.map((group) => {
@@ -2456,6 +2520,8 @@ function MobileGeometryPanel(props) {
2456
2520
  onShowGridChange,
2457
2521
  onUndo,
2458
2522
  canUndo,
2523
+ onRedo,
2524
+ canRedo,
2459
2525
  isDark,
2460
2526
  drawerOpen,
2461
2527
  onDrawerClose
@@ -2504,6 +2570,13 @@ function MobileGeometryPanel(props) {
2504
2570
  icon: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {}),
2505
2571
  onClick: onUndo,
2506
2572
  disabled: !canUndo
2573
+ },
2574
+ {
2575
+ label: "L\xE0m l\u1EA1i",
2576
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
2577
+ icon: /* @__PURE__ */ jsxRuntime.jsx(RedoIcon, {}),
2578
+ onClick: onRedo,
2579
+ disabled: !canRedo
2507
2580
  }
2508
2581
  ],
2509
2582
  groups,
@@ -2969,10 +3042,12 @@ var init_EditorPanel = __esm({
2969
3042
  init_render();
2970
3043
  init_PropertiesPopover();
2971
3044
  init_TransformParamPopover();
3045
+ init_LeftPanel();
2972
3046
  GeometryEditorPanel = react.forwardRef(
2973
- function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark, isMobile = false, onOpenDrawer }, ref) {
3047
+ function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark, isMobile = false, onOpenDrawer, onUndo, onRedo, canUndo, canRedo }, ref) {
2974
3048
  const handleRef = react.useRef(null);
2975
3049
  const [ready, setReady] = react.useState(false);
3050
+ const [hasContent, setHasContent] = react.useState(false);
2976
3051
  const [propsPopover, setPropsPopover] = react.useState(null);
2977
3052
  const [transformPopover, setTransformPopover] = react.useState(null);
2978
3053
  const onStateChangeRef = react.useRef(onStateChange);
@@ -2981,13 +3056,16 @@ var init_EditorPanel = __esm({
2981
3056
  }, [onStateChange]);
2982
3057
  const emitState = react.useCallback(() => {
2983
3058
  const h = handleRef.current;
3059
+ if (!h) return;
3060
+ setHasContent(h.getCreationLog().length > 0);
2984
3061
  const cb = onStateChangeRef.current;
2985
- if (!h || !cb) return;
3062
+ if (!cb) return;
2986
3063
  cb({
2987
3064
  tool: h.getTool(),
2988
3065
  showAxis: h.getShowAxis(),
2989
3066
  showGrid: h.getShowGrid(),
2990
- canUndo: h.canUndo()
3067
+ canUndo: h.canUndo(),
3068
+ canRedo: h.canRedo()
2991
3069
  });
2992
3070
  }, []);
2993
3071
  const handleReady = react.useCallback((h) => {
@@ -3029,6 +3107,7 @@ var init_EditorPanel = __esm({
3029
3107
  setShowAxis: (b) => handleRef.current?.setShowAxis(b),
3030
3108
  setShowGrid: (b) => handleRef.current?.setShowGrid(b),
3031
3109
  undo: () => handleRef.current?.undo(),
3110
+ redo: () => handleRef.current?.redo(),
3032
3111
  insert: performInsert,
3033
3112
  hasContent: () => (handleRef.current?.getCreationLog().length ?? 0) > 0
3034
3113
  }), [performInsert]);
@@ -3078,17 +3157,46 @@ var init_EditorPanel = __esm({
3078
3157
  ] }),
3079
3158
  "D\u1EF1ng h\xECnh h\u1ECDc"
3080
3159
  ] }),
3081
- isMobile && /* @__PURE__ */ jsxRuntime.jsx(
3082
- "button",
3083
- {
3084
- type: "button",
3085
- onClick: handleInsert,
3086
- disabled: !ready,
3087
- "data-testid": "geometry-insert-btn-mobile",
3088
- className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
3089
- children: "Ch\xE8n"
3090
- }
3091
- ),
3160
+ isMobile && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3161
+ /* @__PURE__ */ jsxRuntime.jsx(
3162
+ "button",
3163
+ {
3164
+ type: "button",
3165
+ onClick: onUndo,
3166
+ disabled: !canUndo,
3167
+ "aria-label": "Ho\xE0n t\xE1c",
3168
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
3169
+ "data-testid": "undo-btn-mobile",
3170
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
3171
+ children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {})
3172
+ }
3173
+ ),
3174
+ /* @__PURE__ */ jsxRuntime.jsx(
3175
+ "button",
3176
+ {
3177
+ type: "button",
3178
+ onClick: onRedo,
3179
+ disabled: !canRedo,
3180
+ "aria-label": "L\xE0m l\u1EA1i",
3181
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
3182
+ "data-testid": "redo-btn-mobile",
3183
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
3184
+ children: /* @__PURE__ */ jsxRuntime.jsx(RedoIcon, {})
3185
+ }
3186
+ ),
3187
+ /* @__PURE__ */ jsxRuntime.jsx(
3188
+ "button",
3189
+ {
3190
+ type: "button",
3191
+ onClick: handleInsert,
3192
+ disabled: !ready || !hasContent,
3193
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
3194
+ "data-testid": "geometry-insert-btn-mobile",
3195
+ className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
3196
+ children: "Ch\xE8n"
3197
+ }
3198
+ )
3199
+ ] }),
3092
3200
  /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3093
3201
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
3094
3202
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
@@ -3182,7 +3290,8 @@ var init_EditorPanel = __esm({
3182
3290
  "button",
3183
3291
  {
3184
3292
  onClick: handleInsert,
3185
- disabled: !ready,
3293
+ disabled: !ready || !hasContent,
3294
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
3186
3295
  "data-testid": "geometry-insert-btn",
3187
3296
  className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
3188
3297
  children: "Ch\xE8n"
@@ -3410,7 +3519,8 @@ var init_host = __esm({
3410
3519
  tool: "move",
3411
3520
  showAxis: false,
3412
3521
  showGrid: false,
3413
- canUndo: false
3522
+ canUndo: false,
3523
+ canRedo: false
3414
3524
  };
3415
3525
  GeometryStampHost = react.forwardRef(
3416
3526
  function GeometryStampHost2({ api, editingElement, onClose, isDark }, ref) {
@@ -3476,6 +3586,8 @@ var init_host = __esm({
3476
3586
  onShowGridChange: (b) => panelRef.current?.setShowGrid(b),
3477
3587
  onUndo: () => panelRef.current?.undo(),
3478
3588
  canUndo: geomState.canUndo,
3589
+ onRedo: () => panelRef.current?.redo(),
3590
+ canRedo: geomState.canRedo,
3479
3591
  onClose,
3480
3592
  isDark,
3481
3593
  isMobile,
@@ -3495,7 +3607,11 @@ var init_host = __esm({
3495
3607
  withLeftPanel: !isMobile,
3496
3608
  isDark,
3497
3609
  isMobile,
3498
- onOpenDrawer: () => setDrawerOpen(true)
3610
+ onOpenDrawer: () => setDrawerOpen(true),
3611
+ onUndo: () => panelRef.current?.undo(),
3612
+ onRedo: () => panelRef.current?.redo(),
3613
+ canUndo: geomState.canUndo,
3614
+ canRedo: geomState.canRedo
3499
3615
  }
3500
3616
  )
3501
3617
  ] });