@xom11/whiteboard 0.24.2 → 0.27.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 (103) hide show
  1. package/README.md +84 -11
  2. package/dist/{ExcalidrawWithMenus-WENZRYYE.mjs → ExcalidrawWithMenus-2QPPTXJM.mjs} +3 -2
  3. package/dist/ExcalidrawWithMenus-2QPPTXJM.mjs.map +1 -0
  4. package/dist/ai.d.mts +3217 -434
  5. package/dist/ai.d.ts +3217 -434
  6. package/dist/ai.js +7679 -598
  7. package/dist/ai.js.map +1 -1
  8. package/dist/ai.mjs +5707 -679
  9. package/dist/ai.mjs.map +1 -1
  10. package/dist/catalog.json +5 -5
  11. package/dist/{chunk-7WQXXEVR.mjs → chunk-4ETJ4CDY.mjs} +5 -5
  12. package/dist/{chunk-7WQXXEVR.mjs.map → chunk-4ETJ4CDY.mjs.map} +1 -1
  13. package/dist/chunk-AJAHD35N.mjs +1708 -0
  14. package/dist/chunk-AJAHD35N.mjs.map +1 -0
  15. package/dist/chunk-AYJPOHCI.mjs +265 -0
  16. package/dist/chunk-AYJPOHCI.mjs.map +1 -0
  17. package/dist/chunk-B4NJJZFR.mjs +18 -0
  18. package/dist/chunk-B4NJJZFR.mjs.map +1 -0
  19. package/dist/{chunk-AZIARTGX.mjs → chunk-BNBOIDO5.mjs} +3 -3
  20. package/dist/{chunk-AZIARTGX.mjs.map → chunk-BNBOIDO5.mjs.map} +1 -1
  21. package/dist/{chunk-LVNCYP4J.mjs → chunk-CXHNVYMD.mjs} +5 -5
  22. package/dist/{chunk-LVNCYP4J.mjs.map → chunk-CXHNVYMD.mjs.map} +1 -1
  23. package/dist/{chunk-45CGKJ7S.mjs → chunk-D5JLJ3PT.mjs} +4 -4
  24. package/dist/{chunk-45CGKJ7S.mjs.map → chunk-D5JLJ3PT.mjs.map} +1 -1
  25. package/dist/{chunk-WM2VDYQA.mjs → chunk-D5LWSN2Y.mjs} +944 -196
  26. package/dist/chunk-D5LWSN2Y.mjs.map +1 -0
  27. package/dist/{chunk-KRC2XOIG.mjs → chunk-HLAOGXEK.mjs} +3 -3
  28. package/dist/{chunk-KRC2XOIG.mjs.map → chunk-HLAOGXEK.mjs.map} +1 -1
  29. package/dist/{chunk-2WF6KIGF.mjs → chunk-I3L56GVH.mjs} +212 -71
  30. package/dist/chunk-I3L56GVH.mjs.map +1 -0
  31. package/dist/{chunk-ZBJBQKJ2.mjs → chunk-IHUFOV7L.mjs} +4 -19
  32. package/dist/chunk-IHUFOV7L.mjs.map +1 -0
  33. package/dist/chunk-J5LGTIGS.mjs +10 -0
  34. package/dist/chunk-J5LGTIGS.mjs.map +1 -0
  35. package/dist/{chunk-BEZSQKPY.mjs → chunk-KYMBUTPO.mjs} +5 -4
  36. package/dist/chunk-KYMBUTPO.mjs.map +1 -0
  37. package/dist/{chunk-4DS3MKID.mjs → chunk-KZGPSTZI.mjs} +4 -4
  38. package/dist/{chunk-4DS3MKID.mjs.map → chunk-KZGPSTZI.mjs.map} +1 -1
  39. package/dist/{chunk-SGFJLHHG.mjs → chunk-PPKHCRRE.mjs} +3 -3
  40. package/dist/{chunk-SGFJLHHG.mjs.map → chunk-PPKHCRRE.mjs.map} +1 -1
  41. package/dist/{chunk-BKSXPNPQ.mjs → chunk-SZDAS7LK.mjs} +81 -3
  42. package/dist/chunk-SZDAS7LK.mjs.map +1 -0
  43. package/dist/chunk-T3SOHYB2.mjs +851 -0
  44. package/dist/chunk-T3SOHYB2.mjs.map +1 -0
  45. package/dist/geometry-2d.d.mts +2 -2
  46. package/dist/geometry-2d.d.ts +2 -2
  47. package/dist/geometry-2d.js +6288 -901
  48. package/dist/geometry-2d.js.map +1 -1
  49. package/dist/geometry-2d.mjs +7 -5
  50. package/dist/geometry-3d.d.mts +2 -2
  51. package/dist/geometry-3d.d.ts +2 -2
  52. package/dist/geometry-3d.js +1335 -253
  53. package/dist/geometry-3d.js.map +1 -1
  54. package/dist/geometry-3d.mjs +6 -4
  55. package/dist/graph-2d.d.mts +2 -2
  56. package/dist/graph-2d.d.ts +2 -2
  57. package/dist/graph-2d.js +1501 -342
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +9 -7
  60. package/dist/handleExtractProblem-C-U5KluK.d.mts +158 -0
  61. package/dist/handleExtractProblem-C-U5KluK.d.ts +158 -0
  62. package/dist/{host-EPZCNFLH.mjs → host-HAYCJJ2T.mjs} +1390 -376
  63. package/dist/host-HAYCJJ2T.mjs.map +1 -0
  64. package/dist/{host-LKCMYEAV.mjs → host-LTJHAY5A.mjs} +12 -10
  65. package/dist/host-LTJHAY5A.mjs.map +1 -0
  66. package/dist/{host-ZIQ77W33.mjs → host-M26FS244.mjs} +8 -6
  67. package/dist/host-M26FS244.mjs.map +1 -0
  68. package/dist/{host-QS2EOTRJ.mjs → host-ZQCDAT6O.mjs} +3 -2
  69. package/dist/host-ZQCDAT6O.mjs.map +1 -0
  70. package/dist/index.d.mts +4 -3
  71. package/dist/index.d.ts +4 -3
  72. package/dist/index.js +6493 -1102
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +24 -21
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/latex.d.mts +2 -2
  77. package/dist/latex.d.ts +2 -2
  78. package/dist/latex.mjs +2 -1
  79. package/dist/render-ZX2O2IK7.mjs +10 -0
  80. package/dist/{render-SA4JTOW3.mjs.map → render-ZX2O2IK7.mjs.map} +1 -1
  81. package/dist/serialize-C3LSUMSA.mjs +9 -0
  82. package/dist/{serialize-JAVOU22E.mjs.map → serialize-C3LSUMSA.mjs.map} +1 -1
  83. package/dist/types-zc_Pa0mp.d.mts +418 -0
  84. package/dist/types-zc_Pa0mp.d.ts +418 -0
  85. package/package.json +10 -1
  86. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +0 -1
  87. package/dist/chunk-2WF6KIGF.mjs.map +0 -1
  88. package/dist/chunk-BEZSQKPY.mjs.map +0 -1
  89. package/dist/chunk-BKSXPNPQ.mjs.map +0 -1
  90. package/dist/chunk-CGZZO4BX.mjs +0 -96
  91. package/dist/chunk-CGZZO4BX.mjs.map +0 -1
  92. package/dist/chunk-WM2VDYQA.mjs.map +0 -1
  93. package/dist/chunk-ZBJBQKJ2.mjs.map +0 -1
  94. package/dist/host-EPZCNFLH.mjs.map +0 -1
  95. package/dist/host-LKCMYEAV.mjs.map +0 -1
  96. package/dist/host-QS2EOTRJ.mjs.map +0 -1
  97. package/dist/host-ZIQ77W33.mjs.map +0 -1
  98. package/dist/render-SA4JTOW3.mjs +0 -8
  99. package/dist/serialize-JAVOU22E.mjs +0 -7
  100. package/dist/types-Crbefnfe.d.ts +0 -128
  101. package/dist/types-DxlMPh-6.d.mts +0 -49
  102. package/dist/types-DxlMPh-6.d.ts +0 -49
  103. package/dist/types-vtvyKGAA.d.mts +0 -128
@@ -1,18 +1,22 @@
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 } from './chunk-2WF6KIGF.mjs';
5
- import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData, deserializeBoard } from './chunk-CGZZO4BX.mjs';
4
+ import { safeJsx, initJxgBoard, attachJxgWheelZoom, useToast, ToastHost, STAMP_PANEL_DESKTOP, ToastProvider, useStampStore, StampLeftPanel, ObjectRow } from './chunk-I3L56GVH.mjs';
5
+ import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData, deserializeBoard } from './chunk-AYJPOHCI.mjs';
6
6
  import { themeLabel, paletteFor, themeAxis, themeGrid } from './chunk-R5FL6S7L.mjs';
7
- import { JxgRenderer } from './chunk-BKSXPNPQ.mjs';
7
+ import { JxgRenderer } from './chunk-SZDAS7LK.mjs';
8
8
  import './chunk-ICR4CVOE.mjs';
9
- import { nextLabel, useEditorState, listObjects } from './chunk-WM2VDYQA.mjs';
10
- import './chunk-ZBJBQKJ2.mjs';
9
+ import { nextLabel, useEditorState, listObjects } from './chunk-D5LWSN2Y.mjs';
10
+ import './chunk-IHUFOV7L.mjs';
11
+ import { validateFile, fileToImagePart, describeDsl, serializeState } from './chunk-AJAHD35N.mjs';
12
+ import { handleExtractProblem } from './chunk-T3SOHYB2.mjs';
11
13
  import { DEFAULT_VIEW_2D } from './chunk-73Q7ADVL.mjs';
14
+ import './chunk-B4NJJZFR.mjs';
12
15
  import { useIsMobile } from './chunk-P2AOIF7S.mjs';
13
16
  import { insertStampImage } from './chunk-QGNU34T7.mjs';
14
17
  import './chunk-5UTGXHLJ.mjs';
15
- import { forwardRef, useRef, useId, useState, useEffect, useCallback, useImperativeHandle, useLayoutEffect, useMemo } from 'react';
18
+ import { __export } from './chunk-J5LGTIGS.mjs';
19
+ import { forwardRef, useRef, useId, useState, useEffect, useCallback, useImperativeHandle, useSyncExternalStore, useMemo, useLayoutEffect } from 'react';
16
20
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
17
21
  import { createPortal } from 'react-dom';
18
22
 
@@ -281,6 +285,104 @@ var Icon = {
281
285
  /* @__PURE__ */ jsx("circle", { cx: "8", cy: "15.5", r: "1.7", fill: C_POINT }),
282
286
  /* @__PURE__ */ jsx("circle", { cx: "19", cy: "5.7", r: "1.9", fill: C_CONSTRUCT }),
283
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 })
284
386
  ] })
285
387
  };
286
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: [
@@ -341,7 +443,83 @@ var TOOLS = [
341
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"] },
342
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"] },
343
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"] },
344
- { 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"] }
345
523
  ];
346
524
  var GROUP_LABELS = {
347
525
  move: "C\u01A1 b\u1EA3n",
@@ -353,7 +531,9 @@ var GROUP_LABELS = {
353
531
  triangle: "Tam gi\xE1c",
354
532
  measure: "\u0110o l\u01B0\u1EDDng",
355
533
  edit: "Ch\u1EC9nh s\u1EEDa",
356
- 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"
357
537
  };
358
538
  var GROUP_ORDER = [
359
539
  "move",
@@ -365,7 +545,9 @@ var GROUP_ORDER = [
365
545
  "triangle",
366
546
  "measure",
367
547
  "edit",
368
- "transform"
548
+ "transform",
549
+ "special",
550
+ "advanced"
369
551
  ];
370
552
  var A_CODE = "A".charCodeAt(0);
371
553
  function letterForGroup(g) {
@@ -592,6 +774,20 @@ function handlePolygonTool(ctx, t, toolDef, e, x, y, bestHit) {
592
774
  return true;
593
775
  }
594
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
+
595
791
  // src/stamps/geometry-2d/editor/handlers/classifyPointVsCircle.ts
596
792
  function classifyPointVsCircle(point, circle) {
597
793
  if (!point || !circle || !circle.center) return "inside";
@@ -605,7 +801,7 @@ function classifyPointVsCircle(point, circle) {
605
801
  return d < r ? "inside" : "outside";
606
802
  }
607
803
 
608
- // src/stamps/geometry-2d/editor/handlers/finalizeShape.ts
804
+ // src/stamps/geometry-2d/editor/handlers/finalize/shared.ts
609
805
  function findPickIdByKind(ctx, kind) {
610
806
  const picks = ctx.pendingRef.current;
611
807
  const ids = ctx.pendingIdsRef.current;
@@ -614,362 +810,817 @@ function findPickIdByKind(ctx, kind) {
614
810
  }
615
811
  return null;
616
812
  }
617
- function finalizeShape(ctx, toolDef) {
618
- const ids = ctx.pendingIdsRef.current;
619
- const key = toolDef.key;
620
- switch (key) {
621
- case "segment": {
622
- const id = freshId(ctx, "s");
623
- const label = ctx.nextLabel("segment");
624
- ctx.store.dispatch({
625
- type: "ADD",
626
- payload: { obj: mkSceneObj(id, "segment", label, { p1: ids[0], p2: ids[1] }) }
627
- });
628
- return;
629
- }
630
- case "line": {
631
- const id = freshId(ctx, "l");
632
- const label = ctx.nextLabel("line");
633
- ctx.store.dispatch({
634
- type: "ADD",
635
- payload: { obj: mkSceneObj(id, "line", label, { p1: ids[0], p2: ids[1] }) }
636
- });
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
+ }
637
917
  return;
638
918
  }
639
- case "perpendicular":
640
- case "parallel": {
641
- const throughPoint = findPickIdByKind(ctx, "point");
642
- const toLine = findPickIdByKind(ctx, "line");
643
- if (!throughPoint || !toLine) return;
644
- const id = freshId(ctx, key === "perpendicular" ? "perp" : "par");
645
- const label = ctx.nextLabel("line");
646
- ctx.store.dispatch({
647
- type: "ADD",
648
- payload: { obj: mkSceneObj(id, "line", label, {
649
- construction: { kind: key, throughPoint, toLine }
650
- }) }
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"
651
944
  });
652
945
  return;
653
946
  }
654
- case "perpBisector": {
655
- const id = freshId(ctx, "pb");
947
+ if (pos === "on") {
948
+ const id = freshId(ctx, "t");
656
949
  const label = ctx.nextLabel("line");
657
950
  ctx.store.dispatch({
658
951
  type: "ADD",
659
952
  payload: { obj: mkSceneObj(id, "line", label, {
660
- construction: { kind: "perpBisector", p1: ids[0], p2: ids[1] }
953
+ construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch: "on" }
661
954
  }) }
662
955
  });
663
956
  return;
664
957
  }
665
- case "angleBisector": {
666
- const picks = ctx.pendingRef.current;
667
- if (picks.length === 2 && objKind(picks[0]) === "line" && objKind(picks[1]) === "line") {
668
- for (const branch of [0, 1]) {
669
- const id2 = freshId(ctx, "ab");
670
- const label2 = ctx.nextLabel("line");
671
- ctx.store.dispatch({
672
- type: "ADD",
673
- payload: { obj: mkSceneObj(id2, "line", label2, {
674
- construction: { kind: "angleBisectorLines", line1: ids[0], line2: ids[1], branch }
675
- }) }
676
- });
677
- }
678
- return;
679
- }
680
- const id = freshId(ctx, "ab");
958
+ for (const branch of [0, 1]) {
959
+ const id = freshId(ctx, "t");
681
960
  const label = ctx.nextLabel("line");
682
961
  ctx.store.dispatch({
683
962
  type: "ADD",
684
963
  payload: { obj: mkSceneObj(id, "line", label, {
685
- construction: { kind: "angleBisector", p1: ids[0], vertex: ids[1], p2: ids[2] }
964
+ construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch }
686
965
  }) }
687
966
  });
688
- return;
689
967
  }
690
- case "tangent": {
691
- const throughId = findPickIdByKind(ctx, "point");
692
- const circleId = findPickIdByKind(ctx, "circle");
693
- if (!throughId || !circleId) return;
694
- const picks = ctx.pendingRef.current;
695
- const ids2 = ctx.pendingIdsRef.current;
696
- const through = picks[ids2.indexOf(throughId)];
697
- const circle = picks[ids2.indexOf(circleId)];
698
- const pos = classifyPointVsCircle(through, circle);
699
- if (pos === "inside") {
700
- ctx.toast?.("\u0110i\u1EC3m n\u1EB1m trong \u0111\u01B0\u1EDDng tr\xF2n \u2014 kh\xF4ng c\xF3 ti\u1EBFp tuy\u1EBFn", {
701
- variant: "warning",
702
- id: "tangent-invalid-inside"
703
- });
704
- return;
705
- }
706
- if (pos === "on") {
707
- const id = freshId(ctx, "t");
708
- const label = ctx.nextLabel("line");
709
- ctx.store.dispatch({
710
- type: "ADD",
711
- payload: { obj: mkSceneObj(id, "line", label, {
712
- construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch: "on" }
713
- }) }
714
- });
715
- 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
+ })
716
1020
  }
717
- for (const branch of [0, 1]) {
718
- const id = freshId(ctx, "t");
719
- const label = ctx.nextLabel("line");
720
- ctx.store.dispatch({
721
- type: "ADD",
722
- payload: { obj: mkSceneObj(id, "line", label, {
723
- construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch }
724
- }) }
725
- });
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
+ })
726
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" });
727
1046
  return;
728
1047
  }
729
- case "ray": {
730
- const id = freshId(ctx, "r");
731
- const label = ctx.nextLabel("ray");
732
- ctx.store.dispatch({
733
- type: "ADD",
734
- payload: { obj: mkSceneObj(id, "ray", label, { origin: ids[0], through: ids[1] }) }
735
- });
736
- return;
737
- }
738
- case "vector": {
739
- const id = freshId(ctx, "v");
740
- const label = ctx.nextLabel("vector");
741
- ctx.store.dispatch({
742
- type: "ADD",
743
- payload: { obj: mkSceneObj(id, "vector", label, { from: ids[0], to: ids[1] }) }
744
- });
745
- return;
746
- }
747
- case "circleCenter": {
748
- const id = freshId(ctx, "c");
749
- const label = ctx.nextLabel("circle");
750
- ctx.store.dispatch({
751
- type: "ADD",
752
- payload: {
753
- obj: mkSceneObj(id, "circle", label, {
754
- center: ids[0],
755
- surfacePoint: ids[1]
756
- })
757
- }
758
- });
759
- return;
760
- }
761
- case "circle3": {
762
- const id = freshId(ctx, "cc");
763
- const label = ctx.nextLabel("circle");
764
- ctx.store.dispatch({
765
- type: "ADD",
766
- payload: {
767
- obj: mkSceneObj(id, "circle", label, {
768
- construction: { kind: "circumscribed", p1: ids[0], p2: ids[1], p3: ids[2] }
769
- })
770
- }
771
- });
772
- return;
773
- }
774
- case "semicircle": {
775
- if (ids[0] === ids[1]) {
776
- ctx.toast?.("C\u1EA7n 2 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "semicircle-dup" });
777
- 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
+ })
778
1056
  }
779
- const id = freshId(ctx, "arc");
780
- const label = ctx.nextLabel("arc");
781
- ctx.store.dispatch({
782
- type: "ADD",
783
- payload: {
784
- obj: mkSceneObj(id, "arc", label, {
785
- construction: { kind: "semicircle", p1: ids[0], p2: ids[1] }
786
- })
787
- }
788
- });
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" });
789
1066
  return;
790
1067
  }
791
- case "arcCenter": {
792
- if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
793
- ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "arc-center-dup" });
794
- 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
+ })
795
1076
  }
796
- const id = freshId(ctx, "arc");
797
- const label = ctx.nextLabel("arc");
798
- ctx.store.dispatch({
799
- type: "ADD",
800
- payload: {
801
- obj: mkSceneObj(id, "arc", label, {
802
- construction: { kind: "byCenter", center: ids[0], p1: ids[1], p2: ids[2] }
803
- })
804
- }
805
- });
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" });
806
1086
  return;
807
1087
  }
808
- case "arc3": {
809
- if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
810
- ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "arc3-dup" });
811
- return;
812
- }
813
- const picks = ctx.pendingRef.current;
814
- const ax = picks[0].X(), ay = picks[0].Y();
815
- const bx = picks[1].X(), by = picks[1].Y();
816
- const cx = picks[2].X(), cy = picks[2].Y();
817
- const cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
818
- if (Math.abs(cross) < 1e-6) {
819
- ctx.toast?.("Kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c cung qua 3 \u0111i\u1EC3m th\u1EB3ng h\xE0ng", {
820
- variant: "warning",
821
- id: "arc3-collinear"
822
- });
823
- return;
824
- }
825
- const id = freshId(ctx, "arc");
826
- const label = ctx.nextLabel("arc");
827
- ctx.store.dispatch({
828
- type: "ADD",
829
- payload: {
830
- obj: mkSceneObj(id, "arc", label, {
831
- construction: { kind: "by3Points", p1: ids[0], p2: ids[1], p3: ids[2] }
832
- })
833
- }
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"
834
1097
  });
835
1098
  return;
836
1099
  }
837
- case "sectorCenter": {
838
- if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
839
- ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "sector-center-dup" });
840
- 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
+ })
841
1108
  }
842
- const id = freshId(ctx, "sec");
843
- const label = ctx.nextLabel("sector");
844
- ctx.store.dispatch({
845
- type: "ADD",
846
- payload: {
847
- obj: mkSceneObj(id, "sector", label, {
848
- construction: { kind: "byCenter", center: ids[0], p1: ids[1], p2: ids[2] }
849
- })
850
- }
851
- });
852
- return;
853
- }
854
- case "midpoint": {
855
- const id = freshId(ctx, "mp");
856
- const label = ctx.nextLabel("point");
857
- ctx.store.dispatch({
858
- type: "ADD",
859
- payload: { obj: mkSceneObj(id, "point", label, {
860
- constraint: { kind: "midpoint", p1: ids[0], p2: ids[1] }
861
- }) }
862
- });
863
- return;
864
- }
865
- case "perpFoot": {
866
- const fromPoint = findPickIdByKind(ctx, "point");
867
- const onLine = findPickIdByKind(ctx, "line");
868
- if (!fromPoint || !onLine) return;
869
- const id = freshId(ctx, "pf");
870
- const label = ctx.nextLabel("point");
871
- ctx.store.dispatch({
872
- type: "ADD",
873
- payload: { obj: mkSceneObj(id, "point", label, {
874
- constraint: { kind: "perpFoot", from: fromPoint, onLine }
875
- }) }
876
- });
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" });
877
1118
  return;
878
1119
  }
879
- case "centroid": {
880
- const id = freshId(ctx, "g");
881
- const label = ctx.nextLabel("point");
882
- ctx.store.dispatch({
883
- type: "ADD",
884
- payload: { obj: mkSceneObj(id, "point", label, {
885
- constraint: { kind: "centroid", vertices: [ids[0], ids[1], ids[2]] }
886
- }) }
887
- });
888
- 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]);
889
1320
  }
890
- case "circumcenter": {
891
- 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");
892
1338
  const label = ctx.nextLabel("point");
893
1339
  ctx.store.dispatch({
894
1340
  type: "ADD",
895
1341
  payload: { obj: mkSceneObj(id, "point", label, {
896
- constraint: { kind: "circumcenter", vertices: [ids[0], ids[1], ids[2]] }
1342
+ constraint: { kind: "circleIntersection", c1: ids[0], c2: ids[1], which }
897
1343
  }) }
898
1344
  });
899
- return;
900
1345
  }
901
- case "incenter": {
902
- 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");
903
1356
  const label = ctx.nextLabel("point");
904
1357
  ctx.store.dispatch({
905
1358
  type: "ADD",
906
1359
  payload: { obj: mkSceneObj(id, "point", label, {
907
- constraint: { kind: "incenter", vertices: [ids[0], ids[1], ids[2]] }
1360
+ constraint: { kind: "tangentPointExt", from: fromId, circle: circleId, which }
908
1361
  }) }
909
1362
  });
910
- return;
911
1363
  }
912
- case "orthocenter": {
913
- const id = freshId(ctx, "h");
914
- const label = ctx.nextLabel("point");
915
- ctx.store.dispatch({
916
- type: "ADD",
917
- payload: { obj: mkSceneObj(id, "point", label, {
918
- constraint: { kind: "orthocenter", vertices: [ids[0], ids[1], ids[2]] }
919
- }) }
920
- });
921
- 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 };
922
1395
  }
923
- case "angle": {
924
- const id = freshId(ctx, "ang");
925
- const label = ctx.nextLabel("angle");
926
- ctx.store.dispatch({
927
- type: "ADD",
928
- payload: { obj: mkSceneObj(id, "angle", label, {
929
- p1: ids[0],
930
- vertex: ids[1],
931
- p2: ids[2]
932
- }) }
933
- });
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] });
934
1595
  return;
935
1596
  }
936
- case "distance": {
937
- const id = freshId(ctx, "d");
938
- const label = ctx.nextLabel("distance");
939
- ctx.store.dispatch({
940
- type: "ADD",
941
- 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
942
1606
  });
943
- return;
944
- }
945
- case "intersect": {
946
- const picks = ctx.pendingRef.current;
947
- const pendIds = ctx.pendingIdsRef.current;
948
- const aIdx = pendIds.indexOf(ids[0]);
949
- const bIdx = pendIds.indexOf(ids[1]);
950
- if (aIdx < 0 || bIdx < 0) return;
951
- const aKind = objKind(picks[aIdx]);
952
- const bKind = objKind(picks[bIdx]);
953
- if (aKind === "line" && bKind === "line") {
954
- dispatchAddIntersection(ctx, { kind: "lineLine", ref1: ids[0], ref2: ids[1] });
955
- return;
956
- }
957
- const isLineCircle = aKind === "line" && bKind === "circle" || aKind === "circle" && bKind === "line";
958
- const isCircleCircle = aKind === "circle" && bKind === "circle";
959
- if (!isLineCircle && !isCircleCircle) return;
960
- for (const branch of [0, 1]) {
961
- dispatchAddIntersection(ctx, {
962
- kind: isLineCircle ? "lineCircle" : "circleCircle",
963
- ref1: ids[0],
964
- ref2: ids[1],
965
- branch
966
- });
967
- }
968
- return;
969
1607
  }
970
- default:
971
- return;
972
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);
973
1624
  }
974
1625
 
975
1626
  // src/stamps/geometry-2d/editor/transforms.ts
@@ -1011,6 +1662,20 @@ function finalizeTransform(ctx, tool, pendingIds, value) {
1011
1662
  });
1012
1663
  return;
1013
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
+ }
1014
1679
  const sourceId = pendingIds[0];
1015
1680
  const state = ctx.store.getState();
1016
1681
  const source = state.objects[sourceId];
@@ -1204,7 +1869,7 @@ function handleMultiClickTool(ctx, toolDef, e, x, y, hits, bestHit) {
1204
1869
  }
1205
1870
  if (ctx.pendingIdsRef.current.length >= toolDef.needs) {
1206
1871
  const tk = toolDef.key;
1207
- if (tk === "rotate" || tk === "dilate" || tk === "regularPolygon") {
1872
+ if (tk === "rotate" || tk === "dilate" || tk === "regularPolygon" || tk === "circleCR") {
1208
1873
  const cx = (e.clientX ?? 0) + 8;
1209
1874
  const cy = (e.clientY ?? 0) + 8;
1210
1875
  ctx.pendingTransformRef.current = {
@@ -1220,7 +1885,7 @@ function handleMultiClickTool(ctx, toolDef, e, x, y, hits, bestHit) {
1220
1885
  ctx.clearPending();
1221
1886
  return;
1222
1887
  }
1223
- finalizeShape(ctx, toolDef);
1888
+ finalizeShape(ctx, toolDef, { x, y });
1224
1889
  ctx.clearPending();
1225
1890
  } else {
1226
1891
  ctx.refreshPreview();
@@ -1754,10 +2419,7 @@ function useAxisGridSync({
1754
2419
  }
1755
2420
  function useEditorShortcuts({
1756
2421
  store,
1757
- pendingIdsRef,
1758
2422
  selectedSetRef,
1759
- clearPending,
1760
- clearSelection,
1761
2423
  deleteSelection
1762
2424
  }) {
1763
2425
  useEffect(() => {
@@ -1779,18 +2441,6 @@ function useEditorShortcuts({
1779
2441
  store.redo();
1780
2442
  return;
1781
2443
  }
1782
- if (e.key === "Escape" && !inField) {
1783
- if (pendingIdsRef.current.length > 0) {
1784
- e.preventDefault();
1785
- e.stopPropagation();
1786
- clearPending();
1787
- }
1788
- if (selectedSetRef.current.size > 0) {
1789
- e.preventDefault();
1790
- e.stopPropagation();
1791
- clearSelection();
1792
- }
1793
- }
1794
2444
  if ((e.key === "Delete" || e.key === "Backspace") && !inField && selectedSetRef.current.size > 0) {
1795
2445
  e.preventDefault();
1796
2446
  e.stopPropagation();
@@ -1799,7 +2449,7 @@ function useEditorShortcuts({
1799
2449
  };
1800
2450
  window.addEventListener("keydown", onKey, { capture: true });
1801
2451
  return () => window.removeEventListener("keydown", onKey, { capture: true });
1802
- }, [store, pendingIdsRef, selectedSetRef, clearPending, clearSelection, deleteSelection]);
2452
+ }, [store, selectedSetRef, deleteSelection]);
1803
2453
  }
1804
2454
  function useJxgSceneIdMap({ store, rendererRef }) {
1805
2455
  const jxgIdToSceneRef = useRef(/* @__PURE__ */ new Map());
@@ -2003,10 +2653,7 @@ var MiniBoard2D = forwardRef(function MiniBoard2D2({ onReady, store, selectedToo
2003
2653
  };
2004
2654
  useEditorShortcuts({
2005
2655
  store,
2006
- pendingIdsRef: toolSM.pendingIdsRef,
2007
2656
  selectedSetRef,
2008
- clearPending,
2009
- clearSelection,
2010
2657
  deleteSelection
2011
2658
  });
2012
2659
  useAxisGridSync({ boardRef, axisObjsRef, isDarkRef, showAxis, showGrid });
@@ -2548,7 +3195,8 @@ var MultiPropertiesPopover = (props) => {
2548
3195
  var LABELS = {
2549
3196
  rotate: { aria: "G\xF3c quay", label: "G\xF3c (\xB0)", step: 15 },
2550
3197
  dilate: { aria: "T\u1EF7 s\u1ED1 k", label: "T\u1EF7 s\u1ED1 k", step: 0.5 },
2551
- 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 }
2552
3200
  };
2553
3201
  var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel, isDark }) => {
2554
3202
  const [value, setValue] = useState(defaultValue);
@@ -2620,12 +3268,41 @@ var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel,
2620
3268
  );
2621
3269
  return createPortal(node, document.body);
2622
3270
  };
2623
- function useAiFigure(generator) {
3271
+ function useAiFigure(generator, options = {}) {
3272
+ const { currentState } = options;
2624
3273
  const [prompt, setPrompt] = useState("");
2625
3274
  const [isLoading, setIsLoading] = useState(false);
2626
3275
  const [error, setError] = useState(null);
3276
+ const [tokens, setTokens] = useState(0);
2627
3277
  const abortRef = useRef(null);
2628
3278
  const requestIdRef = useRef(0);
3279
+ const { dsl: currentDsl, unsupported, entityCount, hasContent } = useMemo(() => {
3280
+ if (!currentState || currentState.order.length === 0) {
3281
+ return {
3282
+ dsl: null,
3283
+ unsupported: [],
3284
+ entityCount: { points: 0, shapes: 0 },
3285
+ hasContent: false
3286
+ };
3287
+ }
3288
+ const { dsl, unsupported: unsupported2 } = serializeState(currentState);
3289
+ return {
3290
+ dsl,
3291
+ unsupported: unsupported2,
3292
+ entityCount: { points: dsl.points.length, shapes: dsl.shapes.length },
3293
+ hasContent: true
3294
+ };
3295
+ }, [currentState]);
3296
+ const hasUnsupported = unsupported.length > 0;
3297
+ const initialMode = hasContent && !hasUnsupported ? "refine" : "build";
3298
+ const [mode, setModeInternal] = useState(initialMode);
3299
+ useEffect(() => {
3300
+ if (!hasContent && mode === "refine") setModeInternal("build");
3301
+ if (hasUnsupported && mode === "refine") setModeInternal("build");
3302
+ }, [hasContent, hasUnsupported, mode]);
3303
+ const setMode = useCallback((next) => {
3304
+ setModeInternal(next);
3305
+ }, []);
2629
3306
  useEffect(() => () => abortRef.current?.abort(), []);
2630
3307
  const submit = useCallback(async () => {
2631
3308
  const problem = prompt.trim();
@@ -2643,8 +3320,15 @@ function useAiFigure(generator) {
2643
3320
  abortRef.current = controller;
2644
3321
  setIsLoading(true);
2645
3322
  setError(null);
3323
+ setTokens(0);
2646
3324
  try {
2647
- const generated = await generator(problem, { signal: controller.signal });
3325
+ const generated = await generator(problem, {
3326
+ signal: controller.signal,
3327
+ onProgress: (info) => {
3328
+ if (requestId === requestIdRef.current) setTokens(info.tokens);
3329
+ },
3330
+ ...mode === "refine" && currentDsl ? { currentDsl } : {}
3331
+ });
2648
3332
  if (controller.signal.aborted || requestId !== requestIdRef.current) return null;
2649
3333
  if (!generated.ok) {
2650
3334
  setError(generated.message);
@@ -2656,7 +3340,9 @@ function useAiFigure(generator) {
2656
3340
  return null;
2657
3341
  }
2658
3342
  if (requestId === requestIdRef.current) {
2659
- setError(caught instanceof Error && caught.message ? caught.message : "Kh\xF4ng th\u1EC3 d\u1EF1ng h\xECnh b\u1EB1ng AI.");
3343
+ setError(
3344
+ caught instanceof Error && caught.message ? caught.message : "Kh\xF4ng th\u1EC3 d\u1EF1ng h\xECnh b\u1EB1ng AI."
3345
+ );
2660
3346
  }
2661
3347
  return null;
2662
3348
  } finally {
@@ -2665,60 +3351,351 @@ function useAiFigure(generator) {
2665
3351
  setIsLoading(false);
2666
3352
  }
2667
3353
  }
2668
- }, [generator, prompt]);
2669
- return { prompt, setPrompt, isLoading, error, submit };
3354
+ }, [generator, prompt, mode, currentDsl]);
3355
+ const cancel = useCallback(() => {
3356
+ abortRef.current?.abort();
3357
+ }, []);
3358
+ return {
3359
+ prompt,
3360
+ setPrompt,
3361
+ isLoading,
3362
+ error,
3363
+ submit,
3364
+ cancel,
3365
+ tokens,
3366
+ mode,
3367
+ setMode,
3368
+ entityCount,
3369
+ hasUnsupported
3370
+ };
2670
3371
  }
2671
- function AiFigurePrompt({ generator, onGenerated }) {
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
+ }) {
2672
3410
  const {
2673
3411
  prompt,
2674
3412
  setPrompt,
2675
3413
  isLoading,
2676
3414
  error,
2677
- submit
2678
- } = useAiFigure(generator);
2679
- const handleSubmit = useCallback(async (event) => {
2680
- event.preventDefault();
3415
+ submit,
3416
+ cancel,
3417
+ tokens,
3418
+ mode,
3419
+ setMode,
3420
+ entityCount,
3421
+ hasUnsupported
3422
+ } = useAiFigure(generator, { currentState });
3423
+ const [elapsed, setElapsed] = useState(0);
3424
+ useEffect(() => {
3425
+ if (!isLoading) {
3426
+ setElapsed(0);
3427
+ return;
3428
+ }
3429
+ const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
3430
+ return () => clearInterval(id);
3431
+ }, [isLoading]);
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);
3490
+ },
3491
+ [handleFile]
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
+ }
2681
3516
  const generated = await submit();
2682
3517
  if (generated) onGenerated(generated);
2683
- }, [onGenerated, submit]);
2684
- return /* @__PURE__ */ jsxs(
2685
- "form",
2686
- {
2687
- "data-testid": "geometry-ai-form",
2688
- onSubmit: (event) => {
2689
- void handleSubmit(event);
2690
- },
2691
- className: "border-b border-slate-200 bg-slate-50 px-3 py-2",
2692
- children: [
2693
- /* @__PURE__ */ jsx("label", { htmlFor: "geometry-ai-prompt", className: "mb-1 block text-xs font-medium text-slate-600", children: "D\u1EF1ng h\xECnh b\u1EB1ng AI" }),
2694
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
3518
+ }, [image, prompt, ocrLoading, runOcr, submit, onGenerated]);
3519
+ const handleSwitchToBuild = useCallback(() => {
3520
+ if (currentState && currentState.order.length > 0) {
3521
+ const ok = window.confirm(
3522
+ "D\u1EF1ng m\u1EDBi s\u1EBD thay to\xE0n b\u1ED9 h\xECnh hi\u1EC7n t\u1EA1i b\u1EB1ng h\xECnh m\u1EDBi t\u1EEB AI. Ti\u1EBFp t\u1EE5c?"
3523
+ );
3524
+ if (!ok) return;
3525
+ }
3526
+ setMode("build");
3527
+ }, [currentState, setMode]);
3528
+ const hasContent = currentState != null && currentState.order.length > 0;
3529
+ const promptEmpty = !prompt.trim();
3530
+ const willOcr = image != null && promptEmpty;
3531
+ const sendDisabled = !image && promptEmpty || ocrLoading || isLoading && !willOcr;
3532
+ const refineChipLabel = entityCount.points + entityCount.shapes > 0 ? `Th\xEAm v\xE0o \xB7 ${entityCount.points}\u0111, ${entityCount.shapes}\u0111o\u1EA1n` : "Th\xEAm v\xE0o";
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: [
3538
+ /* @__PURE__ */ jsx(
3539
+ "button",
3540
+ {
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
3550
+ }
3551
+ ),
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
+ ] }) }),
2695
3610
  /* @__PURE__ */ jsx(
2696
3611
  "textarea",
2697
3612
  {
3613
+ ref: textareaRef,
2698
3614
  id: "geometry-ai-prompt",
2699
3615
  "aria-label": "\u0110\u1EC1 b\xE0i cho AI",
3616
+ "data-testid": "geometry-ai-textarea",
2700
3617
  value: prompt,
2701
- 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
+ },
2702
3625
  disabled: isLoading,
2703
3626
  rows: 2,
2704
- placeholder: "V\xED d\u1EE5: Cho tam gi\xE1c ABC, d\u1EF1ng \u0111\u01B0\u1EDDng cao AH.",
2705
- 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"
2706
3629
  }
2707
3630
  ),
2708
- /* @__PURE__ */ jsx(
2709
- "button",
2710
- {
2711
- type: "submit",
2712
- disabled: isLoading || !prompt.trim(),
2713
- className: "rounded bg-emerald-600 px-3 py-2 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
2714
- children: isLoading ? "\u0110ang d\u1EF1ng..." : "D\u1EF1ng b\u1EB1ng AI"
2715
- }
2716
- )
2717
- ] }),
2718
- error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-xs text-red-600", children: error })
2719
- ]
2720
- }
2721
- );
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
+ ] });
2722
3699
  }
2723
3700
  var GeometryEditorPanelInner = forwardRef(
2724
3701
  function GeometryEditorPanelInner2({
@@ -2752,6 +3729,11 @@ var GeometryEditorPanelInner = forwardRef(
2752
3729
  onSelectionChangeRef.current = onSelectionChange;
2753
3730
  }, [onSelectionChange]);
2754
3731
  useEditorState({ store, onHistoryChange });
3732
+ const currentSceneState = useSyncExternalStore(
3733
+ (cb) => store.subscribe(cb),
3734
+ () => store.getState(),
3735
+ () => store.getState()
3736
+ );
2755
3737
  useEffect(() => {
2756
3738
  const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
2757
3739
  sync();
@@ -2945,7 +3927,7 @@ var GeometryEditorPanelInner = forwardRef(
2945
3927
  /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
2946
3928
  ] }) })
2947
3929
  ] }),
2948
- generateGeometryFigure && /* @__PURE__ */ jsx(AiFigurePrompt, { generator: generateGeometryFigure, onGenerated: loadAiFigure }),
3930
+ generateGeometryFigure && /* @__PURE__ */ jsx(AiFigurePrompt, { generator: generateGeometryFigure, onGenerated: loadAiFigure, currentState: currentSceneState }),
2949
3931
  /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1", children: /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
2950
3932
  MiniBoard2D,
2951
3933
  {
@@ -3018,12 +4000,12 @@ var GeometryEditorPanelInner = forwardRef(
3018
4000
  onClose: dismissMultiPopover
3019
4001
  }
3020
4002
  ),
3021
- transformPopover && (transformPopover.tool === "rotate" || transformPopover.tool === "dilate" || transformPopover.tool === "regularPolygon") && /* @__PURE__ */ jsx(
4003
+ transformPopover && (transformPopover.tool === "rotate" || transformPopover.tool === "dilate" || transformPopover.tool === "regularPolygon" || transformPopover.tool === "circleCR") && /* @__PURE__ */ jsx(
3022
4004
  TransformParamPopover,
3023
4005
  {
3024
4006
  kind: transformPopover.tool,
3025
4007
  anchor: transformPopover.anchor,
3026
- defaultValue: transformPopover.tool === "rotate" ? 90 : transformPopover.tool === "dilate" ? 2 : 6,
4008
+ defaultValue: transformPopover.tool === "rotate" ? 90 : transformPopover.tool === "dilate" ? 2 : transformPopover.tool === "circleCR" ? 3 : 6,
3027
4009
  isDark,
3028
4010
  onConfirm: (v) => {
3029
4011
  handleRef.current?.confirmTransformParam(v);
@@ -3070,6 +4052,36 @@ var GeometryEditorPanel = forwardRef(
3070
4052
  return /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(GeometryEditorPanelInner, { ...props, ref }) });
3071
4053
  }
3072
4054
  );
4055
+ function makeDslRenderRow(store) {
4056
+ return function renderDslRow(obj, defaults) {
4057
+ const state = store.getState();
4058
+ const noop = () => {
4059
+ };
4060
+ return /* @__PURE__ */ jsx(
4061
+ ObjectRow,
4062
+ {
4063
+ obj,
4064
+ state,
4065
+ selected: defaults.selected,
4066
+ onSelect: defaults.onClick,
4067
+ onToggleVisible: (id) => {
4068
+ const o = state.objects[id];
4069
+ if (!o) return;
4070
+ store.dispatch({ type: "UPDATE", payload: { id, patch: { visible: !o.visible } } });
4071
+ },
4072
+ onToggleLocked: (id) => {
4073
+ const o = state.objects[id];
4074
+ if (!o) return;
4075
+ store.dispatch({ type: "UPDATE", payload: { id, patch: { locked: !o.locked } } });
4076
+ },
4077
+ onRename: noop,
4078
+ onChangeColor: noop,
4079
+ onDelete: (id) => store.dispatch({ type: "DELETE", payload: { id } }),
4080
+ describe: describeDsl
4081
+ }
4082
+ );
4083
+ };
4084
+ }
3073
4085
  function parseInitialState(data) {
3074
4086
  if (!isGeometryCustomData(data)) return null;
3075
4087
  return deserializeBoard(data.jsonState);
@@ -3100,6 +4112,7 @@ var GeometryStampHost = forwardRef(
3100
4112
  onSelect: (key) => setSelectedTool(key),
3101
4113
  enabled: !isMobile
3102
4114
  });
4115
+ const renderRow = useMemo(() => makeDslRenderRow(sceneStore), [sceneStore]);
3103
4116
  const handleInsert = useCallback(
3104
4117
  async (jsonState, svgString) => {
3105
4118
  if (!api) return;
@@ -3161,7 +4174,8 @@ var GeometryStampHost = forwardRef(
3161
4174
  onObjectSelect: (id) => {
3162
4175
  setSelectedObjectId(id ?? void 0);
3163
4176
  panelRef.current?.selectObject(id);
3164
- }
4177
+ },
4178
+ renderRow
3165
4179
  },
3166
4180
  isMobile,
3167
4181
  drawerOpen,
@@ -3196,5 +4210,5 @@ var GeometryStampHost = forwardRef(
3196
4210
  );
3197
4211
 
3198
4212
  export { GeometryStampHost };
3199
- //# sourceMappingURL=host-EPZCNFLH.mjs.map
3200
- //# sourceMappingURL=host-EPZCNFLH.mjs.map
4213
+ //# sourceMappingURL=host-HAYCJJ2T.mjs.map
4214
+ //# sourceMappingURL=host-HAYCJJ2T.mjs.map