import os import traceback import time from win32com.client import Dispatch, gencache from PIL import Image, ImageDraw, ImageFont from enums import KompasCommand from logger import logger 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_export_path(self, doc_path: str, doc_name: str, ext: str) -> str: """ Формирует путь для экспорта файла по шаблону: - Создаёт подпапку `ext` внутри `doc_path` - Возвращает полный путь к файлу `.` """ output_dir = os.path.join(doc_path, ext) filename = f"{doc_name}.{ext}" full_path = os.path.join(output_dir, filename) return full_path def prepare_export_path(self, doc_path: str, doc_name: str, ext: str) -> str: full_path = self.get_export_path(doc_path, doc_name, ext) os.makedirs(os.path.dirname(full_path), exist_ok=True) return full_path 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""" logger.info("Начинаем сохранение документов в формате IGES...") result = {"result": []} av_actions = self.get_available_actions() docs_collection = self.application.Documents allowed_types = av_actions[KompasCommand.IGES]["allowed_types"] 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 = "-".join(doc.Name.split(".")[:-1]) logger.info(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 full_path = self.prepare_export_path(doc_path, doc_name, "igs") export_success = doc_api5.SaveAsToAdditionFormat(full_path, save_params) # Добавляем информацию о результате в массив logger.info(f"Успешно сохранено: {full_path}") result["result"].append( { "file": full_path, "success": bool(export_success), "document_name": doc_name, "document_type": doc_type, "timestamp": time.time(), } ) if not export_success: logger.error(f"Не удалось сохранить: {full_path}") except Exception as e: logger.error( f"Ошибка при обработке документа #{i + 1}: {e}", exc_info=True, ) result["result"].append( { "file": None, "success": False, "error": str(e), "document_index": i + 1, "timestamp": time.time(), } ) return result 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, }, }