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): """ Создание чертежей для всех уникальных деталей из открытых деталей и сборок. Также создаёт спецификацию для сборок. """ logger.info("Начинаем создание чертежей для деталей...") result = {"result": []} # Получаем доступные типы из разрешённых действий 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_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}") doc_3d = self.api7_module.IKompasDocument3D(doc) top_part = doc_3d.TopPart # Используем общую рекурсивную функцию вместо all_elements if doc_type == self.constants.ksDocumentAssembly: result_recursive = self._traverse_parts_recursive(top_part) elements = result_recursive["elements"] welding = result_recursive["welding"] bends = result_recursive["bends"] else: # Для отдельной детали просто собираем данные elements = [top_part] welding = self._collect_welds_from_drawing(top_part) bends = self._collect_bends_from_element(top_part) logger.info(f"Найдено элементов для создания чертежей: {len(elements)}") # Сохранение чертежей drawings = [] for component in elements: component_ipart = self.api7_module.IPart7(component) if component_ipart.Standard: continue logger.info(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] if hasattr(component_ipart.Owner.ResultBodies, "GetGabarit"): 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))] # Масштабирование c_scale = ( c_sheet.Format.FormatHeight / sum(c_size) if sum(c_size) > 0 else 1 ) # Получаем интерфейс 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 = self.prepare_export_path(doc_path, filename, "cdw") c_doc.SaveAs(full_path) logger.info(f"[OK] Сохранён чертёж: {full_path}") drawings.append( { "name": f"{filename}.cdw", "path": full_path, "success": True, "timestamp": time.time(), } ) # Сохранение спецификации для сборок specifications = [] 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 = self.prepare_export_path(doc_path, filename, "spw") spec.SaveAs(full_path) logger.info(f"[OK] Сохранена спецификация: {full_path}") specifications.append( { "name": f"{filename}.spw", "path": full_path, "success": True, "timestamp": time.time(), } ) # Добавляем результат result["result"].append( { "document_name": doc_name, "drawings": drawings, "specifications": specifications, "success": len(drawings) > 0 or len(specifications) > 0, "timestamp": time.time(), } ) except Exception as e: logger.error( f"[ERROR] Ошибка при обработке документа #{i + 1}: {e}", exc_info=True, ) result["result"].append( { "document_name": f"Документ #{i + 1}", "drawings": [], "specifications": [], "success": False, "error": str(e), "timestamp": time.time(), } ) return result 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_bends_from_element(self, element): bends = [] try: 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) except Exception as e: logger.error("Ошибка при сборе гибов из элемента", exc_info=True) return bends def _collect_welds_from_drawing(self, part): welding = [] 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: logger.error( "Ошибка при сборе сварок из чертежного контекста", exc_info=True ) return welding def _traverse_parts_recursive(self, part): elements = [] welding = [] bends = [] try: # Собираем сварки welding += self._collect_welds_from_drawing(part) # Обходим подчасти 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) bends += self._collect_bends_from_element(element) result = self._traverse_parts_recursive(element) elements += result["elements"] welding += result["welding"] bends += result["bends"] except Exception as e: logger.error("Ошибка при рекурсивном обходе частей", exc_info=True) return {"elements": elements, "welding": welding, "bends": bends} def _build_statistics_data(self, elements, welding): stats = {"Name": {}, "Material": {}, "Area": {}} # Сбор данных по элементам (кроме "Welding") for e in elements: for key in stats.keys(): if key == "Name": value = f"{getattr(e, key)}, масса {round(getattr(e, 'Mass'), 3)}" stats[key][value] = 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}, м²:" stats[key][key_area] = round(stats[key].get(key_area, 0) + area, 6) else: try: value = getattr(e, key) stats[key][value] = stats[key].get(value, 0) + 1 except AttributeError: logger.warning(f"Элемент не имеет атрибута '{key}', пропускаем") # Общая площадь stats["Area"]["Total"] = sum(stats["Area"].values()) # Добавляем сварки в статистику как отдельный раздел stats["Welding"] = {} for w in welding: w_name_split = w.Name.split("-") w_len = w_name_split[-1].split("@")[0] stats["Welding"][w.Name] = w_len if stats["Welding"]: total_length = sum( float(w_len) for w_len in stats["Welding"].values() if w_len.replace(".", "", 1).isdigit() ) stats["Welding"]["Total"] = round(total_length, 2) return stats def collect_statistics(self): """Сбор статистики по элементам, гибам и сваркам в активном документе""" logger.info("Начинаем сбор статистики по элементам...") av_actions = self.get_available_actions() allowed_types = av_actions[KompasCommand.STATS]["allowed_types"] docs_collection = self.application.Documents result = {"result": []} 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]) logger.info(f"Обрабатываем документ: {doc_name}") elements = [] bends = [] welding = [] doc_3d = self.api7_module.IKompasDocument3D(doc) top_part = doc_3d.TopPart if doc_type == self.constants.ksDocumentAssembly: # Рекурсивный обход для сборки res = self._traverse_parts_recursive(top_part) elements += res["elements"] bends += res["bends"] welding += res["welding"] else: # Для деталей просто добавляем верхнюю часть и собираем данные elements += [top_part] bends += self._collect_bends_from_element(top_part) welding += self._collect_welds_from_drawing(top_part) logger.info( f"Найдено:\n Элементов {len(elements)}\n Гибов {len(bends)}\n" ) stats = self._build_statistics_data(elements, welding) # Формируем объект документа для результата result["result"].append( { "name": doc_name, "elements_count": len(elements), "bends_count": len(bends), "statistics": stats, } ) except Exception as e: logger.error(f"[ERROR] Ошибка при обработке документа #{i + 1}: {e}") traceback.print_exc() return result def export_to_raster(self): """Экспорт открытых 2D-документов (чертежи, фрагменты, спецификации) в JPG и DXF. Создание многостраничного PDF.""" logger.info("Начинаем экспорт документов в растровый формат...") images = [] first_doc_name = None result = {"result": []} # Получаем доступные типы для экспорта из разрешенных действий 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] # Имя без расширения logger.info(f"Обрабатываем документ: {doc_name}") # Сохраняем имя первого документа if first_doc_name is None: try: doc7 = self.api7_module.IKompasDocument(doc) first_doc_name = ( doc7.LayoutSheets.ItemByNumber(1).Stamp.Text(2).Str ) except Exception as e: first_doc_name = "Без имени" logger.warning("Не удалось получить имя документа из штампа") # Получаем интерфейс 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 paths = {} # Сохранение в JPG и DXF for ext in ["jpg", "dxf"]: full_path = self.prepare_export_path(doc_path, doc_name, ext) if ext == "jpg": doc_api5.SaveAsToRasterFormat(full_path, raster_params) logger.info(f"[OK] Сохранен JPG: {full_path}") try: img = Image.open(full_path) images.append(img.copy()) img.close() except Exception as e: logger.error( f"Не удалось открыть изображение: {full_path}", exc_info=True, ) elif ext == "dxf": doc_api5.ksSaveToDXF(full_path) logger.info(f"Сохранен DXF: {full_path}") paths[ext] = full_path # Добавляем результат в список result["result"].append( { "document_name": doc_name, "document_type": doc_type, "paths": paths, "success": True, "timestamp": time.time(), } ) except Exception as e: logger.error(f"[ERROR] Не удалось создать PDF: {e}", exc_info=True) result["result"].append( { "file": None, "success": False, "error": str(e), "document_index": i + 1, "timestamp": time.time(), } ) # Создание 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, ) logger.info(f"[OK] PDF успешно сохранен: {pdf_output_path}") result["result"].append( { "document_name": pdf_output_path, "document_type": "pdf", "paths": pdf_output_path, "success": True, "timestamp": time.time(), } ) except Exception as e: logger.info(f"[ERROR] Не удалось создать PDF: {e}") return result 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, }, }