MEGAINFRA-WEBAPP/scripts/site-model/dxf_reader.py

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