dxfedit/03_Python_OpenSource_DXF/draw_table_from_template.py
2025-09-09 18:42:30 +08:00

228 lines
9.2 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 would normally come from Excel, a database, etc.)
# ==============================================================================
BOM_DATA = [
{
"件 号": {"main": "1"},
"图号或标准号": {"main": "JB/T XXXX"},
"名 称": {
"chinese_name": "新零件-A",
"english_name": "NEW PART-A",
"specification": "M20x150"
},
"数量": {"main": "4"},
"材 料": {"main": "Q345R"},
"备 注": {"main": "自定义备注"}
},
{
"件 号": {"main": "2"},
"图号或标准号": {"main": "GB/T YYYY"},
"名 称": {
"chinese_name": "新零件-B",
"english_name": "NEW PART-B",
"specification": "DN200"
},
"数量": {"main": "2"},
"材 料": {"main": "S30408"},
"备 注": {"main": ""}
}
]
# ==============================================================================
# 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)
# --- 2. Draw Data Rows upwards ---
header_top_y = start_pos.y + header_height
current_y = header_top_y
for data_row in data_rows: # Normal order, drawing upwards
row_bottom_y = current_y
row_top_y = row_bottom_y + row_height
# Draw top horizontal line for the row
msp.add_line((start_pos.x, row_top_y), (start_pos.x + table_width, row_top_y))
# Draw vertical divider lines for the row
for x_rel in col_boundaries:
msp.add_line((start_pos.x + x_rel, row_bottom_y), (start_pos.x + x_rel, row_top_y))
# Draw text for each column in the row using the new list structure
for col_def in col_defs:
col_name = col_def["name"]
col_start_x_rel = col_def["relative_x_start"]
if col_name in data_row:
for text_def in col_def["text_definitions"]:
data_key = text_def['data_key']
text_content = data_row[col_name].get(data_key, "")
if not text_content:
continue
# Calculate absolute position for the text's alignment point
# abs_x = table_start + column_start + text_start_in_column
abs_x = start_pos.x + col_start_x_rel + text_def['relative_pos'][0]
abs_y = row_bottom_y + text_def['relative_pos'][1]
add_aligned_text(msp, text_content, (abs_x, abs_y), text_def)
current_y = row_top_y
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.")
parser.add_argument("source_dxf", help="Path to the source DXF file to read.")
parser.add_argument("output_dxf", help="Path to the output DXF file to write.")
args = parser.parse_args()
# Get the absolute path to the directory where this script is located
script_dir = os.path.dirname(os.path.abspath(__file__))
header_template_path = os.path.join(script_dir, "header_template.json")
columns_template_path = os.path.join(script_dir, "columns_template.json")
# --- Load Templates ---
try:
with open(header_template_path, 'r', encoding='utf-8') as f:
header_template = json.load(f)
with open(columns_template_path, '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
# --- Draw Table ---
print("Drawing table from templates...")
# Using a fixed start position for predictability
start_position = Vec3(260, 50)
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()