Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/og.counts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"symbols":82,"openingTypes":27}
Binary file modified assets/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions assets/og.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions scripts/render-og.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 });
}
Expand Down
33 changes: 32 additions & 1 deletion tests/site.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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); };
Expand All @@ -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(
/<path d="M(\d+) (\d+)[HV](\d+)"\/>\s*<path d="M(\d+) (\d+)a(\d+) \d+ 0 0 ([01]) (-?\d+) ?(-?\d+)"\/>/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(" | "));
Expand Down
Loading