@@ -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