``` import ezdxf import matplotlib.pyplot as plt from matplotlib.patches import Arc import numpy as np import math # ============== 사용자 입력 ============== dxf_path = "plan.dxf" # 하이라이트할 노드 그룹 (원하는 만큼 추가) # points: 사용자가 지정한 좌표(대략치여도 스냅됨), label은 옵션 selected_groups = { "CHECK": { "color": "red", "points": [(100.0, 200.0), (150.2, 80.7)], # 예시 좌표 "labels": ["N1", "N2"], # 없으면 자동 번호 }, "MONITOR": { "color": "orange", "points": [(300.0, 210.0)], "labels": None, }, } snap_tol = 1.0 # 스냅 허용오차(도면 단위). 도면 스케일에 맞춰 조정 marker_size = 8 # 마커 픽셀 크기 label_fontsize = 6 # 라벨 폰트 크기 # ============== DXF 로드 ============== doc = ezdxf.readfile(dxf_path) msp = doc.modelspace() fig, ax = plt.subplots(figsize=(10, 10)) # --------------------------- # 공통 유틸 # --------------------------- def plot_segments(ax, points, closed=False, lw=0.6, color="black"): if len(points) < 2: return xs, ys = zip(*points) ax.plot(xs, ys, linewidth=lw, color=color) if closed and (points[0] != points[-1]): ax.plot([points[-1][0], points[0][0]], [points[-1][1], points[0][1]], linewidth=lw, color=color) def plot_lwpolyline(ax, e): """LWPOLYLINE의 bulge(호)를 근사하여 그림.""" pts = list(e.get_points("xyb")) if not pts: return approx = [] for i in range(len(pts) - 1 + int(e.closed)): x1, y1, b1 = pts[i % len(pts)] x2, y2, _ = pts[(i+1) % len(pts)] p1 = np.array([x1, y1]) p2 = np.array([x2, y2]) if abs(b1) < 1e-12: approx += [tuple(p1), tuple(p2)] else: delta = 4 * math.atan(b1) # 중심각 chord = p2 - p1 L = np.linalg.norm(chord) if L < 1e-12: continue R = (L/2) / abs(math.sin(delta/2)) mid = (p1 + p2) / 2 n = np.array([-(chord[1]), chord[0]]) / (L + 1e-12) h = R * math.cos(delta/2) center = mid + np.sign(b1) * h * n a1 = math.atan2(p1[1]-center[1], p1[0]-center[0]) a2 = math.atan2(p2[1]-center[1], p2[0]-center[0]) def angle_range(a_start, a_end, ccw=True, steps=32): if ccw: if a_end <= a_start: a_end += 2*math.pi return np.linspace(a_start, a_end, steps) else: if a_end >= a_start: a_end -= 2*math.pi return np.linspace(a_start, a_end, steps) ccw = (b1 > 0) angles = angle_range(a1, a2, ccw=ccw, steps=max(16, int(abs(delta)*16))) for t in angles: approx.append((center[0] + R*math.cos(t), center[1] + R*math.sin(t))) cleaned = [] for p in approx: if not cleaned or (abs(cleaned[-1][0]-p[0])>1e-9 or abs(cleaned[-1][1]-p[1])>1e-9): cleaned.append(p) plot_segments(ax, cleaned, closed=e.closed, lw=0.6) def draw_basic_entity(ax, ent): """INSERT 전개된 가상 엔티티 포함, 개별 엔티티를 그림.""" t = ent.dxftype() if t == "LINE": x = [ent.dxf.start.x, ent.dxf.end.x] y = [ent.dxf.start.y, ent.dxf.end.y] ax.plot(x, y, linewidth=0.6, color="black") elif t == "LWPOLYLINE": plot_lwpolyline(ax, ent) elif t == "POLYLINE": pts = [(v.dxf.location.x, v.dxf.location.y) for v in ent.vertices] plot_segments(ax, pts, closed=getattr(ent, "is_closed", False), lw=0.6) elif t == "ARC": c = ent.dxf.center; r = ent.dxf.radius arc = Arc((c.x, c.y), width=2*r, height=2*r, angle=0, theta1=ent.dxf.start_angle, theta2=ent.dxf.end_angle, linewidth=0.6, color="black") ax.add_patch(arc) elif t == "CIRCLE": c = ent.dxf.center; r = ent.dxf.radius ax.add_patch(plt.Circle((c.x, c.y), r, fill=False, linewidth=0.6, color="black")) elif t == "ELLIPSE": center = np.array([ent.dxf.center.x, ent.dxf.center.y]) major = np.array([ent.dxf.major_axis.x, ent.dxf.major_axis.y]) ratio = ent.dxf.ratio t0 = ent.dxf.start_param; t1 = ent.dxf.end_param u = major v = np.array([-major[1], major[0]]) v = v / (np.linalg.norm(v) + 1e-12) * (np.linalg.norm(major) * ratio) ts = np.linspace(t0, t1, 200) xs = center[0] + u[0]*np.cos(ts) + v[0]*np.sin(ts) ys = center[1] + u[1]*np.cos(ts) + v[1]*np.sin(ts) ax.plot(xs, ys, linewidth=0.6, color="black") elif t == "SPLINE": pts = ent.approximate(segments=200) xs, ys = zip(*[(p[0], p[1]) for p in pts]) ax.plot(xs, ys, linewidth=0.6, color="black") elif t == "TEXT": ins = ent.dxf.insert; text = ent.dxf.text height = ent.dxf.height if ent.dxf.height else 2.5 rot = ent.dxf.rotation if ent.dxf.hasattr("rotation") else 0.0 ax.text(ins.x, ins.y, text, fontsize=height, rotation=rot, rotation_mode="anchor", ha="left", va="baseline", color="black") elif t in ("MTEXT", "ATTRIB"): ins = ent.dxf.insert text = ent.plain_text() if t == "MTEXT" else ent.dxf.text rot = ent.dxf.rotation if ent.dxf.hasattr("rotation") else 0.0 h = getattr(ent.dxf, "char_height", None) or getattr(ent.dxf, "height", None) or 2.5 ax.text(ins.x, ins.y, text, fontsize=h, rotation=rot, rotation_mode="anchor", ha="left", va="top", color="black") elif t == "HATCH": for path in ent.paths: if path.PATH_TYPE_EDGE: pts = [] for edge in path.edges: typ = edge.EDGE_TYPE if typ == "LineEdge": pts += [(edge.start[0], edge.start[1]), (edge.end[0], edge.end[1])] elif typ == "ArcEdge": cx, cy = edge.center; r = edge.radius a0 = math.radians(edge.start_angle); a1 = math.radians(edge.end_angle) ts = np.linspace(a0, a1, 50) pts += [(cx + r*np.cos(t), cy + r*np.sin(t)) for t in ts] elif typ == "EllipseEdge": (cx, cy) = edge.center major = np.array(edge.major_axis); ratio = edge.ratio t0, t1 = edge.start_param, edge.end_param u = major; v = np.array([-major[1], major[0]]) v = v / (np.linalg.norm(v)+1e-12) * (np.linalg.norm(major)*ratio) ts = np.linspace(t0, t1, 100) pts += [(cx + u[0]*np.cos(t) + v[0]*np.sin(t), cy + u[1]*np.cos(t) + v[1]*np.sin(t)) for t in ts] elif typ == "SplineEdge": ap = edge.spline.approximate(segments=100) pts += [(p[0], p[1]) for p in ap] if len(pts) >= 2: plot_segments(ax, pts, lw=0.4) elif path.PATH_TYPE_POLYLINE: pts = [(v[0], v[1]) for v in path.vertices] plot_segments(ax, pts, lw=0.4) # --------------------------- # 1) 기본 엔티티 그리기(INSERT 제외) # --------------------------- for e in msp: if e.dxftype() == "INSERT": continue draw_basic_entity(ax, e) # --------------------------- # 2) 블록(INSERT) 전개 + DIMENSION # --------------------------- for br in msp.query("INSERT"): try: for ve in br.virtual_entities(): draw_basic_entity(ax, ve) except Exception: continue for dim in msp.query("DIMENSION"): try: dim.render() for ve in dim.virtual_entities(): draw_basic_entity(ax, ve) except Exception: continue # --------------------------- # 3) 노드 후보 추출 (끝점/버텍스 중심) # --------------------------- node_candidates = [] def add_node(p): node_candidates.append((float(p[0]), float(p[1]))) # LINE 끝점 for e in msp.query("LINE"): add_node((e.dxf.start.x, e.dxf.start.y)) add_node((e.dxf.end.x, e.dxf.end.y)) # LWPOLYLINE: 버텍스 좌표(호 중간점까지 다 넣으면 너무 많아져서 버텍스만) for e in msp.query("LWPOLYLINE"): for (x, y, *_) in e.get_points("xyb"): add_node((x, y)) # POLYLINE: 버텍스 for e in msp.query("POLYLINE"): for v in e.vertices: add_node((v.dxf.location.x, v.dxf.location.y)) # ARC/CIRCLE 중심을 노드로 쓰고 싶다면 아래 주석 해제 # for e in msp.query("ARC"): # add_node((e.dxf.center.x, e.dxf.center.y)) # for e in msp.query("CIRCLE"): # add_node((e.dxf.center.x, e.dxf.center.y)) # 중복 제거(격자 스냅) def snap_key(p, tol=1e-6): return (round(p[0]/tol), round(p[1]/tol)) uniq = {} for p in node_candidates: k = snap_key(p, 1e-6) if k not in uniq: uniq[k] = p node_candidates = list(uniq.values()) # --------------------------- # 4) 선택 노드 스냅 & 하이라이트 # --------------------------- def find_nearest_node(pt, candidates, tol): """pt에 가장 가까운 후보를 찾고, 거리가 tol보다 크면 None.""" px, py = pt best = None; best_d2 = None for cx, cy in candidates: d2 = (cx - px)**2 + (cy - py)**2 if (best_d2 is None) or (d2 < best_d2): best_d2 = d2; best = (cx, cy) if best is None: return None dist = math.sqrt(best_d2) return best if dist <= tol else None def draw_marker(ax, x, y, color="red", size=8, zorder=10): # 화면 픽셀 고정 크기 마커(도면 축척 무관) ax.scatter([x], [y], s=size**2, c=color, marker='o', zorder=zorder, linewidths=0.5, edgecolors="black") # 그룹별로 스냅/표시 legend_handles = [] for gname, cfg in selected_groups.items(): color = cfg.get("color", "red") pts = cfg.get("points", []) labels= cfg.get("labels", None) placed = [] for i, pt in enumerate(pts): snapped = find_nearest_node(pt, node_candidates, snap_tol) if snapped is None: # 가까운 노드가 없으면 원래 대략 좌표에 마커(색 다르게 표시) draw_marker(ax, pt[0], pt[1], color="gray", size=marker_size, zorder=12) if labels: ax.text(pt[0], pt[1], labels[i], fontsize=label_fontsize, color="gray", ha="left", va="bottom", zorder=12) continue x, y = snapped draw_marker(ax, x, y, color=color, size=marker_size, zorder=13) if labels: ax.text(x, y, labels[i], fontsize=label_fontsize, color=color, ha="left", va="bottom", zorder=13) placed.append((x, y)) # 범례용 더미(선택사항) lh = ax.scatter([], [], s=marker_size**2, c=color, marker='o', edgecolors="black", label=gname) legend_handles.append(lh) if legend_handles: ax.legend(loc="upper right", fontsize=8, frameon=True) # --------------------------- # 5) 보기/저장 # --------------------------- ax.set_aspect("equal") ax.axis("off") # 도면 전체 extents 자동 크롭(여백 포함) try: # ezdxf의 extents 계산을 쓰고 싶다면: # from ezdxf.addons.drawing import layout # ext = layout.Layout(msp).bbox() # 버전에 따라 다를 수 있음 # 여기서는 산포로 추정: xs, ys = [], [] for (x, y) in node_candidates: xs.append(x); ys.append(y) if xs and ys: margin = 0.05 # 5% 여백 xmin, xmax = min(xs), max(xs) ymin, ymax = min(ys), max(ys) dx = xmax - xmin; dy = ymax - ymin ax.set_xlim(xmin - dx*margin, xmax + dx*margin) ax.set_ylim(ymin - dy*margin, ymax + dy*margin) except Exception: pass plt.savefig("plan_with_nodes.png", dpi=300, bbox_inches="tight", pad_inches=0.02) plt.savefig("plan_with_nodes.pdf", bbox_inches="tight", pad_inches=0.02) plt.close() ```