@xom11/whiteboard 0.25.0 → 0.28.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 (93) hide show
  1. package/dist/{ExcalidrawWithMenus-WENZRYYE.mjs → ExcalidrawWithMenus-2QPPTXJM.mjs} +3 -2
  2. package/dist/ExcalidrawWithMenus-2QPPTXJM.mjs.map +1 -0
  3. package/dist/ai.d.mts +3035 -108
  4. package/dist/ai.d.ts +3035 -108
  5. package/dist/ai.js +6780 -788
  6. package/dist/ai.js.map +1 -1
  7. package/dist/ai.mjs +5140 -577
  8. package/dist/ai.mjs.map +1 -1
  9. package/dist/catalog.json +5 -5
  10. package/dist/{chunk-NDEZJKNY.mjs → chunk-5JM35CXV.mjs} +4 -4
  11. package/dist/{chunk-NDEZJKNY.mjs.map → chunk-5JM35CXV.mjs.map} +1 -1
  12. package/dist/{chunk-VNCCIV6O.mjs → chunk-AJAHD35N.mjs} +779 -9
  13. package/dist/chunk-AJAHD35N.mjs.map +1 -0
  14. package/dist/{chunk-M42TGYT6.mjs → chunk-BNBOIDO5.mjs} +3 -3
  15. package/dist/{chunk-M42TGYT6.mjs.map → chunk-BNBOIDO5.mjs.map} +1 -1
  16. package/dist/{chunk-ONBCUWVI.mjs → chunk-BU5KLO3P.mjs} +3 -3
  17. package/dist/{chunk-ONBCUWVI.mjs.map → chunk-BU5KLO3P.mjs.map} +1 -1
  18. package/dist/{chunk-CJBLJUWG.mjs → chunk-CXHNVYMD.mjs} +4 -4
  19. package/dist/{chunk-CJBLJUWG.mjs.map → chunk-CXHNVYMD.mjs.map} +1 -1
  20. package/dist/chunk-H22OZYTW.mjs +265 -0
  21. package/dist/chunk-H22OZYTW.mjs.map +1 -0
  22. package/dist/chunk-J5LGTIGS.mjs +10 -0
  23. package/dist/chunk-J5LGTIGS.mjs.map +1 -0
  24. package/dist/{chunk-TB4CL25L.mjs → chunk-OQIQNKPQ.mjs} +206 -66
  25. package/dist/chunk-OQIQNKPQ.mjs.map +1 -0
  26. package/dist/{chunk-SGFJLHHG.mjs → chunk-PPKHCRRE.mjs} +3 -3
  27. package/dist/{chunk-SGFJLHHG.mjs.map → chunk-PPKHCRRE.mjs.map} +1 -1
  28. package/dist/{chunk-YSJOVBCD.mjs → chunk-QCZVFEN4.mjs} +4 -4
  29. package/dist/{chunk-YSJOVBCD.mjs.map → chunk-QCZVFEN4.mjs.map} +1 -1
  30. package/dist/{chunk-ESVPQWHX.mjs → chunk-QRUAEXLR.mjs} +5 -5
  31. package/dist/{chunk-ESVPQWHX.mjs.map → chunk-QRUAEXLR.mjs.map} +1 -1
  32. package/dist/{chunk-AYSFWUPK.mjs → chunk-SZDAS7LK.mjs} +79 -2
  33. package/dist/chunk-SZDAS7LK.mjs.map +1 -0
  34. package/dist/chunk-T3SOHYB2.mjs +851 -0
  35. package/dist/chunk-T3SOHYB2.mjs.map +1 -0
  36. package/dist/{chunk-I24QOHPU.mjs → chunk-V3YJ6JFL.mjs} +3 -3
  37. package/dist/{chunk-I24QOHPU.mjs.map → chunk-V3YJ6JFL.mjs.map} +1 -1
  38. package/dist/{chunk-REIJZDVZ.mjs → chunk-ZTQBUKLJ.mjs} +960 -196
  39. package/dist/chunk-ZTQBUKLJ.mjs.map +1 -0
  40. package/dist/geometry-2d.d.mts +1 -1
  41. package/dist/geometry-2d.d.ts +1 -1
  42. package/dist/geometry-2d.js +5521 -1384
  43. package/dist/geometry-2d.js.map +1 -1
  44. package/dist/geometry-2d.mjs +5 -4
  45. package/dist/geometry-3d.d.mts +1 -1
  46. package/dist/geometry-3d.d.ts +1 -1
  47. package/dist/geometry-3d.js +1351 -252
  48. package/dist/geometry-3d.js.map +1 -1
  49. package/dist/geometry-3d.mjs +4 -3
  50. package/dist/graph-2d.d.mts +1 -1
  51. package/dist/graph-2d.d.ts +1 -1
  52. package/dist/graph-2d.js +1517 -341
  53. package/dist/graph-2d.js.map +1 -1
  54. package/dist/graph-2d.mjs +7 -6
  55. package/dist/handleExtractProblem-C-U5KluK.d.mts +158 -0
  56. package/dist/handleExtractProblem-C-U5KluK.d.ts +158 -0
  57. package/dist/{host-A64ITWVX.mjs → host-2ISGVO7O.mjs} +6 -5
  58. package/dist/host-2ISGVO7O.mjs.map +1 -0
  59. package/dist/{host-L7FMFZUW.mjs → host-4P766V4J.mjs} +1363 -463
  60. package/dist/host-4P766V4J.mjs.map +1 -0
  61. package/dist/{host-QK53UYMD.mjs → host-HOSJHQ5H.mjs} +10 -9
  62. package/dist/host-HOSJHQ5H.mjs.map +1 -0
  63. package/dist/{host-QS2EOTRJ.mjs → host-ZQCDAT6O.mjs} +3 -2
  64. package/dist/host-ZQCDAT6O.mjs.map +1 -0
  65. package/dist/index.d.mts +10 -4
  66. package/dist/index.d.ts +10 -4
  67. package/dist/index.js +5746 -1603
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +26 -22
  70. package/dist/index.mjs.map +1 -1
  71. package/dist/latex.d.mts +1 -1
  72. package/dist/latex.d.ts +1 -1
  73. package/dist/latex.mjs +2 -1
  74. package/dist/render-ZX2O2IK7.mjs +10 -0
  75. package/dist/{render-3WTY7NZB.mjs.map → render-ZX2O2IK7.mjs.map} +1 -1
  76. package/dist/serialize-N4G6RFBB.mjs +9 -0
  77. package/dist/{serialize-SRJVKYUG.mjs.map → serialize-N4G6RFBB.mjs.map} +1 -1
  78. package/dist/{types-DWRyCa2m.d.ts → types-BHYC2Fiw.d.mts} +130 -1
  79. package/dist/{types-DWRyCa2m.d.mts → types-BHYC2Fiw.d.ts} +130 -1
  80. package/package.json +10 -1
  81. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +0 -1
  82. package/dist/chunk-AYSFWUPK.mjs.map +0 -1
  83. package/dist/chunk-REIJZDVZ.mjs.map +0 -1
  84. package/dist/chunk-TB4CL25L.mjs.map +0 -1
  85. package/dist/chunk-VNCCIV6O.mjs.map +0 -1
  86. package/dist/chunk-VRHWDZ66.mjs +0 -96
  87. package/dist/chunk-VRHWDZ66.mjs.map +0 -1
  88. package/dist/host-A64ITWVX.mjs.map +0 -1
  89. package/dist/host-L7FMFZUW.mjs.map +0 -1
  90. package/dist/host-QK53UYMD.mjs.map +0 -1
  91. package/dist/host-QS2EOTRJ.mjs.map +0 -1
  92. package/dist/render-3WTY7NZB.mjs +0 -9
  93. package/dist/serialize-SRJVKYUG.mjs +0 -8
@@ -1,19 +1,21 @@
1
1
  "use client";
2
2
  import { useChordShortcut } from './chunk-HNQLZIEP.mjs';
3
3
  import { useToolStateMachine } from './chunk-NVJ7K3DK.mjs';
4
- import { safeJsx, initJxgBoard, attachJxgWheelZoom, useToast, ToastHost, STAMP_PANEL_DESKTOP, ToastProvider, useStampStore, StampLeftPanel, ObjectRow } from './chunk-TB4CL25L.mjs';
5
- import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData, deserializeBoard } from './chunk-VRHWDZ66.mjs';
4
+ import { safeJsx, initJxgBoard, attachJxgWheelZoom, useToast, ToastHost, STAMP_PANEL_DESKTOP, ToastProvider, useStampStore, StampLeftPanel, ObjectRow } from './chunk-OQIQNKPQ.mjs';
5
+ import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData, deserializeBoard } from './chunk-H22OZYTW.mjs';
6
6
  import { themeLabel, paletteFor, themeAxis, themeGrid } from './chunk-R5FL6S7L.mjs';
7
- import { JxgRenderer } from './chunk-AYSFWUPK.mjs';
7
+ import { JxgRenderer } from './chunk-SZDAS7LK.mjs';
8
8
  import './chunk-ICR4CVOE.mjs';
9
- import { nextLabel, useEditorState, listObjects } from './chunk-REIJZDVZ.mjs';
9
+ import { nextLabel, useEditorState, listObjects } from './chunk-ZTQBUKLJ.mjs';
10
10
  import './chunk-IHUFOV7L.mjs';
11
- import { describeDsl, serializeState } from './chunk-VNCCIV6O.mjs';
11
+ import { validateFile, fileToImagePart, describeDsl, serializeState } from './chunk-AJAHD35N.mjs';
12
+ import { handleExtractProblem } from './chunk-T3SOHYB2.mjs';
12
13
  import { DEFAULT_VIEW_2D } from './chunk-73Q7ADVL.mjs';
13
14
  import './chunk-B4NJJZFR.mjs';
14
15
  import { useIsMobile } from './chunk-P2AOIF7S.mjs';
15
16
  import { insertStampImage } from './chunk-QGNU34T7.mjs';
16
17
  import './chunk-5UTGXHLJ.mjs';
18
+ import { __export } from './chunk-J5LGTIGS.mjs';
17
19
  import { forwardRef, useRef, useId, useState, useEffect, useCallback, useImperativeHandle, useSyncExternalStore, useMemo, useLayoutEffect } from 'react';
18
20
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
19
21
  import { createPortal } from 'react-dom';
@@ -283,6 +285,104 @@ var Icon = {
283
285
  /* @__PURE__ */ jsx("circle", { cx: "8", cy: "15.5", r: "1.7", fill: C_POINT }),
284
286
  /* @__PURE__ */ jsx("circle", { cx: "19", cy: "5.7", r: "1.9", fill: C_CONSTRUCT }),
285
287
  /* @__PURE__ */ jsx("text", { x: "10.5", y: "10.5", fontSize: "8", fontFamily: "serif", fontStyle: "italic", fontWeight: "700", fill: "currentColor", children: "k" })
288
+ ] }),
289
+ // ===== Special shapes =====
290
+ square: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
291
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "5", width: "14", height: "14", fill: C_FILL, fillOpacity: "0.18", stroke: "currentColor", strokeWidth: "1.6" }),
292
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "5", r: "1.7", fill: C_POINT }),
293
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "5", r: "1.7", fill: C_POINT })
294
+ ] }),
295
+ rectangle: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
296
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "7", width: "18", height: "10", fill: C_FILL, fillOpacity: "0.18", stroke: "currentColor", strokeWidth: "1.6" }),
297
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "17", r: "1.7", fill: C_POINT }),
298
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "17", r: "1.7", fill: C_POINT }),
299
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "7", r: "1.7", fill: C_POINT })
300
+ ] }),
301
+ rhombus: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
302
+ /* @__PURE__ */ jsx("path", { d: "M 12 3 L 21 12 L 12 21 L 3 12 Z", fill: C_FILL, fillOpacity: "0.18", stroke: "currentColor", strokeWidth: "1.6" }),
303
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "12", r: "1.7", fill: C_POINT }),
304
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "21", r: "1.7", fill: C_POINT }),
305
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "12", r: "1.7", fill: C_POINT })
306
+ ] }),
307
+ parallelogram: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
308
+ /* @__PURE__ */ jsx("path", { d: "M 5 19 L 21 19 L 19 5 L 3 5 Z", fill: C_FILL, fillOpacity: "0.18", stroke: "currentColor", strokeWidth: "1.6" }),
309
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "5", r: "1.7", fill: C_POINT }),
310
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "5", r: "1.7", fill: C_POINT }),
311
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "19", r: "1.7", fill: C_POINT })
312
+ ] }),
313
+ isoTrapezoid: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
314
+ /* @__PURE__ */ jsx("path", { d: "M 3 19 L 21 19 L 17 5 L 7 5 Z", fill: C_FILL, fillOpacity: "0.18", stroke: "currentColor", strokeWidth: "1.6" }),
315
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "19", r: "1.7", fill: C_POINT }),
316
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "19", r: "1.7", fill: C_POINT }),
317
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "5", r: "1.7", fill: C_POINT })
318
+ ] }),
319
+ isoTriangle: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
320
+ /* @__PURE__ */ jsx("path", { d: "M 12 4 L 21 20 L 3 20 Z", fill: C_FILL, fillOpacity: "0.18", stroke: "currentColor", strokeWidth: "1.6" }),
321
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "20", r: "1.7", fill: C_POINT }),
322
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "20", r: "1.7", fill: C_POINT }),
323
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "4", r: "1.7", fill: C_POINT }),
324
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "4", x2: "12", y2: "20", stroke: C_CONSTRUCT, strokeWidth: "0.8", strokeDasharray: "1.5 1.5", opacity: "0.7" })
325
+ ] }),
326
+ rightTriangle: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
327
+ /* @__PURE__ */ jsx("path", { d: "M 4 20 L 20 20 L 4 4 Z", fill: C_FILL, fillOpacity: "0.18", stroke: "currentColor", strokeWidth: "1.6" }),
328
+ /* @__PURE__ */ jsx("path", { d: "M 4 17 L 7 17 L 7 20", fill: "none", stroke: "currentColor", strokeWidth: "1.2" }),
329
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "20", r: "1.7", fill: C_POINT }),
330
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "20", r: "1.7", fill: C_POINT }),
331
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "4", r: "1.7", fill: C_POINT })
332
+ ] }),
333
+ // ===== Nâng cao / kind chưa có icon =====
334
+ excenter: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
335
+ /* @__PURE__ */ jsx("path", { d: "M5 16 L12 5 L19 16 Z", stroke: "currentColor", strokeWidth: "1.3" }),
336
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "20", r: "3", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" }),
337
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "20", r: "1.4", fill: C_CONSTRUCT })
338
+ ] }),
339
+ tangencyPoint: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
340
+ /* @__PURE__ */ jsx("circle", { cx: "10", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.3" }),
341
+ /* @__PURE__ */ jsx("line", { x1: "16.5", y1: "3", x2: "16.5", y2: "21", stroke: "currentColor", strokeWidth: "1.3" }),
342
+ /* @__PURE__ */ jsx("circle", { cx: "16.5", cy: "12", r: "2.2", fill: C_CONSTRUCT })
343
+ ] }),
344
+ secondIntersection: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
345
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.3" }),
346
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "8", x2: "22", y2: "16", stroke: "currentColor", strokeWidth: "1.3" }),
347
+ /* @__PURE__ */ jsx("circle", { cx: "6.2", cy: "9.6", r: "1.6", fill: "currentColor" }),
348
+ /* @__PURE__ */ jsx("circle", { cx: "15.8", cy: "14.4", r: "2.4", fill: C_CONSTRUCT })
349
+ ] }),
350
+ arcMidpoint: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
351
+ /* @__PURE__ */ jsx("path", { d: "M4 17 A 9 9 0 0 1 20 17", stroke: "currentColor", strokeWidth: "1.4" }),
352
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "17", r: "1.6", fill: "currentColor" }),
353
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "17", r: "1.6", fill: "currentColor" }),
354
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8.2", r: "2.4", fill: C_CONSTRUCT })
355
+ ] }),
356
+ circleIntersection: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
357
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.3" }),
358
+ /* @__PURE__ */ jsx("circle", { cx: "15", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.3" }),
359
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "7.6", r: "2", fill: C_CONSTRUCT }),
360
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "16.4", r: "2", fill: C_CONSTRUCT })
361
+ ] }),
362
+ tangentPointExt: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
363
+ /* @__PURE__ */ jsx("circle", { cx: "14", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.3" }),
364
+ /* @__PURE__ */ jsx("circle", { cx: "3.5", cy: "12", r: "1.8", fill: C_POINT }),
365
+ /* @__PURE__ */ jsx("line", { x1: "3.5", y1: "12", x2: "17.5", y2: "7.5", stroke: "currentColor", strokeWidth: "1.2" }),
366
+ /* @__PURE__ */ jsx("line", { x1: "3.5", y1: "12", x2: "17.5", y2: "16.5", stroke: "currentColor", strokeWidth: "1.2" }),
367
+ /* @__PURE__ */ jsx("circle", { cx: "17.5", cy: "7.5", r: "1.8", fill: C_CONSTRUCT }),
368
+ /* @__PURE__ */ jsx("circle", { cx: "17.5", cy: "16.5", r: "1.8", fill: C_CONSTRUCT })
369
+ ] }),
370
+ circleCR: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
371
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
372
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.8", fill: C_POINT }),
373
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "12", x2: "19.5", y2: "12", stroke: C_CONSTRUCT, strokeWidth: "1.3" })
374
+ ] }),
375
+ incircle: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
376
+ /* @__PURE__ */ jsx("path", { d: "M3 19 L12 4 L21 19 Z", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }),
377
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "14", r: "4.2", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" })
378
+ ] }),
379
+ excircle: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
380
+ /* @__PURE__ */ jsx("path", { d: "M6 9 L14 4 L18 13 Z", stroke: "currentColor", strokeWidth: "1.2" }),
381
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "17", r: "4.6", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" })
382
+ ] }),
383
+ pointOn: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
384
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
385
+ /* @__PURE__ */ jsx("circle", { cx: "17.3", cy: "6.7", r: "2.4", fill: C_POINT })
286
386
  ] })
287
387
  };
288
388
  var GeometryIconHeader = /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
@@ -343,7 +443,83 @@ var TOOLS = [
343
443
  { key: "rotate", label: "Quay \u0111\u1ED1i t\u01B0\u1EE3ng", hint: "Click object \u2192 t\xE2m quay \u2192 nh\u1EADp g\xF3c", icon: Icon.rotate, group: "transform", needs: 2, accepts: ["any", "point"] },
344
444
  { key: "reflectLine", label: "\u0110\u1ED1i x\u1EE9ng qua \u0111\u01B0\u1EDDng th\u1EB3ng", hint: "Click object \u2192 \u0111\u01B0\u1EDDng th\u1EB3ng", icon: Icon.reflectLine, group: "transform", needs: 2, accepts: ["any", "line"] },
345
445
  { key: "reflectPoint", label: "\u0110\u1ED1i x\u1EE9ng qua \u0111i\u1EC3m", hint: "Click object \u2192 t\xE2m \u0111\u1ED1i x\u1EE9ng", icon: Icon.reflectPoint, group: "transform", needs: 2, accepts: ["any", "point"] },
346
- { key: "dilate", label: "Ph\xE9p v\u1ECB t\u1EF1", hint: "Click object \u2192 t\xE2m \u2192 nh\u1EADp t\u1EF7 s\u1ED1 k", icon: Icon.dilate, group: "transform", needs: 2, accepts: ["any", "point"] }
446
+ { key: "dilate", label: "Ph\xE9p v\u1ECB t\u1EF1", hint: "Click object \u2192 t\xE2m \u2192 nh\u1EADp t\u1EF7 s\u1ED1 k", icon: Icon.dilate, group: "transform", needs: 2, accepts: ["any", "point"] },
447
+ // ===== Hình đặc biệt (parametric construction) =====
448
+ {
449
+ key: "square",
450
+ label: "H\xECnh vu\xF4ng",
451
+ hint: "Click 2 \u0111i\u1EC3m \u2014 c\u1EA1nh \u0111\u1EA7u (3 \u0111\u1EC9nh c\xF2n l\u1EA1i t\u1EF1 suy, vu\xF4ng g\xF3c + b\u1EB1ng c\u1EA1nh)",
452
+ icon: Icon.square,
453
+ group: "special",
454
+ needs: 2,
455
+ accepts: ["point", "point"]
456
+ },
457
+ {
458
+ key: "rectangle",
459
+ label: "H\xECnh ch\u1EEF nh\u1EADt",
460
+ hint: "Click 2 \u0111i\u1EC3m \u0111\xE1y + 1 \u0111i\u1EC3m chi\u1EC1u cao (auto vu\xF4ng g\xF3c t\u1EA1i \u0111\u1EC9nh 2)",
461
+ icon: Icon.rectangle,
462
+ group: "special",
463
+ needs: 3,
464
+ accepts: ["point", "point", "point"]
465
+ },
466
+ {
467
+ key: "rhombus",
468
+ label: "H\xECnh thoi",
469
+ hint: "Click 2 \u0111i\u1EC3m c\u1EA1nh + 1 \u0111i\u1EC3m h\u01B0\u1EDBng (auto b\u1EB1ng \u0111\u1ED9 d\xE0i c\u1EA1nh)",
470
+ icon: Icon.rhombus,
471
+ group: "special",
472
+ needs: 3,
473
+ accepts: ["point", "point", "point"]
474
+ },
475
+ {
476
+ key: "parallelogram",
477
+ label: "H\xECnh b\xECnh h\xE0nh",
478
+ hint: "Click 3 \u0111i\u1EC3m li\xEAn ti\u1EBFp (\u0111\u1EC9nh 4 t\u1EF1 suy)",
479
+ icon: Icon.parallelogram,
480
+ group: "special",
481
+ needs: 3,
482
+ accepts: ["point", "point", "point"]
483
+ },
484
+ {
485
+ key: "isoTrapezoid",
486
+ label: "H\xECnh thang c\xE2n",
487
+ hint: "Click 2 \u0111i\u1EC3m \u0111\xE1y l\u1EDBn + 1 \u0111\u1EC9nh tr\xEAn (\u0111\u1EC9nh 4 ph\u1EA3n chi\u1EBFu qua trung tr\u1EF1c)",
488
+ icon: Icon.isoTrapezoid,
489
+ group: "special",
490
+ needs: 3,
491
+ accepts: ["point", "point", "point"]
492
+ },
493
+ {
494
+ key: "isoTriangle",
495
+ label: "Tam gi\xE1c c\xE2n",
496
+ hint: "Click 2 \u0111i\u1EC3m \u0111\xE1y + 1 \u0111\u1EC9nh (auto tr\xEAn trung tr\u1EF1c)",
497
+ icon: Icon.isoTriangle,
498
+ group: "special",
499
+ needs: 3,
500
+ accepts: ["point", "point", "point"]
501
+ },
502
+ {
503
+ key: "rightTriangle",
504
+ label: "Tam gi\xE1c vu\xF4ng",
505
+ hint: "Click \u0111\u1EC9nh vu\xF4ng + 2 \u0111\u1EA7u c\u1EA1nh (c\u1EA1nh 2 auto vu\xF4ng g\xF3c)",
506
+ icon: Icon.rightTriangle,
507
+ group: "special",
508
+ needs: 3,
509
+ accepts: ["point", "point", "point"]
510
+ },
511
+ // ===== Điểm / đường tròn thông dụng (kind chưa có icon trước v0.27) =====
512
+ { key: "pointOn", label: "\u0110i\u1EC3m tr\xEAn \u0111\u1ED1i t\u01B0\u1EE3ng", hint: "Click 1 \u0111\u01B0\u1EDDng/\u0111o\u1EA1n/\u0111\u01B0\u1EDDng tr\xF2n c\xF3 s\u1EB5n", icon: Icon.pointOn, group: "point", needs: 1, accepts: ["lineOrCircle"] },
513
+ { key: "circleCR", label: "\u0110\u01B0\u1EDDng tr\xF2n (t\xE2m + b\xE1n k\xEDnh)", hint: "Click t\xE2m r\u1ED3i nh\u1EADp b\xE1n k\xEDnh", icon: Icon.circleCR, group: "circle", needs: 1, accepts: ["point"] },
514
+ { key: "incircle", label: "\u0110\u01B0\u1EDDng tr\xF2n n\u1ED9i ti\u1EBFp", hint: "Click 3 \u0111\u1EC9nh tam gi\xE1c", icon: Icon.incircle, group: "circle", needs: 3, accepts: ["point", "point", "point"] },
515
+ // ===== Nâng cao =====
516
+ { key: "excenter", label: "T\xE2m \u0111\u01B0\u1EDDng tr\xF2n b\xE0ng ti\u1EBFp", hint: "Click 3 \u0111\u1EC9nh tam gi\xE1c (\u0111\u1EC9nh \u0111\u1EA7u = \u0111\u1EC9nh \u0111\u1ED1i di\u1EC7n)", icon: Icon.excenter, group: "advanced", needs: 3, accepts: ["point", "point", "point"] },
517
+ { key: "excircle", label: "\u0110\u01B0\u1EDDng tr\xF2n b\xE0ng ti\u1EBFp", hint: "Click 3 \u0111\u1EC9nh tam gi\xE1c (\u0111\u1EC9nh \u0111\u1EA7u = \u0111\u1EC9nh \u0111\u1ED1i di\u1EC7n)", icon: Icon.excircle, group: "advanced", needs: 3, accepts: ["point", "point", "point"] },
518
+ { key: "tangencyPoint", label: "Ti\u1EBFp \u0111i\u1EC3m (\u0111\u01B0\u1EDDng ti\u1EBFp x\xFAc)", hint: "Click 1 \u0111\u01B0\u1EDDng tr\xF2n + 1 ti\u1EBFp tuy\u1EBFn c\xF3 s\u1EB5n", icon: Icon.tangencyPoint, group: "advanced", needs: 2, accepts: ["circle", "line"] },
519
+ { key: "secondIntersection", label: "Giao \u0111i\u1EC3m th\u1EE9 hai", hint: "Click 1 \u0111\u01B0\u1EDDng + 1 \u0111\u01B0\u1EDDng tr\xF2n + giao \u0111i\u1EC3m \u0111\xE3 bi\u1EBFt", icon: Icon.secondIntersection, group: "advanced", needs: 3, accepts: ["line", "circle", "point"] },
520
+ { key: "arcMidpoint", label: "\u0110i\u1EC3m gi\u1EEFa cung", hint: "Click \u0111\u01B0\u1EDDng tr\xF2n \u2192 2 \u0111\u1EA7u cung A,B \u2192 1 \u0111i\u1EC3m ph\xEDa cung KH\xD4NG ch\u1EE9a", icon: Icon.arcMidpoint, group: "advanced", needs: 4, accepts: ["circle", "point", "point", "point"] },
521
+ { key: "circleIntersection", label: "Giao 2 \u0111\u01B0\u1EDDng tr\xF2n", hint: "Click 2 \u0111\u01B0\u1EDDng tr\xF2n (t\u1EA1o c\u1EA3 2 giao \u0111i\u1EC3m)", icon: Icon.circleIntersection, group: "advanced", needs: 2, accepts: ["circle", "circle"] },
522
+ { key: "tangentPointExt", label: "Ti\u1EBFp \u0111i\u1EC3m t\u1EEB \u0111i\u1EC3m ngo\xE0i", hint: "Click 1 \u0111i\u1EC3m ngo\xE0i + 1 \u0111\u01B0\u1EDDng tr\xF2n (t\u1EA1o c\u1EA3 2 ti\u1EBFp \u0111i\u1EC3m)", icon: Icon.tangentPointExt, group: "advanced", needs: 2, accepts: ["point", "circle"] }
347
523
  ];
348
524
  var GROUP_LABELS = {
349
525
  move: "C\u01A1 b\u1EA3n",
@@ -355,7 +531,9 @@ var GROUP_LABELS = {
355
531
  triangle: "Tam gi\xE1c",
356
532
  measure: "\u0110o l\u01B0\u1EDDng",
357
533
  edit: "Ch\u1EC9nh s\u1EEDa",
358
- transform: "Ph\xE9p bi\u1EBFn h\xECnh"
534
+ transform: "Ph\xE9p bi\u1EBFn h\xECnh",
535
+ special: "H\xECnh \u0111\u1EB7c bi\u1EC7t",
536
+ advanced: "N\xE2ng cao"
359
537
  };
360
538
  var GROUP_ORDER = [
361
539
  "move",
@@ -367,7 +545,9 @@ var GROUP_ORDER = [
367
545
  "triangle",
368
546
  "measure",
369
547
  "edit",
370
- "transform"
548
+ "transform",
549
+ "special",
550
+ "advanced"
371
551
  ];
372
552
  var A_CODE = "A".charCodeAt(0);
373
553
  function letterForGroup(g) {
@@ -594,6 +774,20 @@ function handlePolygonTool(ctx, t, toolDef, e, x, y, bestHit) {
594
774
  return true;
595
775
  }
596
776
 
777
+ // src/stamps/geometry-2d/editor/handlers/finalize/lines.ts
778
+ var lines_exports = {};
779
+ __export(lines_exports, {
780
+ angleBisectorTool: () => angleBisectorTool,
781
+ lineTool: () => lineTool,
782
+ parallelTool: () => parallelTool,
783
+ perpBisectorTool: () => perpBisectorTool,
784
+ perpendicularTool: () => perpendicularTool,
785
+ rayTool: () => rayTool,
786
+ segmentTool: () => segmentTool,
787
+ tangentTool: () => tangentTool,
788
+ vectorTool: () => vectorTool
789
+ });
790
+
597
791
  // src/stamps/geometry-2d/editor/handlers/classifyPointVsCircle.ts
598
792
  function classifyPointVsCircle(point, circle) {
599
793
  if (!point || !circle || !circle.center) return "inside";
@@ -607,7 +801,7 @@ function classifyPointVsCircle(point, circle) {
607
801
  return d < r ? "inside" : "outside";
608
802
  }
609
803
 
610
- // src/stamps/geometry-2d/editor/handlers/finalizeShape.ts
804
+ // src/stamps/geometry-2d/editor/handlers/finalize/shared.ts
611
805
  function findPickIdByKind(ctx, kind) {
612
806
  const picks = ctx.pendingRef.current;
613
807
  const ids = ctx.pendingIdsRef.current;
@@ -616,362 +810,817 @@ function findPickIdByKind(ctx, kind) {
616
810
  }
617
811
  return null;
618
812
  }
619
- function finalizeShape(ctx, toolDef) {
620
- const ids = ctx.pendingIdsRef.current;
621
- const key = toolDef.key;
622
- switch (key) {
623
- case "segment": {
624
- const id = freshId(ctx, "s");
625
- const label = ctx.nextLabel("segment");
626
- ctx.store.dispatch({
627
- type: "ADD",
628
- payload: { obj: mkSceneObj(id, "segment", label, { p1: ids[0], p2: ids[1] }) }
629
- });
630
- return;
631
- }
632
- case "line": {
633
- const id = freshId(ctx, "l");
634
- const label = ctx.nextLabel("line");
635
- ctx.store.dispatch({
636
- type: "ADD",
637
- payload: { obj: mkSceneObj(id, "line", label, { p1: ids[0], p2: ids[1] }) }
638
- });
813
+ function readJxgPos(ctx, id) {
814
+ const j = ctx.jxgFromSceneId(id);
815
+ if (!j || typeof j.X !== "function") return { x: 0, y: 0 };
816
+ return { x: j.X(), y: j.Y() };
817
+ }
818
+ function computePerpendicularT(P, T, A, B) {
819
+ const dx = B.x - A.x, dy = B.y - A.y;
820
+ const len = Math.hypot(dx, dy);
821
+ if (len < 1e-12) return 0;
822
+ const ux = -dy / len, uy = dx / len;
823
+ return (P.x - T.x) * ux + (P.y - T.y) * uy;
824
+ }
825
+ function computePerpBisectorT(P, A, B) {
826
+ const Mx = (A.x + B.x) / 2, My = (A.y + B.y) / 2;
827
+ const dx = B.x - A.x, dy = B.y - A.y;
828
+ const len = Math.hypot(dx, dy);
829
+ if (len < 1e-12) return 0;
830
+ const ux = -dy / len, uy = dx / len;
831
+ return (P.x - Mx) * ux + (P.y - My) * uy;
832
+ }
833
+ function computeCircleTheta(P, C) {
834
+ return Math.atan2(P.y - C.y, P.x - C.x);
835
+ }
836
+
837
+ // src/stamps/geometry-2d/editor/handlers/finalize/lines.ts
838
+ var segmentTool = {
839
+ key: "segment",
840
+ finalize(ctx) {
841
+ const ids = ctx.pendingIdsRef.current;
842
+ const id = freshId(ctx, "s");
843
+ const label = ctx.nextLabel("segment");
844
+ ctx.store.dispatch({
845
+ type: "ADD",
846
+ payload: { obj: mkSceneObj(id, "segment", label, { p1: ids[0], p2: ids[1] }) }
847
+ });
848
+ }
849
+ };
850
+ var lineTool = {
851
+ key: "line",
852
+ finalize(ctx) {
853
+ const ids = ctx.pendingIdsRef.current;
854
+ const id = freshId(ctx, "l");
855
+ const label = ctx.nextLabel("line");
856
+ ctx.store.dispatch({
857
+ type: "ADD",
858
+ payload: { obj: mkSceneObj(id, "line", label, { p1: ids[0], p2: ids[1] }) }
859
+ });
860
+ }
861
+ };
862
+ function finalizePerpParallel(ctx, key) {
863
+ const throughPoint = findPickIdByKind(ctx, "point");
864
+ const toLine = findPickIdByKind(ctx, "line");
865
+ if (!throughPoint || !toLine) return;
866
+ const id = freshId(ctx, key === "perpendicular" ? "perp" : "par");
867
+ const label = ctx.nextLabel("line");
868
+ ctx.store.dispatch({
869
+ type: "ADD",
870
+ payload: { obj: mkSceneObj(id, "line", label, {
871
+ construction: { kind: key, throughPoint, toLine }
872
+ }) }
873
+ });
874
+ }
875
+ var perpendicularTool = {
876
+ key: "perpendicular",
877
+ finalize(ctx) {
878
+ finalizePerpParallel(ctx, "perpendicular");
879
+ }
880
+ };
881
+ var parallelTool = {
882
+ key: "parallel",
883
+ finalize(ctx) {
884
+ finalizePerpParallel(ctx, "parallel");
885
+ }
886
+ };
887
+ var perpBisectorTool = {
888
+ key: "perpBisector",
889
+ finalize(ctx) {
890
+ const ids = ctx.pendingIdsRef.current;
891
+ const id = freshId(ctx, "pb");
892
+ const label = ctx.nextLabel("line");
893
+ ctx.store.dispatch({
894
+ type: "ADD",
895
+ payload: { obj: mkSceneObj(id, "line", label, {
896
+ construction: { kind: "perpBisector", p1: ids[0], p2: ids[1] }
897
+ }) }
898
+ });
899
+ }
900
+ };
901
+ var angleBisectorTool = {
902
+ key: "angleBisector",
903
+ finalize(ctx) {
904
+ const ids = ctx.pendingIdsRef.current;
905
+ const picks = ctx.pendingRef.current;
906
+ if (picks.length === 2 && objKind(picks[0]) === "line" && objKind(picks[1]) === "line") {
907
+ for (const branch of [0, 1]) {
908
+ const id2 = freshId(ctx, "ab");
909
+ const label2 = ctx.nextLabel("line");
910
+ ctx.store.dispatch({
911
+ type: "ADD",
912
+ payload: { obj: mkSceneObj(id2, "line", label2, {
913
+ construction: { kind: "angleBisectorLines", line1: ids[0], line2: ids[1], branch }
914
+ }) }
915
+ });
916
+ }
639
917
  return;
640
918
  }
641
- case "perpendicular":
642
- case "parallel": {
643
- const throughPoint = findPickIdByKind(ctx, "point");
644
- const toLine = findPickIdByKind(ctx, "line");
645
- if (!throughPoint || !toLine) return;
646
- const id = freshId(ctx, key === "perpendicular" ? "perp" : "par");
647
- const label = ctx.nextLabel("line");
648
- ctx.store.dispatch({
649
- type: "ADD",
650
- payload: { obj: mkSceneObj(id, "line", label, {
651
- construction: { kind: key, throughPoint, toLine }
652
- }) }
919
+ const id = freshId(ctx, "ab");
920
+ const label = ctx.nextLabel("line");
921
+ ctx.store.dispatch({
922
+ type: "ADD",
923
+ payload: { obj: mkSceneObj(id, "line", label, {
924
+ construction: { kind: "angleBisector", p1: ids[0], vertex: ids[1], p2: ids[2] }
925
+ }) }
926
+ });
927
+ }
928
+ };
929
+ var tangentTool = {
930
+ key: "tangent",
931
+ finalize(ctx) {
932
+ const throughId = findPickIdByKind(ctx, "point");
933
+ const circleId = findPickIdByKind(ctx, "circle");
934
+ if (!throughId || !circleId) return;
935
+ const picks = ctx.pendingRef.current;
936
+ const ids = ctx.pendingIdsRef.current;
937
+ const through = picks[ids.indexOf(throughId)];
938
+ const circle = picks[ids.indexOf(circleId)];
939
+ const pos = classifyPointVsCircle(through, circle);
940
+ if (pos === "inside") {
941
+ ctx.toast?.("\u0110i\u1EC3m n\u1EB1m trong \u0111\u01B0\u1EDDng tr\xF2n \u2014 kh\xF4ng c\xF3 ti\u1EBFp tuy\u1EBFn", {
942
+ variant: "warning",
943
+ id: "tangent-invalid-inside"
653
944
  });
654
945
  return;
655
946
  }
656
- case "perpBisector": {
657
- const id = freshId(ctx, "pb");
947
+ if (pos === "on") {
948
+ const id = freshId(ctx, "t");
658
949
  const label = ctx.nextLabel("line");
659
950
  ctx.store.dispatch({
660
951
  type: "ADD",
661
952
  payload: { obj: mkSceneObj(id, "line", label, {
662
- construction: { kind: "perpBisector", p1: ids[0], p2: ids[1] }
953
+ construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch: "on" }
663
954
  }) }
664
955
  });
665
956
  return;
666
957
  }
667
- case "angleBisector": {
668
- const picks = ctx.pendingRef.current;
669
- if (picks.length === 2 && objKind(picks[0]) === "line" && objKind(picks[1]) === "line") {
670
- for (const branch of [0, 1]) {
671
- const id2 = freshId(ctx, "ab");
672
- const label2 = ctx.nextLabel("line");
673
- ctx.store.dispatch({
674
- type: "ADD",
675
- payload: { obj: mkSceneObj(id2, "line", label2, {
676
- construction: { kind: "angleBisectorLines", line1: ids[0], line2: ids[1], branch }
677
- }) }
678
- });
679
- }
680
- return;
681
- }
682
- const id = freshId(ctx, "ab");
958
+ for (const branch of [0, 1]) {
959
+ const id = freshId(ctx, "t");
683
960
  const label = ctx.nextLabel("line");
684
961
  ctx.store.dispatch({
685
962
  type: "ADD",
686
963
  payload: { obj: mkSceneObj(id, "line", label, {
687
- construction: { kind: "angleBisector", p1: ids[0], vertex: ids[1], p2: ids[2] }
964
+ construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch }
688
965
  }) }
689
966
  });
690
- return;
691
967
  }
692
- case "tangent": {
693
- const throughId = findPickIdByKind(ctx, "point");
694
- const circleId = findPickIdByKind(ctx, "circle");
695
- if (!throughId || !circleId) return;
696
- const picks = ctx.pendingRef.current;
697
- const ids2 = ctx.pendingIdsRef.current;
698
- const through = picks[ids2.indexOf(throughId)];
699
- const circle = picks[ids2.indexOf(circleId)];
700
- const pos = classifyPointVsCircle(through, circle);
701
- if (pos === "inside") {
702
- ctx.toast?.("\u0110i\u1EC3m n\u1EB1m trong \u0111\u01B0\u1EDDng tr\xF2n \u2014 kh\xF4ng c\xF3 ti\u1EBFp tuy\u1EBFn", {
703
- variant: "warning",
704
- id: "tangent-invalid-inside"
705
- });
706
- return;
707
- }
708
- if (pos === "on") {
709
- const id = freshId(ctx, "t");
710
- const label = ctx.nextLabel("line");
711
- ctx.store.dispatch({
712
- type: "ADD",
713
- payload: { obj: mkSceneObj(id, "line", label, {
714
- construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch: "on" }
715
- }) }
716
- });
717
- return;
968
+ }
969
+ };
970
+ var rayTool = {
971
+ key: "ray",
972
+ finalize(ctx) {
973
+ const ids = ctx.pendingIdsRef.current;
974
+ const id = freshId(ctx, "r");
975
+ const label = ctx.nextLabel("ray");
976
+ ctx.store.dispatch({
977
+ type: "ADD",
978
+ payload: { obj: mkSceneObj(id, "ray", label, { origin: ids[0], through: ids[1] }) }
979
+ });
980
+ }
981
+ };
982
+ var vectorTool = {
983
+ key: "vector",
984
+ finalize(ctx) {
985
+ const ids = ctx.pendingIdsRef.current;
986
+ const id = freshId(ctx, "v");
987
+ const label = ctx.nextLabel("vector");
988
+ ctx.store.dispatch({
989
+ type: "ADD",
990
+ payload: { obj: mkSceneObj(id, "vector", label, { from: ids[0], to: ids[1] }) }
991
+ });
992
+ }
993
+ };
994
+
995
+ // src/stamps/geometry-2d/editor/handlers/finalize/circles.ts
996
+ var circles_exports = {};
997
+ __export(circles_exports, {
998
+ arc3Tool: () => arc3Tool,
999
+ arcCenterTool: () => arcCenterTool,
1000
+ circle3Tool: () => circle3Tool,
1001
+ circleCenterTool: () => circleCenterTool,
1002
+ excircleTool: () => excircleTool,
1003
+ incircleTool: () => incircleTool,
1004
+ sectorCenterTool: () => sectorCenterTool,
1005
+ semicircleTool: () => semicircleTool
1006
+ });
1007
+ var circleCenterTool = {
1008
+ key: "circleCenter",
1009
+ finalize(ctx) {
1010
+ const ids = ctx.pendingIdsRef.current;
1011
+ const id = freshId(ctx, "c");
1012
+ const label = ctx.nextLabel("circle");
1013
+ ctx.store.dispatch({
1014
+ type: "ADD",
1015
+ payload: {
1016
+ obj: mkSceneObj(id, "circle", label, {
1017
+ center: ids[0],
1018
+ surfacePoint: ids[1]
1019
+ })
718
1020
  }
719
- for (const branch of [0, 1]) {
720
- const id = freshId(ctx, "t");
721
- const label = ctx.nextLabel("line");
722
- ctx.store.dispatch({
723
- type: "ADD",
724
- payload: { obj: mkSceneObj(id, "line", label, {
725
- construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch }
726
- }) }
727
- });
1021
+ });
1022
+ }
1023
+ };
1024
+ var circle3Tool = {
1025
+ key: "circle3",
1026
+ finalize(ctx) {
1027
+ const ids = ctx.pendingIdsRef.current;
1028
+ const id = freshId(ctx, "cc");
1029
+ const label = ctx.nextLabel("circle");
1030
+ ctx.store.dispatch({
1031
+ type: "ADD",
1032
+ payload: {
1033
+ obj: mkSceneObj(id, "circle", label, {
1034
+ construction: { kind: "circumscribed", p1: ids[0], p2: ids[1], p3: ids[2] }
1035
+ })
728
1036
  }
1037
+ });
1038
+ }
1039
+ };
1040
+ var semicircleTool = {
1041
+ key: "semicircle",
1042
+ finalize(ctx) {
1043
+ const ids = ctx.pendingIdsRef.current;
1044
+ if (ids[0] === ids[1]) {
1045
+ ctx.toast?.("C\u1EA7n 2 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "semicircle-dup" });
729
1046
  return;
730
1047
  }
731
- case "ray": {
732
- const id = freshId(ctx, "r");
733
- const label = ctx.nextLabel("ray");
734
- ctx.store.dispatch({
735
- type: "ADD",
736
- payload: { obj: mkSceneObj(id, "ray", label, { origin: ids[0], through: ids[1] }) }
737
- });
738
- return;
739
- }
740
- case "vector": {
741
- const id = freshId(ctx, "v");
742
- const label = ctx.nextLabel("vector");
743
- ctx.store.dispatch({
744
- type: "ADD",
745
- payload: { obj: mkSceneObj(id, "vector", label, { from: ids[0], to: ids[1] }) }
746
- });
747
- return;
748
- }
749
- case "circleCenter": {
750
- const id = freshId(ctx, "c");
751
- const label = ctx.nextLabel("circle");
752
- ctx.store.dispatch({
753
- type: "ADD",
754
- payload: {
755
- obj: mkSceneObj(id, "circle", label, {
756
- center: ids[0],
757
- surfacePoint: ids[1]
758
- })
759
- }
760
- });
761
- return;
762
- }
763
- case "circle3": {
764
- const id = freshId(ctx, "cc");
765
- const label = ctx.nextLabel("circle");
766
- ctx.store.dispatch({
767
- type: "ADD",
768
- payload: {
769
- obj: mkSceneObj(id, "circle", label, {
770
- construction: { kind: "circumscribed", p1: ids[0], p2: ids[1], p3: ids[2] }
771
- })
772
- }
773
- });
774
- return;
775
- }
776
- case "semicircle": {
777
- if (ids[0] === ids[1]) {
778
- ctx.toast?.("C\u1EA7n 2 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "semicircle-dup" });
779
- return;
1048
+ const id = freshId(ctx, "arc");
1049
+ const label = ctx.nextLabel("arc");
1050
+ ctx.store.dispatch({
1051
+ type: "ADD",
1052
+ payload: {
1053
+ obj: mkSceneObj(id, "arc", label, {
1054
+ construction: { kind: "semicircle", p1: ids[0], p2: ids[1] }
1055
+ })
780
1056
  }
781
- const id = freshId(ctx, "arc");
782
- const label = ctx.nextLabel("arc");
783
- ctx.store.dispatch({
784
- type: "ADD",
785
- payload: {
786
- obj: mkSceneObj(id, "arc", label, {
787
- construction: { kind: "semicircle", p1: ids[0], p2: ids[1] }
788
- })
789
- }
790
- });
1057
+ });
1058
+ }
1059
+ };
1060
+ var arcCenterTool = {
1061
+ key: "arcCenter",
1062
+ finalize(ctx) {
1063
+ const ids = ctx.pendingIdsRef.current;
1064
+ if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
1065
+ ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "arc-center-dup" });
791
1066
  return;
792
1067
  }
793
- case "arcCenter": {
794
- if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
795
- ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "arc-center-dup" });
796
- return;
1068
+ const id = freshId(ctx, "arc");
1069
+ const label = ctx.nextLabel("arc");
1070
+ ctx.store.dispatch({
1071
+ type: "ADD",
1072
+ payload: {
1073
+ obj: mkSceneObj(id, "arc", label, {
1074
+ construction: { kind: "byCenter", center: ids[0], p1: ids[1], p2: ids[2] }
1075
+ })
797
1076
  }
798
- const id = freshId(ctx, "arc");
799
- const label = ctx.nextLabel("arc");
800
- ctx.store.dispatch({
801
- type: "ADD",
802
- payload: {
803
- obj: mkSceneObj(id, "arc", label, {
804
- construction: { kind: "byCenter", center: ids[0], p1: ids[1], p2: ids[2] }
805
- })
806
- }
807
- });
1077
+ });
1078
+ }
1079
+ };
1080
+ var arc3Tool = {
1081
+ key: "arc3",
1082
+ finalize(ctx) {
1083
+ const ids = ctx.pendingIdsRef.current;
1084
+ if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
1085
+ ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "arc3-dup" });
808
1086
  return;
809
1087
  }
810
- case "arc3": {
811
- if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
812
- ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "arc3-dup" });
813
- return;
814
- }
815
- const picks = ctx.pendingRef.current;
816
- const ax = picks[0].X(), ay = picks[0].Y();
817
- const bx = picks[1].X(), by = picks[1].Y();
818
- const cx = picks[2].X(), cy = picks[2].Y();
819
- const cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
820
- if (Math.abs(cross) < 1e-6) {
821
- ctx.toast?.("Kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c cung qua 3 \u0111i\u1EC3m th\u1EB3ng h\xE0ng", {
822
- variant: "warning",
823
- id: "arc3-collinear"
824
- });
825
- return;
826
- }
827
- const id = freshId(ctx, "arc");
828
- const label = ctx.nextLabel("arc");
829
- ctx.store.dispatch({
830
- type: "ADD",
831
- payload: {
832
- obj: mkSceneObj(id, "arc", label, {
833
- construction: { kind: "by3Points", p1: ids[0], p2: ids[1], p3: ids[2] }
834
- })
835
- }
1088
+ const picks = ctx.pendingRef.current;
1089
+ const ax = picks[0].X(), ay = picks[0].Y();
1090
+ const bx = picks[1].X(), by = picks[1].Y();
1091
+ const cx = picks[2].X(), cy = picks[2].Y();
1092
+ const cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
1093
+ if (Math.abs(cross) < 1e-6) {
1094
+ ctx.toast?.("Kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c cung qua 3 \u0111i\u1EC3m th\u1EB3ng h\xE0ng", {
1095
+ variant: "warning",
1096
+ id: "arc3-collinear"
836
1097
  });
837
1098
  return;
838
1099
  }
839
- case "sectorCenter": {
840
- if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
841
- ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "sector-center-dup" });
842
- return;
1100
+ const id = freshId(ctx, "arc");
1101
+ const label = ctx.nextLabel("arc");
1102
+ ctx.store.dispatch({
1103
+ type: "ADD",
1104
+ payload: {
1105
+ obj: mkSceneObj(id, "arc", label, {
1106
+ construction: { kind: "by3Points", p1: ids[0], p2: ids[1], p3: ids[2] }
1107
+ })
843
1108
  }
844
- const id = freshId(ctx, "sec");
845
- const label = ctx.nextLabel("sector");
846
- ctx.store.dispatch({
847
- type: "ADD",
848
- payload: {
849
- obj: mkSceneObj(id, "sector", label, {
850
- construction: { kind: "byCenter", center: ids[0], p1: ids[1], p2: ids[2] }
851
- })
852
- }
853
- });
854
- return;
855
- }
856
- case "midpoint": {
857
- const id = freshId(ctx, "mp");
858
- const label = ctx.nextLabel("point");
859
- ctx.store.dispatch({
860
- type: "ADD",
861
- payload: { obj: mkSceneObj(id, "point", label, {
862
- constraint: { kind: "midpoint", p1: ids[0], p2: ids[1] }
863
- }) }
864
- });
865
- return;
866
- }
867
- case "perpFoot": {
868
- const fromPoint = findPickIdByKind(ctx, "point");
869
- const onLine = findPickIdByKind(ctx, "line");
870
- if (!fromPoint || !onLine) return;
871
- const id = freshId(ctx, "pf");
872
- const label = ctx.nextLabel("point");
873
- ctx.store.dispatch({
874
- type: "ADD",
875
- payload: { obj: mkSceneObj(id, "point", label, {
876
- constraint: { kind: "perpFoot", from: fromPoint, onLine }
877
- }) }
878
- });
1109
+ });
1110
+ }
1111
+ };
1112
+ var sectorCenterTool = {
1113
+ key: "sectorCenter",
1114
+ finalize(ctx) {
1115
+ const ids = ctx.pendingIdsRef.current;
1116
+ if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
1117
+ ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "sector-center-dup" });
879
1118
  return;
880
1119
  }
881
- case "centroid": {
882
- const id = freshId(ctx, "g");
883
- const label = ctx.nextLabel("point");
884
- ctx.store.dispatch({
885
- type: "ADD",
886
- payload: { obj: mkSceneObj(id, "point", label, {
887
- constraint: { kind: "centroid", vertices: [ids[0], ids[1], ids[2]] }
888
- }) }
889
- });
890
- return;
1120
+ const id = freshId(ctx, "sec");
1121
+ const label = ctx.nextLabel("sector");
1122
+ ctx.store.dispatch({
1123
+ type: "ADD",
1124
+ payload: {
1125
+ obj: mkSceneObj(id, "sector", label, {
1126
+ construction: { kind: "byCenter", center: ids[0], p1: ids[1], p2: ids[2] }
1127
+ })
1128
+ }
1129
+ });
1130
+ }
1131
+ };
1132
+ var incircleTool = {
1133
+ key: "incircle",
1134
+ finalize(ctx) {
1135
+ const ids = ctx.pendingIdsRef.current;
1136
+ const id = freshId(ctx, "ic");
1137
+ const label = ctx.nextLabel("circle");
1138
+ ctx.store.dispatch({
1139
+ type: "ADD",
1140
+ payload: { obj: mkSceneObj(id, "circle", label, {
1141
+ construction: { kind: "incircle", p1: ids[0], p2: ids[1], p3: ids[2] }
1142
+ }) }
1143
+ });
1144
+ }
1145
+ };
1146
+ var excircleTool = {
1147
+ key: "excircle",
1148
+ finalize(ctx) {
1149
+ const ids = ctx.pendingIdsRef.current;
1150
+ const id = freshId(ctx, "exc");
1151
+ const label = ctx.nextLabel("circle");
1152
+ ctx.store.dispatch({
1153
+ type: "ADD",
1154
+ payload: { obj: mkSceneObj(id, "circle", label, {
1155
+ construction: { kind: "excircle", p1: ids[0], p2: ids[1], p3: ids[2], opposite: ids[0] }
1156
+ }) }
1157
+ });
1158
+ }
1159
+ };
1160
+
1161
+ // src/stamps/geometry-2d/editor/handlers/finalize/points.ts
1162
+ var points_exports = {};
1163
+ __export(points_exports, {
1164
+ arcMidpointTool: () => arcMidpointTool,
1165
+ centroidTool: () => centroidTool,
1166
+ circleIntersectionTool: () => circleIntersectionTool,
1167
+ circumcenterTool: () => circumcenterTool,
1168
+ excenterTool: () => excenterTool,
1169
+ incenterTool: () => incenterTool,
1170
+ midpointTool: () => midpointTool,
1171
+ orthocenterTool: () => orthocenterTool,
1172
+ perpFootTool: () => perpFootTool,
1173
+ pointOnTool: () => pointOnTool,
1174
+ secondIntersectionTool: () => secondIntersectionTool,
1175
+ tangencyPointTool: () => tangencyPointTool,
1176
+ tangentPointExtTool: () => tangentPointExtTool
1177
+ });
1178
+ var midpointTool = {
1179
+ key: "midpoint",
1180
+ finalize(ctx) {
1181
+ const ids = ctx.pendingIdsRef.current;
1182
+ const id = freshId(ctx, "mp");
1183
+ const label = ctx.nextLabel("point");
1184
+ ctx.store.dispatch({
1185
+ type: "ADD",
1186
+ payload: { obj: mkSceneObj(id, "point", label, {
1187
+ constraint: { kind: "midpoint", p1: ids[0], p2: ids[1] }
1188
+ }) }
1189
+ });
1190
+ }
1191
+ };
1192
+ var perpFootTool = {
1193
+ key: "perpFoot",
1194
+ finalize(ctx) {
1195
+ const fromPoint = findPickIdByKind(ctx, "point");
1196
+ const onLine = findPickIdByKind(ctx, "line");
1197
+ if (!fromPoint || !onLine) return;
1198
+ const id = freshId(ctx, "pf");
1199
+ const label = ctx.nextLabel("point");
1200
+ ctx.store.dispatch({
1201
+ type: "ADD",
1202
+ payload: { obj: mkSceneObj(id, "point", label, {
1203
+ constraint: { kind: "perpFoot", from: fromPoint, onLine }
1204
+ }) }
1205
+ });
1206
+ }
1207
+ };
1208
+ var centroidTool = {
1209
+ key: "centroid",
1210
+ finalize(ctx) {
1211
+ const ids = ctx.pendingIdsRef.current;
1212
+ const id = freshId(ctx, "g");
1213
+ const label = ctx.nextLabel("point");
1214
+ ctx.store.dispatch({
1215
+ type: "ADD",
1216
+ payload: { obj: mkSceneObj(id, "point", label, {
1217
+ constraint: { kind: "centroid", vertices: [ids[0], ids[1], ids[2]] }
1218
+ }) }
1219
+ });
1220
+ }
1221
+ };
1222
+ var circumcenterTool = {
1223
+ key: "circumcenter",
1224
+ finalize(ctx) {
1225
+ const ids = ctx.pendingIdsRef.current;
1226
+ const id = freshId(ctx, "o");
1227
+ const label = ctx.nextLabel("point");
1228
+ ctx.store.dispatch({
1229
+ type: "ADD",
1230
+ payload: { obj: mkSceneObj(id, "point", label, {
1231
+ constraint: { kind: "circumcenter", vertices: [ids[0], ids[1], ids[2]] }
1232
+ }) }
1233
+ });
1234
+ }
1235
+ };
1236
+ var incenterTool = {
1237
+ key: "incenter",
1238
+ finalize(ctx) {
1239
+ const ids = ctx.pendingIdsRef.current;
1240
+ const id = freshId(ctx, "i");
1241
+ const label = ctx.nextLabel("point");
1242
+ ctx.store.dispatch({
1243
+ type: "ADD",
1244
+ payload: { obj: mkSceneObj(id, "point", label, {
1245
+ constraint: { kind: "incenter", vertices: [ids[0], ids[1], ids[2]] }
1246
+ }) }
1247
+ });
1248
+ }
1249
+ };
1250
+ var orthocenterTool = {
1251
+ key: "orthocenter",
1252
+ finalize(ctx) {
1253
+ const ids = ctx.pendingIdsRef.current;
1254
+ const id = freshId(ctx, "h");
1255
+ const label = ctx.nextLabel("point");
1256
+ ctx.store.dispatch({
1257
+ type: "ADD",
1258
+ payload: { obj: mkSceneObj(id, "point", label, {
1259
+ constraint: { kind: "orthocenter", vertices: [ids[0], ids[1], ids[2]] }
1260
+ }) }
1261
+ });
1262
+ }
1263
+ };
1264
+ var excenterTool = {
1265
+ key: "excenter",
1266
+ finalize(ctx) {
1267
+ const ids = ctx.pendingIdsRef.current;
1268
+ const id = freshId(ctx, "ex");
1269
+ const label = ctx.nextLabel("point");
1270
+ ctx.store.dispatch({
1271
+ type: "ADD",
1272
+ payload: { obj: mkSceneObj(id, "point", label, {
1273
+ constraint: { kind: "excenter", vertices: [ids[0], ids[1], ids[2]], opposite: ids[0] }
1274
+ }) }
1275
+ });
1276
+ }
1277
+ };
1278
+ var tangencyPointTool = {
1279
+ key: "tangencyPoint",
1280
+ finalize(ctx) {
1281
+ const circleId = findPickIdByKind(ctx, "circle");
1282
+ const lineId = findPickIdByKind(ctx, "line");
1283
+ if (!circleId || !lineId) return;
1284
+ const id = freshId(ctx, "tp");
1285
+ const label = ctx.nextLabel("point");
1286
+ ctx.store.dispatch({
1287
+ type: "ADD",
1288
+ payload: { obj: mkSceneObj(id, "point", label, {
1289
+ constraint: { kind: "tangencyPoint", circle: circleId, onLine: lineId }
1290
+ }) }
1291
+ });
1292
+ }
1293
+ };
1294
+ var secondIntersectionTool = {
1295
+ key: "secondIntersection",
1296
+ finalize(ctx) {
1297
+ const lineId = findPickIdByKind(ctx, "line");
1298
+ const circleId = findPickIdByKind(ctx, "circle");
1299
+ const otherId = findPickIdByKind(ctx, "point");
1300
+ if (!lineId || !circleId || !otherId) return;
1301
+ const id = freshId(ctx, "X");
1302
+ const label = ctx.nextLabel("point");
1303
+ ctx.store.dispatch({
1304
+ type: "ADD",
1305
+ payload: { obj: mkSceneObj(id, "point", label, {
1306
+ constraint: { kind: "secondIntersection", line: lineId, circle: circleId, other: otherId }
1307
+ }) }
1308
+ });
1309
+ }
1310
+ };
1311
+ var arcMidpointTool = {
1312
+ key: "arcMidpoint",
1313
+ finalize(ctx) {
1314
+ const circleId = findPickIdByKind(ctx, "circle");
1315
+ const picks = ctx.pendingRef.current;
1316
+ const allIds = ctx.pendingIdsRef.current;
1317
+ const ptIds = [];
1318
+ for (let i = 0; i < picks.length; i += 1) {
1319
+ if (objKind(picks[i]) === "point" && allIds[i]) ptIds.push(allIds[i]);
891
1320
  }
892
- case "circumcenter": {
893
- const id = freshId(ctx, "o");
1321
+ if (!circleId || ptIds.length < 3) return;
1322
+ const id = freshId(ctx, "M");
1323
+ const label = ctx.nextLabel("point");
1324
+ ctx.store.dispatch({
1325
+ type: "ADD",
1326
+ payload: { obj: mkSceneObj(id, "point", label, {
1327
+ constraint: { kind: "arcMidpoint", circle: circleId, a: ptIds[0], b: ptIds[1], notContaining: ptIds[2] }
1328
+ }) }
1329
+ });
1330
+ }
1331
+ };
1332
+ var circleIntersectionTool = {
1333
+ key: "circleIntersection",
1334
+ finalize(ctx) {
1335
+ const ids = ctx.pendingIdsRef.current;
1336
+ for (const which of [0, 1]) {
1337
+ const id = freshId(ctx, "X");
894
1338
  const label = ctx.nextLabel("point");
895
1339
  ctx.store.dispatch({
896
1340
  type: "ADD",
897
1341
  payload: { obj: mkSceneObj(id, "point", label, {
898
- constraint: { kind: "circumcenter", vertices: [ids[0], ids[1], ids[2]] }
1342
+ constraint: { kind: "circleIntersection", c1: ids[0], c2: ids[1], which }
899
1343
  }) }
900
1344
  });
901
- return;
902
1345
  }
903
- case "incenter": {
904
- const id = freshId(ctx, "i");
1346
+ }
1347
+ };
1348
+ var tangentPointExtTool = {
1349
+ key: "tangentPointExt",
1350
+ finalize(ctx) {
1351
+ const fromId = findPickIdByKind(ctx, "point");
1352
+ const circleId = findPickIdByKind(ctx, "circle");
1353
+ if (!fromId || !circleId) return;
1354
+ for (const which of [0, 1]) {
1355
+ const id = freshId(ctx, "T");
905
1356
  const label = ctx.nextLabel("point");
906
1357
  ctx.store.dispatch({
907
1358
  type: "ADD",
908
1359
  payload: { obj: mkSceneObj(id, "point", label, {
909
- constraint: { kind: "incenter", vertices: [ids[0], ids[1], ids[2]] }
1360
+ constraint: { kind: "tangentPointExt", from: fromId, circle: circleId, which }
910
1361
  }) }
911
1362
  });
912
- return;
913
1363
  }
914
- case "orthocenter": {
915
- const id = freshId(ctx, "h");
916
- const label = ctx.nextLabel("point");
917
- ctx.store.dispatch({
918
- type: "ADD",
919
- payload: { obj: mkSceneObj(id, "point", label, {
920
- constraint: { kind: "orthocenter", vertices: [ids[0], ids[1], ids[2]] }
921
- }) }
922
- });
923
- return;
1364
+ }
1365
+ };
1366
+ var pointOnTool = {
1367
+ key: "pointOn",
1368
+ finalize(ctx, _toolDef, clickXY) {
1369
+ const obj = ctx.pendingRef.current[0];
1370
+ const objId = ctx.pendingIdsRef.current[0];
1371
+ if (!obj || !objId) return;
1372
+ const kind = objKind(obj);
1373
+ const id = freshId(ctx, "p");
1374
+ const label = ctx.nextLabel("point");
1375
+ const px = clickXY?.x ?? 0;
1376
+ const py = clickXY?.y ?? 0;
1377
+ let constraint = null;
1378
+ if (kind === "circle") {
1379
+ const o = obj.center ?? obj.midpoint;
1380
+ const ox = o ? o.X() : 0;
1381
+ const oy = o ? o.Y() : 0;
1382
+ constraint = { kind: "onCircle", circleId: objId, theta: Math.atan2(py - oy, px - ox) };
1383
+ } else if (kind === "line") {
1384
+ const elType = (obj.elType || "").toString().toLowerCase();
1385
+ const p1 = obj.point1;
1386
+ const p2 = obj.point2;
1387
+ let t = 0;
1388
+ if (p1 && p2) {
1389
+ const dx = p2.X() - p1.X();
1390
+ const dy = p2.Y() - p1.Y();
1391
+ const len2 = dx * dx + dy * dy || 1;
1392
+ t = ((px - p1.X()) * dx + (py - p1.Y()) * dy) / len2;
1393
+ }
1394
+ constraint = elType === "segment" ? { kind: "onSegment", segmentId: objId, t } : { kind: "onLine", lineId: objId, t };
924
1395
  }
925
- case "angle": {
926
- const id = freshId(ctx, "ang");
927
- const label = ctx.nextLabel("angle");
928
- ctx.store.dispatch({
929
- type: "ADD",
930
- payload: { obj: mkSceneObj(id, "angle", label, {
931
- p1: ids[0],
932
- vertex: ids[1],
933
- p2: ids[2]
934
- }) }
935
- });
1396
+ if (!constraint) return;
1397
+ ctx.store.dispatch({ type: "ADD", payload: { obj: mkSceneObj(id, "point", label, { constraint }) } });
1398
+ }
1399
+ };
1400
+
1401
+ // src/stamps/geometry-2d/editor/handlers/finalize/polygons.ts
1402
+ var polygons_exports = {};
1403
+ __export(polygons_exports, {
1404
+ isoTrapezoidTool: () => isoTrapezoidTool,
1405
+ isoTriangleTool: () => isoTriangleTool,
1406
+ parallelogramTool: () => parallelogramTool,
1407
+ rectangleTool: () => rectangleTool,
1408
+ rhombusTool: () => rhombusTool,
1409
+ rightTriangleTool: () => rightTriangleTool,
1410
+ squareTool: () => squareTool
1411
+ });
1412
+ var squareTool = {
1413
+ key: "square",
1414
+ finalize(ctx) {
1415
+ const ids = ctx.pendingIdsRef.current;
1416
+ const id = freshId(ctx, "sq");
1417
+ const label = ctx.nextLabel("polygon");
1418
+ ctx.store.dispatch({
1419
+ type: "ADD",
1420
+ payload: { obj: mkSceneObj(id, "polygon", label, {
1421
+ construction: { kind: "square", p1: ids[0], p2: ids[1] }
1422
+ }) }
1423
+ });
1424
+ }
1425
+ };
1426
+ var rectangleTool = {
1427
+ key: "rectangle",
1428
+ finalize(ctx) {
1429
+ const ids = ctx.pendingIdsRef.current;
1430
+ const [aId, bId, cId] = ids;
1431
+ const P = readJxgPos(ctx, cId);
1432
+ const Aj = readJxgPos(ctx, aId);
1433
+ const Bj = readJxgPos(ctx, bId);
1434
+ const t = computePerpendicularT(P, Bj, Aj, Bj);
1435
+ const polyId = freshId(ctx, "rect");
1436
+ const label = ctx.nextLabel("polygon");
1437
+ ctx.store.dispatch({
1438
+ type: "TRANSACTION",
1439
+ payload: { actions: [
1440
+ { type: "UPDATE_ATTRS", payload: { id: cId, patch: {
1441
+ constraint: { kind: "onPerpendicular", through: bId, perpToA: aId, perpToB: bId, t }
1442
+ } } },
1443
+ { type: "ADD", payload: { obj: mkSceneObj(polyId, "polygon", label, {
1444
+ construction: { kind: "rectangle", p1: aId, p2: bId, p3: cId }
1445
+ }) } }
1446
+ ] }
1447
+ });
1448
+ }
1449
+ };
1450
+ var rhombusTool = {
1451
+ key: "rhombus",
1452
+ finalize(ctx) {
1453
+ const ids = ctx.pendingIdsRef.current;
1454
+ const [aId, bId, cId] = ids;
1455
+ const P = readJxgPos(ctx, cId);
1456
+ const Bj = readJxgPos(ctx, bId);
1457
+ const theta = computeCircleTheta(P, Bj);
1458
+ const polyId = freshId(ctx, "rho");
1459
+ const label = ctx.nextLabel("polygon");
1460
+ ctx.store.dispatch({
1461
+ type: "TRANSACTION",
1462
+ payload: { actions: [
1463
+ { type: "UPDATE_ATTRS", payload: { id: cId, patch: {
1464
+ constraint: { kind: "onCircleAroundPoint", center: bId, radiusPoint: aId, theta }
1465
+ } } },
1466
+ { type: "ADD", payload: { obj: mkSceneObj(polyId, "polygon", label, {
1467
+ construction: { kind: "rhombus", p1: aId, p2: bId, p3: cId }
1468
+ }) } }
1469
+ ] }
1470
+ });
1471
+ }
1472
+ };
1473
+ function finalizePgmTrap(ctx, key) {
1474
+ const ids = ctx.pendingIdsRef.current;
1475
+ const [aId, bId, cId] = ids;
1476
+ const prefix = key === "parallelogram" ? "pgm" : "trap";
1477
+ const polyId = freshId(ctx, prefix);
1478
+ const label = ctx.nextLabel("polygon");
1479
+ ctx.store.dispatch({
1480
+ type: "ADD",
1481
+ payload: { obj: mkSceneObj(polyId, "polygon", label, {
1482
+ construction: { kind: key, p1: aId, p2: bId, p3: cId }
1483
+ }) }
1484
+ });
1485
+ }
1486
+ var parallelogramTool = {
1487
+ key: "parallelogram",
1488
+ finalize(ctx) {
1489
+ finalizePgmTrap(ctx, "parallelogram");
1490
+ }
1491
+ };
1492
+ var isoTrapezoidTool = {
1493
+ key: "isoTrapezoid",
1494
+ finalize(ctx) {
1495
+ finalizePgmTrap(ctx, "isoTrapezoid");
1496
+ }
1497
+ };
1498
+ var isoTriangleTool = {
1499
+ key: "isoTriangle",
1500
+ finalize(ctx) {
1501
+ const ids = ctx.pendingIdsRef.current;
1502
+ const [b1Id, b2Id, apexId] = ids;
1503
+ const P = readJxgPos(ctx, apexId);
1504
+ const B1 = readJxgPos(ctx, b1Id);
1505
+ const B2 = readJxgPos(ctx, b2Id);
1506
+ const t = computePerpBisectorT(P, B1, B2);
1507
+ const polyId = freshId(ctx, "iso");
1508
+ const label = ctx.nextLabel("polygon");
1509
+ ctx.store.dispatch({
1510
+ type: "TRANSACTION",
1511
+ payload: { actions: [
1512
+ { type: "UPDATE_ATTRS", payload: { id: apexId, patch: {
1513
+ constraint: { kind: "onPerpBisector", p1: b1Id, p2: b2Id, t }
1514
+ } } },
1515
+ { type: "ADD", payload: { obj: mkSceneObj(polyId, "polygon", label, {
1516
+ construction: { kind: "isoTriangle", base1: b1Id, base2: b2Id, apex: apexId }
1517
+ }) } }
1518
+ ] }
1519
+ });
1520
+ }
1521
+ };
1522
+ var rightTriangleTool = {
1523
+ key: "rightTriangle",
1524
+ finalize(ctx) {
1525
+ const ids = ctx.pendingIdsRef.current;
1526
+ const [rId, p1Id, p2Id] = ids;
1527
+ const P = readJxgPos(ctx, p2Id);
1528
+ const R = readJxgPos(ctx, rId);
1529
+ const P1 = readJxgPos(ctx, p1Id);
1530
+ const t = computePerpendicularT(P, R, R, P1);
1531
+ const polyId = freshId(ctx, "rtri");
1532
+ const label = ctx.nextLabel("polygon");
1533
+ ctx.store.dispatch({
1534
+ type: "TRANSACTION",
1535
+ payload: { actions: [
1536
+ { type: "UPDATE_ATTRS", payload: { id: p2Id, patch: {
1537
+ constraint: { kind: "onPerpendicular", through: rId, perpToA: rId, perpToB: p1Id, t }
1538
+ } } },
1539
+ { type: "ADD", payload: { obj: mkSceneObj(polyId, "polygon", label, {
1540
+ construction: { kind: "rightTriangle", rightAngle: rId, leg1End: p1Id, leg2End: p2Id }
1541
+ }) } }
1542
+ ] }
1543
+ });
1544
+ }
1545
+ };
1546
+
1547
+ // src/stamps/geometry-2d/editor/handlers/finalize/measure.ts
1548
+ var measure_exports = {};
1549
+ __export(measure_exports, {
1550
+ angleTool: () => angleTool,
1551
+ distanceTool: () => distanceTool,
1552
+ intersectTool: () => intersectTool
1553
+ });
1554
+ var angleTool = {
1555
+ key: "angle",
1556
+ finalize(ctx) {
1557
+ const ids = ctx.pendingIdsRef.current;
1558
+ const id = freshId(ctx, "ang");
1559
+ const label = ctx.nextLabel("angle");
1560
+ ctx.store.dispatch({
1561
+ type: "ADD",
1562
+ payload: { obj: mkSceneObj(id, "angle", label, {
1563
+ p1: ids[0],
1564
+ vertex: ids[1],
1565
+ p2: ids[2]
1566
+ }) }
1567
+ });
1568
+ }
1569
+ };
1570
+ var distanceTool = {
1571
+ key: "distance",
1572
+ finalize(ctx) {
1573
+ const ids = ctx.pendingIdsRef.current;
1574
+ const id = freshId(ctx, "d");
1575
+ const label = ctx.nextLabel("distance");
1576
+ ctx.store.dispatch({
1577
+ type: "ADD",
1578
+ payload: { obj: mkSceneObj(id, "distance", label, { p1: ids[0], p2: ids[1] }) }
1579
+ });
1580
+ }
1581
+ };
1582
+ var intersectTool = {
1583
+ key: "intersect",
1584
+ finalize(ctx) {
1585
+ const ids = ctx.pendingIdsRef.current;
1586
+ const picks = ctx.pendingRef.current;
1587
+ const pendIds = ctx.pendingIdsRef.current;
1588
+ const aIdx = pendIds.indexOf(ids[0]);
1589
+ const bIdx = pendIds.indexOf(ids[1]);
1590
+ if (aIdx < 0 || bIdx < 0) return;
1591
+ const aKind = objKind(picks[aIdx]);
1592
+ const bKind = objKind(picks[bIdx]);
1593
+ if (aKind === "line" && bKind === "line") {
1594
+ dispatchAddIntersection(ctx, { kind: "lineLine", ref1: ids[0], ref2: ids[1] });
936
1595
  return;
937
1596
  }
938
- case "distance": {
939
- const id = freshId(ctx, "d");
940
- const label = ctx.nextLabel("distance");
941
- ctx.store.dispatch({
942
- type: "ADD",
943
- payload: { obj: mkSceneObj(id, "distance", label, { p1: ids[0], p2: ids[1] }) }
1597
+ const isLineCircle = aKind === "line" && bKind === "circle" || aKind === "circle" && bKind === "line";
1598
+ const isCircleCircle = aKind === "circle" && bKind === "circle";
1599
+ if (!isLineCircle && !isCircleCircle) return;
1600
+ for (const branch of [0, 1]) {
1601
+ dispatchAddIntersection(ctx, {
1602
+ kind: isLineCircle ? "lineCircle" : "circleCircle",
1603
+ ref1: ids[0],
1604
+ ref2: ids[1],
1605
+ branch
944
1606
  });
945
- return;
946
- }
947
- case "intersect": {
948
- const picks = ctx.pendingRef.current;
949
- const pendIds = ctx.pendingIdsRef.current;
950
- const aIdx = pendIds.indexOf(ids[0]);
951
- const bIdx = pendIds.indexOf(ids[1]);
952
- if (aIdx < 0 || bIdx < 0) return;
953
- const aKind = objKind(picks[aIdx]);
954
- const bKind = objKind(picks[bIdx]);
955
- if (aKind === "line" && bKind === "line") {
956
- dispatchAddIntersection(ctx, { kind: "lineLine", ref1: ids[0], ref2: ids[1] });
957
- return;
958
- }
959
- const isLineCircle = aKind === "line" && bKind === "circle" || aKind === "circle" && bKind === "line";
960
- const isCircleCircle = aKind === "circle" && bKind === "circle";
961
- if (!isLineCircle && !isCircleCircle) return;
962
- for (const branch of [0, 1]) {
963
- dispatchAddIntersection(ctx, {
964
- kind: isLineCircle ? "lineCircle" : "circleCircle",
965
- ref1: ids[0],
966
- ref2: ids[1],
967
- branch
968
- });
969
- }
970
- return;
971
1607
  }
972
- default:
973
- return;
974
1608
  }
1609
+ };
1610
+
1611
+ // src/stamps/geometry-2d/editor/handlers/finalize/registry.ts
1612
+ var ALL = [
1613
+ ...Object.values(lines_exports),
1614
+ ...Object.values(circles_exports),
1615
+ ...Object.values(points_exports),
1616
+ ...Object.values(polygons_exports),
1617
+ ...Object.values(measure_exports)
1618
+ ].filter((m) => !!m && typeof m.finalize === "function");
1619
+ var TOOL_MODULES = new Map(ALL.map((m) => [m.key, m]));
1620
+
1621
+ // src/stamps/geometry-2d/editor/handlers/finalizeShape.ts
1622
+ function finalizeShape(ctx, toolDef, clickXY) {
1623
+ TOOL_MODULES.get(toolDef.key)?.finalize(ctx, toolDef, clickXY);
975
1624
  }
976
1625
 
977
1626
  // src/stamps/geometry-2d/editor/transforms.ts
@@ -1013,6 +1662,20 @@ function finalizeTransform(ctx, tool, pendingIds, value) {
1013
1662
  });
1014
1663
  return;
1015
1664
  }
1665
+ if (tool === "circleCR") {
1666
+ const r = Math.abs(value);
1667
+ if (!(r > 0)) {
1668
+ ctx.flashWarn("B\xE1n k\xEDnh ph\u1EA3i > 0");
1669
+ return;
1670
+ }
1671
+ const id = freshId(ctx, "c");
1672
+ const label = ctx.nextLabel("circle");
1673
+ ctx.store.dispatch({
1674
+ type: "ADD",
1675
+ payload: { obj: mkSceneObj(id, "circle", label, { center: pendingIds[0], radius: r }) }
1676
+ });
1677
+ return;
1678
+ }
1016
1679
  const sourceId = pendingIds[0];
1017
1680
  const state = ctx.store.getState();
1018
1681
  const source = state.objects[sourceId];
@@ -1206,7 +1869,7 @@ function handleMultiClickTool(ctx, toolDef, e, x, y, hits, bestHit) {
1206
1869
  }
1207
1870
  if (ctx.pendingIdsRef.current.length >= toolDef.needs) {
1208
1871
  const tk = toolDef.key;
1209
- if (tk === "rotate" || tk === "dilate" || tk === "regularPolygon") {
1872
+ if (tk === "rotate" || tk === "dilate" || tk === "regularPolygon" || tk === "circleCR") {
1210
1873
  const cx = (e.clientX ?? 0) + 8;
1211
1874
  const cy = (e.clientY ?? 0) + 8;
1212
1875
  ctx.pendingTransformRef.current = {
@@ -1222,7 +1885,7 @@ function handleMultiClickTool(ctx, toolDef, e, x, y, hits, bestHit) {
1222
1885
  ctx.clearPending();
1223
1886
  return;
1224
1887
  }
1225
- finalizeShape(ctx, toolDef);
1888
+ finalizeShape(ctx, toolDef, { x, y });
1226
1889
  ctx.clearPending();
1227
1890
  } else {
1228
1891
  ctx.refreshPreview();
@@ -1756,10 +2419,7 @@ function useAxisGridSync({
1756
2419
  }
1757
2420
  function useEditorShortcuts({
1758
2421
  store,
1759
- pendingIdsRef,
1760
2422
  selectedSetRef,
1761
- clearPending,
1762
- clearSelection,
1763
2423
  deleteSelection
1764
2424
  }) {
1765
2425
  useEffect(() => {
@@ -1781,18 +2441,6 @@ function useEditorShortcuts({
1781
2441
  store.redo();
1782
2442
  return;
1783
2443
  }
1784
- if (e.key === "Escape" && !inField) {
1785
- if (pendingIdsRef.current.length > 0) {
1786
- e.preventDefault();
1787
- e.stopPropagation();
1788
- clearPending();
1789
- }
1790
- if (selectedSetRef.current.size > 0) {
1791
- e.preventDefault();
1792
- e.stopPropagation();
1793
- clearSelection();
1794
- }
1795
- }
1796
2444
  if ((e.key === "Delete" || e.key === "Backspace") && !inField && selectedSetRef.current.size > 0) {
1797
2445
  e.preventDefault();
1798
2446
  e.stopPropagation();
@@ -1801,7 +2449,7 @@ function useEditorShortcuts({
1801
2449
  };
1802
2450
  window.addEventListener("keydown", onKey, { capture: true });
1803
2451
  return () => window.removeEventListener("keydown", onKey, { capture: true });
1804
- }, [store, pendingIdsRef, selectedSetRef, clearPending, clearSelection, deleteSelection]);
2452
+ }, [store, selectedSetRef, deleteSelection]);
1805
2453
  }
1806
2454
  function useJxgSceneIdMap({ store, rendererRef }) {
1807
2455
  const jxgIdToSceneRef = useRef(/* @__PURE__ */ new Map());
@@ -2005,10 +2653,7 @@ var MiniBoard2D = forwardRef(function MiniBoard2D2({ onReady, store, selectedToo
2005
2653
  };
2006
2654
  useEditorShortcuts({
2007
2655
  store,
2008
- pendingIdsRef: toolSM.pendingIdsRef,
2009
2656
  selectedSetRef,
2010
- clearPending,
2011
- clearSelection,
2012
2657
  deleteSelection
2013
2658
  });
2014
2659
  useAxisGridSync({ boardRef, axisObjsRef, isDarkRef, showAxis, showGrid });
@@ -2550,7 +3195,8 @@ var MultiPropertiesPopover = (props) => {
2550
3195
  var LABELS = {
2551
3196
  rotate: { aria: "G\xF3c quay", label: "G\xF3c (\xB0)", step: 15 },
2552
3197
  dilate: { aria: "T\u1EF7 s\u1ED1 k", label: "T\u1EF7 s\u1ED1 k", step: 0.5 },
2553
- regularPolygon: { aria: "S\u1ED1 c\u1EA1nh \u0111a gi\xE1c \u0111\u1EC1u", label: "S\u1ED1 c\u1EA1nh (n \u2265 3)", step: 1, min: 3 }
3198
+ regularPolygon: { aria: "S\u1ED1 c\u1EA1nh \u0111a gi\xE1c \u0111\u1EC1u", label: "S\u1ED1 c\u1EA1nh (n \u2265 3)", step: 1, min: 3 },
3199
+ circleCR: { aria: "B\xE1n k\xEDnh \u0111\u01B0\u1EDDng tr\xF2n", label: "B\xE1n k\xEDnh", step: 0.5, min: 0 }
2554
3200
  };
2555
3201
  var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel, isDark }) => {
2556
3202
  const [value, setValue] = useState(defaultValue);
@@ -2723,19 +3369,44 @@ function useAiFigure(generator, options = {}) {
2723
3369
  hasUnsupported
2724
3370
  };
2725
3371
  }
2726
- var BUILD_EXAMPLES = [
2727
- "Tam gi\xE1c ABC, d\u1EF1ng trung \u0111i\u1EC3m M c\u1EE7a BC",
2728
- "Tam gi\xE1c ABC vu\xF4ng t\u1EA1i A, AH l\xE0 \u0111\u01B0\u1EDDng cao xu\u1ED1ng BC",
2729
- "H\xECnh thoi ABCD, hai \u0111\u01B0\u1EDDng ch\xE9o c\u1EAFt nhau t\u1EA1i O",
2730
- "T\u1EEB \u0111i\u1EC3m M ngo\xE0i \u0111\u01B0\u1EDDng tr\xF2n (O), k\u1EBB hai ti\u1EBFp tuy\u1EBFn"
2731
- ];
2732
- var REFINE_EXAMPLES = [
2733
- "Th\xEAm trung \u0111i\u1EC3m M c\u1EE7a BC",
2734
- "D\u1EF1ng \u0111\u01B0\u1EDDng cao AH xu\u1ED1ng BC",
2735
- "V\u1EBD \u0111\u01B0\u1EDDng tr\xF2n ngo\u1EA1i ti\u1EBFp",
2736
- "Th\xEAm ti\u1EBFp tuy\u1EBFn t\u1EA1i A"
2737
- ];
2738
- function AiFigurePrompt({ generator, onGenerated, currentState }) {
3372
+ var PaperclipIcon = (props) => /* @__PURE__ */ jsx(
3373
+ "svg",
3374
+ {
3375
+ viewBox: "0 0 24 24",
3376
+ fill: "none",
3377
+ stroke: "currentColor",
3378
+ strokeWidth: 1.75,
3379
+ strokeLinecap: "round",
3380
+ strokeLinejoin: "round",
3381
+ "aria-hidden": true,
3382
+ ...props,
3383
+ children: /* @__PURE__ */ jsx("path", { d: "M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.41 17.41a2 2 0 1 1-2.83-2.83l8.49-8.49" })
3384
+ }
3385
+ );
3386
+ var ArrowUpIcon = (props) => /* @__PURE__ */ jsxs(
3387
+ "svg",
3388
+ {
3389
+ viewBox: "0 0 24 24",
3390
+ fill: "none",
3391
+ stroke: "currentColor",
3392
+ strokeWidth: 2.25,
3393
+ strokeLinecap: "round",
3394
+ strokeLinejoin: "round",
3395
+ "aria-hidden": true,
3396
+ ...props,
3397
+ children: [
3398
+ /* @__PURE__ */ jsx("path", { d: "M12 19V5" }),
3399
+ /* @__PURE__ */ jsx("path", { d: "m5 12 7-7 7 7" })
3400
+ ]
3401
+ }
3402
+ );
3403
+ var StopIcon = (props) => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, ...props, children: /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) });
3404
+ function AiFigurePrompt({
3405
+ generator,
3406
+ onGenerated,
3407
+ currentState,
3408
+ extractProblem = handleExtractProblem
3409
+ }) {
2739
3410
  const {
2740
3411
  prompt,
2741
3412
  setPrompt,
@@ -2755,18 +3426,96 @@ function AiFigurePrompt({ generator, onGenerated, currentState }) {
2755
3426
  setElapsed(0);
2756
3427
  return;
2757
3428
  }
2758
- setElapsed(0);
2759
3429
  const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
2760
3430
  return () => clearInterval(id);
2761
3431
  }, [isLoading]);
2762
- const handleSubmit = useCallback(
2763
- async (event) => {
2764
- event.preventDefault();
2765
- const generated = await submit();
2766
- if (generated) onGenerated(generated);
3432
+ const [image, setImage] = useState(null);
3433
+ const [ocrLoading, setOcrLoading] = useState(false);
3434
+ const [ocrError, setOcrError] = useState(null);
3435
+ const [ocrWarning, setOcrWarning] = useState(null);
3436
+ const [isDragOver, setIsDragOver] = useState(false);
3437
+ const fileInputRef = useRef(null);
3438
+ const textareaRef = useRef(null);
3439
+ const imagePreview = image ? `data:${image.mediaType};base64,${image.base64}` : null;
3440
+ useEffect(() => {
3441
+ setOcrError(null);
3442
+ setOcrWarning(null);
3443
+ }, [image]);
3444
+ const handleFile = useCallback(
3445
+ async (file) => {
3446
+ if (isLoading || ocrLoading) return;
3447
+ const v = validateFile(file);
3448
+ if (!v.ok) {
3449
+ setOcrError(v.message);
3450
+ return;
3451
+ }
3452
+ try {
3453
+ const part = await fileToImagePart(file);
3454
+ setImage(part);
3455
+ } catch (e) {
3456
+ setOcrError(e instanceof Error ? e.message : "Kh\xF4ng decode \u0111\u01B0\u1EE3c \u1EA3nh");
3457
+ }
3458
+ },
3459
+ [isLoading, ocrLoading]
3460
+ );
3461
+ const handleFileInput = useCallback(
3462
+ (e) => {
3463
+ const file = e.target.files?.[0];
3464
+ if (file) void handleFile(file);
3465
+ e.target.value = "";
3466
+ },
3467
+ [handleFile]
3468
+ );
3469
+ const handlePaste = useCallback(
3470
+ (e) => {
3471
+ const item = Array.from(e.clipboardData.items).find(
3472
+ (it) => it.kind === "file" && it.type.startsWith("image/")
3473
+ );
3474
+ if (!item) return;
3475
+ const file = item.getAsFile();
3476
+ if (!file) return;
3477
+ e.preventDefault();
3478
+ void handleFile(file);
3479
+ },
3480
+ [handleFile]
3481
+ );
3482
+ const handleDrop = useCallback(
3483
+ (e) => {
3484
+ e.preventDefault();
3485
+ setIsDragOver(false);
3486
+ const file = Array.from(e.dataTransfer.files).find(
3487
+ (f) => f.type.startsWith("image/")
3488
+ );
3489
+ if (file) void handleFile(file);
2767
3490
  },
2768
- [onGenerated, submit]
3491
+ [handleFile]
2769
3492
  );
3493
+ const runOcr = useCallback(async () => {
3494
+ if (!image) return;
3495
+ setOcrLoading(true);
3496
+ setOcrError(null);
3497
+ setOcrWarning(null);
3498
+ try {
3499
+ const r = await extractProblem(image);
3500
+ if (r.kind === "success" || r.kind === "low-confidence") {
3501
+ setPrompt(r.text);
3502
+ if (r.kind === "low-confidence") setOcrWarning(r.warning);
3503
+ requestAnimationFrame(() => textareaRef.current?.focus());
3504
+ } else {
3505
+ setOcrError(r.message);
3506
+ }
3507
+ } finally {
3508
+ setOcrLoading(false);
3509
+ }
3510
+ }, [image, setPrompt, extractProblem]);
3511
+ const handleSendClick = useCallback(async () => {
3512
+ if (image && !prompt.trim() && !ocrLoading) {
3513
+ await runOcr();
3514
+ return;
3515
+ }
3516
+ const generated = await submit();
3517
+ if (generated) onGenerated(generated);
3518
+ }, [image, prompt, ocrLoading, runOcr, submit, onGenerated]);
2770
3519
  const handleSwitchToBuild = useCallback(() => {
2771
3520
  if (currentState && currentState.order.length > 0) {
2772
3521
  const ok = window.confirm(
@@ -2776,109 +3525,256 @@ function AiFigurePrompt({ generator, onGenerated, currentState }) {
2776
3525
  }
2777
3526
  setMode("build");
2778
3527
  }, [currentState, setMode]);
2779
- const primaryLabel = isLoading ? tokens > 0 ? `\u0110ang d\u1EF1ng ${tokens}tok / ${elapsed}s \u2014 Hu\u1EF7` : `\u0110ang d\u1EF1ng... ${elapsed}s \u2014 Hu\u1EF7` : "D\u1EF1ng b\u1EB1ng AI";
2780
3528
  const hasContent = currentState != null && currentState.order.length > 0;
2781
- const examples = mode === "refine" ? REFINE_EXAMPLES : BUILD_EXAMPLES;
3529
+ const promptEmpty = !prompt.trim();
3530
+ const willOcr = image != null && promptEmpty;
3531
+ const sendDisabled = !image && promptEmpty || ocrLoading || isLoading && !willOcr;
2782
3532
  const refineChipLabel = entityCount.points + entityCount.shapes > 0 ? `Th\xEAm v\xE0o \xB7 ${entityCount.points}\u0111, ${entityCount.shapes}\u0111o\u1EA1n` : "Th\xEAm v\xE0o";
2783
- return /* @__PURE__ */ jsxs(
2784
- "form",
2785
- {
2786
- "data-testid": "geometry-ai-form",
2787
- onSubmit: (event) => {
2788
- void handleSubmit(event);
2789
- },
2790
- className: "border-b border-slate-200 bg-slate-50 px-3 py-2",
2791
- children: [
3533
+ const placeholder = willOcr ? "B\u1EA5m g\u1EEDi \u0111\u1EC3 \u0111\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh \u2014 ho\u1EB7c t\u1EF1 g\xF5 \u1EDF \u0111\xE2y\u2026" : mode === "refine" ? "M\xF4 t\u1EA3 ph\u1EA7n c\u1EA7n th\xEAm (vd: trung \u0111i\u1EC3m M c\u1EE7a BC). C\xF3 th\u1EC3 d\xE1n \u1EA3nh \u0111\u1EC1 (Ctrl+V)." : "M\xF4 t\u1EA3 \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng \u2014 ho\u1EB7c d\xE1n/\u0111\xEDnh \u1EA3nh \u0111\u1EC1 (Ctrl+V).";
3534
+ return /* @__PURE__ */ jsxs("div", { className: "border-b border-slate-200 bg-slate-50 px-3 py-3", children: [
3535
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
3536
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium tracking-wide text-slate-600", children: "D\u1EF1ng h\xECnh b\u1EB1ng AI" }),
3537
+ hasContent && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", role: "tablist", "aria-label": "Ch\u1EBF \u0111\u1ED9 AI", children: [
2792
3538
  /* @__PURE__ */ jsx(
2793
- "label",
3539
+ "button",
2794
3540
  {
2795
- htmlFor: "geometry-ai-prompt",
2796
- className: "mb-1 block text-xs font-medium text-slate-600",
2797
- children: "D\u1EF1ng h\xECnh b\u1EB1ng AI"
3541
+ type: "button",
3542
+ role: "tab",
3543
+ "aria-selected": mode === "refine",
3544
+ "data-testid": "geometry-ai-mode-refine",
3545
+ onClick: () => setMode("refine"),
3546
+ disabled: isLoading || hasUnsupported,
3547
+ title: hasUnsupported ? "H\xECnh c\xF3 \u0111\u1ED1i t\u01B0\u1EE3ng ngo\xE0i DSL \u2014 ch\u1EC9 d\u1EF1ng m\u1EDBi \u0111\u01B0\u1EE3c" : refineChipLabel,
3548
+ className: `rounded-full px-2.5 py-0.5 text-[11px] transition ${mode === "refine" ? "bg-emerald-600 text-white shadow-sm" : "text-slate-500 hover:text-emerald-700"} ${hasUnsupported ? "cursor-not-allowed opacity-40" : ""}`,
3549
+ children: refineChipLabel
2798
3550
  }
2799
3551
  ),
2800
- hasContent && /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-2", children: [
2801
- /* @__PURE__ */ jsx(
2802
- "button",
2803
- {
2804
- type: "button",
2805
- "data-testid": "geometry-ai-mode-refine",
2806
- onClick: () => setMode("refine"),
2807
- disabled: isLoading || hasUnsupported,
2808
- className: `rounded-full border px-2 py-0.5 text-[11px] transition ${mode === "refine" ? "border-emerald-600 bg-emerald-100 text-emerald-800" : "border-slate-300 bg-white text-slate-600 hover:border-emerald-400"} ${hasUnsupported ? "cursor-not-allowed opacity-50" : ""}`,
2809
- title: hasUnsupported ? "H\xECnh hi\u1EC7n t\u1EA1i c\xF3 \u0111\u1ED1i t\u01B0\u1EE3ng ngo\xE0i DSL \u2014 ch\u1EC9 d\u1EF1ng m\u1EDBi \u0111\u01B0\u1EE3c" : refineChipLabel,
2810
- children: refineChipLabel
2811
- }
2812
- ),
2813
- /* @__PURE__ */ jsx(
2814
- "button",
2815
- {
2816
- type: "button",
2817
- "data-testid": "geometry-ai-mode-build",
2818
- onClick: handleSwitchToBuild,
2819
- disabled: isLoading,
2820
- className: `rounded-full border px-2 py-0.5 text-[11px] transition ${mode === "build" ? "border-emerald-600 bg-emerald-100 text-emerald-800" : "border-slate-300 bg-white text-slate-600 hover:border-emerald-400"}`,
2821
- children: "D\u1EF1ng m\u1EDBi"
2822
- }
2823
- ),
2824
- hasUnsupported && /* @__PURE__ */ jsx(
2825
- "span",
2826
- {
2827
- className: "text-[10px] text-amber-700",
2828
- "data-testid": "geometry-ai-unsupported-warning",
2829
- children: "H\xECnh c\xF3 \u0111\u1ED1i t\u01B0\u1EE3ng ngo\xE0i DSL"
2830
- }
2831
- )
2832
- ] }),
2833
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
3552
+ /* @__PURE__ */ jsx(
3553
+ "button",
3554
+ {
3555
+ type: "button",
3556
+ role: "tab",
3557
+ "aria-selected": mode === "build",
3558
+ "data-testid": "geometry-ai-mode-build",
3559
+ onClick: handleSwitchToBuild,
3560
+ disabled: isLoading,
3561
+ className: `rounded-full px-2.5 py-0.5 text-[11px] transition ${mode === "build" ? "bg-emerald-600 text-white shadow-sm" : "text-slate-500 hover:text-emerald-700"}`,
3562
+ children: "D\u1EF1ng m\u1EDBi"
3563
+ }
3564
+ )
3565
+ ] })
3566
+ ] }),
3567
+ hasUnsupported && /* @__PURE__ */ jsx(
3568
+ "p",
3569
+ {
3570
+ className: "mb-1.5 text-[10px] text-amber-700",
3571
+ "data-testid": "geometry-ai-unsupported-warning",
3572
+ children: "H\xECnh c\xF3 \u0111\u1ED1i t\u01B0\u1EE3ng ngo\xE0i DSL \u2014 ch\u1EC9 d\u1EF1ng m\u1EDBi \u0111\u01B0\u1EE3c"
3573
+ }
3574
+ ),
3575
+ /* @__PURE__ */ jsxs(
3576
+ "div",
3577
+ {
3578
+ onDragOver: (e) => {
3579
+ e.preventDefault();
3580
+ setIsDragOver(true);
3581
+ },
3582
+ onDragLeave: () => setIsDragOver(false),
3583
+ onDrop: handleDrop,
3584
+ onPaste: handlePaste,
3585
+ "aria-label": "Khu v\u1EF1c k\xE9o th\u1EA3 \u1EA3nh",
3586
+ role: "region",
3587
+ className: "group relative flex flex-col rounded-2xl bg-white shadow-sm transition-all duration-150 ring-1 ring-slate-200 focus-within:ring-2 focus-within:ring-emerald-400/70 focus-within:shadow-md " + (isDragOver ? "ring-2 ring-emerald-500 bg-emerald-50/40" : ""),
3588
+ children: [
3589
+ image && imagePreview && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 px-3 pt-2.5", children: /* @__PURE__ */ jsxs("div", { className: "group/chip relative", children: [
3590
+ /* @__PURE__ */ jsx(
3591
+ "img",
3592
+ {
3593
+ src: imagePreview,
3594
+ alt: "\u1EA2nh \u0111\u1EC1 b\xE0i",
3595
+ className: "max-h-48 max-w-full h-auto w-auto rounded-lg border border-slate-200 shadow-sm"
3596
+ }
3597
+ ),
3598
+ /* @__PURE__ */ jsx(
3599
+ "button",
3600
+ {
3601
+ type: "button",
3602
+ onClick: () => setImage(null),
3603
+ disabled: ocrLoading || isLoading,
3604
+ "aria-label": "Xo\xE1 \u1EA3nh",
3605
+ className: "absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-slate-900/85 text-[11px] font-medium text-white shadow ring-2 ring-white transition hover:bg-slate-900 disabled:opacity-50",
3606
+ children: "\xD7"
3607
+ }
3608
+ )
3609
+ ] }) }),
2834
3610
  /* @__PURE__ */ jsx(
2835
3611
  "textarea",
2836
3612
  {
3613
+ ref: textareaRef,
2837
3614
  id: "geometry-ai-prompt",
2838
3615
  "aria-label": "\u0110\u1EC1 b\xE0i cho AI",
3616
+ "data-testid": "geometry-ai-textarea",
2839
3617
  value: prompt,
2840
- onChange: (event) => setPrompt(event.target.value),
3618
+ onChange: (e) => setPrompt(e.target.value),
3619
+ onKeyDown: (e) => {
3620
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !sendDisabled) {
3621
+ e.preventDefault();
3622
+ void handleSendClick();
3623
+ }
3624
+ },
2841
3625
  disabled: isLoading,
2842
3626
  rows: 2,
2843
- placeholder: mode === "refine" ? "V\xED d\u1EE5: th\xEAm trung \u0111i\u1EC3m M c\u1EE7a BC" : "V\xED d\u1EE5: Cho tam gi\xE1c ABC, d\u1EF1ng \u0111\u01B0\u1EDDng cao AH.",
2844
- className: "min-h-12 flex-1 resize-none rounded border border-slate-300 bg-white px-2 py-1.5 text-xs text-slate-800 outline-none focus:border-emerald-500 disabled:opacity-60"
3627
+ placeholder,
3628
+ className: "block w-full resize-none rounded-2xl bg-transparent px-3.5 pt-2.5 pb-1 text-sm leading-relaxed text-slate-800 placeholder:text-slate-400 outline-none disabled:opacity-60 field-sizing-content max-h-44"
2845
3629
  }
2846
3630
  ),
2847
- isLoading ? /* @__PURE__ */ jsx(
2848
- "button",
2849
- {
2850
- type: "button",
2851
- onClick: cancel,
2852
- className: "rounded bg-amber-600 px-3 py-2 text-xs font-medium text-white transition hover:bg-amber-700",
2853
- children: primaryLabel
2854
- }
2855
- ) : /* @__PURE__ */ jsx(
2856
- "button",
2857
- {
2858
- type: "submit",
2859
- disabled: !prompt.trim(),
2860
- className: "rounded bg-emerald-600 px-3 py-2 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
2861
- children: primaryLabel
2862
- }
2863
- )
2864
- ] }),
2865
- error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-xs text-red-600", children: error }),
2866
- !isLoading && !prompt.trim() && !error && /* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex flex-wrap items-center gap-1", children: [
2867
- /* @__PURE__ */ jsx("span", { className: "text-[10px] text-slate-500", children: "G\u1EE3i \xFD:" }),
2868
- examples.map((ex) => /* @__PURE__ */ jsx(
2869
- "button",
2870
- {
2871
- type: "button",
2872
- onClick: () => setPrompt(ex),
2873
- className: "rounded-full border border-slate-300 bg-white px-2 py-0.5 text-[10px] text-slate-600 transition hover:border-emerald-400 hover:bg-emerald-50 hover:text-emerald-700",
2874
- children: ex
2875
- },
2876
- ex
2877
- ))
2878
- ] })
2879
- ]
2880
- }
2881
- );
3631
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-2 pb-2 pt-1", children: [
3632
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
3633
+ /* @__PURE__ */ jsx(
3634
+ "button",
3635
+ {
3636
+ type: "button",
3637
+ onClick: () => fileInputRef.current?.click(),
3638
+ disabled: isLoading || ocrLoading,
3639
+ "aria-label": "\u0110\xEDnh \u1EA3nh \u0111\u1EC1 b\xE0i",
3640
+ title: "\u0110\xEDnh \u1EA3nh (c\u0169ng c\xF3 th\u1EC3 d\xE1n b\u1EB1ng Ctrl+V ho\u1EB7c k\xE9o th\u1EA3)",
3641
+ className: "flex h-8 w-8 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-emerald-700 disabled:opacity-40",
3642
+ children: /* @__PURE__ */ jsx(PaperclipIcon, { className: "h-[18px] w-[18px]" })
3643
+ }
3644
+ ),
3645
+ /* @__PURE__ */ jsx(
3646
+ "input",
3647
+ {
3648
+ ref: fileInputRef,
3649
+ type: "file",
3650
+ accept: "image/png,image/jpeg,image/webp",
3651
+ className: "sr-only",
3652
+ onChange: handleFileInput,
3653
+ disabled: isLoading || ocrLoading,
3654
+ "aria-label": "Ch\u1ECDn \u1EA3nh \u0111\u1EC1 b\xE0i"
3655
+ }
3656
+ )
3657
+ ] }),
3658
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3659
+ (isLoading || ocrLoading) && /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] tabular-nums text-slate-500", children: ocrLoading ? "\u0111\u1ECDc \u1EA3nh\u2026" : tokens > 0 ? `${tokens}tok \xB7 ${elapsed}s` : `${elapsed}s` }),
3660
+ isLoading ? /* @__PURE__ */ jsx(
3661
+ "button",
3662
+ {
3663
+ type: "button",
3664
+ onClick: cancel,
3665
+ "aria-label": "Hu\u1EF7 d\u1EF1ng h\xECnh AI",
3666
+ "data-testid": "geometry-ai-cancel",
3667
+ title: `\u0110ang d\u1EF1ng\u2026 ${elapsed}s \u2014 b\u1EA5m \u0111\u1EC3 hu\u1EF7`,
3668
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-amber-500 text-white shadow-sm transition hover:scale-105 hover:bg-amber-600 active:scale-95",
3669
+ children: /* @__PURE__ */ jsx(StopIcon, { className: "h-3.5 w-3.5" })
3670
+ }
3671
+ ) : /* @__PURE__ */ jsx(
3672
+ "button",
3673
+ {
3674
+ type: "button",
3675
+ onClick: () => void handleSendClick(),
3676
+ disabled: sendDisabled,
3677
+ "aria-label": willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh" : "D\u1EF1ng b\u1EB1ng AI",
3678
+ title: willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh (s\u1EBD \u0111i\u1EC1n v\xE0o \xF4 chat)" : "D\u1EF1ng b\u1EB1ng AI (Ctrl/\u2318+Enter)",
3679
+ "data-testid": willOcr ? "geometry-ai-ocr" : "geometry-ai-submit",
3680
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-emerald-600 text-white shadow-sm transition hover:scale-105 hover:bg-emerald-700 active:scale-95 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:hover:scale-100",
3681
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, { className: "h-[18px] w-[18px]" })
3682
+ }
3683
+ )
3684
+ ] })
3685
+ ] }),
3686
+ isDragOver && /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl bg-emerald-50/60 text-xs font-medium text-emerald-700", children: "Th\u1EA3 \u1EA3nh v\xE0o \u0111\xE2y" })
3687
+ ]
3688
+ }
3689
+ ),
3690
+ /* @__PURE__ */ jsxs("p", { className: "mt-1.5 px-1 text-[10px] text-slate-500", children: [
3691
+ "D\xE1n \u1EA3nh (Ctrl+V), k\xE9o th\u1EA3, ho\u1EB7c b\u1EA5m ",
3692
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u{1F4CE}" }),
3693
+ " \u0111\u1EC3 \u0111\xEDnh \u1EA3nh \u0111\u1EC1."
3694
+ ] }),
3695
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error }),
3696
+ ocrError && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: ocrError }),
3697
+ ocrWarning && /* @__PURE__ */ jsx("p", { className: "mt-1 rounded bg-amber-50 px-2 py-1 text-[11px] text-amber-700", children: ocrWarning })
3698
+ ] });
3699
+ }
3700
+
3701
+ // src/stamps/geometry-2d/draft.ts
3702
+ function svgIntrinsicSize(svg) {
3703
+ const w = svg.match(/<svg[^>]*\swidth="([\d.]+)"/);
3704
+ const h = svg.match(/<svg[^>]*\sheight="([\d.]+)"/);
3705
+ if (w && h) return { width: parseFloat(w[1]), height: parseFloat(h[1]) };
3706
+ const vb = svg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/);
3707
+ if (vb) return { width: parseFloat(vb[1]), height: parseFloat(vb[2]) };
3708
+ return { width: 300, height: 200 };
3709
+ }
3710
+ function draftFromViewport(svg, appState, seq) {
3711
+ const { width, height } = svgIntrinsicSize(svg);
3712
+ const zoom = appState.zoom?.value ?? 1;
3713
+ const vw = appState.width ?? 800;
3714
+ const vh = appState.height ?? 600;
3715
+ const cx = appState.scrollX + vw / 2 / zoom;
3716
+ const cy = appState.scrollY + vh / 2 / zoom;
3717
+ return { svg, width, height, x: cx - width / 2, y: cy - height / 2, seq };
3718
+ }
3719
+ function didStateChange(seen, jsonState) {
3720
+ if (seen.last === jsonState) return false;
3721
+ seen.last = jsonState;
3722
+ return true;
3723
+ }
3724
+
3725
+ // src/stamps/geometry-2d/editor/useGeometryDraftEmit.ts
3726
+ function useGeometryDraftEmit({
3727
+ store,
3728
+ handleRef,
3729
+ api,
3730
+ showAxis,
3731
+ showGrid,
3732
+ onGeometryDraft,
3733
+ debounceMs = 350
3734
+ }) {
3735
+ const seqRef = useRef(0);
3736
+ const seenRef = useRef({ last: null });
3737
+ const timerRef = useRef(null);
3738
+ const cbRef = useRef(onGeometryDraft);
3739
+ cbRef.current = onGeometryDraft;
3740
+ useEffect(() => {
3741
+ if (!cbRef.current) return;
3742
+ const emit = () => {
3743
+ const h = handleRef.current;
3744
+ if (!h) return;
3745
+ const state = h.getState();
3746
+ if (Object.keys(state.objects).length === 0) {
3747
+ if (seenRef.current.last !== null) {
3748
+ seenRef.current.last = null;
3749
+ cbRef.current?.(null);
3750
+ }
3751
+ return;
3752
+ }
3753
+ const bbox = h.getBbox();
3754
+ const jsonState = serializeBoard(state, { bbox, showAxis, showGrid });
3755
+ if (!didStateChange(seenRef.current, jsonState)) return;
3756
+ void (async () => {
3757
+ try {
3758
+ const svg = await renderGeometrySvgFromState(jsonState);
3759
+ const appState = api?.getAppState?.() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
3760
+ seqRef.current += 1;
3761
+ cbRef.current?.(draftFromViewport(svg, appState, seqRef.current));
3762
+ } catch (err) {
3763
+ console.warn("[geometry] draft render failed:", err);
3764
+ }
3765
+ })();
3766
+ };
3767
+ const schedule = () => {
3768
+ if (timerRef.current) clearTimeout(timerRef.current);
3769
+ timerRef.current = setTimeout(emit, debounceMs);
3770
+ };
3771
+ const unsub = store.subscribe(schedule);
3772
+ return () => {
3773
+ unsub();
3774
+ if (timerRef.current) clearTimeout(timerRef.current);
3775
+ cbRef.current?.(null);
3776
+ };
3777
+ }, [store, handleRef, api, showAxis, showGrid, debounceMs]);
2882
3778
  }
2883
3779
  var GeometryEditorPanelInner = forwardRef(
2884
3780
  function GeometryEditorPanelInner2({
@@ -2898,7 +3794,9 @@ var GeometryEditorPanelInner = forwardRef(
2898
3794
  canUndo,
2899
3795
  canRedo,
2900
3796
  onSelectionChange,
2901
- generateGeometryFigure
3797
+ generateGeometryFigure,
3798
+ api,
3799
+ onGeometryDraft
2902
3800
  }, ref) {
2903
3801
  const { showToast } = useToast();
2904
3802
  const handleRef = useRef(null);
@@ -2911,12 +3809,14 @@ var GeometryEditorPanelInner = forwardRef(
2911
3809
  useEffect(() => {
2912
3810
  onSelectionChangeRef.current = onSelectionChange;
2913
3811
  }, [onSelectionChange]);
3812
+ const onGeometryDraftRef = useRef(onGeometryDraft);
3813
+ useEffect(() => {
3814
+ onGeometryDraftRef.current = onGeometryDraft;
3815
+ }, [onGeometryDraft]);
2914
3816
  useEditorState({ store, onHistoryChange });
2915
- const currentSceneState = useSyncExternalStore(
2916
- (cb) => store.subscribe(cb),
2917
- () => store.getState(),
2918
- () => store.getState()
2919
- );
3817
+ useGeometryDraftEmit({ store, handleRef, api, showAxis, showGrid, onGeometryDraft });
3818
+ const snap = () => store.getState();
3819
+ const currentSceneState = useSyncExternalStore((cb) => store.subscribe(cb), snap, snap);
2920
3820
  useEffect(() => {
2921
3821
  const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
2922
3822
  sync();
@@ -2926,22 +3826,22 @@ var GeometryEditorPanelInner = forwardRef(
2926
3826
  const h = handleRef.current;
2927
3827
  if (!h) return;
2928
3828
  setReady(true);
2929
- h.onSelect((snap) => {
2930
- setPropsPopover(snap);
3829
+ h.onSelect((snap2) => {
3830
+ setPropsPopover(snap2);
2931
3831
  setMultiSelection(null);
2932
- onSelectionChangeRef.current?.(snap.id);
3832
+ onSelectionChangeRef.current?.(snap2.id);
2933
3833
  });
2934
3834
  h.onTransformParam((info) => setTransformPopover(info));
2935
- h.onSelectionState((snap) => {
2936
- if (!snap || snap.ids.length === 0) {
3835
+ h.onSelectionState((snap2) => {
3836
+ if (!snap2 || snap2.ids.length === 0) {
2937
3837
  setPropsPopover(null);
2938
3838
  setMultiSelection(null);
2939
3839
  onSelectionChangeRef.current?.(void 0);
2940
3840
  return;
2941
3841
  }
2942
- if (snap.ids.length === 1) {
2943
- const id = snap.ids[0];
2944
- const single = buildObjectSnapshot(store.getState(), id, snap.anchor);
3842
+ if (snap2.ids.length === 1) {
3843
+ const id = snap2.ids[0];
3844
+ const single = buildObjectSnapshot(store.getState(), id, snap2.anchor);
2945
3845
  if (single) {
2946
3846
  setPropsPopover(single);
2947
3847
  setMultiSelection(null);
@@ -2949,7 +3849,7 @@ var GeometryEditorPanelInner = forwardRef(
2949
3849
  }
2950
3850
  return;
2951
3851
  }
2952
- setMultiSelection(snap);
3852
+ setMultiSelection(snap2);
2953
3853
  setPropsPopover(null);
2954
3854
  onSelectionChangeRef.current?.(void 0);
2955
3855
  });
@@ -2993,15 +3893,13 @@ var GeometryEditorPanelInner = forwardRef(
2993
3893
  try {
2994
3894
  const svgString = await renderGeometrySvgFromState(jsonState);
2995
3895
  onInsert(jsonState, svgString);
3896
+ onGeometryDraftRef.current?.(null);
2996
3897
  } catch (err) {
2997
3898
  console.error("Geometry insert failed:", err);
2998
3899
  }
2999
3900
  })();
3000
3901
  return true;
3001
3902
  }, [onInsert, showAxis, showGrid]);
3002
- const handleInsert = useCallback(() => {
3003
- performInsert();
3004
- }, [performInsert]);
3005
3903
  const loadAiFigure = useCallback((generated) => {
3006
3904
  handleRef.current?.clearSelection();
3007
3905
  setPropsPopover(null);
@@ -3096,7 +3994,7 @@ var GeometryEditorPanelInner = forwardRef(
3096
3994
  "button",
3097
3995
  {
3098
3996
  type: "button",
3099
- onClick: handleInsert,
3997
+ onClick: performInsert,
3100
3998
  disabled: !ready || !hasContent,
3101
3999
  title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
3102
4000
  "data-testid": "geometry-insert-btn-mobile",
@@ -3183,12 +4081,12 @@ var GeometryEditorPanelInner = forwardRef(
3183
4081
  onClose: dismissMultiPopover
3184
4082
  }
3185
4083
  ),
3186
- transformPopover && (transformPopover.tool === "rotate" || transformPopover.tool === "dilate" || transformPopover.tool === "regularPolygon") && /* @__PURE__ */ jsx(
4084
+ transformPopover && (transformPopover.tool === "rotate" || transformPopover.tool === "dilate" || transformPopover.tool === "regularPolygon" || transformPopover.tool === "circleCR") && /* @__PURE__ */ jsx(
3187
4085
  TransformParamPopover,
3188
4086
  {
3189
4087
  kind: transformPopover.tool,
3190
4088
  anchor: transformPopover.anchor,
3191
- defaultValue: transformPopover.tool === "rotate" ? 90 : transformPopover.tool === "dilate" ? 2 : 6,
4089
+ defaultValue: transformPopover.tool === "rotate" ? 90 : transformPopover.tool === "dilate" ? 2 : transformPopover.tool === "circleCR" ? 3 : 6,
3192
4090
  isDark,
3193
4091
  onConfirm: (v) => {
3194
4092
  handleRef.current?.confirmTransformParam(v);
@@ -3214,7 +4112,7 @@ var GeometryEditorPanelInner = forwardRef(
3214
4112
  /* @__PURE__ */ jsx(
3215
4113
  "button",
3216
4114
  {
3217
- onClick: handleInsert,
4115
+ onClick: performInsert,
3218
4116
  disabled: !ready || !hasContent,
3219
4117
  title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
3220
4118
  "data-testid": "geometry-insert-btn",
@@ -3270,7 +4168,7 @@ function parseInitialState(data) {
3270
4168
  return deserializeBoard(data.jsonState);
3271
4169
  }
3272
4170
  var GeometryStampHost = forwardRef(
3273
- function GeometryStampHost2({ api, editingElement, onClose, isDark, generateGeometryFigure }, ref) {
4171
+ function GeometryStampHost2({ api, editingElement, onClose, isDark, generateGeometryFigure, onGeometryDraft }, ref) {
3274
4172
  const panelRef = useRef(null);
3275
4173
  const { isMobile } = useIsMobile();
3276
4174
  const [drawerOpen, setDrawerOpen] = useState(false);
@@ -3385,7 +4283,9 @@ var GeometryStampHost = forwardRef(
3385
4283
  canUndo,
3386
4284
  canRedo,
3387
4285
  onSelectionChange: setSelectedObjectId,
3388
- generateGeometryFigure
4286
+ generateGeometryFigure,
4287
+ api,
4288
+ onGeometryDraft
3389
4289
  }
3390
4290
  )
3391
4291
  ] });
@@ -3393,5 +4293,5 @@ var GeometryStampHost = forwardRef(
3393
4293
  );
3394
4294
 
3395
4295
  export { GeometryStampHost };
3396
- //# sourceMappingURL=host-L7FMFZUW.mjs.map
3397
- //# sourceMappingURL=host-L7FMFZUW.mjs.map
4296
+ //# sourceMappingURL=host-4P766V4J.mjs.map
4297
+ //# sourceMappingURL=host-4P766V4J.mjs.map