@xom11/whiteboard 0.29.0 → 0.31.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/dist/ai.d.mts +259 -33
- package/dist/ai.d.ts +259 -33
- package/dist/ai.js +5424 -470
- package/dist/ai.js.map +1 -1
- package/dist/ai.mjs +4971 -351
- package/dist/ai.mjs.map +1 -1
- package/dist/catalog.json +5 -5
- package/dist/{chunk-V3YJ6JFL.mjs → chunk-44JY2AKC.mjs} +3 -3
- package/dist/{chunk-V3YJ6JFL.mjs.map → chunk-44JY2AKC.mjs.map} +1 -1
- package/dist/{chunk-GEC2D2EQ.mjs → chunk-BMYC2ILT.mjs} +4 -4
- package/dist/{chunk-GEC2D2EQ.mjs.map → chunk-BMYC2ILT.mjs.map} +1 -1
- package/dist/{chunk-PPKHCRRE.mjs → chunk-C76SOFXF.mjs} +3 -3
- package/dist/{chunk-PPKHCRRE.mjs.map → chunk-C76SOFXF.mjs.map} +1 -1
- package/dist/{chunk-IHUFOV7L.mjs → chunk-CH6SFONH.mjs} +15 -3
- package/dist/chunk-CH6SFONH.mjs.map +1 -0
- package/dist/{chunk-E6EDOPGT.mjs → chunk-DWIEVCGK.mjs} +254 -16
- package/dist/chunk-DWIEVCGK.mjs.map +1 -0
- package/dist/{chunk-SZDAS7LK.mjs → chunk-IE2GGHNF.mjs} +131 -81
- package/dist/chunk-IE2GGHNF.mjs.map +1 -0
- package/dist/{chunk-ZTQBUKLJ.mjs → chunk-JJ4FPCBE.mjs} +142 -22
- package/dist/chunk-JJ4FPCBE.mjs.map +1 -0
- package/dist/{chunk-QRUAEXLR.mjs → chunk-K5BS2H56.mjs} +5 -5
- package/dist/{chunk-QRUAEXLR.mjs.map → chunk-K5BS2H56.mjs.map} +1 -1
- package/dist/{chunk-BNBOIDO5.mjs → chunk-K7VJU7LQ.mjs} +3 -3
- package/dist/{chunk-BNBOIDO5.mjs.map → chunk-K7VJU7LQ.mjs.map} +1 -1
- package/dist/{chunk-H22OZYTW.mjs → chunk-KOXOC2FI.mjs} +48 -39
- package/dist/chunk-KOXOC2FI.mjs.map +1 -0
- package/dist/{chunk-CXHNVYMD.mjs → chunk-KWDBVLST.mjs} +5 -5
- package/dist/{chunk-CXHNVYMD.mjs.map → chunk-KWDBVLST.mjs.map} +1 -1
- package/dist/{chunk-OQIQNKPQ.mjs → chunk-LTLLQUMN.mjs} +4 -4
- package/dist/{chunk-OQIQNKPQ.mjs.map → chunk-LTLLQUMN.mjs.map} +1 -1
- package/dist/chunk-QK6OVDLC.mjs +103 -0
- package/dist/chunk-QK6OVDLC.mjs.map +1 -0
- package/dist/{chunk-QGNU34T7.mjs → chunk-QLQ4MJNO.mjs} +10 -4
- package/dist/chunk-QLQ4MJNO.mjs.map +1 -0
- package/dist/{chunk-BU5KLO3P.mjs → chunk-T3N4BSJV.mjs} +4 -4
- package/dist/{chunk-BU5KLO3P.mjs.map → chunk-T3N4BSJV.mjs.map} +1 -1
- package/dist/{chunk-5JM35CXV.mjs → chunk-TMRFSOM7.mjs} +4 -4
- package/dist/{chunk-5JM35CXV.mjs.map → chunk-TMRFSOM7.mjs.map} +1 -1
- package/dist/geometry-2d.d.mts +1 -1
- package/dist/geometry-2d.d.ts +1 -1
- package/dist/geometry-2d.js +841 -204
- package/dist/geometry-2d.js.map +1 -1
- package/dist/geometry-2d.mjs +5 -5
- package/dist/geometry-3d.d.mts +1 -1
- package/dist/geometry-3d.d.ts +1 -1
- package/dist/geometry-3d.js +172 -22
- package/dist/geometry-3d.js.map +1 -1
- package/dist/geometry-3d.mjs +4 -4
- package/dist/graph-2d.d.mts +1 -1
- package/dist/graph-2d.d.ts +1 -1
- package/dist/graph-2d.js +307 -100
- package/dist/graph-2d.js.map +1 -1
- package/dist/graph-2d.mjs +7 -7
- package/dist/handleExtractProblem-BrDY9ifM.d.mts +58 -0
- package/dist/handleExtractProblem-BrDY9ifM.d.ts +58 -0
- package/dist/{host-HOSJHQ5H.mjs → host-4FIUNIDQ.mjs} +13 -12
- package/dist/host-4FIUNIDQ.mjs.map +1 -0
- package/dist/{host-2ISGVO7O.mjs → host-4ZB4XD4S.mjs} +9 -8
- package/dist/host-4ZB4XD4S.mjs.map +1 -0
- package/dist/{host-ZQCDAT6O.mjs → host-H2IGOKJU.mjs} +3 -3
- package/dist/{host-ZQCDAT6O.mjs.map → host-H2IGOKJU.mjs.map} +1 -1
- package/dist/{host-HKMZSCIT.mjs → host-KMWP7KBT.mjs} +286 -74
- package/dist/host-KMWP7KBT.mjs.map +1 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +849 -206
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +22 -21
- package/dist/index.mjs.map +1 -1
- package/dist/latex.d.mts +1 -1
- package/dist/latex.d.ts +1 -1
- package/dist/latex.js +8 -2
- package/dist/latex.js.map +1 -1
- package/dist/latex.mjs +1 -1
- package/dist/render-NMS7OAV6.mjs +10 -0
- package/dist/{render-ZX2O2IK7.mjs.map → render-NMS7OAV6.mjs.map} +1 -1
- package/dist/serialize-PGHQZEPV.mjs +9 -0
- package/dist/{serialize-N4G6RFBB.mjs.map → serialize-PGHQZEPV.mjs.map} +1 -1
- package/dist/{types-C3FjpoUi.d.ts → types-tePd94vW.d.mts} +8 -0
- package/dist/{types-C3FjpoUi.d.mts → types-tePd94vW.d.ts} +8 -0
- package/package.json +2 -1
- package/dist/chunk-E6EDOPGT.mjs.map +0 -1
- package/dist/chunk-H22OZYTW.mjs.map +0 -1
- package/dist/chunk-IHUFOV7L.mjs.map +0 -1
- package/dist/chunk-QGNU34T7.mjs.map +0 -1
- package/dist/chunk-SZDAS7LK.mjs.map +0 -1
- package/dist/chunk-ZTQBUKLJ.mjs.map +0 -1
- package/dist/host-2ISGVO7O.mjs.map +0 -1
- package/dist/host-HKMZSCIT.mjs.map +0 -1
- package/dist/host-HOSJHQ5H.mjs.map +0 -1
- package/dist/render-ZX2O2IK7.mjs +0 -10
- package/dist/serialize-N4G6RFBB.mjs +0 -9
package/dist/geometry-2d.js
CHANGED
|
@@ -939,8 +939,12 @@ function constraintRefs2D(c) {
|
|
|
939
939
|
return [c.line, c.circle, c.other];
|
|
940
940
|
case "tangencyPoint":
|
|
941
941
|
return [c.circle, c.onLine];
|
|
942
|
-
case "arcMidpoint":
|
|
943
|
-
|
|
942
|
+
case "arcMidpoint": {
|
|
943
|
+
const containment = c.notContaining ?? c.containing;
|
|
944
|
+
return containment ? [c.circle, c.a, c.b, containment] : [c.circle, c.a, c.b];
|
|
945
|
+
}
|
|
946
|
+
case "mixtilinearPoint":
|
|
947
|
+
return [c.vertices[0], c.vertices[1], c.vertices[2]];
|
|
944
948
|
case "pointAtDistance": {
|
|
945
949
|
const d = c.distance;
|
|
946
950
|
const extra = d.kind === "circleRadius" ? [d.circle] : d.kind === "segmentLength" ? [d.p1, d.p2] : [];
|
|
@@ -1227,7 +1231,7 @@ function dist(p, q) {
|
|
|
1227
1231
|
function sideOf(a, b, p) {
|
|
1228
1232
|
return (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
|
|
1229
1233
|
}
|
|
1230
|
-
function arcMidpoint(center, radius, a, b,
|
|
1234
|
+
function arcMidpoint(center, radius, a, b, reference, sameSide = false) {
|
|
1231
1235
|
const mcx = (a[0] + b[0]) / 2;
|
|
1232
1236
|
const mcy = (a[1] + b[1]) / 2;
|
|
1233
1237
|
let ux = mcx - center[0];
|
|
@@ -1242,12 +1246,18 @@ function arcMidpoint(center, radius, a, b, notContaining) {
|
|
|
1242
1246
|
uy /= len;
|
|
1243
1247
|
const cand1 = [center[0] + radius * ux, center[1] + radius * uy];
|
|
1244
1248
|
const cand2 = [center[0] - radius * ux, center[1] - radius * uy];
|
|
1249
|
+
if (!reference) return cand1[1] >= cand2[1] ? cand1 : cand2;
|
|
1250
|
+
const notContaining = reference;
|
|
1245
1251
|
const sN = sideOf(a, b, notContaining);
|
|
1246
1252
|
if (Math.abs(sN) < 1e-9) {
|
|
1247
|
-
|
|
1253
|
+
const far = dist(cand1, notContaining) >= dist(cand2, notContaining) ? cand1 : cand2;
|
|
1254
|
+
const near = far === cand1 ? cand2 : cand1;
|
|
1255
|
+
return sameSide ? near : far;
|
|
1248
1256
|
}
|
|
1249
1257
|
const s1 = sideOf(a, b, cand1);
|
|
1250
|
-
|
|
1258
|
+
const opp = s1 * sN < 0 ? cand1 : cand2;
|
|
1259
|
+
const same = opp === cand1 ? cand2 : cand1;
|
|
1260
|
+
return sameSide ? same : opp;
|
|
1251
1261
|
}
|
|
1252
1262
|
function excenter(vertices, oppositeIndex) {
|
|
1253
1263
|
const [A, B, C] = vertices;
|
|
@@ -1263,6 +1273,35 @@ function excenter(vertices, oppositeIndex) {
|
|
|
1263
1273
|
(w[0] * A[1] + w[1] * B[1] + w[2] * C[1]) / sum
|
|
1264
1274
|
];
|
|
1265
1275
|
}
|
|
1276
|
+
function circumcenterXY(a, b, c) {
|
|
1277
|
+
const ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
|
|
1278
|
+
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
|
1279
|
+
if (Math.abs(d) < 1e-12) return [(ax + bx + cx) / 3, (ay + by + cy) / 3];
|
|
1280
|
+
const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
|
|
1281
|
+
const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
|
|
1282
|
+
return [ux, uy];
|
|
1283
|
+
}
|
|
1284
|
+
function mixtilinearPoint(a, b, c, which) {
|
|
1285
|
+
const O = circumcenterXY(a, b, c);
|
|
1286
|
+
const R = Math.hypot(O[0] - a[0], O[1] - a[1]);
|
|
1287
|
+
const ux1 = (b[0] - a[0]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
|
|
1288
|
+
const uy1 = (b[1] - a[1]) / (Math.hypot(b[0] - a[0], b[1] - a[1]) || 1);
|
|
1289
|
+
const ux2 = (c[0] - a[0]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
|
|
1290
|
+
const uy2 = (c[1] - a[1]) / (Math.hypot(c[0] - a[0], c[1] - a[1]) || 1);
|
|
1291
|
+
let bx = ux1 + ux2, by = uy1 + uy2;
|
|
1292
|
+
const bl = Math.hypot(bx, by) || 1;
|
|
1293
|
+
bx /= bl;
|
|
1294
|
+
by /= bl;
|
|
1295
|
+
const cosA = ux1 * ux2 + uy1 * uy2;
|
|
1296
|
+
const sinHalf = Math.sqrt(Math.max(0, (1 - cosA) / 2));
|
|
1297
|
+
const cos2Half = Math.max(1e-9, (1 + cosA) / 2);
|
|
1298
|
+
const dotAO = bx * (O[0] - a[0]) + by * (O[1] - a[1]);
|
|
1299
|
+
const d = 2 * (dotAO - R * sinHalf) / cos2Half;
|
|
1300
|
+
const K = [a[0] + d * bx, a[1] + d * by];
|
|
1301
|
+
if (which === "center") return K;
|
|
1302
|
+
const kl = Math.hypot(K[0] - O[0], K[1] - O[1]) || 1;
|
|
1303
|
+
return [O[0] + R * (K[0] - O[0]) / kl, O[1] + R * (K[1] - O[1]) / kl];
|
|
1304
|
+
}
|
|
1266
1305
|
function pointAtDistanceCoord(from, through, d) {
|
|
1267
1306
|
const dx = through[0] - from[0];
|
|
1268
1307
|
const dy = through[1] - from[1];
|
|
@@ -1290,29 +1329,38 @@ var init_arcMidpoint = __esm({
|
|
|
1290
1329
|
arcMidpointConstraint = definePointConstraint({
|
|
1291
1330
|
kind: "arcMidpoint",
|
|
1292
1331
|
validate: (c) => {
|
|
1293
|
-
if (!c.circle || !c.a || !c.b
|
|
1294
|
-
throw new Error("point.arcMidpoint: circle, a, b
|
|
1332
|
+
if (!c.circle || !c.a || !c.b) {
|
|
1333
|
+
throw new Error("point.arcMidpoint: circle, a, b b\u1EAFt bu\u1ED9c");
|
|
1334
|
+
}
|
|
1335
|
+
if (c.notContaining && c.containing) {
|
|
1336
|
+
throw new Error("point.arcMidpoint: kh\xF4ng th\u1EC3 v\u1EEBa notContaining v\u1EEBa containing");
|
|
1295
1337
|
}
|
|
1296
1338
|
},
|
|
1297
1339
|
describe: (obj, state, c) => {
|
|
1298
1340
|
const al = state?.objects[c.a]?.label ?? c.a;
|
|
1299
1341
|
const bl = state?.objects[c.b]?.label ?? c.b;
|
|
1300
|
-
const
|
|
1301
|
-
return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl}
|
|
1342
|
+
const ref = c.containing ?? c.notContaining;
|
|
1343
|
+
if (!ref) return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl}`;
|
|
1344
|
+
const rl = state?.objects[ref]?.label ?? ref;
|
|
1345
|
+
const rel = c.containing ? "ch\u1EE9a" : "kh\xF4ng ch\u1EE9a";
|
|
1346
|
+
return `${obj.label} = trung \u0111i\u1EC3m cung ${al}${bl} (${rel} ${rl})`;
|
|
1302
1347
|
},
|
|
1303
1348
|
render: (obj, ctx, c, opts) => {
|
|
1304
1349
|
const board = ctx.jxg;
|
|
1305
1350
|
const circle = ctx.resolveRef(c.circle);
|
|
1306
1351
|
const A = ctx.resolveRef(c.a);
|
|
1307
1352
|
const B = ctx.resolveRef(c.b);
|
|
1308
|
-
const
|
|
1353
|
+
const refName = c.containing ?? c.notContaining;
|
|
1354
|
+
const ref = refName ? ctx.resolveRef(refName) : void 0;
|
|
1355
|
+
const sameSide = !!c.containing;
|
|
1309
1356
|
const O = circle?.center ?? circle?.midpoint ?? circle;
|
|
1310
1357
|
const am = () => arcMidpoint(
|
|
1311
1358
|
[O.X(), O.Y()],
|
|
1312
1359
|
circle.Radius(),
|
|
1313
1360
|
[A.X(), A.Y()],
|
|
1314
1361
|
[B.X(), B.Y()],
|
|
1315
|
-
[
|
|
1362
|
+
ref ? [ref.X(), ref.Y()] : void 0,
|
|
1363
|
+
sameSide
|
|
1316
1364
|
);
|
|
1317
1365
|
return board.create("point", [() => am()[0], () => am()[1]], opts);
|
|
1318
1366
|
}
|
|
@@ -1362,6 +1410,59 @@ var init_excenter = __esm({
|
|
|
1362
1410
|
}
|
|
1363
1411
|
});
|
|
1364
1412
|
|
|
1413
|
+
// src/core/scene/kinds/point-constraints/mixtilinearPoint.ts
|
|
1414
|
+
var mixtilinearPointConstraint;
|
|
1415
|
+
var init_mixtilinearPoint = __esm({
|
|
1416
|
+
"src/core/scene/kinds/point-constraints/mixtilinearPoint.ts"() {
|
|
1417
|
+
init_pointConstructions();
|
|
1418
|
+
init_types2();
|
|
1419
|
+
mixtilinearPointConstraint = definePointConstraint({
|
|
1420
|
+
kind: "mixtilinearPoint",
|
|
1421
|
+
validate: (c) => {
|
|
1422
|
+
if (!Array.isArray(c.vertices) || c.vertices.length !== 3 || !c.vertices.every(Boolean)) {
|
|
1423
|
+
throw new Error("point.mixtilinearPoint: vertices ph\u1EA3i l\xE0 tuple 3 id non-empty");
|
|
1424
|
+
}
|
|
1425
|
+
if (c.which !== "center" && c.which !== "touch") {
|
|
1426
|
+
throw new Error("point.mixtilinearPoint: which ph\u1EA3i 'center' | 'touch'");
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
describe: (obj, state, c) => {
|
|
1430
|
+
const labels = c.vertices.map((id) => state?.objects[id]?.label ?? id).join("");
|
|
1431
|
+
return c.which === "center" ? `${obj.label} = t\xE2m \u0111\u01B0\u1EDDng tr\xF2n mixtilinear \u0394${labels}` : `${obj.label} = ti\u1EBFp \u0111i\u1EC3m mixtilinear \u0394${labels} v\u1EDBi (O)`;
|
|
1432
|
+
},
|
|
1433
|
+
render: (obj, ctx, c, opts) => {
|
|
1434
|
+
const board = ctx.jxg;
|
|
1435
|
+
const a = ctx.resolveRef(c.vertices[0]);
|
|
1436
|
+
const b = ctx.resolveRef(c.vertices[1]);
|
|
1437
|
+
const d = ctx.resolveRef(c.vertices[2]);
|
|
1438
|
+
const mp = () => mixtilinearPoint(
|
|
1439
|
+
[a.X(), a.Y()],
|
|
1440
|
+
[b.X(), b.Y()],
|
|
1441
|
+
[d.X(), d.Y()],
|
|
1442
|
+
c.which
|
|
1443
|
+
);
|
|
1444
|
+
return board.create("point", [() => mp()[0], () => mp()[1]], opts);
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
// src/core/scene/kinds/_label.ts
|
|
1451
|
+
function labelOpts(labelOffset, dflt) {
|
|
1452
|
+
const offset = labelOffset ?? dflt;
|
|
1453
|
+
return { label: { fixed: false, ...offset ? { offset } : {} } };
|
|
1454
|
+
}
|
|
1455
|
+
function readLabelOffset(label) {
|
|
1456
|
+
const off = label.evalVisProp?.("offset") ?? label.visProp?.offset;
|
|
1457
|
+
const rel = label.relativeCoords?.scrCoords;
|
|
1458
|
+
if (!off || !rel || off.length < 2 || rel.length < 3) return null;
|
|
1459
|
+
return [Math.round(off[0] + rel[1]), Math.round(off[1] - rel[2])];
|
|
1460
|
+
}
|
|
1461
|
+
var init_label = __esm({
|
|
1462
|
+
"src/core/scene/kinds/_label.ts"() {
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1365
1466
|
// src/core/scene/kinds/point-constraints/shared.ts
|
|
1366
1467
|
function buildJxgTransforms(board, ctx, t) {
|
|
1367
1468
|
switch (t.kind) {
|
|
@@ -1413,11 +1514,13 @@ function buildPointOpts(obj) {
|
|
|
1413
1514
|
strokeColor: obj.attrs.color ?? "#1e40af",
|
|
1414
1515
|
fillColor: obj.attrs.color ?? "#1e40af",
|
|
1415
1516
|
face: obj.attrs.face ?? "o",
|
|
1416
|
-
size: obj.attrs.size ?? 4
|
|
1517
|
+
size: obj.attrs.size ?? 4,
|
|
1518
|
+
...labelOpts(obj.attrs.labelOffset, [10, 10])
|
|
1417
1519
|
};
|
|
1418
1520
|
}
|
|
1419
1521
|
var init_shared = __esm({
|
|
1420
1522
|
"src/core/scene/kinds/point-constraints/shared.ts"() {
|
|
1523
|
+
init_label();
|
|
1421
1524
|
}
|
|
1422
1525
|
});
|
|
1423
1526
|
|
|
@@ -1726,6 +1829,7 @@ var init_registry2 = __esm({
|
|
|
1726
1829
|
init_centroid();
|
|
1727
1830
|
init_arcMidpoint();
|
|
1728
1831
|
init_excenter();
|
|
1832
|
+
init_mixtilinearPoint();
|
|
1729
1833
|
init_pointAtDistance();
|
|
1730
1834
|
init_circleIntersection();
|
|
1731
1835
|
init_circleSecondIntersection();
|
|
@@ -1751,6 +1855,7 @@ var init_registry2 = __esm({
|
|
|
1751
1855
|
centroidConstraint,
|
|
1752
1856
|
arcMidpointConstraint,
|
|
1753
1857
|
excenterConstraint,
|
|
1858
|
+
mixtilinearPointConstraint,
|
|
1754
1859
|
pointAtDistanceConstraint,
|
|
1755
1860
|
circleIntersectionConstraint,
|
|
1756
1861
|
circleSecondIntersectionConstraint,
|
|
@@ -1775,6 +1880,7 @@ var init_point = __esm({
|
|
|
1775
1880
|
init_d_constraint2();
|
|
1776
1881
|
init_registry2();
|
|
1777
1882
|
init_shared();
|
|
1883
|
+
init_label();
|
|
1778
1884
|
def3 = {
|
|
1779
1885
|
type: "point",
|
|
1780
1886
|
schemaVersion: 1,
|
|
@@ -1842,7 +1948,8 @@ var init_point = __esm({
|
|
|
1842
1948
|
strokeColor: obj.attrs.color ?? "#1e40af",
|
|
1843
1949
|
fillColor: obj.attrs.color ?? "#1e40af",
|
|
1844
1950
|
face: obj.attrs.face ?? "o",
|
|
1845
|
-
size: obj.attrs.size ?? 4
|
|
1951
|
+
size: obj.attrs.size ?? 4,
|
|
1952
|
+
...labelOpts(obj.attrs.labelOffset, [10, 10])
|
|
1846
1953
|
});
|
|
1847
1954
|
} catch {
|
|
1848
1955
|
}
|
|
@@ -1862,6 +1969,7 @@ var init_segment = __esm({
|
|
|
1862
1969
|
"src/core/scene/kinds/segment.ts"() {
|
|
1863
1970
|
init_registry();
|
|
1864
1971
|
init_labelOf();
|
|
1972
|
+
init_label();
|
|
1865
1973
|
def4 = {
|
|
1866
1974
|
type: "segment",
|
|
1867
1975
|
schemaVersion: 1,
|
|
@@ -1893,7 +2001,8 @@ var init_segment = __esm({
|
|
|
1893
2001
|
strokeWidth: obj.attrs.width ?? 2,
|
|
1894
2002
|
dash: obj.attrs.dash ?? 0,
|
|
1895
2003
|
visible: obj.visible,
|
|
1896
|
-
fixed: obj.locked
|
|
2004
|
+
fixed: obj.locked,
|
|
2005
|
+
...labelOpts(obj.attrs.labelOffset)
|
|
1897
2006
|
});
|
|
1898
2007
|
}
|
|
1899
2008
|
};
|
|
@@ -1930,6 +2039,7 @@ var init_line = __esm({
|
|
|
1930
2039
|
"src/core/scene/kinds/line.ts"() {
|
|
1931
2040
|
init_registry();
|
|
1932
2041
|
init_labelOf();
|
|
2042
|
+
init_label();
|
|
1933
2043
|
init_pointConstructions();
|
|
1934
2044
|
def5 = {
|
|
1935
2045
|
type: "line",
|
|
@@ -1972,7 +2082,8 @@ var init_line = __esm({
|
|
|
1972
2082
|
strokeWidth: obj.attrs.width ?? 2,
|
|
1973
2083
|
dash: obj.attrs.dash ?? 0,
|
|
1974
2084
|
visible: obj.visible,
|
|
1975
|
-
fixed: obj.locked
|
|
2085
|
+
fixed: obj.locked,
|
|
2086
|
+
...labelOpts(obj.attrs.labelOffset)
|
|
1976
2087
|
};
|
|
1977
2088
|
const c = obj.attrs.construction;
|
|
1978
2089
|
if (!c) {
|
|
@@ -2228,6 +2339,7 @@ var init_circle = __esm({
|
|
|
2228
2339
|
"src/core/scene/kinds/circle.ts"() {
|
|
2229
2340
|
init_registry();
|
|
2230
2341
|
init_labelOf();
|
|
2342
|
+
init_label();
|
|
2231
2343
|
init_pointConstructions();
|
|
2232
2344
|
def8 = {
|
|
2233
2345
|
type: "circle",
|
|
@@ -2289,15 +2401,17 @@ var init_circle = __esm({
|
|
|
2289
2401
|
const board = ctx.jxg;
|
|
2290
2402
|
const isCenterLabel = (l) => /^[A-Z]['′]?\d*$/u.test(l);
|
|
2291
2403
|
const isCenter = isCenterLabel(obj.label);
|
|
2404
|
+
const isRenamedCircle = /_c$/.test(obj.label);
|
|
2292
2405
|
const baseOpts = {
|
|
2293
2406
|
name: obj.label,
|
|
2294
|
-
withLabel: isCenter ? obj.attrs.showLabel ?? false : true,
|
|
2407
|
+
withLabel: isCenter ? obj.attrs.showLabel ?? false : isRenamedCircle ? false : true,
|
|
2295
2408
|
strokeColor: obj.attrs.color ?? "#0f172a",
|
|
2296
2409
|
strokeWidth: obj.attrs.width ?? 2,
|
|
2297
2410
|
dash: obj.attrs.dash ?? 0,
|
|
2298
2411
|
fillColor: "none",
|
|
2299
2412
|
visible: obj.visible,
|
|
2300
|
-
fixed: obj.locked
|
|
2413
|
+
fixed: obj.locked,
|
|
2414
|
+
...labelOpts(obj.attrs.labelOffset)
|
|
2301
2415
|
};
|
|
2302
2416
|
const c = asConstruction(obj.attrs);
|
|
2303
2417
|
if (c?.kind === "circumscribed") {
|
|
@@ -2782,8 +2896,11 @@ var init_intersection = __esm({
|
|
|
2782
2896
|
const opts = {
|
|
2783
2897
|
name: obj.label,
|
|
2784
2898
|
withLabel: true,
|
|
2785
|
-
|
|
2786
|
-
|
|
2899
|
+
// Cùng màu xanh với mọi điểm khác (buildPointOpts dùng '#1e40af').
|
|
2900
|
+
// Trước đây điểm giao tô đỏ '#dc2626' → tách biệt thị giác nhưng gây
|
|
2901
|
+
// khó hiểu ("vì sao điểm này khác màu?"). Giữ chung một màu.
|
|
2902
|
+
strokeColor: obj.attrs.color ?? "#1e40af",
|
|
2903
|
+
fillColor: obj.attrs.color ?? "#1e40af",
|
|
2787
2904
|
visible: obj.visible,
|
|
2788
2905
|
fixed: obj.locked
|
|
2789
2906
|
};
|
|
@@ -2792,6 +2909,38 @@ var init_intersection = __esm({
|
|
|
2792
2909
|
}
|
|
2793
2910
|
const branch = obj.attrs.branch ?? 0;
|
|
2794
2911
|
return board.create("intersection", [a, b, branch], opts);
|
|
2912
|
+
},
|
|
2913
|
+
/**
|
|
2914
|
+
* Cập nhật TẠI CHỖ các thuộc tính "trang trí" (tên/màu/ẩn-hiện/khoá) qua
|
|
2915
|
+
* setAttribute — giữ nguyên JxgObj identity nên các object phụ thuộc điểm
|
|
2916
|
+
* giao (đường thẳng qua nó, …) KHÔNG bị stale parent ref. Mô phỏng update
|
|
2917
|
+
* hook của point.ts.
|
|
2918
|
+
*
|
|
2919
|
+
* Nếu định nghĩa hình học đổi (kind/ref1/ref2/branch) thì throw → renderer
|
|
2920
|
+
* fallback remove + create để JSXGraph dựng lại phép giao đúng.
|
|
2921
|
+
*/
|
|
2922
|
+
update: (obj, prev, ctx, existing) => {
|
|
2923
|
+
const a = obj.attrs;
|
|
2924
|
+
const p = prev.attrs;
|
|
2925
|
+
const branchA = a.branch;
|
|
2926
|
+
const branchP = p.branch;
|
|
2927
|
+
if (a.kind !== p.kind || a.ref1 !== p.ref1 || a.ref2 !== p.ref2 || branchA !== branchP) {
|
|
2928
|
+
throw new Error("intersection: \u0111\u1ECBnh ngh\u0129a h\xECnh h\u1ECDc \u0111\u1ED5i \u2014 recreate");
|
|
2929
|
+
}
|
|
2930
|
+
const el = existing;
|
|
2931
|
+
if (typeof el.setAttribute === "function") {
|
|
2932
|
+
try {
|
|
2933
|
+
el.setAttribute({
|
|
2934
|
+
name: obj.label,
|
|
2935
|
+
withLabel: true,
|
|
2936
|
+
strokeColor: a.color ?? "#1e40af",
|
|
2937
|
+
fillColor: a.color ?? "#1e40af",
|
|
2938
|
+
visible: obj.visible,
|
|
2939
|
+
fixed: obj.locked
|
|
2940
|
+
});
|
|
2941
|
+
} catch {
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2795
2944
|
}
|
|
2796
2945
|
};
|
|
2797
2946
|
registerKind(def12);
|
|
@@ -3478,6 +3627,7 @@ var init_JxgRenderer = __esm({
|
|
|
3478
3627
|
init_registry();
|
|
3479
3628
|
init_types2d();
|
|
3480
3629
|
init_parser();
|
|
3630
|
+
init_label();
|
|
3481
3631
|
JxgRenderer = class {
|
|
3482
3632
|
constructor(store, board, options = {}) {
|
|
3483
3633
|
this.elements = /* @__PURE__ */ new Map();
|
|
@@ -3546,6 +3696,7 @@ var init_JxgRenderer = __esm({
|
|
|
3546
3696
|
this.elements.set(obj.id, el);
|
|
3547
3697
|
this.attachFreePointDragSync(obj, el);
|
|
3548
3698
|
this.attachGliderDragSync(obj, el);
|
|
3699
|
+
this.attachLabelDragSync(obj, el);
|
|
3549
3700
|
} catch (err) {
|
|
3550
3701
|
console.warn(`[scene/render/2d] kh\xF4ng render \u0111\u01B0\u1EE3c ${obj.kind} id="${obj.id}":`, err);
|
|
3551
3702
|
}
|
|
@@ -3660,6 +3811,54 @@ var init_JxgRenderer = __esm({
|
|
|
3660
3811
|
});
|
|
3661
3812
|
});
|
|
3662
3813
|
}
|
|
3814
|
+
/**
|
|
3815
|
+
* Cho phép kéo NHÃN của bất kỳ object nào có label (point/line/segment/
|
|
3816
|
+
* circle...). Khi user kéo nhãn, JSXGraph cộng dồn vào label.relativeCoords
|
|
3817
|
+
* (drag-delta screen px) nhưng KHÔNG đổi attribute offset → vị trí cuối =
|
|
3818
|
+
* offset + relativeCoords. Ta gộp lại thành offset thuần (readLabelOffset),
|
|
3819
|
+
* zero relativeCoords để khỏi double-count khi update-hook/recreate áp lại
|
|
3820
|
+
* offset, rồi dispatch UPDATE_ATTRS { labelOffset }. Right-click → reset.
|
|
3821
|
+
*/
|
|
3822
|
+
attachLabelDragSync(obj, el) {
|
|
3823
|
+
const label = el?.label;
|
|
3824
|
+
if (!label || typeof label.on !== "function") return;
|
|
3825
|
+
const sceneId = obj.id;
|
|
3826
|
+
label.on("up", () => {
|
|
3827
|
+
if (this.disposed) return;
|
|
3828
|
+
const off = readLabelOffset(label);
|
|
3829
|
+
if (!off) return;
|
|
3830
|
+
const cur = this.store.getState().objects[sceneId];
|
|
3831
|
+
if (!cur) return;
|
|
3832
|
+
const prev = cur.attrs.labelOffset;
|
|
3833
|
+
if (prev && prev[0] === off[0] && prev[1] === off[1]) return;
|
|
3834
|
+
try {
|
|
3835
|
+
label.setAttribute({ offset: off });
|
|
3836
|
+
if (label.relativeCoords?.scrCoords) {
|
|
3837
|
+
label.relativeCoords.scrCoords[1] = 0;
|
|
3838
|
+
label.relativeCoords.scrCoords[2] = 0;
|
|
3839
|
+
}
|
|
3840
|
+
} catch {
|
|
3841
|
+
}
|
|
3842
|
+
this.store.dispatch({
|
|
3843
|
+
type: "UPDATE_ATTRS",
|
|
3844
|
+
payload: { id: sceneId, patch: { labelOffset: off } }
|
|
3845
|
+
});
|
|
3846
|
+
});
|
|
3847
|
+
const node = label.rendNode;
|
|
3848
|
+
if (node?.addEventListener) {
|
|
3849
|
+
node.addEventListener("contextmenu", (ev) => {
|
|
3850
|
+
if (this.disposed) return;
|
|
3851
|
+
ev.preventDefault();
|
|
3852
|
+
const c = this.store.getState().objects[sceneId];
|
|
3853
|
+
if (!c) return;
|
|
3854
|
+
if (c.attrs.labelOffset === void 0) return;
|
|
3855
|
+
this.store.dispatch({
|
|
3856
|
+
type: "UPDATE_ATTRS",
|
|
3857
|
+
payload: { id: sceneId, patch: { labelOffset: void 0 } }
|
|
3858
|
+
});
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3663
3862
|
remove(id) {
|
|
3664
3863
|
this.removeHalo(id);
|
|
3665
3864
|
this.selectedIds.delete(id);
|
|
@@ -3796,88 +3995,89 @@ var init_JxgRenderer = __esm({
|
|
|
3796
3995
|
layer: 4,
|
|
3797
3996
|
needsRegularUpdate: true
|
|
3798
3997
|
};
|
|
3998
|
+
const elementClass = el.elementClass;
|
|
3999
|
+
const elType = el.elType;
|
|
4000
|
+
const isPointLike = elementClass === 1 || elType === "point" || elType === "glider" || elType === "intersection";
|
|
4001
|
+
const isPolygonLike = Array.isArray(el.vertices) && el.vertices.length >= 3;
|
|
4002
|
+
const isCircleLike = elementClass === 3 || elType === "circle" || elType === "circumcircle" || elType === "incircle";
|
|
4003
|
+
const isSegment = elType === "segment";
|
|
4004
|
+
const isLineLike = elementClass === 2 || elType === "line" || elType === "arrow" || elType === "ray" || elType === "vector" || elType === "tangent" || elType === "normal" || elType === "parallel" || elType === "perpendicular" || elType === "bisector";
|
|
3799
4005
|
const halos = [];
|
|
3800
4006
|
try {
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
});
|
|
3828
|
-
halos.push(halo);
|
|
3829
|
-
}
|
|
3830
|
-
break;
|
|
3831
|
-
}
|
|
3832
|
-
case "line":
|
|
3833
|
-
case "arrow":
|
|
3834
|
-
case "ray":
|
|
3835
|
-
case "vector":
|
|
3836
|
-
case "tangent":
|
|
3837
|
-
case "normal":
|
|
3838
|
-
case "parallel":
|
|
3839
|
-
case "perpendicular":
|
|
3840
|
-
case "bisector": {
|
|
3841
|
-
if (el.point1 && el.point2) {
|
|
3842
|
-
const halo = board.create("line", [el.point1, el.point2], {
|
|
3843
|
-
...haloBase,
|
|
3844
|
-
strokeWidth: 9
|
|
3845
|
-
});
|
|
3846
|
-
halos.push(halo);
|
|
3847
|
-
}
|
|
3848
|
-
break;
|
|
3849
|
-
}
|
|
3850
|
-
case "circle": {
|
|
3851
|
-
if (el.center && typeof el.Radius === "function") {
|
|
3852
|
-
const halo = board.create("circle", [el.center, () => el.Radius?.() ?? 0], {
|
|
3853
|
-
...haloBase,
|
|
3854
|
-
strokeWidth: 9,
|
|
3855
|
-
fillOpacity: 0
|
|
3856
|
-
});
|
|
3857
|
-
halos.push(halo);
|
|
3858
|
-
}
|
|
3859
|
-
break;
|
|
3860
|
-
}
|
|
3861
|
-
case "polygon": {
|
|
3862
|
-
if (Array.isArray(el.vertices) && el.vertices.length >= 3) {
|
|
3863
|
-
const last = el.vertices.length - 1;
|
|
3864
|
-
const verts = el.vertices[last] === el.vertices[0] ? el.vertices.slice(0, last) : el.vertices.slice();
|
|
3865
|
-
const halo = board.create("polygon", verts, {
|
|
3866
|
-
...haloBase,
|
|
3867
|
-
fillOpacity: 0.2,
|
|
3868
|
-
borders: {
|
|
3869
|
-
strokeColor: SEL_STROKE,
|
|
3870
|
-
strokeWidth: 7,
|
|
3871
|
-
strokeOpacity: 0.55,
|
|
3872
|
-
highlight: false
|
|
3873
|
-
}
|
|
3874
|
-
});
|
|
3875
|
-
halos.push(halo);
|
|
4007
|
+
if (isPointLike) {
|
|
4008
|
+
const baseSize = el.getAttribute?.("size") ?? 4;
|
|
4009
|
+
const halo = board.create("point", [
|
|
4010
|
+
() => el.X?.() ?? 0,
|
|
4011
|
+
() => el.Y?.() ?? 0
|
|
4012
|
+
], {
|
|
4013
|
+
...haloBase,
|
|
4014
|
+
size: baseSize + 6,
|
|
4015
|
+
face: "o",
|
|
4016
|
+
strokeWidth: 2,
|
|
4017
|
+
strokeOpacity: 0.75,
|
|
4018
|
+
fillOpacity: 0.25
|
|
4019
|
+
});
|
|
4020
|
+
halos.push(halo);
|
|
4021
|
+
} else if (isPolygonLike) {
|
|
4022
|
+
const verts = el.vertices;
|
|
4023
|
+
const last = verts.length - 1;
|
|
4024
|
+
const trimmed = verts[last] === verts[0] ? verts.slice(0, last) : verts.slice();
|
|
4025
|
+
const halo = board.create("polygon", trimmed, {
|
|
4026
|
+
...haloBase,
|
|
4027
|
+
fillOpacity: 0.2,
|
|
4028
|
+
borders: {
|
|
4029
|
+
strokeColor: SEL_STROKE,
|
|
4030
|
+
strokeWidth: 7,
|
|
4031
|
+
strokeOpacity: 0.55,
|
|
4032
|
+
highlight: false
|
|
3876
4033
|
}
|
|
3877
|
-
|
|
4034
|
+
});
|
|
4035
|
+
halos.push(halo);
|
|
4036
|
+
} else if (isCircleLike && el.center && typeof el.Radius === "function") {
|
|
4037
|
+
const halo = board.create("circle", [el.center, () => el.Radius?.() ?? 0], {
|
|
4038
|
+
...haloBase,
|
|
4039
|
+
strokeWidth: 9,
|
|
4040
|
+
fillOpacity: 0
|
|
4041
|
+
});
|
|
4042
|
+
halos.push(halo);
|
|
4043
|
+
} else if (isSegment && el.point1 && el.point2) {
|
|
4044
|
+
const halo = board.create("segment", [el.point1, el.point2], {
|
|
4045
|
+
...haloBase,
|
|
4046
|
+
strokeWidth: 9,
|
|
4047
|
+
straightFirst: false,
|
|
4048
|
+
straightLast: false
|
|
4049
|
+
});
|
|
4050
|
+
halos.push(halo);
|
|
4051
|
+
} else if (isLineLike && el.point1 && el.point2) {
|
|
4052
|
+
const halo = board.create("line", [el.point1, el.point2], {
|
|
4053
|
+
...haloBase,
|
|
4054
|
+
strokeWidth: 9
|
|
4055
|
+
});
|
|
4056
|
+
halos.push(halo);
|
|
4057
|
+
} else if (el.center && (el.radiuspoint ?? el.radiusPoint) && (el.anglepoint ?? el.anglePoint) && (elType === "arc" || elType === "semicircle" || elType === "circumcirclearc" || elType === "sector" || elType === "angle")) {
|
|
4058
|
+
const rp = el.radiuspoint ?? el.radiusPoint;
|
|
4059
|
+
const ap = el.anglepoint ?? el.anglePoint;
|
|
4060
|
+
if (elType === "sector") {
|
|
4061
|
+
halos.push(board.create("sector", [el.center, rp, ap], {
|
|
4062
|
+
...haloBase,
|
|
4063
|
+
strokeWidth: 7,
|
|
4064
|
+
fillOpacity: 0.2
|
|
4065
|
+
}));
|
|
4066
|
+
} else if (elType === "angle") {
|
|
4067
|
+
halos.push(board.create("angle", [rp, el.center, ap], {
|
|
4068
|
+
...haloBase,
|
|
4069
|
+
strokeWidth: 7,
|
|
4070
|
+
fillOpacity: 0.2,
|
|
4071
|
+
radius: el.getAttribute?.("radius") ?? 1
|
|
4072
|
+
}));
|
|
4073
|
+
} else {
|
|
4074
|
+
halos.push(board.create("arc", [el.center, rp, ap], {
|
|
4075
|
+
...haloBase,
|
|
4076
|
+
strokeWidth: 9,
|
|
4077
|
+
fillColor: "none",
|
|
4078
|
+
fillOpacity: 0
|
|
4079
|
+
}));
|
|
3878
4080
|
}
|
|
3879
|
-
default:
|
|
3880
|
-
break;
|
|
3881
4081
|
}
|
|
3882
4082
|
} catch (err) {
|
|
3883
4083
|
console.warn("[scene/render/2d] halo create fail:", err);
|
|
@@ -4037,6 +4237,49 @@ var init_autoFitBbox = __esm({
|
|
|
4037
4237
|
}
|
|
4038
4238
|
});
|
|
4039
4239
|
|
|
4240
|
+
// src/stamps/geometry-2d/autoFitBoard.ts
|
|
4241
|
+
function isDefaultBbox(bbox) {
|
|
4242
|
+
const d = DEFAULT_VIEW_2D.bbox;
|
|
4243
|
+
return bbox.length === 4 && bbox[0] === d[0] && bbox[1] === d[1] && bbox[2] === d[2] && bbox[3] === d[3];
|
|
4244
|
+
}
|
|
4245
|
+
function fittedBboxFromBoard(board, aspect) {
|
|
4246
|
+
const objs = board?.objectsList;
|
|
4247
|
+
if (!Array.isArray(objs)) return null;
|
|
4248
|
+
const points = [];
|
|
4249
|
+
const circles = [];
|
|
4250
|
+
for (const raw of objs) {
|
|
4251
|
+
const o = raw;
|
|
4252
|
+
if (o?.elementClass === 1 && typeof o.X === "function" && typeof o.Y === "function") {
|
|
4253
|
+
const x = o.X(), y = o.Y();
|
|
4254
|
+
if (Number.isFinite(x) && Number.isFinite(y)) points.push([x, y]);
|
|
4255
|
+
} else if (o?.elementClass === 3 && o.center?.X && typeof o.Radius === "function") {
|
|
4256
|
+
const cx = o.center.X(), cy = o.center.Y(), r = o.Radius();
|
|
4257
|
+
if (Number.isFinite(cx) && Number.isFinite(cy) && Number.isFinite(r)) {
|
|
4258
|
+
circles.push({ cx, cy, r });
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
const safeAspect = Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
|
|
4263
|
+
return computeAutoFitBbox(points, circles, safeAspect);
|
|
4264
|
+
}
|
|
4265
|
+
function autoFitBoardToContent(board, aspect) {
|
|
4266
|
+
const bbox = fittedBboxFromBoard(board, aspect);
|
|
4267
|
+
if (!bbox) return;
|
|
4268
|
+
const b = board;
|
|
4269
|
+
try {
|
|
4270
|
+
b.setBoundingBox?.(bbox);
|
|
4271
|
+
if (typeof b.update === "function") b.update();
|
|
4272
|
+
if (typeof b.fullUpdate === "function") b.fullUpdate();
|
|
4273
|
+
} catch {
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
var init_autoFitBoard = __esm({
|
|
4277
|
+
"src/stamps/geometry-2d/autoFitBoard.ts"() {
|
|
4278
|
+
init_types();
|
|
4279
|
+
init_autoFitBbox();
|
|
4280
|
+
}
|
|
4281
|
+
});
|
|
4282
|
+
|
|
4040
4283
|
// src/stamps/geometry-2d/labelLayout.ts
|
|
4041
4284
|
function radialLabelOffsets(points, R = 14) {
|
|
4042
4285
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -4071,8 +4314,9 @@ function containerDimsForBbox(bbox) {
|
|
|
4071
4314
|
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
|
|
4072
4315
|
return { width: FALLBACK_W, height: FALLBACK_H };
|
|
4073
4316
|
}
|
|
4074
|
-
|
|
4075
|
-
let
|
|
4317
|
+
const pxPerUnit = DEFAULT_VIEW_PX / Math.max(w, h);
|
|
4318
|
+
let width = w * pxPerUnit;
|
|
4319
|
+
let height = h * pxPerUnit;
|
|
4076
4320
|
const maxAxis = Math.max(width, height);
|
|
4077
4321
|
if (maxAxis > MAX_DIM) {
|
|
4078
4322
|
const ratio = MAX_DIM / maxAxis;
|
|
@@ -4087,31 +4331,6 @@ function containerDimsForBbox(bbox) {
|
|
|
4087
4331
|
}
|
|
4088
4332
|
return { width: Math.round(width), height: Math.round(height) };
|
|
4089
4333
|
}
|
|
4090
|
-
function autoFitBboxFromBoard(board, aspect) {
|
|
4091
|
-
const objs = board?.objectsList;
|
|
4092
|
-
if (!Array.isArray(objs)) return;
|
|
4093
|
-
const points = [];
|
|
4094
|
-
const circles = [];
|
|
4095
|
-
for (const o of objs) {
|
|
4096
|
-
if (o?.elementClass === 1 && typeof o.X === "function" && typeof o.Y === "function") {
|
|
4097
|
-
const x = o.X(), y = o.Y();
|
|
4098
|
-
if (Number.isFinite(x) && Number.isFinite(y)) points.push([x, y]);
|
|
4099
|
-
} else if (o?.elementClass === 3 && o.center?.X && typeof o.Radius === "function") {
|
|
4100
|
-
const cx = o.center.X(), cy = o.center.Y(), r = o.Radius();
|
|
4101
|
-
if (Number.isFinite(cx) && Number.isFinite(cy) && Number.isFinite(r)) {
|
|
4102
|
-
circles.push({ cx, cy, r });
|
|
4103
|
-
}
|
|
4104
|
-
}
|
|
4105
|
-
}
|
|
4106
|
-
const bbox = computeAutoFitBbox(points, circles, aspect);
|
|
4107
|
-
if (!bbox) return;
|
|
4108
|
-
try {
|
|
4109
|
-
board.setBoundingBox(bbox);
|
|
4110
|
-
if (typeof board.update === "function") board.update();
|
|
4111
|
-
if (typeof board.fullUpdate === "function") board.fullUpdate();
|
|
4112
|
-
} catch {
|
|
4113
|
-
}
|
|
4114
|
-
}
|
|
4115
4334
|
function applyRadialLabelOffsets(board) {
|
|
4116
4335
|
const objs = board?.objectsList;
|
|
4117
4336
|
if (!Array.isArray(objs)) return;
|
|
@@ -4137,10 +4356,6 @@ function applyRadialLabelOffsets(board) {
|
|
|
4137
4356
|
} catch {
|
|
4138
4357
|
}
|
|
4139
4358
|
}
|
|
4140
|
-
function isDefaultBbox(bbox) {
|
|
4141
|
-
const d = DEFAULT_VIEW_2D.bbox;
|
|
4142
|
-
return bbox.length === 4 && bbox[0] === d[0] && bbox[1] === d[1] && bbox[2] === d[2] && bbox[3] === d[3];
|
|
4143
|
-
}
|
|
4144
4359
|
async function renderGeometrySvgFromState(jsonState) {
|
|
4145
4360
|
const state = deserializeBoard(jsonState);
|
|
4146
4361
|
const view = state.meta.domain === "2d" ? state.meta.view : DEFAULT_VIEW_2D;
|
|
@@ -4175,7 +4390,7 @@ async function renderGeometrySvgFromState(jsonState) {
|
|
|
4175
4390
|
const store = createStore(state);
|
|
4176
4391
|
const renderer = new JxgRenderer(store, board);
|
|
4177
4392
|
if (shouldAutoFit) {
|
|
4178
|
-
|
|
4393
|
+
autoFitBoardToContent(board, dims.width / dims.height);
|
|
4179
4394
|
applyRadialLabelOffsets(board);
|
|
4180
4395
|
}
|
|
4181
4396
|
return renderer;
|
|
@@ -4183,7 +4398,7 @@ async function renderGeometrySvgFromState(jsonState) {
|
|
|
4183
4398
|
});
|
|
4184
4399
|
return svgString;
|
|
4185
4400
|
}
|
|
4186
|
-
var
|
|
4401
|
+
var DEFAULT_VIEW_PX, MIN_DIM, MAX_DIM, FALLBACK_W, FALLBACK_H;
|
|
4187
4402
|
var init_render = __esm({
|
|
4188
4403
|
"src/stamps/geometry-2d/render.ts"() {
|
|
4189
4404
|
init_serialize();
|
|
@@ -4192,9 +4407,9 @@ var init_render = __esm({
|
|
|
4192
4407
|
init_types();
|
|
4193
4408
|
init_JxgRenderer();
|
|
4194
4409
|
init_jxgOffscreenRender();
|
|
4195
|
-
|
|
4410
|
+
init_autoFitBoard();
|
|
4196
4411
|
init_labelLayout();
|
|
4197
|
-
|
|
4412
|
+
DEFAULT_VIEW_PX = 500;
|
|
4198
4413
|
MIN_DIM = 100;
|
|
4199
4414
|
MAX_DIM = 1200;
|
|
4200
4415
|
FALLBACK_W = 400;
|
|
@@ -5754,41 +5969,48 @@ var init_icons = __esm({
|
|
|
5754
5969
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "4", r: "1.7", fill: C_POINT })
|
|
5755
5970
|
] }),
|
|
5756
5971
|
// ===== Nâng cao / kind chưa có icon =====
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
5972
|
+
// Hệ màu đồng bộ: XANH = input click sẵn, ĐỎ = output suy ra. Toạ độ tính để
|
|
5973
|
+
// điểm nằm ĐÚNG trên đường tròn/cung/tiếp tuyến (không trôi nổi).
|
|
5974
|
+
excenter: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5975
|
+
/* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12,3 15,15 9,15", stroke: "currentColor", strokeWidth: "1.4", fill: "none" }),
|
|
5976
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "15", x2: "8", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
|
|
5977
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "15", y1: "15", x2: "16", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
|
|
5978
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "3.8", stroke: "currentColor", strokeWidth: "1.3" }),
|
|
5979
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "3", r: "1.6", fill: C_POINT }),
|
|
5980
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "15", r: "1.6", fill: C_POINT }),
|
|
5981
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "15", r: "1.6", fill: C_POINT }),
|
|
5982
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "2.2", fill: C_CONSTRUCT })
|
|
5761
5983
|
] }),
|
|
5762
|
-
tangencyPoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
|
|
5763
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "
|
|
5764
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16.5", y1: "3", x2: "16.5", y2: "
|
|
5765
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16.5", cy: "12", r: "2.
|
|
5984
|
+
tangencyPoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
|
|
5985
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.5", cy: "12", r: "7", stroke: "currentColor", strokeWidth: "1.4" }),
|
|
5986
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16.5", y1: "3.5", x2: "16.5", y2: "20.5", stroke: "currentColor", strokeWidth: "1.4" }),
|
|
5987
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "16.5", cy: "12", r: "2.3", fill: C_CONSTRUCT })
|
|
5766
5988
|
] }),
|
|
5767
|
-
secondIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
|
|
5768
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.
|
|
5769
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", y1: "8", x2: "
|
|
5770
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "
|
|
5771
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "
|
|
5989
|
+
secondIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
|
|
5990
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "11", cy: "12", r: "6.5", stroke: "currentColor", strokeWidth: "1.4" }),
|
|
5991
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2.4", y1: "8.2", x2: "20.4", y2: "11.4", stroke: "currentColor", strokeWidth: "1.4" }),
|
|
5992
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5.37", cy: "8.75", r: "1.8", fill: C_POINT }),
|
|
5993
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.4", cy: "10.87", r: "2.4", fill: C_CONSTRUCT })
|
|
5772
5994
|
] }),
|
|
5773
5995
|
arcMidpoint: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
|
|
5774
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 17 A
|
|
5775
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "17", r: "1.
|
|
5776
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "17", r: "1.
|
|
5777
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "
|
|
5996
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 17 A 8.5 8.5 0 0 1 20 17", stroke: "currentColor", strokeWidth: "1.6", fill: "none" }),
|
|
5997
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "17", r: "1.8", fill: C_POINT }),
|
|
5998
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "17", r: "1.8", fill: C_POINT }),
|
|
5999
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "11.4", r: "2.3", fill: C_CONSTRUCT })
|
|
5778
6000
|
] }),
|
|
5779
6001
|
circleIntersection: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
|
|
5780
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.
|
|
5781
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.
|
|
5782
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "
|
|
5783
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "
|
|
6002
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
|
|
6003
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
|
|
6004
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "6.8", r: "2", fill: C_CONSTRUCT }),
|
|
6005
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "17.2", r: "2", fill: C_CONSTRUCT })
|
|
5784
6006
|
] }),
|
|
5785
6007
|
tangentPointExt: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
|
|
5786
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "
|
|
5787
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
5788
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3
|
|
5789
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
5790
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "
|
|
5791
|
-
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "
|
|
6008
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "13", cy: "12", r: "6", stroke: "currentColor", strokeWidth: "1.4" }),
|
|
6009
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "11.8", y2: "18.6", stroke: "currentColor", strokeWidth: "1.3" }),
|
|
6010
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "11.8", y2: "5.4", stroke: "currentColor", strokeWidth: "1.3" }),
|
|
6011
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "3", cy: "12", r: "2", fill: C_POINT }),
|
|
6012
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.4", cy: "16.8", r: "1.8", fill: C_CONSTRUCT }),
|
|
6013
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9.4", cy: "7.2", r: "1.8", fill: C_CONSTRUCT })
|
|
5792
6014
|
] }),
|
|
5793
6015
|
circleCR: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
|
|
5794
6016
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
|
|
@@ -5799,9 +6021,11 @@ var init_icons = __esm({
|
|
|
5799
6021
|
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 19 L12 4 L21 19 Z", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }),
|
|
5800
6022
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "14", r: "4.2", fill: "none", stroke: C_CONSTRUCT, strokeWidth: "1.3" })
|
|
5801
6023
|
] }),
|
|
5802
|
-
excircle: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
|
|
5803
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
5804
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
6024
|
+
excircle: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
6025
|
+
/* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12,3 15,15 9,15", stroke: "currentColor", strokeWidth: "1.4", fill: "none" }),
|
|
6026
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "15", x2: "8", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
|
|
6027
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "15", y1: "15", x2: "16", y2: "18.9", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.6 1.6" }),
|
|
6028
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "18.9", r: "3.8", stroke: C_CONSTRUCT, strokeWidth: "1.5", fill: C_CONSTRUCT, fillOpacity: "0.12" })
|
|
5805
6029
|
] }),
|
|
5806
6030
|
pointOn: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
|
|
5807
6031
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "7.5", stroke: "currentColor", strokeWidth: "1.3" }),
|
|
@@ -7811,11 +8035,11 @@ function buildObjectSnapshot(state, id, anchorScreen) {
|
|
|
7811
8035
|
const obj = state.objects[id];
|
|
7812
8036
|
if (!obj) return null;
|
|
7813
8037
|
const k = obj.kind;
|
|
7814
|
-
if (k !== "point" && k !== "line" && k !== "circle" && k !== "segment" && k !== "ray" && k !== "vector") {
|
|
8038
|
+
if (k !== "point" && k !== "line" && k !== "circle" && k !== "segment" && k !== "ray" && k !== "vector" && k !== "intersection") {
|
|
7815
8039
|
return null;
|
|
7816
8040
|
}
|
|
7817
8041
|
const a = obj.attrs;
|
|
7818
|
-
const jKind = k === "point" ? "point" : k === "circle" ? "circle" : "line";
|
|
8042
|
+
const jKind = k === "point" || k === "intersection" ? "point" : k === "circle" ? "circle" : "line";
|
|
7819
8043
|
return {
|
|
7820
8044
|
id,
|
|
7821
8045
|
kind: jKind,
|
|
@@ -8223,6 +8447,7 @@ var init_MiniBoard = __esm({
|
|
|
8223
8447
|
init_safeJsx();
|
|
8224
8448
|
init_attachJxgWheelZoom();
|
|
8225
8449
|
init_initJxgBoard();
|
|
8450
|
+
init_autoFitBoard();
|
|
8226
8451
|
init_snapshot();
|
|
8227
8452
|
init_previewActions();
|
|
8228
8453
|
init_buildHandle();
|
|
@@ -8462,6 +8687,11 @@ var init_MiniBoard = __esm({
|
|
|
8462
8687
|
rendererRef.current = new JxgRenderer(store, board, {
|
|
8463
8688
|
theme: paletteFor(isDarkRef.current)
|
|
8464
8689
|
});
|
|
8690
|
+
if (isDefaultBbox(initialView.bbox)) {
|
|
8691
|
+
const el = containerRef.current;
|
|
8692
|
+
const aspect = el && el.clientHeight > 0 ? el.clientWidth / el.clientHeight : 1;
|
|
8693
|
+
safeJsx("MiniBoard.autoFit", () => autoFitBoardToContent(board, aspect));
|
|
8694
|
+
}
|
|
8465
8695
|
if (containerRef.current) {
|
|
8466
8696
|
wheelCleanup = attachJxgWheelZoom(containerRef.current, board, "MiniBoard.2d");
|
|
8467
8697
|
}
|
|
@@ -9259,6 +9489,7 @@ function useAiFigure(generator) {
|
|
|
9259
9489
|
const [prompt, setPrompt] = React2.useState("");
|
|
9260
9490
|
const [isLoading, setIsLoading] = React2.useState(false);
|
|
9261
9491
|
const [error, setError] = React2.useState(null);
|
|
9492
|
+
const [notice, setNotice] = React2.useState(null);
|
|
9262
9493
|
const [tokens, setTokens] = React2.useState(0);
|
|
9263
9494
|
const abortRef = React2.useRef(null);
|
|
9264
9495
|
const requestIdRef = React2.useRef(0);
|
|
@@ -9279,6 +9510,7 @@ function useAiFigure(generator) {
|
|
|
9279
9510
|
abortRef.current = controller;
|
|
9280
9511
|
setIsLoading(true);
|
|
9281
9512
|
setError(null);
|
|
9513
|
+
setNotice(null);
|
|
9282
9514
|
setTokens(0);
|
|
9283
9515
|
try {
|
|
9284
9516
|
const generated = await generator(problem, {
|
|
@@ -9292,6 +9524,7 @@ function useAiFigure(generator) {
|
|
|
9292
9524
|
setError(generated.message);
|
|
9293
9525
|
return null;
|
|
9294
9526
|
}
|
|
9527
|
+
if (generated.partial) setNotice(generated.partial.message);
|
|
9295
9528
|
return generated.state;
|
|
9296
9529
|
} catch (caught) {
|
|
9297
9530
|
if (controller.signal.aborted || caught instanceof DOMException && caught.name === "AbortError") {
|
|
@@ -9318,6 +9551,7 @@ function useAiFigure(generator) {
|
|
|
9318
9551
|
setPrompt,
|
|
9319
9552
|
isLoading,
|
|
9320
9553
|
error,
|
|
9554
|
+
notice,
|
|
9321
9555
|
submit,
|
|
9322
9556
|
cancel,
|
|
9323
9557
|
tokens
|
|
@@ -9328,12 +9562,207 @@ var init_useAiFigure = __esm({
|
|
|
9328
9562
|
"use client";
|
|
9329
9563
|
}
|
|
9330
9564
|
});
|
|
9565
|
+
|
|
9566
|
+
// src/stamps/geometry-2d/ai/vision/tesseract.ts
|
|
9567
|
+
async function runTesseractOcr(image, opts = {}) {
|
|
9568
|
+
if (opts.signal?.aborted) {
|
|
9569
|
+
const err = new Error("Tesseract OCR aborted");
|
|
9570
|
+
err.name = "AbortError";
|
|
9571
|
+
throw err;
|
|
9572
|
+
}
|
|
9573
|
+
const { createWorker } = await import('tesseract.js');
|
|
9574
|
+
const lang = opts.lang ?? DEFAULT_LANG;
|
|
9575
|
+
const worker = await createWorker(lang);
|
|
9576
|
+
try {
|
|
9577
|
+
const dataUrl = `data:${image.mediaType};base64,${image.base64}`;
|
|
9578
|
+
const { data } = await worker.recognize(dataUrl);
|
|
9579
|
+
return { text: data.text, confidence: data.confidence };
|
|
9580
|
+
} finally {
|
|
9581
|
+
await worker.terminate();
|
|
9582
|
+
}
|
|
9583
|
+
}
|
|
9584
|
+
var DEFAULT_LANG;
|
|
9585
|
+
var init_tesseract = __esm({
|
|
9586
|
+
"src/stamps/geometry-2d/ai/vision/tesseract.ts"() {
|
|
9587
|
+
DEFAULT_LANG = "vie+eng";
|
|
9588
|
+
}
|
|
9589
|
+
});
|
|
9590
|
+
|
|
9591
|
+
// src/stamps/geometry-2d/ai/vision/extractProblem.ts
|
|
9592
|
+
async function extractProblemFromImage(image, opts = {}) {
|
|
9593
|
+
let raw;
|
|
9594
|
+
try {
|
|
9595
|
+
raw = await runTesseractOcr(image, {
|
|
9596
|
+
...opts.tesseractLang ? { lang: opts.tesseractLang } : {},
|
|
9597
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
9598
|
+
});
|
|
9599
|
+
} catch (e) {
|
|
9600
|
+
const err = e;
|
|
9601
|
+
return {
|
|
9602
|
+
ok: false,
|
|
9603
|
+
reason: "unreadable",
|
|
9604
|
+
message: "Tesseract OCR fail: " + (err.message ?? "?")
|
|
9605
|
+
};
|
|
9606
|
+
}
|
|
9607
|
+
const text = postProcess(raw.text);
|
|
9608
|
+
if (text.length === 0) {
|
|
9609
|
+
return { ok: false, reason: "empty", message: "Tesseract kh\xF4ng tr\xEDch \u0111\u01B0\u1EE3c text." };
|
|
9610
|
+
}
|
|
9611
|
+
const tooShort = text.length < MIN_HIGH_CONFIDENCE_CHARS;
|
|
9612
|
+
const lowConf = raw.confidence < TESSERACT_HIGH_CONFIDENCE_THRESHOLD;
|
|
9613
|
+
const confidence = tooShort || lowConf ? "low" : "high";
|
|
9614
|
+
return {
|
|
9615
|
+
ok: true,
|
|
9616
|
+
text,
|
|
9617
|
+
confidence,
|
|
9618
|
+
usage: { inputTokens: 0, outputTokens: 0 }
|
|
9619
|
+
};
|
|
9620
|
+
}
|
|
9621
|
+
function postProcess(raw) {
|
|
9622
|
+
let t = raw.trim();
|
|
9623
|
+
t = t.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
9624
|
+
t = t.replace(/\*(.+?)\*/g, "$1");
|
|
9625
|
+
t = t.replace(/_(.+?)_/g, "$1");
|
|
9626
|
+
t = t.replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1");
|
|
9627
|
+
t = t.replace(/\s+/g, " ").trim();
|
|
9628
|
+
t = t.normalize("NFC");
|
|
9629
|
+
if (t.length > MAX_TEXT_CHARS) t = t.slice(0, MAX_TEXT_CHARS);
|
|
9630
|
+
return t;
|
|
9631
|
+
}
|
|
9632
|
+
var MIN_HIGH_CONFIDENCE_CHARS, MAX_TEXT_CHARS, TESSERACT_HIGH_CONFIDENCE_THRESHOLD;
|
|
9633
|
+
var init_extractProblem = __esm({
|
|
9634
|
+
"src/stamps/geometry-2d/ai/vision/extractProblem.ts"() {
|
|
9635
|
+
init_tesseract();
|
|
9636
|
+
MIN_HIGH_CONFIDENCE_CHARS = 10;
|
|
9637
|
+
MAX_TEXT_CHARS = 2e3;
|
|
9638
|
+
TESSERACT_HIGH_CONFIDENCE_THRESHOLD = 70;
|
|
9639
|
+
}
|
|
9640
|
+
});
|
|
9641
|
+
|
|
9642
|
+
// src/stamps/geometry-2d/ai/handleExtractProblem.ts
|
|
9643
|
+
async function handleExtractProblem(image, opts = {}) {
|
|
9644
|
+
try {
|
|
9645
|
+
const r = await extractProblemFromImage(image, opts);
|
|
9646
|
+
if (r.ok) {
|
|
9647
|
+
if (r.confidence === "low") {
|
|
9648
|
+
return {
|
|
9649
|
+
kind: "low-confidence",
|
|
9650
|
+
text: r.text,
|
|
9651
|
+
warning: "OCR c\xF3 th\u1EC3 kh\xF4ng ch\xEDnh x\xE1c, ki\u1EC3m tra tr\u01B0\u1EDBc khi v\u1EBD.",
|
|
9652
|
+
usage: r.usage
|
|
9653
|
+
};
|
|
9654
|
+
}
|
|
9655
|
+
return { kind: "success", text: r.text, usage: r.usage };
|
|
9656
|
+
}
|
|
9657
|
+
if (r.reason === "not-math") {
|
|
9658
|
+
return { kind: "refused", reason: "not-math", message: r.message };
|
|
9659
|
+
}
|
|
9660
|
+
if (r.reason === "unsupported") {
|
|
9661
|
+
return { kind: "error", code: "unsupported", message: r.message };
|
|
9662
|
+
}
|
|
9663
|
+
if (r.reason === "unreadable") {
|
|
9664
|
+
return { kind: "error", code: "network", message: r.message };
|
|
9665
|
+
}
|
|
9666
|
+
return { kind: "error", code: "empty", message: r.message };
|
|
9667
|
+
} catch (e) {
|
|
9668
|
+
return {
|
|
9669
|
+
kind: "error",
|
|
9670
|
+
code: "unexpected",
|
|
9671
|
+
message: e instanceof Error ? e.message : String(e)
|
|
9672
|
+
};
|
|
9673
|
+
}
|
|
9674
|
+
}
|
|
9675
|
+
var init_handleExtractProblem = __esm({
|
|
9676
|
+
"src/stamps/geometry-2d/ai/handleExtractProblem.ts"() {
|
|
9677
|
+
init_extractProblem();
|
|
9678
|
+
}
|
|
9679
|
+
});
|
|
9680
|
+
|
|
9681
|
+
// src/stamps/geometry-2d/ai/vision/preprocess.ts
|
|
9682
|
+
function inferMediaType(file) {
|
|
9683
|
+
const t = file.type.toLowerCase();
|
|
9684
|
+
if (ALLOWED_TYPES.includes(t)) return t;
|
|
9685
|
+
return null;
|
|
9686
|
+
}
|
|
9687
|
+
function validateFile(file) {
|
|
9688
|
+
const mt = inferMediaType(file);
|
|
9689
|
+
if (mt == null) {
|
|
9690
|
+
return {
|
|
9691
|
+
ok: false,
|
|
9692
|
+
code: "invalid-format",
|
|
9693
|
+
message: "Ch\u1EC9 h\u1ED7 tr\u1EE3 PNG, JPEG, WEBP."
|
|
9694
|
+
};
|
|
9695
|
+
}
|
|
9696
|
+
if (file.size > MAX_RAW_BYTES) {
|
|
9697
|
+
return {
|
|
9698
|
+
ok: false,
|
|
9699
|
+
code: "too-large",
|
|
9700
|
+
message: `\u1EA2nh qu\xE1 l\u1EDBn (> ${Math.round(MAX_RAW_BYTES / 1024 / 1024)} MB). Crop ho\u1EB7c resize tr\u01B0\u1EDBc.`
|
|
9701
|
+
};
|
|
9702
|
+
}
|
|
9703
|
+
return { ok: true, mediaType: mt };
|
|
9704
|
+
}
|
|
9705
|
+
async function fileToImagePart(file) {
|
|
9706
|
+
const v = validateFile(file);
|
|
9707
|
+
if (!v.ok) throw new Error(v.message);
|
|
9708
|
+
const bitmap = await createImageBitmap(file);
|
|
9709
|
+
const { width, height } = bitmap;
|
|
9710
|
+
const maxEdge = Math.max(width, height);
|
|
9711
|
+
const scale = maxEdge > MAX_EDGE_PX ? MAX_EDGE_PX / maxEdge : 1;
|
|
9712
|
+
const targetW = Math.round(width * scale);
|
|
9713
|
+
const targetH = Math.round(height * scale);
|
|
9714
|
+
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(targetW, targetH) : Object.assign(document.createElement("canvas"), { width: targetW, height: targetH });
|
|
9715
|
+
const ctx = canvas.getContext("2d");
|
|
9716
|
+
if (!ctx) throw new Error("Kh\xF4ng t\u1EA1o \u0111\u01B0\u1EE3c canvas 2D context");
|
|
9717
|
+
ctx.drawImage(bitmap, 0, 0, targetW, targetH);
|
|
9718
|
+
bitmap.close();
|
|
9719
|
+
let outputType = v.mediaType === "image/png" ? "image/png" : "image/jpeg";
|
|
9720
|
+
let finalBlob = await canvasToBlob(canvas, outputType, 0.92);
|
|
9721
|
+
if (finalBlob.size > MAX_ENCODED_BYTES) {
|
|
9722
|
+
outputType = "image/jpeg";
|
|
9723
|
+
finalBlob = await canvasToBlob(canvas, "image/jpeg", 0.7);
|
|
9724
|
+
}
|
|
9725
|
+
const base64 = await blobToBase64(finalBlob);
|
|
9726
|
+
return { mediaType: outputType, base64 };
|
|
9727
|
+
}
|
|
9728
|
+
async function canvasToBlob(canvas, type, quality) {
|
|
9729
|
+
if ("convertToBlob" in canvas) {
|
|
9730
|
+
return canvas.convertToBlob({ type, quality });
|
|
9731
|
+
}
|
|
9732
|
+
return new Promise((resolve, reject) => {
|
|
9733
|
+
canvas.toBlob(
|
|
9734
|
+
(b) => b ? resolve(b) : reject(new Error("Canvas encode fail")),
|
|
9735
|
+
type,
|
|
9736
|
+
quality
|
|
9737
|
+
);
|
|
9738
|
+
});
|
|
9739
|
+
}
|
|
9740
|
+
async function blobToBase64(blob) {
|
|
9741
|
+
const buf = await blob.arrayBuffer();
|
|
9742
|
+
let binary = "";
|
|
9743
|
+
const bytes = new Uint8Array(buf);
|
|
9744
|
+
const chunk = 32768;
|
|
9745
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
9746
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
9747
|
+
}
|
|
9748
|
+
return typeof btoa === "function" ? btoa(binary) : Buffer.from(binary, "binary").toString("base64");
|
|
9749
|
+
}
|
|
9750
|
+
var MAX_EDGE_PX, MAX_RAW_BYTES, MAX_ENCODED_BYTES, ALLOWED_TYPES;
|
|
9751
|
+
var init_preprocess = __esm({
|
|
9752
|
+
"src/stamps/geometry-2d/ai/vision/preprocess.ts"() {
|
|
9753
|
+
MAX_EDGE_PX = 2048;
|
|
9754
|
+
MAX_RAW_BYTES = 10 * 1024 * 1024;
|
|
9755
|
+
MAX_ENCODED_BYTES = 3 * 1024 * 1024;
|
|
9756
|
+
ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp"];
|
|
9757
|
+
}
|
|
9758
|
+
});
|
|
9331
9759
|
function AiFigurePrompt({ generator, onGenerated }) {
|
|
9332
9760
|
const {
|
|
9333
9761
|
prompt,
|
|
9334
9762
|
setPrompt,
|
|
9335
9763
|
isLoading,
|
|
9336
9764
|
error,
|
|
9765
|
+
notice,
|
|
9337
9766
|
submit,
|
|
9338
9767
|
cancel,
|
|
9339
9768
|
tokens
|
|
@@ -9347,20 +9776,136 @@ function AiFigurePrompt({ generator, onGenerated }) {
|
|
|
9347
9776
|
const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
|
|
9348
9777
|
return () => clearInterval(id);
|
|
9349
9778
|
}, [isLoading]);
|
|
9779
|
+
const [noticeDismissed, setNoticeDismissed] = React2.useState(false);
|
|
9780
|
+
React2.useEffect(() => {
|
|
9781
|
+
setNoticeDismissed(false);
|
|
9782
|
+
}, [notice]);
|
|
9783
|
+
const [image, setImage] = React2.useState(null);
|
|
9784
|
+
const [ocrLoading, setOcrLoading] = React2.useState(false);
|
|
9785
|
+
const [ocrError, setOcrError] = React2.useState(null);
|
|
9786
|
+
const [ocrWarning, setOcrWarning] = React2.useState(null);
|
|
9787
|
+
const [isDragOver, setIsDragOver] = React2.useState(false);
|
|
9788
|
+
const fileInputRef = React2.useRef(null);
|
|
9350
9789
|
const textareaRef = React2.useRef(null);
|
|
9790
|
+
const imagePreview = image ? `data:${image.mediaType};base64,${image.base64}` : null;
|
|
9791
|
+
React2.useEffect(() => {
|
|
9792
|
+
setOcrError(null);
|
|
9793
|
+
setOcrWarning(null);
|
|
9794
|
+
}, [image]);
|
|
9795
|
+
const handleFile = React2.useCallback(
|
|
9796
|
+
async (file) => {
|
|
9797
|
+
if (isLoading || ocrLoading) return;
|
|
9798
|
+
const v = validateFile(file);
|
|
9799
|
+
if (!v.ok) {
|
|
9800
|
+
setOcrError(v.message);
|
|
9801
|
+
return;
|
|
9802
|
+
}
|
|
9803
|
+
try {
|
|
9804
|
+
const part = await fileToImagePart(file);
|
|
9805
|
+
setImage(part);
|
|
9806
|
+
} catch (e) {
|
|
9807
|
+
setOcrError(e instanceof Error ? e.message : "Kh\xF4ng decode \u0111\u01B0\u1EE3c \u1EA3nh");
|
|
9808
|
+
}
|
|
9809
|
+
},
|
|
9810
|
+
[isLoading, ocrLoading]
|
|
9811
|
+
);
|
|
9812
|
+
const handleFileInput = React2.useCallback(
|
|
9813
|
+
(e) => {
|
|
9814
|
+
const file = e.target.files?.[0];
|
|
9815
|
+
if (file) void handleFile(file);
|
|
9816
|
+
e.target.value = "";
|
|
9817
|
+
},
|
|
9818
|
+
[handleFile]
|
|
9819
|
+
);
|
|
9820
|
+
const handlePaste = React2.useCallback(
|
|
9821
|
+
(e) => {
|
|
9822
|
+
const item = Array.from(e.clipboardData.items).find(
|
|
9823
|
+
(it) => it.kind === "file" && it.type.startsWith("image/")
|
|
9824
|
+
);
|
|
9825
|
+
if (!item) return;
|
|
9826
|
+
const file = item.getAsFile();
|
|
9827
|
+
if (!file) return;
|
|
9828
|
+
e.preventDefault();
|
|
9829
|
+
void handleFile(file);
|
|
9830
|
+
},
|
|
9831
|
+
[handleFile]
|
|
9832
|
+
);
|
|
9833
|
+
const handleDrop = React2.useCallback(
|
|
9834
|
+
(e) => {
|
|
9835
|
+
e.preventDefault();
|
|
9836
|
+
setIsDragOver(false);
|
|
9837
|
+
const file = Array.from(e.dataTransfer.files).find(
|
|
9838
|
+
(f) => f.type.startsWith("image/")
|
|
9839
|
+
);
|
|
9840
|
+
if (file) void handleFile(file);
|
|
9841
|
+
},
|
|
9842
|
+
[handleFile]
|
|
9843
|
+
);
|
|
9844
|
+
const runOcr = React2.useCallback(async () => {
|
|
9845
|
+
if (!image) return;
|
|
9846
|
+
setOcrLoading(true);
|
|
9847
|
+
setOcrError(null);
|
|
9848
|
+
setOcrWarning(null);
|
|
9849
|
+
try {
|
|
9850
|
+
const r = await handleExtractProblem(image);
|
|
9851
|
+
if (r.kind === "success" || r.kind === "low-confidence") {
|
|
9852
|
+
setPrompt(r.text);
|
|
9853
|
+
if (r.kind === "low-confidence") setOcrWarning(r.warning);
|
|
9854
|
+
requestAnimationFrame(() => textareaRef.current?.focus());
|
|
9855
|
+
} else {
|
|
9856
|
+
setOcrError(r.message);
|
|
9857
|
+
}
|
|
9858
|
+
} finally {
|
|
9859
|
+
setOcrLoading(false);
|
|
9860
|
+
}
|
|
9861
|
+
}, [image, setPrompt]);
|
|
9351
9862
|
const handleSendClick = React2.useCallback(async () => {
|
|
9863
|
+
if (image && !prompt.trim() && !ocrLoading) {
|
|
9864
|
+
await runOcr();
|
|
9865
|
+
return;
|
|
9866
|
+
}
|
|
9352
9867
|
const generated = await submit();
|
|
9353
9868
|
if (generated) onGenerated(generated);
|
|
9354
|
-
}, [submit, onGenerated]);
|
|
9869
|
+
}, [image, prompt, ocrLoading, runOcr, submit, onGenerated]);
|
|
9355
9870
|
const promptEmpty = !prompt.trim();
|
|
9356
|
-
const
|
|
9871
|
+
const willOcr = image != null && promptEmpty;
|
|
9872
|
+
const sendDisabled = !image && promptEmpty || ocrLoading || isLoading && !willOcr;
|
|
9873
|
+
const placeholder = willOcr ? "B\u1EA5m g\u1EEDi \u0111\u1EC3 \u0111\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh \u2014 ho\u1EB7c t\u1EF1 g\xF5 \u1EDF \u0111\xE2y\u2026" : "M\xF4 t\u1EA3 \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng \u2014 ho\u1EB7c d\xE1n/\u0111\xEDnh \u1EA3nh \u0111\u1EC1 (Ctrl+V).";
|
|
9357
9874
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-slate-200 bg-slate-50 px-3 py-3", children: [
|
|
9358
9875
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2 flex items-center justify-between gap-2", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium tracking-wide text-slate-600", children: "D\u1EF1ng h\xECnh b\u1EB1ng AI" }) }),
|
|
9359
9876
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
9360
9877
|
"div",
|
|
9361
9878
|
{
|
|
9362
|
-
|
|
9879
|
+
onDragOver: (e) => {
|
|
9880
|
+
e.preventDefault();
|
|
9881
|
+
setIsDragOver(true);
|
|
9882
|
+
},
|
|
9883
|
+
onDragLeave: () => setIsDragOver(false),
|
|
9884
|
+
onDrop: handleDrop,
|
|
9885
|
+
onPaste: handlePaste,
|
|
9886
|
+
className: "group relative flex flex-col rounded-2xl bg-white shadow-sm transition-all duration-150 ring-1 ring-slate-200 focus-within:ring-2 focus-within:ring-emerald-400/70 focus-within:shadow-md " + (isDragOver ? "ring-2 ring-emerald-500 bg-emerald-50/40" : ""),
|
|
9363
9887
|
children: [
|
|
9888
|
+
image && imagePreview && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2 px-3 pt-2.5", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "group/chip relative", children: [
|
|
9889
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9890
|
+
"img",
|
|
9891
|
+
{
|
|
9892
|
+
src: imagePreview,
|
|
9893
|
+
alt: "\u1EA2nh \u0111\u1EC1 b\xE0i",
|
|
9894
|
+
className: "max-h-48 max-w-full h-auto w-auto rounded-lg border border-slate-200 shadow-sm"
|
|
9895
|
+
}
|
|
9896
|
+
),
|
|
9897
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9898
|
+
"button",
|
|
9899
|
+
{
|
|
9900
|
+
type: "button",
|
|
9901
|
+
onClick: () => setImage(null),
|
|
9902
|
+
disabled: ocrLoading || isLoading,
|
|
9903
|
+
"aria-label": "Xo\xE1 \u1EA3nh",
|
|
9904
|
+
className: "absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-slate-900/85 text-[11px] font-medium text-white shadow ring-2 ring-white transition hover:bg-slate-900 disabled:opacity-50",
|
|
9905
|
+
children: "\xD7"
|
|
9906
|
+
}
|
|
9907
|
+
)
|
|
9908
|
+
] }) }),
|
|
9364
9909
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9365
9910
|
"textarea",
|
|
9366
9911
|
{
|
|
@@ -9378,48 +9923,111 @@ function AiFigurePrompt({ generator, onGenerated }) {
|
|
|
9378
9923
|
},
|
|
9379
9924
|
disabled: isLoading,
|
|
9380
9925
|
rows: 2,
|
|
9381
|
-
placeholder
|
|
9926
|
+
placeholder,
|
|
9382
9927
|
className: "block w-full resize-none rounded-2xl bg-transparent px-3.5 pt-2.5 pb-1 text-sm leading-relaxed text-slate-800 placeholder:text-slate-400 outline-none disabled:opacity-60 field-sizing-content max-h-44"
|
|
9383
9928
|
}
|
|
9384
9929
|
),
|
|
9385
|
-
/* @__PURE__ */ jsxRuntime.
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9930
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 px-2 pb-2 pt-1", children: [
|
|
9931
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
9932
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9933
|
+
"button",
|
|
9934
|
+
{
|
|
9935
|
+
type: "button",
|
|
9936
|
+
onClick: () => fileInputRef.current?.click(),
|
|
9937
|
+
disabled: isLoading || ocrLoading,
|
|
9938
|
+
"aria-label": "\u0110\xEDnh \u1EA3nh \u0111\u1EC1 b\xE0i",
|
|
9939
|
+
title: "\u0110\xEDnh \u1EA3nh (c\u0169ng c\xF3 th\u1EC3 d\xE1n b\u1EB1ng Ctrl+V ho\u1EB7c k\xE9o th\u1EA3)",
|
|
9940
|
+
className: "flex h-8 w-8 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-emerald-700 disabled:opacity-40",
|
|
9941
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(PaperclipIcon, { className: "h-[18px] w-[18px]" })
|
|
9942
|
+
}
|
|
9943
|
+
),
|
|
9944
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9945
|
+
"input",
|
|
9946
|
+
{
|
|
9947
|
+
ref: fileInputRef,
|
|
9948
|
+
type: "file",
|
|
9949
|
+
accept: "image/png,image/jpeg,image/webp",
|
|
9950
|
+
className: "sr-only",
|
|
9951
|
+
onChange: handleFileInput,
|
|
9952
|
+
disabled: isLoading || ocrLoading,
|
|
9953
|
+
"aria-label": "Ch\u1ECDn \u1EA3nh \u0111\u1EC1 b\xE0i"
|
|
9954
|
+
}
|
|
9955
|
+
)
|
|
9956
|
+
] }),
|
|
9957
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
9958
|
+
(isLoading || ocrLoading) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] tabular-nums text-slate-500", children: ocrLoading ? "\u0111\u1ECDc \u1EA3nh\u2026" : tokens > 0 ? `${tokens}tok \xB7 ${elapsed}s` : `${elapsed}s` }),
|
|
9959
|
+
isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
9960
|
+
"button",
|
|
9961
|
+
{
|
|
9962
|
+
type: "button",
|
|
9963
|
+
onClick: cancel,
|
|
9964
|
+
"aria-label": "Hu\u1EF7 d\u1EF1ng h\xECnh AI",
|
|
9965
|
+
"data-testid": "geometry-ai-cancel",
|
|
9966
|
+
title: `\u0110ang d\u1EF1ng\u2026 ${elapsed}s \u2014 b\u1EA5m \u0111\u1EC3 hu\u1EF7`,
|
|
9967
|
+
className: "flex h-8 w-8 items-center justify-center rounded-full bg-amber-500 text-white shadow-sm transition hover:scale-105 hover:bg-amber-600 active:scale-95",
|
|
9968
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(StopIcon, { className: "h-3.5 w-3.5" })
|
|
9969
|
+
}
|
|
9970
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
9971
|
+
"button",
|
|
9972
|
+
{
|
|
9973
|
+
type: "button",
|
|
9974
|
+
onClick: () => void handleSendClick(),
|
|
9975
|
+
disabled: sendDisabled,
|
|
9976
|
+
"aria-label": willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh" : "D\u1EF1ng b\u1EB1ng AI",
|
|
9977
|
+
title: willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh (s\u1EBD \u0111i\u1EC1n v\xE0o \xF4 chat)" : "D\u1EF1ng b\u1EB1ng AI (Ctrl/\u2318+Enter)",
|
|
9978
|
+
"data-testid": willOcr ? "geometry-ai-ocr" : "geometry-ai-submit",
|
|
9979
|
+
className: "flex h-8 w-8 items-center justify-center rounded-full bg-emerald-600 text-white shadow-sm transition hover:scale-105 hover:bg-emerald-700 active:scale-95 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:hover:scale-100",
|
|
9980
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ArrowUpIcon, { className: "h-[18px] w-[18px]" })
|
|
9981
|
+
}
|
|
9982
|
+
)
|
|
9983
|
+
] })
|
|
9984
|
+
] }),
|
|
9985
|
+
isDragOver && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl bg-emerald-50/60 text-xs font-medium text-emerald-700", children: "Th\u1EA3 \u1EA3nh v\xE0o \u0111\xE2y" })
|
|
9412
9986
|
]
|
|
9413
9987
|
}
|
|
9414
9988
|
),
|
|
9415
|
-
|
|
9989
|
+
ocrWarning && /* @__PURE__ */ jsxRuntime.jsx(
|
|
9990
|
+
"p",
|
|
9991
|
+
{
|
|
9992
|
+
className: "mt-1 px-1 text-xs text-amber-700",
|
|
9993
|
+
"data-testid": "geometry-ai-ocr-warning",
|
|
9994
|
+
children: ocrWarning
|
|
9995
|
+
}
|
|
9996
|
+
),
|
|
9997
|
+
ocrError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: ocrError }),
|
|
9998
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error }),
|
|
9999
|
+
notice && !noticeDismissed && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10000
|
+
"div",
|
|
10001
|
+
{
|
|
10002
|
+
role: "status",
|
|
10003
|
+
"data-testid": "geometry-ai-partial-notice",
|
|
10004
|
+
className: "relative mt-2 whitespace-pre-wrap rounded-lg border border-amber-200 bg-amber-50 py-2 pl-3 pr-8 text-xs leading-relaxed text-amber-800",
|
|
10005
|
+
children: [
|
|
10006
|
+
notice,
|
|
10007
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10008
|
+
"button",
|
|
10009
|
+
{
|
|
10010
|
+
type: "button",
|
|
10011
|
+
onClick: () => setNoticeDismissed(true),
|
|
10012
|
+
"aria-label": "\u0110\xF3ng th\xF4ng b\xE1o",
|
|
10013
|
+
title: "\u0110\xF3ng th\xF4ng b\xE1o",
|
|
10014
|
+
"data-testid": "geometry-ai-partial-dismiss",
|
|
10015
|
+
className: "absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded text-amber-500 transition hover:bg-amber-100 hover:text-amber-800",
|
|
10016
|
+
children: "\xD7"
|
|
10017
|
+
}
|
|
10018
|
+
)
|
|
10019
|
+
]
|
|
10020
|
+
}
|
|
10021
|
+
)
|
|
9416
10022
|
] });
|
|
9417
10023
|
}
|
|
9418
|
-
var ArrowUpIcon, StopIcon;
|
|
10024
|
+
var ArrowUpIcon, StopIcon, PaperclipIcon;
|
|
9419
10025
|
var init_AiFigurePrompt = __esm({
|
|
9420
10026
|
"src/stamps/geometry-2d/editor/AiFigurePrompt.tsx"() {
|
|
9421
10027
|
"use client";
|
|
9422
10028
|
init_useAiFigure();
|
|
10029
|
+
init_handleExtractProblem();
|
|
10030
|
+
init_preprocess();
|
|
9423
10031
|
ArrowUpIcon = (props) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9424
10032
|
"svg",
|
|
9425
10033
|
{
|
|
@@ -9438,6 +10046,20 @@ var init_AiFigurePrompt = __esm({
|
|
|
9438
10046
|
}
|
|
9439
10047
|
);
|
|
9440
10048
|
StopIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, ...props, children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) });
|
|
10049
|
+
PaperclipIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
10050
|
+
"svg",
|
|
10051
|
+
{
|
|
10052
|
+
viewBox: "0 0 24 24",
|
|
10053
|
+
fill: "none",
|
|
10054
|
+
stroke: "currentColor",
|
|
10055
|
+
strokeWidth: 1.75,
|
|
10056
|
+
strokeLinecap: "round",
|
|
10057
|
+
strokeLinejoin: "round",
|
|
10058
|
+
"aria-hidden": true,
|
|
10059
|
+
...props,
|
|
10060
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" })
|
|
10061
|
+
}
|
|
10062
|
+
);
|
|
9441
10063
|
}
|
|
9442
10064
|
});
|
|
9443
10065
|
|
|
@@ -10021,11 +10643,17 @@ async function insertStampImage(api, opts) {
|
|
|
10021
10643
|
const elements = api.getSceneElements();
|
|
10022
10644
|
const editingId = opts.editingElementId ?? null;
|
|
10023
10645
|
if (editingId) {
|
|
10646
|
+
const old = elements.find((e) => e.id === editingId) ;
|
|
10647
|
+
const oldLongest = old ? Math.max(old.width ?? 0, old.height ?? 0) : 0;
|
|
10648
|
+
const newLongest = Math.max(width, height);
|
|
10649
|
+
const scale = oldLongest > 0 && newLongest > 0 ? oldLongest / newLongest : 1;
|
|
10650
|
+
const w = width * scale;
|
|
10651
|
+
const h = height * scale;
|
|
10024
10652
|
const updated = elements.map(
|
|
10025
|
-
(e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
|
|
10653
|
+
(e) => e.id === editingId ? { ...e, fileId, customData, width: w, height: h } : e
|
|
10026
10654
|
);
|
|
10027
10655
|
api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
|
|
10028
|
-
return { fileId, width, height, elementId: editingId };
|
|
10656
|
+
return { fileId, width: w, height: h, elementId: editingId };
|
|
10029
10657
|
}
|
|
10030
10658
|
const newElement = buildStampImageElement(
|
|
10031
10659
|
api,
|
|
@@ -10179,8 +10807,11 @@ function serializePoint(obj, state) {
|
|
|
10179
10807
|
};
|
|
10180
10808
|
}
|
|
10181
10809
|
case "arcMidpoint": {
|
|
10182
|
-
const
|
|
10183
|
-
|
|
10810
|
+
const containment = c.containing ?? c.notContaining;
|
|
10811
|
+
const baseRefs = [c.circle, c.a, c.b];
|
|
10812
|
+
if (containment) baseRefs.push(containment);
|
|
10813
|
+
const refs = resolveRefs(baseRefs, state);
|
|
10814
|
+
if (!refs) return fail("unresolved-ref", baseRefs.join(","));
|
|
10184
10815
|
return {
|
|
10185
10816
|
ok: true,
|
|
10186
10817
|
entity: {
|
|
@@ -10189,7 +10820,7 @@ function serializePoint(obj, state) {
|
|
|
10189
10820
|
circle: refs[0],
|
|
10190
10821
|
a: refs[1],
|
|
10191
10822
|
b: refs[2],
|
|
10192
|
-
notContaining: refs[3]
|
|
10823
|
+
...c.containing ? { containing: refs[3] } : c.notContaining ? { notContaining: refs[3] } : {}
|
|
10193
10824
|
}
|
|
10194
10825
|
};
|
|
10195
10826
|
}
|
|
@@ -10233,6 +10864,7 @@ function serializePoint(obj, state) {
|
|
|
10233
10864
|
case "onPerpendicular":
|
|
10234
10865
|
case "onPerpBisector":
|
|
10235
10866
|
case "onCircleAroundPoint":
|
|
10867
|
+
case "mixtilinearPoint":
|
|
10236
10868
|
return fail("unsupported-constraint", c.kind);
|
|
10237
10869
|
default: {
|
|
10238
10870
|
return fail("unsupported-constraint");
|
|
@@ -10535,9 +11167,11 @@ function describeEntity(e) {
|
|
|
10535
11167
|
return `${e.name} = \u0111\u01B0\u1EDDng tr\xF2n b\xE0ng ti\u1EBFp ${e.vertices.join("")} \u0111\u1ED1i di\u1EC7n ${e.opposite}`;
|
|
10536
11168
|
// Cụm A
|
|
10537
11169
|
case "arcMidpoint":
|
|
10538
|
-
return `${e.name} = trung \u0111i\u1EC3m cung ${e.a}${e.b} (kh\xF4ng ch\u1EE9a ${e.notContaining}) tr\xEAn ${e.circle}`;
|
|
11170
|
+
return `${e.name} = trung \u0111i\u1EC3m cung ${e.a}${e.b}${e.containing || e.notContaining ? ` (${e.containing ? "ch\u1EE9a" : "kh\xF4ng ch\u1EE9a"} ${e.containing ?? e.notContaining})` : ""} tr\xEAn ${e.circle}`;
|
|
10539
11171
|
case "excenter":
|
|
10540
11172
|
return `${e.name} = t\xE2m b\xE0ng ti\u1EBFp ${e.vertices.join("")} \u0111\u1ED1i di\u1EC7n ${e.opposite}`;
|
|
11173
|
+
case "mixtilinearPoint":
|
|
11174
|
+
return `${e.name} = ${e.which === "center" ? "t\xE2m" : "ti\u1EBFp \u0111i\u1EC3m"} mixtilinear ${e.vertices.join("")}`;
|
|
10541
11175
|
case "reflectPoint":
|
|
10542
11176
|
return `${e.name} = \u0111\u1ED1i x\u1EE9ng ${e.of} qua \u0111i\u1EC3m ${e.through}`;
|
|
10543
11177
|
case "reflectLine":
|
|
@@ -10548,6 +11182,8 @@ function describeEntity(e) {
|
|
|
10548
11182
|
const distStr = d.kind === "circleRadius" ? `r(${d.circle})` : d.kind === "segmentLength" ? `|${d.p1}${d.p2}|` : `${d.value}`;
|
|
10549
11183
|
return `${e.name} = \u0111i\u1EC3m tr\xEAn tia ${e.from}${e.through} c\xE1ch ${e.through} m\u1ED9t kho\u1EA3ng ${distStr}`;
|
|
10550
11184
|
}
|
|
11185
|
+
case "onPerpBisector":
|
|
11186
|
+
return `${e.name} = \u0111i\u1EC3m tr\xEAn trung tr\u1EF1c ${e.p1}${e.p2}`;
|
|
10551
11187
|
default: {
|
|
10552
11188
|
return "";
|
|
10553
11189
|
}
|
|
@@ -10670,7 +11306,8 @@ var init_host = __esm({
|
|
|
10670
11306
|
version: 1,
|
|
10671
11307
|
jsonState
|
|
10672
11308
|
}),
|
|
10673
|
-
editingElementId: editingElement?.id ?? null
|
|
11309
|
+
editingElementId: editingElement?.id ?? null,
|
|
11310
|
+
preserveExistingSize: true
|
|
10674
11311
|
});
|
|
10675
11312
|
} catch (err) {
|
|
10676
11313
|
console.error("Geometry insert failed:", err);
|