diff --git a/papers/medial_tire_cuts/experiments/funcD_seed7_tread0.png b/papers/medial_tire_cuts/experiments/funcD_seed7_tread0.png new file mode 100644 index 0000000..d45a850 Binary files /dev/null and b/papers/medial_tire_cuts/experiments/funcD_seed7_tread0.png differ diff --git a/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py b/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py index 883c0f7..f4801b6 100644 --- a/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py +++ b/papers/medial_tire_cuts/experiments/medial_tire_dual_cut_experiment.py @@ -467,6 +467,80 @@ def draw_tire_cuts_png(result, path): 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(): parser = argparse.ArgumentParser( 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("--tire-png", metavar="PATH", 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() rng = random.Random(args.seed) @@ -506,6 +582,9 @@ def main(): if args.tire_png: draw_tire_cuts_png(result, 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__":