diff --git a/bridge.py b/bridge.py index cde55f0..0facb36 100644 --- a/bridge.py +++ b/bridge.py @@ -35,7 +35,14 @@ class PythonJSBridge(QObject): data = self.kompas.get_open_documents() if command == KompasCommand.GET_AVAILABLE_ACTIONS: data = self.kompas.get_available_actions() - + if command == KompasCommand.IGES: + data = self.kompas.save_to_iges() + if command == KompasCommand.EXPORT_RASTER: + data = self.kompas.export_to_raster() + if command == KompasCommand.PROJECT_SUPPORT: + data = self.kompas.create_drawing_for_parts() + if command == KompasCommand.STATS: + data = self.kompas.collect_statistics() return json.dumps(data, ensure_ascii=False) def send_event_to_js(self, event_type: str, data: dict): diff --git a/enums.py b/enums.py index e18dad4..8810a17 100644 --- a/enums.py +++ b/enums.py @@ -3,6 +3,11 @@ from enum import Enum class KompasCommand(str, Enum): OPEN_KOMPAS = "open_kompas" GET_AVAILABLE_ACTIONS = "get_available_actions" + + IGES = "iges" + STATS = "stats" + EXPORT_RASTER = "export_raster" + PROJECT_SUPPORT = "project_support" @classmethod def has_value(cls, value): diff --git a/parser/main.py b/parser/main.py index 6efa5fb..617ef8b 100644 --- a/parser/main.py +++ b/parser/main.py @@ -2,6 +2,9 @@ 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}", @@ -79,29 +82,442 @@ class KompasDocumentParser: 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"] - def get_all_sheets(self): - """Заглушка: Извлечение данных о листовых деталях""" - pass + 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): - """Заглушка: Экспорт в IGES""" - pass + """Сохраняет открытые 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): - """Заглушка: Сбор статистики по элементам, гибам и сваркам""" - pass + """Сбор статистики по элементам, гибам и сваркам в активном документе""" + 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): - """Заглушка: Экспорт в растровый формат""" - pass + """Экспорт открытых 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 } } + Формат: { + action_key: { + 'label': str, + 'allowed_types': list, + 'method': str, + 'command': str + } + } """ # Допустимые типы документов ALLOWED_TYPES_3D = [ @@ -115,20 +531,28 @@ class KompasDocumentParser: ALLOWED_TYPES_SPEC = [self.constants.ksDocumentSpecification] ALLOWED_TYPES_ALL = ALLOWED_TYPES_3D + ALLOWED_TYPES_2D + ALLOWED_TYPES_SPEC return { - "iges": { + KompasCommand.IGES: { "label": "Сохранить как IGES", "allowed_types": ALLOWED_TYPES_3D, + "method": "save_to_iges", + "command": KompasCommand.IGES, }, - "stats": { + KompasCommand.STATS: { "label": "Собрать статистику по элементам", "allowed_types": ALLOWED_TYPES_3D, + "method": "collect_statistics", + "command": KompasCommand.STATS, }, - "export_pdf": { + KompasCommand.EXPORT_RASTER: { "label": "Экспортировать в PDF", "allowed_types": ALLOWED_TYPES_2D + ALLOWED_TYPES_SPEC, + "method": "export_to_raster", + "command": KompasCommand.EXPORT_RASTER, }, - "project_support": { + KompasCommand.PROJECT_SUPPORT: { "label": "Создать чертежи деталей", "allowed_types": ALLOWED_TYPES_3D, + "method": "create_drawing_for_parts", + "command": KompasCommand.PROJECT_SUPPORT, }, }