sbuffkin_hexmaker/tests/overlayPatterns.test.ts
isaprettycoolguy@protonmail.com f9e292c5f0 feat: faction & region overlay patterns (1.3.0)
Adds per-overlay pattern fills so factions and regions can be visually
distinguished when they overlap (issue #25). Each faction note and each
region note now carries a pattern, scale, and opacity in its
frontmatter, edited via the existing FactionEditor / GeoRegionEditor
modals (dropdown + sliders + live preview swatch).

Pattern set (10 total): solid (the existing flat fill, default),
diagonal stripes ↗ / ↖, crosshatch, polka dots, grid, zigzag,
triangles, scales, checker.

On screen: SVG <pattern> defs are deduped per (key,color,scale) and
referenced from each overlay path's fill. Stroke stays solid color so
blob boundaries remain crisp. Per-overlay opacity replaces the
hardcoded 0.45 group opacity.

PNG export: mirrored via Canvas patterns built from an offscreen tile
per (key,color,scale), cached for the render pass.

Backwards compatible — overlays without the new frontmatter keys fall
back to solid + 0.45 opacity, matching pre-1.3.0 behavior.
2026-06-17 11:53:04 -04:00

133 lines
5 KiB
TypeScript

import { describe, it } from "node:test";
import expect from "expect";
import {
OVERLAY_PATTERN_KEYS,
OVERLAY_PATTERN_LABELS,
DEFAULT_OVERLAY_PATTERN_KEY,
DEFAULT_OVERLAY_PATTERN_SCALE,
DEFAULT_OVERLAY_PATTERN_OPACITY,
MIN_PATTERN_SCALE,
MAX_PATTERN_SCALE,
MIN_PATTERN_OPACITY,
MAX_PATTERN_OPACITY,
isOverlayPatternKey,
normalizeOverlayPatternKey,
normalizeOverlayPatternScale,
normalizeOverlayPatternOpacity,
colorToIdToken,
} from "../src/overlayPatterns";
describe("OVERLAY_PATTERN_LABELS", () => {
it("has a label for every pattern key", () => {
for (const key of OVERLAY_PATTERN_KEYS) {
expect(OVERLAY_PATTERN_LABELS[key]).toBeTruthy();
expect(typeof OVERLAY_PATTERN_LABELS[key]).toBe("string");
}
});
it("includes the starter set (10 patterns)", () => {
expect(OVERLAY_PATTERN_KEYS.length).toBe(10);
expect(OVERLAY_PATTERN_KEYS).toContain("solid");
expect(OVERLAY_PATTERN_KEYS).toContain("stripes-right");
expect(OVERLAY_PATTERN_KEYS).toContain("stripes-left");
expect(OVERLAY_PATTERN_KEYS).toContain("crosshatch");
expect(OVERLAY_PATTERN_KEYS).toContain("polka");
expect(OVERLAY_PATTERN_KEYS).toContain("grid");
expect(OVERLAY_PATTERN_KEYS).toContain("zigzag");
expect(OVERLAY_PATTERN_KEYS).toContain("triangles");
expect(OVERLAY_PATTERN_KEYS).toContain("scales");
expect(OVERLAY_PATTERN_KEYS).toContain("checker");
});
});
describe("isOverlayPatternKey", () => {
it("accepts every key in the list", () => {
for (const key of OVERLAY_PATTERN_KEYS) {
expect(isOverlayPatternKey(key)).toBe(true);
}
});
it("rejects unknown strings, non-strings, null, undefined", () => {
expect(isOverlayPatternKey("not-a-pattern")).toBe(false);
expect(isOverlayPatternKey("")).toBe(false);
expect(isOverlayPatternKey(null)).toBe(false);
expect(isOverlayPatternKey(undefined)).toBe(false);
expect(isOverlayPatternKey(42)).toBe(false);
expect(isOverlayPatternKey({})).toBe(false);
});
});
describe("normalizeOverlayPatternKey", () => {
it("returns valid keys unchanged", () => {
expect(normalizeOverlayPatternKey("polka")).toBe("polka");
expect(normalizeOverlayPatternKey("solid")).toBe("solid");
});
it("falls back to default for invalid input", () => {
expect(normalizeOverlayPatternKey("bogus")).toBe(DEFAULT_OVERLAY_PATTERN_KEY);
expect(normalizeOverlayPatternKey(undefined)).toBe(DEFAULT_OVERLAY_PATTERN_KEY);
expect(normalizeOverlayPatternKey(null)).toBe(DEFAULT_OVERLAY_PATTERN_KEY);
expect(normalizeOverlayPatternKey(123)).toBe(DEFAULT_OVERLAY_PATTERN_KEY);
});
});
describe("normalizeOverlayPatternScale", () => {
it("accepts in-range numbers", () => {
expect(normalizeOverlayPatternScale(16)).toBe(16);
expect(normalizeOverlayPatternScale(MIN_PATTERN_SCALE)).toBe(MIN_PATTERN_SCALE);
expect(normalizeOverlayPatternScale(MAX_PATTERN_SCALE)).toBe(MAX_PATTERN_SCALE);
});
it("clamps out-of-range values", () => {
expect(normalizeOverlayPatternScale(0)).toBe(MIN_PATTERN_SCALE);
expect(normalizeOverlayPatternScale(1000)).toBe(MAX_PATTERN_SCALE);
expect(normalizeOverlayPatternScale(-5)).toBe(MIN_PATTERN_SCALE);
});
it("parses strings", () => {
expect(normalizeOverlayPatternScale("24")).toBe(24);
expect(normalizeOverlayPatternScale("24.5")).toBe(24.5);
});
it("falls back to default for invalid input", () => {
expect(normalizeOverlayPatternScale("nope")).toBe(DEFAULT_OVERLAY_PATTERN_SCALE);
expect(normalizeOverlayPatternScale(undefined)).toBe(DEFAULT_OVERLAY_PATTERN_SCALE);
expect(normalizeOverlayPatternScale(null)).toBe(DEFAULT_OVERLAY_PATTERN_SCALE);
expect(normalizeOverlayPatternScale(NaN)).toBe(DEFAULT_OVERLAY_PATTERN_SCALE);
});
});
describe("normalizeOverlayPatternOpacity", () => {
it("accepts in-range numbers", () => {
expect(normalizeOverlayPatternOpacity(0.5)).toBe(0.5);
expect(normalizeOverlayPatternOpacity(MIN_PATTERN_OPACITY)).toBe(MIN_PATTERN_OPACITY);
expect(normalizeOverlayPatternOpacity(MAX_PATTERN_OPACITY)).toBe(MAX_PATTERN_OPACITY);
});
it("clamps out-of-range values", () => {
expect(normalizeOverlayPatternOpacity(0)).toBe(MIN_PATTERN_OPACITY);
expect(normalizeOverlayPatternOpacity(2)).toBe(MAX_PATTERN_OPACITY);
expect(normalizeOverlayPatternOpacity(-1)).toBe(MIN_PATTERN_OPACITY);
});
it("parses strings", () => {
expect(normalizeOverlayPatternOpacity("0.3")).toBe(0.3);
});
it("falls back to default for invalid input", () => {
expect(normalizeOverlayPatternOpacity("nope")).toBe(DEFAULT_OVERLAY_PATTERN_OPACITY);
expect(normalizeOverlayPatternOpacity(undefined)).toBe(DEFAULT_OVERLAY_PATTERN_OPACITY);
});
});
describe("colorToIdToken", () => {
it("strips non-alphanumeric characters", () => {
expect(colorToIdToken("#ff0099")).toBe("ff0099");
expect(colorToIdToken("rgb(10, 20, 30)")).toBe("rgb102030");
expect(colorToIdToken("red")).toBe("red");
});
it("returns empty string for purely punctuation input", () => {
expect(colorToIdToken("---")).toBe("");
});
});