mirror of
https://github.com/sbuffkin/hexmaker.git
synced 2026-07-22 14:30:24 +00:00
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.
133 lines
5 KiB
TypeScript
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("");
|
|
});
|
|
});
|