diff --git a/assets/og.counts.json b/assets/og.counts.json new file mode 100644 index 0000000..08bce42 --- /dev/null +++ b/assets/og.counts.json @@ -0,0 +1 @@ +{"symbols":82,"openingTypes":27} diff --git a/assets/og.png b/assets/og.png index 790f9f0..1a7c32b 100644 Binary files a/assets/og.png and b/assets/og.png differ diff --git a/assets/og.svg b/assets/og.svg index f52384e..afd780b 100644 --- a/assets/og.svg +++ b/assets/og.svg @@ -34,10 +34,13 @@ - + - + diff --git a/scripts/render-og.ts b/scripts/render-og.ts index aebe768..16932ea 100644 --- a/scripts/render-og.ts +++ b/scripts/render-og.ts @@ -14,6 +14,21 @@ export function ogSvg(source = readFileSync("assets/og.svg", "utf8")): string { .replaceAll("{{OPENING_TYPE_COUNT}}", String(OPENING_TYPE_COUNT)); } +// assets/og.png is a committed raster rendered by hand, so it goes stale the +// moment a symbol is added. Recording the counts it was rendered with is what +// lets tests/site.test.ts fail on that drift rather than the card advertising +// a figure the editor no longer has. +export const OG_STAMP_PATH = "assets/og.counts.json"; +export type OgStamp = { symbols: number; openingTypes: number }; + +export function ogStamp(): OgStamp { + return { symbols: SYMBOLS.length, openingTypes: OPENING_TYPE_COUNT }; +} + +export function renderedStamp(): OgStamp { + return JSON.parse(readFileSync(OG_STAMP_PATH, "utf8")) as OgStamp; +} + function run(command: string, args: string[]): void { const result = spawnSync(command, args, { stdio: "inherit" }); if (result.error) throw result.error; @@ -30,6 +45,7 @@ export function renderOg(): void { run("rsvg-convert", ["-w", "1200", "-h", "630", svg, "-o", png]); run("magick", [png, "-strip", "-colors", "64", `PNG8:${optimized}`]); renameSync(optimized, "assets/og.png"); + writeFileSync(OG_STAMP_PATH, JSON.stringify(ogStamp()) + "\n"); } finally { rmSync(work, { recursive: true, force: true }); } diff --git a/tests/site.test.ts b/tests/site.test.ts index 5da7181..ef47ebe 100644 --- a/tests/site.test.ts +++ b/tests/site.test.ts @@ -9,7 +9,7 @@ import { FURNISHING_PRESETS } from "../src/model/furnishing"; import { WINDOW_KINDS, DOOR_KINDS, type Opening, type PlanDoc } from "../src/model/doc"; import { resources, changeLanguage } from "../src/i18n"; import { DOC_PATHS, DOC_IDS as LINK_IDS, docHref, SITE_ORIGIN } from "../src/links"; -import { ogSvg } from "../scripts/render-og"; +import { ogSvg, renderedStamp } from "../scripts/render-og"; let fail = 0; const ck = (n: string, c: boolean, d = "") => { if (!c) { fail++; console.error("FAIL " + n + " " + d); } else console.log("ok " + n); }; @@ -24,6 +24,37 @@ ck("social card uses the current opening count", socialCard.includes(`${WINDOW_KINDS.length + DOOR_KINDS.length} deur- en raamtypen`)); ck("social card has no unresolved count", !socialCard.includes("{{")); +// The card ships as a committed raster; the stamp says what it was rendered from. +const stamp = renderedStamp(); +ck("social card raster is rendered from the current registries", + stamp.symbols === SYMBOL_TYPES.length + && stamp.openingTypes === WINDOW_KINDS.length + DOOR_KINDS.length, + `assets/og.png states ${stamp.symbols} symbols and ${stamp.openingTypes} opening types; ` + + "run npm run assets:og"); + +/* A door mark is the leaf from its hinge plus a swing of the leaf's own radius + ABOUT THAT HINGE. Both candidate centres yield the same endpoints, so the + sweep flag alone decides whether the arc bows away from the hinge or back + into it, the second being the mark drawn backwards. */ +const swings = [...socialCard.matchAll( + /\s*/g)]; +ck("social card draws both door swings", swings.length === 2, `found ${swings.length}`); +for (const [i, m] of swings.entries()) { + const at = (g: number): number => Number(m[g]); + const [hx, hy] = [at(1), at(2)]; // hinge: where the leaf starts + const [x1, y1, r, sweep] = [at(4), at(5), at(6), at(7)]; // open leaf tip, and the arc from it + const [x2, y2] = [x1 + at(8), y1 + at(9)]; // far jamb: the closed leaf + // SVG endpoint -> centre parameterisation, for rx = ry and the small arc. + const [px, py] = [(x1 - x2) / 2, (y1 - y2) / 2]; + const k = Math.sqrt(Math.max((r * r) / (px * px + py * py) - 1, 0)) * (sweep === 1 ? 1 : -1); + const [cx, cy] = [(x1 + x2) / 2 + k * py, (y1 + y2) / 2 - k * px]; + ck(`social card door ${i + 1} swings about its hinge`, + Math.abs(cx - hx) < 0.5 && Math.abs(cy - hy) < 0.5, + `arc is centred on (${cx}, ${cy}), hinge is (${hx}, ${hy})`); + ck(`social card door ${i + 1} swings at the leaf's own length`, + Math.abs(Math.hypot(x1 - hx, y1 - hy) - r) < 0.5); +} + /* ── schema ── */ ck("validates the demo plan", validate(schema, seedDoc()).length === 0, validate(schema, seedDoc()).join(" | "));