@xom11/whiteboard 0.24.0 → 0.24.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/dist/{ExcalidrawWithMenus-KBLDWPM2.mjs → ExcalidrawWithMenus-WENZRYYE.mjs} +2 -2
- package/dist/{ExcalidrawWithMenus-KBLDWPM2.mjs.map → ExcalidrawWithMenus-WENZRYYE.mjs.map} +1 -1
- package/dist/catalog.json +4 -4
- package/dist/{chunk-TOOHCAWP.mjs → chunk-4D5CSIJO.mjs} +4 -4
- package/dist/{chunk-TOOHCAWP.mjs.map → chunk-4D5CSIJO.mjs.map} +1 -1
- package/dist/chunk-6V4SH4JJ.mjs +1801 -0
- package/dist/chunk-6V4SH4JJ.mjs.map +1 -0
- package/dist/{chunk-VBJLUHCY.mjs → chunk-AZIARTGX.mjs} +3 -3
- package/dist/{chunk-VBJLUHCY.mjs.map → chunk-AZIARTGX.mjs.map} +1 -1
- package/dist/{chunk-6XUPIGVD.mjs → chunk-BKSXPNPQ.mjs} +4 -123
- package/dist/chunk-BKSXPNPQ.mjs.map +1 -0
- package/dist/{chunk-O6QTYAKE.mjs → chunk-CRAPWQKJ.mjs} +4 -4
- package/dist/{chunk-O6QTYAKE.mjs.map → chunk-CRAPWQKJ.mjs.map} +1 -1
- package/dist/{chunk-RBUILBX3.mjs → chunk-CSCF3YFZ.mjs} +5 -5
- package/dist/{chunk-RBUILBX3.mjs.map → chunk-CSCF3YFZ.mjs.map} +1 -1
- package/dist/{chunk-7WG2KDRF.mjs → chunk-IBTRMWD6.mjs} +3 -3
- package/dist/{chunk-7WG2KDRF.mjs.map → chunk-IBTRMWD6.mjs.map} +1 -1
- package/dist/{chunk-RD34F5PM.mjs → chunk-ICR4CVOE.mjs} +2 -2
- package/dist/chunk-ICR4CVOE.mjs.map +1 -0
- package/dist/{chunk-33PEN2WC.mjs → chunk-LVNCYP4J.mjs} +6 -6
- package/dist/{chunk-33PEN2WC.mjs.map → chunk-LVNCYP4J.mjs.map} +1 -1
- package/dist/{chunk-FZY33J6Z.mjs → chunk-MFOGFFIL.mjs} +6 -6
- package/dist/{chunk-FZY33J6Z.mjs.map → chunk-MFOGFFIL.mjs.map} +1 -1
- package/dist/{chunk-TQYQVXNW.mjs → chunk-QGNU34T7.mjs} +2 -2
- package/dist/chunk-QGNU34T7.mjs.map +1 -0
- package/dist/{chunk-RXOFO64U.mjs → chunk-SGFJLHHG.mjs} +3 -3
- package/dist/{chunk-RXOFO64U.mjs.map → chunk-SGFJLHHG.mjs.map} +1 -1
- package/dist/{chunk-2SKXRBGS.mjs → chunk-WWMQ2VHZ.mjs} +4 -4
- package/dist/{chunk-2SKXRBGS.mjs.map → chunk-WWMQ2VHZ.mjs.map} +1 -1
- package/dist/{chunk-XVSO7FBM.mjs → chunk-YIPI3WUL.mjs} +5 -5
- package/dist/{chunk-XVSO7FBM.mjs.map → chunk-YIPI3WUL.mjs.map} +1 -1
- package/dist/{chunk-VRWZILTG.mjs → chunk-ZBJBQKJ2.mjs} +128 -3
- package/dist/chunk-ZBJBQKJ2.mjs.map +1 -0
- package/dist/geometry-2d.js +2056 -100
- package/dist/geometry-2d.js.map +1 -1
- package/dist/geometry-2d.mjs +6 -6
- package/dist/geometry-3d.js +2051 -15
- package/dist/geometry-3d.js.map +1 -1
- package/dist/geometry-3d.mjs +5 -5
- package/dist/graph-2d.js +1938 -8
- package/dist/graph-2d.js.map +1 -1
- package/dist/graph-2d.mjs +8 -8
- package/dist/{host-EVJT3LIF.mjs → host-DOAYVL35.mjs} +29 -28
- package/dist/host-DOAYVL35.mjs.map +1 -0
- package/dist/{host-3N4E4KJH.mjs → host-GKNQBBUE.mjs} +11 -11
- package/dist/{host-3N4E4KJH.mjs.map → host-GKNQBBUE.mjs.map} +1 -1
- package/dist/{host-6SNSZ332.mjs → host-QS2EOTRJ.mjs} +3 -3
- package/dist/{host-6SNSZ332.mjs.map → host-QS2EOTRJ.mjs.map} +1 -1
- package/dist/{host-HN4X3TBC.mjs → host-TLIXN4CF.mjs} +8 -8
- package/dist/{host-HN4X3TBC.mjs.map → host-TLIXN4CF.mjs.map} +1 -1
- package/dist/index.js +2044 -120
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +22 -30
- package/dist/index.mjs.map +1 -1
- package/dist/latex.js.map +1 -1
- package/dist/latex.mjs +1 -1
- package/dist/render-SA4JTOW3.mjs +8 -0
- package/dist/{render-OCVGDKK6.mjs.map → render-SA4JTOW3.mjs.map} +1 -1
- package/dist/serialize-3NZS6A6Q.mjs +6 -0
- package/dist/{serialize-GKN6OVPM.mjs.map → serialize-3NZS6A6Q.mjs.map} +1 -1
- package/package.json +11 -2
- package/dist/chunk-3KBL77M6.mjs +0 -127
- package/dist/chunk-3KBL77M6.mjs.map +0 -1
- package/dist/chunk-6XUPIGVD.mjs.map +0 -1
- package/dist/chunk-RD34F5PM.mjs.map +0 -1
- package/dist/chunk-TQYQVXNW.mjs.map +0 -1
- package/dist/chunk-VRWZILTG.mjs.map +0 -1
- package/dist/host-EVJT3LIF.mjs.map +0 -1
- package/dist/render-OCVGDKK6.mjs +0 -8
- package/dist/serialize-GKN6OVPM.mjs +0 -6
|
@@ -0,0 +1,1801 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { registerKind, compile, validate } from './chunk-ZBJBQKJ2.mjs';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
// src/core/scene/types.ts
|
|
6
|
+
var DEFAULT_VIEW_2D = {
|
|
7
|
+
bbox: [-10, 10, 10, -10],
|
|
8
|
+
showAxis: false,
|
|
9
|
+
showGrid: false
|
|
10
|
+
};
|
|
11
|
+
var DEFAULT_VIEW_3D = {
|
|
12
|
+
bbox3D: [-5, 5, -5, 5, -5, 5],
|
|
13
|
+
azimuth: 60,
|
|
14
|
+
elevation: 30
|
|
15
|
+
};
|
|
16
|
+
var DEFAULT_VIEW_GRAPH2D = {
|
|
17
|
+
xMin: -10,
|
|
18
|
+
xMax: 10,
|
|
19
|
+
yMin: -10,
|
|
20
|
+
yMax: 10,
|
|
21
|
+
showAxis: true,
|
|
22
|
+
showGrid: true
|
|
23
|
+
};
|
|
24
|
+
function createEmptyState(domain) {
|
|
25
|
+
const base = { objects: {}, order: [], counter: 0 };
|
|
26
|
+
switch (domain) {
|
|
27
|
+
case "2d":
|
|
28
|
+
return { ...base, meta: { domain: "2d", version: 1, view: DEFAULT_VIEW_2D } };
|
|
29
|
+
case "3d":
|
|
30
|
+
return { ...base, meta: { domain: "3d", version: 1, view: DEFAULT_VIEW_3D } };
|
|
31
|
+
case "graph2d":
|
|
32
|
+
return { ...base, meta: { domain: "graph2d", version: 1, view: DEFAULT_VIEW_GRAPH2D } };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/core/scene/selectors.ts
|
|
37
|
+
function listObjects(state) {
|
|
38
|
+
return state.order.map((id) => state.objects[id]).filter((o) => o !== void 0);
|
|
39
|
+
}
|
|
40
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
41
|
+
var LABEL_GROUPS = {
|
|
42
|
+
point: ["point", "intersection"],
|
|
43
|
+
intersection: ["point", "intersection"]
|
|
44
|
+
};
|
|
45
|
+
function nextLabel(state, kind) {
|
|
46
|
+
const kinds = LABEL_GROUPS[kind] ?? [kind];
|
|
47
|
+
const used = new Set(
|
|
48
|
+
listObjects(state).filter((o) => kinds.includes(o.kind)).map((o) => o.label)
|
|
49
|
+
);
|
|
50
|
+
for (const c of ALPHABET) if (!used.has(c)) return c;
|
|
51
|
+
let idx = 1;
|
|
52
|
+
while (true) {
|
|
53
|
+
for (const c of ALPHABET) {
|
|
54
|
+
const candidate = `${c}${idx}`;
|
|
55
|
+
if (!used.has(candidate)) return candidate;
|
|
56
|
+
}
|
|
57
|
+
idx += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function useEditorState(opts) {
|
|
61
|
+
const { store, initialState, onHistoryChange, bindKeyboardShortcuts = true } = opts;
|
|
62
|
+
const onHistoryChangeRef = React.useRef(onHistoryChange);
|
|
63
|
+
onHistoryChangeRef.current = onHistoryChange;
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (initialState?.state) {
|
|
66
|
+
const loaded = initialState.state;
|
|
67
|
+
store.withoutHistory(() => {
|
|
68
|
+
store.dispatch({ type: "LOAD", payload: { state: loaded } });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}, []);
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
onHistoryChangeRef.current?.(store.canUndo(), store.canRedo());
|
|
74
|
+
const unsub = store.subscribe(() => {
|
|
75
|
+
onHistoryChangeRef.current?.(store.canUndo(), store.canRedo());
|
|
76
|
+
});
|
|
77
|
+
return unsub;
|
|
78
|
+
}, [store]);
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
if (!bindKeyboardShortcuts) return;
|
|
81
|
+
const onKey = (e) => {
|
|
82
|
+
const ae = document.activeElement;
|
|
83
|
+
const inField = !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
|
|
84
|
+
if (inField) return;
|
|
85
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
86
|
+
const key = e.key.toLowerCase();
|
|
87
|
+
if (key === "z" && !e.shiftKey) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
e.stopPropagation();
|
|
90
|
+
store.undo();
|
|
91
|
+
} else if (key === "z" && e.shiftKey || key === "y" && !e.shiftKey) {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
store.redo();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
window.addEventListener("keydown", onKey, { capture: true });
|
|
98
|
+
return () => window.removeEventListener("keydown", onKey, { capture: true });
|
|
99
|
+
}, [store, bindKeyboardShortcuts]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/core/scene/kinds/3d-constraint.ts
|
|
103
|
+
function constraintRefs(c) {
|
|
104
|
+
switch (c.kind) {
|
|
105
|
+
case "onPlane":
|
|
106
|
+
return [c.planeId];
|
|
107
|
+
case "onLine":
|
|
108
|
+
return [c.lineId];
|
|
109
|
+
case "onPolygon":
|
|
110
|
+
return [c.polygonId];
|
|
111
|
+
case "onSphere":
|
|
112
|
+
return [c.sphereId];
|
|
113
|
+
default:
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/core/scene/kinds/point3d.ts
|
|
119
|
+
var def = {
|
|
120
|
+
type: "point3d",
|
|
121
|
+
schemaVersion: 1,
|
|
122
|
+
migrate: {},
|
|
123
|
+
validate: (a) => {
|
|
124
|
+
if (!a || !a.constraint || !a.constraint.kind) {
|
|
125
|
+
throw new Error("point3d: constraint required");
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
dependsOn: (a) => constraintRefs(a.constraint),
|
|
129
|
+
measure: (obj) => {
|
|
130
|
+
const c = obj.attrs.constraint;
|
|
131
|
+
if (c.kind === "free") {
|
|
132
|
+
return [
|
|
133
|
+
{ label: "x", value: c.x },
|
|
134
|
+
{ label: "y", value: c.y },
|
|
135
|
+
{ label: "z", value: c.z }
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
if (c.kind === "onGround") {
|
|
139
|
+
return [
|
|
140
|
+
{ label: "x", value: c.x },
|
|
141
|
+
{ label: "y", value: c.y },
|
|
142
|
+
{ label: "z", value: 0 }
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
},
|
|
147
|
+
describe: (obj) => {
|
|
148
|
+
const c = obj.attrs.constraint;
|
|
149
|
+
if (c.kind === "free") return `${obj.label} = (${c.x.toFixed(2)}, ${c.y.toFixed(2)}, ${c.z.toFixed(2)})`;
|
|
150
|
+
if (c.kind === "onGround") return `${obj.label} = (${c.x.toFixed(2)}, ${c.y.toFixed(2)}, 0)`;
|
|
151
|
+
if (c.kind === "onAxis") return `${obj.label} tr\xEAn tr\u1EE5c ${c.axis} (t=${c.t.toFixed(2)})`;
|
|
152
|
+
if (c.kind === "onPlane") return `${obj.label} tr\xEAn m\u1EB7t ${c.planeId}`;
|
|
153
|
+
if (c.kind === "onLine") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng ${c.lineId}`;
|
|
154
|
+
if (c.kind === "onPolygon") return `${obj.label} tr\xEAn \u0111a gi\xE1c ${c.polygonId}`;
|
|
155
|
+
if (c.kind === "onSphere") return `${obj.label} tr\xEAn m\u1EB7t c\u1EA7u ${c.sphereId}`;
|
|
156
|
+
return obj.label;
|
|
157
|
+
},
|
|
158
|
+
render: (obj, ctx) => {
|
|
159
|
+
const view = ctx.jxg;
|
|
160
|
+
const c = obj.attrs.constraint;
|
|
161
|
+
const opts = {
|
|
162
|
+
name: obj.label,
|
|
163
|
+
visible: obj.visible,
|
|
164
|
+
fixed: obj.locked,
|
|
165
|
+
strokeColor: obj.attrs.color ?? "#1e40af",
|
|
166
|
+
fillColor: obj.attrs.color ?? "#1e40af",
|
|
167
|
+
size: 4
|
|
168
|
+
};
|
|
169
|
+
if (c.kind === "free") {
|
|
170
|
+
return view.create("point3d", [c.x, c.y, c.z], opts);
|
|
171
|
+
} else if (c.kind === "onGround") {
|
|
172
|
+
return view.create("point3d", [c.x, c.y, 0], opts);
|
|
173
|
+
} else if (c.kind === "onAxis") {
|
|
174
|
+
const coords = c.axis === "x" ? [c.t, 0, 0] : c.axis === "y" ? [0, c.t, 0] : [0, 0, c.t];
|
|
175
|
+
return view.create("point3d", coords, opts);
|
|
176
|
+
} else if (c.kind === "onPlane") {
|
|
177
|
+
const plane = ctx.resolveRef(c.planeId);
|
|
178
|
+
return view.create("point3d", [
|
|
179
|
+
() => plane.F(c.u, c.v)[0],
|
|
180
|
+
() => plane.F(c.u, c.v)[1],
|
|
181
|
+
() => plane.F(c.u, c.v)[2]
|
|
182
|
+
], opts);
|
|
183
|
+
} else if (c.kind === "onLine") {
|
|
184
|
+
const line = ctx.resolveRef(c.lineId);
|
|
185
|
+
return view.create("point3d", [
|
|
186
|
+
() => line.F(c.t)[0],
|
|
187
|
+
() => line.F(c.t)[1],
|
|
188
|
+
() => line.F(c.t)[2]
|
|
189
|
+
], opts);
|
|
190
|
+
} else if (c.kind === "onPolygon") {
|
|
191
|
+
const poly = ctx.resolveRef(c.polygonId);
|
|
192
|
+
return view.create("point3d", [
|
|
193
|
+
() => poly.F(c.u, c.v)[0],
|
|
194
|
+
() => poly.F(c.u, c.v)[1],
|
|
195
|
+
() => poly.F(c.u, c.v)[2]
|
|
196
|
+
], opts);
|
|
197
|
+
} else if (c.kind === "onSphere") {
|
|
198
|
+
const sph = ctx.resolveRef(c.sphereId);
|
|
199
|
+
return view.create("point3d", [
|
|
200
|
+
() => sph.F(c.theta, c.phi)[0],
|
|
201
|
+
() => sph.F(c.theta, c.phi)[1],
|
|
202
|
+
() => sph.F(c.theta, c.phi)[2]
|
|
203
|
+
], opts);
|
|
204
|
+
}
|
|
205
|
+
return view.create("point3d", [0, 0, 0], opts);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
registerKind(def);
|
|
209
|
+
|
|
210
|
+
// src/core/scene/kinds/labelOf.ts
|
|
211
|
+
function labelOf(id, state) {
|
|
212
|
+
return state?.objects[id]?.label ?? id;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/core/scene/kinds/segment3d.ts
|
|
216
|
+
var def2 = {
|
|
217
|
+
type: "segment3d",
|
|
218
|
+
schemaVersion: 1,
|
|
219
|
+
migrate: {},
|
|
220
|
+
validate: (a) => {
|
|
221
|
+
if (!a?.p1 || !a?.p2) throw new Error("segment3d: p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
|
|
222
|
+
},
|
|
223
|
+
dependsOn: (a) => [a.p1, a.p2],
|
|
224
|
+
measure: (obj, state) => {
|
|
225
|
+
const p1 = state.objects[obj.attrs.p1];
|
|
226
|
+
const p2 = state.objects[obj.attrs.p2];
|
|
227
|
+
if (!p1 || !p2) return null;
|
|
228
|
+
const c1 = p1.attrs.constraint;
|
|
229
|
+
const c2 = p2.attrs.constraint;
|
|
230
|
+
if (c1?.kind !== "free" || c2?.kind !== "free") return null;
|
|
231
|
+
const dx = (c2.x ?? 0) - (c1.x ?? 0);
|
|
232
|
+
const dy = (c2.y ?? 0) - (c1.y ?? 0);
|
|
233
|
+
const dz = (c2.z ?? 0) - (c1.z ?? 0);
|
|
234
|
+
return [{ label: "length", value: Math.hypot(dx, dy, dz) }];
|
|
235
|
+
},
|
|
236
|
+
describe: (obj, state) => `\u0110o\u1EA1n th\u1EB3ng ${labelOf(obj.attrs.p1, state)}${labelOf(obj.attrs.p2, state)}`,
|
|
237
|
+
render: (obj, ctx) => {
|
|
238
|
+
const view = ctx.jxg;
|
|
239
|
+
const pA = ctx.resolveRef(obj.attrs.p1);
|
|
240
|
+
const pB = ctx.resolveRef(obj.attrs.p2);
|
|
241
|
+
return view.create("line3d", [pA, pB], {
|
|
242
|
+
straightFirst: false,
|
|
243
|
+
straightLast: false,
|
|
244
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
245
|
+
strokeWidth: 2,
|
|
246
|
+
visible: obj.visible
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
registerKind(def2);
|
|
251
|
+
|
|
252
|
+
// src/core/scene/kinds/line3d.ts
|
|
253
|
+
registerKind({
|
|
254
|
+
type: "line3d",
|
|
255
|
+
schemaVersion: 1,
|
|
256
|
+
migrate: {},
|
|
257
|
+
validate: (a) => {
|
|
258
|
+
if (!a?.p1 || !a?.p2) throw new Error("line3d: p1/p2 required");
|
|
259
|
+
},
|
|
260
|
+
dependsOn: (a) => [a.p1, a.p2],
|
|
261
|
+
describe: (obj, state) => `\u0110\u01B0\u1EDDng ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}`,
|
|
262
|
+
render: (obj, ctx) => {
|
|
263
|
+
const view = ctx.jxg;
|
|
264
|
+
const pA = ctx.resolveRef(obj.attrs.p1);
|
|
265
|
+
const pB = ctx.resolveRef(obj.attrs.p2);
|
|
266
|
+
return view.create("line3d", [pA, pB], {
|
|
267
|
+
straightFirst: true,
|
|
268
|
+
straightLast: true,
|
|
269
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
270
|
+
strokeWidth: 2,
|
|
271
|
+
visible: obj.visible
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// src/core/scene/kinds/ray3d.ts
|
|
277
|
+
registerKind({
|
|
278
|
+
type: "ray3d",
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
migrate: {},
|
|
281
|
+
validate: (a) => {
|
|
282
|
+
if (!a?.origin || !a?.through) throw new Error("ray3d: origin/through required");
|
|
283
|
+
},
|
|
284
|
+
dependsOn: (a) => [a.origin, a.through],
|
|
285
|
+
describe: (obj, state) => `Tia ${obj.label} t\u1EEB ${labelOf(obj.attrs.origin, state)} qua ${labelOf(obj.attrs.through, state)}`,
|
|
286
|
+
render: (obj, ctx) => {
|
|
287
|
+
const view = ctx.jxg;
|
|
288
|
+
const pOrigin = ctx.resolveRef(obj.attrs.origin);
|
|
289
|
+
const pThrough = ctx.resolveRef(obj.attrs.through);
|
|
290
|
+
return view.create("line3d", [pOrigin, pThrough], {
|
|
291
|
+
straightFirst: false,
|
|
292
|
+
straightLast: true,
|
|
293
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
294
|
+
strokeWidth: 2,
|
|
295
|
+
visible: obj.visible
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// src/core/scene/kinds/vector3d.ts
|
|
301
|
+
registerKind({
|
|
302
|
+
type: "vector3d",
|
|
303
|
+
schemaVersion: 1,
|
|
304
|
+
migrate: {},
|
|
305
|
+
validate: (a) => {
|
|
306
|
+
if (!a?.from || !a?.to) throw new Error("vector3d: from/to required");
|
|
307
|
+
},
|
|
308
|
+
dependsOn: (a) => [a.from, a.to],
|
|
309
|
+
describe: (obj, state) => `V\xE9c-t\u01A1 ${obj.label}: ${labelOf(obj.attrs.from, state)} \u2192 ${labelOf(obj.attrs.to, state)}`,
|
|
310
|
+
render: (obj, ctx) => {
|
|
311
|
+
const view = ctx.jxg;
|
|
312
|
+
const pFrom = ctx.resolveRef(obj.attrs.from);
|
|
313
|
+
const pTo = ctx.resolveRef(obj.attrs.to);
|
|
314
|
+
return view.create("line3d", [pFrom, pTo], {
|
|
315
|
+
straightFirst: false,
|
|
316
|
+
straightLast: false,
|
|
317
|
+
lastArrow: { type: 1 },
|
|
318
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
319
|
+
strokeWidth: 2,
|
|
320
|
+
visible: obj.visible
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// src/core/scene/kinds/plane3d.ts
|
|
326
|
+
registerKind({
|
|
327
|
+
type: "plane3d",
|
|
328
|
+
schemaVersion: 1,
|
|
329
|
+
migrate: {},
|
|
330
|
+
validate: (a) => {
|
|
331
|
+
if (!a?.p1 || !a?.p2 || !a?.p3) throw new Error("plane3d: c\u1EA7n 3 \u0111i\u1EC3m");
|
|
332
|
+
},
|
|
333
|
+
dependsOn: (a) => [a.p1, a.p2, a.p3],
|
|
334
|
+
describe: (obj, state) => `M\u1EB7t ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}, ${labelOf(obj.attrs.p3, state)}`,
|
|
335
|
+
render: (obj, ctx) => {
|
|
336
|
+
const view = ctx.jxg;
|
|
337
|
+
return view.create("plane3d", [
|
|
338
|
+
ctx.resolveRef(obj.attrs.p1),
|
|
339
|
+
ctx.resolveRef(obj.attrs.p2),
|
|
340
|
+
ctx.resolveRef(obj.attrs.p3)
|
|
341
|
+
], {
|
|
342
|
+
fillOpacity: 0.15,
|
|
343
|
+
fillColor: obj.attrs.color ?? "#60a5fa",
|
|
344
|
+
strokeColor: obj.attrs.color ?? "#60a5fa",
|
|
345
|
+
visible: obj.visible
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// src/core/scene/kinds/polygon3d.ts
|
|
351
|
+
registerKind({
|
|
352
|
+
type: "polygon3d",
|
|
353
|
+
schemaVersion: 1,
|
|
354
|
+
migrate: {},
|
|
355
|
+
validate: (a) => {
|
|
356
|
+
if (!a?.vertices || a.vertices.length < 3) throw new Error("polygon3d: c\u1EA7n \u22653 vertices");
|
|
357
|
+
},
|
|
358
|
+
dependsOn: (a) => [...a.vertices],
|
|
359
|
+
describe: (obj) => `\u0110a gi\xE1c ${obj.label} (${obj.attrs.vertices.length} \u0111\u1EC9nh)`,
|
|
360
|
+
render: (obj, ctx) => {
|
|
361
|
+
const view = ctx.jxg;
|
|
362
|
+
const refs = obj.attrs.vertices.map((id) => ctx.resolveRef(id));
|
|
363
|
+
return view.create("polygon3d", [refs], {
|
|
364
|
+
fillOpacity: 0.3,
|
|
365
|
+
fillColor: obj.attrs.color ?? "#60a5fa",
|
|
366
|
+
visible: obj.visible
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// src/core/scene/kinds/sphere3d.ts
|
|
372
|
+
registerKind({
|
|
373
|
+
type: "sphere3d",
|
|
374
|
+
schemaVersion: 1,
|
|
375
|
+
migrate: {},
|
|
376
|
+
validate: (a) => {
|
|
377
|
+
if (!a?.center || !a?.surfacePoint) throw new Error("sphere3d: center/surfacePoint required");
|
|
378
|
+
},
|
|
379
|
+
dependsOn: (a) => [a.center, a.surfacePoint],
|
|
380
|
+
describe: (obj, state) => `M\u1EB7t c\u1EA7u ${obj.label} t\xE2m ${labelOf(obj.attrs.center, state)}`,
|
|
381
|
+
render: (obj, ctx) => {
|
|
382
|
+
const view = ctx.jxg;
|
|
383
|
+
return view.create("sphere3d", [
|
|
384
|
+
ctx.resolveRef(obj.attrs.center),
|
|
385
|
+
ctx.resolveRef(obj.attrs.surfacePoint)
|
|
386
|
+
], {
|
|
387
|
+
fillOpacity: 0.25,
|
|
388
|
+
fillColor: obj.attrs.color ?? "#60a5fa",
|
|
389
|
+
visible: obj.visible
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// src/core/scene/kinds/polyhedron3d.ts
|
|
395
|
+
var FLAVOR_LABEL = {
|
|
396
|
+
pyramid: "ch\xF3p",
|
|
397
|
+
prism: "l\u0103ng tr\u1EE5",
|
|
398
|
+
tetrahedron: "t\u1EE9 di\u1EC7n",
|
|
399
|
+
cube: "l\u1EADp ph\u01B0\u01A1ng"
|
|
400
|
+
};
|
|
401
|
+
registerKind({
|
|
402
|
+
type: "polyhedron3d",
|
|
403
|
+
schemaVersion: 1,
|
|
404
|
+
migrate: {},
|
|
405
|
+
validate: (a) => {
|
|
406
|
+
if (!a?.vertices || a.vertices.length < 4) throw new Error("polyhedron3d: c\u1EA7n \u22654 vertices");
|
|
407
|
+
if (!a?.faces || a.faces.length < 4) throw new Error("polyhedron3d: c\u1EA7n \u22654 faces");
|
|
408
|
+
},
|
|
409
|
+
dependsOn: (a) => [...a.vertices],
|
|
410
|
+
describe: (obj) => `Kh\u1ED1i ${FLAVOR_LABEL[obj.attrs.flavor]} ${obj.label}`,
|
|
411
|
+
render: (obj, ctx) => {
|
|
412
|
+
const view = ctx.jxg;
|
|
413
|
+
const verts = obj.attrs.vertices.map((id) => ctx.resolveRef(id));
|
|
414
|
+
const faces = obj.attrs.faces.map(
|
|
415
|
+
(faceIndices, fi) => view.create("polygon3d", [faceIndices.map((i) => verts[i])], {
|
|
416
|
+
id: `${obj.id}.face${faceIndices.join("-")}.${fi}`,
|
|
417
|
+
fillOpacity: 0.25,
|
|
418
|
+
fillColor: obj.attrs.color ?? "#fbbf24",
|
|
419
|
+
strokeColor: "#0066cc",
|
|
420
|
+
strokeWidth: 1.5,
|
|
421
|
+
visible: obj.visible
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
return { faces };
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// src/core/scene/kinds/cylinder3d.ts
|
|
429
|
+
var CURVED_SEGMENTS = 16;
|
|
430
|
+
registerKind({
|
|
431
|
+
type: "cylinder3d",
|
|
432
|
+
schemaVersion: 1,
|
|
433
|
+
migrate: {},
|
|
434
|
+
validate: (a) => {
|
|
435
|
+
if (!a?.baseCenter || !a?.topCenter) throw new Error("cylinder3d: baseCenter/topCenter required");
|
|
436
|
+
if (!(a.radius > 0)) throw new Error("cylinder3d: radius > 0");
|
|
437
|
+
},
|
|
438
|
+
dependsOn: (a) => [a.baseCenter, a.topCenter],
|
|
439
|
+
describe: (obj) => `Tr\u1EE5 ${obj.label} R=${obj.attrs.radius.toFixed(2)}`,
|
|
440
|
+
render: (obj, ctx) => {
|
|
441
|
+
const view = ctx.jxg;
|
|
442
|
+
const a = ctx.resolveRef(obj.attrs.baseCenter);
|
|
443
|
+
const b = ctx.resolveRef(obj.attrs.topCenter);
|
|
444
|
+
const r = obj.attrs.radius;
|
|
445
|
+
const ax = a.X?.() ?? 0, ay = a.Y?.() ?? 0, az = a.Z?.() ?? 0;
|
|
446
|
+
const bx = b.X?.() ?? 0, by = b.Y?.() ?? 0, bz = b.Z?.() ?? 0;
|
|
447
|
+
const baseRing = [];
|
|
448
|
+
const topRing = [];
|
|
449
|
+
for (let i = 0; i < CURVED_SEGMENTS; i++) {
|
|
450
|
+
const theta = i / CURVED_SEGMENTS * Math.PI * 2;
|
|
451
|
+
const dx = r * Math.cos(theta);
|
|
452
|
+
const dy = r * Math.sin(theta);
|
|
453
|
+
baseRing.push([ax + dx, ay + dy, az]);
|
|
454
|
+
topRing.push([bx + dx, by + dy, bz]);
|
|
455
|
+
}
|
|
456
|
+
const vertices = [...baseRing, ...topRing];
|
|
457
|
+
const faces = [];
|
|
458
|
+
faces.push(baseRing.map((_, i) => i));
|
|
459
|
+
faces.push(topRing.map((_, i) => CURVED_SEGMENTS + i));
|
|
460
|
+
for (let i = 0; i < CURVED_SEGMENTS; i++) {
|
|
461
|
+
const next = (i + 1) % CURVED_SEGMENTS;
|
|
462
|
+
faces.push([i, next, CURVED_SEGMENTS + next, CURVED_SEGMENTS + i]);
|
|
463
|
+
}
|
|
464
|
+
const vertJxgs = vertices.map(
|
|
465
|
+
(v, i) => view.create("point3d", v, {
|
|
466
|
+
id: `${obj.id}.v${i}`,
|
|
467
|
+
visible: false,
|
|
468
|
+
fixed: true,
|
|
469
|
+
withLabel: false
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
const faceJxgs = faces.map(
|
|
473
|
+
(face, fi) => view.create("polygon3d", [face.map((idx) => vertJxgs[idx])], {
|
|
474
|
+
id: `${obj.id}.face${fi}`,
|
|
475
|
+
fillOpacity: 0.25,
|
|
476
|
+
fillColor: obj.attrs.color ?? "#f97316",
|
|
477
|
+
strokeColor: "#0066cc",
|
|
478
|
+
strokeWidth: 1.5,
|
|
479
|
+
visible: obj.visible
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
return { _verts: vertJxgs, faces: faceJxgs };
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// src/core/scene/kinds/cone3d.ts
|
|
487
|
+
var CURVED_SEGMENTS2 = 16;
|
|
488
|
+
registerKind({
|
|
489
|
+
type: "cone3d",
|
|
490
|
+
schemaVersion: 1,
|
|
491
|
+
migrate: {},
|
|
492
|
+
validate: (a) => {
|
|
493
|
+
if (!a?.baseCenter || !a?.apex) throw new Error("cone3d: baseCenter/apex required");
|
|
494
|
+
if (!(a.radius > 0)) throw new Error("cone3d: radius > 0");
|
|
495
|
+
},
|
|
496
|
+
dependsOn: (a) => [a.baseCenter, a.apex],
|
|
497
|
+
describe: (obj) => `N\xF3n ${obj.label} R=${obj.attrs.radius.toFixed(2)}`,
|
|
498
|
+
render: (obj, ctx) => {
|
|
499
|
+
const view = ctx.jxg;
|
|
500
|
+
const base = ctx.resolveRef(obj.attrs.baseCenter);
|
|
501
|
+
const apexPt = ctx.resolveRef(obj.attrs.apex);
|
|
502
|
+
const r = obj.attrs.radius;
|
|
503
|
+
const bx = base.X?.() ?? 0, by = base.Y?.() ?? 0, bz = base.Z?.() ?? 0;
|
|
504
|
+
const apexCoords = [
|
|
505
|
+
apexPt.X?.() ?? 0,
|
|
506
|
+
apexPt.Y?.() ?? 0,
|
|
507
|
+
apexPt.Z?.() ?? 0
|
|
508
|
+
];
|
|
509
|
+
const baseRing = [];
|
|
510
|
+
for (let i = 0; i < CURVED_SEGMENTS2; i++) {
|
|
511
|
+
const theta = i / CURVED_SEGMENTS2 * Math.PI * 2;
|
|
512
|
+
baseRing.push([bx + r * Math.cos(theta), by + r * Math.sin(theta), bz]);
|
|
513
|
+
}
|
|
514
|
+
const apexIdx = baseRing.length;
|
|
515
|
+
const vertices = [...baseRing, apexCoords];
|
|
516
|
+
const faces = [baseRing.map((_, i) => i)];
|
|
517
|
+
for (let i = 0; i < CURVED_SEGMENTS2; i++) {
|
|
518
|
+
faces.push([i, (i + 1) % CURVED_SEGMENTS2, apexIdx]);
|
|
519
|
+
}
|
|
520
|
+
const vertJxgs = vertices.map(
|
|
521
|
+
(v, i) => view.create("point3d", v, {
|
|
522
|
+
id: `${obj.id}.v${i}`,
|
|
523
|
+
visible: false,
|
|
524
|
+
fixed: true,
|
|
525
|
+
withLabel: false
|
|
526
|
+
})
|
|
527
|
+
);
|
|
528
|
+
const faceJxgs = faces.map(
|
|
529
|
+
(face, fi) => view.create("polygon3d", [face.map((idx) => vertJxgs[idx])], {
|
|
530
|
+
id: `${obj.id}.face${fi}`,
|
|
531
|
+
fillOpacity: 0.25,
|
|
532
|
+
fillColor: obj.attrs.color ?? "#f59e0b",
|
|
533
|
+
strokeColor: "#0066cc",
|
|
534
|
+
strokeWidth: 1.5,
|
|
535
|
+
visible: obj.visible
|
|
536
|
+
})
|
|
537
|
+
);
|
|
538
|
+
return { _verts: vertJxgs, faces: faceJxgs };
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// src/core/scene/kinds/2d-constraint.ts
|
|
543
|
+
function transformRefs(t) {
|
|
544
|
+
switch (t.kind) {
|
|
545
|
+
case "translate":
|
|
546
|
+
return [];
|
|
547
|
+
case "rotate":
|
|
548
|
+
case "reflectPoint":
|
|
549
|
+
case "dilate":
|
|
550
|
+
return [t.center];
|
|
551
|
+
case "reflectLine":
|
|
552
|
+
return [t.line];
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function constraintRefs2D(c) {
|
|
556
|
+
switch (c.kind) {
|
|
557
|
+
case "onLine":
|
|
558
|
+
return [c.lineId];
|
|
559
|
+
case "onSegment":
|
|
560
|
+
return [c.segmentId];
|
|
561
|
+
case "onCircle":
|
|
562
|
+
return [c.circleId];
|
|
563
|
+
case "onPolygon":
|
|
564
|
+
return [c.polygonId];
|
|
565
|
+
case "midpoint":
|
|
566
|
+
return [c.p1, c.p2];
|
|
567
|
+
case "transformed":
|
|
568
|
+
return [c.source, ...transformRefs(c.transform)];
|
|
569
|
+
case "perpFoot":
|
|
570
|
+
return [c.from, c.onLine];
|
|
571
|
+
case "circumcenter":
|
|
572
|
+
return [c.vertices[0], c.vertices[1], c.vertices[2]];
|
|
573
|
+
case "incenter":
|
|
574
|
+
return [c.vertices[0], c.vertices[1], c.vertices[2]];
|
|
575
|
+
case "centroid":
|
|
576
|
+
return [c.vertices[0], c.vertices[1], c.vertices[2]];
|
|
577
|
+
case "orthocenter":
|
|
578
|
+
return [c.vertices[0], c.vertices[1], c.vertices[2]];
|
|
579
|
+
default:
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/core/scene/kinds/point.ts
|
|
585
|
+
function buildJxgTransforms(board, ctx, t) {
|
|
586
|
+
switch (t.kind) {
|
|
587
|
+
case "translate":
|
|
588
|
+
return [board.create("transform", [t.dx, t.dy], { type: "translate" })];
|
|
589
|
+
case "rotate": {
|
|
590
|
+
const c = ctx.resolveRef(t.center);
|
|
591
|
+
return [board.create("transform", [t.angleRad, c], { type: "rotate" })];
|
|
592
|
+
}
|
|
593
|
+
case "reflectPoint": {
|
|
594
|
+
const c = ctx.resolveRef(t.center);
|
|
595
|
+
return [board.create("transform", [Math.PI, c], { type: "rotate" })];
|
|
596
|
+
}
|
|
597
|
+
case "reflectLine": {
|
|
598
|
+
const l = ctx.resolveRef(t.line);
|
|
599
|
+
return [board.create("transform", [l], { type: "reflect" })];
|
|
600
|
+
}
|
|
601
|
+
case "dilate": {
|
|
602
|
+
const c = ctx.resolveRef(t.center);
|
|
603
|
+
return [
|
|
604
|
+
board.create("transform", [() => -c.X(), () => -c.Y()], { type: "translate" }),
|
|
605
|
+
board.create("transform", [t.k, t.k], { type: "scale" }),
|
|
606
|
+
board.create("transform", [() => c.X(), () => c.Y()], { type: "translate" })
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
var def3 = {
|
|
612
|
+
type: "point",
|
|
613
|
+
schemaVersion: 1,
|
|
614
|
+
migrate: {},
|
|
615
|
+
validate: (a) => {
|
|
616
|
+
if (!a || !a.constraint || !a.constraint.kind) {
|
|
617
|
+
throw new Error("point: constraint required");
|
|
618
|
+
}
|
|
619
|
+
const c = a.constraint;
|
|
620
|
+
if (c.kind === "perpFoot") {
|
|
621
|
+
if (!c.from || !c.onLine) {
|
|
622
|
+
throw new Error("point.perpFoot: from v\xE0 onLine b\u1EAFt bu\u1ED9c");
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (c.kind === "circumcenter") {
|
|
626
|
+
if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
|
|
627
|
+
throw new Error("point.circumcenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
|
|
628
|
+
}
|
|
629
|
+
if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
|
|
630
|
+
throw new Error("point.circumcenter: 3 vertex id ph\u1EA3i non-empty");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (c.kind === "incenter") {
|
|
634
|
+
if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
|
|
635
|
+
throw new Error("point.incenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
|
|
636
|
+
}
|
|
637
|
+
if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
|
|
638
|
+
throw new Error("point.incenter: 3 vertex id ph\u1EA3i non-empty");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (c.kind === "centroid") {
|
|
642
|
+
if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
|
|
643
|
+
throw new Error("point.centroid: vertices ph\u1EA3i l\xE0 tuple 3 id");
|
|
644
|
+
}
|
|
645
|
+
if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
|
|
646
|
+
throw new Error("point.centroid: 3 vertex id ph\u1EA3i non-empty");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (c.kind === "orthocenter") {
|
|
650
|
+
if (!Array.isArray(c.vertices) || c.vertices.length !== 3) {
|
|
651
|
+
throw new Error("point.orthocenter: vertices ph\u1EA3i l\xE0 tuple 3 id");
|
|
652
|
+
}
|
|
653
|
+
if (!c.vertices[0] || !c.vertices[1] || !c.vertices[2]) {
|
|
654
|
+
throw new Error("point.orthocenter: 3 vertex id ph\u1EA3i non-empty");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
dependsOn: (a) => constraintRefs2D(a.constraint),
|
|
659
|
+
measure: (obj) => {
|
|
660
|
+
const c = obj.attrs.constraint;
|
|
661
|
+
if (c.kind === "free") {
|
|
662
|
+
return [
|
|
663
|
+
{ label: "x", value: c.x },
|
|
664
|
+
{ label: "y", value: c.y }
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
},
|
|
669
|
+
describe: (obj, state) => {
|
|
670
|
+
const c = obj.attrs.constraint;
|
|
671
|
+
if (c.kind === "free") return `\u0110i\u1EC3m ${obj.label}`;
|
|
672
|
+
if (c.kind === "onAxis") return `${obj.label} tr\xEAn tr\u1EE5c ${c.axis}`;
|
|
673
|
+
if (c.kind === "onLine") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng ${state?.objects[c.lineId]?.label ?? c.lineId}`;
|
|
674
|
+
if (c.kind === "onSegment") return `${obj.label} tr\xEAn \u0111o\u1EA1n ${state?.objects[c.segmentId]?.label ?? c.segmentId}`;
|
|
675
|
+
if (c.kind === "onCircle") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng tr\xF2n ${state?.objects[c.circleId]?.label ?? c.circleId}`;
|
|
676
|
+
if (c.kind === "onPolygon") return `${obj.label} tr\xEAn \u0111a gi\xE1c ${state?.objects[c.polygonId]?.label ?? c.polygonId}`;
|
|
677
|
+
if (c.kind === "midpoint") {
|
|
678
|
+
const l1 = state?.objects[c.p1]?.label ?? c.p1;
|
|
679
|
+
const l2 = state?.objects[c.p2]?.label ?? c.p2;
|
|
680
|
+
return `${obj.label} = trung \u0111i\u1EC3m ${l1}${l2}`;
|
|
681
|
+
}
|
|
682
|
+
if (c.kind === "transformed") {
|
|
683
|
+
const t = c.transform;
|
|
684
|
+
const labelRef = (id) => state?.objects[id]?.label ?? id;
|
|
685
|
+
const op = t.kind === "translate" ? `t\u1ECBnh ti\u1EBFn (${t.dx.toFixed(2)}, ${t.dy.toFixed(2)})` : t.kind === "rotate" ? `quay ${(t.angleRad * 180 / Math.PI).toFixed(0)}\xB0 quanh ${labelRef(t.center)}` : t.kind === "reflectLine" ? `\u0111\u1ED1i x\u1EE9ng qua ${labelRef(t.line)}` : t.kind === "reflectPoint" ? `\u0111\u1ED1i x\u1EE9ng qua \u0111i\u1EC3m ${labelRef(t.center)}` : t.kind === "dilate" ? `v\u1ECB t\u1EF1 k=${t.k} quanh ${labelRef(t.center)}` : "";
|
|
686
|
+
return `${obj.label} = \u1EA3nh c\u1EE7a ${labelRef(c.source)} (${op})`;
|
|
687
|
+
}
|
|
688
|
+
if (c.kind === "perpFoot") {
|
|
689
|
+
const fromLabel = state?.objects[c.from]?.label ?? c.from;
|
|
690
|
+
const lineLabel = state?.objects[c.onLine]?.label ?? c.onLine;
|
|
691
|
+
return `${obj.label} = ch\xE2n \u27C2 t\u1EEB ${fromLabel} xu\u1ED1ng ${lineLabel}`;
|
|
692
|
+
}
|
|
693
|
+
if (c.kind === "circumcenter") {
|
|
694
|
+
const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
|
|
695
|
+
return `${obj.label} = t\xE2m ngo\u1EA1i ti\u1EBFp \u0394${labels}`;
|
|
696
|
+
}
|
|
697
|
+
if (c.kind === "incenter") {
|
|
698
|
+
const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
|
|
699
|
+
return `${obj.label} = t\xE2m n\u1ED9i ti\u1EBFp \u0394${labels}`;
|
|
700
|
+
}
|
|
701
|
+
if (c.kind === "centroid") {
|
|
702
|
+
const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
|
|
703
|
+
return `${obj.label} = tr\u1ECDng t\xE2m \u0394${labels}`;
|
|
704
|
+
}
|
|
705
|
+
if (c.kind === "orthocenter") {
|
|
706
|
+
const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
|
|
707
|
+
return `${obj.label} = tr\u1EF1c t\xE2m \u0394${labels}`;
|
|
708
|
+
}
|
|
709
|
+
return `\u0110i\u1EC3m ${obj.label}`;
|
|
710
|
+
},
|
|
711
|
+
render: (obj, ctx) => {
|
|
712
|
+
const board = ctx.jxg;
|
|
713
|
+
const c = obj.attrs.constraint;
|
|
714
|
+
const opts = {
|
|
715
|
+
name: obj.label,
|
|
716
|
+
withLabel: obj.attrs.showLabel ?? true,
|
|
717
|
+
visible: obj.visible,
|
|
718
|
+
fixed: obj.locked,
|
|
719
|
+
strokeColor: obj.attrs.color ?? "#1e40af",
|
|
720
|
+
fillColor: obj.attrs.color ?? "#1e40af",
|
|
721
|
+
face: obj.attrs.face ?? "o",
|
|
722
|
+
size: obj.attrs.size ?? 4
|
|
723
|
+
};
|
|
724
|
+
if (c.kind === "free") return board.create("point", [c.x, c.y], opts);
|
|
725
|
+
if (c.kind === "onAxis") {
|
|
726
|
+
const coords = c.axis === "x" ? [c.t, 0] : [0, c.t];
|
|
727
|
+
return board.create("point", coords, opts);
|
|
728
|
+
}
|
|
729
|
+
if (c.kind === "onLine") {
|
|
730
|
+
const line = ctx.resolveRef(c.lineId);
|
|
731
|
+
return board.create("glider", [c.t, c.t, line], opts);
|
|
732
|
+
}
|
|
733
|
+
if (c.kind === "onSegment") {
|
|
734
|
+
const seg = ctx.resolveRef(c.segmentId);
|
|
735
|
+
return board.create("glider", [c.t, c.t, seg], opts);
|
|
736
|
+
}
|
|
737
|
+
if (c.kind === "onCircle") {
|
|
738
|
+
const circle = ctx.resolveRef(c.circleId);
|
|
739
|
+
return board.create("glider", [Math.cos(c.theta), Math.sin(c.theta), circle], opts);
|
|
740
|
+
}
|
|
741
|
+
if (c.kind === "onPolygon") {
|
|
742
|
+
const poly = ctx.resolveRef(c.polygonId);
|
|
743
|
+
return board.create("glider", [c.u, c.v, poly], opts);
|
|
744
|
+
}
|
|
745
|
+
if (c.kind === "midpoint") {
|
|
746
|
+
const p1 = ctx.resolveRef(c.p1);
|
|
747
|
+
const p2 = ctx.resolveRef(c.p2);
|
|
748
|
+
return board.create("midpoint", [p1, p2], opts);
|
|
749
|
+
}
|
|
750
|
+
if (c.kind === "transformed") {
|
|
751
|
+
const src = ctx.resolveRef(c.source);
|
|
752
|
+
const transforms = buildJxgTransforms(board, ctx, c.transform);
|
|
753
|
+
const parent = transforms.length === 1 ? transforms[0] : transforms;
|
|
754
|
+
const pt = board.create("point", [src, parent], opts);
|
|
755
|
+
pt._helpers = transforms;
|
|
756
|
+
return pt;
|
|
757
|
+
}
|
|
758
|
+
if (c.kind === "perpFoot") {
|
|
759
|
+
const from = ctx.resolveRef(c.from);
|
|
760
|
+
const onLine = ctx.resolveRef(c.onLine);
|
|
761
|
+
return board.create("perpendicularpoint", [onLine, from], opts);
|
|
762
|
+
}
|
|
763
|
+
if (c.kind === "circumcenter") {
|
|
764
|
+
const a = ctx.resolveRef(c.vertices[0]);
|
|
765
|
+
const b = ctx.resolveRef(c.vertices[1]);
|
|
766
|
+
const c3 = ctx.resolveRef(c.vertices[2]);
|
|
767
|
+
return board.create("circumcenter", [a, b, c3], opts);
|
|
768
|
+
}
|
|
769
|
+
if (c.kind === "incenter") {
|
|
770
|
+
const a = ctx.resolveRef(c.vertices[0]);
|
|
771
|
+
const b = ctx.resolveRef(c.vertices[1]);
|
|
772
|
+
const c3 = ctx.resolveRef(c.vertices[2]);
|
|
773
|
+
return board.create("incenter", [a, b, c3], opts);
|
|
774
|
+
}
|
|
775
|
+
if (c.kind === "centroid") {
|
|
776
|
+
const a = ctx.resolveRef(c.vertices[0]);
|
|
777
|
+
const b = ctx.resolveRef(c.vertices[1]);
|
|
778
|
+
const c3 = ctx.resolveRef(c.vertices[2]);
|
|
779
|
+
return board.create("point", [
|
|
780
|
+
() => (a.X() + b.X() + c3.X()) / 3,
|
|
781
|
+
() => (a.Y() + b.Y() + c3.Y()) / 3
|
|
782
|
+
], opts);
|
|
783
|
+
}
|
|
784
|
+
if (c.kind === "orthocenter") {
|
|
785
|
+
const a = ctx.resolveRef(c.vertices[0]);
|
|
786
|
+
const b = ctx.resolveRef(c.vertices[1]);
|
|
787
|
+
const c3 = ctx.resolveRef(c.vertices[2]);
|
|
788
|
+
const hide = { visible: false, withLabel: false, fixed: true, name: "" };
|
|
789
|
+
const lineBC = board.create("line", [b, c3], hide);
|
|
790
|
+
const altA = board.create("perpendicular", [lineBC, a], hide);
|
|
791
|
+
const lineAC = board.create("line", [a, c3], hide);
|
|
792
|
+
const altB = board.create("perpendicular", [lineAC, b], hide);
|
|
793
|
+
const ortho = board.create("intersection", [altA, altB, 0], opts);
|
|
794
|
+
ortho._helpers = [lineBC, altA, lineAC, altB];
|
|
795
|
+
return ortho;
|
|
796
|
+
}
|
|
797
|
+
return board.create("point", [0, 0], opts);
|
|
798
|
+
},
|
|
799
|
+
/**
|
|
800
|
+
* Free → Free update giữ nguyên JxgObj identity (gọi setPositionDirectly +
|
|
801
|
+
* setAttribute) để các object phụ thuộc (line/segment/...) không bị stale
|
|
802
|
+
* parent ref. Đổi constraint kind → throw để renderer fallback recreate.
|
|
803
|
+
*
|
|
804
|
+
* Đây cũng là endpoint cho drag-sync dispatch trong JxgRenderer: khi user
|
|
805
|
+
* kéo điểm, listener dispatch UPDATE_ATTRS → update hook chạy, vị trí đã
|
|
806
|
+
* đúng sẵn nên setPositionDirectly là no-op nhưng vẫn cần để sync các attrs
|
|
807
|
+
* khác (label/color/...).
|
|
808
|
+
*/
|
|
809
|
+
update: (obj, prev, ctx, existing) => {
|
|
810
|
+
const c = obj.attrs.constraint;
|
|
811
|
+
const oldC = prev.attrs.constraint;
|
|
812
|
+
if (c.kind === "free" && oldC.kind === "free") {
|
|
813
|
+
const el = existing;
|
|
814
|
+
if (typeof el.setPositionDirectly === "function") {
|
|
815
|
+
try {
|
|
816
|
+
el.setPositionDirectly(1, [c.x, c.y]);
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (typeof el.setAttribute === "function") {
|
|
821
|
+
try {
|
|
822
|
+
el.setAttribute({
|
|
823
|
+
name: obj.label,
|
|
824
|
+
withLabel: obj.attrs.showLabel ?? true,
|
|
825
|
+
visible: obj.visible,
|
|
826
|
+
fixed: obj.locked,
|
|
827
|
+
strokeColor: obj.attrs.color ?? "#1e40af",
|
|
828
|
+
fillColor: obj.attrs.color ?? "#1e40af",
|
|
829
|
+
face: obj.attrs.face ?? "o",
|
|
830
|
+
size: obj.attrs.size ?? 4
|
|
831
|
+
});
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
throw new Error("point: constraint kind changed \u2014 recreate");
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
registerKind(def3);
|
|
841
|
+
|
|
842
|
+
// src/core/scene/kinds/segment.ts
|
|
843
|
+
var def4 = {
|
|
844
|
+
type: "segment",
|
|
845
|
+
schemaVersion: 1,
|
|
846
|
+
migrate: {},
|
|
847
|
+
validate: (a) => {
|
|
848
|
+
if (!a?.p1 || !a?.p2) throw new Error("segment: p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
|
|
849
|
+
},
|
|
850
|
+
dependsOn: (a) => [a.p1, a.p2],
|
|
851
|
+
measure: (obj, state) => {
|
|
852
|
+
const p1 = state.objects[obj.attrs.p1];
|
|
853
|
+
const p2 = state.objects[obj.attrs.p2];
|
|
854
|
+
if (!p1 || !p2) return null;
|
|
855
|
+
const c1 = p1.attrs.constraint;
|
|
856
|
+
const c2 = p2.attrs.constraint;
|
|
857
|
+
if (c1?.kind !== "free" || c2?.kind !== "free") return null;
|
|
858
|
+
const dx = (c2.x ?? 0) - (c1.x ?? 0);
|
|
859
|
+
const dy = (c2.y ?? 0) - (c1.y ?? 0);
|
|
860
|
+
return [{ label: "length", value: Math.hypot(dx, dy) }];
|
|
861
|
+
},
|
|
862
|
+
describe: (obj, state) => `\u0110o\u1EA1n th\u1EB3ng ${labelOf(obj.attrs.p1, state)}${labelOf(obj.attrs.p2, state)}`,
|
|
863
|
+
render: (obj, ctx) => {
|
|
864
|
+
const board = ctx.jxg;
|
|
865
|
+
const p1 = ctx.resolveRef(obj.attrs.p1);
|
|
866
|
+
const p2 = ctx.resolveRef(obj.attrs.p2);
|
|
867
|
+
return board.create("segment", [p1, p2], {
|
|
868
|
+
name: obj.label,
|
|
869
|
+
withLabel: obj.attrs.showLabel ?? false,
|
|
870
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
871
|
+
strokeWidth: obj.attrs.width ?? 2,
|
|
872
|
+
dash: obj.attrs.dash ?? 0,
|
|
873
|
+
visible: obj.visible,
|
|
874
|
+
fixed: obj.locked
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
registerKind(def4);
|
|
879
|
+
|
|
880
|
+
// src/core/scene/kinds/line.ts
|
|
881
|
+
function stripBorderSuffix(id) {
|
|
882
|
+
const m = /^(.+):border:\d+$/.exec(id);
|
|
883
|
+
return m ? m[1] : id;
|
|
884
|
+
}
|
|
885
|
+
function constructionRefs(c) {
|
|
886
|
+
switch (c.kind) {
|
|
887
|
+
case "perpendicular":
|
|
888
|
+
case "parallel":
|
|
889
|
+
return [c.throughPoint, stripBorderSuffix(c.toLine)];
|
|
890
|
+
case "perpBisector":
|
|
891
|
+
return [c.p1, c.p2];
|
|
892
|
+
case "angleBisector":
|
|
893
|
+
return [c.p1, c.vertex, c.p2];
|
|
894
|
+
case "angleBisectorLines":
|
|
895
|
+
return [stripBorderSuffix(c.line1), stripBorderSuffix(c.line2)];
|
|
896
|
+
case "tangent":
|
|
897
|
+
return [c.throughPoint, c.toCircle];
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
var def5 = {
|
|
901
|
+
type: "line",
|
|
902
|
+
schemaVersion: 1,
|
|
903
|
+
migrate: {},
|
|
904
|
+
validate: (a) => {
|
|
905
|
+
if (a?.construction) return;
|
|
906
|
+
if (!a?.p1 || !a?.p2) throw new Error("line: p1 v\xE0 p2 b\u1EAFt bu\u1ED9c (ho\u1EB7c construction)");
|
|
907
|
+
},
|
|
908
|
+
dependsOn: (a) => a.construction ? constructionRefs(a.construction) : [a.p1, a.p2],
|
|
909
|
+
describe: (obj, state) => {
|
|
910
|
+
const L = (id) => labelOf(id, state);
|
|
911
|
+
const c = obj.attrs.construction;
|
|
912
|
+
if (!c) return `\u0110\u01B0\u1EDDng th\u1EB3ng ${L(obj.attrs.p1)}${L(obj.attrs.p2)}`;
|
|
913
|
+
switch (c.kind) {
|
|
914
|
+
case "perpendicular":
|
|
915
|
+
return `${obj.label} \u27C2 ${L(c.toLine)} qua ${L(c.throughPoint)}`;
|
|
916
|
+
case "parallel":
|
|
917
|
+
return `${obj.label} \u2225 ${L(c.toLine)} qua ${L(c.throughPoint)}`;
|
|
918
|
+
case "perpBisector":
|
|
919
|
+
return `${obj.label}: trung tr\u1EF1c ${L(c.p1)}${L(c.p2)}`;
|
|
920
|
+
case "angleBisector":
|
|
921
|
+
return `${obj.label}: ph\xE2n gi\xE1c g\xF3c ${L(c.p1)}${L(c.vertex)}${L(c.p2)}`;
|
|
922
|
+
case "angleBisectorLines":
|
|
923
|
+
return `${obj.label}: ph\xE2n gi\xE1c ${L(c.line1)} & ${L(c.line2)} (${c.branch === 0 ? "1" : "2"})`;
|
|
924
|
+
case "tangent":
|
|
925
|
+
return `${obj.label}: ti\u1EBFp tuy\u1EBFn ${L(c.toCircle)} qua ${L(c.throughPoint)}`;
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
render: (obj, ctx) => {
|
|
929
|
+
const board = ctx.jxg;
|
|
930
|
+
const baseOpts = {
|
|
931
|
+
name: obj.label,
|
|
932
|
+
withLabel: obj.attrs.showLabel ?? false,
|
|
933
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
934
|
+
strokeWidth: obj.attrs.width ?? 2,
|
|
935
|
+
dash: obj.attrs.dash ?? 0,
|
|
936
|
+
visible: obj.visible,
|
|
937
|
+
fixed: obj.locked
|
|
938
|
+
};
|
|
939
|
+
const c = obj.attrs.construction;
|
|
940
|
+
if (!c) {
|
|
941
|
+
const p1 = ctx.resolveRef(obj.attrs.p1);
|
|
942
|
+
const p2 = ctx.resolveRef(obj.attrs.p2);
|
|
943
|
+
return board.create("line", [p1, p2], {
|
|
944
|
+
...baseOpts,
|
|
945
|
+
straightFirst: true,
|
|
946
|
+
straightLast: true
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
switch (c.kind) {
|
|
950
|
+
case "perpendicular": {
|
|
951
|
+
const through = ctx.resolveRef(c.throughPoint);
|
|
952
|
+
const toLine = ctx.resolveRef(c.toLine);
|
|
953
|
+
return board.create("perpendicular", [toLine, through], baseOpts);
|
|
954
|
+
}
|
|
955
|
+
case "parallel": {
|
|
956
|
+
const through = ctx.resolveRef(c.throughPoint);
|
|
957
|
+
const toLine = ctx.resolveRef(c.toLine);
|
|
958
|
+
return board.create("parallel", [toLine, through], baseOpts);
|
|
959
|
+
}
|
|
960
|
+
case "perpBisector": {
|
|
961
|
+
const p1 = ctx.resolveRef(c.p1);
|
|
962
|
+
const p2 = ctx.resolveRef(c.p2);
|
|
963
|
+
const mid = board.create("midpoint", [p1, p2], {
|
|
964
|
+
visible: false,
|
|
965
|
+
withLabel: false,
|
|
966
|
+
fixed: true,
|
|
967
|
+
name: ""
|
|
968
|
+
});
|
|
969
|
+
const helperLine = board.create("line", [p1, p2], {
|
|
970
|
+
visible: false,
|
|
971
|
+
withLabel: false,
|
|
972
|
+
fixed: true,
|
|
973
|
+
name: "",
|
|
974
|
+
straightFirst: true,
|
|
975
|
+
straightLast: true
|
|
976
|
+
});
|
|
977
|
+
const bisector = board.create("perpendicular", [helperLine, mid], baseOpts);
|
|
978
|
+
bisector._helpers = [mid, helperLine];
|
|
979
|
+
return bisector;
|
|
980
|
+
}
|
|
981
|
+
case "angleBisector": {
|
|
982
|
+
const p1 = ctx.resolveRef(c.p1);
|
|
983
|
+
const vertex = ctx.resolveRef(c.vertex);
|
|
984
|
+
const p2 = ctx.resolveRef(c.p2);
|
|
985
|
+
return board.create("bisector", [p1, vertex, p2], baseOpts);
|
|
986
|
+
}
|
|
987
|
+
case "angleBisectorLines": {
|
|
988
|
+
const line1Jxg = ctx.resolveRef(c.line1);
|
|
989
|
+
const line2Jxg = ctx.resolveRef(c.line2);
|
|
990
|
+
const comp = board.create("bisectorlines", [line1Jxg, line2Jxg], {
|
|
991
|
+
line1: { visible: false, withLabel: false, fixed: true, name: "" },
|
|
992
|
+
line2: { visible: false, withLabel: false, fixed: true, name: "" }
|
|
993
|
+
});
|
|
994
|
+
const selected = c.branch === 0 ? comp.line1 : comp.line2;
|
|
995
|
+
const other = c.branch === 0 ? comp.line2 : comp.line1;
|
|
996
|
+
selected.setAttribute({
|
|
997
|
+
...baseOpts,
|
|
998
|
+
visible: obj.visible,
|
|
999
|
+
fixed: obj.locked
|
|
1000
|
+
});
|
|
1001
|
+
selected._helpers = [other];
|
|
1002
|
+
return selected;
|
|
1003
|
+
}
|
|
1004
|
+
case "tangent": {
|
|
1005
|
+
const through = ctx.resolveRef(c.throughPoint);
|
|
1006
|
+
const toCircle = ctx.resolveRef(c.toCircle);
|
|
1007
|
+
const branch = c.branch ?? "on";
|
|
1008
|
+
if (branch === "on") {
|
|
1009
|
+
const glider = board.create("glider", [through.X(), through.Y(), toCircle], {
|
|
1010
|
+
visible: false,
|
|
1011
|
+
withLabel: false,
|
|
1012
|
+
fixed: true,
|
|
1013
|
+
name: ""
|
|
1014
|
+
});
|
|
1015
|
+
const tangent2 = board.create("tangent", [glider], baseOpts);
|
|
1016
|
+
tangent2._helpers = [glider];
|
|
1017
|
+
return tangent2;
|
|
1018
|
+
}
|
|
1019
|
+
const center = toCircle.center;
|
|
1020
|
+
const mid = board.create("midpoint", [center, through], {
|
|
1021
|
+
visible: false,
|
|
1022
|
+
withLabel: false,
|
|
1023
|
+
fixed: true,
|
|
1024
|
+
name: ""
|
|
1025
|
+
});
|
|
1026
|
+
const thales = board.create("circle", [mid, through], {
|
|
1027
|
+
visible: false,
|
|
1028
|
+
withLabel: false,
|
|
1029
|
+
fixed: true,
|
|
1030
|
+
strokeOpacity: 0,
|
|
1031
|
+
fillOpacity: 0
|
|
1032
|
+
});
|
|
1033
|
+
const touch = board.create("intersection", [thales, toCircle, branch], {
|
|
1034
|
+
visible: false,
|
|
1035
|
+
withLabel: false,
|
|
1036
|
+
fixed: true,
|
|
1037
|
+
name: ""
|
|
1038
|
+
});
|
|
1039
|
+
const tangent = board.create("line", [through, touch], {
|
|
1040
|
+
...baseOpts,
|
|
1041
|
+
straightFirst: true,
|
|
1042
|
+
straightLast: true
|
|
1043
|
+
});
|
|
1044
|
+
tangent._helpers = [mid, thales, touch];
|
|
1045
|
+
return tangent;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
registerKind(def5);
|
|
1051
|
+
|
|
1052
|
+
// src/core/scene/kinds/ray.ts
|
|
1053
|
+
var def6 = {
|
|
1054
|
+
type: "ray",
|
|
1055
|
+
schemaVersion: 1,
|
|
1056
|
+
migrate: {},
|
|
1057
|
+
validate: (a) => {
|
|
1058
|
+
if (!a?.origin || !a?.through) throw new Error("ray: origin v\xE0 through b\u1EAFt bu\u1ED9c");
|
|
1059
|
+
},
|
|
1060
|
+
dependsOn: (a) => [a.origin, a.through],
|
|
1061
|
+
describe: (obj, state) => `Tia ${labelOf(obj.attrs.origin, state)}${labelOf(obj.attrs.through, state)}`,
|
|
1062
|
+
render: (obj, ctx) => {
|
|
1063
|
+
const board = ctx.jxg;
|
|
1064
|
+
const o = ctx.resolveRef(obj.attrs.origin);
|
|
1065
|
+
const t = ctx.resolveRef(obj.attrs.through);
|
|
1066
|
+
return board.create("line", [o, t], {
|
|
1067
|
+
name: obj.label,
|
|
1068
|
+
straightFirst: false,
|
|
1069
|
+
straightLast: true,
|
|
1070
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
1071
|
+
strokeWidth: obj.attrs.width ?? 2,
|
|
1072
|
+
dash: obj.attrs.dash ?? 0,
|
|
1073
|
+
visible: obj.visible,
|
|
1074
|
+
fixed: obj.locked
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
registerKind(def6);
|
|
1079
|
+
|
|
1080
|
+
// src/core/scene/kinds/vector.ts
|
|
1081
|
+
var def7 = {
|
|
1082
|
+
type: "vector",
|
|
1083
|
+
schemaVersion: 1,
|
|
1084
|
+
migrate: {},
|
|
1085
|
+
validate: (a) => {
|
|
1086
|
+
if (!a?.from || !a?.to) throw new Error("vector: from v\xE0 to b\u1EAFt bu\u1ED9c");
|
|
1087
|
+
},
|
|
1088
|
+
dependsOn: (a) => [a.from, a.to],
|
|
1089
|
+
describe: (obj, state) => `Vector ${labelOf(obj.attrs.from, state)}${labelOf(obj.attrs.to, state)}`,
|
|
1090
|
+
render: (obj, ctx) => {
|
|
1091
|
+
const board = ctx.jxg;
|
|
1092
|
+
const f = ctx.resolveRef(obj.attrs.from);
|
|
1093
|
+
const t = ctx.resolveRef(obj.attrs.to);
|
|
1094
|
+
return board.create("arrow", [f, t], {
|
|
1095
|
+
name: obj.label,
|
|
1096
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
1097
|
+
strokeWidth: obj.attrs.width ?? 2,
|
|
1098
|
+
visible: obj.visible,
|
|
1099
|
+
fixed: obj.locked
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
registerKind(def7);
|
|
1104
|
+
|
|
1105
|
+
// src/core/scene/kinds/circle.ts
|
|
1106
|
+
function constructionRefs2(c) {
|
|
1107
|
+
switch (c.kind) {
|
|
1108
|
+
case "circumscribed":
|
|
1109
|
+
return [c.p1, c.p2, c.p3];
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
var def8 = {
|
|
1113
|
+
type: "circle",
|
|
1114
|
+
schemaVersion: 1,
|
|
1115
|
+
migrate: {},
|
|
1116
|
+
validate: (a) => {
|
|
1117
|
+
if (a?.construction) return;
|
|
1118
|
+
if (!a?.center || !a?.surfacePoint) {
|
|
1119
|
+
throw new Error("circle: center v\xE0 surfacePoint b\u1EAFt bu\u1ED9c (ho\u1EB7c construction)");
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
dependsOn: (a) => a.construction ? constructionRefs2(a.construction) : [a.center, a.surfacePoint],
|
|
1123
|
+
measure: (obj, state) => {
|
|
1124
|
+
if (obj.attrs.construction) return null;
|
|
1125
|
+
const center = obj.attrs.center ? state.objects[obj.attrs.center] : void 0;
|
|
1126
|
+
const surface = obj.attrs.surfacePoint ? state.objects[obj.attrs.surfacePoint] : void 0;
|
|
1127
|
+
if (!center || !surface) return null;
|
|
1128
|
+
const c1 = center.attrs.constraint;
|
|
1129
|
+
const c2 = surface.attrs.constraint;
|
|
1130
|
+
if (c1?.kind !== "free" || c2?.kind !== "free") return null;
|
|
1131
|
+
const dx = (c2.x ?? 0) - (c1.x ?? 0);
|
|
1132
|
+
const dy = (c2.y ?? 0) - (c1.y ?? 0);
|
|
1133
|
+
return [{ label: "r", value: Math.hypot(dx, dy) }];
|
|
1134
|
+
},
|
|
1135
|
+
describe: (obj, state) => {
|
|
1136
|
+
const L = (id) => labelOf(id, state);
|
|
1137
|
+
const c = obj.attrs.construction;
|
|
1138
|
+
if (c?.kind === "circumscribed") {
|
|
1139
|
+
return `\u0110\u01B0\u1EDDng tr\xF2n \u0111i qua ${L(c.p1)}${L(c.p2)}${L(c.p3)}`;
|
|
1140
|
+
}
|
|
1141
|
+
return `\u0110\u01B0\u1EDDng tr\xF2n t\xE2m ${L(obj.attrs.center)} b\xE1n k\xEDnh ${L(obj.attrs.center)}${L(obj.attrs.surfacePoint)}`;
|
|
1142
|
+
},
|
|
1143
|
+
render: (obj, ctx) => {
|
|
1144
|
+
const board = ctx.jxg;
|
|
1145
|
+
const baseOpts = {
|
|
1146
|
+
name: obj.label,
|
|
1147
|
+
withLabel: obj.attrs.showLabel ?? false,
|
|
1148
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
1149
|
+
strokeWidth: obj.attrs.width ?? 2,
|
|
1150
|
+
dash: obj.attrs.dash ?? 0,
|
|
1151
|
+
fillColor: "none",
|
|
1152
|
+
visible: obj.visible,
|
|
1153
|
+
fixed: obj.locked
|
|
1154
|
+
};
|
|
1155
|
+
const c = obj.attrs.construction;
|
|
1156
|
+
if (c?.kind === "circumscribed") {
|
|
1157
|
+
const p1 = ctx.resolveRef(c.p1);
|
|
1158
|
+
const p2 = ctx.resolveRef(c.p2);
|
|
1159
|
+
const p3 = ctx.resolveRef(c.p3);
|
|
1160
|
+
return board.create("circumcircle", [p1, p2, p3], baseOpts);
|
|
1161
|
+
}
|
|
1162
|
+
const center = ctx.resolveRef(obj.attrs.center);
|
|
1163
|
+
const surface = ctx.resolveRef(obj.attrs.surfacePoint);
|
|
1164
|
+
return board.create("circle", [center, surface], baseOpts);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
registerKind(def8);
|
|
1168
|
+
|
|
1169
|
+
// src/core/scene/kinds/arc.ts
|
|
1170
|
+
function constructionRefs3(c) {
|
|
1171
|
+
switch (c.kind) {
|
|
1172
|
+
case "semicircle":
|
|
1173
|
+
return [c.p1, c.p2];
|
|
1174
|
+
case "byCenter":
|
|
1175
|
+
return [c.center, c.p1, c.p2];
|
|
1176
|
+
case "by3Points":
|
|
1177
|
+
return [c.p1, c.p2, c.p3];
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
var def9 = {
|
|
1181
|
+
type: "arc",
|
|
1182
|
+
schemaVersion: 1,
|
|
1183
|
+
migrate: {},
|
|
1184
|
+
validate: (a) => {
|
|
1185
|
+
const c = a?.construction;
|
|
1186
|
+
if (!c) throw new Error("arc: construction b\u1EAFt bu\u1ED9c");
|
|
1187
|
+
if (c.kind === "semicircle") {
|
|
1188
|
+
if (!c.p1 || !c.p2) throw new Error("arc.semicircle: p1, p2 b\u1EAFt bu\u1ED9c");
|
|
1189
|
+
} else if (c.kind === "byCenter") {
|
|
1190
|
+
if (!c.center || !c.p1 || !c.p2) throw new Error("arc.byCenter: center, p1, p2 b\u1EAFt bu\u1ED9c");
|
|
1191
|
+
} else if (c.kind === "by3Points") {
|
|
1192
|
+
if (!c.p1 || !c.p2 || !c.p3) throw new Error("arc.by3Points: p1, p2, p3 b\u1EAFt bu\u1ED9c");
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
dependsOn: (a) => constructionRefs3(a.construction),
|
|
1196
|
+
describe: (obj, state) => {
|
|
1197
|
+
const L = (id) => labelOf(id, state);
|
|
1198
|
+
const c = obj.attrs.construction;
|
|
1199
|
+
switch (c.kind) {
|
|
1200
|
+
case "semicircle":
|
|
1201
|
+
return `N\u1EEDa \u0111\u01B0\u1EDDng tr\xF2n \u0111\u01B0\u1EDDng k\xEDnh ${L(c.p1)}${L(c.p2)}`;
|
|
1202
|
+
case "byCenter":
|
|
1203
|
+
return `Cung tr\xF2n t\xE2m ${L(c.center)} t\u1EEB ${L(c.p1)} \u0111\u1EBFn ${L(c.p2)}`;
|
|
1204
|
+
case "by3Points":
|
|
1205
|
+
return `Cung tr\xF2n qua ${L(c.p1)}${L(c.p2)}${L(c.p3)}`;
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
render: (obj, ctx) => {
|
|
1209
|
+
const board = ctx.jxg;
|
|
1210
|
+
const baseOpts = {
|
|
1211
|
+
name: obj.label,
|
|
1212
|
+
withLabel: obj.attrs.showLabel ?? false,
|
|
1213
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
1214
|
+
strokeWidth: obj.attrs.width ?? 2,
|
|
1215
|
+
dash: obj.attrs.dash ?? 0,
|
|
1216
|
+
fillColor: "none",
|
|
1217
|
+
visible: obj.visible,
|
|
1218
|
+
fixed: obj.locked
|
|
1219
|
+
};
|
|
1220
|
+
const c = obj.attrs.construction;
|
|
1221
|
+
if (c.kind === "semicircle") {
|
|
1222
|
+
const p1 = ctx.resolveRef(c.p1);
|
|
1223
|
+
const p2 = ctx.resolveRef(c.p2);
|
|
1224
|
+
return board.create("semicircle", [p1, p2], baseOpts);
|
|
1225
|
+
}
|
|
1226
|
+
if (c.kind === "byCenter") {
|
|
1227
|
+
const O = ctx.resolveRef(c.center);
|
|
1228
|
+
const A2 = ctx.resolveRef(c.p1);
|
|
1229
|
+
const B2 = ctx.resolveRef(c.p2);
|
|
1230
|
+
return board.create("arc", [O, A2, B2], baseOpts);
|
|
1231
|
+
}
|
|
1232
|
+
const A = ctx.resolveRef(c.p1);
|
|
1233
|
+
const B = ctx.resolveRef(c.p2);
|
|
1234
|
+
const C = ctx.resolveRef(c.p3);
|
|
1235
|
+
return board.create("circumcirclearc", [A, B, C], baseOpts);
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
registerKind(def9);
|
|
1239
|
+
|
|
1240
|
+
// src/core/scene/kinds/sector.ts
|
|
1241
|
+
var def10 = {
|
|
1242
|
+
type: "sector",
|
|
1243
|
+
schemaVersion: 1,
|
|
1244
|
+
migrate: {},
|
|
1245
|
+
validate: (a) => {
|
|
1246
|
+
const c = a?.construction;
|
|
1247
|
+
if (!c) throw new Error("sector: construction b\u1EAFt bu\u1ED9c");
|
|
1248
|
+
if (c.kind === "byCenter") {
|
|
1249
|
+
if (!c.center || !c.p1 || !c.p2) {
|
|
1250
|
+
throw new Error("sector.byCenter: center, p1, p2 b\u1EAFt bu\u1ED9c");
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
},
|
|
1254
|
+
dependsOn: (a) => {
|
|
1255
|
+
const c = a.construction;
|
|
1256
|
+
return [c.center, c.p1, c.p2];
|
|
1257
|
+
},
|
|
1258
|
+
describe: (obj, state) => {
|
|
1259
|
+
const L = (id) => labelOf(id, state);
|
|
1260
|
+
const c = obj.attrs.construction;
|
|
1261
|
+
return `H\xECnh qu\u1EA1t t\xE2m ${L(c.center)} t\u1EEB ${L(c.p1)} \u0111\u1EBFn ${L(c.p2)}`;
|
|
1262
|
+
},
|
|
1263
|
+
render: (obj, ctx) => {
|
|
1264
|
+
const board = ctx.jxg;
|
|
1265
|
+
const c = obj.attrs.construction;
|
|
1266
|
+
const O = ctx.resolveRef(c.center);
|
|
1267
|
+
const A = ctx.resolveRef(c.p1);
|
|
1268
|
+
const B = ctx.resolveRef(c.p2);
|
|
1269
|
+
return board.create("sector", [O, A, B], {
|
|
1270
|
+
name: obj.label,
|
|
1271
|
+
withLabel: obj.attrs.showLabel ?? false,
|
|
1272
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
1273
|
+
strokeWidth: obj.attrs.width ?? 2,
|
|
1274
|
+
fillColor: obj.attrs.fillColor ?? "#f59e0b",
|
|
1275
|
+
fillOpacity: obj.attrs.fillOpacity ?? 0.18,
|
|
1276
|
+
visible: obj.visible,
|
|
1277
|
+
fixed: obj.locked
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
registerKind(def10);
|
|
1282
|
+
|
|
1283
|
+
// src/core/scene/kinds/polygon.ts
|
|
1284
|
+
function regularPolygonName(n) {
|
|
1285
|
+
if (n === 3) return "Tam gi\xE1c \u0111\u1EC1u";
|
|
1286
|
+
if (n === 4) return "H\xECnh vu\xF4ng";
|
|
1287
|
+
if (n === 5) return "Ng\u0169 gi\xE1c \u0111\u1EC1u";
|
|
1288
|
+
if (n === 6) return "L\u1EE5c gi\xE1c \u0111\u1EC1u";
|
|
1289
|
+
return `${n}-gi\xE1c \u0111\u1EC1u`;
|
|
1290
|
+
}
|
|
1291
|
+
function regularVertexLabels(p1Label, p2Label, n) {
|
|
1292
|
+
const A = "A".charCodeAt(0);
|
|
1293
|
+
const Z = "Z".charCodeAt(0);
|
|
1294
|
+
if (p1Label.length === 1 && p2Label.length === 1) {
|
|
1295
|
+
const c1 = p1Label.charCodeAt(0);
|
|
1296
|
+
const c2 = p2Label.charCodeAt(0);
|
|
1297
|
+
if (c1 >= A && c1 <= Z && c2 === c1 + 1 && c1 + n - 1 <= Z) {
|
|
1298
|
+
let out = "";
|
|
1299
|
+
for (let i = 0; i < n; i++) out += String.fromCharCode(c1 + i);
|
|
1300
|
+
return out;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return `${p1Label}${p2Label}\u2026`;
|
|
1304
|
+
}
|
|
1305
|
+
var def11 = {
|
|
1306
|
+
type: "polygon",
|
|
1307
|
+
schemaVersion: 1,
|
|
1308
|
+
migrate: {},
|
|
1309
|
+
validate: (a) => {
|
|
1310
|
+
if (a?.construction) {
|
|
1311
|
+
if (a.construction.kind === "regular") {
|
|
1312
|
+
if (!a.construction.p1 || !a.construction.p2) {
|
|
1313
|
+
throw new Error("polygon (regular): p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
|
|
1314
|
+
}
|
|
1315
|
+
if (!Number.isFinite(a.construction.n) || a.construction.n < 3) {
|
|
1316
|
+
throw new Error("polygon (regular): n \u2265 3");
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (!Array.isArray(a?.vertices) || a.vertices.length < 3) {
|
|
1322
|
+
throw new Error("polygon: c\u1EA7n \xEDt nh\u1EA5t 3 \u0111\u1EC9nh");
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
dependsOn: (a) => {
|
|
1326
|
+
if (a.construction?.kind === "regular") return [a.construction.p1, a.construction.p2];
|
|
1327
|
+
return [...a.vertices ?? []];
|
|
1328
|
+
},
|
|
1329
|
+
describe: (obj, state) => {
|
|
1330
|
+
if (obj.attrs.construction?.kind === "regular") {
|
|
1331
|
+
const c = obj.attrs.construction;
|
|
1332
|
+
const labels = regularVertexLabels(labelOf(c.p1, state), labelOf(c.p2, state), c.n);
|
|
1333
|
+
return `${regularPolygonName(c.n)} ${labels}`;
|
|
1334
|
+
}
|
|
1335
|
+
return `\u0110a gi\xE1c ${(obj.attrs.vertices ?? []).map((id) => labelOf(id, state)).join("")}`;
|
|
1336
|
+
},
|
|
1337
|
+
render: (obj, ctx) => {
|
|
1338
|
+
const board = ctx.jxg;
|
|
1339
|
+
const label = obj.label;
|
|
1340
|
+
const showValue = obj.attrs.showValue ?? false;
|
|
1341
|
+
if (obj.attrs.construction?.kind === "regular") {
|
|
1342
|
+
const c = obj.attrs.construction;
|
|
1343
|
+
const p1 = ctx.resolveRef(c.p1);
|
|
1344
|
+
const p2 = ctx.resolveRef(c.p2);
|
|
1345
|
+
return board.create("regularpolygon", [p1, p2, c.n], {
|
|
1346
|
+
name: label,
|
|
1347
|
+
withLabel: obj.attrs.showLabel ?? false,
|
|
1348
|
+
borders: {
|
|
1349
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
1350
|
+
strokeWidth: obj.attrs.width ?? 2
|
|
1351
|
+
},
|
|
1352
|
+
fillColor: obj.attrs.color ?? "#60a5fa",
|
|
1353
|
+
fillOpacity: obj.attrs.fillOpacity ?? 0.15,
|
|
1354
|
+
visible: obj.visible,
|
|
1355
|
+
fixed: obj.locked
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
const verts = (obj.attrs.vertices ?? []).map((id) => ctx.resolveRef(id));
|
|
1359
|
+
const poly = board.create("polygon", verts, {
|
|
1360
|
+
name: showValue ? function() {
|
|
1361
|
+
const a = typeof this.Area === "function" ? this.Area() : 0;
|
|
1362
|
+
const prefix = obj.attrs.showLabel ?? true ? `${label}: ` : "";
|
|
1363
|
+
return `${prefix}S = ${Math.abs(a).toFixed(2)}`;
|
|
1364
|
+
} : label,
|
|
1365
|
+
withLabel: showValue ? true : obj.attrs.showLabel ?? false,
|
|
1366
|
+
borders: {
|
|
1367
|
+
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
1368
|
+
strokeWidth: obj.attrs.width ?? 2
|
|
1369
|
+
},
|
|
1370
|
+
fillColor: obj.attrs.color ?? "#60a5fa",
|
|
1371
|
+
fillOpacity: obj.attrs.fillOpacity ?? 0.15,
|
|
1372
|
+
visible: obj.visible,
|
|
1373
|
+
fixed: obj.locked
|
|
1374
|
+
});
|
|
1375
|
+
return poly;
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
registerKind(def11);
|
|
1379
|
+
|
|
1380
|
+
// src/core/scene/kinds/intersection.ts
|
|
1381
|
+
var def12 = {
|
|
1382
|
+
type: "intersection",
|
|
1383
|
+
schemaVersion: 1,
|
|
1384
|
+
migrate: {},
|
|
1385
|
+
validate: (a) => {
|
|
1386
|
+
if (!a || !("kind" in a)) throw new Error("intersection: kind b\u1EAFt bu\u1ED9c");
|
|
1387
|
+
if (!a.ref1 || !a.ref2) throw new Error("intersection: ref1 v\xE0 ref2 b\u1EAFt bu\u1ED9c");
|
|
1388
|
+
if (a.kind === "lineLine") return;
|
|
1389
|
+
if (a.kind === "lineCircle" || a.kind === "circleCircle") {
|
|
1390
|
+
if (a.branch !== 0 && a.branch !== 1) {
|
|
1391
|
+
throw new Error(`intersection.${a.kind}: branch ph\u1EA3i l\xE0 0 ho\u1EB7c 1`);
|
|
1392
|
+
}
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
throw new Error(`intersection: kind kh\xF4ng h\u1EE3p l\u1EC7 "${a.kind}"`);
|
|
1396
|
+
},
|
|
1397
|
+
dependsOn: (a) => [a.ref1, a.ref2],
|
|
1398
|
+
describe: (obj) => {
|
|
1399
|
+
const a = obj.attrs;
|
|
1400
|
+
return `${obj.label} = giao ${a.ref1} \u2229 ${a.ref2}`;
|
|
1401
|
+
},
|
|
1402
|
+
render: (obj, ctx) => {
|
|
1403
|
+
const board = ctx.jxg;
|
|
1404
|
+
const a = ctx.resolveRef(obj.attrs.ref1);
|
|
1405
|
+
const b = ctx.resolveRef(obj.attrs.ref2);
|
|
1406
|
+
const opts = {
|
|
1407
|
+
name: obj.label,
|
|
1408
|
+
withLabel: true,
|
|
1409
|
+
strokeColor: obj.attrs.color ?? "#dc2626",
|
|
1410
|
+
fillColor: obj.attrs.color ?? "#dc2626",
|
|
1411
|
+
visible: obj.visible,
|
|
1412
|
+
fixed: obj.locked
|
|
1413
|
+
};
|
|
1414
|
+
if (obj.attrs.kind === "lineLine") {
|
|
1415
|
+
return board.create("intersection", [a, b, 0], opts);
|
|
1416
|
+
}
|
|
1417
|
+
const branch = obj.attrs.branch ?? 0;
|
|
1418
|
+
return board.create("intersection", [a, b, branch], opts);
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
registerKind(def12);
|
|
1422
|
+
|
|
1423
|
+
// src/core/scene/kinds/angle.ts
|
|
1424
|
+
var def13 = {
|
|
1425
|
+
type: "angle",
|
|
1426
|
+
schemaVersion: 1,
|
|
1427
|
+
migrate: {},
|
|
1428
|
+
validate: (a) => {
|
|
1429
|
+
if (!a?.p1 || !a?.vertex || !a?.p2) {
|
|
1430
|
+
throw new Error("angle: p1, vertex, p2 b\u1EAFt bu\u1ED9c");
|
|
1431
|
+
}
|
|
1432
|
+
},
|
|
1433
|
+
dependsOn: (a) => [a.p1, a.vertex, a.p2],
|
|
1434
|
+
describe: (obj, state) => `G\xF3c ${labelOf(obj.attrs.p1, state)}${labelOf(obj.attrs.vertex, state)}${labelOf(obj.attrs.p2, state)}`,
|
|
1435
|
+
render: (obj, ctx) => {
|
|
1436
|
+
const board = ctx.jxg;
|
|
1437
|
+
const pa = ctx.resolveRef(obj.attrs.p1);
|
|
1438
|
+
const pv = ctx.resolveRef(obj.attrs.vertex);
|
|
1439
|
+
const pc = ctx.resolveRef(obj.attrs.p2);
|
|
1440
|
+
let parents = [pa, pv, pc];
|
|
1441
|
+
try {
|
|
1442
|
+
const ax = pa.X() - pv.X(), ay = pa.Y() - pv.Y();
|
|
1443
|
+
const cx = pc.X() - pv.X(), cy = pc.Y() - pv.Y();
|
|
1444
|
+
if (ax * cy - ay * cx < 0) parents = [pc, pv, pa];
|
|
1445
|
+
} catch {
|
|
1446
|
+
}
|
|
1447
|
+
return board.create("angle", parents, {
|
|
1448
|
+
name: obj.label,
|
|
1449
|
+
withLabel: obj.attrs.showLabel ?? true,
|
|
1450
|
+
radius: obj.attrs.radius ?? 1,
|
|
1451
|
+
fillColor: obj.attrs.color ?? "#22c55e",
|
|
1452
|
+
fillOpacity: obj.attrs.fillOpacity ?? 0.25,
|
|
1453
|
+
strokeColor: obj.attrs.color ?? "#16a34a",
|
|
1454
|
+
strokeWidth: 1.5,
|
|
1455
|
+
visible: obj.visible,
|
|
1456
|
+
fixed: obj.locked
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
registerKind(def13);
|
|
1461
|
+
|
|
1462
|
+
// src/core/scene/kinds/distance.ts
|
|
1463
|
+
var def14 = {
|
|
1464
|
+
type: "distance",
|
|
1465
|
+
schemaVersion: 1,
|
|
1466
|
+
migrate: {},
|
|
1467
|
+
validate: (a) => {
|
|
1468
|
+
if (!a?.p1 || !a?.p2) throw new Error("distance: p1 v\xE0 p2 b\u1EAFt bu\u1ED9c");
|
|
1469
|
+
},
|
|
1470
|
+
dependsOn: (a) => [a.p1, a.p2],
|
|
1471
|
+
describe: (obj, state) => `Kho\u1EA3ng c\xE1ch ${labelOf(obj.attrs.p1, state)}${labelOf(obj.attrs.p2, state)}`,
|
|
1472
|
+
render: (obj, ctx) => {
|
|
1473
|
+
const board = ctx.jxg;
|
|
1474
|
+
const p1 = ctx.resolveRef(obj.attrs.p1);
|
|
1475
|
+
const p2 = ctx.resolveRef(obj.attrs.p2);
|
|
1476
|
+
const prefix = obj.attrs.prefix ?? "d = ";
|
|
1477
|
+
const precision = obj.attrs.precision ?? 2;
|
|
1478
|
+
return board.create("text", [
|
|
1479
|
+
() => (p1.X() + p2.X()) / 2,
|
|
1480
|
+
() => (p1.Y() + p2.Y()) / 2,
|
|
1481
|
+
() => `${prefix}${Math.hypot(p1.X() - p2.X(), p1.Y() - p2.Y()).toFixed(precision)}`
|
|
1482
|
+
], {
|
|
1483
|
+
fontSize: obj.attrs.fontSize ?? 14,
|
|
1484
|
+
strokeColor: obj.attrs.color ?? "#dc2626",
|
|
1485
|
+
anchorX: "middle",
|
|
1486
|
+
anchorY: "middle",
|
|
1487
|
+
visible: obj.visible,
|
|
1488
|
+
fixed: true
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
registerKind(def14);
|
|
1493
|
+
|
|
1494
|
+
// src/core/scene/kinds/function2d.ts
|
|
1495
|
+
var def15 = {
|
|
1496
|
+
type: "function2d",
|
|
1497
|
+
schemaVersion: 1,
|
|
1498
|
+
migrate: {},
|
|
1499
|
+
validate: (a) => {
|
|
1500
|
+
if (!a) throw new Error("function2d: attrs b\u1EAFt bu\u1ED9c");
|
|
1501
|
+
if (typeof a.expression !== "string" || !a.expression.trim()) {
|
|
1502
|
+
throw new Error("function2d: expression r\u1ED7ng");
|
|
1503
|
+
}
|
|
1504
|
+
const v = validate(a.expression);
|
|
1505
|
+
if (!v.ok) throw new Error(`function2d: expression invalid \u2014 ${v.error}`);
|
|
1506
|
+
if (typeof a.color !== "string") throw new Error("function2d: color b\u1EAFt bu\u1ED9c");
|
|
1507
|
+
if (typeof a.visible !== "boolean") throw new Error("function2d: visible b\u1EAFt bu\u1ED9c");
|
|
1508
|
+
if (a.domain) {
|
|
1509
|
+
if (a.domain.min >= a.domain.max) {
|
|
1510
|
+
throw new Error("function2d: domain min ph\u1EA3i < max");
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
},
|
|
1514
|
+
dependsOn: () => [],
|
|
1515
|
+
describe: (obj) => `${obj.label}(x) = ${obj.attrs.expression}`,
|
|
1516
|
+
render: (obj, ctx) => {
|
|
1517
|
+
const board = ctx.jxg;
|
|
1518
|
+
if (!obj.visible || !obj.attrs.visible) return null;
|
|
1519
|
+
const fn = compile(obj.attrs.expression, ctx.paramMap ?? {});
|
|
1520
|
+
if (typeof fn !== "function") return null;
|
|
1521
|
+
const view = ctx.defaults.view;
|
|
1522
|
+
const xMin = obj.attrs.domain?.min ?? view?.xMin ?? -10;
|
|
1523
|
+
const xMax = obj.attrs.domain?.max ?? view?.xMax ?? 10;
|
|
1524
|
+
return board.create("functiongraph", [fn, xMin, xMax], {
|
|
1525
|
+
strokeColor: obj.attrs.color,
|
|
1526
|
+
strokeWidth: 2,
|
|
1527
|
+
name: obj.label,
|
|
1528
|
+
withLabel: false,
|
|
1529
|
+
highlight: false,
|
|
1530
|
+
fixed: true
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
registerKind(def15);
|
|
1535
|
+
|
|
1536
|
+
// src/core/scene/kinds/parameter.ts
|
|
1537
|
+
var def16 = {
|
|
1538
|
+
type: "parameter",
|
|
1539
|
+
schemaVersion: 1,
|
|
1540
|
+
migrate: {},
|
|
1541
|
+
validate: (a) => {
|
|
1542
|
+
if (!a) throw new Error("parameter: attrs b\u1EAFt bu\u1ED9c");
|
|
1543
|
+
if (typeof a.value !== "number" || typeof a.min !== "number" || typeof a.max !== "number") {
|
|
1544
|
+
throw new Error("parameter: value/min/max ph\u1EA3i l\xE0 number");
|
|
1545
|
+
}
|
|
1546
|
+
if (a.min >= a.max) throw new Error("parameter: min ph\u1EA3i < max");
|
|
1547
|
+
if (a.value < a.min || a.value > a.max) throw new Error("parameter: value ngo\xE0i [min, max]");
|
|
1548
|
+
if (typeof a.step !== "number" || a.step <= 0) throw new Error("parameter: step ph\u1EA3i > 0");
|
|
1549
|
+
},
|
|
1550
|
+
dependsOn: () => [],
|
|
1551
|
+
describe: (obj) => `${obj.label} = ${obj.attrs.value}`,
|
|
1552
|
+
render: () => null
|
|
1553
|
+
// Không render lên board
|
|
1554
|
+
};
|
|
1555
|
+
registerKind(def16);
|
|
1556
|
+
|
|
1557
|
+
// src/core/scene/kinds/pointOnCurve.ts
|
|
1558
|
+
var def17 = {
|
|
1559
|
+
type: "pointOnCurve",
|
|
1560
|
+
schemaVersion: 1,
|
|
1561
|
+
migrate: {},
|
|
1562
|
+
validate: (a) => {
|
|
1563
|
+
if (!a || typeof a.functionId !== "string" || !a.functionId) {
|
|
1564
|
+
throw new Error("pointOnCurve: functionId b\u1EAFt bu\u1ED9c");
|
|
1565
|
+
}
|
|
1566
|
+
if (typeof a.x !== "number" || !Number.isFinite(a.x)) {
|
|
1567
|
+
throw new Error("pointOnCurve: x ph\u1EA3i l\xE0 finite number");
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
dependsOn: (a) => [a.functionId],
|
|
1571
|
+
describe: (obj) => `${obj.label} tr\xEAn ${obj.attrs.functionId} t\u1EA1i x=${obj.attrs.x.toFixed(3)}`,
|
|
1572
|
+
render: (obj, ctx) => {
|
|
1573
|
+
const board = ctx.jxg;
|
|
1574
|
+
const curve = ctx.resolveRef(obj.attrs.functionId);
|
|
1575
|
+
if (!curve) return null;
|
|
1576
|
+
return board.create("glider", [obj.attrs.x, 0, curve], {
|
|
1577
|
+
name: obj.label,
|
|
1578
|
+
size: 3,
|
|
1579
|
+
withLabel: obj.label !== "",
|
|
1580
|
+
fillColor: "#000",
|
|
1581
|
+
strokeColor: "#000"
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
registerKind(def17);
|
|
1586
|
+
|
|
1587
|
+
// src/core/scene/kinds/tangent2d.ts
|
|
1588
|
+
var def18 = {
|
|
1589
|
+
type: "tangent2d",
|
|
1590
|
+
schemaVersion: 1,
|
|
1591
|
+
migrate: {},
|
|
1592
|
+
validate: (a) => {
|
|
1593
|
+
if (!a || typeof a.pointId !== "string" || !a.pointId) {
|
|
1594
|
+
throw new Error("tangent2d: pointId b\u1EAFt bu\u1ED9c");
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
dependsOn: (a) => [a.pointId],
|
|
1598
|
+
describe: (obj) => `Ti\u1EBFp tuy\u1EBFn t\u1EA1i ${obj.attrs.pointId}`,
|
|
1599
|
+
render: (obj, ctx) => {
|
|
1600
|
+
const board = ctx.jxg;
|
|
1601
|
+
const pt = ctx.resolveRef(obj.attrs.pointId);
|
|
1602
|
+
if (!pt) return null;
|
|
1603
|
+
return board.create("tangent", [pt], {
|
|
1604
|
+
strokeColor: "#65a30d",
|
|
1605
|
+
strokeWidth: 1.5,
|
|
1606
|
+
dash: 2,
|
|
1607
|
+
withLabel: false
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
registerKind(def18);
|
|
1612
|
+
|
|
1613
|
+
// src/core/scene/expressions/evaluator.ts
|
|
1614
|
+
var DEFAULT_SAMPLES = 1e3;
|
|
1615
|
+
function scanRoots(fn, xMin, xMax, samples = DEFAULT_SAMPLES) {
|
|
1616
|
+
if (!Number.isFinite(xMin) || !Number.isFinite(xMax) || xMin >= xMax) return [];
|
|
1617
|
+
const out = [];
|
|
1618
|
+
const step = (xMax - xMin) / samples;
|
|
1619
|
+
let prev = fn(xMin);
|
|
1620
|
+
for (let i = 1; i <= samples; i++) {
|
|
1621
|
+
const x = xMin + i * step;
|
|
1622
|
+
const curr = fn(x);
|
|
1623
|
+
if (!Number.isFinite(prev) || !Number.isFinite(curr)) {
|
|
1624
|
+
prev = curr;
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (prev * curr < 0) {
|
|
1628
|
+
const root = bisect(fn, x - step, x);
|
|
1629
|
+
if (Number.isFinite(root)) out.push(root);
|
|
1630
|
+
} else if (prev !== 0 && curr === 0) {
|
|
1631
|
+
out.push(x);
|
|
1632
|
+
}
|
|
1633
|
+
prev = curr;
|
|
1634
|
+
}
|
|
1635
|
+
return out;
|
|
1636
|
+
}
|
|
1637
|
+
function bisect(fn, lo, hi) {
|
|
1638
|
+
for (let i = 0; i < 50; i++) {
|
|
1639
|
+
const mid = (lo + hi) / 2;
|
|
1640
|
+
const fmid = fn(mid);
|
|
1641
|
+
if (!Number.isFinite(fmid)) break;
|
|
1642
|
+
if (Math.abs(fmid) < 1e-10) return mid;
|
|
1643
|
+
const flo = fn(lo);
|
|
1644
|
+
if (!Number.isFinite(flo)) break;
|
|
1645
|
+
if (flo * fmid <= 0) hi = mid;
|
|
1646
|
+
else lo = mid;
|
|
1647
|
+
}
|
|
1648
|
+
return (lo + hi) / 2;
|
|
1649
|
+
}
|
|
1650
|
+
function scanExtrema(fn, xMin, xMax, samples = DEFAULT_SAMPLES) {
|
|
1651
|
+
if (!Number.isFinite(xMin) || !Number.isFinite(xMax) || xMin >= xMax) return [];
|
|
1652
|
+
const out = [];
|
|
1653
|
+
const step = (xMax - xMin) / samples;
|
|
1654
|
+
const xs = [];
|
|
1655
|
+
const ys = [];
|
|
1656
|
+
for (let i = 0; i <= samples + 1; i++) {
|
|
1657
|
+
const x = xMin + (i - 1) * step;
|
|
1658
|
+
xs.push(x);
|
|
1659
|
+
ys.push(fn(x));
|
|
1660
|
+
}
|
|
1661
|
+
let prevSign = 0;
|
|
1662
|
+
let prevSignIdx = 1;
|
|
1663
|
+
for (let i = 1; i <= samples; i++) {
|
|
1664
|
+
const d = ys[i + 1] - ys[i - 1];
|
|
1665
|
+
if (!Number.isFinite(d)) continue;
|
|
1666
|
+
const sign = d > 0 ? 1 : d < 0 ? -1 : 0;
|
|
1667
|
+
if (sign === 0) continue;
|
|
1668
|
+
if (prevSign !== 0 && sign !== prevSign) {
|
|
1669
|
+
const type = prevSign > 0 ? "max" : "min";
|
|
1670
|
+
const midI = Math.round((prevSignIdx + i) / 2);
|
|
1671
|
+
out.push({ x: xs[midI], y: ys[midI], type });
|
|
1672
|
+
}
|
|
1673
|
+
prevSign = sign;
|
|
1674
|
+
prevSignIdx = i;
|
|
1675
|
+
}
|
|
1676
|
+
return out;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/core/scene/kinds/extremum2d.ts
|
|
1680
|
+
var def19 = {
|
|
1681
|
+
type: "extremum2d",
|
|
1682
|
+
schemaVersion: 1,
|
|
1683
|
+
migrate: {},
|
|
1684
|
+
validate: (a) => {
|
|
1685
|
+
if (!a || typeof a.functionId !== "string" || !a.functionId) {
|
|
1686
|
+
throw new Error("extremum2d: functionId b\u1EAFt bu\u1ED9c");
|
|
1687
|
+
}
|
|
1688
|
+
if (!a.interval || a.interval.min >= a.interval.max) {
|
|
1689
|
+
throw new Error("extremum2d: interval min ph\u1EA3i < max");
|
|
1690
|
+
}
|
|
1691
|
+
if (a.mode !== "max" && a.mode !== "min") {
|
|
1692
|
+
throw new Error('extremum2d: mode ph\u1EA3i l\xE0 "max" ho\u1EB7c "min"');
|
|
1693
|
+
}
|
|
1694
|
+
},
|
|
1695
|
+
dependsOn: (a) => [a.functionId],
|
|
1696
|
+
describe: (obj) => `${obj.attrs.mode === "max" ? "C\u1EF1c \u0111\u1EA1i" : "C\u1EF1c ti\u1EC3u"} c\u1EE7a ${obj.attrs.functionId} trong [${obj.attrs.interval.min}, ${obj.attrs.interval.max}]`,
|
|
1697
|
+
render: (obj, ctx) => {
|
|
1698
|
+
const board = ctx.jxg;
|
|
1699
|
+
const expr = ctx.defaults._functionExpr?.[obj.attrs.functionId];
|
|
1700
|
+
if (!expr) return null;
|
|
1701
|
+
const fn = compile(expr, ctx.paramMap ?? {});
|
|
1702
|
+
if (typeof fn !== "function") return null;
|
|
1703
|
+
const extrema = scanExtrema(fn, obj.attrs.interval.min, obj.attrs.interval.max).filter((e) => e.type === obj.attrs.mode);
|
|
1704
|
+
return extrema.map((e) => board.create("point", [e.x, e.y], {
|
|
1705
|
+
name: obj.label,
|
|
1706
|
+
size: 3,
|
|
1707
|
+
fillColor: "#dc2626",
|
|
1708
|
+
strokeColor: "#dc2626",
|
|
1709
|
+
withLabel: obj.label !== ""
|
|
1710
|
+
}));
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
registerKind(def19);
|
|
1714
|
+
|
|
1715
|
+
// src/core/scene/kinds/root2d.ts
|
|
1716
|
+
var def20 = {
|
|
1717
|
+
type: "root2d",
|
|
1718
|
+
schemaVersion: 1,
|
|
1719
|
+
migrate: {},
|
|
1720
|
+
validate: (a) => {
|
|
1721
|
+
if (!a || typeof a.functionId !== "string" || !a.functionId) {
|
|
1722
|
+
throw new Error("root2d: functionId b\u1EAFt bu\u1ED9c");
|
|
1723
|
+
}
|
|
1724
|
+
if (!a.interval || a.interval.min >= a.interval.max) {
|
|
1725
|
+
throw new Error("root2d: interval min ph\u1EA3i < max");
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
dependsOn: (a) => [a.functionId],
|
|
1729
|
+
describe: (obj) => `Nghi\u1EC7m c\u1EE7a ${obj.attrs.functionId} trong [${obj.attrs.interval.min}, ${obj.attrs.interval.max}]`,
|
|
1730
|
+
render: (obj, ctx) => {
|
|
1731
|
+
const board = ctx.jxg;
|
|
1732
|
+
const expr = ctx.defaults._functionExpr?.[obj.attrs.functionId];
|
|
1733
|
+
if (!expr) return null;
|
|
1734
|
+
const fn = compile(expr, ctx.paramMap ?? {});
|
|
1735
|
+
if (typeof fn !== "function") return null;
|
|
1736
|
+
const roots = scanRoots(fn, obj.attrs.interval.min, obj.attrs.interval.max);
|
|
1737
|
+
return roots.map((x) => board.create("point", [x, 0], {
|
|
1738
|
+
name: obj.label,
|
|
1739
|
+
size: 3,
|
|
1740
|
+
fillColor: "#dc2626",
|
|
1741
|
+
strokeColor: "#dc2626",
|
|
1742
|
+
withLabel: obj.label !== ""
|
|
1743
|
+
}));
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
registerKind(def20);
|
|
1747
|
+
|
|
1748
|
+
// src/core/scene/kinds/slope2d.ts
|
|
1749
|
+
var def21 = {
|
|
1750
|
+
type: "slope2d",
|
|
1751
|
+
schemaVersion: 1,
|
|
1752
|
+
migrate: {},
|
|
1753
|
+
validate: (a) => {
|
|
1754
|
+
if (!a || typeof a.pointId !== "string" || !a.pointId) {
|
|
1755
|
+
throw new Error("slope2d: pointId b\u1EAFt bu\u1ED9c");
|
|
1756
|
+
}
|
|
1757
|
+
},
|
|
1758
|
+
dependsOn: (a) => [a.pointId],
|
|
1759
|
+
describe: (obj) => `Slope t\u1EA1i ${obj.attrs.pointId}`,
|
|
1760
|
+
render: (obj, ctx) => {
|
|
1761
|
+
const board = ctx.jxg;
|
|
1762
|
+
const pt = ctx.resolveRef(obj.attrs.pointId);
|
|
1763
|
+
if (!pt) return null;
|
|
1764
|
+
return board.create("slopetriangle", [pt], {
|
|
1765
|
+
name: obj.label,
|
|
1766
|
+
withLabel: true,
|
|
1767
|
+
fillColor: "#9333ea",
|
|
1768
|
+
strokeColor: "#9333ea",
|
|
1769
|
+
fillOpacity: 0.2
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
registerKind(def21);
|
|
1774
|
+
|
|
1775
|
+
// src/stamps/shared/serializeScene.ts
|
|
1776
|
+
function serializeScene(state) {
|
|
1777
|
+
return JSON.stringify(state);
|
|
1778
|
+
}
|
|
1779
|
+
function isValidState(value, domain) {
|
|
1780
|
+
if (!value || typeof value !== "object") return false;
|
|
1781
|
+
const v = value;
|
|
1782
|
+
if (!v.meta || typeof v.meta !== "object") return false;
|
|
1783
|
+
if (v.meta.domain !== domain) return false;
|
|
1784
|
+
if (!v.meta.view || typeof v.meta.view !== "object") return false;
|
|
1785
|
+
if (!v.objects || typeof v.objects !== "object") return false;
|
|
1786
|
+
if (!Array.isArray(v.order)) return false;
|
|
1787
|
+
return true;
|
|
1788
|
+
}
|
|
1789
|
+
function deserializeScene(domain, raw) {
|
|
1790
|
+
if (!raw) return createEmptyState(domain);
|
|
1791
|
+
try {
|
|
1792
|
+
const parsed = JSON.parse(raw);
|
|
1793
|
+
if (isValidState(parsed, domain)) return parsed;
|
|
1794
|
+
} catch {
|
|
1795
|
+
}
|
|
1796
|
+
return createEmptyState(domain);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
export { DEFAULT_VIEW_2D, DEFAULT_VIEW_3D, createEmptyState, deserializeScene, listObjects, nextLabel, serializeScene, useEditorState };
|
|
1800
|
+
//# sourceMappingURL=chunk-6V4SH4JJ.mjs.map
|
|
1801
|
+
//# sourceMappingURL=chunk-6V4SH4JJ.mjs.map
|