Skip to content

Commit 3aa2962

Browse files
committed
Working mvp
1 parent 3b326a4 commit 3aa2962

File tree

1 file changed

+181
-84
lines changed

1 file changed

+181
-84
lines changed

mpldxf/backend_dxf.py

Lines changed: 181 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ def __init__(self, width, height, dpi, dxfversion, use_ngi_layers=False):
9595
self.use_ngi_layers = use_ngi_layers
9696
self._init_drawing()
9797
self._groupd = []
98+
self._method_context = False
99+
self._axes_patch_count = {} # Track patches per axes
100+
self._current_axes_id = None
101+
self._axes_counter = 0 # Global axes counter
102+
self._pending_patch_analysis = None # For position-based analysis
98103

99104
def _init_drawing(self):
100105
"""Create a drawing, set some global information and add the layers we need."""
@@ -141,127 +146,202 @@ def _determine_element_layer(self):
141146
context_str = " ".join(self._groupd).lower()
142147
current_element = self._groupd[-1].lower()
143148

144-
# Method icons and symbols (from add_symbol function) - HIGH PRIORITY
145-
# Look for offsetbox/drawingarea/anchored anywhere in the context
146-
if any(
147-
keyword in context_str
148-
for keyword in ["offsetbox", "drawingarea", "anchored"]
149-
):
150-
return "FM-Method"
151-
152-
# Collections that are NOT in axes context = method icons
153-
if current_element == "collection":
154-
# If it's not an axes-level collection, it's likely a method icon
155-
if "axes" not in context_str:
156-
return "FM-Method"
149+
print(f"DETERMINE LAYER: Element='{current_element}', Context='{context_str}'")
157150

158-
# Patches - method icons vs frame elements
159-
elif current_element == "patch":
160-
# Method icons (from add_symbol)
161-
if any(
162-
keyword in context_str
163-
for keyword in ["offsetbox", "drawingarea", "anchored"]
164-
):
165-
return "FM-Method"
166-
# Axes background patches
167-
elif "axes" in context_str and len(self._groupd) <= 2:
168-
return "FM-Frame"
169-
# Other patches could be method icons if not clearly frame-related
170-
elif not any(
171-
keyword in context_str for keyword in ["axes", "spine", "grid", "tick"]
172-
):
173-
return "FM-Method"
174-
else:
175-
return "FM-Frame"
151+
# Patches - defer to size analysis
152+
if current_element == "patch":
153+
return "PENDING" # Will be resolved in _draw_mpl_patch
176154

177-
# Line2D elements - need to distinguish data vs frame
155+
# Line2D elements - distinguish data vs frame
178156
elif current_element == "line2d":
179-
# Frame elements: spines, grids, ticks
180-
if any(keyword in context_str for keyword in ["spine", "grid", "tick"]):
157+
# Frame elements: ticks and axis lines
158+
if any(keyword in context_str for keyword in ["tick", "matplotlib.axis"]):
159+
print(f" -> FM-Frame (tick/axis line)")
181160
return "FM-Frame"
182-
# Axes-level lines that are NOT spines/grids = DATA LINES
161+
# Data lines in axes context
183162
elif "axes" in context_str:
184-
# Check if it's really frame-related
185-
if any(keyword in context_str for keyword in ["xaxis", "yaxis"]):
186-
return "FM-Frame"
187-
else:
188-
return "FM-Graph" # This should be your data lines
163+
print(f" -> FM-Graph (data line)")
164+
return "FM-Graph"
189165
else:
166+
print(f" -> FM-Graph (other line)")
190167
return "FM-Graph"
191168

169+
# Collections - these are often method symbols
170+
elif current_element == "collection":
171+
print(f" -> FM-Method (collection)")
172+
return "FM-Method"
173+
174+
# Text elements
175+
elif current_element == "text":
176+
return "FM-Text" # Will be overridden in text layer logic
177+
192178
# Specific frame elements
193-
elif any(
194-
keyword in context_str for keyword in ["axes", "spine", "grid", "tick"]
195-
):
179+
elif any(keyword in context_str for keyword in ["tick", "matplotlib.axis"]):
180+
print(f" -> FM-Frame (frame element)")
196181
return "FM-Frame"
197182

183+
print(f" -> 0 (default)")
198184
return "0"
199185

200-
def _determine_text_layer(self, text_content):
186+
def _analyze_patch_size(self, vertices):
187+
"""Simple shape-based classification of patches"""
188+
if vertices is None or len(vertices) == 0:
189+
return "FM-Frame"
190+
191+
# Convert to numpy array
192+
verts = np.array(vertices)
193+
194+
# Calculate bounding box
195+
min_x, min_y = np.min(verts, axis=0)
196+
max_x, max_y = np.max(verts, axis=0)
197+
198+
# Calculate dimensions
199+
width = max_x - min_x
200+
height = max_y - min_y
201+
202+
print(f" -> Patch size: {width:.1f}x{height:.1f}")
203+
204+
# Avoid division by zero
205+
if height == 0 or width == 0:
206+
print(f" -> FM-Frame (zero dimension)")
207+
return "FM-Frame"
208+
209+
# Calculate aspect ratio
210+
aspect_ratio = max(width, height) / min(width, height)
211+
212+
print(f" -> Aspect ratio: {aspect_ratio:.2f}")
213+
214+
# Shape-based classification:
215+
# - Very thin/long elements (spines, gridlines) -> Frame
216+
# - Square-ish elements (method icons) -> Method
217+
# - Large backgrounds -> Frame
218+
219+
if aspect_ratio > 10: # Very long/thin = spines, gridlines, borders
220+
print(f" -> FM-Frame (thin line/border)")
221+
return "FM-Frame"
222+
elif (
223+
aspect_ratio < 3 and width < 100 and height < 100
224+
): # Roughly square and small = method icon
225+
print(f" -> FM-Method (square small patch - likely icon)")
226+
return "FM-Method"
227+
else: # Everything else = frame elements
228+
print(f" -> FM-Frame (frame element)")
229+
return "FM-Frame"
230+
231+
def open_group(self, s, gid=None):
232+
"""Open a grouping element with label *s*."""
233+
self._groupd.append(s)
234+
print(f"OPEN GROUP: {s}, Full context: {self._groupd}")
235+
236+
# Track axes changes with a unique counter
237+
if s == "axes":
238+
self._axes_counter += 1
239+
self._current_axes_id = self._axes_counter
240+
if self._current_axes_id not in self._axes_patch_count:
241+
self._axes_patch_count[self._current_axes_id] = 0
242+
print(f" -> New axes #{self._current_axes_id}")
243+
244+
# Check if we're entering a method context
245+
if s.lower() in ["offsetbox", "drawingarea"]:
246+
self._method_context = True
247+
print(f" -> METHOD CONTEXT ACTIVATED")
248+
249+
def close_group(self, s):
250+
"""Close a grouping element with label *s*."""
251+
print(f"CLOSE GROUP: {s}, Context before: {self._groupd}")
252+
if self._groupd and self._groupd[-1] == s:
253+
self._groupd.pop()
254+
255+
# Check if we're exiting a method context
256+
if s.lower() in ["offsetbox", "drawingarea"]: # Remove "anchored"
257+
self._method_context = False
258+
print(f" -> METHOD CONTEXT DEACTIVATED")
259+
260+
def _determine_text_layer(self, text_content, fontsize):
201261
"""Determine text layer based on matplotlib context and content"""
202262
if not self.use_ngi_layers:
203263
return "0"
204264

205265
context_str = " ".join(self._groupd).lower() if self._groupd else ""
206266

207-
# Method text (from add_symbol function) - HIGH PRIORITY
208-
if any(
209-
keyword in context_str
210-
for keyword in ["offsetbox", "drawingarea", "anchored"]
211-
):
212-
return "FM-Method"
267+
print(f"DETERMINE TEXT LAYER: Text='{text_content}', Context='{context_str}'")
213268

214269
# Y-axis elements -> Depth
215270
if any(keyword in context_str for keyword in ["yaxis", "ytick"]):
271+
print(f" -> FM-Depth (y-axis)")
216272
return "FM-Depth"
217273

218274
# X-axis elements -> Value
219275
if any(keyword in context_str for keyword in ["xaxis", "xtick"]):
276+
print(f" -> FM-Value (x-axis)")
220277
return "FM-Value"
221278

222-
# Title elements -> Location (often contains location info)
279+
# Title elements and large text -> Location
223280
if "title" in context_str:
224-
return "FM-Location"
281+
if fontsize > 8:
282+
print(f" -> FM-Location (title)")
283+
return "FM-Location"
284+
else:
285+
print(f" -> FM-Method (title small)")
286+
return "FM-Method"
287+
288+
# Text in general axes context - check position and content
289+
if "axes" in context_str:
290+
# Check if it's a large title-like text at top of plot
291+
# This is often location text even if it's just a number
292+
if len(self._groupd) == 3: # ['figure', 'axes', 'text']
293+
if fontsize > 8:
294+
print(f" -> FM-Location (axes title text)")
295+
return "FM-Location"
296+
else:
297+
print(f" -> FM-Method (axes title text small)")
298+
return "FM-Method"
225299

226300
# Legend elements -> Method
227301
if "legend" in context_str:
302+
print(f" -> FM-Method (legend)")
228303
return "FM-Method"
229304

230305
# Axis labels -> Text
231306
if any(keyword in context_str for keyword in ["xlabel", "ylabel"]):
307+
print(f" -> FM-Text (axis labels)")
232308
return "FM-Text"
233309

234-
# Annotations -> Location
235-
if "annotation" in context_str:
236-
return "FM-Location"
237-
238-
# Content-based classification as fallback
310+
# Content-based classification
239311
text_lower = text_content.lower()
240312

241313
# Location text patterns
242314
if any(
243315
keyword in text_lower
244316
for keyword in ["boring", "bh-", "hole", "site", "location"]
245317
):
246-
return "FM-Location"
318+
if fontsize > 8:
319+
print(f" -> FM-Location (location pattern)")
320+
return "FM-Location"
321+
else:
322+
print(f" -> FM-Method (location pattern small)")
323+
return "FM-Method"
247324

248325
# Method text patterns
249326
if any(
250327
keyword in text_lower
251328
for keyword in ["cpt", "spt", "pmt", "dmt", "method", "test"]
252329
):
330+
print(f" -> FM-Method (method pattern)")
253331
return "FM-Method"
254332

255-
# Numeric patterns might be values or depths
333+
# Numeric patterns
256334
import re
257335

258336
if re.match(r"^\s*[-+]?\d*\.?\d+\s*$", text_content):
259-
# Pure numbers - could be axis values, let context decide
260-
if "y" in context_str:
337+
if "y" in context_str or "ytick" in context_str:
338+
print(f" -> FM-Depth (numeric y)")
261339
return "FM-Depth"
262-
elif "x" in context_str:
340+
elif "x" in context_str or "xtick" in context_str:
341+
print(f" -> FM-Value (numeric x)")
263342
return "FM-Value"
264343

344+
print(f" -> FM-Text (default)")
265345
return "FM-Text"
266346

267347
def _get_polyline_attribs(self, gc):
@@ -321,8 +401,10 @@ def _clip_mpl(self, gc, vertices, obj):
321401

322402
return vertices
323403

324-
def _draw_mpl_lwpoly(self, gc, path, transform, obj):
325-
dxfattribs = self._get_polyline_attribs(gc)
404+
def _draw_mpl_lwpoly(self, gc, path, transform, obj, dxfattribs=None):
405+
if dxfattribs is None:
406+
dxfattribs = self._get_polyline_attribs(gc)
407+
326408
vertices = path.transformed(transform).vertices
327409

328410
if len(vertices) > 0:
@@ -355,9 +437,30 @@ def _draw_mpl_line2d(self, gc, path, transform):
355437

356438
def _draw_mpl_patch(self, gc, path, transform, rgbFace=None):
357439
"""Draw a matplotlib patch object"""
358-
dxfattribs = self._get_polyline_attribs(gc)
359440

360-
poly = self._draw_mpl_lwpoly(gc, path, transform, obj="patch")
441+
# Get vertices for size analysis
442+
vertices = path.transformed(transform).vertices
443+
444+
# Determine layer
445+
layer_name = self._determine_element_layer()
446+
447+
if layer_name == "PENDING":
448+
# Use simple size-based analysis
449+
layer_name = self._analyze_patch_size(vertices)
450+
print(f" -> Final layer: {layer_name}")
451+
452+
# Set up DXF attributes
453+
dxfattribs = {}
454+
if self.use_ngi_layers:
455+
dxfattribs["layer"] = layer_name
456+
dxfattribs["color"] = 256 # ByLayer color
457+
else:
458+
dxfattribs["color"] = rgb_to_dxf(gc.get_rgb())
459+
460+
# Draw the polygon outline
461+
poly = self._draw_mpl_lwpoly(
462+
gc, path, transform, obj="patch", dxfattribs=dxfattribs
463+
)
361464
if not poly:
362465
return
363466

@@ -472,7 +575,13 @@ def draw_path_collection(
472575
urls,
473576
offset_position,
474577
):
475-
# Path collections are often method icons
578+
"""Path collections might be method icons - force to method layer"""
579+
print(f"DRAW PATH COLLECTION: Context: {self._groupd}")
580+
581+
# Force path collections to method layer (these are often scatter plots/symbols)
582+
original_groupd = self._groupd.copy()
583+
self._groupd.append("method_collection") # Add marker
584+
476585
for path in paths:
477586
combined_transform = master_transform
478587
if facecolors.size:
@@ -481,6 +590,9 @@ def draw_path_collection(
481590
rgbFace = None
482591
self._draw_mpl_patch(gc, path, combined_transform, rgbFace=rgbFace)
483592

593+
# Restore original context
594+
self._groupd = original_groupd
595+
484596
def draw_path(self, gc, path, transform, rgbFace=None):
485597
"""Draw a Path instance using the given affine transform."""
486598
if len(self._groupd) > 0:
@@ -514,14 +626,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
514626

515627
dxfattribs = {}
516628
if self.use_ngi_layers:
517-
layer_name = self._determine_text_layer(s)
629+
layer_name = self._determine_text_layer(s, fontsize)
518630
dxfattribs["layer"] = layer_name
519-
520-
# Special handling for location text - use white color for search purposes
521-
if layer_name == "FM-Location" and fontsize > 8:
522-
dxfattribs["color"] = 255 # White for location search
523-
else:
524-
dxfattribs["color"] = 256 # ByLayer color
631+
dxfattribs["color"] = 256 # ByLayer color
525632
else:
526633
dxfattribs["color"] = rgb_to_dxf(gc.get_rgb())
527634

@@ -600,16 +707,6 @@ def _map_align(self, align, vert=False):
600707
align = "MIDDLE"
601708
return align
602709

603-
# Required matplotlib methods
604-
def open_group(self, s, gid=None):
605-
"""Open a grouping element with label *s*."""
606-
self._groupd.append(s)
607-
608-
def close_group(self, s):
609-
"""Close a grouping element with label *s*."""
610-
if self._groupd and self._groupd[-1] == s:
611-
self._groupd.pop()
612-
613710
def flipy(self):
614711
return False
615712

0 commit comments

Comments
 (0)