import json import os import ezdxf from ezdxf.math import Vec3 from ezdxf.enums import TextEntityAlignment import argparse # A mapping from our template strings to ezdxf's internal enums ALIGNMENT_MAP = { "TOP_LEFT": TextEntityAlignment.TOP_LEFT, "TOP_CENTER": TextEntityAlignment.TOP_CENTER, "TOP_RIGHT": TextEntityAlignment.TOP_RIGHT, "MIDDLE_LEFT": TextEntityAlignment.MIDDLE_LEFT, "MIDDLE_CENTER": TextEntityAlignment.MIDDLE_CENTER, "MIDDLE_RIGHT": TextEntityAlignment.MIDDLE_RIGHT, "BOTTOM_LEFT": TextEntityAlignment.BOTTOM_LEFT, "BOTTOM_CENTER": TextEntityAlignment.BOTTOM_CENTER, "BOTTOM_RIGHT": TextEntityAlignment.BOTTOM_RIGHT, } # ============================================================================== # 1. SAMPLE DATA (This is now loaded from an external file) # ============================================================================== def load_bom_data(file_path): """Loads BOM data from a JSON file.""" try: with open(file_path, 'r', encoding='utf-8') as f: return json.load(f) except (IOError, json.JSONDecodeError) as e: print(f"Error loading BOM data from {file_path}: {e}") return None # ============================================================================== # 2. DRAWING LOGIC # ============================================================================== def draw_table_from_template(msp, start_pos, header_template, columns_template, data_rows): """Draws a complete BOM table using separate header and column templates.""" # --- Extract definitions from templates --- header_def = header_template["header_definition"] col_defs = columns_template["column_definitions"] # This is a list row_height = columns_template["row_height"] header_height = header_template["header_height"] col_boundaries = header_template.get("column_boundaries", []) if not col_boundaries: print("Error: Column boundaries not found in template. Cannot draw lines.") return table_width = col_boundaries[-1] - col_boundaries[0] # --- 1. Draw Header at the bottom using precise geometry --- header_bottom_y = start_pos.y # --- Draw header LINES exactly as defined in the template --- header_lines = header_def.get("lines", []) if header_lines: for line_def in header_lines: start_abs_x = start_pos.x + line_def["start"][0] start_abs_y = start_pos.y + line_def["start"][1] end_abs_x = start_pos.x + line_def["end"][0] end_abs_y = start_pos.y + line_def["end"][1] msp.add_line((start_abs_x, start_abs_y), (end_abs_x, end_abs_y)) # --- Add the outer bounding box for the header --- all_x = [] all_y = [] for line in header_lines: all_x.extend([line["start"][0], line["end"][0]]) all_y.extend([line["start"][1], line["end"][1]]) min_rx, max_rx = min(all_x), max(all_x) min_ry, max_ry = min(all_y), max(all_y) # Calculate absolute coords for the bbox abs_min_x, abs_max_x = start_pos.x + min_rx, start_pos.x + max_rx abs_min_y, abs_max_y = start_pos.y + min_ry, start_pos.y + max_ry # Draw the 4 outer lines msp.add_line((abs_min_x, abs_min_y), (abs_max_x, abs_min_y)) # Bottom msp.add_line((abs_min_x, abs_max_y), (abs_max_x, abs_max_y)) # Top msp.add_line((abs_min_x, abs_min_y), (abs_min_x, abs_max_y)) # Left msp.add_line((abs_max_x, abs_min_y), (abs_max_x, abs_max_y)) # Right else: # Fallback to simple grid if lines are not defined print("Warning: Header line geometry not found in template. Drawing a simple grid.") header_top_y = header_bottom_y + header_height msp.add_line((start_pos.x, header_bottom_y), (start_pos.x + table_width, header_bottom_y)) msp.add_line((start_pos.x, header_top_y), (start_pos.x + table_width, header_top_y)) for x_rel in col_boundaries: msp.add_line((start_pos.x + x_rel, header_bottom_y), (start_pos.x + x_rel, header_top_y)) # Draw header text for text_def in header_def["texts"]: abs_x = start_pos.x + text_def['relative_pos'][0] abs_y = header_bottom_y + text_def['relative_pos'][1] add_aligned_text(msp, text_def['content'], (abs_x, abs_y), text_def) # --- Draw Data Rows (growing upwards) --- current_y = header_bottom_y + header_height # Correctly iterate through all data rows and column definitions for i, data_row in enumerate(data_rows): row_y_bottom = current_y + (i * row_height) # Iterate through all column definitions from the template for each row for col_def in col_defs: col_name = col_def["name"] # Check if the data for this column exists in the current data_row if col_name in data_row: cell_data = data_row[col_name] col_start_x_rel = col_def["relative_x_start"] # A column can have multiple text fields (e.g., main and sub-text) for text_def in col_def["text_definitions"]: data_key = text_def["data_key"] # Check if the specific data_key exists for the cell if data_key in cell_data: content = str(cell_data[data_key]) # --- Calculate Absolute Position --- # Text's relative position is relative to the column's start abs_x = start_pos.x + col_start_x_rel + text_def['relative_pos'][0] abs_y = row_y_bottom + text_def['relative_pos'][1] alignment_str = text_def.get("alignment", "BOTTOM_LEFT") alignment = ALIGNMENT_MAP.get(alignment_str, TextEntityAlignment.BOTTOM_LEFT) dxfattribs = { 'style': text_def['style'], 'height': text_def['height'], 'color': text_def['color'], 'width': 0.7 # Ensure width factor is applied } # Add the text entity with correct placement msp.add_text( content, dxfattribs=dxfattribs ).set_placement( (abs_x, abs_y), align=alignment ) # --- Draw Row and Column Lines --- # (This part seems correct, but we'll double check if text fix doesn't solve all issues) num_data_rows = len(data_rows) table_height = header_height + num_data_rows * row_height table_base_y = start_pos.y # Draw horizontal lines for each data row for i in range(num_data_rows + 1): y = table_base_y + header_height + i * row_height msp.add_line((start_pos.x, y), (start_pos.x + table_width, y)) # Draw vertical lines based on column boundaries for x_rel in col_boundaries: x_abs = start_pos.x + x_rel # CORRECTED: Vertical lines should start from the top of the header, not the bottom of the table. msp.add_line((x_abs, table_base_y + header_height), (x_abs, table_base_y + table_height)) def add_aligned_text(msp, content, point, text_def): """Adds a TEXT entity with specified alignment.""" align_str = text_def.get("alignment", "BOTTOM_LEFT") # Convert our string to the ezdxf enum alignment = ALIGNMENT_MAP.get(align_str.upper(), TextEntityAlignment.BOTTOM_LEFT) msp.add_text( content, height=text_def['height'], dxfattribs={ 'style': text_def['style'], 'color': text_def['color'], 'layer': text_def['layer'], 'width': 0.7, # Set a default width factor as requested } ).set_placement(point, align=alignment) # ============================================================================== # 3. MAIN EXECUTION # ============================================================================== def main(): parser = argparse.ArgumentParser(description="Draw a BOM table in a DXF file based on JSON templates.") # Input files parser.add_argument("source_dxf", help="Path to the source DXF file to read.") parser.add_argument("header_template", help="Path to the header template JSON file.") parser.add_argument("columns_template", help="Path to the columns template JSON file.") parser.add_argument("data_json", help="Path to the BOM data JSON file.") # Output file parser.add_argument("output_dxf", help="Path to the output DXF file to write.") # Optional coordinates parser.add_argument("--x", type=float, default=260.0, help="The X coordinate for the table's anchor point.") parser.add_argument("--y", type=float, default=50.0, help="The Y coordinate for the table's anchor point.") parser.add_argument( "--anchor", type=str, default="bottom-left", choices=["bottom-left", "bottom-right", "top-left", "top-right"], help="Sets the anchor point of the table for the insertion coordinates (--x, --y)." ) parser.add_argument("--layout", type=str, default=None, help="The name of the layout to draw on. Defaults to modelspace.") args = parser.parse_args() # --- Load Templates --- try: with open(args.header_template, 'r', encoding='utf-8') as f: header_template = json.load(f) with open(args.columns_template, 'r', encoding='utf-8') as f: columns_template = json.load(f) except (IOError, json.JSONDecodeError) as e: print(f"Error reading template files: {e}") return # --- Load DXF --- doc = None msp = None try: if os.path.exists(args.source_dxf): doc = ezdxf.readfile(args.source_dxf) print(f"Loaded source DXF file: {args.source_dxf}") else: print(f"Source file not found, creating a new DXF document.") doc = ezdxf.new() if args.layout: try: msp = doc.layouts.get(args.layout) print(f"Using layout: {args.layout}") except KeyError: print(f"Error: Layout '{args.layout}' not found in the document.") print("Available layouts:", ", ".join(doc.layouts.names())) return else: msp = doc.modelspace() print("Using modelspace.") except IOError: print(f"Could not read source DXF file: {args.source_dxf}. Creating a new document.") doc = ezdxf.new() msp = doc.modelspace() except Exception as e: print(f"An unexpected error occurred: {e}") return # --- Load Data --- bom_data = load_bom_data(args.data_json) if bom_data is None: return # --- Calculate table dimensions to determine the final start position --- col_boundaries = header_template.get("column_boundaries", [0]) table_width = col_boundaries[-1] - col_boundaries[0] header_height = header_template.get("header_height", 0) row_height = columns_template.get("row_height", 8.0) num_data_rows = len(bom_data) table_height = header_height + num_data_rows * row_height # --- Adjust start position based on the anchor --- start_x = args.x start_y = args.y if args.anchor == "bottom-right": start_x -= table_width elif args.anchor == "top-left": start_y -= table_height elif args.anchor == "top-right": start_x -= table_width start_y -= table_height # The drawing function always uses the bottom-left corner as the start position final_start_position = Vec3(start_x, start_y) # --- Draw Table --- print(f"Drawing table with anchor '{args.anchor}' at ({args.x}, {args.y})...") print(f"Calculated bottom-left start position: ({final_start_position.x:.2f}, {final_start_position.y:.2f})") draw_table_from_template(msp, final_start_position, header_template, columns_template, bom_data) # --- Save Output --- try: doc.saveas(args.output_dxf) print(f"Successfully saved new table to: {args.output_dxf}") except IOError: print(f"Could not save DXF file: {args.output_dxf}") if __name__ == "__main__": main()