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