"""Read the coping top-step outline of D1 (front view) straight from the DXF geometry (2026-10-19, trial of geometry reading). Finds the two end-batter tip lines on layer CS-CONC-MAJR (|dx|~200, |dy| 1300..1900), chains the horizontal / vertical lines between their top ends, converts x to the pier axis (centre of the two tips) and the heights to EL with the 'EL. xx.xxx' label next to the highest step. Reads only. usage: python tools/extract_coping_outline.py [params.json] (params.json: compare with coping.top_steps)""" import json import re import sys import ezdxf dxf, texts = sys.argv[1], sys.argv[2] doc = ezdxf.readfile(dxf) L = [(e.dxf.start.x, e.dxf.start.y, e.dxf.end.x, e.dxf.end.y) for e in doc.modelspace() if e.dxftype() == "LINE" and e.dxf.layer == "CS-CONC-MAJR"] tips = [l for l in L if 150 < abs(l[2] - l[0]) < 250 and 1300 < abs(l[3] - l[1]) < 1900 and l[1] < 25000 and l[1] > 0] tips = sorted({(round(l[0], 1), round(l[1], 1), round(l[2], 1), round(l[3], 1)) for l in tips}) if len(tips) != 2: sys.exit(f"expected 2 tip lines, found {len(tips)}: {tips}") top = lambda l: (l[0], l[1]) if l[1] > l[3] else (l[2], l[3]) # noqa: E731 bot = lambda l: (l[2], l[3]) if l[1] > l[3] else (l[0], l[1]) # noqa: E731 left, right = sorted(tips, key=lambda l: top(l)[0]) cx = (bot(left)[0] + bot(right)[0]) / 2 cur, end = top(left), top(right) hv = [(round(a, 1), round(b, 1), round(c, 1), round(d, 1)) for a, b, c, d in L if (abs(a - c) < 0.05 or abs(b - d) < 0.05)] steps, used = [], set() while abs(cur[0] - end[0]) > 0.5 or abs(cur[1] - end[1]) > 0.5: nxt = None for s in hv: if s in used: continue for p, q in (((s[0], s[1]), (s[2], s[3])), ((s[2], s[3]), (s[0], s[1]))): if abs(p[0] - cur[0]) < 0.5 and abs(p[1] - cur[1]) < 0.5 and q[0] >= cur[0] - 0.5 and (q[1] >= cur[1] - 0.5 or abs(q[0] - cur[0]) > 0.5): nxt = (s, q) break if nxt: break if not nxt: sys.exit(f"chain broken at {cur}") used.add(nxt[0]) if abs(nxt[1][1] - cur[1]) < 0.05: steps.append([cur[0], nxt[1][0], cur[1]]) cur = nxt[1] y_hi = max(s[2] for s in steps) els = [(t["text"], t["x"], t["y"]) for t in json.load(open(texts, encoding="utf-8")) if t["dwg"] == "D1" and t["text"].startswith("EL.") and abs(t["x"] - end[0]) < 1500 and 0 < t["y"] - y_hi < 1200] if len(els) != 1: sys.exit(f"need exactly one EL label next to the highest step, found {els}") el_hi = float(re.search(r"\d+\.\d+", els[0][0]).group()) * 1000 res = [{"x0": round(a - cx), "x1": round(b - cx), "z": round(el_hi - (y_hi - y))} for a, b, y in steps] print("axis x =", round(cx, 1), "| EL label", els[0][0]) for r in res: print(r) if len(sys.argv) > 3: ref = json.load(open(sys.argv[3], encoding="utf-8"))["coping"]["top_steps"] d = max(max(abs(a[k] - b[k]) for k in ("x0", "x1", "z")) for a, b in zip(ref, res)) if len(ref) == len(res) else None print("max difference from params coping.top_steps (mm):", d) # ---- other coping dimensions from the same geometry other = {"length_bottom_tip_to_tip": round(bot(right)[0] - bot(left)[0]), "tip_bottom_rise": round(top(left)[1] - bot(left)[1]), "end_batter_top_inset": round(abs(top(left)[0] - bot(left)[0]))} arcs = {round(e.dxf.radius) for e in doc.modelspace() if e.dxftype() == "ARC" and e.dxf.layer == "CS-CONC-MAJR" and e.dxf.radius > 3000} other["soffit_radius"] = sorted(arcs) flat = [(a, b, c_, d) for a, b, c_, d in hv if abs(b - d) < 0.05 and 1800 < abs(c_ - a) < 2600 and abs((a + c_) / 2 - cx) < 600 and bot(left)[1] - 1700 < b < y_hi] flat = sorted({(round(f[0], 1), round(f[1], 1), round(f[2], 1)) for f in flat if f[1] < bot(left)[1] + 100}) if flat: f = flat[0] other["flat_bottom_half_width"] = round(abs(f[2] - f[0]) / 2) other["z_bottom_flat"] = round(el_hi - (y_hi - f[1])) other["_flat_candidates"] = len(flat) sect = sorted({round(abs(c_ - a), 1) for a, b, c_, d in hv if abs(b - d) < 0.05 and 3000 <= abs(c_ - a) <= 3200 and a > 20000}) other["width_y_candidates"] = sect print("other:", other) if len(sys.argv) > 3: print("params:", {k: ref_ for k, ref_ in json.load(open(sys.argv[3], encoding="utf-8"))["coping"].items() if k in other})