2026-06-10 20:50:13 +01:00
|
|
|
"""Faithful port of Urb's top-down quad geometry (``Urb::Quad``).
|
|
|
|
|
|
|
|
|
|
A leaf has *no* intrinsic dimensions: its corners are derived by walking up to
|
|
|
|
|
the level root, and — for upper storeys — across to the matching quad on the
|
|
|
|
|
level below (wall-stacking). Every function here mirrors the corresponding Perl
|
|
|
|
|
method so areas computed in Python match ``urb-fitness.pl`` to floating point.
|
|
|
|
|
|
|
|
|
|
Corner ids run anti-clockwise and are offset by the node's ``rotation``. A
|
|
|
|
|
division is a line between a point on edge (0->1), parameter ``division[0]``, and
|
|
|
|
|
a point on edge (3->2), parameter ``division[1]`` — independent params allow a
|
|
|
|
|
skewed (non-perpendicular) cut.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import math
|
|
|
|
|
|
|
|
|
|
from .dom import Node
|
|
|
|
|
|
|
|
|
|
Point = list[float]
|
|
|
|
|
|
2026-06-10 21:49:31 +01:00
|
|
|
# Memoisation of derived coordinates. The pull-based recursion mirrors Urb but,
|
|
|
|
|
# uncached, re-derives ancestor/below corners exponentially with depth. Urb
|
|
|
|
|
# itself caches in the node and clears via Clean_Cache(); we do the same with a
|
|
|
|
|
# module cache keyed by node identity. Callers that mutate divisions (the
|
|
|
|
|
# solver) must call clear_cache(); dom.load() clears it for a fresh tree.
|
|
|
|
|
_cache: dict = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_cache() -> None:
|
|
|
|
|
_cache.clear()
|
|
|
|
|
|
2026-06-10 20:50:13 +01:00
|
|
|
|
|
|
|
|
def _interp(a: Point, b: Point, t: float) -> Point:
|
|
|
|
|
return [a[0] * (1 - t) + b[0] * t, a[1] * (1 - t) + b[1] * t]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def coordinate(n: Node, idx: int) -> Point:
|
|
|
|
|
"""Corner ``idx`` (0..3) of ``n``; mirrors ``Urb::Quad::Coordinate``."""
|
2026-06-10 21:49:31 +01:00
|
|
|
key = (id(n), idx)
|
|
|
|
|
hit = _cache.get(key)
|
|
|
|
|
if hit is not None:
|
|
|
|
|
return hit
|
2026-06-10 20:50:13 +01:00
|
|
|
if n.below is not None: # upper storey inherits geometry from below
|
2026-06-10 21:49:31 +01:00
|
|
|
result = coordinate(n.below, idx)
|
|
|
|
|
else:
|
|
|
|
|
rid = (idx + n.rotation) % 4
|
|
|
|
|
if n.parent is None: # level root: stored, rotation-adjusted corner
|
|
|
|
|
result = list(n.node[rid])
|
|
|
|
|
else:
|
|
|
|
|
p = n.parent
|
|
|
|
|
if n.position == "l":
|
|
|
|
|
result = {0: coordinate(p, 0), 1: coord_a(p), 2: coord_b(p), 3: coordinate(p, 3)}[rid]
|
|
|
|
|
else: # 'r'
|
|
|
|
|
result = {0: coord_a(p), 1: coordinate(p, 1), 2: coordinate(p, 2), 3: coord_b(p)}[rid]
|
|
|
|
|
_cache[key] = result
|
|
|
|
|
return result
|
2026-06-10 20:50:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def coord_a(n: Node) -> Point:
|
|
|
|
|
"""End 'a' of the division line; mirrors ``Urb::Quad::Coordinate_a``."""
|
2026-06-10 21:49:31 +01:00
|
|
|
key = (id(n), "a")
|
|
|
|
|
hit = _cache.get(key)
|
|
|
|
|
if hit is not None:
|
|
|
|
|
return hit
|
2026-06-10 20:50:13 +01:00
|
|
|
if n.below is not None and n.below.divided:
|
2026-06-10 21:49:31 +01:00
|
|
|
result = coord_a(n.below)
|
|
|
|
|
else:
|
|
|
|
|
result = _interp(coordinate(n, 0), coordinate(n, 1), n.division[0])
|
|
|
|
|
_cache[key] = result
|
|
|
|
|
return result
|
2026-06-10 20:50:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def coord_b(n: Node) -> Point:
|
|
|
|
|
"""End 'b' of the division line; mirrors ``Urb::Quad::Coordinate_b``."""
|
2026-06-10 21:49:31 +01:00
|
|
|
key = (id(n), "b")
|
|
|
|
|
hit = _cache.get(key)
|
|
|
|
|
if hit is not None:
|
|
|
|
|
return hit
|
2026-06-10 20:50:13 +01:00
|
|
|
if n.below is not None and n.below.divided:
|
2026-06-10 21:49:31 +01:00
|
|
|
result = coord_b(n.below)
|
|
|
|
|
else:
|
|
|
|
|
result = _interp(coordinate(n, 3), coordinate(n, 2), n.division[1])
|
|
|
|
|
_cache[key] = result
|
|
|
|
|
return result
|
2026-06-10 20:50:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _dist(a: Point, b: Point) -> float:
|
|
|
|
|
return math.hypot(a[0] - b[0], a[1] - b[1])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _triangle_area(a: Point, b: Point, c: Point) -> float:
|
|
|
|
|
# Heron's formula, matching Urb::Math::triangle_area (always >= 0).
|
|
|
|
|
da, db, dc = _dist(b, c), _dist(a, c), _dist(a, b)
|
|
|
|
|
s = (da + db + dc) / 2
|
|
|
|
|
return math.sqrt(max(0.0, s * (s - da) * (s - db) * (s - dc)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def area(n: Node) -> float:
|
|
|
|
|
"""Area of quad ``n``; mirrors ``Urb::Quad::Area`` (two Heron triangles)."""
|
|
|
|
|
c = [coordinate(n, i) for i in range(4)]
|
|
|
|
|
return _triangle_area(c[0], c[1], c[2]) + _triangle_area(c[0], c[2], c[3])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def edge_length(n: Node, idx: int) -> float:
|
|
|
|
|
"""Length of edge from corner ``idx`` to ``idx+1`` (``Urb::Quad::Length``)."""
|
|
|
|
|
return _dist(coordinate(n, idx), coordinate(n, (idx + 1) % 4))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Plot wall inset (Urb::Quad::Coordinate_Offset, used on the root in Urb::Dom).
|
|
|
|
|
# Positive offset moves a corner outward, negative inward. Computed per corner
|
|
|
|
|
# from its two neighbours along the interior-angle bisector; independent of
|
|
|
|
|
# rotation, so it operates directly on the stored corner order.
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
def _corner_offset(prev: Point, b: Point, c: Point, offset: float) -> Point:
|
|
|
|
|
side_a = _dist(b, c)
|
|
|
|
|
side_b = _dist(prev, b)
|
|
|
|
|
side_c = _dist(c, prev)
|
|
|
|
|
cos_t = (side_a**2 + side_b**2 - side_c**2) / (2 * side_a * side_b)
|
|
|
|
|
theta2 = math.acos(max(-1.0, min(1.0, cos_t))) / 2
|
|
|
|
|
angle_new = math.atan2(c[1] - b[1], c[0] - b[0]) + theta2
|
|
|
|
|
scale = offset / math.sin(theta2)
|
|
|
|
|
return [b[0] - math.cos(angle_new) * scale, b[1] - math.sin(angle_new) * scale]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def offset_quad(corners: list[Point], offset: float) -> list[Point]:
|
|
|
|
|
"""Offset a 4-corner plot by ``offset`` (negative = inward)."""
|
|
|
|
|
n = len(corners)
|
|
|
|
|
return [_corner_offset(corners[(k - 1) % n], corners[k], corners[(k + 1) % n], offset)
|
|
|
|
|
for k in range(n)]
|