@xom11/whiteboard 0.7.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/dist/chunk-74VEEZBV.mjs +619 -0
- package/dist/chunk-74VEEZBV.mjs.map +1 -0
- package/dist/{chunk-BJX4YNA5.mjs → chunk-G7FR3AIV.mjs} +68 -12
- package/dist/chunk-G7FR3AIV.mjs.map +1 -0
- package/dist/{chunk-SHFOGORM.mjs → chunk-PDKKDZ4H.mjs} +4 -4
- package/dist/{chunk-SHFOGORM.mjs.map → chunk-PDKKDZ4H.mjs.map} +1 -1
- package/dist/chunk-PWIMZIB6.mjs +62 -0
- package/dist/chunk-PWIMZIB6.mjs.map +1 -0
- package/dist/{chunk-LPM4MM45.mjs → chunk-SBDMF4NQ.mjs} +3 -2
- package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
- package/dist/chunk-WQOABS6N.mjs +197 -0
- package/dist/chunk-WQOABS6N.mjs.map +1 -0
- package/dist/{chunk-3SSQKRRO.mjs → chunk-ZVN356JZ.mjs} +4 -4
- package/dist/{chunk-3SSQKRRO.mjs.map → chunk-ZVN356JZ.mjs.map} +1 -1
- package/dist/geometry-2d.js +344 -228
- package/dist/geometry-2d.js.map +1 -1
- package/dist/geometry-2d.mjs +2 -2
- package/dist/geometry-3d.d.mts +1 -1
- package/dist/geometry-3d.d.ts +1 -1
- package/dist/geometry-3d.js +3411 -1277
- package/dist/geometry-3d.js.map +1 -1
- package/dist/geometry-3d.mjs +3 -2
- package/dist/graph-2d.js +360 -66
- package/dist/graph-2d.js.map +1 -1
- package/dist/graph-2d.mjs +2 -2
- package/dist/{host-T2W6R6SO.mjs → host-DJETSFCG.mjs} +272 -223
- package/dist/host-DJETSFCG.mjs.map +1 -0
- package/dist/{host-2QGKMGCT.mjs → host-LZH2FZ2N.mjs} +3 -3
- package/dist/{host-2QGKMGCT.mjs.map → host-LZH2FZ2N.mjs.map} +1 -1
- package/dist/host-N6ACNJKI.mjs +3226 -0
- package/dist/host-N6ACNJKI.mjs.map +1 -0
- package/dist/index.d.mts +133 -6
- package/dist/index.d.ts +133 -6
- package/dist/index.js +5634 -1999
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1231 -146
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -6
- package/dist/chunk-BJX4YNA5.mjs.map +0 -1
- package/dist/chunk-DJTBZEAR.mjs +0 -25
- package/dist/chunk-DJTBZEAR.mjs.map +0 -1
- package/dist/chunk-HM7RIXJE.mjs +0 -331
- package/dist/chunk-HM7RIXJE.mjs.map +0 -1
- package/dist/chunk-HYXFHEDJ.mjs +0 -129
- package/dist/chunk-HYXFHEDJ.mjs.map +0 -1
- package/dist/chunk-LPM4MM45.mjs.map +0 -1
- package/dist/host-T2W6R6SO.mjs.map +0 -1
- package/dist/host-XUFON6CQ.mjs +0 -1422
- 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
|
|
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
|