import os import traceback import time import re from collections import defaultdict from win32com.client import Dispatch, gencache from PIL import Image, ImageDraw, ImageFont import xmltodict import pythoncom from win32com.client import Dispatch, VARIANT 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) 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": {}} item_template = lambda: {"quantity": 0, "area": 0, "mass": 0, "material": ""} detail_stats = { "standard": defaultdict(item_template), "custom": defaultdict(item_template), } # Сбор данных по элементам (кроме "Welding") for e in elements: type_key = "standard" if e.Standard else "custom" name = getattr(e, "Name", "Неизвестно") material = getattr(e, "Material", "Неизвестно") element_key = (name, material) mass_inertial_params = self.api7_module.IMassInertiaParam7(e) area = mass_inertial_params.Area * 0.0001 mass = round(getattr(e, "Mass"), 3) quantity = 1 detail_stats[type_key][element_key]["material"] = material detail_stats[type_key][element_key]["quantity"] += quantity detail_stats[type_key][element_key]["area"] += area detail_stats[type_key][element_key]["mass"] += mass 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}', пропускаем") # logger.info(detail_stats) # Общая площадь 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 extract_section(name): """Извлечение сечения из имени профиля""" match = re.search(r"(\d+)х(\d+)(?:х(\d+))?", name) if match: w, h, t = match.groups() return f"{w}x{h}" + (f", толщ. {t}" if t else "") return "Без сечения" def classify_item(self, name, obj): """Классификация элементов по типу и имени""" name_lower = name.lower() cls_name = str(type(obj)) # Гиб листовой детали if isinstance( obj, (self.api7_module.ISheetMetalBend, self.api7_module.ISheetMetalLineBend), ): return "Гиб" # Сварной шов elif "IUserDesignationCompObj" in cls_name or ( "IUserObject3D" in cls_name and "свар" in name_lower ): return "Сварной шов" # Стандартное изделие elif "IMacroObject3D" in cls_name or any( kw in name_lower for kw in ["болт", "гайка", "шайба"] ): return "Стандартное изделие" # Профильная труба elif "IUserObject3D" in cls_name and any( kw in name_lower for kw in ["труба", "профиль"] ): return "Профильная труба" # Листовая оболочка elif "ISheetMetalRuledShell" in cls_name: return "Листовая деталь" # Обычная деталь elif "IPart7" in cls_name: return "Кастомная деталь" # Вспомогательные объекты elif any( kw in cls_name for kw in [ "IPlane3D", "IAxis3D", "IModelObject", "IMateConstraint3D", "ISketch", ] ): return "Вспомогательный объект" else: return "Неизвестный тип" def merge_results(self, target, source): for category in ["Standard", "Sheet", "Pipe"]: for key, items in source.get(category, {}).items(): target[category].setdefault(key, []).extend(items) return target def keeper(self, obj): property_keeper = self.api7_module.IPropertyKeeper(obj) try: data = xmltodict.parse(property_keeper.Properties) properties_list = data.get("infObject", {}).get("property", []) if not isinstance(properties_list, list): properties_list = [properties_list] # Создаём словарь по @id props = {prop["@id"]: prop for prop in properties_list if "@id" in prop} if props.get("276039607982", None): section_prop = props.get("276039607982") # Сечение (тип профиля) sketch_prop = props.get("316618764777") # Эскиз сечения profile_length_prop = props.get("235833998283") # Длина профиля key = sketch_prop["@value"] return (key, profile_length_prop) except Exception as e: logger.error(e) return None return None def traverse_ipart(self, element, depth=0, viewed=None, result=None): if viewed is None: viewed = set() obj_id = id(element) if obj_id in viewed: return viewed.add(obj_id) if result is None: result = {"Standard": {}, "Sheet": {}, "Pipe": {}, "Weld": {}} property_manager = self.api7_module.IPropertyMng(self.application) parts_collection = self.api7_module.IPart7(element).Parts if not isinstance(parts_collection, tuple): parts_collection = (parts_collection,) sub_features = self.api7_module.IFeature7(element).SubFeatures(1, True, True) if not isinstance(sub_features, tuple): sub_features = (sub_features,) bodies = self.api7_module.IFeature7(element).ResultBodies if not isinstance(bodies, tuple): bodies = (bodies,) for f in parts_collection + sub_features + bodies: print(f"Обрабатываем: {type(f)} - {getattr(f, 'Name', 'Без имени')}") if getattr(f, "Standard", False): key = (f.Name, getattr(f, "Material", "Без материала")) result["Standard"].setdefault(key, []).append(f.Name) elif getattr(f, "FileName", None): param = f.GetOpenDocumentParam() param.Visible = True param.ReadOnly = True new_doc3d = f.OpenSourceDocument(param) new_doc = self.api7_module.IKompasDocument(new_doc3d) new_doc.Active = True new_doc3d = self.api7_module.IKompasDocument3D(new_doc) new_result = self.traverse_ipart(new_doc3d.TopPart) self.merge_results(result, new_result) new_doc.Close(self.constants.kdDoNotSaveChanges) else: if isinstance(f, self.api7_module.IBody7): res = self.keeper(f) if res: key, value = res result["Pipe"].setdefault(key, []).append(value) if isinstance(f, self.api7_module.IUserDesignationCompObj): weldes = self.api7_module.IFeature7(f).SubFeatures(1, True, True) user_params = self.api7_module.IUserParameters(f) lib_filename = user_params.LibraryName if lib_filename == "Welding3D": prop = property_manager.GetProperty( self.application.ActiveDocument, "Длина сварного шва" ) property_manager = self.api7_module.IPropertyMng( self.application ) for w in weldes: print(w) property_keeper = self.api7_module.IPropertyKeeper(w) res, value, from_source = property_keeper.GetPropertyValue( prop, "", True, True ) if res: key = w.Name value = value result["Weld"].setdefault(key, []).append(value) if isinstance(f, self.api7_module.ISheetMetalRuledShell): f = self.api7_module.ISheetMetalBody(f) sheets = self.api7_module.IFeature7(f).SubFeatures(1, True, True) key = (getattr(f, "Thickness", None), getattr(f, "Radius", None)) for s in sheets: area = self.api7_module.IMassInertiaParam7(s.Part).Area * 0.001 value = (area) result["Sheet"].setdefault(key, []).append(value) if isinstance( f, ( self.api7_module.IPart7, self.api7_module.IBody7, self.api7_module.IFeature7, ), ): self.traverse_ipart(f, depth + 1, viewed, result) return result 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 + 1): 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) res = self.traverse_ipart(top_part) print("@@@") print(res) 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, }, }