initial commit with script, doc and one sample dwg
This commit is contained in:
commit
3ea09dccf8
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# Python
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Build / generated
|
||||
build/
|
||||
dist/
|
||||
*.spec
|
||||
export_progress.json
|
||||
|
||||
# AutoCAD generated/temp
|
||||
*.bak
|
||||
*.dwl
|
||||
*.dwl2
|
||||
|
||||
# CAD source files (keep out of git unless intentionally tracked)
|
||||
*.dwg
|
||||
BIN
2501-AMZ-TPA8-MCM01-600.dwg
Normal file
BIN
2501-AMZ-TPA8-MCM01-600.dwg
Normal file
Binary file not shown.
130
README.md
Normal file
130
README.md
Normal file
@ -0,0 +1,130 @@
|
||||
# CAD Layout Batch Export Tool
|
||||
|
||||
This project automates DWG layout preparation in AutoCAD.
|
||||
|
||||
It opens selected drawings, attaches a master drawing as XREF, imports a template layout if missing, applies viewport zoom positioning, updates title block attributes, removes default layouts, saves, and closes each file.
|
||||
|
||||
The app has two modes:
|
||||
- IO
|
||||
- NETWORK
|
||||
|
||||
The script is in export_layouts_ui.py.
|
||||
|
||||
## What This Tool Does
|
||||
|
||||
For each drawing in the selected batch, the tool:
|
||||
1. Opens the DWG in AutoCAD (COM automation).
|
||||
2. Uses the selected Master DWG as an XREF source (name: XREF1), except when processing the master itself.
|
||||
3. Ensures the target template layout exists by importing from the selected Template DWG if needed.
|
||||
4. Activates that layout and runs viewport zoom using mode-specific coordinates.
|
||||
5. Updates title block attributes:
|
||||
- IO mode: uses grouped names from the names file.
|
||||
- NETWORK mode: uses one name per sheet and transforms naming for network labels.
|
||||
6. Deletes Layout1 and Layout2.
|
||||
7. Saves and closes the DWG.
|
||||
8. Writes progress to export_progress.json for resume support.
|
||||
9. Deletes matching .bak right after each successful DWG.
|
||||
10. On full completion of all files, performs a final recursive cleanup of remaining .bak files.
|
||||
|
||||
## Requirements
|
||||
|
||||
System:
|
||||
- Windows
|
||||
- AutoCAD installed
|
||||
- AutoCAD must be available through COM
|
||||
|
||||
Python:
|
||||
- Python 3.x
|
||||
- Package: pywin32
|
||||
|
||||
Standard library modules used:
|
||||
- os
|
||||
- re
|
||||
- json
|
||||
- threading
|
||||
- time
|
||||
- tkinter
|
||||
|
||||
## Installation
|
||||
|
||||
1. Open PowerShell in the project folder.
|
||||
2. Install dependency:
|
||||
|
||||
python -m pip install pywin32
|
||||
|
||||
If you use a virtual environment, activate it first and run the same command.
|
||||
|
||||
## How To Run
|
||||
|
||||
Start the app:
|
||||
|
||||
python export_layouts_ui.py
|
||||
|
||||
In the UI, select:
|
||||
1. Master DWG (XREF source)
|
||||
2. Template DWG (layout source)
|
||||
3. Drawing files to process
|
||||
4. Block Names File (TXT)
|
||||
|
||||
Then click Run Batch.
|
||||
|
||||
You can also use the same DWG as both Master and Template if that drawing contains a valid paper-space layout.
|
||||
|
||||
## Input File Notes
|
||||
|
||||
Block Names TXT:
|
||||
- One name per line
|
||||
- Empty lines are ignored
|
||||
|
||||
Example names file is list.txt.
|
||||
|
||||
Progress file:
|
||||
- export_progress.json is auto-created/updated.
|
||||
- It stores completed DWG file names.
|
||||
- Start Over in the UI resets this progress file.
|
||||
|
||||
## Zoom Logic Overview
|
||||
|
||||
The script computes viewport center per file index:
|
||||
- center_x = base_x + (index * block_width)
|
||||
- center_y = base_y
|
||||
|
||||
It then sends AutoCAD command:
|
||||
- ZOOM C center_x,center_y height
|
||||
|
||||
IO and NETWORK modes use different base values and height.
|
||||
|
||||
## Resume and Cleanup Behavior
|
||||
|
||||
- If a batch is interrupted, rerun and it resumes using export_progress.json.
|
||||
- After each successful file, the related .bak is removed.
|
||||
- After full completion (all files done and not canceled), remaining .bak files are deleted recursively from the current working folder.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
ModuleNotFoundError: pythoncom
|
||||
- Cause: pywin32 not installed in the same Python interpreter used to run the script.
|
||||
- Fix:
|
||||
1. Check python path:
|
||||
where python
|
||||
2. Install in that interpreter:
|
||||
python -m pip install pywin32
|
||||
|
||||
AutoCAD COM errors (for example call rejected by callee)
|
||||
- Keep AutoCAD open and responsive during batch processing.
|
||||
- Retry the batch (the script already includes retry logic for open calls).
|
||||
|
||||
No layout found in template file
|
||||
- Ensure the template DWG has at least one paper-space layout (not only Model).
|
||||
|
||||
## Main Project Files
|
||||
|
||||
- export_layouts_ui.py: main application
|
||||
- list.txt: example block names input
|
||||
- export_progress.json: generated progress log during runs
|
||||
|
||||
## Safety Notes
|
||||
|
||||
- This tool edits and saves DWG files in place.
|
||||
- Test with a small file set first.
|
||||
- Keep a separate backup of critical project drawings before first production run.
|
||||
761
export_layouts_ui.py
Normal file
761
export_layouts_ui.py
Normal file
@ -0,0 +1,761 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
import pythoncom
|
||||
import win32com.client
|
||||
import time
|
||||
from win32com.client import pythoncom as pycom
|
||||
|
||||
PROGRESS_LOG = 'export_progress.json'
|
||||
|
||||
def safe_open_doc(docs, dwg_path, retries=5, delay=3):
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
return docs.Open(dwg_path)
|
||||
except Exception as e:
|
||||
if 'Call was rejected by callee' in str(e) and attempt < retries - 1:
|
||||
time.sleep(delay)
|
||||
else:
|
||||
raise
|
||||
|
||||
# --- Backend Logic ---
|
||||
def set_block_attribute(doc, block_name, attr_tag, value):
|
||||
# Search PaperSpace for block references
|
||||
for entity in doc.PaperSpace:
|
||||
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == block_name:
|
||||
try:
|
||||
for attrib in entity.GetAttributes():
|
||||
if attrib.TagString.upper() == attr_tag.upper():
|
||||
attrib.TextString = value
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
def set_wyncorp_block(doc, chunk, log_operation=None):
|
||||
# First try to find UPS.AE 11X17 (V0) block and set DWGDESC1
|
||||
for entity in doc.PaperSpace:
|
||||
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "UPS.AE 11X17 (V0)":
|
||||
try:
|
||||
value = "IO LAYOUT " + ", ".join(chunk)
|
||||
for attrib in entity.GetAttributes():
|
||||
if attrib.TagString.upper() == "DWGDESC1":
|
||||
attrib.TextString = value
|
||||
if log_operation:
|
||||
log_operation(f"Inputting block DWGDESC1: {value}")
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# If UPS.AE block not found, try WYNCORP-DSIZE_AS block and set TITLE2 and TITLE3
|
||||
for entity in doc.PaperSpace:
|
||||
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "WYNCORP-DSIZE_AS":
|
||||
try:
|
||||
title2_val = "/".join(chunk[:2]) if chunk[:2] else ""
|
||||
title3_val = "/".join(chunk[2:4]) if chunk[2:4] else ""
|
||||
if title3_val:
|
||||
title3_val += " IO DRAWINGS"
|
||||
else:
|
||||
title3_val = "IO DRAWINGS"
|
||||
for attrib in entity.GetAttributes():
|
||||
if attrib.TagString.upper() == "TITLE2":
|
||||
attrib.TextString = title2_val
|
||||
if log_operation:
|
||||
log_operation(f"Inputting block TITLE2: {title2_val}")
|
||||
if attrib.TagString.upper() == "TITLE3":
|
||||
attrib.TextString = title3_val
|
||||
if log_operation:
|
||||
log_operation(f"Inputting block TITLE3: {title3_val}")
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
def transform_dpm_name_for_network(raw_name):
|
||||
try:
|
||||
import re as _re
|
||||
m = _re.search(r"^(.*)_(DPM\d+)$", raw_name, _re.IGNORECASE)
|
||||
if m:
|
||||
rest = m.group(1)
|
||||
dpm = m.group(2).upper()
|
||||
return f"{dpm}_{rest}_ENET"
|
||||
return f"{raw_name}_ENET"
|
||||
except Exception:
|
||||
return f"{raw_name}_ENET"
|
||||
|
||||
def set_network_block(doc, raw_name, log_operation=None):
|
||||
value_title3 = transform_dpm_name_for_network(raw_name)
|
||||
# Prefer WYNCORP-DSIZE_AS: TITLE2 = NETWORK DETAILS, TITLE3 = transformed
|
||||
for entity in doc.PaperSpace:
|
||||
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "WYNCORP-DSIZE_AS":
|
||||
try:
|
||||
for attrib in entity.GetAttributes():
|
||||
if attrib.TagString.upper() == "TITLE2":
|
||||
attrib.TextString = "NETWORK DETAILS"
|
||||
if log_operation:
|
||||
log_operation("Inputting block TITLE2: NETWORK DETAILS")
|
||||
if attrib.TagString.upper() == "TITLE3":
|
||||
attrib.TextString = value_title3
|
||||
if log_operation:
|
||||
log_operation(f"Inputting block TITLE3: {value_title3}")
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
# Fallback to UPS.AE 11X17 (V0) -> DWGDESC1 (use raw DPM name, no transform)
|
||||
for entity in doc.PaperSpace:
|
||||
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "UPS.AE 11X17 (V0)":
|
||||
try:
|
||||
for attrib in entity.GetAttributes():
|
||||
if attrib.TagString.upper() == "DWGDESC1":
|
||||
fallback_val = f"NETWORK DETAILS {raw_name}"
|
||||
attrib.TextString = fallback_val
|
||||
if log_operation:
|
||||
log_operation(f"Inputting block DWGDESC1: {fallback_val}")
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
def process_single_file(dwg_path, base_dwg_path, layout_template, template_layout_name, index, total, acad, docs, block_names, mode="IO", log_operation=None, error_callback=None):
|
||||
new_doc = None
|
||||
# Auto-close if already open
|
||||
for doc in list(docs):
|
||||
try:
|
||||
if os.path.abspath(doc.FullName).lower() == os.path.abspath(dwg_path).lower():
|
||||
doc.Close(False)
|
||||
# time.sleep(2)
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
if log_operation:
|
||||
log_operation(f"Opening file: {os.path.basename(dwg_path)}")
|
||||
new_doc = safe_open_doc(docs, dwg_path)
|
||||
# time.sleep(2)
|
||||
# Check if Xref already exists (skip attaching when processing the master file itself)
|
||||
is_master_file = os.path.abspath(dwg_path).lower() == os.path.abspath(base_dwg_path).lower()
|
||||
xref_exists = False
|
||||
if not is_master_file:
|
||||
for item in new_doc.ModelSpace:
|
||||
if item.ObjectName == "AcDbBlockReference":
|
||||
try:
|
||||
if item.Name == "XREF1":
|
||||
xref_exists = True
|
||||
break
|
||||
except:
|
||||
continue
|
||||
if not xref_exists:
|
||||
if log_operation:
|
||||
log_operation("Attaching Xref...")
|
||||
# Use relative path - just the filename if in same folder
|
||||
master_filename = os.path.basename(base_dwg_path)
|
||||
insertion_point = win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, [0, 0, 0])
|
||||
new_doc.ModelSpace.AttachExternalReference(
|
||||
master_filename, "XREF1", insertion_point, 1, 1, 1, 0, False)
|
||||
# Check if layout exists
|
||||
layout_exists = False
|
||||
for layout in new_doc.Layouts:
|
||||
if layout.Name.upper() == template_layout_name.upper():
|
||||
layout_exists = True
|
||||
break
|
||||
if not layout_exists:
|
||||
if log_operation:
|
||||
log_operation("Adding template layout...")
|
||||
filedia_original = new_doc.GetVariable("FILEDIA")
|
||||
new_doc.SetVariable("FILEDIA", 0)
|
||||
command_sequence = (
|
||||
"_-LAYOUT\n"
|
||||
"T\n"
|
||||
f'"{layout_template}"\n'
|
||||
f"{template_layout_name}\n"
|
||||
"\n"
|
||||
)
|
||||
acad.ActiveDocument.SendCommand(command_sequence)
|
||||
# time.sleep(2)
|
||||
new_doc.SetVariable("FILEDIA", filedia_original)
|
||||
# Activate layout
|
||||
new_doc.ActiveLayout = new_doc.Layouts.Item(template_layout_name)
|
||||
# time.sleep(0.5)
|
||||
# Pan/zoom viewport
|
||||
if log_operation:
|
||||
log_operation("Zooming to layout area...")
|
||||
|
||||
# Set viewport values based on mode
|
||||
if mode == "IO":
|
||||
base_x = 19.13
|
||||
base_y = 0.7881
|
||||
block_width = 38.5
|
||||
height = 22.2955
|
||||
else: # NETWORK mode
|
||||
# NETWORK mode needs different values - working backwards from your feedback
|
||||
# For 19 files with total width 826.5: block_width = 826.5/19 = 43.5
|
||||
# For 10th file (index 9) to be 369: 369 = base_x + (9 * 43.5)
|
||||
# So base_x = 369 - 391.5 = -22.5
|
||||
base_x = -22
|
||||
base_y = 13.25
|
||||
block_width = 43.5
|
||||
height = 26.1748
|
||||
|
||||
acad.ActiveDocument.SendCommand("MSPACE\n")
|
||||
# time.sleep(0.5)
|
||||
# For first file (index 0), use base position. For subsequent files, add block_width * index
|
||||
center_x = base_x + (index * block_width)
|
||||
center_y = base_y
|
||||
zoom_command = f'ZOOM C {center_x},{center_y} {height}\n'
|
||||
acad.ActiveDocument.SendCommand(zoom_command)
|
||||
# time.sleep(1)
|
||||
acad.ActiveDocument.SendCommand("PSPACE\n")
|
||||
# time.sleep(0.5)
|
||||
# --- Set block attribute for this layout ---
|
||||
if mode == "IO":
|
||||
# IO mode: use chunks of 4 names
|
||||
start_idx = index * 4
|
||||
chunk = block_names[start_idx:start_idx+4]
|
||||
block_set = False
|
||||
if chunk:
|
||||
value = "IO LAYOUT " + "/".join(chunk)
|
||||
if log_operation:
|
||||
log_operation(f"Inputting block names: {value}")
|
||||
block_set = set_block_attribute(new_doc, "UPS.AE 11X17 (V0)", "DWGDESC1", value)
|
||||
# If not found, try WYNCORP-DSIZE_AS logic
|
||||
if not block_set and chunk:
|
||||
if log_operation:
|
||||
log_operation(f"Trying WYNCORP-DSIZE_AS block for chunk: {chunk}")
|
||||
set_wyncorp_block(new_doc, chunk, log_operation=log_operation)
|
||||
else: # NETWORK mode
|
||||
# NETWORK mode: use single name transformed: DPMx_<rest>_ENET,
|
||||
# and set TITLE2/TITLE3 on WYNCORP when present; fallback to UPS.AE
|
||||
if block_names and index < len(block_names):
|
||||
raw_name = block_names[index]
|
||||
if log_operation:
|
||||
log_operation(f"Preparing NETWORK labels for: {raw_name}")
|
||||
set_network_block(new_doc, raw_name, log_operation=log_operation)
|
||||
# Delete default layouts
|
||||
if log_operation:
|
||||
log_operation("Deleting default layouts (Layout1, Layout2)...")
|
||||
for layout_name in ["Layout1", "Layout2"]:
|
||||
delete_command = f"_-LAYOUT\nD\n{layout_name}\n"
|
||||
acad.ActiveDocument.SendCommand(delete_command)
|
||||
time.sleep(1)
|
||||
# Save and restore FILEDIA, then close
|
||||
if log_operation:
|
||||
log_operation("Saving file and restoring FILEDIA...")
|
||||
new_doc.Save()
|
||||
try:
|
||||
new_doc.SetVariable("FILEDIA", 1)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
if error_callback:
|
||||
error_callback(str(e))
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
if new_doc is not None:
|
||||
new_doc.Close(False)
|
||||
# time.sleep(2)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
# --- Progress Log ---
|
||||
def load_progress():
|
||||
if os.path.exists(PROGRESS_LOG):
|
||||
with open(PROGRESS_LOG, 'r') as f:
|
||||
return set(json.load(f))
|
||||
return set()
|
||||
|
||||
def save_progress(done_files):
|
||||
with open(PROGRESS_LOG, 'w') as f:
|
||||
json.dump(list(done_files), f)
|
||||
|
||||
def is_autocad_running():
|
||||
try:
|
||||
win32com.client.GetActiveObject("AutoCAD.Application")
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def read_block_names(names_file):
|
||||
if not names_file or not os.path.exists(names_file):
|
||||
return []
|
||||
with open(names_file, 'r', encoding='utf-8') as f:
|
||||
return [line.strip() for line in f if line.strip()]
|
||||
|
||||
def delete_bak_for_dwg(dwg_path, log_operation=None, error_callback=None):
|
||||
bak_path = os.path.splitext(dwg_path)[0] + '.bak'
|
||||
try:
|
||||
if os.path.exists(bak_path):
|
||||
os.remove(bak_path)
|
||||
if log_operation:
|
||||
log_operation(f"Deleted backup file: {os.path.basename(bak_path)}")
|
||||
except Exception as e:
|
||||
if error_callback:
|
||||
error_callback(f"Failed to delete backup file {os.path.basename(bak_path)}: {e}")
|
||||
|
||||
def delete_all_bak_files(root_dir, log_operation=None, error_callback=None):
|
||||
deleted_count = 0
|
||||
try:
|
||||
for current_root, _, files in os.walk(root_dir):
|
||||
for file_name in files:
|
||||
if file_name.lower().endswith('.bak'):
|
||||
bak_path = os.path.join(current_root, file_name)
|
||||
try:
|
||||
os.remove(bak_path)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
if error_callback:
|
||||
error_callback(f"Failed to delete backup file {file_name}: {e}")
|
||||
if log_operation:
|
||||
log_operation(f"Deleted {deleted_count} remaining .bak file(s) after full completion.")
|
||||
except Exception as e:
|
||||
if error_callback:
|
||||
error_callback(f"Failed during final backup cleanup: {e}")
|
||||
|
||||
# --- UI ---
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
root.title("Export Layouts Batch Processor")
|
||||
root.geometry("750x750")
|
||||
root.configure(bg="#181818")
|
||||
root.resizable(False, False)
|
||||
|
||||
# Modern font
|
||||
FONT = ("Segoe UI", 11)
|
||||
FONT_BOLD = ("Segoe UI", 12, "bold")
|
||||
GOLD = "#FFD700"
|
||||
BLACK = "#181818"
|
||||
WHITE = "#FFFFFF"
|
||||
|
||||
style = ttk.Style()
|
||||
style.theme_use('clam')
|
||||
style.configure("TButton", font=FONT, background=GOLD, foreground=BLACK, borderwidth=0, focusthickness=3, focuscolor=GOLD, padding=8)
|
||||
style.map("TButton", background=[('active', GOLD), ('pressed', GOLD)])
|
||||
style.configure("TEntry", fieldbackground=BLACK, foreground=GOLD, borderwidth=1)
|
||||
style.configure("TLabel", background=BLACK, foreground=GOLD, font=FONT)
|
||||
style.configure("TProgressbar", troughcolor=BLACK, bordercolor=BLACK, background=GOLD, lightcolor=GOLD, darkcolor=GOLD, thickness=20)
|
||||
|
||||
master_path = tk.StringVar()
|
||||
template_path = tk.StringVar()
|
||||
drawings_files = [] # List of selected DWG files
|
||||
drawings_files_var = tk.StringVar(value="No drawings selected.")
|
||||
progress_var = tk.IntVar(value=0)
|
||||
status_var = tk.StringVar(value="Idle.")
|
||||
current_file_var = tk.StringVar(value="No file processing yet.")
|
||||
error_log_var = tk.StringVar(value="")
|
||||
names_file = tk.StringVar()
|
||||
block_names = []
|
||||
|
||||
# --- Logging variables ---
|
||||
operations_log_var = tk.StringVar(value="")
|
||||
error_log_var = tk.StringVar(value="")
|
||||
|
||||
def log_operation(msg):
|
||||
prev = operations_log_var.get()
|
||||
operations_log_var.set((prev + "\n" if prev else "") + msg)
|
||||
|
||||
def append_error(msg):
|
||||
prev = error_log_var.get()
|
||||
error_log_var.set((prev + "\n" if prev else "") + msg)
|
||||
|
||||
def select_master():
|
||||
path = filedialog.askopenfilename(title="Select Master DWG", filetypes=[("DWG Files", "*.dwg")])
|
||||
if path:
|
||||
master_path.set(path)
|
||||
|
||||
def select_template():
|
||||
path = filedialog.askopenfilename(title="Select Template DWG", filetypes=[("DWG Files", "*.dwg")])
|
||||
if path:
|
||||
template_path.set(path)
|
||||
|
||||
def select_drawings_files():
|
||||
nonlocal drawings_files
|
||||
files = filedialog.askopenfilenames(title="Select Drawing Files", filetypes=[("DWG Files", "*.dwg")])
|
||||
if files:
|
||||
drawings_files = list(files)
|
||||
drawings_files_var.set(f"{len(drawings_files)} files selected.")
|
||||
else:
|
||||
drawings_files = []
|
||||
drawings_files_var.set("No drawings selected.")
|
||||
|
||||
def select_names_file():
|
||||
path = filedialog.askopenfilename(title="Select Block Names File", filetypes=[("Text Files", "*.txt")])
|
||||
if path:
|
||||
names_file.set(path)
|
||||
|
||||
cancel_requested = tk.BooleanVar(value=False)
|
||||
|
||||
def set_buttons_state(state):
|
||||
# IO tab buttons
|
||||
try:
|
||||
run_btn.config(state=state)
|
||||
start_over_btn.config(state=state)
|
||||
cancel_btn.config(state="normal" if state == "disabled" else "disabled")
|
||||
except Exception:
|
||||
pass
|
||||
# NETWORK tab buttons
|
||||
try:
|
||||
n_run_btn.config(state=state)
|
||||
n_start_over_btn.config(state=state)
|
||||
n_cancel_btn.config(state="normal" if state == "disabled" else "disabled")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def start_batch():
|
||||
nonlocal block_names
|
||||
if not master_path.get() or not template_path.get() or not drawings_files or not names_file.get():
|
||||
messagebox.showerror("Error", "Please select master, template DWG files, drawing files, and block names file.")
|
||||
return
|
||||
block_names = read_block_names(names_file.get())
|
||||
if not block_names:
|
||||
messagebox.showerror("Error", "Block names file is empty or not found.")
|
||||
return
|
||||
|
||||
# Determine which tab is active
|
||||
current_tab = main_notebook.select()
|
||||
if "IO" in main_notebook.tab(current_tab, "text"):
|
||||
mode = "IO"
|
||||
else:
|
||||
mode = "NETWORK"
|
||||
|
||||
progress_var.set(0)
|
||||
status_var.set("Processing...")
|
||||
current_file_var.set("")
|
||||
operations_log_var.set("")
|
||||
error_log_var.set("")
|
||||
cancel_requested.set(False)
|
||||
set_buttons_state("disabled")
|
||||
threading.Thread(target=lambda: run_batch(mode), daemon=True).start()
|
||||
|
||||
def start_over():
|
||||
if os.path.exists(PROGRESS_LOG):
|
||||
os.remove(PROGRESS_LOG)
|
||||
progress_var.set(0)
|
||||
status_var.set("Progress reset. Ready.")
|
||||
current_file_var.set("")
|
||||
error_log_var.set("")
|
||||
operations_log_var.set("")
|
||||
|
||||
def cancel_batch():
|
||||
cancel_requested.set(True)
|
||||
status_var.set("Cancelling... (will stop after current file)")
|
||||
|
||||
def run_batch(mode="IO"):
|
||||
try:
|
||||
# Build processing order: master first, then sorted drawings (excluding master if present)
|
||||
master = master_path.get()
|
||||
drawings = [f for f in list(drawings_files) if os.path.abspath(f).lower() != os.path.abspath(master).lower()]
|
||||
def sort_key(file_name):
|
||||
match = re.search(r'LAYOUT\s*(\d+)', os.path.basename(file_name), re.IGNORECASE)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return float('inf')
|
||||
drawings.sort(key=sort_key)
|
||||
files = [master] + drawings
|
||||
total = len(files)
|
||||
done_files = load_progress()
|
||||
pythoncom.CoInitialize()
|
||||
acad = win32com.client.Dispatch("AutoCAD.Application")
|
||||
docs = acad.Documents
|
||||
|
||||
template_doc = docs.Open(template_path.get())
|
||||
time.sleep(2) # Give AutoCAD time to fully load the template
|
||||
template_layout_name = None
|
||||
try:
|
||||
for layout in template_doc.Layouts:
|
||||
if layout.Name.upper() != "MODEL":
|
||||
template_layout_name = layout.Name
|
||||
break
|
||||
except Exception as e:
|
||||
append_error(f"Error reading template layouts: {e}")
|
||||
template_layout_name = "TITLE PAGE" # Fallback to default name
|
||||
finally:
|
||||
try:
|
||||
template_doc.Close(False)
|
||||
except Exception:
|
||||
pass
|
||||
if not template_layout_name:
|
||||
status_var.set("No layout found in template file!")
|
||||
pythoncom.CoUninitialize()
|
||||
set_buttons_state("normal")
|
||||
return
|
||||
idx = 0
|
||||
while idx < total:
|
||||
if cancel_requested.get():
|
||||
status_var.set("Batch cancelled by user.")
|
||||
break
|
||||
fname = files[idx]
|
||||
base_fname = os.path.basename(fname)
|
||||
if base_fname in done_files:
|
||||
progress_var.set(int(100 * (idx + 1) / total))
|
||||
idx += 1
|
||||
continue
|
||||
dwg_path = fname
|
||||
current_file_var.set(f"Processing: {base_fname} ({idx+1}/{total})")
|
||||
try:
|
||||
ok = process_single_file(
|
||||
dwg_path, master_path.get(), template_path.get(), template_layout_name, idx, total, acad, docs, block_names, mode, log_operation=log_operation, error_callback=append_error
|
||||
)
|
||||
if ok:
|
||||
done_files.add(base_fname)
|
||||
save_progress(done_files)
|
||||
delete_bak_for_dwg(dwg_path, log_operation=log_operation, error_callback=append_error)
|
||||
progress_var.set(int(100 * (idx + 1) / total))
|
||||
idx += 1
|
||||
else:
|
||||
append_error(f"Failed: {base_fname}")
|
||||
idx += 1 # Continue to next file
|
||||
except Exception as e:
|
||||
append_error(f"Error processing {base_fname}: {e}")
|
||||
idx += 1 # Continue to next file
|
||||
if not cancel_requested.get() and len(done_files) >= total:
|
||||
delete_all_bak_files(os.getcwd(), log_operation=log_operation, error_callback=append_error)
|
||||
|
||||
if not cancel_requested.get():
|
||||
status_var.set("All files processed (or resumed to last file).")
|
||||
current_file_var.set("")
|
||||
except Exception as e:
|
||||
append_error(f"Batch processing error: {e}")
|
||||
status_var.set("Batch processing failed.")
|
||||
finally:
|
||||
set_buttons_state("normal")
|
||||
try:
|
||||
pythoncom.CoUninitialize()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- Modern UI Layout ---
|
||||
pad = {'padx': 16, 'pady': 8}
|
||||
frame = tk.Frame(root, bg=BLACK)
|
||||
frame.pack(fill="both", expand=True)
|
||||
|
||||
tk.Label(frame, text="Export Layouts Batch Processor", font=FONT_BOLD, fg=GOLD, bg=BLACK).pack(pady=(18, 10))
|
||||
|
||||
# Main tabbed interface
|
||||
main_notebook = ttk.Notebook(frame)
|
||||
main_notebook.pack(fill="both", expand=True, padx=16, pady=(0, 10))
|
||||
|
||||
# IO Tab
|
||||
io_frame = tk.Frame(main_notebook, bg=BLACK)
|
||||
network_frame = tk.Frame(main_notebook, bg=BLACK)
|
||||
main_notebook.add(io_frame, text="IO")
|
||||
main_notebook.add(network_frame, text="NETWORK")
|
||||
|
||||
# IO Tab Content
|
||||
# Master DWG
|
||||
row1 = tk.Frame(io_frame, bg=BLACK)
|
||||
row1.pack(fill="x", **pad)
|
||||
row1.grid_columnconfigure(1, weight=1)
|
||||
tk.Label(row1, text="Master DWG (Xref):", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
|
||||
master_entry = tk.Entry(row1, textvariable=master_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD)
|
||||
master_entry.grid(row=0, column=1, sticky="ew", padx=(0,8))
|
||||
ttk.Button(row1, text="Browse...", command=select_master).grid(row=0, column=2, sticky="e")
|
||||
|
||||
# Template DWG
|
||||
row2 = tk.Frame(io_frame, bg=BLACK)
|
||||
row2.pack(fill="x", **pad)
|
||||
row2.grid_columnconfigure(1, weight=1)
|
||||
tk.Label(row2, text="Template DWG:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
|
||||
template_entry = tk.Entry(row2, textvariable=template_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD)
|
||||
template_entry.grid(row=0, column=1, sticky="ew", padx=(0,8))
|
||||
ttk.Button(row2, text="Browse...", command=select_template).grid(row=0, column=2, sticky="e")
|
||||
|
||||
# Drawings files (multi-select)
|
||||
row3 = tk.Frame(io_frame, bg=BLACK)
|
||||
row3.pack(fill="x", **pad)
|
||||
row3.grid_columnconfigure(1, weight=1)
|
||||
tk.Label(row3, text="Drawings:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
|
||||
tk.Label(row3, textvariable=drawings_files_var, font=FONT, fg=GOLD, bg=BLACK, anchor="w").grid(row=0, column=1, sticky="ew", padx=(0,8))
|
||||
ttk.Button(row3, text="Browse...", command=select_drawings_files).grid(row=0, column=2, sticky="e")
|
||||
|
||||
# Block names file
|
||||
row4 = tk.Frame(io_frame, bg=BLACK)
|
||||
row4.pack(fill="x", **pad)
|
||||
row4.grid_columnconfigure(1, weight=1)
|
||||
tk.Label(row4, text="Block Names File:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
|
||||
tk.Entry(row4, textvariable=names_file, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD).grid(row=0, column=1, sticky="ew", padx=(0,8))
|
||||
ttk.Button(row4, text="Browse...", command=select_names_file).grid(row=0, column=2, sticky="e")
|
||||
|
||||
# Progress bar with white border
|
||||
tk.Label(io_frame, text="Progress:", font=FONT, fg=GOLD, bg=BLACK, anchor="w").pack(fill="x", **pad)
|
||||
pb_border = tk.Frame(io_frame, bg=WHITE, padx=2, pady=2)
|
||||
pb_border.pack(pady=(0,10), padx=32, fill="x")
|
||||
ttk.Progressbar(pb_border, variable=progress_var, maximum=100, length=500, style="TProgressbar").pack(fill="x")
|
||||
|
||||
# Status and current file
|
||||
tk.Label(io_frame, textvariable=status_var, font=FONT, fg=WHITE, bg=BLACK).pack(fill="x", **pad)
|
||||
tk.Label(io_frame, textvariable=current_file_var, font=FONT, fg=GOLD, bg=BLACK).pack(fill="x", **pad)
|
||||
|
||||
# Tabbed log area
|
||||
log_notebook = ttk.Notebook(io_frame)
|
||||
log_notebook.pack(padx=32, pady=(10,10), fill="both", expand=False)
|
||||
op_frame = tk.Frame(log_notebook, bg=BLACK)
|
||||
err_frame = tk.Frame(log_notebook, bg=BLACK)
|
||||
log_notebook.add(op_frame, text="Operations Log")
|
||||
log_notebook.add(err_frame, text="Error Log")
|
||||
# Operations log with auto-hiding scrollbar
|
||||
op_scroll = tk.Scrollbar(op_frame)
|
||||
op_text = tk.Text(op_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=op_scroll.set, wrap="word")
|
||||
op_scroll.config(command=op_text.yview)
|
||||
op_text.pack(side="left", fill="both", expand=True)
|
||||
# Error log with auto-hiding scrollbar
|
||||
err_scroll = tk.Scrollbar(err_frame)
|
||||
err_text = tk.Text(err_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=err_scroll.set, wrap="word")
|
||||
err_scroll.config(command=err_text.yview)
|
||||
err_text.pack(side="left", fill="both", expand=True)
|
||||
|
||||
def show_scrollbar_if_needed(text_widget, scrollbar):
|
||||
text_widget.update_idletasks()
|
||||
if text_widget.yview()[0] > 0 or text_widget.yview()[1] < 1:
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
else:
|
||||
scrollbar.pack_forget()
|
||||
|
||||
def update_op_log(*args):
|
||||
op_text.config(state="normal")
|
||||
op_text.delete(1.0, tk.END)
|
||||
op_text.insert(tk.END, operations_log_var.get())
|
||||
op_text.see(tk.END)
|
||||
op_text.config(state="disabled")
|
||||
show_scrollbar_if_needed(op_text, op_scroll)
|
||||
def update_err_log(*args):
|
||||
err_text.config(state="normal")
|
||||
err_text.delete(1.0, tk.END)
|
||||
err_text.insert(tk.END, error_log_var.get())
|
||||
err_text.see(tk.END)
|
||||
err_text.config(state="disabled")
|
||||
show_scrollbar_if_needed(err_text, err_scroll)
|
||||
operations_log_var.trace_add('write', update_op_log)
|
||||
error_log_var.trace_add('write', update_err_log)
|
||||
op_text.bind('<Configure>', lambda e: show_scrollbar_if_needed(op_text, op_scroll))
|
||||
err_text.bind('<Configure>', lambda e: show_scrollbar_if_needed(err_text, err_scroll))
|
||||
|
||||
# Style the selected tab
|
||||
style.layout("TNotebook.Tab", [
|
||||
('Notebook.tab', {'sticky': 'nswe', 'children': [
|
||||
('Notebook.padding', {'side': 'top', 'sticky': 'nswe', 'children': [
|
||||
('Notebook.focus', {'side': 'top', 'sticky': 'nswe', 'children': [
|
||||
('Notebook.label', {'side': 'top', 'sticky': ''})
|
||||
]})
|
||||
]})
|
||||
]})
|
||||
])
|
||||
style.map("TNotebook.Tab",
|
||||
background=[('selected', GOLD), ('!selected', BLACK)],
|
||||
foreground=[('selected', BLACK), ('!selected', GOLD)])
|
||||
|
||||
# Buttons
|
||||
btn_frame = tk.Frame(io_frame, bg=BLACK)
|
||||
btn_frame.pack(pady=(10, 18))
|
||||
run_btn = ttk.Button(btn_frame, text="Run Batch", command=start_batch)
|
||||
run_btn.pack(side="left", padx=12)
|
||||
start_over_btn = ttk.Button(btn_frame, text="Start Over", command=start_over)
|
||||
start_over_btn.pack(side="left", padx=12)
|
||||
cancel_btn = ttk.Button(btn_frame, text="Cancel", command=cancel_batch, state="disabled")
|
||||
cancel_btn.pack(side="left", padx=12)
|
||||
|
||||
# NETWORK Tab Content
|
||||
# Master DWG
|
||||
n_row1 = tk.Frame(network_frame, bg=BLACK)
|
||||
n_row1.pack(fill="x", **pad)
|
||||
n_row1.grid_columnconfigure(1, weight=1)
|
||||
tk.Label(n_row1, text="Master DWG (Xref):", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
|
||||
n_master_entry = tk.Entry(n_row1, textvariable=master_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD)
|
||||
n_master_entry.grid(row=0, column=1, sticky="ew", padx=(0,8))
|
||||
ttk.Button(n_row1, text="Browse...", command=select_master).grid(row=0, column=2, sticky="e")
|
||||
|
||||
# Template DWG
|
||||
n_row2 = tk.Frame(network_frame, bg=BLACK)
|
||||
n_row2.pack(fill="x", **pad)
|
||||
n_row2.grid_columnconfigure(1, weight=1)
|
||||
tk.Label(n_row2, text="Template DWG:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
|
||||
n_template_entry = tk.Entry(n_row2, textvariable=template_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD)
|
||||
n_template_entry.grid(row=0, column=1, sticky="ew", padx=(0,8))
|
||||
ttk.Button(n_row2, text="Browse...", command=select_template).grid(row=0, column=2, sticky="e")
|
||||
|
||||
# Drawings files (multi-select)
|
||||
n_row3 = tk.Frame(network_frame, bg=BLACK)
|
||||
n_row3.pack(fill="x", **pad)
|
||||
n_row3.grid_columnconfigure(1, weight=1)
|
||||
tk.Label(n_row3, text="Drawings:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
|
||||
tk.Label(n_row3, textvariable=drawings_files_var, font=FONT, fg=GOLD, bg=BLACK, anchor="w").grid(row=0, column=1, sticky="ew", padx=(0,8))
|
||||
ttk.Button(n_row3, text="Browse...", command=select_drawings_files).grid(row=0, column=2, sticky="e")
|
||||
|
||||
# Block names file
|
||||
n_row4 = tk.Frame(network_frame, bg=BLACK)
|
||||
n_row4.pack(fill="x", **pad)
|
||||
n_row4.grid_columnconfigure(1, weight=1)
|
||||
tk.Label(n_row4, text="Block Names File:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
|
||||
tk.Entry(n_row4, textvariable=names_file, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD).grid(row=0, column=1, sticky="ew", padx=(0,8))
|
||||
ttk.Button(n_row4, text="Browse...", command=select_names_file).grid(row=0, column=2, sticky="e")
|
||||
|
||||
# Progress bar with white border
|
||||
tk.Label(network_frame, text="Progress:", font=FONT, fg=GOLD, bg=BLACK, anchor="w").pack(fill="x", **pad)
|
||||
n_pb_border = tk.Frame(network_frame, bg=WHITE, padx=2, pady=2)
|
||||
n_pb_border.pack(pady=(0,10), padx=32, fill="x")
|
||||
ttk.Progressbar(n_pb_border, variable=progress_var, maximum=100, length=500, style="TProgressbar").pack(fill="x")
|
||||
|
||||
# Status and current file
|
||||
tk.Label(network_frame, textvariable=status_var, font=FONT, fg=WHITE, bg=BLACK).pack(fill="x", **pad)
|
||||
tk.Label(network_frame, textvariable=current_file_var, font=FONT, fg=GOLD, bg=BLACK).pack(fill="x", **pad)
|
||||
|
||||
# Tabbed log area
|
||||
n_log_notebook = ttk.Notebook(network_frame)
|
||||
n_log_notebook.pack(padx=32, pady=(10,10), fill="both", expand=False)
|
||||
n_op_frame = tk.Frame(n_log_notebook, bg=BLACK)
|
||||
n_err_frame = tk.Frame(n_log_notebook, bg=BLACK)
|
||||
n_log_notebook.add(n_op_frame, text="Operations Log")
|
||||
n_log_notebook.add(n_err_frame, text="Error Log")
|
||||
# Operations log with auto-hiding scrollbar
|
||||
n_op_scroll = tk.Scrollbar(n_op_frame)
|
||||
n_op_text = tk.Text(n_op_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=n_op_scroll.set, wrap="word")
|
||||
n_op_scroll.config(command=n_op_text.yview)
|
||||
n_op_text.pack(side="left", fill="both", expand=True)
|
||||
# Error log with auto-hiding scrollbar
|
||||
n_err_scroll = tk.Scrollbar(n_err_frame)
|
||||
n_err_text = tk.Text(n_err_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=n_err_scroll.set, wrap="word")
|
||||
n_err_scroll.config(command=n_err_text.yview)
|
||||
n_err_text.pack(side="left", fill="both", expand=True)
|
||||
|
||||
def n_show_scrollbar_if_needed(text_widget, scrollbar):
|
||||
text_widget.update_idletasks()
|
||||
if text_widget.yview()[0] > 0 or text_widget.yview()[1] < 1:
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
else:
|
||||
scrollbar.pack_forget()
|
||||
|
||||
def n_update_op_log(*args):
|
||||
n_op_text.config(state="normal")
|
||||
n_op_text.delete(1.0, tk.END)
|
||||
n_op_text.insert(tk.END, operations_log_var.get())
|
||||
n_op_text.see(tk.END)
|
||||
n_op_text.config(state="disabled")
|
||||
n_show_scrollbar_if_needed(n_op_text, n_op_scroll)
|
||||
def n_update_err_log(*args):
|
||||
n_err_text.config(state="normal")
|
||||
n_err_text.delete(1.0, tk.END)
|
||||
n_err_text.insert(tk.END, error_log_var.get())
|
||||
n_err_text.see(tk.END)
|
||||
n_err_text.config(state="disabled")
|
||||
n_show_scrollbar_if_needed(n_err_text, n_err_scroll)
|
||||
operations_log_var.trace_add('write', n_update_op_log)
|
||||
error_log_var.trace_add('write', n_update_err_log)
|
||||
n_op_text.bind('<Configure>', lambda e: n_show_scrollbar_if_needed(n_op_text, n_op_scroll))
|
||||
n_err_text.bind('<Configure>', lambda e: n_show_scrollbar_if_needed(n_err_text, n_err_scroll))
|
||||
|
||||
# Buttons
|
||||
n_btn_frame = tk.Frame(network_frame, bg=BLACK)
|
||||
n_btn_frame.pack(pady=(10, 18))
|
||||
n_run_btn = ttk.Button(n_btn_frame, text="Run Batch", command=start_batch)
|
||||
n_run_btn.pack(side="left", padx=12)
|
||||
n_start_over_btn = ttk.Button(n_btn_frame, text="Start Over", command=start_over)
|
||||
n_start_over_btn.pack(side="left", padx=12)
|
||||
n_cancel_btn = ttk.Button(n_btn_frame, text="Cancel", command=cancel_batch, state="disabled")
|
||||
n_cancel_btn.pack(side="left", padx=12)
|
||||
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
86
list.txt
Normal file
86
list.txt
Normal file
@ -0,0 +1,86 @@
|
||||
PDP01_FIO1
|
||||
PDP01_FIOH1
|
||||
PDP06_FIO1
|
||||
PDP06_FIOH1
|
||||
PS1_1_VFD
|
||||
PS1_2_FIO1
|
||||
PS1_2_VFD
|
||||
PS1_3_VFD
|
||||
PS1_4_VFD
|
||||
PS1_5_VFD
|
||||
PS2_1_VFD
|
||||
PS2_2_VFD
|
||||
PS2_3_VFD
|
||||
PS2_4_VFD
|
||||
PS2_5_VFD
|
||||
PS2_6_VFD
|
||||
UL1_3_SIO1
|
||||
UL1_3_VFD
|
||||
UL1_4_VFD
|
||||
UL1_5_VFD
|
||||
UL1_6_VFD
|
||||
UL1_7_VFD
|
||||
UL1_8_VFD
|
||||
UL1_9_VFD
|
||||
UL1_10_FIO1
|
||||
UL1_10_VFD
|
||||
UL1_11A_VFD
|
||||
UL1_11B_VFD
|
||||
UL1_12_VFD
|
||||
UL1_13_VFD
|
||||
UL2_3_SIO1
|
||||
UL2_3_VFD
|
||||
UL2_4_VFD
|
||||
UL2_5_VFD
|
||||
UL2_6_VFD
|
||||
UL2_7_VFD
|
||||
UL2_8_VFD
|
||||
UL2_9_VFD
|
||||
UL2_10_VFD
|
||||
UL2_11_VFD
|
||||
UL3_1_VFD
|
||||
UL3_2_FIO1
|
||||
UL3_2_VFD
|
||||
UL3_3_VFD
|
||||
UL3_4_VFD
|
||||
UL3_5_VFD
|
||||
UL3_6_VFD
|
||||
UL3_7_VFD
|
||||
UL3_8_VFD
|
||||
UL3_9_VFD
|
||||
UL3_10_VFD
|
||||
UL4_3_SIO1
|
||||
UL4_3_VFD
|
||||
UL4_4_VFD
|
||||
UL4_5_VFD
|
||||
UL4_6_VFD
|
||||
UL4_7_VFD
|
||||
UL4_8_VFD
|
||||
UL4_9_VFD
|
||||
UL4_10_FIO1
|
||||
UL4_10_VFD
|
||||
UL4_11A_VFD
|
||||
UL4_11B_VFD
|
||||
UL4_12_VFD
|
||||
UL4_13_VFD
|
||||
UL5_3_SIO1
|
||||
UL5_3_VFD
|
||||
UL5_4_VFD
|
||||
UL5_5_VFD
|
||||
UL5_6_VFD
|
||||
UL5_7_VFD
|
||||
UL5_8_VFD
|
||||
UL5_9_VFD
|
||||
UL5_10_VFD
|
||||
UL5_11_VFD
|
||||
UL6_1_VFD
|
||||
UL6_2_FIO1
|
||||
UL6_2_VFD
|
||||
UL6_3_VFD
|
||||
UL6_4_VFD
|
||||
UL6_5_VFD
|
||||
UL6_6_VFD
|
||||
UL6_7_VFD
|
||||
UL6_8_VFD
|
||||
UL6_9_VFD
|
||||
UL6_10_VFD
|
||||
Loading…
x
Reference in New Issue
Block a user