@xom11/whiteboard 0.7.0 → 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.
Files changed (49) hide show
  1. package/README.md +51 -1
  2. package/dist/chunk-74VEEZBV.mjs +619 -0
  3. package/dist/chunk-74VEEZBV.mjs.map +1 -0
  4. package/dist/chunk-DU2NFHRR.mjs +103 -0
  5. package/dist/chunk-DU2NFHRR.mjs.map +1 -0
  6. package/dist/{chunk-SHFOGORM.mjs → chunk-DU3RHKT5.mjs} +4 -4
  7. package/dist/{chunk-SHFOGORM.mjs.map → chunk-DU3RHKT5.mjs.map} +1 -1
  8. package/dist/{chunk-HYXFHEDJ.mjs → chunk-IUVV52HO.mjs} +22 -7
  9. package/dist/chunk-IUVV52HO.mjs.map +1 -0
  10. package/dist/{chunk-BJX4YNA5.mjs → chunk-KEYZ5EZT.mjs} +26 -9
  11. package/dist/chunk-KEYZ5EZT.mjs.map +1 -0
  12. package/dist/{chunk-LPM4MM45.mjs → chunk-SBDMF4NQ.mjs} +3 -2
  13. package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
  14. package/dist/{chunk-3SSQKRRO.mjs → chunk-ZVN356JZ.mjs} +4 -4
  15. package/dist/{chunk-3SSQKRRO.mjs.map → chunk-ZVN356JZ.mjs.map} +1 -1
  16. package/dist/geometry-2d.js +250 -218
  17. package/dist/geometry-2d.js.map +1 -1
  18. package/dist/geometry-2d.mjs +2 -2
  19. package/dist/geometry-3d.d.mts +1 -1
  20. package/dist/geometry-3d.d.ts +1 -1
  21. package/dist/geometry-3d.js +3276 -1201
  22. package/dist/geometry-3d.js.map +1 -1
  23. package/dist/geometry-3d.mjs +3 -2
  24. package/dist/graph-2d.js +360 -66
  25. package/dist/graph-2d.js.map +1 -1
  26. package/dist/graph-2d.mjs +2 -2
  27. package/dist/{host-2QGKMGCT.mjs → host-LZH2FZ2N.mjs} +3 -3
  28. package/dist/{host-2QGKMGCT.mjs.map → host-LZH2FZ2N.mjs.map} +1 -1
  29. package/dist/host-PIIDSMVE.mjs +3187 -0
  30. package/dist/host-PIIDSMVE.mjs.map +1 -0
  31. package/dist/{host-T2W6R6SO.mjs → host-VDNAJMLC.mjs} +221 -216
  32. package/dist/host-VDNAJMLC.mjs.map +1 -0
  33. package/dist/index.d.mts +6 -5
  34. package/dist/index.d.ts +6 -5
  35. package/dist/index.js +4365 -1821
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +246 -102
  38. package/dist/index.mjs.map +1 -1
  39. package/package.json +6 -6
  40. package/dist/chunk-BJX4YNA5.mjs.map +0 -1
  41. package/dist/chunk-DJTBZEAR.mjs +0 -25
  42. package/dist/chunk-DJTBZEAR.mjs.map +0 -1
  43. package/dist/chunk-HM7RIXJE.mjs +0 -331
  44. package/dist/chunk-HM7RIXJE.mjs.map +0 -1
  45. package/dist/chunk-HYXFHEDJ.mjs.map +0 -1
  46. package/dist/chunk-LPM4MM45.mjs.map +0 -1
  47. package/dist/host-T2W6R6SO.mjs.map +0 -1
  48. package/dist/host-XUFON6CQ.mjs +0 -1422
  49. package/dist/host-XUFON6CQ.mjs.map +0 -1
package/README.md CHANGED
@@ -14,7 +14,33 @@ npm install github:xom11/whiteboard#v0.2.0
14
14
  "@xom11/whiteboard": "github:xom11/whiteboard#v0.2.0"
15
15
  ```
16
16
 
17
- Peer deps: `react >=18`, `react-dom >=18`, `next >=14`.
17
+ ### Peer dependencies
18
+
19
+ Từ phiên bản này, `@excalidraw/excalidraw`, `jsxgraph` và `katex` được externalize khỏi bundle (giảm bundle ~70%). Consumer **bắt buộc** cài kèm:
20
+
21
+ ```bash
22
+ npm install @excalidraw/excalidraw@^0.18.1 jsxgraph@^1.12.2 katex@^0.16.45 react@>=18 react-dom@>=18
23
+ ```
24
+
25
+ Hoặc thêm vào `package.json` của consumer:
26
+
27
+ ```json
28
+ {
29
+ "dependencies": {
30
+ "@excalidraw/excalidraw": "^0.18.1",
31
+ "jsxgraph": "^1.12.2",
32
+ "katex": "^0.16.45",
33
+ "react": ">=18",
34
+ "react-dom": ">=18"
35
+ }
36
+ }
37
+ ```
38
+
39
+ Lý do externalize:
40
+
41
+ - **Tránh duplicate React** khi consumer cũng dùng Excalidraw trực tiếp.
42
+ - **Bundle nhẹ hơn ~70%** (jsxgraph ~600KB + katex ~280KB + excalidraw ~2MB không còn nằm trong dist của whiteboard).
43
+ - **Consumer kiểm soát version** + dedupe qua npm/pnpm tự nhiên.
18
44
 
19
45
  ## Usage
20
46
 
@@ -35,6 +61,19 @@ export function ClassroomBoard() {
35
61
  }
36
62
  ```
37
63
 
64
+ ## Migration to v0.8.0 (geometry-3d redesign)
65
+
66
+ `geometry3dStamp` được viết lại theo UX của GeoGebra 3D Calculator:
67
+
68
+ - Click trên mặt nền / trục / mặt phẳng / mặt cầu để **đặt điểm constraint** (không còn prompt nhập toạ độ).
69
+ - **Drag điểm** trên Move tool — điểm trượt theo surface (z-axis chỉ thay đổi z, mặt nền giữ z=0).
70
+ - **Algebra panel** mới (tab bên trái) hiển thị mỗi object: label, biểu thức symbolic, giá trị numeric, menu ⋮ (đổi tên / màu / ẩn / xoá).
71
+ - 16 tool: Move, Point, Point-on-Object, Segment, Line, Ray, Vector, Polygon, Plane (3 điểm), Pyramid, Prism, Tetrahedron, Cube, Sphere, Cylinder, Cone.
72
+
73
+ **Backward compat**: Stamps lưu từ v0.7.0 load OK (legacy points → constraint free). API consumer giữ nguyên.
74
+
75
+ **Tạm thời bỏ**: Chord-shortcut 2 phím (`G S` cho segment, v.v.) — sẽ trở lại với letter-mapping mới.
76
+
38
77
  ## Migration to v0.7.0 (BREAKING)
39
78
 
40
79
  `DEFAULT_STAMPS` v0.7.0 chỉ gồm 2 stamps stable: `geometry` + `latex`. 3D + graph2d chuyển sang opt-IN (experimental).
@@ -80,6 +119,17 @@ npm run build # tsup → dist/{index.js, index.mjs, index.d.ts}
80
119
  npm run dev # tsup watch mode
81
120
  ```
82
121
 
122
+ ## E2E tests
123
+
124
+ Playwright smoke tests chạy qua headless Chromium, tự start vite demo:
125
+
126
+ ```bash
127
+ npx playwright install chromium # cài browser binary (chỉ làm 1 lần)
128
+ npm run test:e2e # chạy specs
129
+ ```
130
+
131
+ Chi tiết xem [`tests/e2e/README.md`](./tests/e2e/README.md).
132
+
83
133
  ## Workflow phát hành phiên bản mới
84
134
 
85
135
  `npm ci --ignore-scripts` ở consumer skip prepare hook → phải commit `dist/`:
@@ -0,0 +1,619 @@
1
+ "use client";
2
+ // src/stamps/graph-2d/serialize.ts
3
+ var EMPTY_GRAPH = {
4
+ version: 1,
5
+ view: { xMin: -10, xMax: 10, yMin: -10, yMax: 10, showAxis: true, showGrid: true },
6
+ functions: [],
7
+ parameters: [],
8
+ points: [],
9
+ intersections: [],
10
+ tangents: []
11
+ };
12
+ function stringifySerializedGraph(graph) {
13
+ return JSON.stringify(graph);
14
+ }
15
+ function parseSerializedGraph(jsonState) {
16
+ let raw;
17
+ try {
18
+ raw = JSON.parse(jsonState);
19
+ } catch {
20
+ return null;
21
+ }
22
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
23
+ const r = raw;
24
+ if (r.version !== 1) return null;
25
+ if (!r.view || typeof r.view !== "object") return null;
26
+ const v = r.view;
27
+ 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") {
28
+ return null;
29
+ }
30
+ for (const key of ["functions", "parameters", "points", "intersections", "tangents"]) {
31
+ if (!Array.isArray(r[key])) return null;
32
+ }
33
+ return raw;
34
+ }
35
+
36
+ // src/stamps/graph-2d/evaluator.ts
37
+ var ALLOWED_FUNCTIONS = {
38
+ sin: Math.sin,
39
+ cos: Math.cos,
40
+ tan: Math.tan,
41
+ asin: Math.asin,
42
+ acos: Math.acos,
43
+ atan: Math.atan,
44
+ log: Math.log10,
45
+ // log = log10 (khớp với rewriteToJs)
46
+ ln: Math.log,
47
+ // ln = log tự nhiên
48
+ exp: Math.exp,
49
+ sqrt: Math.sqrt,
50
+ abs: Math.abs,
51
+ floor: Math.floor,
52
+ ceil: Math.ceil,
53
+ round: Math.round
54
+ };
55
+ var ALLOWED_CONSTANTS = {
56
+ pi: Math.PI,
57
+ e: Math.E
58
+ };
59
+ var OPERATORS = /* @__PURE__ */ new Set(["+", "-", "*", "/", "^"]);
60
+ function tokenize(src) {
61
+ const tokens = [];
62
+ let i = 0;
63
+ while (i < src.length) {
64
+ const ch = src[i];
65
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
66
+ i++;
67
+ continue;
68
+ }
69
+ if (ch >= "0" && ch <= "9" || ch === ".") {
70
+ let j = i;
71
+ let hasDot = false;
72
+ let hasExp = false;
73
+ while (j < src.length) {
74
+ const c = src[j];
75
+ if (c >= "0" && c <= "9") {
76
+ j++;
77
+ } else if (c === "." && !hasDot && !hasExp) {
78
+ hasDot = true;
79
+ j++;
80
+ } else if ((c === "e" || c === "E") && !hasExp) {
81
+ hasExp = true;
82
+ j++;
83
+ if (src[j] === "+" || src[j] === "-") j++;
84
+ } else {
85
+ break;
86
+ }
87
+ }
88
+ const raw = src.slice(i, j);
89
+ if (!/[0-9]/.test(raw)) {
90
+ throw new Error(`S\u1ED1 kh\xF4ng h\u1EE3p l\u1EC7 t\u1EA1i v\u1ECB tr\xED ${i}: "${raw}"`);
91
+ }
92
+ tokens.push({ type: "NUMBER", value: raw, pos: i });
93
+ i = j;
94
+ continue;
95
+ }
96
+ if (ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z") {
97
+ let j = i;
98
+ while (j < src.length) {
99
+ const c = src[j];
100
+ if (c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "_") {
101
+ j++;
102
+ } else {
103
+ break;
104
+ }
105
+ }
106
+ tokens.push({ type: "IDENT", value: src.slice(i, j), pos: i });
107
+ i = j;
108
+ continue;
109
+ }
110
+ if (OPERATORS.has(ch)) {
111
+ tokens.push({ type: "OP", value: ch, pos: i });
112
+ i++;
113
+ continue;
114
+ }
115
+ if (ch === "(") {
116
+ tokens.push({ type: "LPAREN", value: ch, pos: i });
117
+ i++;
118
+ continue;
119
+ }
120
+ if (ch === ")") {
121
+ tokens.push({ type: "RPAREN", value: ch, pos: i });
122
+ i++;
123
+ continue;
124
+ }
125
+ if (ch === ",") {
126
+ tokens.push({ type: "COMMA", value: ch, pos: i });
127
+ i++;
128
+ continue;
129
+ }
130
+ throw new Error(`K\xFD t\u1EF1 kh\xF4ng h\u1EE3p l\u1EC7 t\u1EA1i v\u1ECB tr\xED ${i}: "${ch}"`);
131
+ }
132
+ return tokens;
133
+ }
134
+ var Parser = class {
135
+ constructor(tokens) {
136
+ this.tokens = tokens;
137
+ this.pos = 0;
138
+ }
139
+ peek() {
140
+ return this.tokens[this.pos];
141
+ }
142
+ consume() {
143
+ const t = this.tokens[this.pos++];
144
+ if (!t) throw new Error("C\xFA ph\xE1p: h\u1EBFt token s\u1EDBm");
145
+ return t;
146
+ }
147
+ parseExpression() {
148
+ const node = this.parseAddSub();
149
+ if (this.pos < this.tokens.length) {
150
+ const t = this.tokens[this.pos];
151
+ throw new Error(`C\xFA ph\xE1p: token th\u1EEBa "${t.value}" t\u1EA1i v\u1ECB tr\xED ${t.pos}`);
152
+ }
153
+ return node;
154
+ }
155
+ // + - (left assoc)
156
+ parseAddSub() {
157
+ let lhs = this.parseMulDiv();
158
+ while (true) {
159
+ const t = this.peek();
160
+ if (t && t.type === "OP" && (t.value === "+" || t.value === "-")) {
161
+ this.consume();
162
+ const rhs = this.parseMulDiv();
163
+ lhs = { kind: "binary", op: t.value, lhs, rhs };
164
+ } else {
165
+ break;
166
+ }
167
+ }
168
+ return lhs;
169
+ }
170
+ // * / (left assoc)
171
+ parseMulDiv() {
172
+ let lhs = this.parseUnary();
173
+ while (true) {
174
+ const t = this.peek();
175
+ if (t && t.type === "OP" && (t.value === "*" || t.value === "/")) {
176
+ this.consume();
177
+ const rhs = this.parseUnary();
178
+ lhs = { kind: "binary", op: t.value, lhs, rhs };
179
+ } else {
180
+ break;
181
+ }
182
+ }
183
+ return lhs;
184
+ }
185
+ // unary + - (right assoc) sau đó parsePow
186
+ parseUnary() {
187
+ const t = this.peek();
188
+ if (t && t.type === "OP" && (t.value === "+" || t.value === "-")) {
189
+ this.consume();
190
+ const arg = this.parseUnary();
191
+ return { kind: "unary", op: t.value, arg };
192
+ }
193
+ return this.parsePow();
194
+ }
195
+ // ^ (right assoc)
196
+ parsePow() {
197
+ const lhs = this.parsePrimary();
198
+ const t = this.peek();
199
+ if (t && t.type === "OP" && t.value === "^") {
200
+ this.consume();
201
+ const rhs = this.parseUnary();
202
+ return { kind: "binary", op: "^", lhs, rhs };
203
+ }
204
+ return lhs;
205
+ }
206
+ parsePrimary() {
207
+ const t = this.peek();
208
+ if (!t) throw new Error("C\xFA ph\xE1p: thi\u1EBFu bi\u1EC3u th\u1EE9c");
209
+ if (t.type === "NUMBER") {
210
+ this.consume();
211
+ const v = Number(t.value);
212
+ return { kind: "num", value: v };
213
+ }
214
+ if (t.type === "IDENT") {
215
+ this.consume();
216
+ const next = this.peek();
217
+ if (next && next.type === "LPAREN") {
218
+ this.consume();
219
+ const args = [];
220
+ const lookahead = this.peek();
221
+ if (!lookahead || lookahead.type !== "RPAREN") {
222
+ args.push(this.parseAddSub());
223
+ while (true) {
224
+ const nx = this.peek();
225
+ if (nx && nx.type === "COMMA") {
226
+ this.consume();
227
+ args.push(this.parseAddSub());
228
+ } else {
229
+ break;
230
+ }
231
+ }
232
+ }
233
+ const close = this.peek();
234
+ if (!close || close.type !== "RPAREN") {
235
+ throw new Error(`C\xFA ph\xE1p: thi\u1EBFu ")" sau h\xE0m "${t.value}"`);
236
+ }
237
+ this.consume();
238
+ return { kind: "call", name: t.value, args };
239
+ }
240
+ return { kind: "ident", name: t.value };
241
+ }
242
+ if (t.type === "LPAREN") {
243
+ this.consume();
244
+ const inner = this.parseAddSub();
245
+ const close = this.peek();
246
+ if (!close || close.type !== "RPAREN") {
247
+ throw new Error('C\xFA ph\xE1p: thi\u1EBFu ")"');
248
+ }
249
+ this.consume();
250
+ return inner;
251
+ }
252
+ throw new Error(`C\xFA ph\xE1p: token b\u1EA5t ng\u1EDD "${t.value}" t\u1EA1i v\u1ECB tr\xED ${t.pos}`);
253
+ }
254
+ };
255
+ function parseAst(src) {
256
+ const tokens = tokenize(src);
257
+ if (tokens.length === 0) throw new Error("Bi\u1EC3u th\u1EE9c r\u1ED7ng");
258
+ const p = new Parser(tokens);
259
+ return p.parseExpression();
260
+ }
261
+ function evaluate(node, env) {
262
+ switch (node.kind) {
263
+ case "num":
264
+ return node.value;
265
+ case "ident": {
266
+ const name = node.name;
267
+ if (name === "x") return env.x;
268
+ if (Object.prototype.hasOwnProperty.call(ALLOWED_CONSTANTS, name)) {
269
+ return ALLOWED_CONSTANTS[name];
270
+ }
271
+ if (name.length === 1 && Object.prototype.hasOwnProperty.call(env.params, name)) {
272
+ return env.params[name];
273
+ }
274
+ throw new Error(`Identifier kh\xF4ng h\u1EE3p l\u1EC7: "${name}"`);
275
+ }
276
+ case "unary": {
277
+ const v = evaluate(node.arg, env);
278
+ return node.op === "-" ? -v : +v;
279
+ }
280
+ case "binary": {
281
+ const a = evaluate(node.lhs, env);
282
+ const b = evaluate(node.rhs, env);
283
+ switch (node.op) {
284
+ case "+":
285
+ return a + b;
286
+ case "-":
287
+ return a - b;
288
+ case "*":
289
+ return a * b;
290
+ case "/":
291
+ return a / b;
292
+ // có thể trả Infinity/NaN — đúng theo IEEE 754
293
+ case "^":
294
+ return Math.pow(a, b);
295
+ }
296
+ throw new Error(`To\xE1n t\u1EED kh\xF4ng h\u1ED7 tr\u1EE3: "${node.op}"`);
297
+ }
298
+ case "call": {
299
+ const fn = ALLOWED_FUNCTIONS[node.name];
300
+ if (typeof fn !== "function") {
301
+ throw new Error(`H\xE0m kh\xF4ng h\u1EE3p l\u1EC7: "${node.name}"`);
302
+ }
303
+ const args = node.args.map((a) => evaluate(a, env));
304
+ return fn(...args);
305
+ }
306
+ }
307
+ }
308
+ function collectFreeVars(node, out = /* @__PURE__ */ new Set()) {
309
+ switch (node.kind) {
310
+ case "num":
311
+ return out;
312
+ case "ident": {
313
+ const name = node.name;
314
+ if (name === "x") return out;
315
+ if (Object.prototype.hasOwnProperty.call(ALLOWED_CONSTANTS, name)) return out;
316
+ if (name.length === 1) out.add(name);
317
+ return out;
318
+ }
319
+ case "unary":
320
+ return collectFreeVars(node.arg, out);
321
+ case "binary":
322
+ collectFreeVars(node.lhs, out);
323
+ collectFreeVars(node.rhs, out);
324
+ return out;
325
+ case "call":
326
+ for (const a of node.args) collectFreeVars(a, out);
327
+ return out;
328
+ }
329
+ }
330
+ function checkIdentifiers(node) {
331
+ switch (node.kind) {
332
+ case "num":
333
+ return null;
334
+ case "ident": {
335
+ const name = node.name;
336
+ if (name === "x") return null;
337
+ if (Object.prototype.hasOwnProperty.call(ALLOWED_CONSTANTS, name)) return null;
338
+ if (name.length === 1) return null;
339
+ return `T\xEAn kh\xF4ng h\u1EE3p l\u1EC7: "${name}"`;
340
+ }
341
+ case "unary":
342
+ return checkIdentifiers(node.arg);
343
+ case "binary":
344
+ return checkIdentifiers(node.lhs) ?? checkIdentifiers(node.rhs);
345
+ case "call": {
346
+ if (!Object.prototype.hasOwnProperty.call(ALLOWED_FUNCTIONS, node.name)) {
347
+ return `T\xEAn h\xE0m kh\xF4ng h\u1EE3p l\u1EC7: "${node.name}"`;
348
+ }
349
+ for (const a of node.args) {
350
+ const e = checkIdentifiers(a);
351
+ if (e) return e;
352
+ }
353
+ return null;
354
+ }
355
+ }
356
+ }
357
+ var ALLOWED_FUNCTION_NAMES = new Set(Object.keys(ALLOWED_FUNCTIONS));
358
+ new Set(Object.keys(ALLOWED_CONSTANTS));
359
+
360
+ // src/stamps/graph-2d/parser.ts
361
+ var ALLOWED_FUNCTIONS2 = ALLOWED_FUNCTION_NAMES;
362
+ var ALLOWED_CHARS = /^[a-zA-Z0-9_.+\-*/^()\s,]+$/;
363
+ var SUGGESTIONS = {
364
+ tg: "tan",
365
+ arcsin: "asin",
366
+ arccos: "acos",
367
+ arctan: "atan"
368
+ };
369
+ function errResult(message) {
370
+ return { ok: false, error: message, freeVars: /* @__PURE__ */ new Set() };
371
+ }
372
+ function validate(expr) {
373
+ const trimmed = expr.trim();
374
+ if (!trimmed) return errResult("Bi\u1EC3u th\u1EE9c r\u1ED7ng");
375
+ if (!ALLOWED_CHARS.test(trimmed)) return errResult("K\xFD t\u1EF1 kh\xF4ng h\u1EE3p l\u1EC7");
376
+ let tokens;
377
+ try {
378
+ tokens = tokenize(trimmed);
379
+ } catch {
380
+ return errResult("L\u1ED7i c\xFA ph\xE1p");
381
+ }
382
+ const earlyFree = /* @__PURE__ */ new Set();
383
+ for (const tok of tokens) {
384
+ if (tok.type !== "IDENT") continue;
385
+ const id = tok.value;
386
+ if (id === "x" || id === "pi" || id === "e") continue;
387
+ if (ALLOWED_FUNCTIONS2.has(id)) continue;
388
+ if (id.length === 1) {
389
+ earlyFree.add(id);
390
+ continue;
391
+ }
392
+ const hint = SUGGESTIONS[id];
393
+ return errResult(
394
+ 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}"`
395
+ );
396
+ }
397
+ let ast;
398
+ try {
399
+ ast = parseAst(trimmed);
400
+ } catch {
401
+ return errResult("L\u1ED7i c\xFA ph\xE1p");
402
+ }
403
+ const idErr = checkIdentifiers(ast);
404
+ if (idErr) return errResult(idErr);
405
+ const freeVars = collectFreeVars(ast);
406
+ for (const v of earlyFree) freeVars.add(v);
407
+ return { ok: true, freeVars };
408
+ }
409
+ function compile(expr, paramValues) {
410
+ const v = validate(expr);
411
+ if (!v.ok) return { error: v.error ?? "Invalid" };
412
+ let ast;
413
+ try {
414
+ ast = parseAst(expr.trim());
415
+ } catch (err) {
416
+ return { error: err instanceof Error ? err.message : String(err) };
417
+ }
418
+ return (x) => {
419
+ try {
420
+ const y = evaluate(ast, { x, params: paramValues });
421
+ return typeof y === "number" ? y : NaN;
422
+ } catch {
423
+ return NaN;
424
+ }
425
+ };
426
+ }
427
+
428
+ // src/stamps/graph-2d/editor/handlers.ts
429
+ function addPointOnCurve(graph, ctx, idFactory) {
430
+ if (!ctx.functionId) return graph;
431
+ const point = {
432
+ id: idFactory(),
433
+ functionId: ctx.functionId,
434
+ x: ctx.x
435
+ };
436
+ return { ...graph, points: [...graph.points, point] };
437
+ }
438
+ function addIntersection(graph, functionIdA, functionIdB, idFactory) {
439
+ if (functionIdA === functionIdB) return graph;
440
+ const exists = graph.intersections.some(
441
+ (i) => i.functionIdA === functionIdA && i.functionIdB === functionIdB || i.functionIdA === functionIdB && i.functionIdB === functionIdA
442
+ );
443
+ if (exists) return graph;
444
+ const intersection = {
445
+ id: idFactory(),
446
+ functionIdA,
447
+ functionIdB
448
+ };
449
+ return { ...graph, intersections: [...graph.intersections, intersection] };
450
+ }
451
+ function numericalDerivative(expression, paramValues, x, h = 1e-4) {
452
+ const fn = compile(expression, paramValues);
453
+ if (typeof fn !== "function") return NaN;
454
+ const y1 = fn(x - h);
455
+ const y2 = fn(x + h);
456
+ return (y2 - y1) / (2 * h);
457
+ }
458
+
459
+ // src/stamps/graph-2d/renderObjects.ts
460
+ function renderGraphObjects(board, graph) {
461
+ const paramMap = {};
462
+ for (const p of graph.parameters) paramMap[p.name] = p.value;
463
+ for (const f of graph.functions) {
464
+ if (!f.visible) continue;
465
+ const compiled = compile(f.expression, paramMap);
466
+ if (typeof compiled !== "function") continue;
467
+ const domain = f.domain ?? { min: graph.view.xMin, max: graph.view.xMax };
468
+ board.create("functiongraph", [compiled, domain.min, domain.max], {
469
+ strokeColor: f.color,
470
+ strokeWidth: 2,
471
+ name: f.name,
472
+ withLabel: false,
473
+ highlight: false
474
+ });
475
+ }
476
+ for (const point of graph.points) {
477
+ const fn = graph.functions.find((f) => f.id === point.functionId);
478
+ if (!fn || !fn.visible) continue;
479
+ const compiled = compile(fn.expression, paramMap);
480
+ if (typeof compiled !== "function") continue;
481
+ const y = compiled(point.x);
482
+ board.create("point", [point.x, y], {
483
+ name: point.label ?? "",
484
+ size: 3,
485
+ fillColor: fn.color,
486
+ strokeColor: fn.color,
487
+ withLabel: !!point.label
488
+ });
489
+ }
490
+ for (const inter of graph.intersections) {
491
+ const fa = graph.functions.find((f) => f.id === inter.functionIdA);
492
+ const fb = graph.functions.find((f) => f.id === inter.functionIdB);
493
+ if (!fa || !fb || !fa.visible || !fb.visible) continue;
494
+ const cfa = compile(fa.expression, paramMap);
495
+ const cfb = compile(fb.expression, paramMap);
496
+ if (typeof cfa !== "function" || typeof cfb !== "function") continue;
497
+ const roots = scanRoots((x) => cfa(x) - cfb(x), graph.view.xMin, graph.view.xMax);
498
+ for (const x of roots) {
499
+ board.create("point", [x, cfa(x)], {
500
+ size: 3,
501
+ fillColor: "#000",
502
+ strokeColor: "#000"
503
+ });
504
+ }
505
+ }
506
+ for (const tan of graph.tangents) {
507
+ const pt = graph.points.find((p) => p.id === tan.pointId);
508
+ if (!pt) continue;
509
+ const fn = graph.functions.find((f) => f.id === pt.functionId);
510
+ if (!fn || !fn.visible) continue;
511
+ const slope = numericalDerivative(fn.expression, paramMap, pt.x);
512
+ const cfn = compile(fn.expression, paramMap);
513
+ if (typeof cfn !== "function" || !Number.isFinite(slope)) continue;
514
+ const y0 = cfn(pt.x);
515
+ const x1 = graph.view.xMin;
516
+ const x2 = graph.view.xMax;
517
+ board.create(
518
+ "line",
519
+ [
520
+ [x1, slope * (x1 - pt.x) + y0],
521
+ [x2, slope * (x2 - pt.x) + y0]
522
+ ],
523
+ {
524
+ strokeColor: fn.color,
525
+ strokeWidth: 1,
526
+ dash: 2,
527
+ straightFirst: false,
528
+ straightLast: false
529
+ }
530
+ );
531
+ }
532
+ }
533
+ function scanRoots(fn, xMin, xMax, samples = 200) {
534
+ const roots = [];
535
+ const step = (xMax - xMin) / samples;
536
+ let prevX = xMin;
537
+ let prevY = fn(prevX);
538
+ for (let i = 1; i <= samples; i++) {
539
+ const x = xMin + i * step;
540
+ const y = fn(x);
541
+ if (Number.isFinite(prevY) && Number.isFinite(y) && prevY * y < 0) {
542
+ let a = prevX;
543
+ let b = x;
544
+ let ya = prevY;
545
+ for (let j = 0; j < 30; j++) {
546
+ const m = (a + b) / 2;
547
+ const ym = fn(m);
548
+ if (Math.abs(ym) < 1e-6) {
549
+ a = b = m;
550
+ break;
551
+ }
552
+ if (ya * ym < 0) {
553
+ b = m;
554
+ } else {
555
+ a = m;
556
+ ya = ym;
557
+ }
558
+ }
559
+ roots.push((a + b) / 2);
560
+ }
561
+ prevX = x;
562
+ prevY = y;
563
+ }
564
+ return roots;
565
+ }
566
+
567
+ // src/stamps/graph-2d/render.ts
568
+ async function renderGraph2dSvgFromState(jsonState) {
569
+ const parsed = parseSerializedGraph(jsonState);
570
+ if (!parsed) throw new Error("renderGraph2dSvgFromState: jsonState corrupt");
571
+ const JXG = (await import('jsxgraph')).default;
572
+ const opts = JXG.Options;
573
+ if (opts) {
574
+ opts.text = opts.text || {};
575
+ opts.text.display = "internal";
576
+ opts.text.useASCIIMathML = false;
577
+ opts.text.useMathJax = false;
578
+ opts.text.useKatex = false;
579
+ opts.label = opts.label || {};
580
+ opts.label.display = "internal";
581
+ }
582
+ const container = document.createElement("div");
583
+ container.id = `jxg_graph2d_off_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
584
+ container.style.cssText = "position:absolute;top:-99999px;left:-99999px;width:600px;height:400px;visibility:hidden;pointer-events:none;";
585
+ document.body.appendChild(container);
586
+ let board = null;
587
+ try {
588
+ board = JXG.JSXGraph.initBoard(container.id, {
589
+ boundingbox: [parsed.view.xMin, parsed.view.yMax, parsed.view.xMax, parsed.view.yMin],
590
+ axis: parsed.view.showAxis,
591
+ grid: parsed.view.showGrid,
592
+ showCopyright: false,
593
+ showNavigation: false,
594
+ keepAspectRatio: false
595
+ });
596
+ renderGraphObjects(board, parsed);
597
+ board.update();
598
+ const svgEl = container.querySelector("svg");
599
+ if (!svgEl) throw new Error("renderGraph2dSvgFromState: no svg generated");
600
+ return svgEl.outerHTML;
601
+ } finally {
602
+ try {
603
+ if (board) JXG.JSXGraph.freeBoard(board);
604
+ } catch {
605
+ }
606
+ if (container.parentNode) container.parentNode.removeChild(container);
607
+ }
608
+ }
609
+
610
+ // src/stamps/graph-2d/types.ts
611
+ function isGraph2DCustomData(data) {
612
+ if (!data || typeof data !== "object") return false;
613
+ const d = data;
614
+ return d.kind === "graph2d" && d.version === 1 && typeof d.jsonState === "string";
615
+ }
616
+
617
+ export { EMPTY_GRAPH, addIntersection, addPointOnCurve, compile, isGraph2DCustomData, numericalDerivative, parseSerializedGraph, renderGraph2dSvgFromState, stringifySerializedGraph, validate };
618
+ //# sourceMappingURL=chunk-74VEEZBV.mjs.map
619
+ //# sourceMappingURL=chunk-74VEEZBV.mjs.map