import logging import re import numpy as np import math from typing import Dict, Any, Tuple, Optional # Group inheritance behavior: # - Group prefixes always override individual element prefixes # - Group suffixes always override individual element suffixes # - This ensures consistent behavior for all elements within a group # Import necessary functions/modules from . import svg_math from . import svg_utils from . import json_builder from . import element_mapper from .geometry_extractor import GeometryExtractor, get_element_geometry, DEFAULT_GEOMETRY from .prefix_resolver import PrefixResolver from .position_utils import extract_position_from_attributes logger = logging.getLogger(__name__) def process_element(element, element_count, svg_type, svg_dimensions, custom_options, debug=False, group_prefix=None, group_suffix=None, center_mode='bbox') -> Dict[str, Any]: """Process a single SVG element and return its JSON representation. Args: element: The SVG element element_count: A counter for this element type svg_type: The tag name (e.g., 'rect') svg_dimensions: (width, height) of the parent SVG custom_options: Configuration options including mappings debug: Enable debug logging group_prefix: Prefix inherited from a parent group group_suffix: Suffix inherited from a parent group center_mode: How to calculate the center for paths ('bbox' or 'centroid') Returns: dict: The JSON representation of the element, or default on error """ try: debug_buffer = [] if debug: logger.debug(f"Processing {svg_type} #{element_count} (Group context: prefix='{group_prefix}', suffix='{group_suffix}')") debug_buffer.append(f"Processing {svg_type} #{element_count}") # --- Get element name and identifiers --- element_id = element.getAttribute('id') or "" element_label = element.getAttribute('inkscape:label') or element_id element_name = element_label or f"{svg_type}{element_count}" original_name = element_name # --- Mapping Logic --- element_mappings = custom_options.get('element_mappings', []) element_candidate_prefix = PrefixResolver.get_element_prefix(element_label) if debug and element_candidate_prefix: logger.debug(f"Element '{element_label}' has candidate prefix: '{element_candidate_prefix}'") # Determine which prefix to use for mapping lookup prefix_to_find, prefix_source = PrefixResolver.determine_prefix_to_use( element_candidate_prefix, group_prefix, element_mappings, svg_type ) if debug: if prefix_source == "element": logger.debug(f"Using element's own prefix '{prefix_to_find}' (no group prefix override)") elif prefix_source == "group": logger.debug(f"Group prefix '{prefix_to_find}' overrides element prefix '{element_candidate_prefix}'") else: logger.debug(f"No valid prefix found, will look for fallback mapping") # Find the appropriate mapping mapping_to_use, match_type = element_mapper.find_mapping_for_element( svg_type, prefix_to_find, element_mappings, debug ) # Determine label prefix for JSON meta label_prefix = "" has_prefix_mapping = False if prefix_source == "element" and mapping_to_use: label_prefix = mapping_to_use.get('label_prefix', '') has_prefix_mapping = True elif prefix_source == "group" and mapping_to_use: label_prefix = group_prefix has_prefix_mapping = True if debug: logger.debug(f"Element '{element_label}' inherited mapping via group prefix '{prefix_to_find}'.") if debug: logger.debug(f"Mapping result: type='{match_type}', mapping={mapping_to_use}") # --- Geometry and Transformation --- geometry = get_element_geometry(element, svg_type, center_mode) orig_center_x = geometry['center_x'] orig_center_y = geometry['center_y'] geom_width = geometry['width'] geom_height = geometry['height'] logger.debug(f"Geometric Center=({orig_center_x:.2f}, {orig_center_y:.2f}), Size=({geom_width:.2f}x{geom_height:.2f})") # Get transformation matrix and apply it transform_matrix = svg_math.get_combined_transform_matrix(element) logger.debug(f"Transform matrix:\n{transform_matrix}") transformed_center_x, transformed_center_y = svg_math.apply_transform( (orig_center_x, orig_center_y), transform_matrix ) logger.debug(f"Transformed Center=({transformed_center_x:.2f}, {transformed_center_y:.2f})") # Extract rotation angle rotation_angle = svg_math.extract_rotation_from_transform(element) # --- Determine Final Size --- # Start with mapped values, then fall back to geometric size element_width = mapping_to_use.get('width') if mapping_to_use else None element_height = mapping_to_use.get('height') if mapping_to_use else None if element_width is not None or element_height is not None: logger.debug(f"Using dimensions from {match_type} mapping: W={element_width}, H={element_height}") # Use geometric size if not specified in mapping if element_width is None: element_width = geom_width if debug: logger.debug(f"Using geometric width as fallback: {element_width:.2f}") if element_height is None: element_height = geom_height if debug: logger.debug(f"Using geometric height as fallback: {element_height:.2f}") # Ensure final dimensions are non-zero element_width = max(element_width, 1.0) element_height = max(element_height, 1.0) logger.debug(f"Final dimensions: {element_width:.2f}x{element_height:.2f}") # --- Calculate Final Position --- # Calculate top-left from transformed center and final size final_x = transformed_center_x - element_width / 2 final_y = transformed_center_y - element_height / 2 logger.debug(f"Calculated initial centered position: ({final_x:.2f}, {final_y:.2f})") # Check for suspicious default position (-10,-10) if abs(final_x + 10.0) < 1e-6 and abs(final_y + 10.0) < 1e-6: logger.warning(f"Position is suspiciously close to (-10,-10) for {element_name}.") logger.debug(f"SVG type={svg_type}, element_id={element_id}") logger.debug(f"Original geometry: {geometry}") logger.debug(f"Transform attribute: {element.getAttribute('transform')}") if abs(final_x + 10.0) < 1e-9 and abs(final_y + 10.0) < 1e-9: # Try to use the original center coordinates if they're valid if abs(orig_center_x) > 1e-6 or abs(orig_center_y) > 1e-6: logger.info(f"Overriding suspicious position with original center") final_x = orig_center_x - element_width / 2 final_y = orig_center_y - element_height / 2 else: # Try to extract position from element attributes extracted_x, extracted_y = extract_position_from_attributes(element, svg_type) if extracted_x is not None and extracted_y is not None: logger.info(f"Overriding suspicious position with attribute-based position") final_x = extracted_x final_y = extracted_y # Apply offsets from mapping x_offset = mapping_to_use.get('x_offset', 0) if mapping_to_use else 0 y_offset = mapping_to_use.get('y_offset', 0) if mapping_to_use else 0 if x_offset != 0 or y_offset != 0: final_x += x_offset final_y += y_offset if debug: logger.debug(f"Applied offsets: x={x_offset}, y={y_offset} -> ({final_x}, {final_y})") # --- Suffix Handling (Rotation Override) --- suffix = None suffix_source = None # First check group suffix since it should take precedence if group_suffix: suffix = group_suffix suffix_source = "group" if debug: logger.debug(f"Using group suffix '{suffix}' for element {element_name}") # Only check element suffix if no group suffix exists elif element_name and len(element_name) >= 2: last_char = element_name[-1].lower() if last_char in ['r', 'd', 'l', 'u']: suffix = last_char suffix_source = "element" if debug: logger.debug(f"Using element's own suffix '{suffix}'") # Apply rotation override if a suffix was found if suffix: original_rotation = rotation_angle rotation_map = {'r': 0, 'd': 90, 'l': 180, 'u': 270} rotation_angle = rotation_map.get(suffix, rotation_angle) if debug: logger.debug(f"SUFFIX ROTATION OVERRIDE ({suffix_source}): '{suffix}' " f"changed {original_rotation:.1f}deg to {rotation_angle}deg") if debug: logger.debug(f"Final state: Pos=({final_x:.2f}, {final_y:.2f}), " f"Size=({element_width:.2f}x{element_height:.2f}), " f"Rot={rotation_angle:.1f}deg, Prefix='{label_prefix}', Suffix={suffix}") # --- Name Cleaning --- cleaned_name = svg_utils.clean_element_name( element_name, label_prefix if has_prefix_mapping else None, suffix, has_prefix_mapping, mapping_to_use ) if debug and cleaned_name != element_name: logger.debug(f"Cleaned name: '{element_name}' -> '{cleaned_name}'") element_name = cleaned_name # --- JSON Construction --- return json_builder.create_element_json( element_name=element_name, element_id=element_id, element_label=element_label, element_count=element_count, x=final_x, y=final_y, svg_type=svg_type, label_prefix=label_prefix, rotation_angle=rotation_angle, element_width=element_width, element_height=element_height, x_offset=x_offset, y_offset=y_offset, original_name=original_name, debug_buffer=debug_buffer, has_prefix_mapping=has_prefix_mapping, custom_options=custom_options, debug=debug ) except Exception as e: logger.error(f"Failed processing {svg_type} #{element_count}: {str(e)}") import traceback traceback.print_exc() return svg_utils.create_default_element(element_count, svg_type, str(e)) # Placeholder for process_element function (to be moved next) # def process_element(...): # pass