Draw tread 0 (the source cap) in the dual-cut experiment
Add draw_cap_png and a --cap-png flag: render tread 0 as a wheel (source hub, link-cycle rim, cap triangles filled, cap cut marked) from the extract_tread roles, since tread 0 is skipped by tire recognition (a wheel has no up teeth). Render funcD seed7's cap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@@ -467,6 +467,80 @@ def draw_tire_cuts_png(result, path):
|
|||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_cap_png(result, path):
|
||||||
|
"""Render tread 0, the source cap: a wheel with the source at the hub, its
|
||||||
|
link cycle as the rim, the cap triangles (down teeth) filled, and the cap
|
||||||
|
cut marked. Tread 0 is skipped by tire recognition (a wheel has no up
|
||||||
|
teeth), so this draws the ``extract_tread`` roles directly."""
|
||||||
|
import math
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
G, source = result["G"], result["source"]
|
||||||
|
faces, emb = triangular_faces(G)
|
||||||
|
levels = nx.single_source_shortest_path_length(G, source)
|
||||||
|
tr = extract_tread(faces, levels, 0)
|
||||||
|
if tr is None:
|
||||||
|
raise ValueError("no tread-0 (cap) faces")
|
||||||
|
link = list(emb.neighbors_cw_order(source))
|
||||||
|
cap_cuts = {c["medial_vertex"] for c in result.get("cap_cuts", [])}
|
||||||
|
|
||||||
|
pos = {source: (0.0, 0.0)}
|
||||||
|
k = len(link)
|
||||||
|
for i, v in enumerate(link):
|
||||||
|
a = math.radians(90 - i * 360.0 / k)
|
||||||
|
pos[v] = (math.cos(a), math.sin(a))
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(6.5, 6.8))
|
||||||
|
for f in tr["tread_faces"]:
|
||||||
|
if all(v in pos for v in f):
|
||||||
|
xy = [pos[v] for v in f]
|
||||||
|
ax.fill([p[0] for p in xy], [p[1] for p in xy],
|
||||||
|
color="#eef3fa", zorder=0)
|
||||||
|
|
||||||
|
def edge(u, v, **kw):
|
||||||
|
ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]], **kw)
|
||||||
|
|
||||||
|
for u, v in tr["annular"]: # spokes (source -> link)
|
||||||
|
edge(u, v, color="0.45", lw=1.0, zorder=1)
|
||||||
|
for u, v in tr["down"]: # link cycle (down-tooth bases)
|
||||||
|
edge(u, v, color="black", lw=1.6, zorder=1)
|
||||||
|
|
||||||
|
ax.plot(*pos[source], "o", ms=11, mfc="#cfe0f3", mec="#3a6ea5", zorder=4)
|
||||||
|
ax.text(*pos[source], str(source), ha="center", va="center", fontsize=9,
|
||||||
|
fontweight="bold", color="#234", zorder=5)
|
||||||
|
for v in link:
|
||||||
|
ax.plot(*pos[v], "o", ms=9, mfc="white", mec="black", zorder=4)
|
||||||
|
x, y = pos[v]
|
||||||
|
ax.text(x * 1.13, y * 1.13, str(v), ha="center", va="center", fontsize=9)
|
||||||
|
|
||||||
|
for u, v in list(tr["annular"]) + list(tr["down"]):
|
||||||
|
mx, my = (pos[u][0] + pos[v][0]) / 2, (pos[u][1] + pos[v][1]) / 2
|
||||||
|
cut = ekey(u, v) in cap_cuts
|
||||||
|
ax.plot(mx, my, "s", ms=5, mfc=("#cc2020" if cut else "#888"),
|
||||||
|
mec="none", zorder=3)
|
||||||
|
if cut:
|
||||||
|
dx, dy = pos[v][0] - pos[u][0], pos[v][1] - pos[u][1]
|
||||||
|
L = math.hypot(dx, dy) or 1.0
|
||||||
|
px, py = -dy / L * 0.13, dx / L * 0.13
|
||||||
|
ax.plot([mx - px, mx + px], [my - py, my + py],
|
||||||
|
color="#cc2020", lw=2.2, zorder=5)
|
||||||
|
ax.text(mx + 0.12, my, "cap cut", color="#cc2020", fontsize=7,
|
||||||
|
va="center")
|
||||||
|
|
||||||
|
ax.set_title(f"tread 0 (source cap) -- source {source}, link {link}\n"
|
||||||
|
f"{len(tr['tread_faces'])} cap triangles; no up teeth (skipped); "
|
||||||
|
f"down teeth = link cycle", fontsize=8)
|
||||||
|
ax.set_aspect("equal")
|
||||||
|
ax.axis("off")
|
||||||
|
ax.set_xlim(-1.4, 1.4)
|
||||||
|
ax.set_ylim(-1.4, 1.4)
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(path, dpi=150)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
@@ -480,6 +554,8 @@ def main():
|
|||||||
parser.add_argument("--png", metavar="PATH", help="render the dual cut to PNG")
|
parser.add_argument("--png", metavar="PATH", help="render the dual cut to PNG")
|
||||||
parser.add_argument("--tire-png", metavar="PATH",
|
parser.add_argument("--tire-png", metavar="PATH",
|
||||||
help="render each full medial tire cut to PNG")
|
help="render each full medial tire cut to PNG")
|
||||||
|
parser.add_argument("--cap-png", metavar="PATH",
|
||||||
|
help="render tread 0 (the source cap) to PNG")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
rng = random.Random(args.seed)
|
rng = random.Random(args.seed)
|
||||||
@@ -506,6 +582,9 @@ def main():
|
|||||||
if args.tire_png:
|
if args.tire_png:
|
||||||
draw_tire_cuts_png(result, args.tire_png)
|
draw_tire_cuts_png(result, args.tire_png)
|
||||||
print(f"wrote {args.tire_png}")
|
print(f"wrote {args.tire_png}")
|
||||||
|
if args.cap_png:
|
||||||
|
draw_cap_png(result, args.cap_png)
|
||||||
|
print(f"wrote {args.cap_png}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user