559 lines
24 KiB
Python
559 lines
24 KiB
Python
import os
|
||
import traceback
|
||
import pythoncom
|
||
from win32com.client import Dispatch, gencache
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
from enums import KompasCommand
|
||
|
||
ids = {
|
||
"api_5": "{0422828C-F174-495E-AC5D-D31014DBBE87}",
|
||
"api_7": "{69AC2981-37C0-4379-84FD-5DD2F3C0A520}",
|
||
"const": "{75C9F5D0-B5B8-4526-8681-9903C567D2ED}",
|
||
}
|
||
|
||
|
||
class KompasDocumentParser:
|
||
def __init__(self):
|
||
self.api5 = None
|
||
self.api7 = None
|
||
self.constants = None
|
||
self.application = None
|
||
self._init_kompas()
|
||
|
||
def _init_kompas(self):
|
||
"""Инициализация API КОМПАС версий 5 и 7"""
|
||
try:
|
||
import pythoncom
|
||
|
||
pythoncom.CoInitialize()
|
||
|
||
# Получаем API версии 5
|
||
self.api5_module = gencache.EnsureModule(ids["api_5"], 0, 1, 0)
|
||
self.api5 = self.api5_module.KompasObject(
|
||
Dispatch("Kompas.Application.5")._oleobj_.QueryInterface(
|
||
self.api5_module.KompasObject.CLSID, pythoncom.IID_IDispatch
|
||
)
|
||
)
|
||
|
||
# Получаем API версии 7
|
||
self.api7_module = gencache.EnsureModule(ids["api_7"], 0, 1, 0)
|
||
self.api7 = self.api7_module.IKompasAPIObject(
|
||
Dispatch("Kompas.Application.7")._oleobj_.QueryInterface(
|
||
self.api7_module.IKompasAPIObject.CLSID, pythoncom.IID_IDispatch
|
||
)
|
||
)
|
||
|
||
# Получаем константы
|
||
constants_module = gencache.EnsureModule(ids["const"], 0, 1, 0)
|
||
self.constants = constants_module.constants
|
||
|
||
# Правильное получение IApplication
|
||
self.application = self.api7_module.IApplication(self.api7)
|
||
self.application.Visible = True
|
||
|
||
except Exception as e:
|
||
# Выводим полный traceback и сообщение об ошибке
|
||
print("[ERROR] Ошибка при подключении к КОМПАС:")
|
||
traceback.print_exc() # <-- Выводит номер строки, файл и стек
|
||
raise RuntimeError(f"Ошибка при подключении к КОМПАС: {e}")
|
||
|
||
def get_open_documents(self):
|
||
"""Возвращает список информации о всех открытых документах"""
|
||
|
||
documents = []
|
||
docs_collection = self.application.Documents
|
||
|
||
for i in range(docs_collection.Count):
|
||
try:
|
||
doc = docs_collection.Item(i) # Индексация с 1
|
||
if doc is None:
|
||
continue
|
||
|
||
documents.append(
|
||
{
|
||
"type": doc.DocumentType,
|
||
"name": doc.Name,
|
||
"path": doc.Path,
|
||
"active": doc.Active,
|
||
}
|
||
)
|
||
except Exception as e:
|
||
print(f"Ошибка при обработке документа #{i + 1}: {e}")
|
||
|
||
return documents
|
||
|
||
def create_drawing_for_parts(self):
|
||
"""
|
||
Создание чертежей для всех уникальных деталей из открытых деталей и сборок.
|
||
Также создаёт спецификацию для сборок.
|
||
"""
|
||
print("Начинаем создание чертежей для деталей...")
|
||
saving_path = "../cdw/"
|
||
|
||
# Получаем доступные типы из разрешённых действий
|
||
av_actions = self.get_available_actions()
|
||
allowed_types = av_actions[KompasCommand.PROJECT_SUPPORT]["allowed_types"]
|
||
|
||
docs_collection = self.application.Documents
|
||
|
||
for i in range(docs_collection.Count):
|
||
try:
|
||
doc = docs_collection.Item(i)
|
||
if doc is None:
|
||
continue
|
||
|
||
doc_path = doc.Path
|
||
|
||
# Сохранение чертежа
|
||
output_dir = os.path.join(doc_path, saving_path.strip("/"))
|
||
if not os.path.exists(output_dir):
|
||
os.makedirs(output_dir)
|
||
print(f"Создана папка: {output_dir}")
|
||
|
||
doc_type = doc.DocumentType
|
||
if doc_type not in allowed_types:
|
||
continue
|
||
|
||
doc.Active = True
|
||
doc_name = "-".join(doc.Name.split(".")[:-1])
|
||
print(f"Обрабатываем документ: {doc_name}")
|
||
|
||
# Сохранение чертежа
|
||
output_dir = os.path.join(doc_path, saving_path.strip("/"))
|
||
if not os.path.exists(output_dir):
|
||
os.makedirs(output_dir)
|
||
print(f"Создана папка: {output_dir}")
|
||
|
||
doc_3d = self.api7_module.IKompasDocument3D(doc)
|
||
doc_components = []
|
||
|
||
def all_elements(part):
|
||
"""Рекурсивный обход всех компонентов сборки"""
|
||
parts = self.api7_module.IParts7(part.Parts)
|
||
for j in range(parts.Count):
|
||
element = parts.Part(j)
|
||
if element.Parts.Count == 0:
|
||
if not element.Standard and not any(
|
||
el.Name == element.Name for el in doc_components
|
||
):
|
||
doc_components.append(element)
|
||
else:
|
||
all_elements(element)
|
||
|
||
if doc_type == self.constants.ksDocumentAssembly:
|
||
all_elements(doc_3d.TopPart)
|
||
else:
|
||
doc_components.append(doc_3d.TopPart)
|
||
|
||
for component in doc_components:
|
||
component_ipart = self.api7_module.IPart7(component)
|
||
if component_ipart.Standard:
|
||
continue
|
||
|
||
print(f"Создаём чертёж для: {component.Name}")
|
||
|
||
# Создаём новый чертёж
|
||
c_doc = self.application.Documents.Add(self.constants.ksDocumentDrawing)
|
||
c_layout = c_doc.LayoutSheets
|
||
c_sheet = c_layout.Item(0)
|
||
c_sheet.Format.Format = self.constants.ksFormatA3
|
||
c_sheet.Format.VerticalOrientation = False
|
||
c_sheet.Update()
|
||
|
||
# Расчёт габаритов
|
||
c_size = [1, 1, 1]
|
||
if component_ipart.Owner.ResultBodies:
|
||
gabarit = [0, 0, 0, 0, 0, 0]
|
||
component_ipart.Owner.ResultBodies.GetGabarit(*gabarit)
|
||
g1 = gabarit[1:4]
|
||
g2 = gabarit[4:]
|
||
c_size = [abs(g1[i] - g2[i]) for i in range(len(g1))]
|
||
g = max(c_size)
|
||
|
||
# Масштабирование
|
||
c_scale = c_sheet.Format.FormatHeight / sum(c_size)
|
||
|
||
# Получаем интерфейс 2D документа
|
||
c_doc_2d = self.api7_module.IKompasDocument2D(c_doc)
|
||
c_views = c_doc_2d.ViewsAndLayersManager.Views
|
||
|
||
# Добавляем стандартные виды
|
||
c_views.AddStandartViews(
|
||
component_ipart.FileName,
|
||
component_ipart.Name,
|
||
[1, 3, 5, 7],
|
||
c_size[1] * c_scale,
|
||
c_sheet.Format.FormatHeight - 25,
|
||
c_scale,
|
||
20,
|
||
20,
|
||
)
|
||
|
||
|
||
filename = "_".join(
|
||
filter(
|
||
None,
|
||
[component_ipart.Marking, component_ipart.Name[:20]],
|
||
)
|
||
).replace(" ", "-")
|
||
|
||
full_path = os.path.join(output_dir, f"{filename}.cdw")
|
||
c_doc.SaveAs(full_path)
|
||
print(f"[OK] Сохранён чертёж: {full_path}")
|
||
|
||
# Сохранение спецификации для сборок
|
||
if doc_type == self.constants.ksDocumentAssembly:
|
||
spec = self.application.Documents.Add(
|
||
self.constants.ksDocumentSpecification
|
||
)
|
||
spec_doc = self.api7_module.ISpecificationDocument(spec)
|
||
spec_doc.AttachedDocuments.Add(doc.PathName, True)
|
||
|
||
filename = "Список_деталей"
|
||
full_path = os.path.join(output_dir, f"{filename}.spw")
|
||
spec.SaveAs(full_path)
|
||
print(f"[OK] Сохранена спецификация: {full_path}")
|
||
|
||
except Exception as e:
|
||
print(f"[ERROR] Ошибка при обработке документа #{i + 1}: {e}")
|
||
traceback.print_exc()
|
||
|
||
def save_to_iges(self):
|
||
"""Сохраняет открытые 3D-документы (детали/сборки) в формате IGES"""
|
||
print("Начинаем сохранение документов в формате IGES...")
|
||
av_actions = self.get_available_actions()
|
||
docs_collection = self.application.Documents
|
||
|
||
for i in range(docs_collection.Count):
|
||
try:
|
||
doc = docs_collection.Item(i)
|
||
if doc is None:
|
||
continue
|
||
|
||
doc_type = doc.DocumentType
|
||
if doc_type not in av_actions[KompasCommand.IGES]["allowed_types"]:
|
||
continue
|
||
|
||
doc.Active = True
|
||
doc_path = doc.Path
|
||
doc_name = "-".join(doc.Name.split(".")[:-1])
|
||
print(f"Попытка сохранить: {doc_name}")
|
||
|
||
# Получаем 3D-документ через API v5
|
||
doc_api5 = self.api5.ActiveDocument3D()
|
||
if not doc_api5:
|
||
raise RuntimeError(
|
||
"Не удалось получить активный 3D-документ через API v5"
|
||
)
|
||
|
||
# Подготавливаем параметры сохранения в IGES
|
||
save_params = doc_api5.AdditionFormatParam()
|
||
save_params.Init()
|
||
save_params.format = self.constants.ksConverterToIGES
|
||
|
||
ext = "igs"
|
||
output_dir = os.path.join(doc_path, ext)
|
||
filename = f"{doc_name}.{ext}"
|
||
full_path = os.path.join(output_dir, filename)
|
||
|
||
# Создаём директорию, если её нет
|
||
if not os.path.exists(output_dir):
|
||
os.makedirs(output_dir)
|
||
|
||
# Сохраняем файл
|
||
result = doc_api5.SaveAsToAdditionFormat(full_path, save_params)
|
||
if result:
|
||
print(f"[OK] Сохранено: {full_path}")
|
||
else:
|
||
print(f"[ERROR] Не удалось сохранить: {full_path}")
|
||
|
||
except Exception as e:
|
||
print(f"[ERROR] Ошибка при обработке документа #{i + 1}: {e}")
|
||
traceback.print_exc()
|
||
|
||
def collect_statistics(self):
|
||
"""Сбор статистики по элементам, гибам и сваркам в активном документе"""
|
||
print("Начинаем сбор статистики по элементам...")
|
||
|
||
# Получаем доступные типы из разрешённых действий
|
||
av_actions = self.get_available_actions()
|
||
allowed_types = av_actions[KompasCommand.STATS]["allowed_types"]
|
||
|
||
docs_collection = self.application.Documents
|
||
|
||
for i in range(docs_collection.Count):
|
||
try:
|
||
doc = docs_collection.Item(i)
|
||
if doc is None:
|
||
continue
|
||
doc_type = doc.DocumentType
|
||
if doc_type not in allowed_types:
|
||
continue
|
||
doc.Active = True
|
||
doc_name = "-".join(doc.Name.split(".")[:-1])
|
||
print(f"Обрабатываем документ: {doc_name}")
|
||
|
||
doc_3d = self.api7_module.IKompasDocument3D(doc)
|
||
top_part = doc_3d.TopPart
|
||
elements = []
|
||
bends = []
|
||
welding = []
|
||
|
||
def look_features(element):
|
||
feature = self.api7_module.IFeature7(element)
|
||
sub_features = feature.SubFeatures(1, True, False) or []
|
||
for item in sub_features:
|
||
if type(item) in (
|
||
self.api7_module.ISheetMetalBend,
|
||
self.api7_module.ISheetMetalLineBend,
|
||
self.api7_module.ISheetMetalBody,
|
||
):
|
||
sub_sheets = item.Owner.SubFeatures(1, True, False)
|
||
if sub_sheets:
|
||
for b in sub_sheets:
|
||
bend = self.api7_module.ISheetMetalBend(b)
|
||
bends.append(bend)
|
||
|
||
def look_drawing(part):
|
||
drawing_context = self.api7_module.IDrawingContainer(part)
|
||
macro = self.api7_module.IMacroObject3D(drawing_context)
|
||
sub_features = macro.Owner.SubFeatures(1, True, False) or []
|
||
for item in sub_features:
|
||
if isinstance(item, self.api7_module.IUserDesignationCompObj):
|
||
welding.append(item)
|
||
|
||
def find_el(part):
|
||
try:
|
||
drawing_context = self.api7_module.IDrawingContainer(part)
|
||
macro = self.api7_module.IMacroObject3D(drawing_context)
|
||
sub_features = macro.Owner.SubFeatures(1, True, False) or []
|
||
for item in sub_features:
|
||
if isinstance(item, self.api7_module.IUserDesignationCompObj):
|
||
welding.append(item)
|
||
except Exception as e:
|
||
print('Ошибка в DrawingContext:', e)
|
||
|
||
try:
|
||
doc_parts = self.api7_module.IParts7(part.Parts)
|
||
for j in range(doc_parts.Count):
|
||
element = doc_parts.Part(j)
|
||
if element.Parts.Count == 0:
|
||
elements.append(element)
|
||
look_features(element)
|
||
find_el(element)
|
||
except Exception as e:
|
||
print('Ошибка в Parts:', e)
|
||
|
||
if doc_type == self.constants.ksDocumentAssembly:
|
||
find_el(top_part)
|
||
else:
|
||
elements.append(top_part)
|
||
look_drawing(top_part)
|
||
look_features(top_part)
|
||
|
||
print(f"Найдено:\n Элементов {len(elements)}\n Гибов {len(bends)}\n")
|
||
|
||
sorted_stats = {
|
||
"Name": {},
|
||
"Material": {},
|
||
"Area": {},
|
||
}
|
||
|
||
for e in elements:
|
||
for key in sorted_stats.keys():
|
||
if key == 'Name':
|
||
value = f"{getattr(e, key)}, масса {round(getattr(e, 'Mass'), 3)}"
|
||
sorted_stats[key][value] = sorted_stats[key].get(value, 0) + 1
|
||
elif key == 'Area':
|
||
mass_inertial_params = self.api7_module.IMassInertiaParam7(e)
|
||
area = mass_inertial_params.Area * 0.0001 # м²
|
||
material = getattr(e, 'Material', 'Неизвестно')
|
||
key_area = f"Площадь {material}, м²:"
|
||
sorted_stats[key][key_area] = round(sorted_stats[key].get(key_area, 0) + area, 6)
|
||
else:
|
||
value = getattr(e, key)
|
||
sorted_stats[key][value] = sorted_stats[key].get(value, 0) + 1
|
||
|
||
sorted_stats['Area']['Total'] = sum(sorted_stats['Area'].values())
|
||
|
||
welding_key = "Welding"
|
||
for w in welding:
|
||
if welding_key not in sorted_stats:
|
||
sorted_stats[welding_key] = {}
|
||
w_name_split = w.Name.split('-')
|
||
w_len = w_name_split[-1].split('@')[0]
|
||
sorted_stats[welding_key][w.Name] = w_len
|
||
|
||
if welding_key in sorted_stats:
|
||
def float_f(n):
|
||
try:
|
||
return float(n)
|
||
except:
|
||
return 0
|
||
total_length = sum(float_f(v) for v in sorted_stats[welding_key].values())
|
||
sorted_stats[welding_key]["Total"] = total_length
|
||
|
||
for section in sorted_stats:
|
||
print(section)
|
||
for name, value in sorted_stats[section].items():
|
||
print(f"{name} -- {value}")
|
||
print('-----')
|
||
|
||
except Exception as e:
|
||
print(f"[ERROR] Ошибка при обработке документа #{i + 1}: {e}")
|
||
traceback.print_exc()
|
||
|
||
def export_to_raster(self):
|
||
"""Экспорт открытых 2D-документов (чертежи, фрагменты, спецификации) в JPG и DXF. Создание многостраничного PDF."""
|
||
print("Начинаем экспорт документов в растровый формат...")
|
||
images = []
|
||
first_doc_name = None
|
||
|
||
# Получаем доступные типы для экспорта из разрешенных действий
|
||
av_actions = self.get_available_actions()
|
||
allowed_types = av_actions[KompasCommand.EXPORT_RASTER]["allowed_types"]
|
||
|
||
docs_collection = self.application.Documents
|
||
|
||
for i in range(docs_collection.Count):
|
||
try:
|
||
doc = docs_collection.Item(i)
|
||
if doc is None:
|
||
continue
|
||
|
||
doc_type = doc.DocumentType
|
||
if doc_type not in allowed_types:
|
||
continue
|
||
|
||
doc.Active = True
|
||
doc_path = doc.Path
|
||
doc_name = os.path.splitext(doc.Name)[0] # Имя без расширения
|
||
print(f"Обрабатываем документ: {doc_name}")
|
||
|
||
# Сохраняем имя первого документа
|
||
if first_doc_name is None:
|
||
doc7 = self.api7_module.IKompasDocument(doc)
|
||
first_doc_name = doc7.LayoutSheets.ItemByNumber(1).Stamp.Text(2).Str
|
||
|
||
# Получаем интерфейс 2D-документа
|
||
if doc_type == self.constants.ksDocumentSpecification:
|
||
doc_api5 = self.api5.SpcActiveDocument()
|
||
else:
|
||
doc_api5 = self.api5.ActiveDocument2D()
|
||
|
||
if not doc_api5:
|
||
raise RuntimeError("Не удалось получить активный 2D-документ")
|
||
|
||
# Параметры экспорта в JPG
|
||
raster_params = doc_api5.RasterFormatParam()
|
||
raster_params.Init()
|
||
raster_params.colorBPP = 8
|
||
raster_params.colorType = 3
|
||
raster_params.extResolution = 96
|
||
raster_params.format = 0 # JPEG format
|
||
|
||
# Сохранение в JPG и DXF
|
||
for ext in ["jpg", "dxf"]:
|
||
output_dir = os.path.join(doc_path, ext)
|
||
filename = f"{doc_name}.{ext}"
|
||
full_path = os.path.join(output_dir, filename)
|
||
|
||
if not os.path.exists(output_dir):
|
||
os.makedirs(output_dir)
|
||
print(f"Создана папка: {output_dir}")
|
||
|
||
if ext == "jpg":
|
||
doc_api5.SaveAsToRasterFormat(full_path, raster_params)
|
||
print(f"[OK] Сохранен JPG: {full_path}")
|
||
images.append(Image.open(full_path))
|
||
elif ext == "dxf":
|
||
doc_api5.ksSaveToDXF(full_path)
|
||
print(f"[OK] Сохранен DXF: {full_path}")
|
||
|
||
except Exception as e:
|
||
print(f"[ERROR] Ошибка при обработке документа #{i + 1}: {e}")
|
||
traceback.print_exc()
|
||
|
||
# Создание PDF
|
||
if images:
|
||
desktop_path = os.path.expanduser("~\\Desktop")
|
||
pdf_filename = f"{first_doc_name}_pages.pdf"
|
||
pdf_output_path = os.path.join(desktop_path, pdf_filename)
|
||
|
||
# Создаем титульную страницу с названием и количеством страниц
|
||
try:
|
||
font = ImageFont.truetype("arial.ttf", size=48)
|
||
except IOError:
|
||
print("Шрифт Arial не найден. Используется стандартный шрифт.")
|
||
font = ImageFont.load_default()
|
||
|
||
title_image = Image.new("RGB", (images[0].width, 200), color="white")
|
||
draw = ImageDraw.Draw(title_image)
|
||
title_text = f"{first_doc_name}\nКоличество страниц: {len(images)}"
|
||
draw.text((10, 50), title_text, fill="black", font=font, spacing=10)
|
||
images.insert(0, title_image)
|
||
|
||
# Сохраняем как PDF
|
||
try:
|
||
images[0].save(
|
||
pdf_output_path,
|
||
save_all=True,
|
||
append_images=images[1:],
|
||
format="PDF",
|
||
resolution=96.0,
|
||
)
|
||
print(f"[OK] PDF успешно сохранен: {pdf_output_path}")
|
||
except Exception as e:
|
||
print(f"[ERROR] Не удалось создать PDF: {e}")
|
||
|
||
def get_available_actions(self):
|
||
"""
|
||
Возвращает список доступных действий и допустимые типы документов
|
||
Формат: {
|
||
action_key: {
|
||
'label': str,
|
||
'allowed_types': list,
|
||
'method': str,
|
||
'command': str
|
||
}
|
||
}
|
||
"""
|
||
# Допустимые типы документов
|
||
ALLOWED_TYPES_3D = [
|
||
self.constants.ksDocumentPart,
|
||
self.constants.ksDocumentAssembly,
|
||
]
|
||
ALLOWED_TYPES_2D = [
|
||
self.constants.ksDocumentDrawing,
|
||
self.constants.ksDocumentFragment,
|
||
]
|
||
ALLOWED_TYPES_SPEC = [self.constants.ksDocumentSpecification]
|
||
ALLOWED_TYPES_ALL = ALLOWED_TYPES_3D + ALLOWED_TYPES_2D + ALLOWED_TYPES_SPEC
|
||
return {
|
||
KompasCommand.IGES: {
|
||
"label": "Сохранить как IGES",
|
||
"allowed_types": ALLOWED_TYPES_3D,
|
||
"method": "save_to_iges",
|
||
"command": KompasCommand.IGES,
|
||
},
|
||
KompasCommand.STATS: {
|
||
"label": "Собрать статистику по элементам",
|
||
"allowed_types": ALLOWED_TYPES_3D,
|
||
"method": "collect_statistics",
|
||
"command": KompasCommand.STATS,
|
||
},
|
||
KompasCommand.EXPORT_RASTER: {
|
||
"label": "Экспортировать в PDF",
|
||
"allowed_types": ALLOWED_TYPES_2D + ALLOWED_TYPES_SPEC,
|
||
"method": "export_to_raster",
|
||
"command": KompasCommand.EXPORT_RASTER,
|
||
},
|
||
KompasCommand.PROJECT_SUPPORT: {
|
||
"label": "Создать чертежи деталей",
|
||
"allowed_types": ALLOWED_TYPES_3D,
|
||
"method": "create_drawing_for_parts",
|
||
"command": KompasCommand.PROJECT_SUPPORT,
|
||
},
|
||
}
|