Files
ykcheungab 267334d543 fix cli redundant images
finger-xynova first design completed
2026-09-28 16:18:17 +08:00

172 lines
5.9 KiB
Python

"""Seam margin as a post-process on generated UV polygons (stdlib only).
Why not remove fragments
------------------------
The first attempt simply dropped every fragment that came within the margin of the seam.
That produces a sawtooth edge, because a fragment can only be kept or dropped whole, so the
band boundary snaps to fragment boundaries and its width varies from zero to a full fragment.
The band is also the one place where the width actually matters: it exists to absorb two-pass
registration error, so an uncontrolled width means an uncontrolled overlap risk.
What this does instead
----------------------
The margin is a set of half-spaces cut into the *surface* a fixed distance back from each seam
edge. Within one triangle the surface-to-UV map is affine, so a world-space half-space maps to
a UV-space half-space exactly:
(p - A) . d >= inset, p = P0 + M (u - U0)
=> c . u >= k, c = M^T d, k = inset - (P0 - A) . d + c . U0
where ``M`` is the triangle's 3x2 Jacobian built from its world and UV edges. Clipping the
already-generated UV polygon against those half-planes therefore gives a straight edge at the
requested physical distance, whatever the fragment size and however distorted the layout is in
UV. Nothing upstream changes, which is what makes it a post-process.
"""
from __future__ import annotations
from typing import Iterable, Sequence
# Below this the UV triangle or the projected constraint is degenerate and is skipped rather
# than producing a garbage half-plane.
_EPS = 1e-18
def _sub(a, b):
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
def _dot(a, b):
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def _cross(a, b):
return (
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
)
def _length(a):
return (_dot(a, a)) ** 0.5
def _normalize(a):
length = _length(a)
if length <= _EPS:
return None
return (a[0] / length, a[1] / length, a[2] / length)
def triangle_gap_planes(
world_triangle: Sequence[Sequence[float]],
uv_triangle: Sequence[Sequence[float]],
seam_edges: Iterable[Sequence[Sequence[float]]],
inset_mm: float,
) -> list[tuple[tuple[float, float], float]]:
"""Half-planes ``(c, k)`` in UV, each imposing ``c . u >= k`` for this triangle.
``seam_edges`` holds the world-space ``(a, b)`` pairs that are seam edges *of this
triangle*. Returns an empty list when the triangle is not on the seam, when its UV
triangle is degenerate, or when a seam edge is parallel to the triangle normal (which
cannot happen for an edge of the triangle itself, but is guarded anyway).
"""
if len(world_triangle) < 3 or len(uv_triangle) < 3:
return []
p0, p1, p2 = (tuple(map(float, p)) for p in world_triangle[:3])
u0, u1, u2 = (tuple(map(float, u)) for u in uv_triangle[:3])
du1 = (u1[0] - u0[0], u1[1] - u0[1])
du2 = (u2[0] - u0[0], u2[1] - u0[1])
det = du1[0] * du2[1] - du1[1] * du2[0]
if abs(det) <= _EPS:
return []
dp1 = _sub(p1, p0)
dp2 = _sub(p2, p0)
normal = _normalize(_cross(dp1, dp2))
if normal is None:
return []
centroid = tuple((p0[k] + p1[k] + p2[k]) / 3.0 for k in range(3))
planes: list[tuple[tuple[float, float], float]] = []
for edge in seam_edges:
a, b = (tuple(map(float, point)) for point in edge[:2])
tangent = _normalize(_sub(b, a))
if tangent is None:
continue
direction = _normalize(_cross(tangent, normal))
if direction is None:
continue
# Orient the cut so the triangle's own interior is the kept side.
if _dot(_sub(centroid, a), direction) < 0.0:
direction = (-direction[0], -direction[1], -direction[2])
w0 = _dot(dp1, direction)
w1 = _dot(dp2, direction)
# c = (J^-1)^T w with J = [[du1.x, du2.x], [du1.y, du2.y]].
cx = (du2[1] * w0 - du1[1] * w1) / det
cy = (-du2[0] * w0 + du1[0] * w1) / det
c = (cx, cy)
if abs(cx) <= _EPS and abs(cy) <= _EPS:
continue
k = inset_mm - _dot(_sub(p0, a), direction) + (cx * u0[0] + cy * u0[1])
planes.append((c, k))
return planes
def _lerp(a, b, t):
return (a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t)
def clip_polygon(
points: Sequence[Sequence[float]],
planes: Iterable[tuple[tuple[float, float], float]],
) -> list[tuple[float, float]]:
"""Sutherland-Hodgman clip of a UV polygon against ``c . u >= k`` half-planes.
The polygon stays convex if it starts convex, which every emitted fragment is (fragments
are triangles), so a single pass per plane is exact.
"""
out = [(float(p[0]), float(p[1])) for p in points]
for c, k in planes:
if len(out) < 3:
return []
cx, cy = c
result: list[tuple[float, float]] = []
previous = out[-1]
d_previous = cx * previous[0] + cy * previous[1] - k
for current in out:
d_current = cx * current[0] + cy * current[1] - k
if d_current >= 0.0:
if d_previous < 0.0:
result.append(_lerp(previous, current, d_previous / (d_previous - d_current)))
result.append(current)
elif d_previous >= 0.0:
result.append(_lerp(previous, current, d_previous / (d_previous - d_current)))
previous, d_previous = current, d_current
out = result
return out if len(out) >= 3 else []
def polygon_area(points: Sequence[Sequence[float]]) -> float:
"""Absolute shoelace area, for discarding slivers."""
def _area(seq):
total = 0.0
count = len(seq)
for index in range(count):
x0, y0 = seq[index]
x1, y1 = seq[(index + 1) % count]
total += x0 * y1 - x1 * y0
return abs(total) * 0.5
return _area(list(points)) if len(points) >= 3 else 0.0
__all__ = [
"clip_polygon",
"polygon_area",
"triangle_gap_planes",
]