@alpaca-software/40kdc-data 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/fix-corner-ruin.d.ts +2 -0
- package/dist/fix-corner-ruin.d.ts.map +1 -0
- package/dist/fix-corner-ruin.js +160 -0
- package/dist/fix-corner-ruin.js.map +1 -0
- package/dist/gen-conformance.js +204 -0
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +19 -0
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +34 -1
- package/dist/runner.js.map +1 -1
- package/dist/terrain/index.d.ts +3 -1
- package/dist/terrain/index.d.ts.map +1 -1
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/index.js.map +1 -1
- package/dist/terrain/keystones.d.ts +49 -0
- package/dist/terrain/keystones.d.ts.map +1 -0
- package/dist/terrain/keystones.js +87 -0
- package/dist/terrain/keystones.js.map +1 -0
- package/dist/terrain/resolve.d.ts +26 -0
- package/dist/terrain/resolve.d.ts.map +1 -1
- package/dist/terrain/resolve.js.map +1 -1
- package/dist/terrain/solve.d.ts +2 -15
- package/dist/terrain/solve.d.ts.map +1 -1
- package/dist/terrain/solve.js +1 -1
- package/dist/terrain/solve.js.map +1 -1
- package/package.json +1 -1
- package/schemas/core/terrain-layout.schema.json +39 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/terrain/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAkEH,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC;AAE1B,4EAA4E;AAC5E,MAAM,UAAU,iBAAiB,CAAC,EAAa;IAC7C,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;QAChB,KAAK,WAAW;YACd,OAAO;gBACL,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;gBACd,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;gBACrB,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE;gBAC7B,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE;aACvB,CAAC;QACJ,KAAK,gBAAgB;YACnB,yDAAyD;YACzD,OAAO;gBACL,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;gBACd,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;gBACrB,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE;aACvB,CAAC;QACJ,KAAK,SAAS;YACZ,OAAO,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpD,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,UAAU,GAAU,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACnC,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACpC,SAAS,IAAI,KAAK,CAAC;QACnB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;QAC1B,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;IAC5B,CAAC;IACD,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5F,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1C,CAAC;IACD,OAAO,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED,SAAS,WAAW,CAAC,CAAO,EAAE,CAAS;IACrC,QAAQ,CAAC,EAAE,CAAC;QACV,KAAK,YAAY;YACf,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B,KAAK,UAAU;YACb,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED,+DAA+D;AAC/D,SAAS,QAAQ,CAAC,CAAO,EAAE,GAAW;IACpC,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACzC,MAAM,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC;IACpB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtB,OAAO,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,kFAAkF;AAClF,SAAS,MAAM,CAAC,CAAO,EAAE,QAAgB,EAAE,MAAc;IACvD,OAAO,QAAQ,CAAC,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,SAAoB,EAAE,QAAgB,EAAE,MAAc;IACpF,MAAM,KAAK,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC3C,MAAM,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CACrB,EAAa,EACb,QAAc,EACd,QAAgB,EAChB,MAAc;IAEd,MAAM,KAAK,GAAG,iBAAiB,CAAC,EAAE,CAAC,CAAC;IACpC,MAAM,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACrB,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QACnE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,SAAS,MAAM,CAAC,CAAO;IACrB,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,GAAG,YAAY,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,GAAG,YAAY,EAAE,CAAC;AAChH,CAAC;AAED,SAAS,cAAc,CAAC,KAAqC;IAC3D,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;AAC5D,CAAC;AAED,MAAM,OAAO,mBAAoB,SAAQ,KAAK;CAAG;AAEjD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAAqB,EAAE,SAA4B;IAC/E,MAAM,IAAI,GAAG,IAAI,GAAG,EAA2B,CAAC;IAChD,KAAK,MAAM,CAAC,IAAI,SAAS;QAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAE7C,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;IACnC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAuB,CAAC;IACjD,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,CAAC,EAAE;YAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAEzD,MAAM,WAAW,GAAG,CAAC,KAAmD,EAAE,KAAa,EAAa,EAAE;QACpG,IAAI,KAAK,CAAC,SAAS;YAAE,OAAO,KAAK,CAAC,SAAS,CAAC;QAC5C,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACnC,IAAI,CAAC,CAAC;gBAAE,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,uBAAuB,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC;YACxF,OAAO,CAAC,CAAC,SAAS,CAAC;QACrB,CAAC;QACD,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,4CAA4C,CAAC,CAAC;IACtF,CAAC,CAAC;IAEF,MAAM,GAAG,GAAoB,EAAE,CAAC;IAEhC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,IAAI,SAAS,CAAC;QAClD,MAAM,EAAE,GAAG,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,gBAAgB,IAAI,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC;QACtC,MAAM,SAAS,GAAG,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAElF,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;YACzB,4DAA4D;YAC5D,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;YACnD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,6BAA6B,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC;YAC9F,CAAC;YACD,MAAM,SAAS,GAAG,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YACvE,MAAM,IAAI,GAAG,MAAM,CAAC,gBAAgB,IAAI,CAAC,CAAC;YAC1C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC;YACxC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;gBACnC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;gBACnC,OAAO,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC;YAC5E,CAAC,CAAC,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjG,SAAS;QACX,CAAC;QAED,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAClF,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QAEjG,2EAA2E;QAC3E,2DAA2D;QAC3D,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACnC,KAAK,MAAM,IAAI,IAAI,CAAC,EAAE,QAAQ,IAAI,EAAE,EAAE,CAAC;gBACrC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACnC,IAAI,CAAC,EAAE,EAAE,CAAC;oBACR,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,mDAAmD,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;gBAC7G,CAAC;gBACD,MAAM,SAAS,GAAG,cAAc,CAC9B,EAAE,CAAC,SAAS,EACZ,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,gBAAgB,IAAI,CAAC,EAC1B,IAAI,CAAC,MAAM,IAAI,MAAM,CACtB,CAAC;gBACF,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;oBACpC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;oBACtC,OAAO,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC;gBAC1E,CAAC,CAAC,CAAC;gBACH,GAAG,CAAC,IAAI,CAAC;oBACP,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI;oBACnB,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI;oBACrB,UAAU,EAAE,SAAS;oBACrB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;oBACtB,QAAQ,EAAE,SAAS;iBACpB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * Terrain layout resolver — turns a {@link TerrainLayout} (template references +\n * centroid-anchored placements + rotation/mirror) into absolute board-space\n * polygon vertices. This is the shared geometry contract pinned by the\n * `conformance/terrain-resolver` corpus; the Rust crate implements the same\n * function and must reproduce these vertices byte-for-byte (4-dp rounded).\n *\n * ## Transform contract\n *\n * Frames are board inches, origin at a board corner, **y-down** (per\n * `common.schema.json#/$defs/vec2`). A footprint is authored in natural local\n * y-down coordinates; the resolver derives its **polygon area centroid** and\n * treats local vertices as `(v - centroid)`, so `position` always denotes the\n * centroid and is invariant under rotation and mirror.\n *\n * Local → board, for an unparented piece, is `mirror → rotate → translate`:\n *\n * board = position + R_cw(rotation) · M(mirror) · (v - centroid)\n *\n * with `M`: horizontal → (-x, y), vertical → (x, -y); and `R_cw(θ)` a clockwise\n * rotation in the y-down frame, `[[cosθ, -sinθ], [sinθ, cosθ]]`.\n *\n * A feature with a `parent_area_id` (or a template's composed feature) is first\n * placed in the parent area's **centroid-local frame** (origin at the area\n * centroid), then carried through the area's own placement:\n *\n * board = T_area ∘ R_area ∘ M_area ( featurePos + R_feat · M_feat · (w - C_feat) )\n *\n * ## Emission order (a pinned invariant)\n *\n * Pieces are emitted in `layout.pieces` order. When a piece instances an area\n * template that carries composed `features`, those features are emitted\n * immediately after their area, in template-declaration order.\n */\n\nexport interface Vec2 {\n x: number;\n y: number;\n}\n\nexport type Footprint =\n | { type: \"rectangle\"; width: number; height: number }\n | { type: \"right-triangle\"; width: number; height: number }\n | { type: \"polygon\"; points: Vec2[] };\n\nexport type Mirror = \"none\" | \"horizontal\" | \"vertical\";\n\nexport interface ComposedFeature {\n id?: string;\n template: string;\n position: Vec2;\n rotation_degrees?: number;\n mirror?: Mirror;\n floor?: number;\n}\n\nexport interface TerrainTemplate {\n id: string;\n name?: string;\n kind: \"area\" | \"feature\";\n footprint: Footprint;\n default_height_inches?: number;\n default_blocking?: boolean;\n default_terrain_area_keywords?: string[];\n features?: ComposedFeature[];\n}\n\nexport interface LayoutPiece {\n id?: string;\n name?: string;\n piece_type?: \"area\" | \"feature\";\n template?: string;\n footprint?: Footprint;\n position: Vec2;\n rotation_degrees?: number;\n mirror?: Mirror;\n parent_area_id?: string;\n floor?: number;\n height_inches?: number;\n terrain_area_keywords?: string[];\n link_group?: string;\n}\n\nexport interface TerrainLayout {\n id: string;\n name: string;\n pieces?: LayoutPiece[];\n}\n\nexport interface ResolvedPiece {\n /** Layout-local id when present, else the piece name, else null. */\n id: string | null;\n name: string | null;\n piece_type: \"area\" | \"feature\";\n floor: number;\n /** Absolute board-space polygon vertices, y-down. */\n vertices: Vec2[];\n}\n\nconst DEG = Math.PI / 180;\n\n/** A footprint's polygon vertices in natural local (y-down) coordinates. */\nexport function footprintVertices(fp: Footprint): Vec2[] {\n switch (fp.type) {\n case \"rectangle\":\n return [\n { x: 0, y: 0 },\n { x: fp.width, y: 0 },\n { x: fp.width, y: fp.height },\n { x: 0, y: fp.height },\n ];\n case \"right-triangle\":\n // Right angle at the local origin, legs along +x and +y.\n return [\n { x: 0, y: 0 },\n { x: fp.width, y: 0 },\n { x: 0, y: fp.height },\n ];\n case \"polygon\":\n return fp.points.map((p) => ({ x: p.x, y: p.y }));\n default: {\n const exhaustive: never = fp;\n throw new Error(`unknown footprint type: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n/**\n * Polygon area centroid (shoelace). Falls back to the vertex mean when the\n * polygon is degenerate (zero signed area, e.g. collinear points) so the\n * resolver never divides by zero.\n */\nexport function polygonCentroid(verts: Vec2[]): Vec2 {\n const n = verts.length;\n if (n === 0) return { x: 0, y: 0 };\n let twiceArea = 0;\n let cx = 0;\n let cy = 0;\n for (let i = 0; i < n; i++) {\n const a = verts[i];\n const b = verts[(i + 1) % n];\n const cross = a.x * b.y - b.x * a.y;\n twiceArea += cross;\n cx += (a.x + b.x) * cross;\n cy += (a.y + b.y) * cross;\n }\n if (twiceArea === 0) {\n const mean = verts.reduce((acc, v) => ({ x: acc.x + v.x, y: acc.y + v.y }), { x: 0, y: 0 });\n return { x: mean.x / n, y: mean.y / n };\n }\n return { x: cx / (3 * twiceArea), y: cy / (3 * twiceArea) };\n}\n\nfunction applyMirror(v: Vec2, m: Mirror): Vec2 {\n switch (m) {\n case \"horizontal\":\n return { x: -v.x, y: v.y };\n case \"vertical\":\n return { x: v.x, y: -v.y };\n default:\n return v;\n }\n}\n\n/** Clockwise rotation by `deg` degrees in the y-down frame. */\nfunction rotateCw(v: Vec2, deg: number): Vec2 {\n if (deg === 0) return { x: v.x, y: v.y };\n const r = deg * DEG;\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}\n\n/** mirror → rotate (no translation). The orientation-only part of a placement. */\nfunction orient(v: Vec2, rotation: number, mirror: Mirror): Vec2 {\n return rotateCw(applyMirror(v, mirror), rotation);\n}\n\n/**\n * The board-space offset of each footprint vertex from the piece centroid,\n * after mirror + rotation but before translation. Adding `position` to each\n * gives the resolved board vertices; this is the orientation-only part a\n * card-measurement solver inverts to recover the centroid. Vertex order matches\n * {@link footprintVertices}.\n */\nexport function orientedOffsets(footprint: Footprint, rotation: number, mirror: Mirror): Vec2[] {\n const verts = footprintVertices(footprint);\n const c = polygonCentroid(verts);\n return verts.map((v) => orient({ x: v.x - c.x, y: v.y - c.y }, rotation, mirror));\n}\n\n/**\n * Place a footprint's local vertices into a target frame: recenter on the\n * footprint centroid, mirror, rotate, then translate so the centroid lands on\n * `position`. The target frame is board space for an area, or the parent area's\n * centroid-local frame for a composed/parented feature.\n */\nfunction placeFootprint(\n fp: Footprint,\n position: Vec2,\n rotation: number,\n mirror: Mirror,\n): Vec2[] {\n const verts = footprintVertices(fp);\n const c = polygonCentroid(verts);\n return verts.map((v) => {\n const o = orient({ x: v.x - c.x, y: v.y - c.y }, rotation, mirror);\n return { x: o.x + position.x, y: o.y + position.y };\n });\n}\n\nconst TWO_DP_ROUND = 1e4;\nfunction round4(v: Vec2): Vec2 {\n return { x: Math.round(v.x * TWO_DP_ROUND) / TWO_DP_ROUND, y: Math.round(v.y * TWO_DP_ROUND) / TWO_DP_ROUND };\n}\n\nfunction resolvedIdName(piece: { id?: string; name?: string }): { id: string | null; name: string | null } {\n return { id: piece.id ?? null, name: piece.name ?? null };\n}\n\nexport class TerrainResolveError extends Error {}\n\n/**\n * Resolve a layout to absolute board-space vertices per piece. `templates` is\n * the catalog a piece's `template` references resolve against.\n */\nexport function resolveLayout(layout: TerrainLayout, templates: TerrainTemplate[]): ResolvedPiece[] {\n const byId = new Map<string, TerrainTemplate>();\n for (const t of templates) byId.set(t.id, t);\n\n const pieces = layout.pieces ?? [];\n const areasById = new Map<string, LayoutPiece>();\n for (const p of pieces) if (p.id) areasById.set(p.id, p);\n\n const footprintOf = (piece: { template?: string; footprint?: Footprint }, where: string): Footprint => {\n if (piece.footprint) return piece.footprint;\n if (piece.template) {\n const t = byId.get(piece.template);\n if (!t) throw new TerrainResolveError(`${where}: unknown template \"${piece.template}\"`);\n return t.footprint;\n }\n throw new TerrainResolveError(`${where}: piece has neither footprint nor template`);\n };\n\n const out: ResolvedPiece[] = [];\n\n for (const piece of pieces) {\n const where = piece.id ?? piece.name ?? \"<piece>\";\n const fp = footprintOf(piece, where);\n const rotation = piece.rotation_degrees ?? 0;\n const mirror = piece.mirror ?? \"none\";\n const pieceType = piece.piece_type ?? (piece.parent_area_id ? \"feature\" : \"area\");\n\n if (piece.parent_area_id) {\n // Feature placed in its parent area's centroid-local frame.\n const parent = areasById.get(piece.parent_area_id);\n if (!parent) {\n throw new TerrainResolveError(`${where}: unknown parent_area_id \"${piece.parent_area_id}\"`);\n }\n const areaLocal = placeFootprint(fp, piece.position, rotation, mirror);\n const aRot = parent.rotation_degrees ?? 0;\n const aMirror = parent.mirror ?? \"none\";\n const vertices = areaLocal.map((p) => {\n const o = orient(p, aRot, aMirror);\n return round4({ x: o.x + parent.position.x, y: o.y + parent.position.y });\n });\n out.push({ ...resolvedIdName(piece), piece_type: pieceType, floor: piece.floor ?? 0, vertices });\n continue;\n }\n\n // Unparented area or feature: place directly in board space.\n const vertices = placeFootprint(fp, piece.position, rotation, mirror).map(round4);\n out.push({ ...resolvedIdName(piece), piece_type: pieceType, floor: piece.floor ?? 0, vertices });\n\n // Expand an area template's composed features, carried through this area's\n // placement (same composition math as a parented feature).\n if (piece.template) {\n const t = byId.get(piece.template);\n for (const feat of t?.features ?? []) {\n const ft = byId.get(feat.template);\n if (!ft) {\n throw new TerrainResolveError(`${where}: composed feature references unknown template \"${feat.template}\"`);\n }\n const areaLocal = placeFootprint(\n ft.footprint,\n feat.position,\n feat.rotation_degrees ?? 0,\n feat.mirror ?? \"none\",\n );\n const featVerts = areaLocal.map((p) => {\n const o = orient(p, rotation, mirror);\n return round4({ x: o.x + piece.position.x, y: o.y + piece.position.y });\n });\n out.push({\n id: feat.id ?? null,\n name: ft.name ?? null,\n piece_type: \"feature\",\n floor: feat.floor ?? 0,\n vertices: featVerts,\n });\n }\n }\n }\n\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/terrain/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Terrain layout resolver — turns a {@link TerrainLayout} (template references +\n * centroid-anchored placements + rotation/mirror) into absolute board-space\n * polygon vertices. This is the shared geometry contract pinned by the\n * `conformance/terrain-resolver` corpus; the Rust crate implements the same\n * function and must reproduce these vertices byte-for-byte (4-dp rounded).\n *\n * ## Transform contract\n *\n * Frames are board inches, origin at a board corner, **y-down** (per\n * `common.schema.json#/$defs/vec2`). A footprint is authored in natural local\n * y-down coordinates; the resolver derives its **polygon area centroid** and\n * treats local vertices as `(v - centroid)`, so `position` always denotes the\n * centroid and is invariant under rotation and mirror.\n *\n * Local → board, for an unparented piece, is `mirror → rotate → translate`:\n *\n * board = position + R_cw(rotation) · M(mirror) · (v - centroid)\n *\n * with `M`: horizontal → (-x, y), vertical → (x, -y); and `R_cw(θ)` a clockwise\n * rotation in the y-down frame, `[[cosθ, -sinθ], [sinθ, cosθ]]`.\n *\n * A feature with a `parent_area_id` (or a template's composed feature) is first\n * placed in the parent area's **centroid-local frame** (origin at the area\n * centroid), then carried through the area's own placement:\n *\n * board = T_area ∘ R_area ∘ M_area ( featurePos + R_feat · M_feat · (w - C_feat) )\n *\n * ## Emission order (a pinned invariant)\n *\n * Pieces are emitted in `layout.pieces` order. When a piece instances an area\n * template that carries composed `features`, those features are emitted\n * immediately after their area, in template-declaration order.\n */\n\nexport interface Vec2 {\n x: number;\n y: number;\n}\n\nexport type Footprint =\n | { type: \"rectangle\"; width: number; height: number }\n | { type: \"right-triangle\"; width: number; height: number }\n | { type: \"polygon\"; points: Vec2[] };\n\nexport type Mirror = \"none\" | \"horizontal\" | \"vertical\";\n\n/** A board edge a card dimension is measured from. left/right pin x; top/bottom pin y. */\nexport type BoardEdge = \"left\" | \"right\" | \"top\" | \"bottom\";\n\n/**\n * Which feature of the placed area a dimension line reaches: a specific\n * footprint vertex (by index, in {@link footprintVertices} order), or one of\n * the placed area's axis-aligned bounding faces (\"the left face\", etc.).\n */\nexport type FeatureRef =\n | { kind: \"vertex\"; index: number }\n | { kind: \"face\"; side: \"min-x\" | \"max-x\" | \"min-y\" | \"max-y\" };\n\n/**\n * One authored measurement keystone: the dimension line a reference card\n * prints so a player can place the piece with a tape measure. Only the\n * selection is stored — the distance is always derived from resolved geometry\n * (see `keystoneMeasurements`), so a keystone can never disagree with the\n * layout.\n */\nexport interface Keystone {\n edge: BoardEdge;\n ref: FeatureRef;\n}\n\nexport interface ComposedFeature {\n id?: string;\n template: string;\n position: Vec2;\n rotation_degrees?: number;\n mirror?: Mirror;\n floor?: number;\n}\n\nexport interface TerrainTemplate {\n id: string;\n name?: string;\n kind: \"area\" | \"feature\";\n footprint: Footprint;\n default_height_inches?: number;\n default_blocking?: boolean;\n default_terrain_area_keywords?: string[];\n features?: ComposedFeature[];\n}\n\nexport interface LayoutPiece {\n id?: string;\n name?: string;\n piece_type?: \"area\" | \"feature\";\n template?: string;\n footprint?: Footprint;\n position: Vec2;\n rotation_degrees?: number;\n mirror?: Mirror;\n parent_area_id?: string;\n floor?: number;\n height_inches?: number;\n terrain_area_keywords?: string[];\n link_group?: string;\n keystones?: Keystone[];\n}\n\nexport interface TerrainLayout {\n id: string;\n name: string;\n pieces?: LayoutPiece[];\n}\n\nexport interface ResolvedPiece {\n /** Layout-local id when present, else the piece name, else null. */\n id: string | null;\n name: string | null;\n piece_type: \"area\" | \"feature\";\n floor: number;\n /** Absolute board-space polygon vertices, y-down. */\n vertices: Vec2[];\n}\n\nconst DEG = Math.PI / 180;\n\n/** A footprint's polygon vertices in natural local (y-down) coordinates. */\nexport function footprintVertices(fp: Footprint): Vec2[] {\n switch (fp.type) {\n case \"rectangle\":\n return [\n { x: 0, y: 0 },\n { x: fp.width, y: 0 },\n { x: fp.width, y: fp.height },\n { x: 0, y: fp.height },\n ];\n case \"right-triangle\":\n // Right angle at the local origin, legs along +x and +y.\n return [\n { x: 0, y: 0 },\n { x: fp.width, y: 0 },\n { x: 0, y: fp.height },\n ];\n case \"polygon\":\n return fp.points.map((p) => ({ x: p.x, y: p.y }));\n default: {\n const exhaustive: never = fp;\n throw new Error(`unknown footprint type: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n/**\n * Polygon area centroid (shoelace). Falls back to the vertex mean when the\n * polygon is degenerate (zero signed area, e.g. collinear points) so the\n * resolver never divides by zero.\n */\nexport function polygonCentroid(verts: Vec2[]): Vec2 {\n const n = verts.length;\n if (n === 0) return { x: 0, y: 0 };\n let twiceArea = 0;\n let cx = 0;\n let cy = 0;\n for (let i = 0; i < n; i++) {\n const a = verts[i];\n const b = verts[(i + 1) % n];\n const cross = a.x * b.y - b.x * a.y;\n twiceArea += cross;\n cx += (a.x + b.x) * cross;\n cy += (a.y + b.y) * cross;\n }\n if (twiceArea === 0) {\n const mean = verts.reduce((acc, v) => ({ x: acc.x + v.x, y: acc.y + v.y }), { x: 0, y: 0 });\n return { x: mean.x / n, y: mean.y / n };\n }\n return { x: cx / (3 * twiceArea), y: cy / (3 * twiceArea) };\n}\n\nfunction applyMirror(v: Vec2, m: Mirror): Vec2 {\n switch (m) {\n case \"horizontal\":\n return { x: -v.x, y: v.y };\n case \"vertical\":\n return { x: v.x, y: -v.y };\n default:\n return v;\n }\n}\n\n/** Clockwise rotation by `deg` degrees in the y-down frame. */\nfunction rotateCw(v: Vec2, deg: number): Vec2 {\n if (deg === 0) return { x: v.x, y: v.y };\n const r = deg * DEG;\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}\n\n/** mirror → rotate (no translation). The orientation-only part of a placement. */\nfunction orient(v: Vec2, rotation: number, mirror: Mirror): Vec2 {\n return rotateCw(applyMirror(v, mirror), rotation);\n}\n\n/**\n * The board-space offset of each footprint vertex from the piece centroid,\n * after mirror + rotation but before translation. Adding `position` to each\n * gives the resolved board vertices; this is the orientation-only part a\n * card-measurement solver inverts to recover the centroid. Vertex order matches\n * {@link footprintVertices}.\n */\nexport function orientedOffsets(footprint: Footprint, rotation: number, mirror: Mirror): Vec2[] {\n const verts = footprintVertices(footprint);\n const c = polygonCentroid(verts);\n return verts.map((v) => orient({ x: v.x - c.x, y: v.y - c.y }, rotation, mirror));\n}\n\n/**\n * Place a footprint's local vertices into a target frame: recenter on the\n * footprint centroid, mirror, rotate, then translate so the centroid lands on\n * `position`. The target frame is board space for an area, or the parent area's\n * centroid-local frame for a composed/parented feature.\n */\nfunction placeFootprint(\n fp: Footprint,\n position: Vec2,\n rotation: number,\n mirror: Mirror,\n): Vec2[] {\n const verts = footprintVertices(fp);\n const c = polygonCentroid(verts);\n return verts.map((v) => {\n const o = orient({ x: v.x - c.x, y: v.y - c.y }, rotation, mirror);\n return { x: o.x + position.x, y: o.y + position.y };\n });\n}\n\nconst TWO_DP_ROUND = 1e4;\nfunction round4(v: Vec2): Vec2 {\n return { x: Math.round(v.x * TWO_DP_ROUND) / TWO_DP_ROUND, y: Math.round(v.y * TWO_DP_ROUND) / TWO_DP_ROUND };\n}\n\nfunction resolvedIdName(piece: { id?: string; name?: string }): { id: string | null; name: string | null } {\n return { id: piece.id ?? null, name: piece.name ?? null };\n}\n\nexport class TerrainResolveError extends Error {}\n\n/**\n * Resolve a layout to absolute board-space vertices per piece. `templates` is\n * the catalog a piece's `template` references resolve against.\n */\nexport function resolveLayout(layout: TerrainLayout, templates: TerrainTemplate[]): ResolvedPiece[] {\n const byId = new Map<string, TerrainTemplate>();\n for (const t of templates) byId.set(t.id, t);\n\n const pieces = layout.pieces ?? [];\n const areasById = new Map<string, LayoutPiece>();\n for (const p of pieces) if (p.id) areasById.set(p.id, p);\n\n const footprintOf = (piece: { template?: string; footprint?: Footprint }, where: string): Footprint => {\n if (piece.footprint) return piece.footprint;\n if (piece.template) {\n const t = byId.get(piece.template);\n if (!t) throw new TerrainResolveError(`${where}: unknown template \"${piece.template}\"`);\n return t.footprint;\n }\n throw new TerrainResolveError(`${where}: piece has neither footprint nor template`);\n };\n\n const out: ResolvedPiece[] = [];\n\n for (const piece of pieces) {\n const where = piece.id ?? piece.name ?? \"<piece>\";\n const fp = footprintOf(piece, where);\n const rotation = piece.rotation_degrees ?? 0;\n const mirror = piece.mirror ?? \"none\";\n const pieceType = piece.piece_type ?? (piece.parent_area_id ? \"feature\" : \"area\");\n\n if (piece.parent_area_id) {\n // Feature placed in its parent area's centroid-local frame.\n const parent = areasById.get(piece.parent_area_id);\n if (!parent) {\n throw new TerrainResolveError(`${where}: unknown parent_area_id \"${piece.parent_area_id}\"`);\n }\n const areaLocal = placeFootprint(fp, piece.position, rotation, mirror);\n const aRot = parent.rotation_degrees ?? 0;\n const aMirror = parent.mirror ?? \"none\";\n const vertices = areaLocal.map((p) => {\n const o = orient(p, aRot, aMirror);\n return round4({ x: o.x + parent.position.x, y: o.y + parent.position.y });\n });\n out.push({ ...resolvedIdName(piece), piece_type: pieceType, floor: piece.floor ?? 0, vertices });\n continue;\n }\n\n // Unparented area or feature: place directly in board space.\n const vertices = placeFootprint(fp, piece.position, rotation, mirror).map(round4);\n out.push({ ...resolvedIdName(piece), piece_type: pieceType, floor: piece.floor ?? 0, vertices });\n\n // Expand an area template's composed features, carried through this area's\n // placement (same composition math as a parented feature).\n if (piece.template) {\n const t = byId.get(piece.template);\n for (const feat of t?.features ?? []) {\n const ft = byId.get(feat.template);\n if (!ft) {\n throw new TerrainResolveError(`${where}: composed feature references unknown template \"${feat.template}\"`);\n }\n const areaLocal = placeFootprint(\n ft.footprint,\n feat.position,\n feat.rotation_degrees ?? 0,\n feat.mirror ?? \"none\",\n );\n const featVerts = areaLocal.map((p) => {\n const o = orient(p, rotation, mirror);\n return round4({ x: o.x + piece.position.x, y: o.y + piece.position.y });\n });\n out.push({\n id: feat.id ?? null,\n name: ft.name ?? null,\n piece_type: \"feature\",\n floor: feat.floor ?? 0,\n vertices: featVerts,\n });\n }\n }\n }\n\n return out;\n}\n"]}
|
package/dist/terrain/solve.d.ts
CHANGED
|
@@ -12,21 +12,8 @@
|
|
|
12
12
|
* Because the centroid is rotation- and mirror-invariant, orientation is fixed
|
|
13
13
|
* first; each dimension line then pins one axis of the centroid in closed form.
|
|
14
14
|
*/
|
|
15
|
-
import { type Footprint, type Mirror, type Vec2 } from "./resolve.js";
|
|
16
|
-
|
|
17
|
-
export type BoardEdge = "left" | "right" | "top" | "bottom";
|
|
18
|
-
/**
|
|
19
|
-
* Which feature of the placed area a dimension line reaches: a specific
|
|
20
|
-
* footprint vertex (by index, in {@link footprintVertices} order), or one of
|
|
21
|
-
* the placed area's axis-aligned bounding faces ("the left face", etc.).
|
|
22
|
-
*/
|
|
23
|
-
export type FeatureRef = {
|
|
24
|
-
kind: "vertex";
|
|
25
|
-
index: number;
|
|
26
|
-
} | {
|
|
27
|
-
kind: "face";
|
|
28
|
-
side: "min-x" | "max-x" | "min-y" | "max-y";
|
|
29
|
-
};
|
|
15
|
+
import { type BoardEdge, type FeatureRef, type Footprint, type Mirror, type Vec2 } from "./resolve.js";
|
|
16
|
+
export type { BoardEdge, FeatureRef } from "./resolve.js";
|
|
30
17
|
/** One card dimension line: `distance` inches from `edge` to `feature`. */
|
|
31
18
|
export interface DimensionLine {
|
|
32
19
|
edge: BoardEdge;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"solve.d.ts","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,
|
|
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"}
|
package/dist/terrain/solve.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Because the centroid is rotation- and mirror-invariant, orientation is fixed
|
|
13
13
|
* first; each dimension line then pins one axis of the centroid in closed form.
|
|
14
14
|
*/
|
|
15
|
-
import { orientedOffsets } from "./resolve.js";
|
|
15
|
+
import { orientedOffsets, } from "./resolve.js";
|
|
16
16
|
export class TerrainSolveError extends Error {
|
|
17
17
|
}
|
|
18
18
|
/** The signed offset (from the centroid) the given feature resolves to, on its axis. */
|
|
@@ -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 { orientedOffsets, type Footprint, type Mirror, type Vec2 } from \"./resolve.js\";\n\n/** A board edge a card dimension is measured from. left/right pin x; top/bottom pin y. */\nexport type BoardEdge = \"left\" | \"right\" | \"top\" | \"bottom\";\n\n/**\n * Which feature of the placed area a dimension line reaches: a specific\n * footprint vertex (by index, in {@link footprintVertices} order), or one of\n * the placed area's axis-aligned bounding faces (\"the left face\", etc.).\n */\nexport type FeatureRef =\n | { kind: \"vertex\"; index: number }\n | { kind: \"face\"; side: \"min-x\" | \"max-x\" | \"min-y\" | \"max-y\" };\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"]}
|
|
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"]}
|
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.7",
|
|
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": [
|
|
@@ -96,6 +96,45 @@
|
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"additionalProperties": false
|
|
99
|
+
},
|
|
100
|
+
"keystones": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"description": "Measurement keystones: the author-selected dimension lines a reference card prints so a player can place this piece with a tape measure (board edge → a feature of the placed piece). Only the selection is stored — the distance is always DERIVED from the resolved geometry by the shared keystone resolver (pinned by the conformance corpus), so a keystone can never disagree with the layout. Vertex indices follow the resolver's pinned vertex order; re-authoring a template's footprint invalidates them, so review keystones when geometry changes.",
|
|
103
|
+
"items": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"edge": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"enum": ["left", "right", "top", "bottom"],
|
|
109
|
+
"description": "The board edge the measurement runs from, in the y-down board frame (left/right pin x against board width; top/bottom pin y against board height)."
|
|
110
|
+
},
|
|
111
|
+
"ref": {
|
|
112
|
+
"description": "Which feature of the placed piece the measurement reaches: a footprint vertex (by resolver vertex order) or an axis-aligned bounding face of the placed footprint.",
|
|
113
|
+
"oneOf": [
|
|
114
|
+
{
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"kind": { "const": "vertex" },
|
|
118
|
+
"index": { "type": "integer", "minimum": 0 }
|
|
119
|
+
},
|
|
120
|
+
"required": ["kind", "index"],
|
|
121
|
+
"additionalProperties": false
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"type": "object",
|
|
125
|
+
"properties": {
|
|
126
|
+
"kind": { "const": "face" },
|
|
127
|
+
"side": { "type": "string", "enum": ["min-x", "max-x", "min-y", "max-y"] }
|
|
128
|
+
},
|
|
129
|
+
"required": ["kind", "side"],
|
|
130
|
+
"additionalProperties": false
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"required": ["edge", "ref"],
|
|
136
|
+
"additionalProperties": false
|
|
137
|
+
}
|
|
99
138
|
}
|
|
100
139
|
},
|
|
101
140
|
"required": ["position"],
|