@alpaca-software/40kdc-data 0.4.5 → 0.4.6
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.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fix-corner-ruin.d.ts","sourceRoot":"","sources":["../src/fix-corner-ruin.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot correction (refuses to re-run once applied) of the `corner-ruin-left` /
|
|
3
|
+
* `corner-ruin-right` catalog templates: the long leg is 7″ but the physical
|
|
4
|
+
* piece is 6.5″. Shrinks the leg in `data/core/terrain-templates.json` and
|
|
5
|
+
* compensates every layout piece referencing those templates in
|
|
6
|
+
* `data/core/terrain-layouts.json` so the resolved outer corner stays exactly
|
|
7
|
+
* where it was placed.
|
|
8
|
+
*
|
|
9
|
+
* Pieces are centroid-anchored (`board = position + R·M·(v − c)`), so changing
|
|
10
|
+
* the footprint moves the centroid `c` and would shift the whole piece. Keeping
|
|
11
|
+
* any unchanged vertex fixed requires `position += R·M·(c_new − c_old)` — the
|
|
12
|
+
* same correction in board space and in a parent area's centroid-local frame
|
|
13
|
+
* (the area transform is rigid and applied after).
|
|
14
|
+
*
|
|
15
|
+
* Pieces with an inline baked `footprint` (the migrated gw-11e-crucible corner
|
|
16
|
+
* segments, which carry the template id only as provenance) are intentionally
|
|
17
|
+
* untouched: the template edit cannot affect them.
|
|
18
|
+
*
|
|
19
|
+
* Verifies by resolving every layout before and after: unchanged footprint
|
|
20
|
+
* vertices must not move, the two shortened leg vertices must move by exactly
|
|
21
|
+
* 0.5″, and every piece not referencing the templates must be bit-identical.
|
|
22
|
+
*
|
|
23
|
+
* Usage: `npx tsx src/fix-corner-ruin.ts` (run from `tools/`).
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { resolveLayout, polygonCentroid, footprintVertices, } from "./terrain/resolve.js";
|
|
28
|
+
const REPO_ROOT = join(new URL("../..", import.meta.url).pathname);
|
|
29
|
+
const CATALOG_PATH = join(REPO_ROOT, "data", "core", "terrain-templates.json");
|
|
30
|
+
const LAYOUTS_PATH = join(REPO_ROOT, "data", "core", "terrain-layouts.json");
|
|
31
|
+
const OLD_LEG = 7;
|
|
32
|
+
const NEW_LEG = 6.5;
|
|
33
|
+
const TARGETS = new Set(["corner-ruin-left", "corner-ruin-right"]);
|
|
34
|
+
/** Resolver output is rounded to 1e-4; position rounding adds ≤5e-5 per axis. */
|
|
35
|
+
const TOL = 2e-4;
|
|
36
|
+
// Mirror → rotate, matching the resolver's orientation math (y-down, CW).
|
|
37
|
+
function applyMirror(v, m) {
|
|
38
|
+
if (m === "horizontal")
|
|
39
|
+
return { x: -v.x, y: v.y };
|
|
40
|
+
if (m === "vertical")
|
|
41
|
+
return { x: v.x, y: -v.y };
|
|
42
|
+
return v;
|
|
43
|
+
}
|
|
44
|
+
function rotateCw(v, deg) {
|
|
45
|
+
const r = (deg * Math.PI) / 180;
|
|
46
|
+
const c = Math.cos(r);
|
|
47
|
+
const s = Math.sin(r);
|
|
48
|
+
return { x: c * v.x - s * v.y, y: s * v.x + c * v.y };
|
|
49
|
+
}
|
|
50
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
51
|
+
function main() {
|
|
52
|
+
const catalog = JSON.parse(readFileSync(CATALOG_PATH, "utf8"));
|
|
53
|
+
const layouts = JSON.parse(readFileSync(LAYOUTS_PATH, "utf8"));
|
|
54
|
+
// ---- snapshot ground truth with the OLD catalog --------------------------
|
|
55
|
+
const before = new Map(layouts.map((l) => [l.id, resolveLayout(l, catalog)]));
|
|
56
|
+
// ---- patch the two templates, recording centroid deltas ------------------
|
|
57
|
+
const deltas = new Map();
|
|
58
|
+
for (const t of catalog) {
|
|
59
|
+
if (!TARGETS.has(t.id))
|
|
60
|
+
continue;
|
|
61
|
+
if (t.footprint.type !== "polygon")
|
|
62
|
+
throw new Error(`${t.id}: expected polygon footprint`);
|
|
63
|
+
const oldVerts = footprintVertices(t.footprint);
|
|
64
|
+
const movedIdx = [];
|
|
65
|
+
t.footprint.points.forEach((p, i) => {
|
|
66
|
+
if (p.y === OLD_LEG) {
|
|
67
|
+
p.y = NEW_LEG;
|
|
68
|
+
movedIdx.push(i);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (movedIdx.length !== 2) {
|
|
72
|
+
throw new Error(`${t.id}: expected exactly 2 leg vertices at y=${OLD_LEG}, found ${movedIdx.length}`);
|
|
73
|
+
}
|
|
74
|
+
const cOld = polygonCentroid(oldVerts);
|
|
75
|
+
const cNew = polygonCentroid(footprintVertices(t.footprint));
|
|
76
|
+
const delta = { x: cNew.x - cOld.x, y: cNew.y - cOld.y };
|
|
77
|
+
deltas.set(t.id, { delta, movedIdx });
|
|
78
|
+
console.log(`${t.id}: centroid (${round4(cOld.x)}, ${round4(cOld.y)}) → (${round4(cNew.x)}, ${round4(cNew.y)})`);
|
|
79
|
+
}
|
|
80
|
+
if (deltas.size !== 2)
|
|
81
|
+
throw new Error(`expected both templates in catalog, found ${deltas.size}`);
|
|
82
|
+
const touched = [];
|
|
83
|
+
let skippedInline = 0;
|
|
84
|
+
for (const layout of layouts) {
|
|
85
|
+
for (const piece of layout.pieces ?? []) {
|
|
86
|
+
if (!piece.template || !TARGETS.has(piece.template))
|
|
87
|
+
continue;
|
|
88
|
+
if (piece.footprint) {
|
|
89
|
+
skippedInline++; // baked geometry: template id is provenance only
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const { delta, movedIdx } = deltas.get(piece.template);
|
|
93
|
+
const d = rotateCw(applyMirror(delta, piece.mirror ?? "none"), piece.rotation_degrees ?? 0);
|
|
94
|
+
piece.position = { x: round4(piece.position.x + d.x), y: round4(piece.position.y + d.y) };
|
|
95
|
+
touched.push({
|
|
96
|
+
layoutId: layout.id,
|
|
97
|
+
pieceId: piece.id ?? "<anon>",
|
|
98
|
+
template: piece.template,
|
|
99
|
+
movedIdx,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ---- verify against the snapshot ------------------------------------------
|
|
104
|
+
const after = new Map(layouts.map((l) => [l.id, resolveLayout(l, catalog)]));
|
|
105
|
+
const touchedKey = new Set(touched.map((t) => `${t.layoutId}/${t.pieceId}`));
|
|
106
|
+
let failures = 0;
|
|
107
|
+
console.log("\nlayout/piece fixed-corner Δ leg Δ");
|
|
108
|
+
for (const t of touched) {
|
|
109
|
+
const b = before.get(t.layoutId).find((r) => r.id === t.pieceId);
|
|
110
|
+
const a = after.get(t.layoutId).find((r) => r.id === t.pieceId);
|
|
111
|
+
if (!b || !a || b.vertices.length !== a.vertices.length) {
|
|
112
|
+
failures++;
|
|
113
|
+
console.error(` ✗ ${t.layoutId}/${t.pieceId}: piece missing or vertex count changed`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let fixedMax = 0;
|
|
117
|
+
let legMin = Infinity;
|
|
118
|
+
let legMax = 0;
|
|
119
|
+
b.vertices.forEach((bv, i) => {
|
|
120
|
+
const av = a.vertices[i];
|
|
121
|
+
const moved = Math.hypot(av.x - bv.x, av.y - bv.y);
|
|
122
|
+
if (t.movedIdx.includes(i)) {
|
|
123
|
+
legMin = Math.min(legMin, moved);
|
|
124
|
+
legMax = Math.max(legMax, moved);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
fixedMax = Math.max(fixedMax, moved);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
const ok = fixedMax <= TOL && Math.abs(legMin - 0.5) <= TOL && Math.abs(legMax - 0.5) <= TOL;
|
|
131
|
+
if (!ok)
|
|
132
|
+
failures++;
|
|
133
|
+
console.log(` ${ok ? "✓" : "✗"} ${`${t.layoutId}/${t.pieceId}`.padEnd(52)} ${fixedMax.toExponential(1).padStart(8)} ${legMin.toFixed(4)}–${legMax.toFixed(4)}`);
|
|
134
|
+
}
|
|
135
|
+
// Everything NOT touched must be bit-identical.
|
|
136
|
+
for (const layout of layouts) {
|
|
137
|
+
const b = before.get(layout.id);
|
|
138
|
+
const a = after.get(layout.id);
|
|
139
|
+
for (let i = 0; i < b.length; i++) {
|
|
140
|
+
const key = `${layout.id}/${b[i].id ?? ""}`;
|
|
141
|
+
if (touchedKey.has(key))
|
|
142
|
+
continue;
|
|
143
|
+
const same = b[i].vertices.length === a[i].vertices.length &&
|
|
144
|
+
b[i].vertices.every((v, j) => v.x === a[i].vertices[j].x && v.y === a[i].vertices[j].y);
|
|
145
|
+
if (!same) {
|
|
146
|
+
failures++;
|
|
147
|
+
console.error(` ✗ ${key}: untouched piece geometry changed`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (failures > 0) {
|
|
152
|
+
console.error(`\nFIX FAILED: ${failures} verification failures; files NOT written.`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
writeFileSync(CATALOG_PATH, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
156
|
+
writeFileSync(LAYOUTS_PATH, `${JSON.stringify(layouts, null, 2)}\n`);
|
|
157
|
+
console.log(`\n✓ ${touched.length} placements compensated (${skippedInline} inline-baked pieces left untouched). Wrote both data files.`);
|
|
158
|
+
}
|
|
159
|
+
main();
|
|
160
|
+
//# sourceMappingURL=fix-corner-ruin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fix-corner-ruin.js","sourceRoot":"","sources":["../src/fix-corner-ruin.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One-shot correction (refuses to re-run once applied) of the `corner-ruin-left` /\n * `corner-ruin-right` catalog templates: the long leg is 7″ but the physical\n * piece is 6.5″. Shrinks the leg in `data/core/terrain-templates.json` and\n * compensates every layout piece referencing those templates in\n * `data/core/terrain-layouts.json` so the resolved outer corner stays exactly\n * where it was placed.\n *\n * Pieces are centroid-anchored (`board = position + R·M·(v − c)`), so changing\n * the footprint moves the centroid `c` and would shift the whole piece. Keeping\n * any unchanged vertex fixed requires `position += R·M·(c_new − c_old)` — the\n * same correction in board space and in a parent area's centroid-local frame\n * (the area transform is rigid and applied after).\n *\n * Pieces with an inline baked `footprint` (the migrated gw-11e-crucible corner\n * segments, which carry the template id only as provenance) are intentionally\n * untouched: the template edit cannot affect them.\n *\n * Verifies by resolving every layout before and after: unchanged footprint\n * vertices must not move, the two shortened leg vertices must move by exactly\n * 0.5″, and every piece not referencing the templates must be bit-identical.\n *\n * Usage: `npx tsx src/fix-corner-ruin.ts` (run from `tools/`).\n */\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport {\n resolveLayout,\n polygonCentroid,\n footprintVertices,\n type Vec2,\n type Mirror,\n type TerrainTemplate,\n type TerrainLayout,\n} from \"./terrain/resolve.js\";\n\nconst REPO_ROOT = join(new URL(\"../..\", import.meta.url).pathname);\nconst CATALOG_PATH = join(REPO_ROOT, \"data\", \"core\", \"terrain-templates.json\");\nconst LAYOUTS_PATH = join(REPO_ROOT, \"data\", \"core\", \"terrain-layouts.json\");\n\nconst OLD_LEG = 7;\nconst NEW_LEG = 6.5;\nconst TARGETS = new Set([\"corner-ruin-left\", \"corner-ruin-right\"]);\n/** Resolver output is rounded to 1e-4; position rounding adds ≤5e-5 per axis. */\nconst TOL = 2e-4;\n\n// Mirror → rotate, matching the resolver's orientation math (y-down, CW).\nfunction applyMirror(v: Vec2, m: Mirror): Vec2 {\n if (m === \"horizontal\") return { x: -v.x, y: v.y };\n if (m === \"vertical\") return { x: v.x, y: -v.y };\n return v;\n}\nfunction rotateCw(v: Vec2, deg: number): Vec2 {\n const r = (deg * Math.PI) / 180;\n const c = Math.cos(r);\n const s = Math.sin(r);\n return { x: c * v.x - s * v.y, y: s * v.x + c * v.y };\n}\nconst round4 = (n: number): number => Math.round(n * 1e4) / 1e4;\n\nfunction main(): void {\n const catalog = JSON.parse(readFileSync(CATALOG_PATH, \"utf8\")) as TerrainTemplate[];\n const layouts = JSON.parse(readFileSync(LAYOUTS_PATH, \"utf8\")) as TerrainLayout[];\n\n // ---- snapshot ground truth with the OLD catalog --------------------------\n const before = new Map(layouts.map((l) => [l.id, resolveLayout(l, catalog)]));\n\n // ---- patch the two templates, recording centroid deltas ------------------\n const deltas = new Map<string, { delta: Vec2; movedIdx: number[] }>();\n for (const t of catalog) {\n if (!TARGETS.has(t.id)) continue;\n if (t.footprint.type !== \"polygon\") throw new Error(`${t.id}: expected polygon footprint`);\n const oldVerts = footprintVertices(t.footprint);\n const movedIdx: number[] = [];\n t.footprint.points.forEach((p, i) => {\n if (p.y === OLD_LEG) {\n p.y = NEW_LEG;\n movedIdx.push(i);\n }\n });\n if (movedIdx.length !== 2) {\n throw new Error(`${t.id}: expected exactly 2 leg vertices at y=${OLD_LEG}, found ${movedIdx.length}`);\n }\n const cOld = polygonCentroid(oldVerts);\n const cNew = polygonCentroid(footprintVertices(t.footprint));\n const delta = { x: cNew.x - cOld.x, y: cNew.y - cOld.y };\n deltas.set(t.id, { delta, movedIdx });\n console.log(\n `${t.id}: centroid (${round4(cOld.x)}, ${round4(cOld.y)}) → (${round4(cNew.x)}, ${round4(cNew.y)})`,\n );\n }\n if (deltas.size !== 2) throw new Error(`expected both templates in catalog, found ${deltas.size}`);\n\n // ---- compensate every template-referencing placement ---------------------\n interface Touched {\n layoutId: string;\n pieceId: string;\n template: string;\n movedIdx: number[];\n }\n const touched: Touched[] = [];\n let skippedInline = 0;\n for (const layout of layouts) {\n for (const piece of layout.pieces ?? []) {\n if (!piece.template || !TARGETS.has(piece.template)) continue;\n if (piece.footprint) {\n skippedInline++; // baked geometry: template id is provenance only\n continue;\n }\n const { delta, movedIdx } = deltas.get(piece.template)!;\n const d = rotateCw(applyMirror(delta, piece.mirror ?? \"none\"), piece.rotation_degrees ?? 0);\n piece.position = { x: round4(piece.position.x + d.x), y: round4(piece.position.y + d.y) };\n touched.push({\n layoutId: layout.id,\n pieceId: piece.id ?? \"<anon>\",\n template: piece.template,\n movedIdx,\n });\n }\n }\n\n // ---- verify against the snapshot ------------------------------------------\n const after = new Map(layouts.map((l) => [l.id, resolveLayout(l, catalog)]));\n const touchedKey = new Set(touched.map((t) => `${t.layoutId}/${t.pieceId}`));\n let failures = 0;\n\n console.log(\"\\nlayout/piece fixed-corner Δ leg Δ\");\n for (const t of touched) {\n const b = before.get(t.layoutId)!.find((r) => r.id === t.pieceId);\n const a = after.get(t.layoutId)!.find((r) => r.id === t.pieceId);\n if (!b || !a || b.vertices.length !== a.vertices.length) {\n failures++;\n console.error(` ✗ ${t.layoutId}/${t.pieceId}: piece missing or vertex count changed`);\n continue;\n }\n let fixedMax = 0;\n let legMin = Infinity;\n let legMax = 0;\n b.vertices.forEach((bv, i) => {\n const av = a.vertices[i];\n const moved = Math.hypot(av.x - bv.x, av.y - bv.y);\n if (t.movedIdx.includes(i)) {\n legMin = Math.min(legMin, moved);\n legMax = Math.max(legMax, moved);\n } else {\n fixedMax = Math.max(fixedMax, moved);\n }\n });\n const ok = fixedMax <= TOL && Math.abs(legMin - 0.5) <= TOL && Math.abs(legMax - 0.5) <= TOL;\n if (!ok) failures++;\n console.log(\n ` ${ok ? \"✓\" : \"✗\"} ${`${t.layoutId}/${t.pieceId}`.padEnd(52)} ${fixedMax.toExponential(1).padStart(8)} ${legMin.toFixed(4)}–${legMax.toFixed(4)}`,\n );\n }\n\n // Everything NOT touched must be bit-identical.\n for (const layout of layouts) {\n const b = before.get(layout.id)!;\n const a = after.get(layout.id)!;\n for (let i = 0; i < b.length; i++) {\n const key = `${layout.id}/${b[i].id ?? \"\"}`;\n if (touchedKey.has(key)) continue;\n const same =\n b[i].vertices.length === a[i].vertices.length &&\n b[i].vertices.every((v, j) => v.x === a[i].vertices[j].x && v.y === a[i].vertices[j].y);\n if (!same) {\n failures++;\n console.error(` ✗ ${key}: untouched piece geometry changed`);\n }\n }\n }\n\n if (failures > 0) {\n console.error(`\\nFIX FAILED: ${failures} verification failures; files NOT written.`);\n process.exit(1);\n }\n\n writeFileSync(CATALOG_PATH, `${JSON.stringify(catalog, null, 2)}\\n`);\n writeFileSync(LAYOUTS_PATH, `${JSON.stringify(layouts, null, 2)}\\n`);\n console.log(\n `\\n✓ ${touched.length} placements compensated (${skippedInline} inline-baked pieces left untouched). Wrote both data files.`,\n );\n}\n\nmain();\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpaca-software/40kdc-data",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The 40kdc Warhammer 40K dataset behind a linked, typed API — find units, follow them to their weapons, abilities, phases, and factions. Also validates data against the canonical JSON Schemas.",
|
|
6
6
|
"keywords": [
|