← Files Comic SolARCHIVED FILE
skills/comic-sol/scripts/layouts.py
4.74 KB · Sep 30, 2026 · 23:14 UTC
#!/usr/bin/env python3
"""Immutable deterministic page-layout registry for Comic Sol."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable, Mapping, Sequence
PAGE_WIDTH = 1600
PAGE_HEIGHT = 2400
MARGIN = 64
GUTTER = 32
LAYOUT_VERSION = "1"
Rectangle = tuple[int, int, int, int]
FOUR_GRID_RECTS: tuple[Rectangle, ...] = (
(50, 50, 740, 1135),
(810, 50, 740, 1135),
(50, 1215, 740, 1135),
(810, 1215, 740, 1135),
)
@dataclass(frozen=True)
class LayoutDefinition:
name: str
version: str
rectangles: tuple[Rectangle, ...]
reading_order: tuple[int, ...]
def _legacy_layouts() -> dict[str, tuple[Rectangle, ...]]:
inner_width = PAGE_WIDTH - 2 * MARGIN
inner_height = PAGE_HEIGHT - 2 * MARGIN
half_width = (inner_width - GUTTER) // 2
half_height = (inner_height - GUTTER) // 2
third_height = (inner_height - 2 * GUTTER) // 3
hero_height = 1176
support_height = inner_height - GUTTER - hero_height
return {
"full-page": ((MARGIN, MARGIN, inner_width, inner_height),),
"two-horizontal": (
(MARGIN, MARGIN, inner_width, half_height),
(MARGIN, MARGIN + half_height + GUTTER, inner_width, half_height),
),
"three-horizontal": tuple(
(MARGIN, MARGIN + index * (third_height + GUTTER), inner_width, third_height)
for index in range(3)
),
"hero-top-two-bottom": (
(MARGIN, MARGIN, inner_width, hero_height),
(MARGIN, MARGIN + hero_height + GUTTER, half_width, support_height),
(MARGIN + half_width + GUTTER, MARGIN + hero_height + GUTTER,
half_width, support_height),
),
"two-top-hero-bottom": (
(MARGIN, MARGIN, half_width, support_height),
(MARGIN + half_width + GUTTER, MARGIN, half_width, support_height),
(MARGIN, MARGIN + support_height + GUTTER, inner_width, hero_height),
),
"four-grid": FOUR_GRID_RECTS,
}
def _rectangle(value: object) -> Rectangle:
if isinstance(value, Mapping):
values = tuple(value.get(key) for key in ("x", "y", "width", "height"))
elif isinstance(value, (list, tuple)):
values = tuple(value)
else:
raise ValueError("layout rectangle must be an object or four values")
if (
len(values) != 4
or any(not isinstance(item, int) or isinstance(item, bool) for item in values)
):
raise ValueError("layout rectangle must contain four integers")
return values # type: ignore[return-value]
def _overlap(first: Rectangle, second: Rectangle) -> bool:
ax, ay, aw, ah = first
bx, by, bw, bh = second
return not (
ax + aw <= bx or bx + bw <= ax or ay + ah <= by or by + bh <= ay
)
def validate_custom_layout(
rectangles: Iterable[object], reading_order: Sequence[int]
) -> tuple[Rectangle, ...]:
"""Validate and return canonical rectangles for a custom page layout."""
canonical = tuple(_rectangle(value) for value in rectangles)
if not canonical:
raise ValueError("layout requires at least one rectangle")
for x, y, width, height in canonical:
if width <= 0 or height <= 0:
raise ValueError("layout rectangles require positive dimensions")
if x < 0 or y < 0 or x + width > PAGE_WIDTH or y + height > PAGE_HEIGHT:
raise ValueError("layout rectangles must be contained by the canonical page")
for index, first in enumerate(canonical):
for second in canonical[index + 1:]:
if _overlap(first, second):
raise ValueError("layout rectangles overlap")
expected_order = tuple(range(1, len(canonical) + 1))
if tuple(reading_order) != expected_order:
raise ValueError("layout reading order must be unique and complete")
return canonical
_LAYOUTS = {
name: LayoutDefinition(
name=name,
version=LAYOUT_VERSION,
rectangles=validate_custom_layout(rectangles, tuple(range(1, len(rectangles) + 1))),
reading_order=tuple(range(1, len(rectangles) + 1)),
)
for name, rectangles in _legacy_layouts().items()
}
def get_layout(name: str) -> LayoutDefinition:
"""Return one immutable named layout definition."""
try:
return _LAYOUTS[name]
except KeyError as error:
raise ValueError(f"unknown layout: {name}") from error
def match_layout(rectangles: Iterable[object]) -> str:
"""Return the matching registered layout name, otherwise ``custom``."""
canonical = tuple(_rectangle(value) for value in rectangles)
validate_custom_layout(canonical, tuple(range(1, len(canonical) + 1)))
for name, definition in _LAYOUTS.items():
if definition.rectangles == canonical:
return name
return "custom"
SHA-256: 4c5ff5843d36196aeb5b27facd4cad4a54ed66c85d939531eb75ca97c29e7acd