246 lines
10 KiB
Python
246 lines
10 KiB
Python
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 bottom-left insertion point.")
|
|
parser.add_argument("--y", type=float, default=50.0, help="The Y coordinate for the table's bottom-left insertion point.")
|
|
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 ---
|
|
try:
|
|
if os.path.exists(args.source_dxf):
|
|
doc = ezdxf.readfile(args.source_dxf)
|
|
msp = doc.modelspace()
|
|
print(f"Loaded source DXF file: {args.source_dxf}")
|
|
else:
|
|
print(f"Source file not found, creating a new DXF document.")
|
|
doc = ezdxf.new()
|
|
msp = doc.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
|
|
|
|
# --- Draw Table ---
|
|
print("Drawing table from templates...")
|
|
start_position = Vec3(args.x, args.y)
|
|
draw_table_from_template(msp, 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()
|