Doramong commited on
Commit
b449b07
ยท
verified ยท
1 Parent(s): 589d996

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +172 -67
README.md CHANGED
@@ -5,27 +5,47 @@ from matplotlib.patches import Arc
5
  import numpy as np
6
  import math
7
 
8
- doc = ezdxf.readfile("plan.dxf")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  msp = doc.modelspace()
10
 
11
- fig, ax = plt.subplots(figsize=(8, 8))
12
 
13
  # ---------------------------
14
  # ๊ณตํ†ต ์œ ํ‹ธ
15
  # ---------------------------
16
- def plot_segments(points, closed=False, lw=0.6):
17
- """๋‹จ์ˆœ ์„ ๋ถ„ ์—ฐ๊ฒฐ(์ด๋ฏธ ํ˜ธ๋กœ ๊ทผ์‚ฌ๋œ ์ ์—ด์ด๋ผ๋Š” ๊ฐ€์ •)."""
18
  if len(points) < 2:
19
  return
20
  xs, ys = zip(*points)
21
- ax.plot(xs, ys, linewidth=lw, color="black")
22
  if closed and (points[0] != points[-1]):
23
  ax.plot([points[-1][0], points[0][0]],
24
- [points[-1][1], points[0][1]], linewidth=lw, color="black")
25
 
26
- def plot_lwpolyline(e):
27
- """LWPOLYLINE์˜ bulge๋ฅผ ํฌํ•จํ•ด ํ˜ธ๋ฅผ ๊ทผ์‚ฌํ•˜์—ฌ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค."""
28
- # get_points("xyb") โ†’ (x, y, bulge) ํŠœํ”Œ
29
  pts = list(e.get_points("xyb"))
30
  if not pts:
31
  return
@@ -36,30 +56,20 @@ def plot_lwpolyline(e):
36
  p1 = np.array([x1, y1])
37
  p2 = np.array([x2, y2])
38
  if abs(b1) < 1e-12:
39
- # ์ง์„  ์„ธ๊ทธ๋จผํŠธ
40
  approx += [tuple(p1), tuple(p2)]
41
  else:
42
- # bulge โ†’ ํ˜ธ ๊ทผ์‚ฌ
43
- # bulge = tan(delta/4), delta: ์ค‘์‹ฌ๊ฐ
44
- delta = 4 * math.atan(b1)
45
  chord = p2 - p1
46
  L = np.linalg.norm(chord)
47
  if L < 1e-12:
48
  continue
49
- # ํ˜ธ ๋ฐ˜์ง€๋ฆ„
50
  R = (L/2) / abs(math.sin(delta/2))
51
- # chord ์ค‘์ 
52
  mid = (p1 + p2) / 2
53
- # chord ๋ฒ•์„  ๋‹จ์œ„๋ฒกํ„ฐ
54
  n = np.array([-(chord[1]), chord[0]]) / (L + 1e-12)
55
- # ์ค‘์ ์—์„œ ์› ์ค‘์‹ฌ๊นŒ์ง€ ๊ฑฐ๋ฆฌ
56
  h = R * math.cos(delta/2)
57
- # bulge์˜ ๋ถ€ํ˜ธ๋กœ ์ค‘์‹ฌ ๋ฐฉํ–ฅ ๊ฒฐ์ •
58
  center = mid + np.sign(b1) * h * n
59
- # p1, p2 ๊ฐ๋„
60
  a1 = math.atan2(p1[1]-center[1], p1[0]-center[0])
61
  a2 = math.atan2(p2[1]-center[1], p2[0]-center[0])
62
- # ์ง„ํ–‰ ๋ฐฉํ–ฅ: bulge ๋ถ€ํ˜ธ์— ๋”ฐ๋ผ ์‹œ๊ณ„/๋ฐ˜์‹œ๊ณ„
63
  def angle_range(a_start, a_end, ccw=True, steps=32):
64
  if ccw:
65
  if a_end <= a_start:
@@ -69,19 +79,18 @@ def plot_lwpolyline(e):
69
  if a_end >= a_start:
70
  a_end -= 2*math.pi
71
  return np.linspace(a_start, a_end, steps)
72
- ccw = (b1 > 0) # bulge>0์ด๋ฉด ๋ฐ˜์‹œ๊ณ„
73
  angles = angle_range(a1, a2, ccw=ccw, steps=max(16, int(abs(delta)*16)))
74
  for t in angles:
75
  approx.append((center[0] + R*math.cos(t), center[1] + R*math.sin(t)))
76
- # ์ค‘๋ณต์  ์ •๋ฆฌ
77
  cleaned = []
78
  for p in approx:
79
  if not cleaned or (abs(cleaned[-1][0]-p[0])>1e-9 or abs(cleaned[-1][1]-p[1])>1e-9):
80
  cleaned.append(p)
81
- plot_segments(cleaned, closed=e.closed, lw=0.6)
82
 
83
- def draw_basic_entity(ent):
84
- """INSERT๋กœ ํ’€๋ฆฐ ๊ฐ€์ƒ ์—”ํ‹ฐํ‹ฐ ํฌํ•จ, ๊ฐœ๋ณ„ ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ˜„์žฌ ์ถ•์— ๊ทธ๋ฆผ."""
85
  t = ent.dxftype()
86
 
87
  if t == "LINE":
@@ -90,33 +99,28 @@ def draw_basic_entity(ent):
90
  ax.plot(x, y, linewidth=0.6, color="black")
91
 
92
  elif t == "LWPOLYLINE":
93
- # bulge ์ง€์›
94
- plot_lwpolyline(ent)
95
 
96
  elif t == "POLYLINE":
97
  pts = [(v.dxf.location.x, v.dxf.location.y) for v in ent.vertices]
98
- plot_segments(pts, closed=getattr(ent, "is_closed", False), lw=0.6)
99
 
100
  elif t == "ARC":
101
- c = ent.dxf.center
102
- r = ent.dxf.radius
103
  arc = Arc((c.x, c.y), width=2*r, height=2*r, angle=0,
104
  theta1=ent.dxf.start_angle, theta2=ent.dxf.end_angle,
105
  linewidth=0.6, color="black")
106
  ax.add_patch(arc)
107
 
108
  elif t == "CIRCLE":
109
- c = ent.dxf.center
110
- r = ent.dxf.radius
111
- circle = plt.Circle((c.x, c.y), r, fill=False, linewidth=0.6, color="black")
112
- ax.add_patch(circle)
113
 
114
  elif t == "ELLIPSE":
115
  center = np.array([ent.dxf.center.x, ent.dxf.center.y])
116
- major = np.array([ent.dxf.major_axis.x, ent.dxf.major_axis.y])
117
- ratio = ent.dxf.ratio
118
- t0 = ent.dxf.start_param
119
- t1 = ent.dxf.end_param
120
  u = major
121
  v = np.array([-major[1], major[0]])
122
  v = v / (np.linalg.norm(v) + 1e-12) * (np.linalg.norm(major) * ratio)
@@ -131,8 +135,7 @@ def draw_basic_entity(ent):
131
  ax.plot(xs, ys, linewidth=0.6, color="black")
132
 
133
  elif t == "TEXT":
134
- ins = ent.dxf.insert
135
- text = ent.dxf.text
136
  height = ent.dxf.height if ent.dxf.height else 2.5
137
  rot = ent.dxf.rotation if ent.dxf.hasattr("rotation") else 0.0
138
  ax.text(ins.x, ins.y, text, fontsize=height, rotation=rot,
@@ -141,9 +144,9 @@ def draw_basic_entity(ent):
141
  elif t in ("MTEXT", "ATTRIB"):
142
  ins = ent.dxf.insert
143
  text = ent.plain_text() if t == "MTEXT" else ent.dxf.text
144
- rot = ent.dxf.rotation if ent.dxf.hasattr("rotation") else 0.0
145
- char_height = getattr(ent.dxf, "char_height", None) or getattr(ent.dxf, "height", None) or 2.5
146
- ax.text(ins.x, ins.y, text, fontsize=char_height, rotation=rot,
147
  rotation_mode="anchor", ha="left", va="top", color="black")
148
 
149
  elif t == "HATCH":
@@ -155,20 +158,16 @@ def draw_basic_entity(ent):
155
  if typ == "LineEdge":
156
  pts += [(edge.start[0], edge.start[1]), (edge.end[0], edge.end[1])]
157
  elif typ == "ArcEdge":
158
- cx, cy = edge.center
159
- r = edge.radius
160
- a0 = math.radians(edge.start_angle)
161
- a1 = math.radians(edge.end_angle)
162
  ts = np.linspace(a0, a1, 50)
163
  pts += [(cx + r*np.cos(t), cy + r*np.sin(t)) for t in ts]
164
  elif typ == "EllipseEdge":
165
  (cx, cy) = edge.center
166
- major = np.array(edge.major_axis)
167
- ratio = edge.ratio
168
  t0, t1 = edge.start_param, edge.end_param
169
- u = major
170
- v = np.array([-major[1], major[0]])
171
- v = v / (np.linalg.norm(v) + 1e-12) * (np.linalg.norm(major) * ratio)
172
  ts = np.linspace(t0, t1, 100)
173
  pts += [(cx + u[0]*np.cos(t) + v[0]*np.sin(t),
174
  cy + u[1]*np.cos(t) + v[1]*np.sin(t)) for t in ts]
@@ -176,45 +175,151 @@ def draw_basic_entity(ent):
176
  ap = edge.spline.approximate(segments=100)
177
  pts += [(p[0], p[1]) for p in ap]
178
  if len(pts) >= 2:
179
- xs, ys = zip(*pts)
180
- ax.plot(xs, ys, linewidth=0.4, color="black")
181
  elif path.PATH_TYPE_POLYLINE:
182
  pts = [(v[0], v[1]) for v in path.vertices]
183
- plot_segments(pts, lw=0.4)
184
 
185
  # ---------------------------
186
- # 1) ๋ชจ๋ธ๊ณต๊ฐ„ ๊ธฐ๋ณธ ์—”ํ‹ฐํ‹ฐ
187
  # ---------------------------
188
  for e in msp:
189
- # ๋ธ”๋ก ์ฐธ์กฐ(INSERT)๋Š” ์•„๋ž˜์—์„œ ๋”ฐ๋กœ ์ฒ˜๋ฆฌ
190
  if e.dxftype() == "INSERT":
191
  continue
192
- draw_basic_entity(e)
193
 
194
  # ---------------------------
195
- # 2) ๋ธ”๋ก(INSERT) ์ „๊ฐœ
196
  # ---------------------------
197
  for br in msp.query("INSERT"):
198
- # MINSERT ํฌํ•จํ•ด ๋ฐฐ์—ด/์Šค์ผ€์ผ/ํšŒ์ „/์ด๋™์ด ์ ์šฉ๋œ ๊ฐ€์ƒ ์—”ํ‹ฐํ‹ฐ๋กœ ํ™•์žฅ
199
  try:
200
  for ve in br.virtual_entities():
201
- draw_basic_entity(ve)
202
  except Exception:
203
- # ๋ˆ„๋ฝ๋œ ๋ธ”๋ก ์ •์˜ ๋“ฑ ์˜ค๋ฅ˜๋Š” ๋„˜์–ด๊ฐ
204
  continue
205
 
206
- # (์„ ํƒ) DIMENSION์€ virtual_entities()๋ฅผ ์“ฐ๋ ค๋ฉด ๋ณดํ†ต render() ํ•„์š”
207
  for dim in msp.query("DIMENSION"):
208
  try:
209
- dim.render() # ์น˜์ˆ˜์„ โ†’์„ /ํ…์ŠคํŠธ ๋“ฑ์œผ๋กœ ์‹ค์ฒดํ™” (๋ ˆ๊ฑฐ์‹œ DIM์—๋งŒ ํ•„์š”)
210
  for ve in dim.virtual_entities():
211
- draw_basic_entity(ve)
212
  except Exception:
213
  continue
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  ax.set_aspect("equal")
216
  ax.axis("off")
217
- plt.savefig("output.png", dpi=300)
218
- plt.savefig("output.pdf")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  plt.close()
220
  ```
 
5
  import numpy as np
6
  import math
7
 
8
+ # ============== ์‚ฌ์šฉ์ž ์ž…๋ ฅ ==============
9
+ dxf_path = "plan.dxf"
10
+
11
+ # ํ•˜์ด๋ผ์ดํŠธํ•  ๋…ธ๋“œ ๊ทธ๋ฃน (์›ํ•˜๋Š” ๋งŒํผ ์ถ”๊ฐ€)
12
+ # points: ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ •ํ•œ ์ขŒํ‘œ(๋Œ€๋žต์น˜์—ฌ๋„ ์Šค๋ƒ…๋จ), label์€ ์˜ต์…˜
13
+ selected_groups = {
14
+ "CHECK": {
15
+ "color": "red",
16
+ "points": [(100.0, 200.0), (150.2, 80.7)], # ์˜ˆ์‹œ ์ขŒํ‘œ
17
+ "labels": ["N1", "N2"], # ์—†์œผ๋ฉด ์ž๋™ ๋ฒˆํ˜ธ
18
+ },
19
+ "MONITOR": {
20
+ "color": "orange",
21
+ "points": [(300.0, 210.0)],
22
+ "labels": None,
23
+ },
24
+ }
25
+ snap_tol = 1.0 # ์Šค๋ƒ… ํ—ˆ์šฉ์˜ค์ฐจ(๋„๋ฉด ๋‹จ์œ„). ๋„๋ฉด ์Šค์ผ€์ผ์— ๋งž์ถฐ ์กฐ์ •
26
+ marker_size = 8 # ๋งˆ์ปค ํ”ฝ์…€ ํฌ๊ธฐ
27
+ label_fontsize = 6 # ๋ผ๋ฒจ ํฐํŠธ ํฌ๊ธฐ
28
+
29
+ # ============== DXF ๋กœ๋“œ ==============
30
+ doc = ezdxf.readfile(dxf_path)
31
  msp = doc.modelspace()
32
 
33
+ fig, ax = plt.subplots(figsize=(10, 10))
34
 
35
  # ---------------------------
36
  # ๊ณตํ†ต ์œ ํ‹ธ
37
  # ---------------------------
38
+ def plot_segments(ax, points, closed=False, lw=0.6, color="black"):
 
39
  if len(points) < 2:
40
  return
41
  xs, ys = zip(*points)
42
+ ax.plot(xs, ys, linewidth=lw, color=color)
43
  if closed and (points[0] != points[-1]):
44
  ax.plot([points[-1][0], points[0][0]],
45
+ [points[-1][1], points[0][1]], linewidth=lw, color=color)
46
 
47
+ def plot_lwpolyline(ax, e):
48
+ """LWPOLYLINE์˜ bulge(ํ˜ธ)๋ฅผ ๊ทผ์‚ฌํ•˜์—ฌ ๊ทธ๋ฆผ."""
 
49
  pts = list(e.get_points("xyb"))
50
  if not pts:
51
  return
 
56
  p1 = np.array([x1, y1])
57
  p2 = np.array([x2, y2])
58
  if abs(b1) < 1e-12:
 
59
  approx += [tuple(p1), tuple(p2)]
60
  else:
61
+ delta = 4 * math.atan(b1) # ์ค‘์‹ฌ๊ฐ
 
 
62
  chord = p2 - p1
63
  L = np.linalg.norm(chord)
64
  if L < 1e-12:
65
  continue
 
66
  R = (L/2) / abs(math.sin(delta/2))
 
67
  mid = (p1 + p2) / 2
 
68
  n = np.array([-(chord[1]), chord[0]]) / (L + 1e-12)
 
69
  h = R * math.cos(delta/2)
 
70
  center = mid + np.sign(b1) * h * n
 
71
  a1 = math.atan2(p1[1]-center[1], p1[0]-center[0])
72
  a2 = math.atan2(p2[1]-center[1], p2[0]-center[0])
 
73
  def angle_range(a_start, a_end, ccw=True, steps=32):
74
  if ccw:
75
  if a_end <= a_start:
 
79
  if a_end >= a_start:
80
  a_end -= 2*math.pi
81
  return np.linspace(a_start, a_end, steps)
82
+ ccw = (b1 > 0)
83
  angles = angle_range(a1, a2, ccw=ccw, steps=max(16, int(abs(delta)*16)))
84
  for t in angles:
85
  approx.append((center[0] + R*math.cos(t), center[1] + R*math.sin(t)))
 
86
  cleaned = []
87
  for p in approx:
88
  if not cleaned or (abs(cleaned[-1][0]-p[0])>1e-9 or abs(cleaned[-1][1]-p[1])>1e-9):
89
  cleaned.append(p)
90
+ plot_segments(ax, cleaned, closed=e.closed, lw=0.6)
91
 
92
+ def draw_basic_entity(ax, ent):
93
+ """INSERT ์ „๊ฐœ๋œ ๊ฐ€์ƒ ์—”ํ‹ฐํ‹ฐ ํฌํ•จ, ๊ฐœ๋ณ„ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ทธ๋ฆผ."""
94
  t = ent.dxftype()
95
 
96
  if t == "LINE":
 
99
  ax.plot(x, y, linewidth=0.6, color="black")
100
 
101
  elif t == "LWPOLYLINE":
102
+ plot_lwpolyline(ax, ent)
 
103
 
104
  elif t == "POLYLINE":
105
  pts = [(v.dxf.location.x, v.dxf.location.y) for v in ent.vertices]
106
+ plot_segments(ax, pts, closed=getattr(ent, "is_closed", False), lw=0.6)
107
 
108
  elif t == "ARC":
109
+ c = ent.dxf.center; r = ent.dxf.radius
 
110
  arc = Arc((c.x, c.y), width=2*r, height=2*r, angle=0,
111
  theta1=ent.dxf.start_angle, theta2=ent.dxf.end_angle,
112
  linewidth=0.6, color="black")
113
  ax.add_patch(arc)
114
 
115
  elif t == "CIRCLE":
116
+ c = ent.dxf.center; r = ent.dxf.radius
117
+ ax.add_patch(plt.Circle((c.x, c.y), r, fill=False, linewidth=0.6, color="black"))
 
 
118
 
119
  elif t == "ELLIPSE":
120
  center = np.array([ent.dxf.center.x, ent.dxf.center.y])
121
+ major = np.array([ent.dxf.major_axis.x, ent.dxf.major_axis.y])
122
+ ratio = ent.dxf.ratio
123
+ t0 = ent.dxf.start_param; t1 = ent.dxf.end_param
 
124
  u = major
125
  v = np.array([-major[1], major[0]])
126
  v = v / (np.linalg.norm(v) + 1e-12) * (np.linalg.norm(major) * ratio)
 
135
  ax.plot(xs, ys, linewidth=0.6, color="black")
136
 
137
  elif t == "TEXT":
138
+ ins = ent.dxf.insert; text = ent.dxf.text
 
139
  height = ent.dxf.height if ent.dxf.height else 2.5
140
  rot = ent.dxf.rotation if ent.dxf.hasattr("rotation") else 0.0
141
  ax.text(ins.x, ins.y, text, fontsize=height, rotation=rot,
 
144
  elif t in ("MTEXT", "ATTRIB"):
145
  ins = ent.dxf.insert
146
  text = ent.plain_text() if t == "MTEXT" else ent.dxf.text
147
+ rot = ent.dxf.rotation if ent.dxf.hasattr("rotation") else 0.0
148
+ h = getattr(ent.dxf, "char_height", None) or getattr(ent.dxf, "height", None) or 2.5
149
+ ax.text(ins.x, ins.y, text, fontsize=h, rotation=rot,
150
  rotation_mode="anchor", ha="left", va="top", color="black")
151
 
152
  elif t == "HATCH":
 
158
  if typ == "LineEdge":
159
  pts += [(edge.start[0], edge.start[1]), (edge.end[0], edge.end[1])]
160
  elif typ == "ArcEdge":
161
+ cx, cy = edge.center; r = edge.radius
162
+ a0 = math.radians(edge.start_angle); a1 = math.radians(edge.end_angle)
 
 
163
  ts = np.linspace(a0, a1, 50)
164
  pts += [(cx + r*np.cos(t), cy + r*np.sin(t)) for t in ts]
165
  elif typ == "EllipseEdge":
166
  (cx, cy) = edge.center
167
+ major = np.array(edge.major_axis); ratio = edge.ratio
 
168
  t0, t1 = edge.start_param, edge.end_param
169
+ u = major; v = np.array([-major[1], major[0]])
170
+ v = v / (np.linalg.norm(v)+1e-12) * (np.linalg.norm(major)*ratio)
 
171
  ts = np.linspace(t0, t1, 100)
172
  pts += [(cx + u[0]*np.cos(t) + v[0]*np.sin(t),
173
  cy + u[1]*np.cos(t) + v[1]*np.sin(t)) for t in ts]
 
175
  ap = edge.spline.approximate(segments=100)
176
  pts += [(p[0], p[1]) for p in ap]
177
  if len(pts) >= 2:
178
+ plot_segments(ax, pts, lw=0.4)
 
179
  elif path.PATH_TYPE_POLYLINE:
180
  pts = [(v[0], v[1]) for v in path.vertices]
181
+ plot_segments(ax, pts, lw=0.4)
182
 
183
  # ---------------------------
184
+ # 1) ๊ธฐ๋ณธ ์—”ํ‹ฐํ‹ฐ ๊ทธ๋ฆฌ๊ธฐ(INSERT ์ œ์™ธ)
185
  # ---------------------------
186
  for e in msp:
 
187
  if e.dxftype() == "INSERT":
188
  continue
189
+ draw_basic_entity(ax, e)
190
 
191
  # ---------------------------
192
+ # 2) ๋ธ”๋ก(INSERT) ์ „๊ฐœ + DIMENSION
193
  # ---------------------------
194
  for br in msp.query("INSERT"):
 
195
  try:
196
  for ve in br.virtual_entities():
197
+ draw_basic_entity(ax, ve)
198
  except Exception:
 
199
  continue
200
 
 
201
  for dim in msp.query("DIMENSION"):
202
  try:
203
+ dim.render()
204
  for ve in dim.virtual_entities():
205
+ draw_basic_entity(ax, ve)
206
  except Exception:
207
  continue
208
 
209
+ # ---------------------------
210
+ # 3) ๋…ธ๋“œ ํ›„๋ณด ์ถ”์ถœ (๋์ /๋ฒ„ํ…์Šค ์ค‘์‹ฌ)
211
+ # ---------------------------
212
+ node_candidates = []
213
+
214
+ def add_node(p):
215
+ node_candidates.append((float(p[0]), float(p[1])))
216
+
217
+ # LINE ๋์ 
218
+ for e in msp.query("LINE"):
219
+ add_node((e.dxf.start.x, e.dxf.start.y))
220
+ add_node((e.dxf.end.x, e.dxf.end.y))
221
+
222
+ # LWPOLYLINE: ๋ฒ„ํ…์Šค ์ขŒํ‘œ(ํ˜ธ ์ค‘๊ฐ„์ ๊นŒ์ง€ ๋‹ค ๋„ฃ์œผ๋ฉด ๋„ˆ๋ฌด ๋งŽ์•„์ ธ์„œ ๋ฒ„ํ…์Šค๋งŒ)
223
+ for e in msp.query("LWPOLYLINE"):
224
+ for (x, y, *_) in e.get_points("xyb"):
225
+ add_node((x, y))
226
+
227
+ # POLYLINE: ๋ฒ„ํ…์Šค
228
+ for e in msp.query("POLYLINE"):
229
+ for v in e.vertices:
230
+ add_node((v.dxf.location.x, v.dxf.location.y))
231
+
232
+ # ARC/CIRCLE ์ค‘์‹ฌ์„ ๋…ธ๋“œ๋กœ ์“ฐ๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜ ์ฃผ์„ ํ•ด์ œ
233
+ # for e in msp.query("ARC"):
234
+ # add_node((e.dxf.center.x, e.dxf.center.y))
235
+ # for e in msp.query("CIRCLE"):
236
+ # add_node((e.dxf.center.x, e.dxf.center.y))
237
+
238
+ # ์ค‘๋ณต ์ œ๊ฑฐ(๊ฒฉ์ž ์Šค๋ƒ…)
239
+ def snap_key(p, tol=1e-6):
240
+ return (round(p[0]/tol), round(p[1]/tol))
241
+ uniq = {}
242
+ for p in node_candidates:
243
+ k = snap_key(p, 1e-6)
244
+ if k not in uniq:
245
+ uniq[k] = p
246
+ node_candidates = list(uniq.values())
247
+
248
+ # ---------------------------
249
+ # 4) ์„ ํƒ ๋…ธ๋“œ ์Šค๋ƒ… & ํ•˜์ด๋ผ์ดํŠธ
250
+ # ---------------------------
251
+ def find_nearest_node(pt, candidates, tol):
252
+ """pt์— ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ํ›„๋ณด๋ฅผ ์ฐพ๊ณ , ๊ฑฐ๋ฆฌ๊ฐ€ tol๋ณด๋‹ค ํฌ๋ฉด None."""
253
+ px, py = pt
254
+ best = None; best_d2 = None
255
+ for cx, cy in candidates:
256
+ d2 = (cx - px)**2 + (cy - py)**2
257
+ if (best_d2 is None) or (d2 < best_d2):
258
+ best_d2 = d2; best = (cx, cy)
259
+ if best is None:
260
+ return None
261
+ dist = math.sqrt(best_d2)
262
+ return best if dist <= tol else None
263
+
264
+ def draw_marker(ax, x, y, color="red", size=8, zorder=10):
265
+ # ํ™”๋ฉด ํ”ฝ์…€ ๊ณ ์ • ํฌ๊ธฐ ๋งˆ์ปค(๋„๋ฉด ์ถ•์ฒ™ ๋ฌด๊ด€)
266
+ ax.scatter([x], [y], s=size**2, c=color, marker='o', zorder=zorder, linewidths=0.5, edgecolors="black")
267
+
268
+ # ๊ทธ๋ฃน๋ณ„๋กœ ์Šค๋ƒ…/ํ‘œ์‹œ
269
+ legend_handles = []
270
+ for gname, cfg in selected_groups.items():
271
+ color = cfg.get("color", "red")
272
+ pts = cfg.get("points", [])
273
+ labels= cfg.get("labels", None)
274
+ placed = []
275
+ for i, pt in enumerate(pts):
276
+ snapped = find_nearest_node(pt, node_candidates, snap_tol)
277
+ if snapped is None:
278
+ # ๊ฐ€๊นŒ์šด ๋…ธ๋“œ๊ฐ€ ์—†์œผ๋ฉด ์›๋ž˜ ๋Œ€๋žต ์ขŒํ‘œ์— ๋งˆ์ปค(์ƒ‰ ๋‹ค๋ฅด๊ฒŒ ํ‘œ์‹œ)
279
+ draw_marker(ax, pt[0], pt[1], color="gray", size=marker_size, zorder=12)
280
+ if labels:
281
+ ax.text(pt[0], pt[1], labels[i], fontsize=label_fontsize, color="gray",
282
+ ha="left", va="bottom", zorder=12)
283
+ continue
284
+ x, y = snapped
285
+ draw_marker(ax, x, y, color=color, size=marker_size, zorder=13)
286
+ if labels:
287
+ ax.text(x, y, labels[i], fontsize=label_fontsize, color=color,
288
+ ha="left", va="bottom", zorder=13)
289
+ placed.append((x, y))
290
+ # ๋ฒ”๋ก€์šฉ ๋”๋ฏธ(์„ ํƒ์‚ฌํ•ญ)
291
+ lh = ax.scatter([], [], s=marker_size**2, c=color, marker='o', edgecolors="black", label=gname)
292
+ legend_handles.append(lh)
293
+
294
+ if legend_handles:
295
+ ax.legend(loc="upper right", fontsize=8, frameon=True)
296
+
297
+ # ---------------------------
298
+ # 5) ๋ณด๊ธฐ/์ €์žฅ
299
+ # ---------------------------
300
  ax.set_aspect("equal")
301
  ax.axis("off")
302
+
303
+ # ๋„๋ฉด ์ „์ฒด extents ์ž๋™ ํฌ๋กญ(์—ฌ๋ฐฑ ํฌํ•จ)
304
+ try:
305
+ # ezdxf์˜ extents ๊ณ„์‚ฐ์„ ์“ฐ๊ณ  ์‹ถ๋‹ค๋ฉด:
306
+ # from ezdxf.addons.drawing import layout
307
+ # ext = layout.Layout(msp).bbox() # ๋ฒ„์ „์— ๋”ฐ๋ผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ
308
+ # ์—ฌ๊ธฐ์„œ๋Š” ์‚ฐํฌ๋กœ ์ถ”์ •:
309
+ xs, ys = [], []
310
+ for (x, y) in node_candidates:
311
+ xs.append(x); ys.append(y)
312
+ if xs and ys:
313
+ margin = 0.05 # 5% ์—ฌ๋ฐฑ
314
+ xmin, xmax = min(xs), max(xs)
315
+ ymin, ymax = min(ys), max(ys)
316
+ dx = xmax - xmin; dy = ymax - ymin
317
+ ax.set_xlim(xmin - dx*margin, xmax + dx*margin)
318
+ ax.set_ylim(ymin - dy*margin, ymax + dy*margin)
319
+ except Exception:
320
+ pass
321
+
322
+ plt.savefig("plan_with_nodes.png", dpi=300, bbox_inches="tight", pad_inches=0.02)
323
+ plt.savefig("plan_with_nodes.pdf", bbox_inches="tight", pad_inches=0.02)
324
  plt.close()
325
  ```