"""Minimal DXF reader: sections/entities plus geometry helpers (polylines with bulges, arcs, hatch boundaries).""" from collections import Counter, defaultdict def pairs(path): with open(path, encoding='utf-8', errors='replace') as f: while True: code = f.readline() if not code: return val = f.readline().rstrip('\r\n') yield int(code.strip()), val def read_sections(path): """Return {'BLOCKS': [entities...], 'ENTITIES': [...]}; each entity = (type, [(code, val), ...]).""" out = defaultdict(list) section = None cur = None it = pairs(path) for code, val in it: if code == 0 and val == 'SECTION': _, name = next(it) section = name cur = None continue if code == 0 and val == 'ENDSEC': if cur: out[section].append(cur) section, cur = None, None continue if section in ('BLOCKS', 'ENTITIES'): if code == 0: if cur: out[section].append(cur) cur = (val, []) elif cur: cur[1].append((code, val)) return out def get(tags, code, default=None): for c, v in tags: if c == code: return v return default import math def _f(v): return float(v) def bulge_points(p1, p2, bulge, seg_len=1.0): """Points along an arc defined by a polyline bulge between p1 and p2 (excluding p1, including p2).""" if abs(bulge) < 1e-9: return [p2] x1, y1 = p1; x2, y2 = p2 theta = 4 * math.atan(bulge) chord = math.hypot(x2 - x1, y2 - y1) if chord < 1e-9: return [p2] r = chord / (2 * math.sin(theta / 2)) # centre mx, my = (x1 + x2) / 2, (y1 + y2) / 2 d = math.sqrt(max(r * r - (chord / 2) ** 2, 0)) ux, uy = (x2 - x1) / chord, (y2 - y1) / chord s = 1 if bulge > 0 else -1 # centre lies to the left for positive bulge when |theta| < pi sign = s if abs(theta) < math.pi else -s cx, cy = mx - sign * d * uy, my + sign * d * ux a1 = math.atan2(y1 - cy, x1 - cx) n = max(2, int(abs(theta) * abs(r) / seg_len)) return [(cx + abs(r) * math.cos(a1 + theta * k / n), cy + abs(r) * math.sin(a1 + theta * k / n)) for k in range(1, n + 1)] def arc_points(cx, cy, r, a0, a1, seg_len=1.0): a0, a1 = math.radians(a0), math.radians(a1) if a1 < a0: a1 += 2 * math.pi n = max(2, int((a1 - a0) * r / seg_len)) return [(cx + r * math.cos(a0 + (a1 - a0) * k / n), cy + r * math.sin(a0 + (a1 - a0) * k / n)) for k in range(n + 1)] def lwpolyline(tags): pts, bulges, closed, elev = [], [], False, 0.0 for c, v in tags: if c == 70: closed = bool(int(v) & 1) elif c == 38: elev = _f(v) elif c == 10: pts.append([_f(v), None]); bulges.append(0.0) elif c == 20 and pts: pts[-1][1] = _f(v) elif c == 42 and pts: bulges[-1] = _f(v) pts = [tuple(p) for p in pts if p[1] is not None] if len(pts) < 2: return None, closed, elev out = [pts[0]] n = len(pts) if closed else len(pts) - 1 for i in range(n): out += bulge_points(pts[i], pts[(i + 1) % len(pts)], bulges[i]) return out, closed, elev def line(tags): d = {} for c, v in tags: if c in (10, 20, 11, 21): d[c] = _f(v) return [(d[10], d[20]), (d[11], d[21])] def arc(tags): d = {} for c, v in tags: if c in (10, 20, 40, 50, 51): d[c] = _f(v) return arc_points(d[10], d[20], d[40], d[50], d[51]) def circle(tags): d = {} for c, v in tags: if c in (10, 20, 40): d[c] = _f(v) return arc_points(d[10], d[20], d[40], 0, 360) def hatch_rings(tags): """Boundary rings of a HATCH (polyline paths, plus line/arc edge paths). Returns list of point lists.""" rings = [] i, n = 0, len(tags) # skip to the boundary-path count (code 91) while i < n and tags[i][0] != 91: i += 1 if i >= n: return rings npaths = int(tags[i][1]); i += 1 for _ in range(npaths): while i < n and tags[i][0] != 92: i += 1 if i >= n: break flags = int(tags[i][1]); i += 1 ring = [] if flags & 2: # polyline path has_bulge = closed = 0; count = 0 while i < n and tags[i][0] in (72, 73, 93): if tags[i][0] == 72: has_bulge = int(tags[i][1]) if tags[i][0] == 93: count = int(tags[i][1]) i += 1 pts, bul = [], [] while i < n and len(pts) <= count and tags[i][0] in (10, 20, 42): c, v = tags[i] if c == 10: pts.append([_f(v), None]); bul.append(0.0) elif c == 20: pts[-1][1] = _f(v) elif c == 42: bul[-1] = _f(v) i += 1 if len(pts) == count and (i >= n or tags[i][0] not in (20, 42)): break pts = [tuple(p) for p in pts] if pts: ring = [pts[0]] for k in range(len(pts)): ring += bulge_points(pts[k], pts[(k + 1) % len(pts)], bul[k] if has_bulge else 0) else: # edge path while i < n and tags[i][0] != 93: i += 1 if i >= n: break nedges = int(tags[i][1]); i += 1 for _e in range(nedges): while i < n and tags[i][0] != 72: i += 1 if i >= n: break et = int(tags[i][1]); i += 1 d = {} while i < n and tags[i][0] not in (72, 97, 92): c, v = tags[i] if c in (10, 20, 11, 21, 40, 50, 51, 73) and c not in d: d[c] = _f(v) i += 1 if et == 1 and 10 in d and 11 in d: ring += [(d[10], d[20]), (d[11], d[21])] elif et == 2 and 40 in d: a0, a1 = d[50], d[51] if d.get(73, 1) == 0: # clockwise arc: DXF stores the angles mirrored (360 - angle) pts = list(reversed(arc_points(d[10], d[20], d[40], 360 - a1, 360 - a0))) else: pts = arc_points(d[10], d[20], d[40], a0, a1) ring += pts if len(ring) >= 3: rings.append(ring) return rings def ring_area(r): a = 0.0 for k in range(len(r)): x1, y1 = r[k][0], r[k][1]; x2, y2 = r[(k + 1) % len(r)][0], r[(k + 1) % len(r)][1] a += x1 * y2 - x2 * y1 return a / 2