@alpaca-software/40kdc-data 0.5.14 → 0.5.16

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 (96) hide show
  1. package/dist/abilities-resolver/resolver.d.ts.map +1 -1
  2. package/dist/abilities-resolver/resolver.js +6 -2
  3. package/dist/abilities-resolver/resolver.js.map +1 -1
  4. package/dist/audit-coverage.d.ts.map +1 -1
  5. package/dist/audit-coverage.js +90 -1
  6. package/dist/audit-coverage.js.map +1 -1
  7. package/dist/audit-phrasing.d.ts +64 -0
  8. package/dist/audit-phrasing.d.ts.map +1 -0
  9. package/dist/audit-phrasing.js +208 -0
  10. package/dist/audit-phrasing.js.map +1 -0
  11. package/dist/build-share-registry.d.ts +2 -0
  12. package/dist/build-share-registry.d.ts.map +1 -0
  13. package/dist/build-share-registry.js +115 -0
  14. package/dist/build-share-registry.js.map +1 -0
  15. package/dist/cli.js +7 -0
  16. package/dist/cli.js.map +1 -1
  17. package/dist/codegen-data.js +23 -0
  18. package/dist/codegen-data.js.map +1 -1
  19. package/dist/commands/translate.d.ts.map +1 -1
  20. package/dist/commands/translate.js +2 -5
  21. package/dist/commands/translate.js.map +1 -1
  22. package/dist/commands/validate-all.d.ts.map +1 -1
  23. package/dist/commands/validate-all.js +8 -3
  24. package/dist/commands/validate-all.js.map +1 -1
  25. package/dist/data/bundle.generated.js +1 -1
  26. package/dist/data/bundle.generated.js.map +1 -1
  27. package/dist/data/dataset.d.ts +33 -1
  28. package/dist/data/dataset.d.ts.map +1 -1
  29. package/dist/data/dataset.js +107 -1
  30. package/dist/data/dataset.js.map +1 -1
  31. package/dist/data/index.d.ts +4 -0
  32. package/dist/data/index.d.ts.map +1 -1
  33. package/dist/data/index.js +4 -0
  34. package/dist/data/index.js.map +1 -1
  35. package/dist/data/types.d.ts +3 -1
  36. package/dist/data/types.d.ts.map +1 -1
  37. package/dist/data/types.js +1 -0
  38. package/dist/data/types.js.map +1 -1
  39. package/dist/gen-conformance.js +162 -0
  40. package/dist/gen-conformance.js.map +1 -1
  41. package/dist/generated.d.ts +125 -0
  42. package/dist/generated.d.ts.map +1 -1
  43. package/dist/generated.js.map +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/integrity.d.ts +32 -0
  49. package/dist/integrity.d.ts.map +1 -0
  50. package/dist/integrity.js +120 -0
  51. package/dist/integrity.js.map +1 -0
  52. package/dist/report.d.ts +1 -1
  53. package/dist/report.d.ts.map +1 -1
  54. package/dist/report.js +2 -2
  55. package/dist/report.js.map +1 -1
  56. package/dist/runner.d.ts.map +1 -1
  57. package/dist/runner.js +42 -0
  58. package/dist/runner.js.map +1 -1
  59. package/dist/share/codec.d.ts +83 -0
  60. package/dist/share/codec.d.ts.map +1 -0
  61. package/dist/share/codec.js +230 -0
  62. package/dist/share/codec.js.map +1 -0
  63. package/dist/share/index.d.ts +30 -0
  64. package/dist/share/index.d.ts.map +1 -0
  65. package/dist/share/index.js +36 -0
  66. package/dist/share/index.js.map +1 -0
  67. package/dist/share/registry.d.ts +42 -0
  68. package/dist/share/registry.d.ts.map +1 -0
  69. package/dist/share/registry.generated.d.ts +4 -0
  70. package/dist/share/registry.generated.d.ts.map +1 -0
  71. package/dist/share/registry.generated.js +4 -0
  72. package/dist/share/registry.generated.js.map +1 -0
  73. package/dist/share/registry.js +68 -0
  74. package/dist/share/registry.js.map +1 -0
  75. package/dist/terrain/index.d.ts +2 -2
  76. package/dist/terrain/index.d.ts.map +1 -1
  77. package/dist/terrain/index.js +1 -1
  78. package/dist/terrain/index.js.map +1 -1
  79. package/dist/terrain/solve.d.ts +76 -0
  80. package/dist/terrain/solve.d.ts.map +1 -1
  81. package/dist/terrain/solve.js +138 -0
  82. package/dist/terrain/solve.js.map +1 -1
  83. package/dist/translate/condition.d.ts +1 -0
  84. package/dist/translate/condition.d.ts.map +1 -1
  85. package/dist/translate/condition.js +101 -7
  86. package/dist/translate/condition.js.map +1 -1
  87. package/dist/translate/effect.d.ts +28 -20
  88. package/dist/translate/effect.d.ts.map +1 -1
  89. package/dist/translate/effect.js +483 -114
  90. package/dist/translate/effect.js.map +1 -1
  91. package/dist/validate.d.ts.map +1 -1
  92. package/dist/validate.js +1 -0
  93. package/dist/validate.js.map +1 -1
  94. package/package.json +3 -1
  95. package/schemas/core/allied-rule.schema.json +115 -0
  96. package/schemas/core/detachment.schema.json +38 -1
@@ -152,4 +152,80 @@ export declare function solveCentroidAttached(input: AttachInput): {
152
152
  rotation: number;
153
153
  };
154
154
  };
155
+ /**
156
+ * One card dimension line in a fixed-anchor solve: `distance` inches from board
157
+ * `edge` to a specific footprint `vertex` of the moving piece. This single line
158
+ * pins the one degree of freedom the attachment to the fixed anchor leaves.
159
+ */
160
+ export interface FixedLockLine {
161
+ edge: BoardEdge;
162
+ distance: number;
163
+ vertex: number;
164
+ }
165
+ /**
166
+ * The already-placed piece the moving piece attaches to. Its pose is taken as
167
+ * given — `vertices` are its resolved board-space polygon vertices (the
168
+ * orientation-only offsets from {@link orientedOffsets} added to the piece's
169
+ * board centroid) — and the solve never moves it.
170
+ */
171
+ export interface FixedAnchor {
172
+ vertices: Vec2[];
173
+ /**
174
+ * The anchor feature the moving piece contacts: a specific vertex, or the
175
+ * footprint edge running from vertex `index` to vertex `index + 1` (wrapping).
176
+ */
177
+ attach: {
178
+ kind: "vertex" | "edge";
179
+ index: number;
180
+ };
181
+ }
182
+ /** The piece being placed against a {@link FixedAnchor}. */
183
+ export interface MovingAttachPiece {
184
+ footprint: Footprint;
185
+ mirror: Mirror;
186
+ /**
187
+ * The moving feature that meets the anchor: a vertex (coincides with the
188
+ * anchor vertex — the joint pivots), or the edge `index → index + 1` (lies
189
+ * flush on the anchor edge — the contact slides). Must match the anchor's kind.
190
+ */
191
+ attach: {
192
+ kind: "vertex" | "edge";
193
+ index: number;
194
+ };
195
+ /** The single card line pinning the remaining freedom. */
196
+ line: FixedLockLine;
197
+ /** Current rotation in degrees, used to pick among the candidate roots. */
198
+ rotationHint?: number;
199
+ }
200
+ export interface SolveAgainstFixedInput {
201
+ /** Board extents in inches (40kdc standard is 60 × 44). */
202
+ board: {
203
+ width: number;
204
+ height: number;
205
+ };
206
+ moving: MovingAttachPiece;
207
+ fixed: FixedAnchor;
208
+ }
209
+ /**
210
+ * Back-solve the centroid AND rotation of ONE piece against an already-placed
211
+ * anchor. The attachment removes two degrees of freedom — corners coincide
212
+ * (the joint pivots) or edges lie flush (the contact slides) — so a single card
213
+ * dimension line pins what remains. The anchor is given by its resolved
214
+ * board-space `vertices` and never moves.
215
+ *
216
+ * Closed form:
217
+ * - **vertex mode** — the moving attach vertex equals the fixed point `P`, so
218
+ * `centroid = P − R(θ)·o_att`. The lock line to vertex `j` resolves to
219
+ * `P + R(θ)·(o_j − o_att)`; pinning one axis gives `A·cosθ + B·sinθ = C`,
220
+ * solved `θ = atan2(B,A) ± acos(C/√(A²+B²))`, the root nearest `rotationHint`.
221
+ * - **edge mode** — flush forces the moving edge parallel to the anchor edge
222
+ * (two senses → two θ), and for each θ the centroid solves from a 2×2 linear
223
+ * system: the flush line equation plus the single lock line. The candidate
224
+ * nearest `rotationHint` wins.
225
+ */
226
+ export declare function solveCentroidAgainstFixed(input: SolveAgainstFixedInput): {
227
+ x: number;
228
+ y: number;
229
+ rotation: number;
230
+ };
155
231
  //# sourceMappingURL=solve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"solve.d.ts","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,IAAI,EACV,MAAM,cAAc,CAAC;AAItB,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1D,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,iFAAiF;IACjF,KAAK,EAAE,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;CACvC;AAED,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAkD/C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAYrD;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC;;;OAGG;IACH,KAAK,EAAE,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;IACjE,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,GACtB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAuF5C;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,KAAK,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IAChC;;;OAGG;IACH,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,8BAA8B;IAC9B,CAAC,EAAE,WAAW,CAAC;IACf,gCAAgC;IAChC,CAAC,EAAE,WAAW,CAAC;CAChB;AAyKD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,WAAW,GAAG;IACzD,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C,CAoBA"}
1
+ {"version":3,"file":"solve.d.ts","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"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"}
@@ -342,4 +342,142 @@ export function solveCentroidAttached(input) {
342
342
  : bestSoFar);
343
343
  return { a: attachPose(pa, best.thetaA), b: attachPose(pb, best.thetaB) };
344
344
  }
345
+ /** The board coordinate (and axis) a single edge measurement pins. */
346
+ function edgeTarget(edge, distance, board) {
347
+ switch (edge) {
348
+ case "left":
349
+ return { axis: "x", value: distance };
350
+ case "right":
351
+ return { axis: "x", value: board.width - distance };
352
+ case "top":
353
+ return { axis: "y", value: distance };
354
+ case "bottom":
355
+ return { axis: "y", value: board.height - distance };
356
+ }
357
+ }
358
+ /**
359
+ * Back-solve the centroid AND rotation of ONE piece against an already-placed
360
+ * anchor. The attachment removes two degrees of freedom — corners coincide
361
+ * (the joint pivots) or edges lie flush (the contact slides) — so a single card
362
+ * dimension line pins what remains. The anchor is given by its resolved
363
+ * board-space `vertices` and never moves.
364
+ *
365
+ * Closed form:
366
+ * - **vertex mode** — the moving attach vertex equals the fixed point `P`, so
367
+ * `centroid = P − R(θ)·o_att`. The lock line to vertex `j` resolves to
368
+ * `P + R(θ)·(o_j − o_att)`; pinning one axis gives `A·cosθ + B·sinθ = C`,
369
+ * solved `θ = atan2(B,A) ± acos(C/√(A²+B²))`, the root nearest `rotationHint`.
370
+ * - **edge mode** — flush forces the moving edge parallel to the anchor edge
371
+ * (two senses → two θ), and for each θ the centroid solves from a 2×2 linear
372
+ * system: the flush line equation plus the single lock line. The candidate
373
+ * nearest `rotationHint` wins.
374
+ */
375
+ export function solveCentroidAgainstFixed(input) {
376
+ const { moving, fixed, board } = input;
377
+ if (moving.attach.kind !== fixed.attach.kind) {
378
+ throw new TerrainSolveError("attachment kinds must match: a vertex pins to a vertex, an edge lies flush on an edge — no mixing");
379
+ }
380
+ // Mirror-applied, pre-rotation offsets (θ is the unknown we solve for).
381
+ const offsets = orientedOffsets(moving.footprint, 0, moving.mirror);
382
+ const n = offsets.length;
383
+ const m = fixed.vertices.length;
384
+ if (moving.attach.index < 0 || moving.attach.index >= n) {
385
+ throw new TerrainSolveError(`moving attach ${moving.attach.kind} index ${moving.attach.index} out of range`);
386
+ }
387
+ if (fixed.attach.index < 0 || fixed.attach.index >= m) {
388
+ throw new TerrainSolveError(`fixed attach ${fixed.attach.kind} index ${fixed.attach.index} out of range`);
389
+ }
390
+ const oj = offsets[moving.line.vertex];
391
+ if (!oj)
392
+ throw new TerrainSolveError(`lock line vertex index ${moving.line.vertex} out of range`);
393
+ const hint = ((moving.rotationHint ?? 0) * Math.PI) / 180;
394
+ const target = edgeTarget(moving.line.edge, moving.line.distance, board);
395
+ const pose = (theta, centroid) => ({
396
+ x: centroid.x,
397
+ y: centroid.y,
398
+ rotation: ((((theta * 180) / Math.PI) % 360) + 360) % 360,
399
+ });
400
+ if (moving.attach.kind === "vertex") {
401
+ const P = fixed.vertices[fixed.attach.index];
402
+ const oAtt = offsets[moving.attach.index];
403
+ const rel = { x: oj.x - oAtt.x, y: oj.y - oAtt.y };
404
+ if (Math.hypot(rel.x, rel.y) < 1e-9) {
405
+ throw new TerrainSolveError("the lock line must measure to a different vertex than the attach corner");
406
+ }
407
+ // P.axis + (R(θ)·rel).axis = target → A·cosθ + B·sinθ = C.
408
+ const A = target.axis === "x" ? rel.x : rel.y;
409
+ const B = target.axis === "x" ? -rel.y : rel.x;
410
+ const C = target.value - (target.axis === "x" ? P.x : P.y);
411
+ const R = Math.hypot(A, B);
412
+ const ratio = C / R;
413
+ if (ratio > 1 + 1e-6 || ratio < -1 - 1e-6) {
414
+ throw new TerrainSolveError(`the locked corner is only ${round2(R)}″ from the attach corner — it cannot reach that measurement`);
415
+ }
416
+ const phi = Math.atan2(B, A);
417
+ const baseAcos = Math.acos(Math.max(-1, Math.min(1, ratio)));
418
+ const theta = [phi + baseAcos, phi - baseAcos].reduce((best, c) => angularGap(c, hint) < angularGap(best, hint) ? c : best);
419
+ const cos = Math.cos(theta);
420
+ const sin = Math.sin(theta);
421
+ // centroid = P − R(θ)·o_att.
422
+ const centroid = {
423
+ x: P.x - (cos * oAtt.x - sin * oAtt.y),
424
+ y: P.y - (sin * oAtt.x + cos * oAtt.y),
425
+ };
426
+ return pose(theta, centroid);
427
+ }
428
+ // Edge mode: the moving edge ei→ei+1 lies flush on the anchor edge fi→fi+1.
429
+ const ei = moving.attach.index;
430
+ const oe = offsets[ei];
431
+ const oe1 = offsets[(ei + 1) % n];
432
+ const u = { x: oe1.x - oe.x, y: oe1.y - oe.y };
433
+ if (Math.hypot(u.x, u.y) < 1e-9) {
434
+ throw new TerrainSolveError(`moving edge ${ei} is degenerate (zero length)`);
435
+ }
436
+ const fi = fixed.attach.index;
437
+ const F0 = fixed.vertices[fi];
438
+ const F1 = fixed.vertices[(fi + 1) % m];
439
+ const t = { x: F1.x - F0.x, y: F1.y - F0.y };
440
+ const tLen = Math.hypot(t.x, t.y);
441
+ if (tLen < 1e-9) {
442
+ throw new TerrainSolveError(`anchor edge ${fi} is degenerate (zero length)`);
443
+ }
444
+ const tHat = { x: t.x / tLen, y: t.y / tLen };
445
+ const uAng = angle(u);
446
+ const tAng = angle(tHat);
447
+ // angle(R(θ)·u) = angle(u) + θ, so parallel ⇒ θ = tAng − uAng (same sense) or +π (opposite).
448
+ const candidates = [tAng - uAng, tAng + Math.PI - uAng];
449
+ let best = null;
450
+ for (const theta of candidates) {
451
+ const cos = Math.cos(theta);
452
+ const sin = Math.sin(theta);
453
+ const rot = (v) => ({ x: cos * v.x - sin * v.y, y: sin * v.x + cos * v.y });
454
+ // Flush: cross(t̂, c + R(θ)·o_e − F0) = 0.
455
+ // (−t̂.y)·cx + (t̂.x)·cy = t̂.y·q.x − t̂.x·q.y, q = R(θ)·o_e − F0
456
+ const q = rot(oe);
457
+ q.x -= F0.x;
458
+ q.y -= F0.y;
459
+ const a1 = -tHat.y;
460
+ const b1 = tHat.x;
461
+ const d1 = tHat.y * q.x - tHat.x * q.y;
462
+ // Lock line: pins one axis of (c + R(θ)·o_j).
463
+ const rj = rot(oj);
464
+ const a2 = target.axis === "x" ? 1 : 0;
465
+ const b2 = target.axis === "x" ? 0 : 1;
466
+ const d2 = target.value - (target.axis === "x" ? rj.x : rj.y);
467
+ const det = a1 * b2 - b1 * a2;
468
+ if (Math.abs(det) < 1e-9)
469
+ continue; // lock line parallel to the contact edge for this θ
470
+ const centroid = {
471
+ x: (d1 * b2 - b1 * d2) / det,
472
+ y: (a1 * d2 - d1 * a2) / det,
473
+ };
474
+ if (best === null || angularGap(theta, hint) < angularGap(best.theta, hint)) {
475
+ best = { theta, centroid };
476
+ }
477
+ }
478
+ if (best === null) {
479
+ throw new TerrainSolveError("the lock line is parallel to the contact edge — measure to the other axis (left/right vs top/bottom)");
480
+ }
481
+ return pose(best.theta, best.centroid);
482
+ }
345
483
  //# sourceMappingURL=solve.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"solve.js","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Card-measurement centroid solver — the inverse of the resolver's placement.\n *\n * Reference cards locate a terrain area by dimension lines: \"this feature of the\n * area is D inches from a board edge\". The feature referenced varies per card\n * and per piece, which is exactly why a single canonical anchor (the centroid)\n * is hard to read off a card directly. This solver lets a user transcribe the\n * card verbatim — pick the template, set the orientation shown, then enter one\n * horizontal and one vertical dimension line against whatever feature the card\n * happens to draw — and back-solves the centroid `position` the schema stores.\n *\n * Because the centroid is rotation- and mirror-invariant, orientation is fixed\n * first; each dimension line then pins one axis of the centroid in closed form.\n */\nimport {\n orientedOffsets,\n type BoardEdge,\n type FeatureRef,\n type Footprint,\n type Mirror,\n type Vec2,\n} from \"./resolve.js\";\n\n// The edge/feature vocabulary lives in resolve.ts (shared with keystones);\n// re-exported here so existing solver imports keep working.\nexport type { BoardEdge, FeatureRef } from \"./resolve.js\";\n\n/** One card dimension line: `distance` inches from `edge` to `feature`. */\nexport interface DimensionLine {\n edge: BoardEdge;\n distance: number;\n feature: FeatureRef;\n}\n\nexport interface SolveInput {\n footprint: Footprint;\n rotation: number;\n mirror: Mirror;\n /** Board extents in inches (40kdc standard is 60 × 44). */\n board: { width: number; height: number };\n /** Two perpendicular dimension lines: exactly one must pin x, one must pin y. */\n lines: [DimensionLine, DimensionLine];\n}\n\nexport class TerrainSolveError extends Error {}\n\n/** The signed offset (from the centroid) the given feature resolves to, on its axis. */\nfunction featureOffset(offsets: Vec2[], feature: FeatureRef, axis: \"x\" | \"y\"): number {\n if (feature.kind === \"vertex\") {\n const o = offsets[feature.index];\n if (!o) throw new TerrainSolveError(`vertex index ${feature.index} out of range`);\n return axis === \"x\" ? o.x : o.y;\n }\n const xs = offsets.map((o) => o.x);\n const ys = offsets.map((o) => o.y);\n switch (feature.side) {\n case \"min-x\":\n return Math.min(...xs);\n case \"max-x\":\n return Math.max(...xs);\n case \"min-y\":\n return Math.min(...ys);\n case \"max-y\":\n return Math.max(...ys);\n }\n}\n\nfunction axisOfEdge(edge: BoardEdge): \"x\" | \"y\" {\n return edge === \"left\" || edge === \"right\" ? \"x\" : \"y\";\n}\n\n/** Solve one axis of the centroid from a single dimension line. */\nfunction solveAxis(line: DimensionLine, offsets: Vec2[], board: { width: number; height: number }): { axis: \"x\" | \"y\"; value: number } {\n const axis = axisOfEdge(line.edge);\n const o = featureOffset(offsets, line.feature, axis);\n // edge → centroid: near-side edges measure from 0; far-side from the extent.\n let value: number;\n switch (line.edge) {\n case \"left\":\n value = line.distance - o;\n break;\n case \"right\":\n value = board.width - line.distance - o;\n break;\n case \"top\":\n value = line.distance - o;\n break;\n case \"bottom\":\n value = board.height - line.distance - o;\n break;\n }\n return { axis, value };\n}\n\n/**\n * Back-solve the centroid `position` from a template, its orientation, and two\n * perpendicular card dimension lines. Closed form — one x-line and one y-line\n * pin the two unknowns directly.\n */\nexport function solveCentroid(input: SolveInput): Vec2 {\n const offsets = orientedOffsets(input.footprint, input.rotation, input.mirror);\n const a = solveAxis(input.lines[0], offsets, input.board);\n const b = solveAxis(input.lines[1], offsets, input.board);\n if (a.axis === b.axis) {\n throw new TerrainSolveError(\n \"the two dimension lines must pin different axes (one of left/right, one of top/bottom)\",\n );\n }\n const x = a.axis === \"x\" ? a.value : b.value;\n const y = a.axis === \"y\" ? a.value : b.value;\n return { x, y };\n}\n\n/**\n * One triangulation measurement: `distance` inches from board `edge` to a\n * specific footprint vertex (corner). Faces are intentionally excluded — an\n * arbitrarily-rotated piece has no axis-aligned face to measure to.\n */\nexport interface TriangulationLine {\n edge: BoardEdge;\n distance: number;\n vertex: number;\n}\n\nexport interface TriangulateInput {\n footprint: Footprint;\n mirror: Mirror;\n board: { width: number; height: number };\n /**\n * Three corner measurements. At least two must share an axis (left/right or\n * top/bottom) to fix the angle, and at least one must pin the other axis.\n */\n lines: [TriangulationLine, TriangulationLine, TriangulationLine];\n /** Current rotation in degrees, used to choose between the two angle roots. */\n rotationHint?: number;\n}\n\nconst TWO_PI = Math.PI * 2;\n/** Smallest absolute angular separation between two radian angles. */\nfunction angularGap(a: number, b: number): number {\n const d = (((a - b) % TWO_PI) + TWO_PI) % TWO_PI;\n return Math.min(d, TWO_PI - d);\n}\n\n/**\n * Back-solve a piece's centroid AND rotation from three card measurements to\n * specific footprint corners — the inverse needed for pieces at non-90° angles,\n * where the card pins three corner-to-edge distances rather than one per axis.\n *\n * Closed form: with the (unknown) rotation θ, each corner `v` resolves to\n * `centroid + R(θ)·v`. Subtracting two same-axis measurements cancels the\n * centroid and leaves `A·cosθ + B·sinθ = C`, solved as `θ = atan2(B,A) ±\n * acos(C/√(A²+B²))`; the root nearest `rotationHint` is chosen. One measurement\n * on each axis then pins the centroid.\n */\nexport function solveCentroidTriangulated(\n input: TriangulateInput,\n): { x: number; y: number; rotation: number } {\n // Mirror-applied, pre-rotation offsets (θ is the unknown we're solving for).\n const offsets = orientedOffsets(input.footprint, 0, input.mirror);\n const items = input.lines.map((l) => {\n const o = offsets[l.vertex];\n if (!o) throw new TerrainSolveError(`vertex index ${l.vertex} out of range`);\n const axis = axisOfEdge(l.edge);\n let target: number;\n switch (l.edge) {\n case \"left\":\n target = l.distance;\n break;\n case \"right\":\n target = input.board.width - l.distance;\n break;\n case \"top\":\n target = l.distance;\n break;\n case \"bottom\":\n target = input.board.height - l.distance;\n break;\n }\n return { axis, target, o };\n });\n const xs = items.filter((i) => i.axis === \"x\");\n const ys = items.filter((i) => i.axis === \"y\");\n\n let pivot: typeof items;\n let pivotAxis: \"x\" | \"y\";\n if (xs.length >= 2 && ys.length >= 1) {\n pivot = xs;\n pivotAxis = \"x\";\n } else if (ys.length >= 2 && xs.length >= 1) {\n pivot = ys;\n pivotAxis = \"y\";\n } else {\n throw new TerrainSolveError(\n \"triangulation needs two measurements from one pair of edges (left/right or top/bottom) and one from the other\",\n );\n }\n\n // Best-conditioned pair on the pivot axis (corners that are furthest apart).\n let a = pivot[0];\n let b = pivot[1];\n let spread = -1;\n for (let i = 0; i < pivot.length; i++) {\n for (let j = i + 1; j < pivot.length; j++) {\n const d = Math.hypot(pivot[i].o.x - pivot[j].o.x, pivot[i].o.y - pivot[j].o.y);\n if (d > spread) {\n spread = d;\n a = pivot[i];\n b = pivot[j];\n }\n }\n }\n\n // Subtract the two same-axis equations → A·cosθ + B·sinθ = C.\n // x-axis vertex eq: cx + (cosθ·o.x − sinθ·o.y) = target\n // y-axis vertex eq: cy + (sinθ·o.x + cosθ·o.y) = target\n const dx = a.o.x - b.o.x;\n const dy = a.o.y - b.o.y;\n const C = a.target - b.target;\n const A = pivotAxis === \"x\" ? dx : dy;\n const B = pivotAxis === \"x\" ? -dy : dx;\n const R = Math.hypot(A, B);\n if (R < 1e-9) {\n throw new TerrainSolveError(\"the two same-edge measurements must reference different corners\");\n }\n const ratio = C / R;\n if (ratio > 1 + 1e-6 || ratio < -1 - 1e-6) {\n throw new TerrainSolveError(\"measurements are inconsistent — no orientation fits\");\n }\n const phi = Math.atan2(B, A);\n const base = Math.acos(Math.max(-1, Math.min(1, ratio)));\n const hint = ((input.rotationHint ?? 0) * Math.PI) / 180;\n const theta = [phi + base, phi - base].reduce((best, c) =>\n angularGap(c, hint) < angularGap(best, hint) ? c : best,\n );\n\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n const xLine = xs[0];\n const yLine = ys[0];\n const x = xLine.target - (cos * xLine.o.x - sin * xLine.o.y);\n const y = yLine.target - (sin * yLine.o.x + cos * yLine.o.y);\n const rotation = (((theta * 180) / Math.PI) % 360 + 360) % 360;\n return { x, y, rotation };\n}\n\n/**\n * One card dimension line in an attachment solve: `distance` inches from board\n * `edge` to the piece's lock vertex. No feature ref — both of a piece's lines\n * reach the same locked vertex (the card pattern this solver exists for).\n */\nexport interface AttachLine {\n edge: BoardEdge;\n distance: number;\n}\n\n/**\n * One piece of a two-body attachment solve. The card pins exactly ONE vertex\n * (the keystone anchor) with two perpendicular dimension lines; the piece is\n * otherwise free to rotate about that point. The `attach` feature is what\n * connects it to the other piece and removes that rotational freedom.\n */\nexport interface AttachPiece {\n footprint: Footprint;\n mirror: Mirror;\n /** The vertex pinned by `lines` — the piece's keystone anchor vertex. */\n lockVertex: number;\n /** Two perpendicular card lines pinning the lock vertex: one must pin x, one y. */\n lines: [AttachLine, AttachLine];\n /**\n * The attached feature: a specific vertex, or the footprint edge running\n * from vertex `index` to vertex `index + 1` (wrapping).\n */\n attach: { kind: \"vertex\" | \"edge\"; index: number };\n /** Current rotation in degrees, used to pick among the candidate roots. */\n rotationHint?: number;\n}\n\nexport interface AttachInput {\n /** Board extents in inches (40kdc standard is 60 × 44). */\n board: { width: number; height: number };\n /** The piece being placed. */\n a: AttachPiece;\n /** The piece it attaches to. */\n b: AttachPiece;\n}\n\n/** How far apart attached corners may be (measurement noise) before the solve refuses. */\nconst ATTACH_TOLERANCE = 0.1;\n\nconst cross = (p: Vec2, q: Vec2): number => p.x * q.y - p.y * q.x;\nconst angle = (v: Vec2): number => Math.atan2(v.y, v.x);\n\n/** The board point a pair of perpendicular lock lines pins. */\nfunction lockPoint(lines: [AttachLine, AttachLine], board: { width: number; height: number }): Vec2 {\n const coord = (l: AttachLine): { axis: \"x\" | \"y\"; value: number } => {\n switch (l.edge) {\n case \"left\":\n return { axis: \"x\", value: l.distance };\n case \"right\":\n return { axis: \"x\", value: board.width - l.distance };\n case \"top\":\n return { axis: \"y\", value: l.distance };\n case \"bottom\":\n return { axis: \"y\", value: board.height - l.distance };\n }\n };\n const a = coord(lines[0]);\n const b = coord(lines[1]);\n if (a.axis === b.axis) {\n throw new TerrainSolveError(\n \"the two lock lines must pin different axes (one of left/right, one of top/bottom)\",\n );\n }\n return { x: a.axis === \"x\" ? a.value : b.value, y: a.axis === \"y\" ? a.value : b.value };\n}\n\ninterface AttachPrepared {\n /** Fixed board position of the lock vertex. */\n P: Vec2;\n /** Mirror-applied, pre-rotation offset of the lock vertex from the centroid. */\n lockOffset: Vec2;\n /** Mirror-applied, pre-rotation offsets of every vertex from the centroid. */\n offsets: Vec2[];\n hint: number;\n}\n\nfunction prepAttach(piece: AttachPiece, board: { width: number; height: number }): AttachPrepared {\n const offsets = orientedOffsets(piece.footprint, 0, piece.mirror);\n const lockOffset = offsets[piece.lockVertex];\n if (!lockOffset) throw new TerrainSolveError(`lock vertex index ${piece.lockVertex} out of range`);\n const n = offsets.length;\n if (piece.attach.index < 0 || piece.attach.index >= n) {\n throw new TerrainSolveError(`attach ${piece.attach.kind} index ${piece.attach.index} out of range`);\n }\n return {\n P: lockPoint(piece.lines, board),\n lockOffset,\n offsets,\n hint: ((piece.rotationHint ?? 0) * Math.PI) / 180,\n };\n}\n\n/** A candidate rotation pair (radians) for the two pieces. */\ntype AttachCandidate = { thetaA: number; thetaB: number };\n\n/**\n * Vertex ↔ vertex (corners coincide; the joint pivots): each attach vertex\n * traces a circle about its lock point with the rigid lock→attach radius, so\n * the shared corner is a circle–circle intersection — two candidate points.\n */\nfunction vertexCandidates(\n a: AttachPrepared,\n b: AttachPrepared,\n attachA: number,\n attachB: number,\n): AttachCandidate[] {\n const relA = { x: a.offsets[attachA].x - a.lockOffset.x, y: a.offsets[attachA].y - a.lockOffset.y };\n const relB = { x: b.offsets[attachB].x - b.lockOffset.x, y: b.offsets[attachB].y - b.lockOffset.y };\n const r1 = Math.hypot(relA.x, relA.y);\n const r2 = Math.hypot(relB.x, relB.y);\n if (r1 < 1e-9 || r2 < 1e-9) {\n throw new TerrainSolveError(\"the attach vertex must differ from the lock vertex\");\n }\n const D = { x: b.P.x - a.P.x, y: b.P.y - a.P.y };\n const d = Math.hypot(D.x, D.y);\n if (d < 1e-9) throw new TerrainSolveError(\"the two lock points coincide — nothing to attach across\");\n const miss = Math.max(d - (r1 + r2), Math.abs(r1 - r2) - d);\n if (miss > ATTACH_TOLERANCE) {\n throw new TerrainSolveError(\n `attached corners cannot meet: the lock points are ${round2(d)}″ apart but the corner radii are ${round2(r1)}″ and ${round2(r2)}″`,\n );\n }\n // Circle–circle intersection; a tiny miss (measurement noise) clamps to tangent.\n const along = (d * d + r1 * r1 - r2 * r2) / (2 * d);\n const h = Math.sqrt(Math.max(0, r1 * r1 - along * along));\n const u = { x: D.x / d, y: D.y / d };\n const foot = { x: a.P.x + along * u.x, y: a.P.y + along * u.y };\n const points: Vec2[] =\n h < 1e-9\n ? [foot]\n : [\n { x: foot.x - h * u.y, y: foot.y + h * u.x },\n { x: foot.x + h * u.y, y: foot.y - h * u.x },\n ];\n return points.map((X) => ({\n thetaA: angle({ x: X.x - a.P.x, y: X.y - a.P.y }) - angle(relA),\n thetaB: angle({ x: X.x - b.P.x, y: X.y - b.P.y }) - angle(relB),\n }));\n}\n\n/**\n * Edge ↔ edge (edges flush; the contact slides): the signed perpendicular\n * offset of a lock vertex from its own edge line is rotation-invariant, so the\n * shared line is a common tangent of the two circles those offsets define.\n * With line direction `t̂(ψ)`: `cross(t̂, P_a − P_b) = σ_a − ε·σ_b` for the two\n * relative edge senses ε — up to four candidate orientations.\n */\nfunction edgeCandidates(\n a: AttachPrepared,\n b: AttachPrepared,\n edgeA: number,\n edgeB: number,\n): AttachCandidate[] {\n const edgeGeom = (p: AttachPrepared, e: number): { uAng: number; sigma: number } => {\n const v0 = p.offsets[e];\n const v1 = p.offsets[(e + 1) % p.offsets.length];\n const a0 = { x: v0.x - p.lockOffset.x, y: v0.y - p.lockOffset.y };\n const u = { x: v1.x - v0.x, y: v1.y - v0.y };\n const len = Math.hypot(u.x, u.y);\n if (len < 1e-9) throw new TerrainSolveError(`edge ${e} is degenerate (zero length)`);\n const uHat = { x: u.x / len, y: u.y / len };\n // Signed distance of the lock vertex from the (lock-relative) edge line.\n return { uAng: angle(uHat), sigma: cross(uHat, { x: -a0.x, y: -a0.y }) };\n };\n const ga = edgeGeom(a, edgeA);\n const gb = edgeGeom(b, edgeB);\n const D = { x: a.P.x - b.P.x, y: a.P.y - b.P.y };\n const d = Math.hypot(D.x, D.y);\n if (d < 1e-9) throw new TerrainSolveError(\"the two lock points coincide — nothing to attach across\");\n const beta = angle(D);\n const out: AttachCandidate[] = [];\n for (const eps of [1, -1]) {\n const k = ga.sigma - eps * gb.sigma;\n const s = k / d;\n if (s > 1 + 1e-9 || s < -1 - 1e-9) continue;\n const asin = Math.asin(Math.max(-1, Math.min(1, s)));\n for (const psi of [beta - asin, beta - (Math.PI - asin)]) {\n out.push({\n thetaA: psi - ga.uAng,\n thetaB: psi - gb.uAng + (eps < 0 ? Math.PI : 0),\n });\n }\n }\n if (out.length === 0) {\n throw new TerrainSolveError(\n `attached edges cannot lie on a common line: the lock points are ${round2(d)}″ apart but the edge offsets are ${round2(ga.sigma)}″ and ${round2(gb.sigma)}″`,\n );\n }\n return out;\n}\n\nconst round2 = (n: number): number => Math.round(n * 100) / 100;\n\nfunction attachPose(p: AttachPrepared, theta: number): { x: number; y: number; rotation: number } {\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n return {\n x: p.P.x - (cos * p.lockOffset.x - sin * p.lockOffset.y),\n y: p.P.y - (sin * p.lockOffset.x + cos * p.lockOffset.y),\n rotation: ((((theta * 180) / Math.PI) % 360) + 360) % 360,\n };\n}\n\n/**\n * Back-solve the centroid AND rotation of TWO pieces at once from the cluster\n * card pattern: each piece's card prints exactly two dimension lines, both to\n * one vertex — pinning that vertex's board position but not the rotation. The\n * rotations come from the pieces' attachment to each other: matched vertices\n * coincide (the pair pivots), or matched edges lie on one line (the contact\n * slides — each piece's position along the line is fixed by its lock vertex).\n *\n * Closed form: with each lock vertex pinned, one rotation per piece is the\n * only unknown. Vertex mode intersects the two circles the attach vertices\n * sweep; edge mode finds the common line via the rotation-invariant signed\n * offset of each lock vertex from its own edge. Both yield a small candidate\n * set; the pair nearest the pieces' current rotations is chosen — rough both\n * pieces in first.\n */\nexport function solveCentroidAttached(input: AttachInput): {\n a: { x: number; y: number; rotation: number };\n b: { x: number; y: number; rotation: number };\n} {\n const { a, b } = input;\n if (a.attach.kind !== b.attach.kind) {\n throw new TerrainSolveError(\n \"attachment kinds must match: vertex pairs pivot, edge pairs slide — no mixing\",\n );\n }\n const pa = prepAttach(a, input.board);\n const pb = prepAttach(b, input.board);\n const candidates =\n a.attach.kind === \"vertex\"\n ? vertexCandidates(pa, pb, a.attach.index, b.attach.index)\n : edgeCandidates(pa, pb, a.attach.index, b.attach.index);\n const best = candidates.reduce((bestSoFar, c) =>\n angularGap(c.thetaA, pa.hint) + angularGap(c.thetaB, pb.hint) <\n angularGap(bestSoFar.thetaA, pa.hint) + angularGap(bestSoFar.thetaB, pb.hint)\n ? c\n : bestSoFar,\n );\n return { a: attachPose(pa, best.thetaA), b: attachPose(pb, best.thetaB) };\n}\n"]}
1
+ {"version":3,"file":"solve.js","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Card-measurement centroid solver — the inverse of the resolver's placement.\n *\n * Reference cards locate a terrain area by dimension lines: \"this feature of the\n * area is D inches from a board edge\". The feature referenced varies per card\n * and per piece, which is exactly why a single canonical anchor (the centroid)\n * is hard to read off a card directly. This solver lets a user transcribe the\n * card verbatim — pick the template, set the orientation shown, then enter one\n * horizontal and one vertical dimension line against whatever feature the card\n * happens to draw — and back-solves the centroid `position` the schema stores.\n *\n * Because the centroid is rotation- and mirror-invariant, orientation is fixed\n * first; each dimension line then pins one axis of the centroid in closed form.\n */\nimport {\n orientedOffsets,\n type BoardEdge,\n type FeatureRef,\n type Footprint,\n type Mirror,\n type Vec2,\n} from \"./resolve.js\";\n\n// The edge/feature vocabulary lives in resolve.ts (shared with keystones);\n// re-exported here so existing solver imports keep working.\nexport type { BoardEdge, FeatureRef } from \"./resolve.js\";\n\n/** One card dimension line: `distance` inches from `edge` to `feature`. */\nexport interface DimensionLine {\n edge: BoardEdge;\n distance: number;\n feature: FeatureRef;\n}\n\nexport interface SolveInput {\n footprint: Footprint;\n rotation: number;\n mirror: Mirror;\n /** Board extents in inches (40kdc standard is 60 × 44). */\n board: { width: number; height: number };\n /** Two perpendicular dimension lines: exactly one must pin x, one must pin y. */\n lines: [DimensionLine, DimensionLine];\n}\n\nexport class TerrainSolveError extends Error {}\n\n/** The signed offset (from the centroid) the given feature resolves to, on its axis. */\nfunction featureOffset(offsets: Vec2[], feature: FeatureRef, axis: \"x\" | \"y\"): number {\n if (feature.kind === \"vertex\") {\n const o = offsets[feature.index];\n if (!o) throw new TerrainSolveError(`vertex index ${feature.index} out of range`);\n return axis === \"x\" ? o.x : o.y;\n }\n const xs = offsets.map((o) => o.x);\n const ys = offsets.map((o) => o.y);\n switch (feature.side) {\n case \"min-x\":\n return Math.min(...xs);\n case \"max-x\":\n return Math.max(...xs);\n case \"min-y\":\n return Math.min(...ys);\n case \"max-y\":\n return Math.max(...ys);\n }\n}\n\nfunction axisOfEdge(edge: BoardEdge): \"x\" | \"y\" {\n return edge === \"left\" || edge === \"right\" ? \"x\" : \"y\";\n}\n\n/** Solve one axis of the centroid from a single dimension line. */\nfunction solveAxis(line: DimensionLine, offsets: Vec2[], board: { width: number; height: number }): { axis: \"x\" | \"y\"; value: number } {\n const axis = axisOfEdge(line.edge);\n const o = featureOffset(offsets, line.feature, axis);\n // edge → centroid: near-side edges measure from 0; far-side from the extent.\n let value: number;\n switch (line.edge) {\n case \"left\":\n value = line.distance - o;\n break;\n case \"right\":\n value = board.width - line.distance - o;\n break;\n case \"top\":\n value = line.distance - o;\n break;\n case \"bottom\":\n value = board.height - line.distance - o;\n break;\n }\n return { axis, value };\n}\n\n/**\n * Back-solve the centroid `position` from a template, its orientation, and two\n * perpendicular card dimension lines. Closed form — one x-line and one y-line\n * pin the two unknowns directly.\n */\nexport function solveCentroid(input: SolveInput): Vec2 {\n const offsets = orientedOffsets(input.footprint, input.rotation, input.mirror);\n const a = solveAxis(input.lines[0], offsets, input.board);\n const b = solveAxis(input.lines[1], offsets, input.board);\n if (a.axis === b.axis) {\n throw new TerrainSolveError(\n \"the two dimension lines must pin different axes (one of left/right, one of top/bottom)\",\n );\n }\n const x = a.axis === \"x\" ? a.value : b.value;\n const y = a.axis === \"y\" ? a.value : b.value;\n return { x, y };\n}\n\n/**\n * One triangulation measurement: `distance` inches from board `edge` to a\n * specific footprint vertex (corner). Faces are intentionally excluded — an\n * arbitrarily-rotated piece has no axis-aligned face to measure to.\n */\nexport interface TriangulationLine {\n edge: BoardEdge;\n distance: number;\n vertex: number;\n}\n\nexport interface TriangulateInput {\n footprint: Footprint;\n mirror: Mirror;\n board: { width: number; height: number };\n /**\n * Three corner measurements. At least two must share an axis (left/right or\n * top/bottom) to fix the angle, and at least one must pin the other axis.\n */\n lines: [TriangulationLine, TriangulationLine, TriangulationLine];\n /** Current rotation in degrees, used to choose between the two angle roots. */\n rotationHint?: number;\n}\n\nconst TWO_PI = Math.PI * 2;\n/** Smallest absolute angular separation between two radian angles. */\nfunction angularGap(a: number, b: number): number {\n const d = (((a - b) % TWO_PI) + TWO_PI) % TWO_PI;\n return Math.min(d, TWO_PI - d);\n}\n\n/**\n * Back-solve a piece's centroid AND rotation from three card measurements to\n * specific footprint corners — the inverse needed for pieces at non-90° angles,\n * where the card pins three corner-to-edge distances rather than one per axis.\n *\n * Closed form: with the (unknown) rotation θ, each corner `v` resolves to\n * `centroid + R(θ)·v`. Subtracting two same-axis measurements cancels the\n * centroid and leaves `A·cosθ + B·sinθ = C`, solved as `θ = atan2(B,A) ±\n * acos(C/√(A²+B²))`; the root nearest `rotationHint` is chosen. One measurement\n * on each axis then pins the centroid.\n */\nexport function solveCentroidTriangulated(\n input: TriangulateInput,\n): { x: number; y: number; rotation: number } {\n // Mirror-applied, pre-rotation offsets (θ is the unknown we're solving for).\n const offsets = orientedOffsets(input.footprint, 0, input.mirror);\n const items = input.lines.map((l) => {\n const o = offsets[l.vertex];\n if (!o) throw new TerrainSolveError(`vertex index ${l.vertex} out of range`);\n const axis = axisOfEdge(l.edge);\n let target: number;\n switch (l.edge) {\n case \"left\":\n target = l.distance;\n break;\n case \"right\":\n target = input.board.width - l.distance;\n break;\n case \"top\":\n target = l.distance;\n break;\n case \"bottom\":\n target = input.board.height - l.distance;\n break;\n }\n return { axis, target, o };\n });\n const xs = items.filter((i) => i.axis === \"x\");\n const ys = items.filter((i) => i.axis === \"y\");\n\n let pivot: typeof items;\n let pivotAxis: \"x\" | \"y\";\n if (xs.length >= 2 && ys.length >= 1) {\n pivot = xs;\n pivotAxis = \"x\";\n } else if (ys.length >= 2 && xs.length >= 1) {\n pivot = ys;\n pivotAxis = \"y\";\n } else {\n throw new TerrainSolveError(\n \"triangulation needs two measurements from one pair of edges (left/right or top/bottom) and one from the other\",\n );\n }\n\n // Best-conditioned pair on the pivot axis (corners that are furthest apart).\n let a = pivot[0];\n let b = pivot[1];\n let spread = -1;\n for (let i = 0; i < pivot.length; i++) {\n for (let j = i + 1; j < pivot.length; j++) {\n const d = Math.hypot(pivot[i].o.x - pivot[j].o.x, pivot[i].o.y - pivot[j].o.y);\n if (d > spread) {\n spread = d;\n a = pivot[i];\n b = pivot[j];\n }\n }\n }\n\n // Subtract the two same-axis equations → A·cosθ + B·sinθ = C.\n // x-axis vertex eq: cx + (cosθ·o.x − sinθ·o.y) = target\n // y-axis vertex eq: cy + (sinθ·o.x + cosθ·o.y) = target\n const dx = a.o.x - b.o.x;\n const dy = a.o.y - b.o.y;\n const C = a.target - b.target;\n const A = pivotAxis === \"x\" ? dx : dy;\n const B = pivotAxis === \"x\" ? -dy : dx;\n const R = Math.hypot(A, B);\n if (R < 1e-9) {\n throw new TerrainSolveError(\"the two same-edge measurements must reference different corners\");\n }\n const ratio = C / R;\n if (ratio > 1 + 1e-6 || ratio < -1 - 1e-6) {\n throw new TerrainSolveError(\"measurements are inconsistent — no orientation fits\");\n }\n const phi = Math.atan2(B, A);\n const base = Math.acos(Math.max(-1, Math.min(1, ratio)));\n const hint = ((input.rotationHint ?? 0) * Math.PI) / 180;\n const theta = [phi + base, phi - base].reduce((best, c) =>\n angularGap(c, hint) < angularGap(best, hint) ? c : best,\n );\n\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n const xLine = xs[0];\n const yLine = ys[0];\n const x = xLine.target - (cos * xLine.o.x - sin * xLine.o.y);\n const y = yLine.target - (sin * yLine.o.x + cos * yLine.o.y);\n const rotation = (((theta * 180) / Math.PI) % 360 + 360) % 360;\n return { x, y, rotation };\n}\n\n/**\n * One card dimension line in an attachment solve: `distance` inches from board\n * `edge` to the piece's lock vertex. No feature ref — both of a piece's lines\n * reach the same locked vertex (the card pattern this solver exists for).\n */\nexport interface AttachLine {\n edge: BoardEdge;\n distance: number;\n}\n\n/**\n * One piece of a two-body attachment solve. The card pins exactly ONE vertex\n * (the keystone anchor) with two perpendicular dimension lines; the piece is\n * otherwise free to rotate about that point. The `attach` feature is what\n * connects it to the other piece and removes that rotational freedom.\n */\nexport interface AttachPiece {\n footprint: Footprint;\n mirror: Mirror;\n /** The vertex pinned by `lines` — the piece's keystone anchor vertex. */\n lockVertex: number;\n /** Two perpendicular card lines pinning the lock vertex: one must pin x, one y. */\n lines: [AttachLine, AttachLine];\n /**\n * The attached feature: a specific vertex, or the footprint edge running\n * from vertex `index` to vertex `index + 1` (wrapping).\n */\n attach: { kind: \"vertex\" | \"edge\"; index: number };\n /** Current rotation in degrees, used to pick among the candidate roots. */\n rotationHint?: number;\n}\n\nexport interface AttachInput {\n /** Board extents in inches (40kdc standard is 60 × 44). */\n board: { width: number; height: number };\n /** The piece being placed. */\n a: AttachPiece;\n /** The piece it attaches to. */\n b: AttachPiece;\n}\n\n/** How far apart attached corners may be (measurement noise) before the solve refuses. */\nconst ATTACH_TOLERANCE = 0.1;\n\nconst cross = (p: Vec2, q: Vec2): number => p.x * q.y - p.y * q.x;\nconst angle = (v: Vec2): number => Math.atan2(v.y, v.x);\n\n/** The board point a pair of perpendicular lock lines pins. */\nfunction lockPoint(lines: [AttachLine, AttachLine], board: { width: number; height: number }): Vec2 {\n const coord = (l: AttachLine): { axis: \"x\" | \"y\"; value: number } => {\n switch (l.edge) {\n case \"left\":\n return { axis: \"x\", value: l.distance };\n case \"right\":\n return { axis: \"x\", value: board.width - l.distance };\n case \"top\":\n return { axis: \"y\", value: l.distance };\n case \"bottom\":\n return { axis: \"y\", value: board.height - l.distance };\n }\n };\n const a = coord(lines[0]);\n const b = coord(lines[1]);\n if (a.axis === b.axis) {\n throw new TerrainSolveError(\n \"the two lock lines must pin different axes (one of left/right, one of top/bottom)\",\n );\n }\n return { x: a.axis === \"x\" ? a.value : b.value, y: a.axis === \"y\" ? a.value : b.value };\n}\n\ninterface AttachPrepared {\n /** Fixed board position of the lock vertex. */\n P: Vec2;\n /** Mirror-applied, pre-rotation offset of the lock vertex from the centroid. */\n lockOffset: Vec2;\n /** Mirror-applied, pre-rotation offsets of every vertex from the centroid. */\n offsets: Vec2[];\n hint: number;\n}\n\nfunction prepAttach(piece: AttachPiece, board: { width: number; height: number }): AttachPrepared {\n const offsets = orientedOffsets(piece.footprint, 0, piece.mirror);\n const lockOffset = offsets[piece.lockVertex];\n if (!lockOffset) throw new TerrainSolveError(`lock vertex index ${piece.lockVertex} out of range`);\n const n = offsets.length;\n if (piece.attach.index < 0 || piece.attach.index >= n) {\n throw new TerrainSolveError(`attach ${piece.attach.kind} index ${piece.attach.index} out of range`);\n }\n return {\n P: lockPoint(piece.lines, board),\n lockOffset,\n offsets,\n hint: ((piece.rotationHint ?? 0) * Math.PI) / 180,\n };\n}\n\n/** A candidate rotation pair (radians) for the two pieces. */\ntype AttachCandidate = { thetaA: number; thetaB: number };\n\n/**\n * Vertex ↔ vertex (corners coincide; the joint pivots): each attach vertex\n * traces a circle about its lock point with the rigid lock→attach radius, so\n * the shared corner is a circle–circle intersection — two candidate points.\n */\nfunction vertexCandidates(\n a: AttachPrepared,\n b: AttachPrepared,\n attachA: number,\n attachB: number,\n): AttachCandidate[] {\n const relA = { x: a.offsets[attachA].x - a.lockOffset.x, y: a.offsets[attachA].y - a.lockOffset.y };\n const relB = { x: b.offsets[attachB].x - b.lockOffset.x, y: b.offsets[attachB].y - b.lockOffset.y };\n const r1 = Math.hypot(relA.x, relA.y);\n const r2 = Math.hypot(relB.x, relB.y);\n if (r1 < 1e-9 || r2 < 1e-9) {\n throw new TerrainSolveError(\"the attach vertex must differ from the lock vertex\");\n }\n const D = { x: b.P.x - a.P.x, y: b.P.y - a.P.y };\n const d = Math.hypot(D.x, D.y);\n if (d < 1e-9) throw new TerrainSolveError(\"the two lock points coincide — nothing to attach across\");\n const miss = Math.max(d - (r1 + r2), Math.abs(r1 - r2) - d);\n if (miss > ATTACH_TOLERANCE) {\n throw new TerrainSolveError(\n `attached corners cannot meet: the lock points are ${round2(d)}″ apart but the corner radii are ${round2(r1)}″ and ${round2(r2)}″`,\n );\n }\n // Circle–circle intersection; a tiny miss (measurement noise) clamps to tangent.\n const along = (d * d + r1 * r1 - r2 * r2) / (2 * d);\n const h = Math.sqrt(Math.max(0, r1 * r1 - along * along));\n const u = { x: D.x / d, y: D.y / d };\n const foot = { x: a.P.x + along * u.x, y: a.P.y + along * u.y };\n const points: Vec2[] =\n h < 1e-9\n ? [foot]\n : [\n { x: foot.x - h * u.y, y: foot.y + h * u.x },\n { x: foot.x + h * u.y, y: foot.y - h * u.x },\n ];\n return points.map((X) => ({\n thetaA: angle({ x: X.x - a.P.x, y: X.y - a.P.y }) - angle(relA),\n thetaB: angle({ x: X.x - b.P.x, y: X.y - b.P.y }) - angle(relB),\n }));\n}\n\n/**\n * Edge ↔ edge (edges flush; the contact slides): the signed perpendicular\n * offset of a lock vertex from its own edge line is rotation-invariant, so the\n * shared line is a common tangent of the two circles those offsets define.\n * With line direction `t̂(ψ)`: `cross(t̂, P_a − P_b) = σ_a − ε·σ_b` for the two\n * relative edge senses ε — up to four candidate orientations.\n */\nfunction edgeCandidates(\n a: AttachPrepared,\n b: AttachPrepared,\n edgeA: number,\n edgeB: number,\n): AttachCandidate[] {\n const edgeGeom = (p: AttachPrepared, e: number): { uAng: number; sigma: number } => {\n const v0 = p.offsets[e];\n const v1 = p.offsets[(e + 1) % p.offsets.length];\n const a0 = { x: v0.x - p.lockOffset.x, y: v0.y - p.lockOffset.y };\n const u = { x: v1.x - v0.x, y: v1.y - v0.y };\n const len = Math.hypot(u.x, u.y);\n if (len < 1e-9) throw new TerrainSolveError(`edge ${e} is degenerate (zero length)`);\n const uHat = { x: u.x / len, y: u.y / len };\n // Signed distance of the lock vertex from the (lock-relative) edge line.\n return { uAng: angle(uHat), sigma: cross(uHat, { x: -a0.x, y: -a0.y }) };\n };\n const ga = edgeGeom(a, edgeA);\n const gb = edgeGeom(b, edgeB);\n const D = { x: a.P.x - b.P.x, y: a.P.y - b.P.y };\n const d = Math.hypot(D.x, D.y);\n if (d < 1e-9) throw new TerrainSolveError(\"the two lock points coincide — nothing to attach across\");\n const beta = angle(D);\n const out: AttachCandidate[] = [];\n for (const eps of [1, -1]) {\n const k = ga.sigma - eps * gb.sigma;\n const s = k / d;\n if (s > 1 + 1e-9 || s < -1 - 1e-9) continue;\n const asin = Math.asin(Math.max(-1, Math.min(1, s)));\n for (const psi of [beta - asin, beta - (Math.PI - asin)]) {\n out.push({\n thetaA: psi - ga.uAng,\n thetaB: psi - gb.uAng + (eps < 0 ? Math.PI : 0),\n });\n }\n }\n if (out.length === 0) {\n throw new TerrainSolveError(\n `attached edges cannot lie on a common line: the lock points are ${round2(d)}″ apart but the edge offsets are ${round2(ga.sigma)}″ and ${round2(gb.sigma)}″`,\n );\n }\n return out;\n}\n\nconst round2 = (n: number): number => Math.round(n * 100) / 100;\n\nfunction attachPose(p: AttachPrepared, theta: number): { x: number; y: number; rotation: number } {\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n return {\n x: p.P.x - (cos * p.lockOffset.x - sin * p.lockOffset.y),\n y: p.P.y - (sin * p.lockOffset.x + cos * p.lockOffset.y),\n rotation: ((((theta * 180) / Math.PI) % 360) + 360) % 360,\n };\n}\n\n/**\n * Back-solve the centroid AND rotation of TWO pieces at once from the cluster\n * card pattern: each piece's card prints exactly two dimension lines, both to\n * one vertex — pinning that vertex's board position but not the rotation. The\n * rotations come from the pieces' attachment to each other: matched vertices\n * coincide (the pair pivots), or matched edges lie on one line (the contact\n * slides — each piece's position along the line is fixed by its lock vertex).\n *\n * Closed form: with each lock vertex pinned, one rotation per piece is the\n * only unknown. Vertex mode intersects the two circles the attach vertices\n * sweep; edge mode finds the common line via the rotation-invariant signed\n * offset of each lock vertex from its own edge. Both yield a small candidate\n * set; the pair nearest the pieces' current rotations is chosen — rough both\n * pieces in first.\n */\nexport function solveCentroidAttached(input: AttachInput): {\n a: { x: number; y: number; rotation: number };\n b: { x: number; y: number; rotation: number };\n} {\n const { a, b } = input;\n if (a.attach.kind !== b.attach.kind) {\n throw new TerrainSolveError(\n \"attachment kinds must match: vertex pairs pivot, edge pairs slide — no mixing\",\n );\n }\n const pa = prepAttach(a, input.board);\n const pb = prepAttach(b, input.board);\n const candidates =\n a.attach.kind === \"vertex\"\n ? vertexCandidates(pa, pb, a.attach.index, b.attach.index)\n : edgeCandidates(pa, pb, a.attach.index, b.attach.index);\n const best = candidates.reduce((bestSoFar, c) =>\n angularGap(c.thetaA, pa.hint) + angularGap(c.thetaB, pb.hint) <\n angularGap(bestSoFar.thetaA, pa.hint) + angularGap(bestSoFar.thetaB, pb.hint)\n ? c\n : bestSoFar,\n );\n return { a: attachPose(pa, best.thetaA), b: attachPose(pb, best.thetaB) };\n}\n\n/**\n * One card dimension line in a fixed-anchor solve: `distance` inches from board\n * `edge` to a specific footprint `vertex` of the moving piece. This single line\n * pins the one degree of freedom the attachment to the fixed anchor leaves.\n */\nexport interface FixedLockLine {\n edge: BoardEdge;\n distance: number;\n vertex: number;\n}\n\n/**\n * The already-placed piece the moving piece attaches to. Its pose is taken as\n * given — `vertices` are its resolved board-space polygon vertices (the\n * orientation-only offsets from {@link orientedOffsets} added to the piece's\n * board centroid) — and the solve never moves it.\n */\nexport interface FixedAnchor {\n vertices: Vec2[];\n /**\n * The anchor feature the moving piece contacts: a specific vertex, or the\n * footprint edge running from vertex `index` to vertex `index + 1` (wrapping).\n */\n attach: { kind: \"vertex\" | \"edge\"; index: number };\n}\n\n/** The piece being placed against a {@link FixedAnchor}. */\nexport interface MovingAttachPiece {\n footprint: Footprint;\n mirror: Mirror;\n /**\n * The moving feature that meets the anchor: a vertex (coincides with the\n * anchor vertex — the joint pivots), or the edge `index → index + 1` (lies\n * flush on the anchor edge — the contact slides). Must match the anchor's kind.\n */\n attach: { kind: \"vertex\" | \"edge\"; index: number };\n /** The single card line pinning the remaining freedom. */\n line: FixedLockLine;\n /** Current rotation in degrees, used to pick among the candidate roots. */\n rotationHint?: number;\n}\n\nexport interface SolveAgainstFixedInput {\n /** Board extents in inches (40kdc standard is 60 × 44). */\n board: { width: number; height: number };\n moving: MovingAttachPiece;\n fixed: FixedAnchor;\n}\n\n/** The board coordinate (and axis) a single edge measurement pins. */\nfunction edgeTarget(\n edge: BoardEdge,\n distance: number,\n board: { width: number; height: number },\n): { axis: \"x\" | \"y\"; value: number } {\n switch (edge) {\n case \"left\":\n return { axis: \"x\", value: distance };\n case \"right\":\n return { axis: \"x\", value: board.width - distance };\n case \"top\":\n return { axis: \"y\", value: distance };\n case \"bottom\":\n return { axis: \"y\", value: board.height - distance };\n }\n}\n\n/**\n * Back-solve the centroid AND rotation of ONE piece against an already-placed\n * anchor. The attachment removes two degrees of freedom — corners coincide\n * (the joint pivots) or edges lie flush (the contact slides) — so a single card\n * dimension line pins what remains. The anchor is given by its resolved\n * board-space `vertices` and never moves.\n *\n * Closed form:\n * - **vertex mode** — the moving attach vertex equals the fixed point `P`, so\n * `centroid = P − R(θ)·o_att`. The lock line to vertex `j` resolves to\n * `P + R(θ)·(o_j − o_att)`; pinning one axis gives `A·cosθ + B·sinθ = C`,\n * solved `θ = atan2(B,A) ± acos(C/√(A²+B²))`, the root nearest `rotationHint`.\n * - **edge mode** — flush forces the moving edge parallel to the anchor edge\n * (two senses → two θ), and for each θ the centroid solves from a 2×2 linear\n * system: the flush line equation plus the single lock line. The candidate\n * nearest `rotationHint` wins.\n */\nexport function solveCentroidAgainstFixed(\n input: SolveAgainstFixedInput,\n): { x: number; y: number; rotation: number } {\n const { moving, fixed, board } = input;\n if (moving.attach.kind !== fixed.attach.kind) {\n throw new TerrainSolveError(\n \"attachment kinds must match: a vertex pins to a vertex, an edge lies flush on an edge — no mixing\",\n );\n }\n // Mirror-applied, pre-rotation offsets (θ is the unknown we solve for).\n const offsets = orientedOffsets(moving.footprint, 0, moving.mirror);\n const n = offsets.length;\n const m = fixed.vertices.length;\n if (moving.attach.index < 0 || moving.attach.index >= n) {\n throw new TerrainSolveError(`moving attach ${moving.attach.kind} index ${moving.attach.index} out of range`);\n }\n if (fixed.attach.index < 0 || fixed.attach.index >= m) {\n throw new TerrainSolveError(`fixed attach ${fixed.attach.kind} index ${fixed.attach.index} out of range`);\n }\n const oj = offsets[moving.line.vertex];\n if (!oj) throw new TerrainSolveError(`lock line vertex index ${moving.line.vertex} out of range`);\n const hint = ((moving.rotationHint ?? 0) * Math.PI) / 180;\n const target = edgeTarget(moving.line.edge, moving.line.distance, board);\n\n const pose = (theta: number, centroid: Vec2): { x: number; y: number; rotation: number } => ({\n x: centroid.x,\n y: centroid.y,\n rotation: ((((theta * 180) / Math.PI) % 360) + 360) % 360,\n });\n\n if (moving.attach.kind === \"vertex\") {\n const P = fixed.vertices[fixed.attach.index];\n const oAtt = offsets[moving.attach.index];\n const rel = { x: oj.x - oAtt.x, y: oj.y - oAtt.y };\n if (Math.hypot(rel.x, rel.y) < 1e-9) {\n throw new TerrainSolveError(\"the lock line must measure to a different vertex than the attach corner\");\n }\n // P.axis + (R(θ)·rel).axis = target → A·cosθ + B·sinθ = C.\n const A = target.axis === \"x\" ? rel.x : rel.y;\n const B = target.axis === \"x\" ? -rel.y : rel.x;\n const C = target.value - (target.axis === \"x\" ? P.x : P.y);\n const R = Math.hypot(A, B);\n const ratio = C / R;\n if (ratio > 1 + 1e-6 || ratio < -1 - 1e-6) {\n throw new TerrainSolveError(\n `the locked corner is only ${round2(R)}″ from the attach corner — it cannot reach that measurement`,\n );\n }\n const phi = Math.atan2(B, A);\n const baseAcos = Math.acos(Math.max(-1, Math.min(1, ratio)));\n const theta = [phi + baseAcos, phi - baseAcos].reduce((best, c) =>\n angularGap(c, hint) < angularGap(best, hint) ? c : best,\n );\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n // centroid = P − R(θ)·o_att.\n const centroid = {\n x: P.x - (cos * oAtt.x - sin * oAtt.y),\n y: P.y - (sin * oAtt.x + cos * oAtt.y),\n };\n return pose(theta, centroid);\n }\n\n // Edge mode: the moving edge ei→ei+1 lies flush on the anchor edge fi→fi+1.\n const ei = moving.attach.index;\n const oe = offsets[ei];\n const oe1 = offsets[(ei + 1) % n];\n const u = { x: oe1.x - oe.x, y: oe1.y - oe.y };\n if (Math.hypot(u.x, u.y) < 1e-9) {\n throw new TerrainSolveError(`moving edge ${ei} is degenerate (zero length)`);\n }\n const fi = fixed.attach.index;\n const F0 = fixed.vertices[fi];\n const F1 = fixed.vertices[(fi + 1) % m];\n const t = { x: F1.x - F0.x, y: F1.y - F0.y };\n const tLen = Math.hypot(t.x, t.y);\n if (tLen < 1e-9) {\n throw new TerrainSolveError(`anchor edge ${fi} is degenerate (zero length)`);\n }\n const tHat = { x: t.x / tLen, y: t.y / tLen };\n const uAng = angle(u);\n const tAng = angle(tHat);\n // angle(R(θ)·u) = angle(u) + θ, so parallel ⇒ θ = tAng − uAng (same sense) or +π (opposite).\n const candidates = [tAng - uAng, tAng + Math.PI - uAng];\n\n let best: { theta: number; centroid: Vec2 } | null = null;\n for (const theta of candidates) {\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n const rot = (v: Vec2): Vec2 => ({ x: cos * v.x - sin * v.y, y: sin * v.x + cos * v.y });\n // Flush: cross(t̂, c + R(θ)·o_e − F0) = 0.\n // (−t̂.y)·cx + (t̂.x)·cy = t̂.y·q.x − t̂.x·q.y, q = R(θ)·o_e − F0\n const q = rot(oe);\n q.x -= F0.x;\n q.y -= F0.y;\n const a1 = -tHat.y;\n const b1 = tHat.x;\n const d1 = tHat.y * q.x - tHat.x * q.y;\n // Lock line: pins one axis of (c + R(θ)·o_j).\n const rj = rot(oj);\n const a2 = target.axis === \"x\" ? 1 : 0;\n const b2 = target.axis === \"x\" ? 0 : 1;\n const d2 = target.value - (target.axis === \"x\" ? rj.x : rj.y);\n const det = a1 * b2 - b1 * a2;\n if (Math.abs(det) < 1e-9) continue; // lock line parallel to the contact edge for this θ\n const centroid = {\n x: (d1 * b2 - b1 * d2) / det,\n y: (a1 * d2 - d1 * a2) / det,\n };\n if (best === null || angularGap(theta, hint) < angularGap(best.theta, hint)) {\n best = { theta, centroid };\n }\n }\n if (best === null) {\n throw new TerrainSolveError(\n \"the lock line is parallel to the contact edge — measure to the other axis (left/right vs top/bottom)\",\n );\n }\n return pose(best.theta, best.centroid);\n}\n"]}
@@ -22,5 +22,6 @@ export interface Condition {
22
22
  }
23
23
  /** kebab-case → space-separated words (`enemy-territory` → `enemy territory`). */
24
24
  export declare function dekebab(s: string): string;
25
+ export declare function describeTiming(timing: unknown): string;
25
26
  export declare function describeCondition(c: Condition): string;
26
27
  //# sourceMappingURL=condition.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"condition.d.ts","sourceRoot":"","sources":["../../src/translate/condition.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IAChC,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,kFAAkF;AAClF,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzC;AAYD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAoKtD"}
1
+ {"version":3,"file":"condition.d.ts","sourceRoot":"","sources":["../../src/translate/condition.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IAChC,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,kFAAkF;AAClF,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzC;AAuDD,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAOtD;AAOD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAgMtD"}
@@ -16,6 +16,65 @@ function str(v) {
16
16
  return "?";
17
17
  return typeof v === "string" ? v : String(v);
18
18
  }
19
+ /**
20
+ * A `timing-is` event token → natural GW-voice clause ("each time a model in
21
+ * this unit is destroyed", "at the start of the phase"). Structural phase/turn
22
+ * markers and the common trigger families are mapped explicitly; the fallback
23
+ * routes `after-*`/`on-*` prefixes to "after …"/"when …" (so the old "at on …"
24
+ * double-preposition can't occur) and everything else to "at <event>".
25
+ */
26
+ const TIMING_PHRASES = {
27
+ "start-of-phase": "at the start of the phase",
28
+ "end-of-phase": "at the end of the phase",
29
+ "start-of-turn": "at the start of the turn",
30
+ "end-of-turn": "at the end of the turn",
31
+ "end-of-opponent-turn": "at the end of the opponent's turn",
32
+ "start-of-battle-round": "at the start of the battle round",
33
+ start: "at the start of the turn",
34
+ end: "at the end of the turn",
35
+ "command-phase": "in the Command phase",
36
+ "shooting-phase": "in the Shooting phase",
37
+ "on-model-destroyed": "each time a model in this unit is destroyed",
38
+ "model-destroyed": "each time a model in this unit is destroyed",
39
+ "first-model-destroyed": "the first time a model in this unit is destroyed",
40
+ "first-this-battle": "the first time this battle",
41
+ "first-time-this-phase": "the first time this phase",
42
+ "on-unit-destroyed": "each time this unit is destroyed",
43
+ "on-destroyed": "each time this unit is destroyed",
44
+ "enemy-unit-destroyed-in-melee": "each time an enemy unit is destroyed in melee",
45
+ "in-reserves": "while it is in Reserves",
46
+ "game-start-in-reserves": "if it begins the battle in Reserves",
47
+ "starts-in-strategic-reserves": "if it starts in Strategic Reserves",
48
+ "deep-strike-setup": "when it is set up by Deep Strike",
49
+ "deep-strike": "when it is set up by Deep Strike",
50
+ "set-up-from-reserves": "when it arrives from Reserves",
51
+ "arrives-from-strategic-reserves": "when it arrives from Strategic Reserves",
52
+ reinforcements: "when it arrives as Reinforcements",
53
+ "reinforcements-step": "during the Reinforcements step",
54
+ "post-deployment": "after deployment",
55
+ "declare-battle-formations": "when declaring Battle Formations",
56
+ "normal-move": "when it makes a Normal move",
57
+ "advance-move": "when it makes an Advance move",
58
+ advance: "when it Advances",
59
+ "fall-back-move": "when it makes a Fall Back move",
60
+ "fall-back": "when it Falls Back",
61
+ "charge-move": "when it makes a Charge move",
62
+ "once-per-battle": "once per battle",
63
+ "once-per-phase": "once per phase",
64
+ "once-per-opponent-turn": "once per opponent's turn",
65
+ };
66
+ export function describeTiming(timing) {
67
+ const t = str(timing);
68
+ if (TIMING_PHRASES[t])
69
+ return TIMING_PHRASES[t];
70
+ if (t.startsWith("after-"))
71
+ return `after ${dekebab(t.slice(6))}`;
72
+ if (t.startsWith("on-"))
73
+ return `when ${dekebab(t.slice(3))}`;
74
+ if (t.endsWith("-destroyed"))
75
+ return `each time ${dekebab(t)}`;
76
+ return `at ${dekebab(t)}`;
77
+ }
19
78
  /** `2` + `objective` → `2+ objectives`. Nouns here are all regular plurals. */
20
79
  function count(n, noun) {
21
80
  return `${str(n)}+ ${noun}s`;
@@ -39,7 +98,7 @@ export function describeCondition(c) {
39
98
  case "phase-is":
40
99
  return `${negate}during the ${str(p.phase)} phase`;
41
100
  case "timing-is":
42
- return `${negate}at ${dekebab(str(p.timing))}`;
101
+ return `${negate}${describeTiming(p.timing)}`;
43
102
  case "player-turn-is":
44
103
  return `${negate}in ${p.turn === "your-turn" ? "your" : p.turn === "opponent-turn" ? "the opponent's" : "either player's"} turn`;
45
104
  case "charged-this-turn":
@@ -61,6 +120,10 @@ export function describeCondition(c) {
61
120
  case "is-attached":
62
121
  return `${negate}attached to a ${p.keyword ? `${str(p.keyword)} ` : ""}unit`;
63
122
  case "attack-is-type":
123
+ if (p.comparison === "strength-greater-than-toughness")
124
+ return `${negate}when this attack's Strength is greater than the target's Toughness`;
125
+ if (p.comparison != null)
126
+ return `${negate}when ${dekebab(str(p.comparison))}`;
64
127
  return `${negate}for ${str(p.attack_type)} attacks`;
65
128
  case "is-battle-shocked":
66
129
  return `${negate}the unit is battle-shocked`;
@@ -75,10 +138,34 @@ export function describeCondition(c) {
75
138
  return `${negate}${subject} was hit by ${n}+ ${atk}attacks${weapon} this phase`;
76
139
  return `${negate}${subject} was hit by ${atk === "" ? "an attack" : `a ${atk}attack`}${weapon} this phase`;
77
140
  }
78
- case "opponent-unit-within-range":
79
- return `${negate}an enemy unit is within ${p.range === "engagement" ? "engagement range" : `${str(p.range)}"`}`;
80
- case "unit-within-range-of":
81
- return `${negate}within ${str(p.range)}" of ${str(p.target_type ?? "target")}${p.keyword ? ` (${str(p.keyword)})` : ""}`;
141
+ case "opponent-unit-within-range": {
142
+ let where;
143
+ if (p.weapon_name != null)
144
+ where = `range of ${dekebab(str(p.weapon_name))}`;
145
+ else if (p.range_multiplier != null)
146
+ where = "half range of its ranged weapons";
147
+ else if (p.range === "engagement")
148
+ where = "engagement range";
149
+ else
150
+ where = `${str(p.range)}"`;
151
+ return `${negate}an enemy unit is within ${where}`;
152
+ }
153
+ case "unit-within-range-of": {
154
+ const tt = str(p.target_type ?? "target");
155
+ // Targets that name a specific model, not a radius — no inches apply.
156
+ if (tt === "closest-eligible")
157
+ return `${negate}the target is the closest eligible target`;
158
+ if (tt === "area-terrain")
159
+ return `${negate}within an area terrain feature`;
160
+ const who = tt === "friendly-keyword" && p.keyword
161
+ ? `a friendly ${str(p.keyword)} unit`
162
+ : tt === "friendly"
163
+ ? "a friendly unit"
164
+ : dekebab(tt);
165
+ // A missing range stays as `?"` so the audit still flags it as a data gap.
166
+ const dist = p.range != null ? `${str(p.range)}"` : '?"';
167
+ return `${negate}within ${dist} of ${who}`;
168
+ }
82
169
  case "within-range-of-objective":
83
170
  return `${negate}within range of an objective`;
84
171
  case "has-fought-this-phase":
@@ -99,8 +186,12 @@ export function describeCondition(c) {
99
186
  s += ` (excluding ${dekebab(str(p.exclude))})`;
100
187
  return s;
101
188
  }
102
- case "units-destroyed":
103
- return `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} destroyed ${dekebab(str(p.window))}`;
189
+ case "units-destroyed": {
190
+ let s = `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} destroyed`;
191
+ if (p.window != null)
192
+ s += ` ${dekebab(str(p.window))}`;
193
+ return s;
194
+ }
104
195
  case "units-destroyed-comparison": {
105
196
  const subj = (p.subject ?? {});
106
197
  const ref = (p.reference ?? {});
@@ -178,6 +269,9 @@ export function describeCondition(c) {
178
269
  return s;
179
270
  }
180
271
  case "unit-has-tag": {
272
+ // Ability-gate use (no side/count) reads as a unit state; scoring use counts tagged units.
273
+ if (p.side == null && p.count_min == null)
274
+ return `${negate}the unit is tagged ${dekebab(str(p.tag))}`;
181
275
  let s = `${negate}${count(p.count_min ?? 1, `${str(p.side)} unit`)} tagged ${dekebab(str(p.tag))}`;
182
276
  if (p.window != null)
183
277
  s += ` (${dekebab(str(p.window))})`;