@xom11/whiteboard 0.6.4 → 0.7.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 (66) hide show
  1. package/README.md +36 -0
  2. package/dist/chunk-3SSQKRRO.mjs +58 -0
  3. package/dist/chunk-3SSQKRRO.mjs.map +1 -0
  4. package/dist/chunk-7P7SQFOW.mjs +39 -0
  5. package/dist/chunk-7P7SQFOW.mjs.map +1 -0
  6. package/dist/chunk-BJX4YNA5.mjs +137 -0
  7. package/dist/chunk-BJX4YNA5.mjs.map +1 -0
  8. package/dist/chunk-C6SCVOMC.mjs +111 -0
  9. package/dist/chunk-C6SCVOMC.mjs.map +1 -0
  10. package/dist/chunk-DJTBZEAR.mjs +25 -0
  11. package/dist/chunk-DJTBZEAR.mjs.map +1 -0
  12. package/dist/chunk-HM7RIXJE.mjs +331 -0
  13. package/dist/chunk-HM7RIXJE.mjs.map +1 -0
  14. package/dist/chunk-HTBLO5JO.mjs +41 -0
  15. package/dist/chunk-HTBLO5JO.mjs.map +1 -0
  16. package/dist/chunk-HYXFHEDJ.mjs +129 -0
  17. package/dist/chunk-HYXFHEDJ.mjs.map +1 -0
  18. package/dist/chunk-LPM4MM45.mjs +211 -0
  19. package/dist/chunk-LPM4MM45.mjs.map +1 -0
  20. package/dist/chunk-P2AOIF7S.mjs +40 -0
  21. package/dist/chunk-P2AOIF7S.mjs.map +1 -0
  22. package/dist/chunk-SHFOGORM.mjs +44 -0
  23. package/dist/chunk-SHFOGORM.mjs.map +1 -0
  24. package/dist/chunk-X5R72SSJ.mjs +52 -0
  25. package/dist/chunk-X5R72SSJ.mjs.map +1 -0
  26. package/dist/geometry-2d.d.mts +16 -0
  27. package/dist/geometry-2d.d.ts +16 -0
  28. package/dist/geometry-2d.js +3549 -0
  29. package/dist/geometry-2d.js.map +1 -0
  30. package/dist/geometry-2d.mjs +7 -0
  31. package/dist/geometry-2d.mjs.map +1 -0
  32. package/dist/geometry-3d.d.mts +16 -0
  33. package/dist/geometry-3d.d.ts +16 -0
  34. package/dist/geometry-3d.js +2030 -0
  35. package/dist/geometry-3d.js.map +1 -0
  36. package/dist/geometry-3d.mjs +6 -0
  37. package/dist/geometry-3d.mjs.map +1 -0
  38. package/dist/graph-2d.d.mts +16 -0
  39. package/dist/graph-2d.d.ts +16 -0
  40. package/dist/graph-2d.js +1725 -0
  41. package/dist/graph-2d.js.map +1 -0
  42. package/dist/graph-2d.mjs +6 -0
  43. package/dist/graph-2d.mjs.map +1 -0
  44. package/dist/host-2QGKMGCT.mjs +1066 -0
  45. package/dist/host-2QGKMGCT.mjs.map +1 -0
  46. package/dist/host-T2W6R6SO.mjs +2859 -0
  47. package/dist/host-T2W6R6SO.mjs.map +1 -0
  48. package/dist/host-XUFON6CQ.mjs +1422 -0
  49. package/dist/host-XUFON6CQ.mjs.map +1 -0
  50. package/dist/host-Z3TEJKZA.mjs +466 -0
  51. package/dist/host-Z3TEJKZA.mjs.map +1 -0
  52. package/dist/index.d.mts +27 -146
  53. package/dist/index.d.ts +27 -146
  54. package/dist/index.js +4694 -4482
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +136 -7179
  57. package/dist/index.mjs.map +1 -1
  58. package/dist/latex.d.mts +15 -0
  59. package/dist/latex.d.ts +15 -0
  60. package/dist/latex.js +750 -0
  61. package/dist/latex.js.map +1 -0
  62. package/dist/latex.mjs +6 -0
  63. package/dist/latex.mjs.map +1 -0
  64. package/dist/types-CinstD7T.d.mts +110 -0
  65. package/dist/types-CinstD7T.d.ts +110 -0
  66. package/package.json +24 -2
@@ -0,0 +1,1725 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
+ }) : x)(function(x) {
12
+ if (typeof require !== "undefined") return require.apply(this, arguments);
13
+ throw Error('Dynamic require of "' + x + '" is not supported');
14
+ });
15
+ var __esm = (fn, res) => function __init() {
16
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
17
+ };
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, { get: all[name], enumerable: true });
21
+ };
22
+
23
+ // src/stamps/graph-2d/serialize.ts
24
+ function stringifySerializedGraph(graph) {
25
+ return JSON.stringify(graph);
26
+ }
27
+ function parseSerializedGraph(jsonState) {
28
+ let raw;
29
+ try {
30
+ raw = JSON.parse(jsonState);
31
+ } catch {
32
+ return null;
33
+ }
34
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
35
+ const r = raw;
36
+ if (r.version !== 1) return null;
37
+ if (!r.view || typeof r.view !== "object") return null;
38
+ const v = r.view;
39
+ if (typeof v.xMin !== "number" || typeof v.xMax !== "number" || typeof v.yMin !== "number" || typeof v.yMax !== "number" || typeof v.showAxis !== "boolean" || typeof v.showGrid !== "boolean") {
40
+ return null;
41
+ }
42
+ for (const key of ["functions", "parameters", "points", "intersections", "tangents"]) {
43
+ if (!Array.isArray(r[key])) return null;
44
+ }
45
+ return raw;
46
+ }
47
+ var EMPTY_GRAPH;
48
+ var init_serialize = __esm({
49
+ "src/stamps/graph-2d/serialize.ts"() {
50
+ EMPTY_GRAPH = {
51
+ version: 1,
52
+ view: { xMin: -10, xMax: 10, yMin: -10, yMax: 10, showAxis: true, showGrid: true },
53
+ functions: [],
54
+ parameters: [],
55
+ points: [],
56
+ intersections: [],
57
+ tangents: []
58
+ };
59
+ }
60
+ });
61
+
62
+ // src/stamps/graph-2d/parser.ts
63
+ function errResult(message) {
64
+ return { ok: false, error: message, freeVars: /* @__PURE__ */ new Set() };
65
+ }
66
+ function validate(expr) {
67
+ const trimmed = expr.trim();
68
+ if (!trimmed) return errResult("Bi\u1EC3u th\u1EE9c r\u1ED7ng");
69
+ if (!ALLOWED_CHARS.test(trimmed)) return errResult("K\xFD t\u1EF1 kh\xF4ng h\u1EE3p l\u1EC7");
70
+ const ids = trimmed.match(IDENTIFIER_RE) ?? [];
71
+ const freeVars = /* @__PURE__ */ new Set();
72
+ for (const id of ids) {
73
+ if (id === "x" || id === "pi" || id === "e") continue;
74
+ if (ALLOWED_FUNCTIONS.has(id)) continue;
75
+ if (id.length === 1) {
76
+ freeVars.add(id);
77
+ continue;
78
+ }
79
+ const hint = SUGGESTIONS[id];
80
+ return errResult(
81
+ hint ? `T\xEAn h\xE0m kh\xF4ng h\u1EE3p l\u1EC7: "${id}". B\u1EA1n c\xF3 \xFD l\xE0 "${hint}" kh\xF4ng?` : `T\xEAn kh\xF4ng h\u1EE3p l\u1EC7: "${id}"`
82
+ );
83
+ }
84
+ try {
85
+ const paramSubs = Object.fromEntries([...freeVars].map((v) => [v, 1]));
86
+ const rewritten = rewriteToJs(trimmed, paramSubs);
87
+ new Function("x", `return (${rewritten})`);
88
+ } catch {
89
+ return errResult("L\u1ED7i c\xFA ph\xE1p");
90
+ }
91
+ return { ok: true, freeVars };
92
+ }
93
+ function rewriteToJs(expr, params) {
94
+ let s = expr.replace(/\^/g, "**");
95
+ s = s.replace(/\bpi\b/g, "Math.PI");
96
+ s = s.replace(/\be\b/g, "Math.E");
97
+ for (const [from, to] of FUNCTION_REPLACEMENTS) {
98
+ s = s.replace(new RegExp(`\\b${from}\\b`, "g"), to);
99
+ }
100
+ for (const [name, value] of Object.entries(params)) {
101
+ if (name.length !== 1) continue;
102
+ s = s.replace(new RegExp(`\\b${name}\\b`, "g"), `(${value})`);
103
+ }
104
+ return s;
105
+ }
106
+ function compile(expr, paramValues) {
107
+ const v = validate(expr);
108
+ if (!v.ok) return { error: v.error ?? "Invalid" };
109
+ try {
110
+ const rewritten = rewriteToJs(expr, paramValues);
111
+ const raw = new Function("x", `return (${rewritten})`);
112
+ return (x) => {
113
+ try {
114
+ const y = raw(x);
115
+ return typeof y === "number" ? y : NaN;
116
+ } catch {
117
+ return NaN;
118
+ }
119
+ };
120
+ } catch (err) {
121
+ return { error: err instanceof Error ? err.message : String(err) };
122
+ }
123
+ }
124
+ var ALLOWED_FUNCTIONS, ALLOWED_CHARS, IDENTIFIER_RE, SUGGESTIONS, FUNCTION_REPLACEMENTS;
125
+ var init_parser = __esm({
126
+ "src/stamps/graph-2d/parser.ts"() {
127
+ ALLOWED_FUNCTIONS = /* @__PURE__ */ new Set([
128
+ "sin",
129
+ "cos",
130
+ "tan",
131
+ "asin",
132
+ "acos",
133
+ "atan",
134
+ "log",
135
+ "ln",
136
+ "exp",
137
+ "sqrt",
138
+ "abs",
139
+ "floor",
140
+ "ceil",
141
+ "round"
142
+ ]);
143
+ ALLOWED_CHARS = /^[a-zA-Z0-9_.+\-*/^()\s,]+$/;
144
+ IDENTIFIER_RE = /[a-zA-Z][a-zA-Z0-9_]*/g;
145
+ SUGGESTIONS = {
146
+ tg: "tan",
147
+ arcsin: "asin",
148
+ arccos: "acos",
149
+ arctan: "atan"
150
+ };
151
+ FUNCTION_REPLACEMENTS = [
152
+ // longest first để tránh substring conflict (asin trước sin)
153
+ ["asin", "Math.asin"],
154
+ ["acos", "Math.acos"],
155
+ ["atan", "Math.atan"],
156
+ ["sqrt", "Math.sqrt"],
157
+ ["floor", "Math.floor"],
158
+ ["round", "Math.round"],
159
+ ["ceil", "Math.ceil"],
160
+ ["sin", "Math.sin"],
161
+ ["cos", "Math.cos"],
162
+ ["tan", "Math.tan"],
163
+ ["abs", "Math.abs"],
164
+ ["exp", "Math.exp"],
165
+ ["log", "Math.log10"],
166
+ ["ln", "Math.log"]
167
+ ];
168
+ }
169
+ });
170
+
171
+ // src/stamps/graph-2d/editor/handlers.ts
172
+ function addPointOnCurve(graph, ctx, idFactory) {
173
+ if (!ctx.functionId) return graph;
174
+ const point = {
175
+ id: idFactory(),
176
+ functionId: ctx.functionId,
177
+ x: ctx.x
178
+ };
179
+ return { ...graph, points: [...graph.points, point] };
180
+ }
181
+ function addIntersection(graph, functionIdA, functionIdB, idFactory) {
182
+ if (functionIdA === functionIdB) return graph;
183
+ const exists = graph.intersections.some(
184
+ (i) => i.functionIdA === functionIdA && i.functionIdB === functionIdB || i.functionIdA === functionIdB && i.functionIdB === functionIdA
185
+ );
186
+ if (exists) return graph;
187
+ const intersection = {
188
+ id: idFactory(),
189
+ functionIdA,
190
+ functionIdB
191
+ };
192
+ return { ...graph, intersections: [...graph.intersections, intersection] };
193
+ }
194
+ function numericalDerivative(expression, paramValues, x, h = 1e-4) {
195
+ const fn = compile(expression, paramValues);
196
+ if (typeof fn !== "function") return NaN;
197
+ const y1 = fn(x - h);
198
+ const y2 = fn(x + h);
199
+ return (y2 - y1) / (2 * h);
200
+ }
201
+ var init_handlers = __esm({
202
+ "src/stamps/graph-2d/editor/handlers.ts"() {
203
+ init_parser();
204
+ }
205
+ });
206
+
207
+ // src/stamps/graph-2d/renderObjects.ts
208
+ function renderGraphObjects(board, graph) {
209
+ const paramMap = {};
210
+ for (const p of graph.parameters) paramMap[p.name] = p.value;
211
+ for (const f of graph.functions) {
212
+ if (!f.visible) continue;
213
+ const compiled = compile(f.expression, paramMap);
214
+ if (typeof compiled !== "function") continue;
215
+ const domain = f.domain ?? { min: graph.view.xMin, max: graph.view.xMax };
216
+ board.create("functiongraph", [compiled, domain.min, domain.max], {
217
+ strokeColor: f.color,
218
+ strokeWidth: 2,
219
+ name: f.name,
220
+ withLabel: false,
221
+ highlight: false
222
+ });
223
+ }
224
+ for (const point of graph.points) {
225
+ const fn = graph.functions.find((f) => f.id === point.functionId);
226
+ if (!fn || !fn.visible) continue;
227
+ const compiled = compile(fn.expression, paramMap);
228
+ if (typeof compiled !== "function") continue;
229
+ const y = compiled(point.x);
230
+ board.create("point", [point.x, y], {
231
+ name: point.label ?? "",
232
+ size: 3,
233
+ fillColor: fn.color,
234
+ strokeColor: fn.color,
235
+ withLabel: !!point.label
236
+ });
237
+ }
238
+ for (const inter of graph.intersections) {
239
+ const fa = graph.functions.find((f) => f.id === inter.functionIdA);
240
+ const fb = graph.functions.find((f) => f.id === inter.functionIdB);
241
+ if (!fa || !fb || !fa.visible || !fb.visible) continue;
242
+ const cfa = compile(fa.expression, paramMap);
243
+ const cfb = compile(fb.expression, paramMap);
244
+ if (typeof cfa !== "function" || typeof cfb !== "function") continue;
245
+ const roots = scanRoots((x) => cfa(x) - cfb(x), graph.view.xMin, graph.view.xMax);
246
+ for (const x of roots) {
247
+ board.create("point", [x, cfa(x)], {
248
+ size: 3,
249
+ fillColor: "#000",
250
+ strokeColor: "#000"
251
+ });
252
+ }
253
+ }
254
+ for (const tan of graph.tangents) {
255
+ const pt = graph.points.find((p) => p.id === tan.pointId);
256
+ if (!pt) continue;
257
+ const fn = graph.functions.find((f) => f.id === pt.functionId);
258
+ if (!fn || !fn.visible) continue;
259
+ const slope = numericalDerivative(fn.expression, paramMap, pt.x);
260
+ const cfn = compile(fn.expression, paramMap);
261
+ if (typeof cfn !== "function" || !Number.isFinite(slope)) continue;
262
+ const y0 = cfn(pt.x);
263
+ const x1 = graph.view.xMin;
264
+ const x2 = graph.view.xMax;
265
+ board.create(
266
+ "line",
267
+ [
268
+ [x1, slope * (x1 - pt.x) + y0],
269
+ [x2, slope * (x2 - pt.x) + y0]
270
+ ],
271
+ {
272
+ strokeColor: fn.color,
273
+ strokeWidth: 1,
274
+ dash: 2,
275
+ straightFirst: false,
276
+ straightLast: false
277
+ }
278
+ );
279
+ }
280
+ }
281
+ function scanRoots(fn, xMin, xMax, samples = 200) {
282
+ const roots = [];
283
+ const step = (xMax - xMin) / samples;
284
+ let prevX = xMin;
285
+ let prevY = fn(prevX);
286
+ for (let i = 1; i <= samples; i++) {
287
+ const x = xMin + i * step;
288
+ const y = fn(x);
289
+ if (Number.isFinite(prevY) && Number.isFinite(y) && prevY * y < 0) {
290
+ let a = prevX;
291
+ let b = x;
292
+ let ya = prevY;
293
+ for (let j = 0; j < 30; j++) {
294
+ const m = (a + b) / 2;
295
+ const ym = fn(m);
296
+ if (Math.abs(ym) < 1e-6) {
297
+ a = b = m;
298
+ break;
299
+ }
300
+ if (ya * ym < 0) {
301
+ b = m;
302
+ } else {
303
+ a = m;
304
+ ya = ym;
305
+ }
306
+ }
307
+ roots.push((a + b) / 2);
308
+ }
309
+ prevX = x;
310
+ prevY = y;
311
+ }
312
+ return roots;
313
+ }
314
+ var init_renderObjects = __esm({
315
+ "src/stamps/graph-2d/renderObjects.ts"() {
316
+ init_parser();
317
+ init_handlers();
318
+ }
319
+ });
320
+
321
+ // src/stamps/graph-2d/render.ts
322
+ async function renderGraph2dSvgFromState(jsonState) {
323
+ const parsed = parseSerializedGraph(jsonState);
324
+ if (!parsed) throw new Error("renderGraph2dSvgFromState: jsonState corrupt");
325
+ const JXG = (await import('jsxgraph')).default;
326
+ const opts = JXG.Options;
327
+ if (opts) {
328
+ opts.text = opts.text || {};
329
+ opts.text.display = "internal";
330
+ opts.text.useASCIIMathML = false;
331
+ opts.text.useMathJax = false;
332
+ opts.text.useKatex = false;
333
+ opts.label = opts.label || {};
334
+ opts.label.display = "internal";
335
+ }
336
+ const container = document.createElement("div");
337
+ container.id = `jxg_graph2d_off_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
338
+ container.style.cssText = "position:absolute;top:-99999px;left:-99999px;width:600px;height:400px;visibility:hidden;pointer-events:none;";
339
+ document.body.appendChild(container);
340
+ let board = null;
341
+ try {
342
+ board = JXG.JSXGraph.initBoard(container.id, {
343
+ boundingbox: [parsed.view.xMin, parsed.view.yMax, parsed.view.xMax, parsed.view.yMin],
344
+ axis: parsed.view.showAxis,
345
+ grid: parsed.view.showGrid,
346
+ showCopyright: false,
347
+ showNavigation: false,
348
+ keepAspectRatio: false
349
+ });
350
+ renderGraphObjects(board, parsed);
351
+ board.update();
352
+ const svgEl = container.querySelector("svg");
353
+ if (!svgEl) throw new Error("renderGraph2dSvgFromState: no svg generated");
354
+ return svgEl.outerHTML;
355
+ } finally {
356
+ try {
357
+ if (board) JXG.JSXGraph.freeBoard(board);
358
+ } catch {
359
+ }
360
+ if (container.parentNode) container.parentNode.removeChild(container);
361
+ }
362
+ }
363
+ var init_render = __esm({
364
+ "src/stamps/graph-2d/render.ts"() {
365
+ init_serialize();
366
+ init_renderObjects();
367
+ }
368
+ });
369
+
370
+ // src/stamps/graph-2d/types.ts
371
+ function isGraph2DCustomData(data) {
372
+ if (!data || typeof data !== "object") return false;
373
+ const d = data;
374
+ return d.kind === "graph2d" && d.version === 1 && typeof d.jsonState === "string";
375
+ }
376
+ var init_types = __esm({
377
+ "src/stamps/graph-2d/types.ts"() {
378
+ }
379
+ });
380
+
381
+ // src/stamps/graph-2d/editor/tools.ts
382
+ var GRAPH_TOOLS;
383
+ var init_tools = __esm({
384
+ "src/stamps/graph-2d/editor/tools.ts"() {
385
+ GRAPH_TOOLS = [
386
+ { id: "move", label: "Di chuy\u1EC3n", title: "Di chuy\u1EC3n / ch\u1ECDn" },
387
+ { id: "point-on-curve", label: "\u0110i\u1EC3m tr\xEAn curve", title: "T\u1EA1o \u0111i\u1EC3m c\u1ED1 \u0111\u1ECBnh tr\xEAn \u0111\u1ED3 th\u1ECB" },
388
+ { id: "intersect", label: "Giao \u0111i\u1EC3m", title: "\u0110\xE1nh d\u1EA5u giao \u0111i\u1EC3m 2 \u0111\u1ED3 th\u1ECB" },
389
+ { id: "tangent", label: "Ti\u1EBFp tuy\u1EBFn", title: "V\u1EBD ti\u1EBFp tuy\u1EBFn t\u1EA1i \u0111i\u1EC3m tr\xEAn \u0111\u1ED3 th\u1ECB" }
390
+ ];
391
+ }
392
+ });
393
+ function FunctionRow(props) {
394
+ const { id, name, expression, color, visible, error } = props;
395
+ const [draft, setDraft] = react.useState(expression);
396
+ react.useEffect(() => {
397
+ setDraft(expression);
398
+ }, [expression]);
399
+ const commit = () => {
400
+ if (draft !== expression) props.onExpressionCommit(draft);
401
+ };
402
+ const handleKeyDown = (e) => {
403
+ if (e.key === "Enter") {
404
+ e.preventDefault();
405
+ commit();
406
+ e.target.blur();
407
+ } else if (e.key === "Escape") {
408
+ setDraft(expression);
409
+ e.target.blur();
410
+ }
411
+ };
412
+ const handleBlur = (_) => commit();
413
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `graph-function-row${error ? " is-error" : ""}`, "data-testid": `graph-function-row-${id}`, children: [
414
+ /* @__PURE__ */ jsxRuntime.jsx(
415
+ "span",
416
+ {
417
+ className: "graph-function-color",
418
+ style: { backgroundColor: color },
419
+ "aria-hidden": "true"
420
+ }
421
+ ),
422
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "graph-function-name", "data-testid": `graph-function-name-${id}`, children: [
423
+ name,
424
+ "(x) ="
425
+ ] }),
426
+ /* @__PURE__ */ jsxRuntime.jsx(
427
+ "input",
428
+ {
429
+ "aria-label": "Bi\u1EC3u th\u1EE9c",
430
+ className: "graph-function-input",
431
+ type: "text",
432
+ value: draft,
433
+ onChange: (e) => setDraft(e.target.value),
434
+ onKeyDown: handleKeyDown,
435
+ onBlur: handleBlur,
436
+ spellCheck: false,
437
+ autoCorrect: "off",
438
+ autoCapitalize: "off"
439
+ }
440
+ ),
441
+ /* @__PURE__ */ jsxRuntime.jsx(
442
+ "button",
443
+ {
444
+ type: "button",
445
+ "aria-label": "\u1EA8n/hi\u1EC7n \u0111\u1ED3 th\u1ECB",
446
+ className: `graph-function-eye${visible ? "" : " is-hidden"}`,
447
+ onClick: props.onToggleVisible,
448
+ children: visible ? "\u{1F441}" : "\u2298"
449
+ }
450
+ ),
451
+ /* @__PURE__ */ jsxRuntime.jsx(
452
+ "button",
453
+ {
454
+ type: "button",
455
+ "aria-label": "Xo\xE1 \u0111\u1ED3 th\u1ECB",
456
+ className: "graph-function-remove",
457
+ onClick: props.onRemove,
458
+ children: "\u2715"
459
+ }
460
+ ),
461
+ error ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "graph-function-error", children: error }) : null
462
+ ] });
463
+ }
464
+ var init_FunctionRow = __esm({
465
+ "src/stamps/graph-2d/editor/FunctionRow.tsx"() {
466
+ "use client";
467
+ }
468
+ });
469
+ function SliderRow(props) {
470
+ const { name, value, min, max, step } = props;
471
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-slider-row", "data-testid": `graph-slider-row-${name}`, children: [
472
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-slider-header", children: [
473
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "graph-slider-name", children: name }),
474
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "graph-slider-value", children: [
475
+ "= ",
476
+ value.toFixed(2)
477
+ ] }),
478
+ /* @__PURE__ */ jsxRuntime.jsx(
479
+ "button",
480
+ {
481
+ type: "button",
482
+ "aria-label": `Xo\xE1 tham s\u1ED1 ${name}`,
483
+ className: "graph-slider-remove",
484
+ onClick: props.onRemove,
485
+ children: "\u2715"
486
+ }
487
+ )
488
+ ] }),
489
+ /* @__PURE__ */ jsxRuntime.jsx(
490
+ "input",
491
+ {
492
+ type: "range",
493
+ "aria-label": `Slider ${name}`,
494
+ min,
495
+ max,
496
+ step,
497
+ value,
498
+ onChange: (e) => props.onChange(parseFloat(e.target.value)),
499
+ className: "graph-slider-input"
500
+ }
501
+ ),
502
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-slider-range", children: [
503
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: min }),
504
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: max })
505
+ ] })
506
+ ] });
507
+ }
508
+ var init_SliderRow = __esm({
509
+ "src/stamps/graph-2d/editor/SliderRow.tsx"() {
510
+ "use client";
511
+ }
512
+ });
513
+
514
+ // src/stamps/graph-2d/colors.ts
515
+ function nextColor(usedColors) {
516
+ for (const c of GRAPH_PALETTE) {
517
+ if (!usedColors.includes(c)) return c;
518
+ }
519
+ return GRAPH_PALETTE[usedColors.length % GRAPH_PALETTE.length];
520
+ }
521
+ function nextFunctionName(usedNames) {
522
+ for (const n of FUNCTION_NAMES) {
523
+ if (!usedNames.includes(n)) return n;
524
+ }
525
+ return FUNCTION_NAMES[usedNames.length % FUNCTION_NAMES.length];
526
+ }
527
+ var GRAPH_PALETTE, FUNCTION_NAMES, MAX_FUNCTIONS, MAX_PARAMETERS;
528
+ var init_colors = __esm({
529
+ "src/stamps/graph-2d/colors.ts"() {
530
+ GRAPH_PALETTE = [
531
+ "#2563eb",
532
+ // blue
533
+ "#dc2626",
534
+ // red
535
+ "#16a34a",
536
+ // green
537
+ "#9333ea",
538
+ // purple
539
+ "#ea580c",
540
+ // orange
541
+ "#0891b2",
542
+ // cyan
543
+ "#db2777",
544
+ // pink
545
+ "#65a30d"
546
+ // lime
547
+ ];
548
+ FUNCTION_NAMES = ["f", "g", "h", "i", "j", "k", "l", "m"];
549
+ MAX_FUNCTIONS = 8;
550
+ MAX_PARAMETERS = 8;
551
+ }
552
+ });
553
+ function AlgebraView(props) {
554
+ const { graph, errors } = props;
555
+ const atMax = graph.functions.length >= MAX_FUNCTIONS;
556
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-algebra-view", children: [
557
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-algebra-section", children: [
558
+ graph.functions.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
559
+ FunctionRow,
560
+ {
561
+ id: f.id,
562
+ name: f.name,
563
+ expression: f.expression,
564
+ color: f.color,
565
+ visible: f.visible,
566
+ error: errors[f.id] ?? null,
567
+ onExpressionCommit: (expr) => props.onCommitFunctionExpr(f.id, expr),
568
+ onToggleVisible: () => props.onToggleFunctionVisible(f.id),
569
+ onRemove: () => props.onRemoveFunction(f.id)
570
+ },
571
+ f.id
572
+ )),
573
+ /* @__PURE__ */ jsxRuntime.jsx(
574
+ "button",
575
+ {
576
+ type: "button",
577
+ "aria-label": "Th\xEAm h\xE0m s\u1ED1",
578
+ className: "graph-algebra-add",
579
+ onClick: props.onAddFunctionDraft,
580
+ disabled: atMax,
581
+ children: "+ Th\xEAm h\xE0m"
582
+ }
583
+ )
584
+ ] }),
585
+ graph.parameters.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "graph-algebra-section graph-algebra-parameters", children: graph.parameters.map((p) => /* @__PURE__ */ jsxRuntime.jsx(
586
+ SliderRow,
587
+ {
588
+ name: p.name,
589
+ value: p.value,
590
+ min: p.min,
591
+ max: p.max,
592
+ step: p.step,
593
+ onChange: (v) => props.onParameterChange(p.name, v),
594
+ onRangeChange: (min, max, step) => props.onParameterRangeChange(p.name, min, max, step),
595
+ onRemove: () => props.onRemoveParameter(p.name)
596
+ },
597
+ p.name
598
+ )) }) : null
599
+ ] });
600
+ }
601
+ var init_AlgebraView = __esm({
602
+ "src/stamps/graph-2d/editor/AlgebraView.tsx"() {
603
+ "use client";
604
+ init_FunctionRow();
605
+ init_SliderRow();
606
+ init_colors();
607
+ }
608
+ });
609
+ function CloseIcon() {
610
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
611
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
612
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
613
+ ] });
614
+ }
615
+ function UndoIcon() {
616
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
617
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
618
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
619
+ ] });
620
+ }
621
+ function ResetViewIcon() {
622
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
623
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "9" }),
624
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "21" }),
625
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" })
626
+ ] });
627
+ }
628
+ function MoveIcon() {
629
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 4 L9 4 L9 9 L4 9 Z" }) });
630
+ }
631
+ function PointOnCurveIcon() {
632
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
633
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 17 C7 8, 14 8, 21 14" }),
634
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "11", r: "2.2", fill: "currentColor", stroke: "none" })
635
+ ] });
636
+ }
637
+ function IntersectIcon() {
638
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
639
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 17 C8 5, 14 5, 21 17" }),
640
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 5 C8 17, 14 17, 21 5" }),
641
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "11", r: "1.6", fill: "currentColor", stroke: "none" })
642
+ ] });
643
+ }
644
+ function TangentIcon() {
645
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
646
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 17 C8 7, 14 7, 21 16" }),
647
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "14", x2: "20", y2: "6" }),
648
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "10", r: "1.8", fill: "currentColor", stroke: "none" })
649
+ ] });
650
+ }
651
+ function Section({ label, children }) {
652
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
653
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
654
+ children
655
+ ] });
656
+ }
657
+ function PanelBody(props) {
658
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
659
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-wrap text-[11px] text-slate-700", children: [
660
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
661
+ /* @__PURE__ */ jsxRuntime.jsx(
662
+ "input",
663
+ {
664
+ type: "checkbox",
665
+ checked: props.showAxis,
666
+ onChange: (e) => props.onShowAxisChange(e.target.checked),
667
+ "data-testid": "toggle-axis"
668
+ }
669
+ ),
670
+ "Tr\u1EE5c"
671
+ ] }),
672
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
673
+ /* @__PURE__ */ jsxRuntime.jsx(
674
+ "input",
675
+ {
676
+ type: "checkbox",
677
+ checked: props.showGrid,
678
+ onChange: (e) => props.onShowGridChange(e.target.checked),
679
+ "data-testid": "toggle-grid"
680
+ }
681
+ ),
682
+ "L\u01B0\u1EDBi"
683
+ ] }),
684
+ /* @__PURE__ */ jsxRuntime.jsx(
685
+ "button",
686
+ {
687
+ type: "button",
688
+ onClick: props.onResetView,
689
+ title: "\u0110\u1EB7t l\u1EA1i t\u1EA7m nh\xECn",
690
+ "aria-label": "\u0110\u1EB7t l\u1EA1i t\u1EA7m nh\xECn",
691
+ className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900",
692
+ children: /* @__PURE__ */ jsxRuntime.jsx(ResetViewIcon, {})
693
+ }
694
+ ),
695
+ /* @__PURE__ */ jsxRuntime.jsx(
696
+ "button",
697
+ {
698
+ type: "button",
699
+ onClick: props.onUndo,
700
+ disabled: !props.canUndo,
701
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
702
+ "aria-label": "Ho\xE0n t\xE1c",
703
+ className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
704
+ children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {})
705
+ }
706
+ )
707
+ ] }) }),
708
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { label: "C\xF4ng c\u1EE5", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: GRAPH_TOOLS.map((t) => {
709
+ const isActive = props.activeTool === t.id;
710
+ return /* @__PURE__ */ jsxRuntime.jsx(
711
+ "button",
712
+ {
713
+ type: "button",
714
+ "aria-label": t.title,
715
+ title: t.title,
716
+ "aria-pressed": isActive,
717
+ onClick: () => props.onToolChange(t.id),
718
+ "data-testid": `graph-tool-${t.id}`,
719
+ className: [
720
+ "flex h-8 items-center justify-center rounded-md transition",
721
+ isActive ? "bg-orange-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
722
+ ].join(" "),
723
+ children: TOOL_ICONS[t.id]
724
+ },
725
+ t.id
726
+ );
727
+ }) }) }),
728
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { label: "H\xE0m s\u1ED1", children: /* @__PURE__ */ jsxRuntime.jsx(
729
+ AlgebraView,
730
+ {
731
+ graph: props.graph,
732
+ errors: props.errors,
733
+ onAddFunctionDraft: props.onAddFunctionDraft,
734
+ onCommitFunctionExpr: props.onCommitFunctionExpr,
735
+ onToggleFunctionVisible: props.onToggleFunctionVisible,
736
+ onRemoveFunction: props.onRemoveFunction,
737
+ onParameterChange: props.onParameterChange,
738
+ onParameterRangeChange: props.onParameterRangeChange,
739
+ onRemoveParameter: props.onRemoveParameter
740
+ }
741
+ ) })
742
+ ] });
743
+ }
744
+ function GraphLeftPanel(props) {
745
+ const { isMobile, drawerOpen, isDark, onClose, onDrawerClose } = props;
746
+ if (isMobile && !drawerOpen) return null;
747
+ const handleClose = isMobile ? onDrawerClose : onClose;
748
+ return /* @__PURE__ */ jsxRuntime.jsxs(
749
+ "aside",
750
+ {
751
+ role: "complementary",
752
+ "aria-label": "\u0110\u1ED3 th\u1ECB 2D",
753
+ "data-testid": "graph-left-panel",
754
+ "data-stamp-area": "true",
755
+ className: [
756
+ isDark ? "theme--dark " : "",
757
+ isMobile ? "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[85vw] flex-col bg-white shadow-2xl animate-in slide-in-from-left duration-200" : "absolute left-0 top-0 z-30 flex h-full w-60 flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200"
758
+ ].join(" "),
759
+ children: [
760
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
761
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
762
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: GraphIconHeader }),
763
+ "\u0110\u1ED3 th\u1ECB 2D"
764
+ ] }),
765
+ /* @__PURE__ */ jsxRuntime.jsx(
766
+ "button",
767
+ {
768
+ onClick: handleClose,
769
+ "aria-label": "\u0110\xF3ng",
770
+ className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
771
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {})
772
+ }
773
+ )
774
+ ] }),
775
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children: /* @__PURE__ */ jsxRuntime.jsx(PanelBody, { ...props }) })
776
+ ]
777
+ }
778
+ );
779
+ }
780
+ var GraphIconHeader, TOOL_ICONS;
781
+ var init_LeftPanel = __esm({
782
+ "src/stamps/graph-2d/editor/LeftPanel.tsx"() {
783
+ "use client";
784
+ init_tools();
785
+ init_AlgebraView();
786
+ GraphIconHeader = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
787
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 V3" }),
788
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 H21" }),
789
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
790
+ ] });
791
+ TOOL_ICONS = {
792
+ move: /* @__PURE__ */ jsxRuntime.jsx(MoveIcon, {}),
793
+ "point-on-curve": /* @__PURE__ */ jsxRuntime.jsx(PointOnCurveIcon, {}),
794
+ intersect: /* @__PURE__ */ jsxRuntime.jsx(IntersectIcon, {}),
795
+ tangent: /* @__PURE__ */ jsxRuntime.jsx(TangentIcon, {})
796
+ };
797
+ }
798
+ });
799
+ var init_theme = __esm({
800
+ "src/stamps/graph-2d/editor/theme.ts"() {
801
+ }
802
+ });
803
+ function MiniBoard({ graph, activeTool, isDark, onBoardEvent }) {
804
+ const containerRef = react.useRef(null);
805
+ const boardRef = react.useRef(null);
806
+ const curvesRef = react.useRef(/* @__PURE__ */ new Map());
807
+ react.useEffect(() => {
808
+ let cancelled = false;
809
+ let createdBoard = null;
810
+ const containerEl = containerRef.current;
811
+ if (!containerEl) return;
812
+ const containerId = `jxg_graph2d_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
813
+ containerEl.id = containerId;
814
+ (async () => {
815
+ const JXG = (await import('jsxgraph')).default;
816
+ if (cancelled) return;
817
+ const opts = JXG.Options;
818
+ if (opts) {
819
+ opts.text = opts.text || {};
820
+ opts.text.display = "internal";
821
+ opts.label = opts.label || {};
822
+ opts.label.display = "internal";
823
+ }
824
+ const board = JXG.JSXGraph.initBoard(containerId, {
825
+ boundingbox: [graph.view.xMin, graph.view.yMax, graph.view.xMax, graph.view.yMin],
826
+ axis: graph.view.showAxis,
827
+ grid: graph.view.showGrid,
828
+ showCopyright: false,
829
+ showNavigation: true,
830
+ pan: { enabled: true, needShift: false },
831
+ zoom: { wheel: true, needShift: false },
832
+ keepAspectRatio: false
833
+ });
834
+ boardRef.current = board;
835
+ createdBoard = board;
836
+ syncObjects(board, graph, curvesRef.current);
837
+ board.on("boundingbox", () => {
838
+ const bb = board.getBoundingBox();
839
+ onBoardEvent({
840
+ type: "view-change",
841
+ view: {
842
+ xMin: bb[0],
843
+ xMax: bb[2],
844
+ yMax: bb[1],
845
+ yMin: bb[3],
846
+ showAxis: graph.view.showAxis,
847
+ showGrid: graph.view.showGrid
848
+ }
849
+ });
850
+ });
851
+ board.on("down", (ev) => {
852
+ const usrCoords = board.getUsrCoordsOfMouse?.(ev);
853
+ const x = usrCoords?.[0] ?? 0;
854
+ const y = usrCoords?.[1] ?? 0;
855
+ let functionId;
856
+ for (const [id, ref] of curvesRef.current) {
857
+ const obj = ref.obj;
858
+ if (obj?.hasPoint && obj.hasPoint(ev.clientX ?? 0, ev.clientY ?? 0)) {
859
+ functionId = id;
860
+ break;
861
+ }
862
+ }
863
+ if (functionId) onBoardEvent({ type: "click-curve", functionId, x, y });
864
+ else onBoardEvent({ type: "click-empty", x, y });
865
+ });
866
+ })().catch((err) => console.error("MiniBoard init failed:", err));
867
+ return () => {
868
+ cancelled = true;
869
+ try {
870
+ if (createdBoard) __require("jsxgraph").default.JSXGraph.freeBoard(createdBoard);
871
+ } catch {
872
+ }
873
+ boardRef.current = null;
874
+ curvesRef.current.clear();
875
+ };
876
+ }, []);
877
+ react.useEffect(() => {
878
+ if (!boardRef.current) return;
879
+ syncObjects(boardRef.current, graph, curvesRef.current);
880
+ }, [graph]);
881
+ react.useEffect(() => {
882
+ const el = containerRef.current;
883
+ if (!el) return;
884
+ el.style.cursor = activeTool === "move" ? "" : "crosshair";
885
+ }, [activeTool]);
886
+ return /* @__PURE__ */ jsxRuntime.jsx(
887
+ "div",
888
+ {
889
+ ref: containerRef,
890
+ className: "graph-miniboard",
891
+ style: { width: "100%", height: "100%", minHeight: "300px" },
892
+ "data-testid": "graph-miniboard"
893
+ }
894
+ );
895
+ }
896
+ function paramSig(graph) {
897
+ return graph.parameters.map((p) => `${p.name}=${p.value}`).join(",");
898
+ }
899
+ function syncObjects(board, graph, curves) {
900
+ const sig = paramSig(graph);
901
+ const paramMap = {};
902
+ for (const p of graph.parameters) paramMap[p.name] = p.value;
903
+ const wantedIds = new Set(graph.functions.map((f) => f.id));
904
+ for (const [id, ref] of curves) {
905
+ if (!wantedIds.has(id)) {
906
+ try {
907
+ board.removeObject(ref.obj);
908
+ } catch {
909
+ }
910
+ curves.delete(id);
911
+ }
912
+ }
913
+ for (const f of graph.functions) {
914
+ const existing = curves.get(f.id);
915
+ const needsRecreate = !existing || existing.expression !== f.expression || existing.color !== f.color || existing.visible !== f.visible || existing.paramSignature !== sig;
916
+ if (!needsRecreate) continue;
917
+ if (existing) {
918
+ try {
919
+ board.removeObject(existing.obj);
920
+ } catch {
921
+ }
922
+ }
923
+ if (!f.visible) {
924
+ curves.delete(f.id);
925
+ continue;
926
+ }
927
+ const compiled = compile(f.expression, paramMap);
928
+ if (typeof compiled !== "function") continue;
929
+ const domain = f.domain ?? { min: graph.view.xMin, max: graph.view.xMax };
930
+ const obj = board.create("functiongraph", [compiled, domain.min, domain.max], {
931
+ strokeColor: f.color,
932
+ strokeWidth: 2,
933
+ name: f.name,
934
+ withLabel: false,
935
+ highlight: false
936
+ });
937
+ curves.set(f.id, {
938
+ obj,
939
+ expression: f.expression,
940
+ color: f.color,
941
+ visible: f.visible,
942
+ paramSignature: sig
943
+ });
944
+ }
945
+ for (const point of graph.points) {
946
+ const fn = graph.functions.find((f) => f.id === point.functionId);
947
+ if (!fn || !fn.visible) continue;
948
+ const compiled = compile(fn.expression, paramMap);
949
+ if (typeof compiled !== "function") continue;
950
+ const y = compiled(point.x);
951
+ board.create("point", [point.x, y], {
952
+ name: point.label ?? "",
953
+ size: 3,
954
+ fillColor: fn.color,
955
+ strokeColor: fn.color,
956
+ withLabel: !!point.label
957
+ });
958
+ }
959
+ for (const inter of graph.intersections) {
960
+ const fa = graph.functions.find((f) => f.id === inter.functionIdA);
961
+ const fb = graph.functions.find((f) => f.id === inter.functionIdB);
962
+ if (!fa || !fb || !fa.visible || !fb.visible) continue;
963
+ const cfa = compile(fa.expression, paramMap);
964
+ const cfb = compile(fb.expression, paramMap);
965
+ if (typeof cfa !== "function" || typeof cfb !== "function") continue;
966
+ const roots = scanRoots2((x) => cfa(x) - cfb(x), graph.view.xMin, graph.view.xMax);
967
+ for (const x of roots) {
968
+ board.create("point", [x, cfa(x)], {
969
+ size: 3,
970
+ fillColor: "#000",
971
+ strokeColor: "#000"
972
+ });
973
+ }
974
+ }
975
+ for (const tan of graph.tangents) {
976
+ const pt = graph.points.find((p) => p.id === tan.pointId);
977
+ if (!pt) continue;
978
+ const fn = graph.functions.find((f) => f.id === pt.functionId);
979
+ if (!fn || !fn.visible) continue;
980
+ const slope = numericalDerivative(fn.expression, paramMap, pt.x);
981
+ const cfn = compile(fn.expression, paramMap);
982
+ if (typeof cfn !== "function" || !Number.isFinite(slope)) continue;
983
+ const y0 = cfn(pt.x);
984
+ const x1 = graph.view.xMin;
985
+ const x2 = graph.view.xMax;
986
+ board.create(
987
+ "line",
988
+ [
989
+ [x1, slope * (x1 - pt.x) + y0],
990
+ [x2, slope * (x2 - pt.x) + y0]
991
+ ],
992
+ {
993
+ strokeColor: fn.color,
994
+ strokeWidth: 1,
995
+ dash: 2,
996
+ straightFirst: false,
997
+ straightLast: false
998
+ }
999
+ );
1000
+ }
1001
+ board.update();
1002
+ }
1003
+ function scanRoots2(fn, xMin, xMax, samples = 200) {
1004
+ const roots = [];
1005
+ const step = (xMax - xMin) / samples;
1006
+ let prevX = xMin;
1007
+ let prevY = fn(prevX);
1008
+ for (let i = 1; i <= samples; i++) {
1009
+ const x = xMin + i * step;
1010
+ const y = fn(x);
1011
+ if (Number.isFinite(prevY) && Number.isFinite(y) && prevY * y < 0) {
1012
+ let a = prevX;
1013
+ let b = x;
1014
+ let ya = prevY;
1015
+ for (let j = 0; j < 30; j++) {
1016
+ const m = (a + b) / 2;
1017
+ const ym = fn(m);
1018
+ if (Math.abs(ym) < 1e-6) {
1019
+ a = b = m;
1020
+ break;
1021
+ }
1022
+ if (ya * ym < 0) {
1023
+ b = m;
1024
+ } else {
1025
+ a = m;
1026
+ ya = ym;
1027
+ }
1028
+ }
1029
+ roots.push((a + b) / 2);
1030
+ }
1031
+ prevX = x;
1032
+ prevY = y;
1033
+ }
1034
+ return roots;
1035
+ }
1036
+ var init_MiniBoard = __esm({
1037
+ "src/stamps/graph-2d/editor/MiniBoard.tsx"() {
1038
+ "use client";
1039
+ init_parser();
1040
+ init_theme();
1041
+ init_handlers();
1042
+ }
1043
+ });
1044
+ var GraphEditorPanel;
1045
+ var init_EditorPanel = __esm({
1046
+ "src/stamps/graph-2d/editor/EditorPanel.tsx"() {
1047
+ "use client";
1048
+ init_MiniBoard();
1049
+ init_serialize();
1050
+ init_parser();
1051
+ init_render();
1052
+ init_colors();
1053
+ init_handlers();
1054
+ GraphEditorPanel = react.forwardRef(function GraphEditorPanel2(props, ref) {
1055
+ const initialGraph = props.initialState ?? EMPTY_GRAPH;
1056
+ const graphRef = react.useRef(initialGraph);
1057
+ const [, forceUpdate] = react.useState(0);
1058
+ const [errors, setErrors] = react.useState({});
1059
+ const [tool, setToolState] = react.useState("move");
1060
+ const undoStackRef = react.useRef([]);
1061
+ const idCounterRef = react.useRef(1);
1062
+ const toolRef = react.useRef(tool);
1063
+ toolRef.current = tool;
1064
+ const intersectFirstRef = react.useRef(null);
1065
+ const propsRef = react.useRef(props);
1066
+ propsRef.current = props;
1067
+ const initialGraphNotifiedRef = react.useRef(false);
1068
+ const pushUndo = react.useCallback((g) => {
1069
+ undoStackRef.current.push(g);
1070
+ if (undoStackRef.current.length > 30) undoStackRef.current.shift();
1071
+ }, []);
1072
+ const setErrorsWithNotify = react.useCallback(
1073
+ (updater) => {
1074
+ setErrors((prev) => {
1075
+ const next = updater(prev);
1076
+ propsRef.current.onErrorsChange?.(next);
1077
+ return next;
1078
+ });
1079
+ },
1080
+ []
1081
+ );
1082
+ const notifyStateChange = react.useCallback((g, t) => {
1083
+ propsRef.current.onStateChange({
1084
+ tool: t,
1085
+ showAxis: g.view.showAxis,
1086
+ showGrid: g.view.showGrid,
1087
+ canUndo: undoStackRef.current.length > 0
1088
+ });
1089
+ }, []);
1090
+ const updateGraph = react.useCallback(
1091
+ (mutator) => {
1092
+ const prev = graphRef.current;
1093
+ pushUndo(prev);
1094
+ const next = mutator(prev);
1095
+ graphRef.current = next;
1096
+ notifyStateChange(next, toolRef.current);
1097
+ forceUpdate((n) => n + 1);
1098
+ propsRef.current.onGraphChange?.(next);
1099
+ },
1100
+ [pushUndo, notifyStateChange]
1101
+ );
1102
+ const onBoardEvent = react.useCallback((ev) => {
1103
+ const currentTool = toolRef.current;
1104
+ if (currentTool === "point-on-curve" && ev.type === "click-curve" && ev.functionId && ev.x !== void 0) {
1105
+ updateGraph(
1106
+ (g) => addPointOnCurve(
1107
+ g,
1108
+ { x: ev.x, y: ev.y ?? 0, functionId: ev.functionId },
1109
+ () => `p${idCounterRef.current++}`
1110
+ )
1111
+ );
1112
+ setToolState("move");
1113
+ } else if (currentTool === "intersect" && ev.type === "click-curve" && ev.functionId) {
1114
+ if (!intersectFirstRef.current) {
1115
+ intersectFirstRef.current = ev.functionId;
1116
+ } else {
1117
+ const a = intersectFirstRef.current;
1118
+ const b = ev.functionId;
1119
+ intersectFirstRef.current = null;
1120
+ updateGraph(
1121
+ (g) => addIntersection(g, a, b, () => `i${idCounterRef.current++}`)
1122
+ );
1123
+ setToolState("move");
1124
+ }
1125
+ } else if (currentTool === "tangent" && ev.type === "click-curve" && ev.functionId && ev.x !== void 0) {
1126
+ const pointId = `p${idCounterRef.current++}`;
1127
+ const tangentId = `t${idCounterRef.current++}`;
1128
+ updateGraph((g) => ({
1129
+ ...g,
1130
+ points: [...g.points, { id: pointId, functionId: ev.functionId, x: ev.x }],
1131
+ tangents: [...g.tangents, { id: tangentId, pointId }]
1132
+ }));
1133
+ setToolState("move");
1134
+ }
1135
+ }, [updateGraph]);
1136
+ react.useImperativeHandle(
1137
+ ref,
1138
+ () => ({
1139
+ insert: () => {
1140
+ const g = graphRef.current;
1141
+ if (g.functions.length === 0) return false;
1142
+ const jsonState = stringifySerializedGraph(g);
1143
+ renderGraph2dSvgFromState(jsonState).then((svg) => propsRef.current.onInsert(jsonState, svg)).catch((err) => console.error("Graph2D insert render failed:", err));
1144
+ return true;
1145
+ },
1146
+ hasContent: () => graphRef.current.functions.length > 0,
1147
+ setTool: (t) => {
1148
+ setToolState(t);
1149
+ const g = graphRef.current;
1150
+ propsRef.current.onStateChange({
1151
+ tool: t,
1152
+ showAxis: g.view.showAxis,
1153
+ showGrid: g.view.showGrid,
1154
+ canUndo: undoStackRef.current.length > 0
1155
+ });
1156
+ },
1157
+ setShowAxis: (b) => updateGraph((g) => ({ ...g, view: { ...g.view, showAxis: b } })),
1158
+ setShowGrid: (b) => updateGraph((g) => ({ ...g, view: { ...g.view, showGrid: b } })),
1159
+ resetView: () => updateGraph((g) => ({
1160
+ ...g,
1161
+ view: { ...g.view, xMin: -10, xMax: 10, yMin: -10, yMax: 10 }
1162
+ })),
1163
+ undo: () => {
1164
+ const prev = undoStackRef.current.pop();
1165
+ if (!prev) return;
1166
+ graphRef.current = prev;
1167
+ forceUpdate((n) => n + 1);
1168
+ propsRef.current.onStateChange({
1169
+ tool: toolRef.current,
1170
+ showAxis: prev.view.showAxis,
1171
+ showGrid: prev.view.showGrid,
1172
+ canUndo: undoStackRef.current.length > 0
1173
+ });
1174
+ propsRef.current.onGraphChange?.(prev);
1175
+ },
1176
+ addFunction: (expr) => {
1177
+ const g = graphRef.current;
1178
+ if (g.functions.length >= MAX_FUNCTIONS) {
1179
+ return { ok: false, error: `T\u1ED1i \u0111a ${MAX_FUNCTIONS} h\xE0m` };
1180
+ }
1181
+ const v = validate(expr);
1182
+ if (!v.ok) return { ok: false, error: v.error ?? "Invalid" };
1183
+ const id = `f${idCounterRef.current++}`;
1184
+ const usedNames = g.functions.map((f) => f.name);
1185
+ const usedColors = g.functions.map((f) => f.color);
1186
+ const newFn = {
1187
+ id,
1188
+ name: nextFunctionName(usedNames),
1189
+ expression: expr,
1190
+ color: nextColor(usedColors),
1191
+ visible: true
1192
+ };
1193
+ const usedParamNames = new Set(g.parameters.map((p) => p.name));
1194
+ const newParams = [];
1195
+ for (const varName of v.freeVars) {
1196
+ if (usedParamNames.has(varName)) continue;
1197
+ if (g.parameters.length + newParams.length >= MAX_PARAMETERS) break;
1198
+ newParams.push({ name: varName, value: 1, min: -5, max: 5, step: 0.1 });
1199
+ }
1200
+ updateGraph((prev) => ({
1201
+ ...prev,
1202
+ functions: [...prev.functions, newFn],
1203
+ parameters: [...prev.parameters, ...newParams]
1204
+ }));
1205
+ setErrorsWithNotify((e) => ({ ...e, [id]: null }));
1206
+ return { ok: true, id };
1207
+ },
1208
+ commitFunctionExpression: (id, expr) => {
1209
+ const g = graphRef.current;
1210
+ const v = validate(expr);
1211
+ if (!v.ok) {
1212
+ setErrorsWithNotify((e) => ({ ...e, [id]: v.error ?? "Invalid" }));
1213
+ return;
1214
+ }
1215
+ const usedParamNames = new Set(g.parameters.map((p) => p.name));
1216
+ const newParams = [];
1217
+ for (const varName of v.freeVars) {
1218
+ if (usedParamNames.has(varName)) continue;
1219
+ if (g.parameters.length + newParams.length >= MAX_PARAMETERS) break;
1220
+ newParams.push({ name: varName, value: 1, min: -5, max: 5, step: 0.1 });
1221
+ }
1222
+ updateGraph((prev) => ({
1223
+ ...prev,
1224
+ functions: prev.functions.map(
1225
+ (f) => f.id === id ? { ...f, expression: expr } : f
1226
+ ),
1227
+ parameters: [...prev.parameters, ...newParams]
1228
+ }));
1229
+ setErrorsWithNotify((e) => ({ ...e, [id]: null }));
1230
+ },
1231
+ toggleFunctionVisible: (id) => updateGraph((g) => ({
1232
+ ...g,
1233
+ functions: g.functions.map(
1234
+ (f) => f.id === id ? { ...f, visible: !f.visible } : f
1235
+ )
1236
+ })),
1237
+ removeFunction: (id) => updateGraph((g) => ({
1238
+ ...g,
1239
+ functions: g.functions.filter((f) => f.id !== id)
1240
+ })),
1241
+ // setParameter does NOT push undo — would flood the stack (slider drag)
1242
+ setParameter: (name, value) => {
1243
+ const next = {
1244
+ ...graphRef.current,
1245
+ parameters: graphRef.current.parameters.map(
1246
+ (p) => p.name === name ? { ...p, value } : p
1247
+ )
1248
+ };
1249
+ graphRef.current = next;
1250
+ forceUpdate((n) => n + 1);
1251
+ propsRef.current.onGraphChange?.(next);
1252
+ },
1253
+ setParameterRange: (name, min, max, step) => updateGraph((g) => ({
1254
+ ...g,
1255
+ parameters: g.parameters.map(
1256
+ (p) => p.name === name ? { ...p, min, max, step, value: Math.min(max, Math.max(min, p.value)) } : p
1257
+ )
1258
+ })),
1259
+ removeParameter: (name) => updateGraph((g) => ({
1260
+ ...g,
1261
+ parameters: g.parameters.filter((p) => p.name !== name)
1262
+ })),
1263
+ getGraph: () => graphRef.current,
1264
+ getErrors: () => errors
1265
+ }),
1266
+ // deps: updateGraph stable; errors changes when function errors change; setErrorsWithNotify stable
1267
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1268
+ [updateGraph, errors, setErrorsWithNotify]
1269
+ );
1270
+ react.useEffect(() => {
1271
+ if (!initialGraphNotifiedRef.current) {
1272
+ initialGraphNotifiedRef.current = true;
1273
+ propsRef.current.onGraphChange?.(graphRef.current);
1274
+ }
1275
+ }, []);
1276
+ const graph = graphRef.current;
1277
+ const hasContent = graph.functions.length > 0;
1278
+ const handleInsert = () => {
1279
+ const g = graphRef.current;
1280
+ if (g.functions.length === 0) return;
1281
+ const jsonState = stringifySerializedGraph(g);
1282
+ renderGraph2dSvgFromState(jsonState).then((svg) => propsRef.current.onInsert(jsonState, svg)).catch((err) => console.error("Graph2D insert render failed:", err));
1283
+ };
1284
+ const { isMobile, isDark, withLeftPanel } = props;
1285
+ const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
1286
+ position: "absolute",
1287
+ top: "50%",
1288
+ left: withLeftPanel ? "calc(50% + 120px)" : "50%",
1289
+ transform: "translate(-50%, -50%)",
1290
+ zIndex: 40
1291
+ };
1292
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1293
+ "div",
1294
+ {
1295
+ role: "dialog",
1296
+ "aria-label": "\u0110\u1ED3 th\u1ECB 2D",
1297
+ "data-testid": "graph-editor-panel",
1298
+ "data-stamp-area": "true",
1299
+ "data-mobile-editor": isMobile ? "true" : void 0,
1300
+ style: wrapperStyle,
1301
+ className: [
1302
+ isDark ? "theme--dark " : "",
1303
+ "flex flex-col overflow-hidden bg-white",
1304
+ isMobile ? "h-full w-full" : "h-[540px] max-h-[85vh] w-[640px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5"
1305
+ ].join(" "),
1306
+ children: [
1307
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-orange-500 to-amber-600 px-3 py-2 text-white", children: [
1308
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx(
1309
+ "button",
1310
+ {
1311
+ type: "button",
1312
+ onClick: props.onOpenDrawer,
1313
+ "aria-label": "M\u1EDF b\u1EA3ng \u0111\u1EA1i s\u1ED1",
1314
+ "data-testid": "graph-drawer-toggle",
1315
+ className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
1316
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1317
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
1318
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
1319
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
1320
+ ] })
1321
+ }
1322
+ ),
1323
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
1324
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1325
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 V3" }),
1326
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 H21" }),
1327
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
1328
+ ] }),
1329
+ "\u0110\u1ED3 th\u1ECB 2D"
1330
+ ] }),
1331
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx(
1332
+ "button",
1333
+ {
1334
+ type: "button",
1335
+ onClick: handleInsert,
1336
+ disabled: !hasContent,
1337
+ "data-testid": "graph-insert-btn-mobile",
1338
+ className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
1339
+ children: "Ch\xE8n"
1340
+ }
1341
+ ),
1342
+ /* @__PURE__ */ jsxRuntime.jsx(
1343
+ "button",
1344
+ {
1345
+ onClick: props.onClose,
1346
+ "aria-label": "\u0110\xF3ng",
1347
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
1348
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1349
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
1350
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
1351
+ ] })
1352
+ }
1353
+ )
1354
+ ] }),
1355
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
1356
+ MiniBoard,
1357
+ {
1358
+ graph,
1359
+ activeTool: tool,
1360
+ isDark,
1361
+ onBoardEvent
1362
+ }
1363
+ ) }),
1364
+ !isMobile && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
1365
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: "Nh\u1EADp bi\u1EC3u th\u1EE9c trong b\u1EA3ng \u0111\u1EA1i s\u1ED1 b\xEAn tr\xE1i." }),
1366
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
1367
+ /* @__PURE__ */ jsxRuntime.jsx(
1368
+ "button",
1369
+ {
1370
+ onClick: props.onClose,
1371
+ className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
1372
+ children: "Hu\u1EF7"
1373
+ }
1374
+ ),
1375
+ /* @__PURE__ */ jsxRuntime.jsx(
1376
+ "button",
1377
+ {
1378
+ onClick: handleInsert,
1379
+ disabled: !hasContent,
1380
+ "data-testid": "graph-insert-btn",
1381
+ className: "rounded bg-orange-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-orange-700 disabled:opacity-50",
1382
+ children: "Ch\xE8n"
1383
+ }
1384
+ )
1385
+ ] })
1386
+ ] })
1387
+ ]
1388
+ }
1389
+ );
1390
+ });
1391
+ }
1392
+ });
1393
+
1394
+ // src/stamps/shared/svgToImage.ts
1395
+ async function hashString(input) {
1396
+ if (typeof crypto !== "undefined" && crypto.subtle) {
1397
+ const buf = new TextEncoder().encode(input);
1398
+ const digest = await crypto.subtle.digest("SHA-256", buf);
1399
+ return Array.from(new Uint8Array(digest)).slice(0, 16).map((b) => b.toString(16).padStart(2, "0")).join("");
1400
+ }
1401
+ let h1 = 2166136261;
1402
+ let h2 = 3421674724;
1403
+ for (let i = 0; i < input.length; i++) {
1404
+ const c = input.charCodeAt(i);
1405
+ h1 ^= c;
1406
+ h1 = Math.imul(h1, 16777619);
1407
+ h2 ^= c + i;
1408
+ h2 = Math.imul(h2, 1099511628211 & 4294967295);
1409
+ }
1410
+ return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
1411
+ }
1412
+ function parseSize(svg, attr) {
1413
+ const re = new RegExp(`<svg[^>]*\\s${attr}="(\\d+(?:\\.\\d+)?)`, "i");
1414
+ const m = svg.match(re);
1415
+ if (m) return Math.max(1, Math.round(parseFloat(m[1])));
1416
+ const vb = svg.match(/viewBox="([\d.\s-]+)"/i);
1417
+ if (vb) {
1418
+ const parts = vb[1].trim().split(/\s+/).map(parseFloat);
1419
+ if (parts.length === 4) return Math.max(1, Math.round(attr === "width" ? parts[2] : parts[3]));
1420
+ }
1421
+ return attr === "width" ? 200 : 100;
1422
+ }
1423
+ async function svgToImageElement(svg) {
1424
+ const width = parseSize(svg, "width");
1425
+ const height = parseSize(svg, "height");
1426
+ const utf8 = unescape(encodeURIComponent(svg));
1427
+ const dataURL = "data:image/svg+xml;base64," + btoa(utf8);
1428
+ const fileId = await hashString(dataURL);
1429
+ return { dataURL, fileId, width, height, mimeType: "image/svg+xml" };
1430
+ }
1431
+ var init_svgToImage = __esm({
1432
+ "src/stamps/shared/svgToImage.ts"() {
1433
+ }
1434
+ });
1435
+
1436
+ // src/stamps/shared/insertImage.ts
1437
+ function buildStampImageElement(api, fileId, width, height, customData, x, y) {
1438
+ const appState = api?.getAppState() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
1439
+ const cx = x ?? appState.scrollX + (appState.width ?? 800) / 2 / (appState.zoom?.value ?? 1) - width / 2;
1440
+ const cy = y ?? appState.scrollY + (appState.height ?? 600) / 2 / (appState.zoom?.value ?? 1) - height / 2;
1441
+ return {
1442
+ type: "image",
1443
+ id: "stamp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8),
1444
+ x: cx,
1445
+ y: cy,
1446
+ width,
1447
+ height,
1448
+ fileId,
1449
+ customData,
1450
+ angle: 0,
1451
+ strokeColor: "transparent",
1452
+ backgroundColor: "transparent",
1453
+ fillStyle: "solid",
1454
+ strokeWidth: 1,
1455
+ strokeStyle: "solid",
1456
+ roughness: 0,
1457
+ opacity: 100,
1458
+ groupIds: [],
1459
+ roundness: null,
1460
+ seed: Math.floor(Math.random() * 1e9),
1461
+ versionNonce: 0,
1462
+ version: 1,
1463
+ isDeleted: false,
1464
+ boundElements: null,
1465
+ updated: Date.now(),
1466
+ link: null,
1467
+ locked: false,
1468
+ status: "saved",
1469
+ scale: [1, 1]
1470
+ };
1471
+ }
1472
+ async function insertStampImage(api, opts) {
1473
+ const { dataURL, fileId, width, height, mimeType } = await svgToImageElement(opts.svgString);
1474
+ api.addFiles([{ id: fileId, dataURL, mimeType, created: Date.now() }]);
1475
+ const customData = opts.makeCustomData(width, height);
1476
+ const elements = api.getSceneElements();
1477
+ const editingId = opts.editingElementId ?? null;
1478
+ if (editingId) {
1479
+ const updated = elements.map(
1480
+ (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
1481
+ );
1482
+ api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
1483
+ return { fileId, width, height, elementId: editingId };
1484
+ }
1485
+ const newElement = buildStampImageElement(
1486
+ api,
1487
+ fileId,
1488
+ width,
1489
+ height,
1490
+ customData,
1491
+ opts.position?.x,
1492
+ opts.position?.y
1493
+ );
1494
+ api.updateScene({
1495
+ elements: [...elements, newElement],
1496
+ appState: clearAppStateAfterInsert()
1497
+ });
1498
+ return { fileId, width, height, elementId: newElement.id };
1499
+ }
1500
+ var clearAppStateAfterInsert;
1501
+ var init_insertImage = __esm({
1502
+ "src/stamps/shared/insertImage.ts"() {
1503
+ init_svgToImage();
1504
+ clearAppStateAfterInsert = () => ({
1505
+ selectedElementIds: {},
1506
+ croppingElementId: null
1507
+ });
1508
+ }
1509
+ });
1510
+ function readMatch(query) {
1511
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1512
+ try {
1513
+ return window.matchMedia(query).matches;
1514
+ } catch {
1515
+ return false;
1516
+ }
1517
+ }
1518
+ function useIsMobile() {
1519
+ const [state, setState] = react.useState(() => ({
1520
+ isMobile: readMatch(MOBILE_QUERY),
1521
+ isTouchOnly: readMatch(NO_HOVER_QUERY)
1522
+ }));
1523
+ react.useEffect(() => {
1524
+ if (typeof window === "undefined" || !window.matchMedia) return;
1525
+ const mql = window.matchMedia(MOBILE_QUERY);
1526
+ const tql = window.matchMedia(NO_HOVER_QUERY);
1527
+ const update = () => {
1528
+ setState({ isMobile: mql.matches, isTouchOnly: tql.matches });
1529
+ };
1530
+ update();
1531
+ mql.addEventListener("change", update);
1532
+ tql.addEventListener("change", update);
1533
+ return () => {
1534
+ mql.removeEventListener("change", update);
1535
+ tql.removeEventListener("change", update);
1536
+ };
1537
+ }, []);
1538
+ return state;
1539
+ }
1540
+ var MOBILE_QUERY, NO_HOVER_QUERY;
1541
+ var init_useIsMobile = __esm({
1542
+ "src/stamps/shared/useIsMobile.ts"() {
1543
+ "use client";
1544
+ MOBILE_QUERY = "(max-width: 768px)";
1545
+ NO_HOVER_QUERY = "(hover: none)";
1546
+ }
1547
+ });
1548
+
1549
+ // src/stamps/graph-2d/host.tsx
1550
+ var host_exports = {};
1551
+ __export(host_exports, {
1552
+ Graph2DStampHost: () => Graph2DStampHost
1553
+ });
1554
+ var INITIAL_GRAPH_STATE, Graph2DStampHost;
1555
+ var init_host = __esm({
1556
+ "src/stamps/graph-2d/host.tsx"() {
1557
+ "use client";
1558
+ init_LeftPanel();
1559
+ init_EditorPanel();
1560
+ init_insertImage();
1561
+ init_serialize();
1562
+ init_useIsMobile();
1563
+ init_types();
1564
+ INITIAL_GRAPH_STATE = {
1565
+ tool: "move",
1566
+ showAxis: true,
1567
+ showGrid: true,
1568
+ canUndo: false
1569
+ };
1570
+ Graph2DStampHost = react.forwardRef(
1571
+ function Graph2DStampHost2({ api, editingElement, onClose, isDark }, ref) {
1572
+ const panelRef = react.useRef(null);
1573
+ const [graphUIState, setGraphUIState] = react.useState(INITIAL_GRAPH_STATE);
1574
+ const { isMobile } = useIsMobile();
1575
+ const [drawerOpen, setDrawerOpen] = react.useState(false);
1576
+ const initialState = react.useMemo(() => {
1577
+ if (!editingElement) return null;
1578
+ if (!isGraph2DCustomData(editingElement.customData)) return null;
1579
+ return parseSerializedGraph(editingElement.customData.jsonState);
1580
+ }, [editingElement]);
1581
+ const [graphSnapshot, setGraphSnapshot] = react.useState(
1582
+ initialState ?? EMPTY_GRAPH
1583
+ );
1584
+ const [errorsSnapshot, setErrorsSnapshot] = react.useState({});
1585
+ const handleInsert = react.useCallback(
1586
+ async (jsonState, svgString) => {
1587
+ if (!api) return;
1588
+ try {
1589
+ await insertStampImage(api, {
1590
+ svgString,
1591
+ makeCustomData: (width, height) => ({
1592
+ kind: "graph2d",
1593
+ version: 1,
1594
+ jsonState,
1595
+ svgWidth: width,
1596
+ svgHeight: height
1597
+ }),
1598
+ editingElementId: editingElement?.id ?? null
1599
+ });
1600
+ } catch (err) {
1601
+ console.error("Graph2D insert failed:", err);
1602
+ }
1603
+ onClose();
1604
+ },
1605
+ [api, editingElement?.id, onClose]
1606
+ );
1607
+ react.useImperativeHandle(
1608
+ ref,
1609
+ () => ({
1610
+ tryInsert: () => panelRef.current?.insert() ?? false,
1611
+ hasContent: () => panelRef.current?.hasContent() ?? false
1612
+ }),
1613
+ []
1614
+ );
1615
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1616
+ /* @__PURE__ */ jsxRuntime.jsx(
1617
+ GraphLeftPanel,
1618
+ {
1619
+ activeTool: graphUIState.tool,
1620
+ onToolChange: (t) => panelRef.current?.setTool(t),
1621
+ showAxis: graphUIState.showAxis,
1622
+ showGrid: graphUIState.showGrid,
1623
+ onShowAxisChange: (b) => panelRef.current?.setShowAxis(b),
1624
+ onShowGridChange: (b) => panelRef.current?.setShowGrid(b),
1625
+ onResetView: () => panelRef.current?.resetView(),
1626
+ onUndo: () => panelRef.current?.undo(),
1627
+ canUndo: graphUIState.canUndo,
1628
+ onClose,
1629
+ isDark,
1630
+ isMobile,
1631
+ drawerOpen,
1632
+ onDrawerClose: () => setDrawerOpen(false),
1633
+ graph: graphSnapshot,
1634
+ errors: errorsSnapshot,
1635
+ onAddFunctionDraft: () => {
1636
+ const result = panelRef.current?.addFunction("x");
1637
+ if (result && !result.ok) console.warn("addFunction failed:", result.error);
1638
+ },
1639
+ onCommitFunctionExpr: (id, expr) => panelRef.current?.commitFunctionExpression(id, expr),
1640
+ onToggleFunctionVisible: (id) => panelRef.current?.toggleFunctionVisible(id),
1641
+ onRemoveFunction: (id) => panelRef.current?.removeFunction(id),
1642
+ onParameterChange: (name, v) => panelRef.current?.setParameter(name, v),
1643
+ onParameterRangeChange: (name, min, max, step) => panelRef.current?.setParameterRange(name, min, max, step),
1644
+ onRemoveParameter: (name) => panelRef.current?.removeParameter(name)
1645
+ }
1646
+ ),
1647
+ /* @__PURE__ */ jsxRuntime.jsx(
1648
+ GraphEditorPanel,
1649
+ {
1650
+ ref: panelRef,
1651
+ initialState,
1652
+ onInsert: handleInsert,
1653
+ onClose,
1654
+ onStateChange: setGraphUIState,
1655
+ onGraphChange: setGraphSnapshot,
1656
+ onErrorsChange: setErrorsSnapshot,
1657
+ withLeftPanel: !isMobile,
1658
+ isDark,
1659
+ isMobile,
1660
+ onOpenDrawer: () => setDrawerOpen(true)
1661
+ }
1662
+ )
1663
+ ] });
1664
+ }
1665
+ );
1666
+ }
1667
+ });
1668
+
1669
+ // src/stamps/graph-2d/index.tsx
1670
+ init_render();
1671
+ init_types();
1672
+ var Graph2DStampHost3 = react.lazy(
1673
+ () => Promise.resolve().then(() => (init_host(), host_exports)).then((m) => ({ default: m.Graph2DStampHost }))
1674
+ );
1675
+ var Graph2DIcon = /* @__PURE__ */ jsxRuntime.jsxs(
1676
+ "svg",
1677
+ {
1678
+ width: "20",
1679
+ height: "20",
1680
+ viewBox: "0 0 24 24",
1681
+ fill: "none",
1682
+ stroke: "currentColor",
1683
+ strokeWidth: "1.6",
1684
+ strokeLinecap: "round",
1685
+ strokeLinejoin: "round",
1686
+ "aria-hidden": "true",
1687
+ children: [
1688
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 V3" }),
1689
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 H21" }),
1690
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
1691
+ ]
1692
+ }
1693
+ );
1694
+ var graph2dStamp = {
1695
+ kind: "graph2d",
1696
+ experimental: true,
1697
+ shortcutKey: "h",
1698
+ toolbarLabel: "H",
1699
+ toolbarTitle: "Ch\xE8n \u0111\u1ED3 th\u1ECB 2D (H)",
1700
+ toolbarIcon: Graph2DIcon,
1701
+ toolbarTestId: "stamp-toolbar-graph2d",
1702
+ matchesCustomData: isGraph2DCustomData,
1703
+ async renderSvgFromCustomData(data) {
1704
+ if (!isGraph2DCustomData(data)) {
1705
+ throw new Error("graph2dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i graph2d");
1706
+ }
1707
+ return renderGraph2dSvgFromState(data.jsonState);
1708
+ },
1709
+ async restoreFileFromCustomData(element) {
1710
+ const data = element.customData;
1711
+ const fileId = element.fileId;
1712
+ if (!data || !fileId) return null;
1713
+ if (!isGraph2DCustomData(data)) return null;
1714
+ const svgString = await renderGraph2dSvgFromState(data.jsonState);
1715
+ const utf8 = unescape(encodeURIComponent(svgString));
1716
+ const dataURL = "data:image/svg+xml;base64," + (typeof btoa !== "undefined" ? btoa(utf8) : Buffer.from(utf8).toString("base64"));
1717
+ return { fileId, dataURL, mimeType: "image/svg+xml" };
1718
+ },
1719
+ Host: Graph2DStampHost3
1720
+ };
1721
+
1722
+ exports.graph2dStamp = graph2dStamp;
1723
+ exports.isGraph2DCustomData = isGraph2DCustomData;
1724
+ //# sourceMappingURL=graph-2d.js.map
1725
+ //# sourceMappingURL=graph-2d.js.map