209 lines
6.7 KiB
Python
209 lines
6.7 KiB
Python
"""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
|