@xom11/whiteboard 0.31.0 → 0.32.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.
Files changed (64) hide show
  1. package/dist/ai.d.mts +148 -27
  2. package/dist/ai.d.ts +148 -27
  3. package/dist/ai.js +2652 -430
  4. package/dist/ai.js.map +1 -1
  5. package/dist/ai.mjs +2482 -426
  6. package/dist/ai.mjs.map +1 -1
  7. package/dist/catalog.json +4 -4
  8. package/dist/{chunk-JJ4FPCBE.mjs → chunk-4EXRNZRX.mjs} +641 -75
  9. package/dist/chunk-4EXRNZRX.mjs.map +1 -0
  10. package/dist/{chunk-BMYC2ILT.mjs → chunk-DBG6DZ2V.mjs} +4 -4
  11. package/dist/{chunk-BMYC2ILT.mjs.map → chunk-DBG6DZ2V.mjs.map} +1 -1
  12. package/dist/{chunk-T3N4BSJV.mjs → chunk-DCFZTKFE.mjs} +13 -5
  13. package/dist/chunk-DCFZTKFE.mjs.map +1 -0
  14. package/dist/{chunk-DWIEVCGK.mjs → chunk-DJM5EWTT.mjs} +53 -3
  15. package/dist/chunk-DJM5EWTT.mjs.map +1 -0
  16. package/dist/{chunk-44JY2AKC.mjs → chunk-F42MZ745.mjs} +3 -3
  17. package/dist/{chunk-44JY2AKC.mjs.map → chunk-F42MZ745.mjs.map} +1 -1
  18. package/dist/{chunk-TMRFSOM7.mjs → chunk-FI2MGX5H.mjs} +4 -4
  19. package/dist/{chunk-TMRFSOM7.mjs.map → chunk-FI2MGX5H.mjs.map} +1 -1
  20. package/dist/{chunk-KOXOC2FI.mjs → chunk-KLGPAMJS.mjs} +10 -7
  21. package/dist/chunk-KLGPAMJS.mjs.map +1 -0
  22. package/dist/{chunk-73Q7ADVL.mjs → chunk-MNHOFOM5.mjs} +2 -2
  23. package/dist/{chunk-73Q7ADVL.mjs.map → chunk-MNHOFOM5.mjs.map} +1 -1
  24. package/dist/{chunk-K5BS2H56.mjs → chunk-MOAXHVKI.mjs} +4 -4
  25. package/dist/{chunk-K5BS2H56.mjs.map → chunk-MOAXHVKI.mjs.map} +1 -1
  26. package/dist/chunk-SUXIMLYN.mjs +238 -0
  27. package/dist/chunk-SUXIMLYN.mjs.map +1 -0
  28. package/dist/{chunk-LTLLQUMN.mjs → chunk-XSTGTEGP.mjs} +4 -4
  29. package/dist/{chunk-LTLLQUMN.mjs.map → chunk-XSTGTEGP.mjs.map} +1 -1
  30. package/dist/geometry-2d.js +883 -119
  31. package/dist/geometry-2d.js.map +1 -1
  32. package/dist/geometry-2d.mjs +4 -4
  33. package/dist/geometry-3d.js +3995 -471
  34. package/dist/geometry-3d.js.map +1 -1
  35. package/dist/geometry-3d.mjs +4 -4
  36. package/dist/graph-2d.js +706 -111
  37. package/dist/graph-2d.js.map +1 -1
  38. package/dist/graph-2d.mjs +4 -4
  39. package/dist/{handleExtractProblem-BrDY9ifM.d.ts → handleExtractProblem-mtJyhEw3.d.mts} +2 -0
  40. package/dist/{handleExtractProblem-BrDY9ifM.d.mts → handleExtractProblem-mtJyhEw3.d.ts} +2 -0
  41. package/dist/{host-KMWP7KBT.mjs → host-JNLJ5BVR.mjs} +8 -8
  42. package/dist/{host-KMWP7KBT.mjs.map → host-JNLJ5BVR.mjs.map} +1 -1
  43. package/dist/{host-4FIUNIDQ.mjs → host-VRWLQSMH.mjs} +7 -7
  44. package/dist/{host-4FIUNIDQ.mjs.map → host-VRWLQSMH.mjs.map} +1 -1
  45. package/dist/host-W4U6YCSX.mjs +4973 -0
  46. package/dist/host-W4U6YCSX.mjs.map +1 -0
  47. package/dist/index.d.mts +1 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +4090 -392
  50. package/dist/index.js.map +1 -1
  51. package/dist/index.mjs +27 -21
  52. package/dist/index.mjs.map +1 -1
  53. package/dist/serialize-7UZWQQ4I.mjs +9 -0
  54. package/dist/{serialize-PGHQZEPV.mjs.map → serialize-7UZWQQ4I.mjs.map} +1 -1
  55. package/package.json +4 -1
  56. package/dist/chunk-DWIEVCGK.mjs.map +0 -1
  57. package/dist/chunk-JJ4FPCBE.mjs.map +0 -1
  58. package/dist/chunk-KOXOC2FI.mjs.map +0 -1
  59. package/dist/chunk-QK6OVDLC.mjs +0 -103
  60. package/dist/chunk-QK6OVDLC.mjs.map +0 -1
  61. package/dist/chunk-T3N4BSJV.mjs.map +0 -1
  62. package/dist/host-4ZB4XD4S.mjs +0 -2378
  63. package/dist/host-4ZB4XD4S.mjs.map +0 -1
  64. package/dist/serialize-PGHQZEPV.mjs +0 -9
package/dist/graph-2d.js CHANGED
@@ -37,16 +37,16 @@ var __export = (target, all) => {
37
37
  };
38
38
 
39
39
  // src/core/scene/registry.ts
40
- function registerKind(def22) {
41
- if (registry.has(def22.type)) {
42
- console.warn(`[scene] kind "${def22.type}" \u0111\xE3 \u0111\u01B0\u1EE3c \u0111\u0103ng k\xFD \u2014 ghi \u0111\xE8 \u0111\u1ECBnh ngh\u0129a c\u0169`);
40
+ function registerKind(def24) {
41
+ if (registry.has(def24.type)) {
42
+ console.warn(`[scene] kind "${def24.type}" \u0111\xE3 \u0111\u01B0\u1EE3c \u0111\u0103ng k\xFD \u2014 ghi \u0111\xE8 \u0111\u1ECBnh ngh\u0129a c\u0169`);
43
43
  }
44
- registry.set(def22.type, def22);
44
+ registry.set(def24.type, def24);
45
45
  }
46
46
  function getKind(type) {
47
- const def22 = registry.get(type);
48
- if (!def22) throw new Error(`[scene] unknown kind: ${type}`);
49
- return def22;
47
+ const def24 = registry.get(type);
48
+ if (!def24) throw new Error(`[scene] unknown kind: ${type}`);
49
+ return def24;
50
50
  }
51
51
  var registry;
52
52
  var init_registry = __esm({
@@ -132,8 +132,8 @@ function reduce(draft, action) {
132
132
  return;
133
133
  }
134
134
  case "TRANSACTION": {
135
- for (const sub of action.payload.actions) {
136
- reduce(draft, sub);
135
+ for (const sub2 of action.payload.actions) {
136
+ reduce(draft, sub2);
137
137
  }
138
138
  return;
139
139
  }
@@ -479,8 +479,8 @@ var init_JxgRenderer = __esm({
479
479
  }
480
480
  create(obj) {
481
481
  try {
482
- const def22 = getKind(obj.kind);
483
- const el = def22.render(obj, this.ctx());
482
+ const def24 = getKind(obj.kind);
483
+ const el = def24.render(obj, this.ctx());
484
484
  this.elements.set(obj.id, el);
485
485
  this.attachFreePointDragSync(obj, el);
486
486
  this.attachGliderDragSync(obj, el);
@@ -684,11 +684,11 @@ var init_JxgRenderer = __esm({
684
684
  continue;
685
685
  }
686
686
  if (Object.is(old, cur)) continue;
687
- const def22 = getKind(cur.kind);
687
+ const def24 = getKind(cur.kind);
688
688
  const existing = this.elements.get(id);
689
- if (def22.update && existing) {
689
+ if (def24.update && existing) {
690
690
  try {
691
- def22.update(cur, old, this.ctx(), existing);
691
+ def24.update(cur, old, this.ctx(), existing);
692
692
  continue;
693
693
  } catch (err) {
694
694
  console.warn(`[scene/render/2d] update fail, recreate:`, err);
@@ -1157,8 +1157,36 @@ function constraintRefs(c) {
1157
1157
  return [c.polygonId];
1158
1158
  case "onSphere":
1159
1159
  return [c.sphereId];
1160
- default:
1160
+ case "midpoint":
1161
+ return [c.p1, c.p2];
1162
+ case "centroid":
1163
+ return [...c.vertices];
1164
+ case "intersectionLines":
1165
+ return [c.a1, c.b1, c.a2, c.b2];
1166
+ case "intersectionLinePlane":
1167
+ return [c.a, c.b, c.plane];
1168
+ case "perpFootLine":
1169
+ return [c.from, c.a, c.b];
1170
+ case "perpFootPlane":
1171
+ return [c.from, c.plane];
1172
+ case "circumsphereCenter":
1173
+ return [...c.vertices];
1174
+ case "pyramidInsphereCenter":
1175
+ return [c.apex, ...c.vertices];
1176
+ case "faceCircumcenter":
1177
+ return [...c.vertices];
1178
+ case "pointAboveFace":
1179
+ return [c.base, c.apex, ...c.vertices];
1180
+ // Kind KHÔNG có ref scene-object (toạ độ literal): liệt kê TƯỜNG MINH để
1181
+ // exhaustive never-guard bên dưới buộc khai báo khi thêm constraint kind mới
1182
+ // (mô phỏng constraintRefs2D — quên case = cascade-delete/deps sai âm thầm).
1183
+ case "free":
1184
+ case "onGround":
1185
+ case "onAxis":
1161
1186
  return [];
1187
+ default: {
1188
+ return [];
1189
+ }
1162
1190
  }
1163
1191
  }
1164
1192
  var init_d_constraint = __esm({
@@ -1166,12 +1194,359 @@ var init_d_constraint = __esm({
1166
1194
  }
1167
1195
  });
1168
1196
 
1197
+ // src/core/scene/kinds/constraint3d-math.ts
1198
+ function sub(a, b) {
1199
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1200
+ }
1201
+ function add(a, b) {
1202
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
1203
+ }
1204
+ function scale(a, k) {
1205
+ return [a[0] * k, a[1] * k, a[2] * k];
1206
+ }
1207
+ function dot(a, b) {
1208
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1209
+ }
1210
+ function cross(a, b) {
1211
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
1212
+ }
1213
+ function norm(a) {
1214
+ return Math.sqrt(dot(a, a));
1215
+ }
1216
+ function normalize(a) {
1217
+ const n = norm(a);
1218
+ return n === 0 ? a : scale(a, 1 / n);
1219
+ }
1220
+ function getPointWorld(id, state) {
1221
+ const obj = state.objects[id];
1222
+ if (!obj || obj.kind !== "point3d") {
1223
+ throw new Error(`constraintMath: point ${id} not found`);
1224
+ }
1225
+ const attrs = obj.attrs;
1226
+ return constraintToWorld(attrs.constraint, state);
1227
+ }
1228
+ function getPlaneBasis(planeObj, state) {
1229
+ const a = planeObj.attrs;
1230
+ let p1, p2, p3;
1231
+ if (a.construction) {
1232
+ const r = planeConstructionWorld(a.construction, state);
1233
+ p1 = r.p1;
1234
+ p2 = r.p2;
1235
+ p3 = r.p3;
1236
+ } else {
1237
+ p1 = getPointWorld(a.p1, state);
1238
+ p2 = getPointWorld(a.p2, state);
1239
+ p3 = getPointWorld(a.p3, state);
1240
+ }
1241
+ const basis1 = sub(p2, p1);
1242
+ const tmp = sub(p3, p1);
1243
+ const normal = normalize(cross(basis1, tmp));
1244
+ const basis2 = cross(normal, basis1);
1245
+ return { origin: p1, basis1, basis2, normal };
1246
+ }
1247
+ function orthoBasisFromNormal(n) {
1248
+ const seed = Math.abs(n[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
1249
+ const basis1 = normalize(cross(n, seed));
1250
+ const basis2 = cross(n, basis1);
1251
+ return { basis1, basis2 };
1252
+ }
1253
+ function planeConstructionWorld(c, state) {
1254
+ if (_recDepth >= MAX_REC_DEPTH) throw new Error("planeConstructionWorld: \u0111\u1EC7 quy qu\xE1 s\xE2u \u2014 chu tr\xECnh tham chi\u1EBFu m\u1EB7t?");
1255
+ _recDepth++;
1256
+ try {
1257
+ return planeConstructionWorldInner(c, state);
1258
+ } finally {
1259
+ _recDepth--;
1260
+ }
1261
+ }
1262
+ function planeConstructionWorldInner(c, state) {
1263
+ switch (c.kind) {
1264
+ case "planeParallelThrough": {
1265
+ const P = getPointWorld(c.point, state);
1266
+ const ref = state.objects[c.refPlane];
1267
+ if (!ref || ref.kind !== "plane3d") throw new Error("planeParallelThrough: m\u1EB7t tham chi\u1EBFu thi\u1EBFu");
1268
+ const { basis1, basis2 } = getPlaneBasis(ref, state);
1269
+ if (norm(basis1) < 1e-12 || norm(basis2) < 1e-12) {
1270
+ return { p1: P, p2: add(P, [1, 0, 0]), p3: add(P, [0, 1, 0]) };
1271
+ }
1272
+ return { p1: P, p2: add(P, basis1), p3: add(P, basis2) };
1273
+ }
1274
+ case "planePerpToLine": {
1275
+ const P = getPointWorld(c.point, state);
1276
+ const dir = sub(getPointWorld(c.lineB, state), getPointWorld(c.lineA, state));
1277
+ const dn = norm(dir);
1278
+ if (dn < 1e-12) return { p1: P, p2: add(P, [1, 0, 0]), p3: add(P, [0, 1, 0]) };
1279
+ const { basis1, basis2 } = orthoBasisFromNormal(scale(dir, 1 / dn));
1280
+ return { p1: P, p2: add(P, basis1), p3: add(P, basis2) };
1281
+ }
1282
+ default: {
1283
+ throw new Error("planeConstructionWorld: kind kh\xF4ng h\u1ED7 tr\u1EE3");
1284
+ }
1285
+ }
1286
+ }
1287
+ function lineDefiningPointIds(line) {
1288
+ if (line.kind === "line3d" || line.kind === "segment3d") {
1289
+ const a = line.attrs;
1290
+ return a.p1 && a.p2 ? [a.p1, a.p2] : null;
1291
+ }
1292
+ if (line.kind === "ray3d") {
1293
+ const a = line.attrs;
1294
+ return [a.origin, a.through];
1295
+ }
1296
+ if (line.kind === "vector3d") {
1297
+ const a = line.attrs;
1298
+ return [a.from, a.to];
1299
+ }
1300
+ return null;
1301
+ }
1302
+ function lineEndpointsWorld(lineId, state, ctx) {
1303
+ const line = state.objects[lineId];
1304
+ if (!line) throw new Error(`${ctx}: \u0111\u01B0\u1EDDng ${lineId} kh\xF4ng t\u1ED3n t\u1EA1i`);
1305
+ const ids = lineDefiningPointIds(line);
1306
+ if (!ids) throw new Error(`${ctx}: kind ${line.kind} kh\xF4ng ph\u1EA3i \u0111\u01B0\u1EDDng`);
1307
+ return { a: getPointWorld(ids[0], state), b: getPointWorld(ids[1], state) };
1308
+ }
1309
+ function lineLineClosestMidpoint(A, B, C, D) {
1310
+ const u = sub(B, A), v = sub(D, C), w0 = sub(A, C);
1311
+ const a = dot(u, u), b = dot(u, v), cc = dot(v, v), d = dot(u, w0), e = dot(v, w0);
1312
+ const denom = a * cc - b * b;
1313
+ if (Math.abs(denom) < 1e-12) return scale(add(A, C), 0.5);
1314
+ const sc = (b * e - cc * d) / denom;
1315
+ const tc = (a * e - b * d) / denom;
1316
+ return scale(add(add(A, scale(u, sc)), add(C, scale(v, tc))), 0.5);
1317
+ }
1318
+ function planeOriginNormal(planeId, state, ctx) {
1319
+ const plane = state.objects[planeId];
1320
+ if (!plane || plane.kind !== "plane3d") throw new Error(`${ctx}: m\u1EB7t ph\u1EB3ng ${planeId} thi\u1EBFu`);
1321
+ const { origin, normal } = getPlaneBasis(plane, state);
1322
+ return { origin, normal };
1323
+ }
1324
+ function lineConstructionWorld(c, state) {
1325
+ switch (c.kind) {
1326
+ case "planePlaneIntersection": {
1327
+ const P1 = planeOriginNormal(c.plane1, state, "planePlaneIntersection");
1328
+ const P2 = planeOriginNormal(c.plane2, state, "planePlaneIntersection");
1329
+ const d = cross(P1.normal, P2.normal);
1330
+ const dd = dot(d, d);
1331
+ if (dd < 1e-12) {
1332
+ return { a: P1.origin, b: add(P1.origin, P2.normal) };
1333
+ }
1334
+ const c1 = dot(P1.normal, P1.origin);
1335
+ const c2 = dot(P2.normal, P2.origin);
1336
+ const p0 = scale(add(scale(cross(P2.normal, d), c1), scale(cross(d, P1.normal), c2)), 1 / dd);
1337
+ return { a: p0, b: add(p0, d) };
1338
+ }
1339
+ case "lineParallelThrough": {
1340
+ const P = getPointWorld(c.point, state);
1341
+ const dir = sub(getPointWorld(c.dirB, state), getPointWorld(c.dirA, state));
1342
+ if (norm(dir) < 1e-12) return { a: P, b: add(P, [1, 0, 0]) };
1343
+ return { a: P, b: add(P, dir) };
1344
+ }
1345
+ case "linePerpToPlane": {
1346
+ const P = getPointWorld(c.point, state);
1347
+ const { normal } = planeOriginNormal(c.plane, state, "linePerpToPlane");
1348
+ return { a: P, b: add(P, normal) };
1349
+ }
1350
+ default: {
1351
+ throw new Error("lineConstructionWorld: kind kh\xF4ng h\u1ED7 tr\u1EE3");
1352
+ }
1353
+ }
1354
+ }
1355
+ function solve3(M, b) {
1356
+ const det3 = (m) => m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
1357
+ const det = det3(M);
1358
+ if (Math.abs(det) < 1e-9) return null;
1359
+ const colDet = (j) => {
1360
+ const A = M.map((r) => r.slice());
1361
+ for (let i = 0; i < 3; i++) A[i][j] = b[i];
1362
+ return det3(A);
1363
+ };
1364
+ return [colDet(0) / det, colDet(1) / det, colDet(2) / det];
1365
+ }
1366
+ function constraintToWorld(c, state) {
1367
+ if (_recDepth >= MAX_REC_DEPTH) throw new Error("constraintToWorld: \u0111\u1EC7 quy qu\xE1 s\xE2u \u2014 chu tr\xECnh tham chi\u1EBFu?");
1368
+ _recDepth++;
1369
+ try {
1370
+ return constraintToWorldInner(c, state);
1371
+ } finally {
1372
+ _recDepth--;
1373
+ }
1374
+ }
1375
+ function constraintToWorldInner(c, state) {
1376
+ switch (c.kind) {
1377
+ case "free":
1378
+ return [c.x, c.y, c.z];
1379
+ case "onGround":
1380
+ return [c.x, c.y, 0];
1381
+ case "onAxis": {
1382
+ if (c.axis === "x") return [c.t, 0, 0];
1383
+ if (c.axis === "y") return [0, c.t, 0];
1384
+ return [0, 0, c.t];
1385
+ }
1386
+ case "onPlane": {
1387
+ const plane = state.objects[c.planeId];
1388
+ if (!plane || plane.kind !== "plane3d") throw new Error("onPlane: plane missing");
1389
+ const { origin, basis1, basis2 } = getPlaneBasis(plane, state);
1390
+ return add(add(origin, scale(basis1, c.u)), scale(basis2, c.v));
1391
+ }
1392
+ case "onLine": {
1393
+ const { a, b } = lineEndpointsWorld(c.lineId, state, "onLine");
1394
+ return add(a, scale(sub(b, a), c.t));
1395
+ }
1396
+ case "onPolygon": {
1397
+ const pg = state.objects[c.polygonId];
1398
+ if (!pg || pg.kind !== "polygon3d") throw new Error("onPolygon: parent missing");
1399
+ const v = pg.attrs.vertices;
1400
+ if (v.length < 3) throw new Error("onPolygon: < 3 vertices");
1401
+ const p1 = getPointWorld(v[0], state);
1402
+ const p2 = getPointWorld(v[1], state);
1403
+ const p3 = getPointWorld(v[2], state);
1404
+ const basis1 = sub(p2, p1);
1405
+ const tmp = sub(p3, p1);
1406
+ const normal = normalize(cross(basis1, tmp));
1407
+ const basis2 = cross(normal, basis1);
1408
+ return add(add(p1, scale(basis1, c.u)), scale(basis2, c.v));
1409
+ }
1410
+ case "onSphere": {
1411
+ const sph = state.objects[c.sphereId];
1412
+ if (!sph || sph.kind !== "sphere3d") throw new Error("onSphere: parent missing");
1413
+ const a = sph.attrs;
1414
+ const center = getPointWorld(a.center, state);
1415
+ const surface = getPointWorld(a.surfacePoint, state);
1416
+ const radius = norm(sub(surface, center));
1417
+ const x = center[0] + radius * Math.sin(c.phi) * Math.cos(c.theta);
1418
+ const y = center[1] + radius * Math.sin(c.phi) * Math.sin(c.theta);
1419
+ const z = center[2] + radius * Math.cos(c.phi);
1420
+ return [x, y, z];
1421
+ }
1422
+ // ───── Điểm phái sinh (v1) ─────
1423
+ case "midpoint": {
1424
+ const p1 = getPointWorld(c.p1, state);
1425
+ const p2 = getPointWorld(c.p2, state);
1426
+ return scale(add(p1, p2), 0.5);
1427
+ }
1428
+ case "centroid": {
1429
+ const n = c.vertices.length;
1430
+ if (n === 0) return [0, 0, 0];
1431
+ let acc = [0, 0, 0];
1432
+ for (const id of c.vertices) acc = add(acc, getPointWorld(id, state));
1433
+ return scale(acc, 1 / n);
1434
+ }
1435
+ case "circumsphereCenter": {
1436
+ const P = c.vertices.map((id) => getPointWorld(id, state));
1437
+ if (P.length < 4) return P.length ? P[0] : [0, 0, 0];
1438
+ const p0 = P[0];
1439
+ const M = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
1440
+ const rhs = [0, 0, 0];
1441
+ for (let i = 1; i < P.length; i++) {
1442
+ const r = [2 * (P[i][0] - p0[0]), 2 * (P[i][1] - p0[1]), 2 * (P[i][2] - p0[2])];
1443
+ const bi = dot(P[i], P[i]) - dot(p0, p0);
1444
+ for (let a = 0; a < 3; a++) {
1445
+ for (let bcol = 0; bcol < 3; bcol++) M[a][bcol] += r[a] * r[bcol];
1446
+ rhs[a] += r[a] * bi;
1447
+ }
1448
+ }
1449
+ const sol = solve3(M, rhs);
1450
+ if (sol) return sol;
1451
+ let acc = [0, 0, 0];
1452
+ for (const p of P) acc = add(acc, p);
1453
+ return scale(acc, 1 / P.length);
1454
+ }
1455
+ case "pyramidInsphereCenter": {
1456
+ const S = getPointWorld(c.apex, state);
1457
+ const base = c.vertices.map((id) => getPointWorld(id, state));
1458
+ const n = base.length;
1459
+ if (n < 3) return S;
1460
+ let G = [0, 0, 0];
1461
+ for (const p of base) G = add(G, p);
1462
+ G = scale(G, 1 / n);
1463
+ let nb = normalize(cross(sub(base[1], base[0]), sub(base[2], base[0])));
1464
+ if (dot(nb, sub(S, G)) < 0) nb = scale(nb, -1);
1465
+ let m = normalize(cross(sub(base[0], S), sub(base[1], S)));
1466
+ if (dot(m, sub(G, base[0])) < 0) m = scale(m, -1);
1467
+ const denom = 1 - dot(m, nb);
1468
+ const s = Math.abs(denom) < 1e-9 ? NaN : dot(m, sub(G, base[0])) / denom;
1469
+ if (!Number.isFinite(s) || s <= 0) {
1470
+ return scale(add(S, scale(G, n)), 1 / (n + 1));
1471
+ }
1472
+ return add(G, scale(nb, s));
1473
+ }
1474
+ case "faceCircumcenter": {
1475
+ const P = c.vertices.map((id) => getPointWorld(id, state));
1476
+ if (P.length < 3) return P.length ? P[0] : [0, 0, 0];
1477
+ const [p0, p1, p2] = P;
1478
+ const e1 = sub(p1, p0), e2 = sub(p2, p0);
1479
+ const nrm = cross(e1, e2);
1480
+ const M = [
1481
+ [2 * e1[0], 2 * e1[1], 2 * e1[2]],
1482
+ [2 * e2[0], 2 * e2[1], 2 * e2[2]],
1483
+ [nrm[0], nrm[1], nrm[2]]
1484
+ ];
1485
+ const rhs = [dot(p1, p1) - dot(p0, p0), dot(p2, p2) - dot(p0, p0), dot(nrm, p0)];
1486
+ const sol = solve3(M, rhs);
1487
+ if (sol) return sol;
1488
+ return scale(add(add(p0, p1), p2), 1 / 3);
1489
+ }
1490
+ case "pointAboveFace": {
1491
+ const G = getPointWorld(c.base, state);
1492
+ const P = c.vertices.map((id) => getPointWorld(id, state));
1493
+ if (P.length < 3) return G;
1494
+ let n = normalize(cross(sub(P[1], P[0]), sub(P[2], P[0])));
1495
+ const S = getPointWorld(c.apex, state);
1496
+ if (dot(n, sub(S, P[0])) < 0) n = scale(n, -1);
1497
+ const h = dot(sub(S, P[0]), n);
1498
+ if (!Number.isFinite(h) || h <= 1e-9) return G;
1499
+ return add(G, scale(n, h));
1500
+ }
1501
+ case "intersectionLines": {
1502
+ const A = getPointWorld(c.a1, state), B = getPointWorld(c.b1, state);
1503
+ const C = getPointWorld(c.a2, state), D = getPointWorld(c.b2, state);
1504
+ return lineLineClosestMidpoint(A, B, C, D);
1505
+ }
1506
+ case "intersectionLinePlane": {
1507
+ const A = getPointWorld(c.a, state), B = getPointWorld(c.b, state);
1508
+ const plane = state.objects[c.plane];
1509
+ if (!plane || plane.kind !== "plane3d") throw new Error("intersectionLinePlane: m\u1EB7t ph\u1EB3ng thi\u1EBFu");
1510
+ const { origin, normal } = getPlaneBasis(plane, state);
1511
+ const dir = sub(B, A);
1512
+ const dn = dot(dir, normal);
1513
+ if (Math.abs(dn) < 1e-12) return A;
1514
+ const t = dot(sub(origin, A), normal) / dn;
1515
+ return add(A, scale(dir, t));
1516
+ }
1517
+ case "perpFootLine": {
1518
+ const P = getPointWorld(c.from, state);
1519
+ const A = getPointWorld(c.a, state), B = getPointWorld(c.b, state);
1520
+ const u = sub(B, A);
1521
+ const uu = dot(u, u);
1522
+ const t = uu === 0 ? 0 : dot(sub(P, A), u) / uu;
1523
+ return add(A, scale(u, t));
1524
+ }
1525
+ case "perpFootPlane": {
1526
+ const P = getPointWorld(c.from, state);
1527
+ const plane = state.objects[c.plane];
1528
+ if (!plane || plane.kind !== "plane3d") throw new Error("perpFootPlane: m\u1EB7t ph\u1EB3ng thi\u1EBFu");
1529
+ const { origin, normal } = getPlaneBasis(plane, state);
1530
+ const dist2 = dot(sub(P, origin), normal);
1531
+ return sub(P, scale(normal, dist2));
1532
+ }
1533
+ }
1534
+ }
1535
+ var MAX_REC_DEPTH, _recDepth;
1536
+ var init_constraint3d_math = __esm({
1537
+ "src/core/scene/kinds/constraint3d-math.ts"() {
1538
+ MAX_REC_DEPTH = 512;
1539
+ _recDepth = 0;
1540
+ }
1541
+ });
1542
+
1169
1543
  // src/core/scene/kinds/point3d.ts
1170
1544
  var def;
1171
1545
  var init_point3d = __esm({
1172
1546
  "src/core/scene/kinds/point3d.ts"() {
1173
1547
  init_registry();
1174
1548
  init_d_constraint();
1549
+ init_constraint3d_math();
1175
1550
  def = {
1176
1551
  type: "point3d",
1177
1552
  schemaVersion: 1,
@@ -1209,6 +1584,15 @@ var init_point3d = __esm({
1209
1584
  if (c.kind === "onLine") return `${obj.label} tr\xEAn \u0111\u01B0\u1EDDng ${c.lineId}`;
1210
1585
  if (c.kind === "onPolygon") return `${obj.label} tr\xEAn \u0111a gi\xE1c ${c.polygonId}`;
1211
1586
  if (c.kind === "onSphere") return `${obj.label} tr\xEAn m\u1EB7t c\u1EA7u ${c.sphereId}`;
1587
+ if (c.kind === "midpoint") return `${obj.label} = trung \u0111i\u1EC3m ${c.p1}${c.p2}`;
1588
+ if (c.kind === "centroid") return `${obj.label} = tr\u1ECDng t\xE2m ${c.vertices.join("")}`;
1589
+ if (c.kind === "intersectionLines") return `${obj.label} = giao 2 \u0111\u01B0\u1EDDng (${c.a1}${c.b1}, ${c.a2}${c.b2})`;
1590
+ if (c.kind === "intersectionLinePlane") return `${obj.label} = giao ${c.a}${c.b} \u2229 ${c.plane}`;
1591
+ if (c.kind === "perpFootLine") return `${obj.label} = ch\xE2n \u22A5 t\u1EEB ${c.from} xu\u1ED1ng ${c.a}${c.b}`;
1592
+ if (c.kind === "perpFootPlane") return `${obj.label} = ch\xE2n \u22A5 t\u1EEB ${c.from} xu\u1ED1ng ${c.plane}`;
1593
+ if (c.kind === "pyramidInsphereCenter") return `${obj.label} = t\xE2m c\u1EA7u n\u1ED9i ti\u1EBFp ch\xF3p ${c.apex}.${c.vertices.join("")}`;
1594
+ if (c.kind === "faceCircumcenter") return `${obj.label} = t\xE2m ngo\u1EA1i ti\u1EBFp ${c.vertices.join("")}`;
1595
+ if (c.kind === "pointAboveFace") return `${obj.label} = \u0111\u1EC9nh tr\u1EE5c tr\u1EE5 \u22A5 m\u1EB7t ${c.vertices.join("")}`;
1212
1596
  return obj.label;
1213
1597
  },
1214
1598
  render: (obj, ctx) => {
@@ -1258,6 +1642,15 @@ var init_point3d = __esm({
1258
1642
  () => sph.F(c.theta, c.phi)[2]
1259
1643
  ], opts);
1260
1644
  }
1645
+ const getState = ctx.getState;
1646
+ if (getState) {
1647
+ const cw = () => constraintToWorld(c, getState());
1648
+ return view.create("point3d", [
1649
+ () => cw()[0],
1650
+ () => cw()[1],
1651
+ () => cw()[2]
1652
+ ], { ...opts, needsRegularUpdate: true });
1653
+ }
1261
1654
  return view.create("point3d", [0, 0, 0], opts);
1262
1655
  }
1263
1656
  };
@@ -1319,32 +1712,77 @@ var init_segment3d = __esm({
1319
1712
  });
1320
1713
 
1321
1714
  // src/core/scene/kinds/line3d.ts
1715
+ function line3dConstructionRefs(c) {
1716
+ switch (c.kind) {
1717
+ case "planePlaneIntersection":
1718
+ return [c.plane1, c.plane2];
1719
+ case "lineParallelThrough":
1720
+ return [c.point, c.dirA, c.dirB];
1721
+ case "linePerpToPlane":
1722
+ return [c.point, c.plane];
1723
+ default: {
1724
+ return [];
1725
+ }
1726
+ }
1727
+ }
1728
+ var def3;
1322
1729
  var init_line3d = __esm({
1323
1730
  "src/core/scene/kinds/line3d.ts"() {
1324
1731
  init_registry();
1325
1732
  init_labelOf();
1326
- registerKind({
1733
+ init_constraint3d_math();
1734
+ def3 = {
1327
1735
  type: "line3d",
1328
1736
  schemaVersion: 1,
1329
1737
  migrate: {},
1330
1738
  validate: (a) => {
1331
- if (!a?.p1 || !a?.p2) throw new Error("line3d: p1/p2 required");
1739
+ if (a?.construction) return;
1740
+ if (!a?.p1 || !a?.p2) throw new Error("line3d: p1/p2 required (ho\u1EB7c construction)");
1741
+ },
1742
+ dependsOn: (a) => a.construction ? line3dConstructionRefs(a.construction) : [a.p1, a.p2],
1743
+ describe: (obj, state) => {
1744
+ const c = obj.attrs.construction;
1745
+ if (!c) return `\u0110\u01B0\u1EDDng ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}`;
1746
+ switch (c.kind) {
1747
+ case "planePlaneIntersection":
1748
+ return `Giao tuy\u1EBFn ${obj.label} = ${labelOf(c.plane1, state)} \u2229 ${labelOf(c.plane2, state)}`;
1749
+ case "lineParallelThrough":
1750
+ return `${obj.label} \u2225 ${labelOf(c.dirA, state)}${labelOf(c.dirB, state)} qua ${labelOf(c.point, state)}`;
1751
+ case "linePerpToPlane":
1752
+ return `${obj.label} \u22A5 ${labelOf(c.plane, state)} qua ${labelOf(c.point, state)}`;
1753
+ default:
1754
+ return obj.label;
1755
+ }
1332
1756
  },
1333
- dependsOn: (a) => [a.p1, a.p2],
1334
- describe: (obj, state) => `\u0110\u01B0\u1EDDng ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}`,
1335
1757
  render: (obj, ctx) => {
1336
1758
  const view = ctx.jxg;
1759
+ const baseOpts = {
1760
+ strokeColor: obj.attrs.color ?? "#0f172a",
1761
+ strokeWidth: 2,
1762
+ visible: obj.visible
1763
+ };
1764
+ const c = obj.attrs.construction;
1765
+ if (c) {
1766
+ const getState = ctx.getState;
1767
+ if (!getState) throw new Error("line3d construction: c\u1EA7n ctx.getState");
1768
+ const lc = () => lineConstructionWorld(c, getState());
1769
+ return view.create("line3d", [() => lc().a, () => lc().b], {
1770
+ ...baseOpts,
1771
+ straightFirst: true,
1772
+ straightLast: true,
1773
+ needsRegularUpdate: true
1774
+ });
1775
+ }
1337
1776
  const pA = ctx.resolveRef(obj.attrs.p1);
1338
1777
  const pB = ctx.resolveRef(obj.attrs.p2);
1339
1778
  return view.create("line3d", [pA, pB], {
1779
+ ...baseOpts,
1340
1780
  straightFirst: true,
1341
- straightLast: true,
1342
- strokeColor: obj.attrs.color ?? "#0f172a",
1343
- strokeWidth: 2,
1344
- visible: obj.visible
1781
+ straightLast: true
1345
1782
  });
1346
1783
  }
1347
- });
1784
+ };
1785
+ registerKind(def3);
1348
1786
  }
1349
1787
  });
1350
1788
 
@@ -1410,33 +1848,77 @@ var init_vector3d = __esm({
1410
1848
  });
1411
1849
 
1412
1850
  // src/core/scene/kinds/plane3d.ts
1851
+ function plane3dConstructionRefs(c) {
1852
+ switch (c.kind) {
1853
+ case "planeParallelThrough":
1854
+ return [c.point, c.refPlane];
1855
+ case "planePerpToLine":
1856
+ return [c.point, c.lineA, c.lineB];
1857
+ default: {
1858
+ return [];
1859
+ }
1860
+ }
1861
+ }
1862
+ var def4;
1413
1863
  var init_plane3d = __esm({
1414
1864
  "src/core/scene/kinds/plane3d.ts"() {
1415
1865
  init_registry();
1416
1866
  init_labelOf();
1417
- registerKind({
1867
+ init_constraint3d_math();
1868
+ def4 = {
1418
1869
  type: "plane3d",
1419
1870
  schemaVersion: 1,
1420
1871
  migrate: {},
1421
1872
  validate: (a) => {
1422
- if (!a?.p1 || !a?.p2 || !a?.p3) throw new Error("plane3d: c\u1EA7n 3 \u0111i\u1EC3m");
1873
+ if (a?.construction) return;
1874
+ if (!a?.p1 || !a?.p2 || !a?.p3) throw new Error("plane3d: c\u1EA7n 3 \u0111i\u1EC3m (ho\u1EB7c construction)");
1875
+ },
1876
+ dependsOn: (a) => a.construction ? plane3dConstructionRefs(a.construction) : [a.p1, a.p2, a.p3],
1877
+ describe: (obj, state) => {
1878
+ const c = obj.attrs.construction;
1879
+ if (!c) {
1880
+ return `M\u1EB7t ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}, ${labelOf(obj.attrs.p3, state)}`;
1881
+ }
1882
+ switch (c.kind) {
1883
+ case "planeParallelThrough":
1884
+ return `${obj.label} \u2225 ${labelOf(c.refPlane, state)} qua ${labelOf(c.point, state)}`;
1885
+ case "planePerpToLine":
1886
+ return `${obj.label} \u22A5 ${labelOf(c.lineA, state)}${labelOf(c.lineB, state)} qua ${labelOf(c.point, state)}`;
1887
+ default:
1888
+ return obj.label;
1889
+ }
1423
1890
  },
1424
- dependsOn: (a) => [a.p1, a.p2, a.p3],
1425
- describe: (obj, state) => `M\u1EB7t ${obj.label} qua ${labelOf(obj.attrs.p1, state)}, ${labelOf(obj.attrs.p2, state)}, ${labelOf(obj.attrs.p3, state)}`,
1426
1891
  render: (obj, ctx) => {
1427
1892
  const view = ctx.jxg;
1428
- return view.create("plane3d", [
1429
- ctx.resolveRef(obj.attrs.p1),
1430
- ctx.resolveRef(obj.attrs.p2),
1431
- ctx.resolveRef(obj.attrs.p3)
1432
- ], {
1893
+ const opts = {
1433
1894
  fillOpacity: 0.15,
1434
1895
  fillColor: obj.attrs.color ?? "#60a5fa",
1435
1896
  strokeColor: obj.attrs.color ?? "#60a5fa",
1436
1897
  visible: obj.visible
1437
- });
1898
+ };
1899
+ const c = obj.attrs.construction;
1900
+ if (c) {
1901
+ const getState = ctx.getState;
1902
+ if (!getState) throw new Error("plane3d construction: c\u1EA7n ctx.getState");
1903
+ const pc = () => planeConstructionWorld(c, getState());
1904
+ const diff = (which) => {
1905
+ const r = pc();
1906
+ const q = r[which];
1907
+ return [q[0] - r.p1[0], q[1] - r.p1[1], q[2] - r.p1[2]];
1908
+ };
1909
+ return view.create("plane3d", [() => pc().p1, () => diff("p2"), () => diff("p3")], {
1910
+ ...opts,
1911
+ needsRegularUpdate: true
1912
+ });
1913
+ }
1914
+ return view.create("plane3d", [
1915
+ ctx.resolveRef(obj.attrs.p1),
1916
+ ctx.resolveRef(obj.attrs.p2),
1917
+ ctx.resolveRef(obj.attrs.p3)
1918
+ ], opts);
1438
1919
  }
1439
- });
1920
+ };
1921
+ registerKind(def4);
1440
1922
  }
1441
1923
  });
1442
1924
 
@@ -1535,11 +2017,37 @@ var init_polyhedron3d = __esm({
1535
2017
  }
1536
2018
  });
1537
2019
 
2020
+ // src/core/scene/kinds/_ringBasis.ts
2021
+ function perpBasis(axis) {
2022
+ const n = Math.hypot(axis[0], axis[1], axis[2]);
2023
+ if (n < 1e-12) return [[1, 0, 0], [0, 1, 0]];
2024
+ const a = [axis[0] / n, axis[1] / n, axis[2] / n];
2025
+ const seed = Math.abs(a[2]) < 0.9 ? [0, 0, 1] : [1, 0, 0];
2026
+ let u = [
2027
+ seed[1] * a[2] - seed[2] * a[1],
2028
+ seed[2] * a[0] - seed[0] * a[2],
2029
+ seed[0] * a[1] - seed[1] * a[0]
2030
+ ];
2031
+ const un = Math.hypot(u[0], u[1], u[2]) || 1;
2032
+ u = [u[0] / un, u[1] / un, u[2] / un];
2033
+ const v = [
2034
+ a[1] * u[2] - a[2] * u[1],
2035
+ a[2] * u[0] - a[0] * u[2],
2036
+ a[0] * u[1] - a[1] * u[0]
2037
+ ];
2038
+ return [u, v];
2039
+ }
2040
+ var init_ringBasis = __esm({
2041
+ "src/core/scene/kinds/_ringBasis.ts"() {
2042
+ }
2043
+ });
2044
+
1538
2045
  // src/core/scene/kinds/cylinder3d.ts
1539
2046
  var CURVED_SEGMENTS;
1540
2047
  var init_cylinder3d = __esm({
1541
2048
  "src/core/scene/kinds/cylinder3d.ts"() {
1542
2049
  init_registry();
2050
+ init_ringBasis();
1543
2051
  CURVED_SEGMENTS = 16;
1544
2052
  registerKind({
1545
2053
  type: "cylinder3d",
@@ -1558,14 +2066,17 @@ var init_cylinder3d = __esm({
1558
2066
  const r = obj.attrs.radius;
1559
2067
  const ax = a.X?.() ?? 0, ay = a.Y?.() ?? 0, az = a.Z?.() ?? 0;
1560
2068
  const bx = b.X?.() ?? 0, by = b.Y?.() ?? 0, bz = b.Z?.() ?? 0;
2069
+ const [u, v] = perpBasis([bx - ax, by - ay, bz - az]);
1561
2070
  const baseRing = [];
1562
2071
  const topRing = [];
1563
2072
  for (let i = 0; i < CURVED_SEGMENTS; i++) {
1564
2073
  const theta = i / CURVED_SEGMENTS * Math.PI * 2;
1565
- const dx = r * Math.cos(theta);
1566
- const dy = r * Math.sin(theta);
1567
- baseRing.push([ax + dx, ay + dy, az]);
1568
- topRing.push([bx + dx, by + dy, bz]);
2074
+ const c = Math.cos(theta), s = Math.sin(theta);
2075
+ const ox = r * (c * u[0] + s * v[0]);
2076
+ const oy = r * (c * u[1] + s * v[1]);
2077
+ const oz = r * (c * u[2] + s * v[2]);
2078
+ baseRing.push([ax + ox, ay + oy, az + oz]);
2079
+ topRing.push([bx + ox, by + oy, bz + oz]);
1569
2080
  }
1570
2081
  const vertices = [...baseRing, ...topRing];
1571
2082
  const faces = [];
@@ -1576,7 +2087,7 @@ var init_cylinder3d = __esm({
1576
2087
  faces.push([i, next, CURVED_SEGMENTS + next, CURVED_SEGMENTS + i]);
1577
2088
  }
1578
2089
  const vertJxgs = vertices.map(
1579
- (v, i) => view.create("point3d", v, {
2090
+ (v2, i) => view.create("point3d", v2, {
1580
2091
  id: `${obj.id}.v${i}`,
1581
2092
  visible: false,
1582
2093
  fixed: true,
@@ -1604,6 +2115,7 @@ var CURVED_SEGMENTS2;
1604
2115
  var init_cone3d = __esm({
1605
2116
  "src/core/scene/kinds/cone3d.ts"() {
1606
2117
  init_registry();
2118
+ init_ringBasis();
1607
2119
  CURVED_SEGMENTS2 = 16;
1608
2120
  registerKind({
1609
2121
  type: "cone3d",
@@ -1626,10 +2138,16 @@ var init_cone3d = __esm({
1626
2138
  apexPt.Y?.() ?? 0,
1627
2139
  apexPt.Z?.() ?? 0
1628
2140
  ];
2141
+ const [u, v] = perpBasis([apexCoords[0] - bx, apexCoords[1] - by, apexCoords[2] - bz]);
1629
2142
  const baseRing = [];
1630
2143
  for (let i = 0; i < CURVED_SEGMENTS2; i++) {
1631
2144
  const theta = i / CURVED_SEGMENTS2 * Math.PI * 2;
1632
- baseRing.push([bx + r * Math.cos(theta), by + r * Math.sin(theta), bz]);
2145
+ const c = Math.cos(theta), s = Math.sin(theta);
2146
+ baseRing.push([
2147
+ bx + r * (c * u[0] + s * v[0]),
2148
+ by + r * (c * u[1] + s * v[1]),
2149
+ bz + r * (c * u[2] + s * v[2])
2150
+ ]);
1633
2151
  }
1634
2152
  const apexIdx = baseRing.length;
1635
2153
  const vertices = [...baseRing, apexCoords];
@@ -1638,7 +2156,7 @@ var init_cone3d = __esm({
1638
2156
  faces.push([i, (i + 1) % CURVED_SEGMENTS2, apexIdx]);
1639
2157
  }
1640
2158
  const vertJxgs = vertices.map(
1641
- (v, i) => view.create("point3d", v, {
2159
+ (v2, i) => view.create("point3d", v2, {
1642
2160
  id: `${obj.id}.v${i}`,
1643
2161
  visible: false,
1644
2162
  fixed: true,
@@ -1727,8 +2245,16 @@ function constraintRefs2D(c) {
1727
2245
  }
1728
2246
  case "excenter":
1729
2247
  return [c.vertices[0], c.vertices[1], c.vertices[2]];
1730
- default:
2248
+ case "commonTangentPoint":
2249
+ return [c.circles[0], c.circles[1]];
2250
+ // Các kind KHÔNG có ref scene-object (toạ độ literal): liệt kê TƯỜNG MINH để
2251
+ // exhaustive never-guard bên dưới buộc khai báo khi thêm constraint kind mới.
2252
+ case "free":
2253
+ case "onAxis":
2254
+ return [];
2255
+ default: {
1731
2256
  return [];
2257
+ }
1732
2258
  }
1733
2259
  }
1734
2260
  var init_d_constraint2 = __esm({
@@ -2250,19 +2776,19 @@ function buildJxgTransforms(board, ctx, t) {
2250
2776
  }
2251
2777
  }
2252
2778
  function makeDistanceFn(ctx, d) {
2253
- const scale = d.scale ?? 1;
2779
+ const scale2 = d.scale ?? 1;
2254
2780
  const offset = d.offset ?? 0;
2255
2781
  if (d.kind === "literal") {
2256
2782
  const v = d.value;
2257
- return () => scale * v + offset;
2783
+ return () => scale2 * v + offset;
2258
2784
  }
2259
2785
  if (d.kind === "segmentLength") {
2260
2786
  const p = ctx.resolveRef(d.p1);
2261
2787
  const q = ctx.resolveRef(d.p2);
2262
- return () => scale * Math.hypot(p.X() - q.X(), p.Y() - q.Y()) + offset;
2788
+ return () => scale2 * Math.hypot(p.X() - q.X(), p.Y() - q.Y()) + offset;
2263
2789
  }
2264
2790
  const circle = ctx.resolveRef(d.circle);
2265
- return () => scale * circle.Radius() + offset;
2791
+ return () => scale2 * circle.Radius() + offset;
2266
2792
  }
2267
2793
  function buildPointOpts(obj) {
2268
2794
  return {
@@ -2311,6 +2837,73 @@ var init_pointAtDistance = __esm({
2311
2837
  }
2312
2838
  });
2313
2839
 
2840
+ // src/stamps/geometry-2d/geometry/commonTangent.ts
2841
+ function computeCommonTangentPoint(O1, r1, O2, r2, on, variant, side) {
2842
+ const dx = O2[0] - O1[0];
2843
+ const dy = O2[1] - O1[1];
2844
+ const d = Math.hypot(dx, dy);
2845
+ if (d < EPS) return null;
2846
+ const beta = Math.atan2(dy, dx);
2847
+ const ratio = variant === "external" ? (r1 - r2) / d : (r1 + r2) / d;
2848
+ if (Math.abs(ratio) > 1) return null;
2849
+ const gamma = Math.acos(ratio);
2850
+ const s = side === 0 ? 1 : -1;
2851
+ const angle = beta + s * gamma;
2852
+ const nx = Math.cos(angle);
2853
+ const ny = Math.sin(angle);
2854
+ if (on === 0) {
2855
+ return [O1[0] + r1 * nx, O1[1] + r1 * ny];
2856
+ }
2857
+ if (variant === "external") {
2858
+ return [O2[0] + r2 * nx, O2[1] + r2 * ny];
2859
+ }
2860
+ return [O2[0] - r2 * nx, O2[1] - r2 * ny];
2861
+ }
2862
+ var EPS;
2863
+ var init_commonTangent = __esm({
2864
+ "src/stamps/geometry-2d/geometry/commonTangent.ts"() {
2865
+ EPS = 1e-9;
2866
+ }
2867
+ });
2868
+
2869
+ // src/core/scene/kinds/point-constraints/commonTangentPoint.ts
2870
+ var commonTangentPointConstraint;
2871
+ var init_commonTangentPoint = __esm({
2872
+ "src/core/scene/kinds/point-constraints/commonTangentPoint.ts"() {
2873
+ init_commonTangent();
2874
+ init_types3();
2875
+ commonTangentPointConstraint = definePointConstraint({
2876
+ kind: "commonTangentPoint",
2877
+ describe: (obj, state, c) => {
2878
+ const c1 = state?.objects[c.circles[0]]?.label ?? c.circles[0];
2879
+ const c2 = state?.objects[c.circles[1]]?.label ?? c.circles[1];
2880
+ const v = c.variant === "internal" ? "trong" : "ngo\xE0i";
2881
+ return `${obj.label} = ti\u1EBFp \u0111i\u1EC3m ti\u1EBFp tuy\u1EBFn chung ${v} ${c1},${c2} (tr\xEAn ${c.on === 0 ? c1 : c2})`;
2882
+ },
2883
+ render: (obj, ctx, c, opts) => {
2884
+ const board = ctx.jxg;
2885
+ const k1 = ctx.resolveRef(c.circles[0]);
2886
+ const k2 = ctx.resolveRef(c.circles[1]);
2887
+ const O1 = k1?.center ?? k1?.midpoint ?? k1;
2888
+ const O2 = k2?.center ?? k2?.midpoint ?? k2;
2889
+ const compute = () => {
2890
+ const t = computeCommonTangentPoint(
2891
+ [O1.X(), O1.Y()],
2892
+ k1.Radius(),
2893
+ [O2.X(), O2.Y()],
2894
+ k2.Radius(),
2895
+ c.on,
2896
+ c.variant,
2897
+ c.side
2898
+ );
2899
+ return t ?? [0, 0];
2900
+ };
2901
+ return board.create("point", [() => compute()[0], () => compute()[1]], opts);
2902
+ }
2903
+ });
2904
+ }
2905
+ });
2906
+
2314
2907
  // src/core/scene/kinds/point-constraints/circleIntersection.ts
2315
2908
  var circleIntersectionConstraint;
2316
2909
  var init_circleIntersection = __esm({
@@ -2590,6 +3183,7 @@ var init_registry2 = __esm({
2590
3183
  init_excenter();
2591
3184
  init_mixtilinearPoint();
2592
3185
  init_pointAtDistance();
3186
+ init_commonTangentPoint();
2593
3187
  init_circleIntersection();
2594
3188
  init_circleSecondIntersection();
2595
3189
  init_secondIntersection();
@@ -2616,6 +3210,7 @@ var init_registry2 = __esm({
2616
3210
  excenterConstraint,
2617
3211
  mixtilinearPointConstraint,
2618
3212
  pointAtDistanceConstraint,
3213
+ commonTangentPointConstraint,
2619
3214
  circleIntersectionConstraint,
2620
3215
  circleSecondIntersectionConstraint,
2621
3216
  secondIntersectionConstraint,
@@ -2632,7 +3227,7 @@ var init_registry2 = __esm({
2632
3227
  });
2633
3228
 
2634
3229
  // src/core/scene/kinds/point.ts
2635
- var def3;
3230
+ var def5;
2636
3231
  var init_point = __esm({
2637
3232
  "src/core/scene/kinds/point.ts"() {
2638
3233
  init_registry();
@@ -2640,7 +3235,7 @@ var init_point = __esm({
2640
3235
  init_registry2();
2641
3236
  init_shared();
2642
3237
  init_label();
2643
- def3 = {
3238
+ def5 = {
2644
3239
  type: "point",
2645
3240
  schemaVersion: 1,
2646
3241
  migrate: {},
@@ -2718,18 +3313,18 @@ var init_point = __esm({
2718
3313
  throw new Error("point: constraint kind changed \u2014 recreate");
2719
3314
  }
2720
3315
  };
2721
- registerKind(def3);
3316
+ registerKind(def5);
2722
3317
  }
2723
3318
  });
2724
3319
 
2725
3320
  // src/core/scene/kinds/segment.ts
2726
- var def4;
3321
+ var def6;
2727
3322
  var init_segment = __esm({
2728
3323
  "src/core/scene/kinds/segment.ts"() {
2729
3324
  init_registry();
2730
3325
  init_labelOf();
2731
3326
  init_label();
2732
- def4 = {
3327
+ def6 = {
2733
3328
  type: "segment",
2734
3329
  schemaVersion: 1,
2735
3330
  migrate: {},
@@ -2765,7 +3360,7 @@ var init_segment = __esm({
2765
3360
  });
2766
3361
  }
2767
3362
  };
2768
- registerKind(def4);
3363
+ registerKind(def6);
2769
3364
  }
2770
3365
  });
2771
3366
 
@@ -2793,14 +3388,14 @@ function constructionRefs(c) {
2793
3388
  return [c.throughPoint, c.toCircle];
2794
3389
  }
2795
3390
  }
2796
- var def5;
3391
+ var def7;
2797
3392
  var init_line = __esm({
2798
3393
  "src/core/scene/kinds/line.ts"() {
2799
3394
  init_registry();
2800
3395
  init_labelOf();
2801
3396
  init_label();
2802
3397
  init_pointConstructions();
2803
- def5 = {
3398
+ def7 = {
2804
3399
  type: "line",
2805
3400
  schemaVersion: 1,
2806
3401
  migrate: {},
@@ -2996,17 +3591,17 @@ var init_line = __esm({
2996
3591
  }
2997
3592
  }
2998
3593
  };
2999
- registerKind(def5);
3594
+ registerKind(def7);
3000
3595
  }
3001
3596
  });
3002
3597
 
3003
3598
  // src/core/scene/kinds/ray.ts
3004
- var def6;
3599
+ var def8;
3005
3600
  var init_ray = __esm({
3006
3601
  "src/core/scene/kinds/ray.ts"() {
3007
3602
  init_registry();
3008
3603
  init_labelOf();
3009
- def6 = {
3604
+ def8 = {
3010
3605
  type: "ray",
3011
3606
  schemaVersion: 1,
3012
3607
  migrate: {},
@@ -3031,17 +3626,17 @@ var init_ray = __esm({
3031
3626
  });
3032
3627
  }
3033
3628
  };
3034
- registerKind(def6);
3629
+ registerKind(def8);
3035
3630
  }
3036
3631
  });
3037
3632
 
3038
3633
  // src/core/scene/kinds/vector.ts
3039
- var def7;
3634
+ var def9;
3040
3635
  var init_vector = __esm({
3041
3636
  "src/core/scene/kinds/vector.ts"() {
3042
3637
  init_registry();
3043
3638
  init_labelOf();
3044
- def7 = {
3639
+ def9 = {
3045
3640
  type: "vector",
3046
3641
  schemaVersion: 1,
3047
3642
  migrate: {},
@@ -3063,7 +3658,7 @@ var init_vector = __esm({
3063
3658
  });
3064
3659
  }
3065
3660
  };
3066
- registerKind(def7);
3661
+ registerKind(def9);
3067
3662
  }
3068
3663
  });
3069
3664
 
@@ -3093,14 +3688,14 @@ function constructionRefs2(c) {
3093
3688
  return [c.p1, c.p2];
3094
3689
  }
3095
3690
  }
3096
- var def8;
3691
+ var def10;
3097
3692
  var init_circle = __esm({
3098
3693
  "src/core/scene/kinds/circle.ts"() {
3099
3694
  init_registry();
3100
3695
  init_labelOf();
3101
3696
  init_label();
3102
3697
  init_pointConstructions();
3103
- def8 = {
3698
+ def10 = {
3104
3699
  type: "circle",
3105
3700
  schemaVersion: 1,
3106
3701
  migrate: {},
@@ -3252,7 +3847,7 @@ var init_circle = __esm({
3252
3847
  return board.create("circle", [center, surface], baseOpts);
3253
3848
  }
3254
3849
  };
3255
- registerKind(def8);
3850
+ registerKind(def10);
3256
3851
  }
3257
3852
  });
3258
3853
 
@@ -3267,12 +3862,12 @@ function constructionRefs3(c) {
3267
3862
  return [c.p1, c.p2, c.p3];
3268
3863
  }
3269
3864
  }
3270
- var def9;
3865
+ var def11;
3271
3866
  var init_arc = __esm({
3272
3867
  "src/core/scene/kinds/arc.ts"() {
3273
3868
  init_registry();
3274
3869
  init_labelOf();
3275
- def9 = {
3870
+ def11 = {
3276
3871
  type: "arc",
3277
3872
  schemaVersion: 1,
3278
3873
  migrate: {},
@@ -3330,17 +3925,17 @@ var init_arc = __esm({
3330
3925
  return board.create("circumcirclearc", [A, B, C], baseOpts);
3331
3926
  }
3332
3927
  };
3333
- registerKind(def9);
3928
+ registerKind(def11);
3334
3929
  }
3335
3930
  });
3336
3931
 
3337
3932
  // src/core/scene/kinds/sector.ts
3338
- var def10;
3933
+ var def12;
3339
3934
  var init_sector = __esm({
3340
3935
  "src/core/scene/kinds/sector.ts"() {
3341
3936
  init_registry();
3342
3937
  init_labelOf();
3343
- def10 = {
3938
+ def12 = {
3344
3939
  type: "sector",
3345
3940
  schemaVersion: 1,
3346
3941
  migrate: {},
@@ -3380,7 +3975,7 @@ var init_sector = __esm({
3380
3975
  });
3381
3976
  }
3382
3977
  };
3383
- registerKind(def10);
3978
+ registerKind(def12);
3384
3979
  }
3385
3980
  });
3386
3981
 
@@ -3426,12 +4021,12 @@ function specialShapeName(kind) {
3426
4021
  return "";
3427
4022
  }
3428
4023
  }
3429
- var def11;
4024
+ var def13;
3430
4025
  var init_polygon = __esm({
3431
4026
  "src/core/scene/kinds/polygon.ts"() {
3432
4027
  init_registry();
3433
4028
  init_labelOf();
3434
- def11 = {
4029
+ def13 = {
3435
4030
  type: "polygon",
3436
4031
  schemaVersion: 1,
3437
4032
  migrate: {},
@@ -3618,16 +4213,16 @@ var init_polygon = __esm({
3618
4213
  return poly;
3619
4214
  }
3620
4215
  };
3621
- registerKind(def11);
4216
+ registerKind(def13);
3622
4217
  }
3623
4218
  });
3624
4219
 
3625
4220
  // src/core/scene/kinds/intersection.ts
3626
- var def12;
4221
+ var def14;
3627
4222
  var init_intersection = __esm({
3628
4223
  "src/core/scene/kinds/intersection.ts"() {
3629
4224
  init_registry();
3630
- def12 = {
4225
+ def14 = {
3631
4226
  type: "intersection",
3632
4227
  schemaVersion: 1,
3633
4228
  migrate: {},
@@ -3702,17 +4297,17 @@ var init_intersection = __esm({
3702
4297
  }
3703
4298
  }
3704
4299
  };
3705
- registerKind(def12);
4300
+ registerKind(def14);
3706
4301
  }
3707
4302
  });
3708
4303
 
3709
4304
  // src/core/scene/kinds/angle.ts
3710
- var def13;
4305
+ var def15;
3711
4306
  var init_angle = __esm({
3712
4307
  "src/core/scene/kinds/angle.ts"() {
3713
4308
  init_registry();
3714
4309
  init_labelOf();
3715
- def13 = {
4310
+ def15 = {
3716
4311
  type: "angle",
3717
4312
  schemaVersion: 1,
3718
4313
  migrate: {},
@@ -3748,17 +4343,17 @@ var init_angle = __esm({
3748
4343
  });
3749
4344
  }
3750
4345
  };
3751
- registerKind(def13);
4346
+ registerKind(def15);
3752
4347
  }
3753
4348
  });
3754
4349
 
3755
4350
  // src/core/scene/kinds/distance.ts
3756
- var def14;
4351
+ var def16;
3757
4352
  var init_distance = __esm({
3758
4353
  "src/core/scene/kinds/distance.ts"() {
3759
4354
  init_registry();
3760
4355
  init_labelOf();
3761
- def14 = {
4356
+ def16 = {
3762
4357
  type: "distance",
3763
4358
  schemaVersion: 1,
3764
4359
  migrate: {},
@@ -3787,17 +4382,17 @@ var init_distance = __esm({
3787
4382
  });
3788
4383
  }
3789
4384
  };
3790
- registerKind(def14);
4385
+ registerKind(def16);
3791
4386
  }
3792
4387
  });
3793
4388
 
3794
4389
  // src/core/scene/kinds/function2d.ts
3795
- var def15;
4390
+ var def17;
3796
4391
  var init_function2d = __esm({
3797
4392
  "src/core/scene/kinds/function2d.ts"() {
3798
4393
  init_registry();
3799
4394
  init_parser();
3800
- def15 = {
4395
+ def17 = {
3801
4396
  type: "function2d",
3802
4397
  schemaVersion: 1,
3803
4398
  migrate: {},
@@ -3836,16 +4431,16 @@ var init_function2d = __esm({
3836
4431
  });
3837
4432
  }
3838
4433
  };
3839
- registerKind(def15);
4434
+ registerKind(def17);
3840
4435
  }
3841
4436
  });
3842
4437
 
3843
4438
  // src/core/scene/kinds/parameter.ts
3844
- var def16;
4439
+ var def18;
3845
4440
  var init_parameter = __esm({
3846
4441
  "src/core/scene/kinds/parameter.ts"() {
3847
4442
  init_registry();
3848
- def16 = {
4443
+ def18 = {
3849
4444
  type: "parameter",
3850
4445
  schemaVersion: 1,
3851
4446
  migrate: {},
@@ -3863,16 +4458,16 @@ var init_parameter = __esm({
3863
4458
  render: () => null
3864
4459
  // Không render lên board
3865
4460
  };
3866
- registerKind(def16);
4461
+ registerKind(def18);
3867
4462
  }
3868
4463
  });
3869
4464
 
3870
4465
  // src/core/scene/kinds/pointOnCurve.ts
3871
- var def17;
4466
+ var def19;
3872
4467
  var init_pointOnCurve = __esm({
3873
4468
  "src/core/scene/kinds/pointOnCurve.ts"() {
3874
4469
  init_registry();
3875
- def17 = {
4470
+ def19 = {
3876
4471
  type: "pointOnCurve",
3877
4472
  schemaVersion: 1,
3878
4473
  migrate: {},
@@ -3899,16 +4494,16 @@ var init_pointOnCurve = __esm({
3899
4494
  });
3900
4495
  }
3901
4496
  };
3902
- registerKind(def17);
4497
+ registerKind(def19);
3903
4498
  }
3904
4499
  });
3905
4500
 
3906
4501
  // src/core/scene/kinds/tangent2d.ts
3907
- var def18;
4502
+ var def20;
3908
4503
  var init_tangent2d = __esm({
3909
4504
  "src/core/scene/kinds/tangent2d.ts"() {
3910
4505
  init_registry();
3911
- def18 = {
4506
+ def20 = {
3912
4507
  type: "tangent2d",
3913
4508
  schemaVersion: 1,
3914
4509
  migrate: {},
@@ -3931,7 +4526,7 @@ var init_tangent2d = __esm({
3931
4526
  });
3932
4527
  }
3933
4528
  };
3934
- registerKind(def18);
4529
+ registerKind(def20);
3935
4530
  }
3936
4531
  });
3937
4532
 
@@ -4007,13 +4602,13 @@ var init_evaluator = __esm({
4007
4602
  });
4008
4603
 
4009
4604
  // src/core/scene/kinds/extremum2d.ts
4010
- var def19;
4605
+ var def21;
4011
4606
  var init_extremum2d = __esm({
4012
4607
  "src/core/scene/kinds/extremum2d.ts"() {
4013
4608
  init_registry();
4014
4609
  init_evaluator();
4015
4610
  init_parser();
4016
- def19 = {
4611
+ def21 = {
4017
4612
  type: "extremum2d",
4018
4613
  schemaVersion: 1,
4019
4614
  migrate: {},
@@ -4046,18 +4641,18 @@ var init_extremum2d = __esm({
4046
4641
  }));
4047
4642
  }
4048
4643
  };
4049
- registerKind(def19);
4644
+ registerKind(def21);
4050
4645
  }
4051
4646
  });
4052
4647
 
4053
4648
  // src/core/scene/kinds/root2d.ts
4054
- var def20;
4649
+ var def22;
4055
4650
  var init_root2d = __esm({
4056
4651
  "src/core/scene/kinds/root2d.ts"() {
4057
4652
  init_registry();
4058
4653
  init_evaluator();
4059
4654
  init_parser();
4060
- def20 = {
4655
+ def22 = {
4061
4656
  type: "root2d",
4062
4657
  schemaVersion: 1,
4063
4658
  migrate: {},
@@ -4087,16 +4682,16 @@ var init_root2d = __esm({
4087
4682
  }));
4088
4683
  }
4089
4684
  };
4090
- registerKind(def20);
4685
+ registerKind(def22);
4091
4686
  }
4092
4687
  });
4093
4688
 
4094
4689
  // src/core/scene/kinds/slope2d.ts
4095
- var def21;
4690
+ var def23;
4096
4691
  var init_slope2d = __esm({
4097
4692
  "src/core/scene/kinds/slope2d.ts"() {
4098
4693
  init_registry();
4099
- def21 = {
4694
+ def23 = {
4100
4695
  type: "slope2d",
4101
4696
  schemaVersion: 1,
4102
4697
  migrate: {},
@@ -4120,7 +4715,7 @@ var init_slope2d = __esm({
4120
4715
  });
4121
4716
  }
4122
4717
  };
4123
- registerKind(def21);
4718
+ registerKind(def23);
4124
4719
  }
4125
4720
  });
4126
4721
 
@@ -5819,7 +6414,7 @@ var init_useToolHoverTooltip = __esm({
5819
6414
  init_types4();
5820
6415
  }
5821
6416
  });
5822
- function normalize(s) {
6417
+ function normalize2(s) {
5823
6418
  return s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "d");
5824
6419
  }
5825
6420
  function SearchIcon() {
@@ -5866,12 +6461,12 @@ function ToolGrid(props) {
5866
6461
  const { tools, groupOrder, groupLabels, activeTool, onToolChange, chord } = props;
5867
6462
  const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
5868
6463
  const [query, setQuery] = React5.useState("");
5869
- const normalizedQuery = React5.useMemo(() => normalize(query.trim()), [query]);
6464
+ const normalizedQuery = React5.useMemo(() => normalize2(query.trim()), [query]);
5870
6465
  const filteredTools = React5.useMemo(() => {
5871
6466
  if (!normalizedQuery) return tools;
5872
6467
  return tools.filter((t) => {
5873
- if (normalize(t.label).includes(normalizedQuery)) return true;
5874
- if (t.hint && normalize(t.hint).includes(normalizedQuery)) return true;
6468
+ if (normalize2(t.label).includes(normalizedQuery)) return true;
6469
+ if (t.hint && normalize2(t.hint).includes(normalizedQuery)) return true;
5875
6470
  return false;
5876
6471
  });
5877
6472
  }, [tools, normalizedQuery]);
@@ -6461,9 +7056,9 @@ async function insertStampImage(api, opts) {
6461
7056
  const old = elements.find((e) => e.id === editingId) ;
6462
7057
  const oldLongest = old ? Math.max(old.width ?? 0, old.height ?? 0) : 0;
6463
7058
  const newLongest = Math.max(width, height);
6464
- const scale = oldLongest > 0 && newLongest > 0 ? oldLongest / newLongest : 1;
6465
- const w = width * scale;
6466
- const h = height * scale;
7059
+ const scale2 = oldLongest > 0 && newLongest > 0 ? oldLongest / newLongest : 1;
7060
+ const w = width * scale2;
7061
+ const h = height * scale2;
6467
7062
  const updated = elements.map(
6468
7063
  (e) => e.id === editingId ? { ...e, fileId, customData, width: w, height: h } : e
6469
7064
  );