diff --git a/.gitignore b/.gitignore index 33406e3..73d42f0 100644 --- a/.gitignore +++ b/.gitignore @@ -112,4 +112,8 @@ install_plugin_cad.sh *# .#* \#*\# -out/ \ No newline at end of file +out/ + +#freecad_workbench +freecad_workbench/freecad/update_workbench.sh +*.FCBak \ No newline at end of file diff --git a/freecad_workbench/freecad/robossembler/Frames.py b/freecad_workbench/freecad/robossembler/Frames.py index 6a794f9..7b3599f 100644 --- a/freecad_workbench/freecad/robossembler/Frames.py +++ b/freecad_workbench/freecad/robossembler/Frames.py @@ -4,8 +4,9 @@ from . import ICONPATH, TRANSLATIONSPATH from .Tools import spawnClassCommand, placement2axisvec, describeSubObject, getPrimitiveInfo #, RobossemblerFreeCadExportScenario from .pddl import freecad2pddl from .BoMList import run_BoM_list -from .ImportExportEntities import export_coordinate_systems +from .export_entities import export_coordinate_systems from .utils.freecad_processor import process_file +from .project_validator import validate_project from .usecases.asm4parser_usecase import Asm4StructureParseUseCase @@ -296,11 +297,6 @@ spawnClassCommand("FrameCommand", "MenuText": "Make a free frame", "ToolTip": "Make a freestanding reference frame."}) -# spawnClassCommand("ASM4StructureParsing", -# RobossemblerFreeCadExportScenario().call, -# {"Pixmap": str(os.path.join(ICONPATH, "assembly4.svg")), -# "MenuText": "Make a ASM4 parsing", -# "ToolTip": "Make a ASM4 1"}) spawnClassCommand("SelectedPartFrameCommand", makeSelectedPartFrames, @@ -319,44 +315,17 @@ spawnClassCommand("FeatureFrameCommand", "MenuText": "frame on selected primitive", "ToolTip": "Create a frame on selected primitive."}) -spawnClassCommand("PDDL_CreateTypes", - freecad2pddl.add_types, - {"Pixmap": str(os.path.join(ICONPATH, "featureframecreator.svg")), - "MenuText": "Types", - "ToolTip": "Add Types"}) - -spawnClassCommand("PDDL_CreateParameters", - freecad2pddl.add_parameters, - {"Pixmap": str(os.path.join(ICONPATH, "featureframecreator.svg")), - "MenuText": "Parameters", - "ToolTip": "Add Parameters"}) -spawnClassCommand("PDDL_CreateAction", - freecad2pddl.add_action, - {"Pixmap": str(os.path.join(ICONPATH, "featureframecreator.svg")), - "MenuText": "Action", - "ToolTip": "Add Action"}) -spawnClassCommand("PDDL_CreatePredicate", - freecad2pddl.add_predicate, - {"Pixmap": str(os.path.join(ICONPATH, "featureframecreator.svg")), - "MenuText": "Predicate", - "ToolTip": "Add Predicate"}) -spawnClassCommand("PDDL_CreateDurativeAction", - freecad2pddl.add_durative_action, - {"Pixmap": str(os.path.join(ICONPATH, "featureframecreator.svg")), - "MenuText": "DurativeAction", - "ToolTip": "Add Durative Action"}) -spawnClassCommand("PDDL_ExportPDDL", - freecad2pddl.export_to_file, - {"Pixmap": str(os.path.join(ICONPATH, "featureframecreator.svg")), - "MenuText": "ExportDomain", - "ToolTip": "Create and Export Domain.pddl to File"}) - spawnClassCommand("Export_Entities", export_coordinate_systems, {"Pixmap": str(os.path.join(ICONPATH, "BoMList.svg")), "MenuText": "ExportLCS", "ToolTip": "Export all the markups"}) +spawnClassCommand("Validate_Project", + validate_project, + {"Pixmap": str(os.path.join(ICONPATH, "")), + "MenuText": "Validate Project", + "ToolTip": "Check errors in project file"}) spawnClassCommand("Publish_Project", process_file, {"Pixmap": str(os.path.join(ICONPATH, "publish.svg")), diff --git a/freecad_workbench/freecad/robossembler/ImportExportEntities.py b/freecad_workbench/freecad/robossembler/export_entities.py similarity index 100% rename from freecad_workbench/freecad/robossembler/ImportExportEntities.py rename to freecad_workbench/freecad/robossembler/export_entities.py diff --git a/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/geometry_usecase.py b/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/geometry_usecase.py new file mode 100644 index 0000000..3807ad5 --- /dev/null +++ b/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/geometry_usecase.py @@ -0,0 +1,63 @@ +import FreeCAD as App +import FreeCAD.Console as Console + +class GeometryUseCase: + def __init__(self, document): + self.document = document + + def is_valid_geometry(self, obj): + return hasattr(obj, "Shape") and obj.Shape.Volume is not None + + def create_intersection_matrix(self): + bodies = [obj for obj in self.document.Objects if self.is_valid_geometry(obj)] + num_bodies = len(bodies) + intersection_matrix = [[False for _ in range(num_bodies)] for _ in range(num_bodies)] + + for i in range(num_bodies): + for j in range(i + 1, num_bodies): + body1 = bodies[i] + body2 = bodies[j] + + if body1.Shape.common(body2.Shape).Volume > 0: + intersection_matrix[i][j] = True + intersection_matrix[j][i] = True + + if not hasattr(body1, "AllowIntersections") or not hasattr(body2, "AllowIntersections"): + Console.PrintWarning( + f"Пересечение между '{body1.Name}' и '{body2.Name}' без разрешения.\n" + ) + + return intersection_matrix + + def create_contact_matrix(self): + bodies = [obj for obj in self.document.Objects if self.is_valid_geometry(obj)] + num_bodies = len(bodies) + contact_matrix = [[False for _ in range(num_bodies)] for _ in range(num_bodies)] + + for i in range(num_bodies): + for j in range(i + 1, num_bodies): + body1 = bodies[i] + body2 = bodies[j] + + if body1.Shape.common(body2.Shape).SurfaceArea > 0: + contact_matrix[i][j] = True + contact_matrix[j][i] = True + + return contact_matrix + + +doc = App.ActiveDocument +if doc is None: + Console.PrintError("Нет активного документа в FreeCAD.\n") +else: + use_case = GeometryUseCase(doc) + + intersection_matrix = use_case.create_intersection_matrix() + Console.PrintMessage("Матрица пересечений:\n") + for row in intersection_matrix: + Console.PrintMessage(f"{row}\n") + + contact_matrix = use_case.create_contact_matrix() + Console.PrintMessage("Матрица контактов:\n") + for row in contact_matrix: + Console.PrintMessage(f"{row}\n") diff --git a/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/main.py b/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/main.py index c669ba4..f11e36a 100644 --- a/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/main.py +++ b/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/main.py @@ -31,6 +31,7 @@ from usecases.open_freecad_document_use_case import ( from mocks.mock_structure import bottle_jack_mock_structure, simple_cube_mock_structure + def main(): try: EnvReaderUseCase.call().either( diff --git a/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/re_main.py b/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/re_main.py new file mode 100644 index 0000000..19bce43 --- /dev/null +++ b/freecad_workbench/freecad/robossembler/geometric_feasibility_predicate/re_main.py @@ -0,0 +1,1313 @@ +import FreeCAD as App +import uuid +import os +import json +from typing import List, Dict, Any, TypeVar, Callable, Type, cast +from itertools import repeat +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any, TypeVar, Type, cast + + +T = TypeVar("T") + + +class BackgroundConsoleColors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + + +def from_float(x: Any) -> float: + assert isinstance(x, (float, int)) and not isinstance(x, bool) + return float(x) + + +def to_float(x: Any) -> float: + assert isinstance(x, float) + return x + + +def from_str(x: Any) -> str: + assert isinstance(x, str) + return x + + +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + return x + + +def to_class(c: Type[T], x: Any) -> dict: + assert isinstance(x, c) + return cast(Any, x).to_dict() + + +@dataclass +class Env: + cadFilePath: str + outPath: str + solidBodyPadding: float + firstDetail: str + sequencesFixed: list[list[str]] + + @staticmethod + def from_dict(obj: Any) -> "Env": + assert isinstance(obj, dict) + cadFilePath = from_str(obj.get("cadFilePath")) + outPath = from_str(obj.get("outPath")) + solidBodyPadding = from_float(obj.get("solidBodyPadding")) + firstDetail = from_str(obj.get("firstDetail")) + sequencesFixed = [] + sequencesFixedParse = CoreList(obj.get("sequencesFixed")) + for el in sequencesFixedParse: + sequencesFixed.append(el) + + restrictionsOnFasteners = CoreList(obj.get("restrictionsOnFasteners")) + + for el in restrictionsOnFasteners: + for part in el["parts"]: + sequencesFixed.append([part, el["fastener"]]) + return Env( + cadFilePath, + outPath, + solidBodyPadding, + firstDetail, + sequencesFixed, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["cadFilePath"] = from_str(self.cadFilePath) + result["outPath"] = from_str(self.outPath) + result["solidBodyPadding"] = from_float(self.solidBodyPadding) + result["firstDetail"] = from_str(self.firstDetail) + result["sequencesFixed"] = self.sequencesFixed + return result + + +class Either(ABC): + @abstractmethod + def isLeft(self): + pass + + @abstractmethod + def isRight(self): + pass + + @abstractmethod + def getLeft(self): + pass + + @abstractmethod + def getRight(self): + pass + + @abstractmethod + def mapLeft(self, lf): + pass + + @abstractmethod + def mapRight(self, rf): + pass + + @abstractmethod + def either(self, leftF, rightF): + pass + + +class Left(Either): + def __init__(self, lvalue): + self.lvalue = lvalue + + def isLeft(self): + return True + + def isRight(self): + return False + + def getLeft(self): + return self.lvalue + + def getRight(self): + raise Error("Cannot get a right value out of Left.") + + def mapLeft(self, lf): + return Left(lf(self.lvalue)) + + def mapRight(self, rf): + return Left(self.lvalue) + + def either(self, leftF, rightF): + return leftF(self.lvalue) + + +class Right(Either): + def __init__(self, rvalue): + self.rvalue = rvalue + + def isLeft(self): + return False + + def isRight(self): + return True + + def getLeft(self): + raise Error("Cannot get a left value out of Right.") + + def getRight(self): + return self.rvalue + + def mapLeft(self, lf): + return Right(self.rvalue) + + def mapRight(self, rf): + return Right(rf(self.rvalue)) + + def either(self, leftF, rightF): + return rightF(self.rvalue) + + +class CoreDict(dict): + def isEquivalentByKeys(self, checkDict) -> bool: + print(checkDict) + for key in self: + value = checkDict.get(key) + if value is None: + return False + + return True + + def isMatchByKeys(self, checkList): + if len(self) != len(checkList): + return False + + sorted_dict_keys = sorted(self.keys()) + sorted_list_elements = sorted(checkList) + + if sorted_dict_keys != sorted_list_elements: + missing_keys = [ + key for key in sorted_list_elements if key not in sorted_dict_keys + ] + print(f"Отсутствующие ключи в словаре: {missing_keys}") + return False + + return True + + +class CoreList(List): + def onlyTrue(self) -> bool: + for el in self: + if el is True: + return True + return False + + def onlyUniqueElementAppend(self, el): + if el is None: + return + if self.is_element_in_array(el) is False: + self.append(el) + pass + + def is_element_in_array(self, element): + return element in self + + def equal(self, array: list) -> bool: + if len(self) != len(array): + return False + return self.sort() == array.sort() + + def isConstrainsString(self) -> bool: + for el in self: + if isinstance(el, str): + return True + return False + + def indexedPriorities(self, lowerPriority, highPriority) -> bool: + try: + lowerIndex = self.index(lowerPriority) + highPriorityIndex = self.index(highPriority) + return lowerIndex < highPriorityIndex + except: + return False + + def getAllString(self) -> list[str]: + return list(filter(lambda x: isinstance(x, str), self)) + + def onlyUnique(self) -> list: + result = [] + [result.append(x) for x in self if x not in self] + return result + + def spreadArray(self) -> "CoreList": + unpacked_array = CoreList([]) + for el in self: + if isinstance(el, list): + unpacked_array.extend(el) + else: + unpacked_array.append(el) + return unpacked_array + + +def isInListRange(listIn, index): + try: + listIn[index] + return False + except: + return True + + +class AllSequences: + all_sequences = None + adj_matrix = None + topologyIds = None + adj_matrix_names = None + + def __init__(self, adj_matrix, restrictions: list[str]) -> None: + self.adj_matrix = adj_matrix + self.all_possible_sequences(self.adj_matrix) + self.matrix_by_name() + if restrictions.__len__() != 0: + self.restrictionsValidate(restrictions) + pass + + def restrictionsValidate(self, restrictions: CoreList[str]): + filterMatrix = CoreList() + + for el in self.adj_matrix_names: + result = False + for restraint in restrictions: + result = CoreList(el).indexedPriorities(restraint[0], restraint[1]) + if result: + filterMatrix.onlyUniqueElementAppend(el) + self.adj_matrix_names = filterMatrix + pass + + def matrix_by_name(self): + result = self.all_sequences + inc = 0 + for matrix in self.all_sequences: + for index in range(len(matrix)): + result[inc][index] = CoreList( + filter( + lambda el: el.get("number") == matrix[index] + 1, + self.topologyIds, + ) + )[0].get("name") + inc += 1 + self.adj_matrix_names = result + pass + + def find_all_sequences(self, adj_matrix): + sequences = [] + num_vertices = len(adj_matrix) + + def dfs(vertex, sequence): + sequence.append(vertex) + + if len(sequence) == num_vertices: + sequences.append(sequence) + return + + for i in range(num_vertices): + if adj_matrix[vertex][i] == 1 and i not in sequence: + dfs(i, sequence.copy()) + + for i in range(num_vertices): + dfs(i, []) + + self.all_sequences = sequences + + def findId(self, listMatrix, id): + def filter_odd_num(in_num): + if in_num["name"] == id: + return True + else: + return False + + return list(filter(filter_odd_num, listMatrix))[0]["number"] + + def iter_paths(self, adj, min_length=6, path=None): + if not path: + for start_node in range(len(adj)): + yield from self.iter_paths(adj, min_length, [start_node]) + else: + if len(path) >= min_length: + yield path + if path[-1] in path[:-1]: + return + current_node = path[-1] + for next_node in range(len(adj[current_node])): + if adj[current_node][next_node] == 1: + yield from self.iter_paths(adj, min_length, path + [next_node]) + + def allUnique(self, x): + seen = set() + return not any(i in seen or seen.add(i) for i in x) + + def all_possible_sequences(self, matrix): + topologyIds = [] + topologyMatrixNumber = {} + inc = 0 + for k, v in matrix.items(): + inc += 1 + topologyIds.append({"name": k, "number": inc}) + inc = 0 + for k, v in matrix.items(): + inc += 1 + topologyMatrixNumber[inc] = list( + map(lambda el: self.findId(topologyIds, el), v) + ) + self.topologyIds = topologyIds + adj = [] + matrixSize = matrix.keys().__len__() + inc = 0 + for k, v in topologyMatrixNumber.items(): + adj.append(list(repeat(0, matrixSize))) + for el in v: + adj[inc][el - 1] = 1 + inc += 1 + return self.find_all_sequences(adj) + + +class VectorModel: + x: float + y: float + z: float + + def __init__(self, cadVector) -> None: + self.x = cadVector[0] + self.y = cadVector[1] + self.z = cadVector[2] + pass + + def toFreeCadVector(self): + return App.Vector(self.x, self.y, self.z) + + def toString(self): + return str("x:" + str(self.x) + "y:" + str(self.y) + "z:" + str(self.z)) + + +class FreeCadRepository: + _solids = [] + + def openDocument(self, path: str): + App.open("" + path) + + def closeIfOpenDocument(self): + # print('Проверка на документ') + try: + if App.ActiveDocument is not None: + # print(App.ActiveDocument.name + "закрыт") + # App.closeDocument(App.ActiveDocument.name) + App.ActiveDocument.clearDocument() + except Exception as e: + print(e) + + def getAllLabelsSolids(self) -> List[str]: + return list(map(lambda el: el.Label, self.getAllSolids())) + + def isAllObjectsSolids(self) -> List[str]: + result = [] + for part in App.ActiveDocument.Objects: + if self.is_object_solid(part) is False: + result.append(part.Label) + return result + + def objectSetPosition(self, solid, cadVector): + solid.Placement.Base = cadVector + pass + + def objectGetPosition(self, solid) -> VectorModel: + return VectorModel(cadVector=solid.Placement.Base) + + def isObjectIntersections(self, part) -> str: + result = [] + for solid in self.getAllSolids(): + if solid.ID != part.ID: + collisionResult: int = int(part.Shape.distToShape(solid.Shape)[0]) + if collisionResult == 0: + result.append(solid.Label) + if result.__len__() == 0: + return None + return result + + def objectHasTouches(self, part, solidBodyPadding: float) -> List[str]: + try: + positionVector = self.objectGetPosition(part) + result = CoreList() + result.append(self.isObjectIntersections(part=part)) + if solidBodyPadding != 0 and solidBodyPadding != None: + result.append( + self.axis_movement_and_intersections_observer( + positionVector=positionVector, + alongAxis="x", + solidBodyPadding=solidBodyPadding, + part=part, + ) + ) + result.append( + self.axis_movement_and_intersections_observer( + positionVector=positionVector, + alongAxis="y", + solidBodyPadding=solidBodyPadding, + part=part, + ) + ) + result.append( + self.axis_movement_and_intersections_observer( + positionVector=positionVector, + alongAxis="z", + solidBodyPadding=solidBodyPadding, + part=part, + ) + ) + spreadArr = result.spreadArray() + if spreadArr.isConstrainsString(): + return spreadArr.getAllString() + return None + except Exception as error: + print(error) + return None + + def axis_movement_and_intersections_observer( + self, + positionVector: VectorModel, + alongAxis: str, + solidBodyPadding: float, + part, + ) -> bool: + result = CoreList() + # UP + positionVector.__setattr__( + alongAxis, + positionVector.__getattribute__(alongAxis) + solidBodyPadding, + ) + self.objectSetPosition(part, positionVector.toFreeCadVector()) + # result.onlyUniqueElementAppend(self.isObjectIntersections(part=part)) + result.extend(self.isObjectIntersections(part=part)) + # RESET UP CHANGES + positionVector.__setattr__( + alongAxis, + positionVector.__getattribute__(alongAxis) - solidBodyPadding, + ) + self.objectSetPosition(part, positionVector.toFreeCadVector()) + + # DOWN + positionVector.__setattr__( + alongAxis, + positionVector.__getattribute__(alongAxis) - solidBodyPadding, + ) + self.objectSetPosition(part, positionVector.toFreeCadVector()) + # CHECK DOWN INTERSECTIONS + # result.onlyUniqueElementAppend(self.isObjectIntersections(part=part)) + result.extend(self.isObjectIntersections(part=part)) + + # RESET DOWN CHANGES + positionVector.__setattr__( + alongAxis, + positionVector.__getattribute__(alongAxis) + solidBodyPadding, + ) + self.objectSetPosition(part, positionVector.toFreeCadVector()) + if result.__len__() == 0: + return None + + return result.onlyUnique() + + def getAllSolids(self): + if self._solids.__len__() == 0: + for part in App.ActiveDocument.Objects: + if self.is_object_solid(part): + self._solids.append(part) + + return self._solids + + def is_object_solid(self, obj): + if not isinstance(obj, App.DocumentObject): + return False + if hasattr(obj, "Group"): + return False + + if not hasattr(obj, "Shape"): + return False + if not hasattr(obj.Shape, "Solids"): + return False + + if len(obj.Shape.Solids) == 0: + return False + + return True + + +T = TypeVar("T") + + +def from_list(f: Callable[[Any], T], x: Any) -> List[T]: + assert isinstance(x, list) + return [f(y) for y in x] + + +def from_str(x: Any) -> str: + assert isinstance(x, str) + return x + + +def from_dict(f: Callable[[Any], T], x: Any) -> Dict[str, T]: + assert isinstance(x, dict) + return {k: f(v) for (k, v) in x.items()} + + +def to_class(c: Type[T], x: Any) -> dict: + assert isinstance(x, c) + return cast(Any, x).to_dict() + + +# Вспомогательный класс который делает генрацию JSON на основе пайтон обьектов + + +class AdjacencyMatrix: + matrixError: Dict[str, str] = {} + all_parts: List[str] + first_detail: str + matrix: Dict[str, List[str]] + + fileName = "adjacency_matrix.json" + + def __init__( + self, all_parts: List[str], first_detail: str, matrix: Dict[str, List[str]] + ) -> None: + self.all_parts = all_parts + self.first_detail = first_detail + self.matrix = matrix + self.validateMatrix() + + def matrixToFileSystem(self, path: str): + FileSystemRepository.writeFile( + json.dumps(self.to_dict(), ensure_ascii=False, indent=4), + path, + AdjacencyMatrix.fileName, + ) + pass + + def sequencesToFileSystem(self, path: str, restrictions: list[str]): + FileSystemRepository.writeFile( + json.dumps( + {"sequences": AllSequences(self.matrix, restrictions).adj_matrix_names}, + ensure_ascii=False, + indent=4, + ), + path, + "sequences.json", + ), + pass + + def matrixGetUniqueContact(self): + detailsToCheck = [] + detailsHashCheck = {} + for k, v in self.matrix.items(): + for el in v: + if el != k: + hash = to_ascii_hash(k + el) + if detailsHashCheck.get(hash) == None: + detailsHashCheck[hash] = hash + detailsToCheck.append({"child": el, "parent": k}) + return detailsToCheck + + def whatPlaceLeadingPartIndex(self): + i = 0 + for el in self.matrix: + if el == self.first_detail: + return i + i = +1 + + def validateMatrix(self): + for el in self.all_parts: + if self.matrix.get(el) == None: + self.matrixError[el] = "Not found adjacency " + el + + @staticmethod + def from_dict(obj: Any) -> "AdjacencyMatrix": + assert isinstance(obj, dict) + + all_pars = from_list(from_str, obj.get("allParts")) + first_detail = from_str(obj.get("firstDetail")) + matrix = from_dict(lambda x: from_list(from_str, x), obj.get("matrix")) + + return AdjacencyMatrix(all_pars, first_detail, matrix) + + def to_dict(self) -> dict: + result: dict = {} + result["allParts"] = from_list(from_str, self.all_parts) + result["firstDetail"] = from_str(self.first_detail) + result["matrix"] = from_dict(lambda x: from_list(from_str, x), self.matrix) + if self.matrixError.values().__len__() == 0: + result["matrixError"] = None + else: + result["matrixError"] = self.matrixError + return result + + def getDictMatrix(self) -> dict: + result = {} + + for k, v in self.matrix.items(): + result[k] = {} + for el in v: + result[k][el] = el + + return result + + +def adjacency_matrix_from_dict(s: Any) -> AdjacencyMatrix: + return AdjacencyMatrix.from_dict(s) + + +def adjacency_matrix_to_dict(x: AdjacencyMatrix) -> Any: + return to_class(AdjacencyMatrix, x) + + +# Вспомогательный класс для работы с Freecad + + +class FreeCadMetaModel(object): + def __init__(self, label, vertex) -> None: + self.label = label + self.vertex = vertex + + +collision_squares_labels = [] + + +class MeshGeometryCoordinateModel(object): + # Получение геометрии мешей + def __init__( + self, + x, + y, + z, + label, + ): + self.x = x + self.y = y + self.z = z + self.label = label + self.cadLabel = "" + + def initializePrimitivesByCoordinate(self, detailSquares): + uuidDoc = str(uuid.uuid1()) + App.ActiveDocument.addObject("Part::Box", "Box") + App.ActiveDocument.ActiveObject.Label = uuidDoc + App.ActiveDocument.recompute() + part = App.ActiveDocument.getObjectsByLabel(uuidDoc)[0] + collision_squares_labels.append(uuidDoc) + part.Width = 2 + part.Height = 2 + part.Length = 2 + part.Placement = App.Placement( + App.Vector(self.x - 1, self.y - 1, self.z - 1), + App.Rotation(App.Vector(0.00, 0.00, 1.00), 0.00), + ) + if detailSquares.get(self.label) is None: + detailSquares[self.label] = [] + detailSquares[self.label].append(self) + self.cadLabel = uuidDoc + App.ActiveDocument.recompute() + + +class FileSystemRepository: + def readJSON(path: str): + return json.loads((open(path)).read()) + + def writeFile(data, filePath, fileName): + file_to_open = filePath + fileName + + f = open(file_to_open, "w", encoding="utf8") + + f.write(data) + + def readFile(path: str): + return open(path).read() + + def readFilesTypeFolder(pathFolder: str, fileType=".json"): + filesJson = list( + filter( + lambda x: x[-fileType.__len__() :] == fileType, os.listdir(pathFolder) + ) + ) + return filesJson + + +class GetAllPartsLabelsUseCase: + # Получение всех названий деталей + def call(self): + parts = [] + for part in FreeCadRepository().getAllSolids(): + parts.append(part.Label) + return parts + + +def isUnique(array, element): + for i in array: + if i == element: + return False + + return True + + +class GetCollisionAtPrimitiveUseCase(object): + # Получение колизий примитивов + def call(self, freeCadMetaModels, detailSquares) -> Dict[str, List[str]]: + matrix: Dict[str, List[str]] = {} + for model in freeCadMetaModels: + activePart = App.ActiveDocument.getObjectsByLabel(model.label)[0] + for key in detailSquares: + if model.label != key: + for renderPrimitive in detailSquares[key]: + primitivePart = App.ActiveDocument.getObjectsByLabel( + renderPrimitive.cadLabel + )[0] + collisionResult: int = int( + activePart.Shape.distToShape(primitivePart.Shape)[0] + ) + if collisionResult == 0: + if matrix.get(model.label) == None: + matrix[model.label] = [renderPrimitive.label] + else: + if isUnique(matrix[model.label], renderPrimitive.label): + matrix[model.label].append(renderPrimitive.label) + return matrix + + +class GetFirstDetailUseCase: + # Получение первой детали + def call(self): + return FreeCadRepository().getAllSolids()[0].Label + + +class GetPartPrimitiveCoordinatesUseCase(object): + # Получение координат примитивов + def call(self, freeCadMetaModels): + meshCoordinates: list[MeshGeometryCoordinateModel] = [] + for model in freeCadMetaModels: + vertexesDetail = model.vertex + labelDetail = model.label + for coords in vertexesDetail: + detailVertex = MeshGeometryCoordinateModel( + coords.X, + coords.Y, + coords.Z, + labelDetail, + ) + meshCoordinates.append(detailVertex) + + return meshCoordinates + + +class InitPartsParseUseCase: + # Инициализация парсинга + def call(self): + product_details = [] + for part in FreeCadRepository().getAllSolids(): + if part is not None: + model = FreeCadMetaModel(part.Label, part.Shape.Vertexes) + if model is not None: + product_details.append(model) + return product_details + + +class RenderPrimitiveUseCase(object): + # Рендеринг премитивов + def call( + self, meshModels: list[MeshGeometryCoordinateModel], detailSquares + ) -> None: + for mesh in meshModels: + mesh.initializePrimitivesByCoordinate(detailSquares) + + +class ClearWorkSpaceDocumentUseCase(object): + # Очистка рабочего простарнства + def call(self, detailSquares): + for key in detailSquares: + for renderPrimitive in detailSquares[key]: + primitivePart = App.ActiveDocument.getObjectsByLabel( + renderPrimitive.cadLabel + )[0] + App.ActiveDocument.removeObject(primitivePart.Name) + + +class RenderPrimitivesScenario(object): + def __init__( + self, + initPartsParseUseCase: InitPartsParseUseCase, + getPartPrimitiveCoordinatesUseCase: GetPartPrimitiveCoordinatesUseCase, + renderPrimitiveUseCase: RenderPrimitiveUseCase, + getCollisionAtPrimitives: GetCollisionAtPrimitiveUseCase, + clearWorkSpaceDocument: ClearWorkSpaceDocumentUseCase, + ) -> None: + self.initPartsParseUseCase = initPartsParseUseCase + self.getPartPrimitiveCoordinatesUseCase = getPartPrimitiveCoordinatesUseCase + self.renderPrimitiveUseCase = renderPrimitiveUseCase + self.getCollisionAtPrimitives = getCollisionAtPrimitives + self.clearWorkSpaceDocument = clearWorkSpaceDocument + + def call(self) -> None: + meshCoordinates = [] + detailSquares = {} + parts = self.initPartsParseUseCase.call() + meshCoordinates = self.getPartPrimitiveCoordinatesUseCase.call(parts) + self.renderPrimitiveUseCase.call(meshCoordinates, detailSquares) + matrix = self.getCollisionAtPrimitives.call(parts, detailSquares) + self.clearWorkSpaceDocument.call(detailSquares) + return matrix + + +class ClearWorkSpaceDocumentUseCase(object): + # Очистака рабочего пространства + def call(self, detailSquares): + for key in detailSquares: + for renderPrimitive in detailSquares[key]: + primitivePart = App.ActiveDocument.getObjectsByLabel( + renderPrimitive.cadLabel + )[0] + App.ActiveDocument.removeObject(primitivePart.Name) + + +class CadAdjacencyMatrix: + # Матрица основанная на соприкосновении примитива с обьектами + def primitiveMatrix(self): + # Получение матрицы + matrix = RenderPrimitivesScenario( + InitPartsParseUseCase(), + GetPartPrimitiveCoordinatesUseCase(), + RenderPrimitiveUseCase(), + GetCollisionAtPrimitiveUseCase(), + ClearWorkSpaceDocumentUseCase(), + ).call() + return AdjacencyMatrix( + all_parts=GetAllPartsLabelsUseCase().call(), + first_detail=GetFirstDetailUseCase().call(), + matrix=matrix, + ) + + # Матрица основанная на соприкосновении обьектов + + def matrixBySurfaces(self): + matrix = {} + for part in FreeCadRepository().getAllSolids(): + matrix[part.Label] = [] + for nextPart in FreeCadRepository().getAllSolids(): + if part.Label != nextPart.Label: + # Вычисление соприконсоновения площади деталей + collisionResult: int = int( + part.Shape.distToShape(nextPart.Shape)[0] + ) + + if collisionResult == 0: + matrix[part.Label].append(nextPart.Label) + + return AdjacencyMatrix( + all_parts=GetAllPartsLabelsUseCase().call(), + first_detail=GetFirstDetailUseCase().call(), + matrix=matrix, + ) + + +def reduce(function, iterable, initializer=None): + it = iter(iterable) + if initializer is None: + value = next(it) + else: + value = initializer + for element in it: + value = function(value, element) + return value + + +def to_ascii_hash(text): + ascii_values = [ord(character) for character in text] + return reduce(lambda x, y: x + y, ascii_values) + + +class IntersectionComputedUseCase: + def call(parts): + App.activeDocument().addObject("Part::MultiCommon", "Common") + App.activeDocument().Common.Shapes = [parts[0], parts[1]] + App.activeDocument().getObject("Common").ViewObject.ShapeColor = getattr( + parts[0].getLinkedObject(True).ViewObject, + "ShapeColor", + App.activeDocument().getObject("Common").ViewObject.ShapeColor, + ) + App.activeDocument().getObject("Common").ViewObject.DisplayMode = getattr( + parts[0].getLinkedObject(True).ViewObject, + "DisplayMode", + App.activeDocument().getObject("Common").ViewObject.DisplayMode, + ) + App.ActiveDocument.recompute() + area = App.activeDocument().getObject("Common").Shape.Area + App.ActiveDocument.removeObject("Common") + return area + + +class ErrorStringModel: + def __init__(self, error: str) -> None: + self.error = error + pass + + error: str + + def toString(self) -> str: + return json.dumps( + { + "error": self.error, + }, + ensure_ascii=False, + indent=4, + ) + + def toFileSystem(self, path: str): + return ( + FileSystemRepository.writeFile(self.toString(), path, "error.json"), + ExitFreeCadUseCase.call(), + ) + + +class IsAllObjectSolidsCheckUseCase: + def call() -> Either: + result = FreeCadRepository().isAllObjectsSolids() + if result.__len__() == 0: + return Left(None) + + return Right( + ErrorStringModel(error="Is not solid objects: " + ",".join(result)) + ) + + +class CheckObjectHasTouchesUseCase: + freeCadRepository = FreeCadRepository() + + def call(self, solidBodyPadding: float) -> Either: + try: + errorResult = [] + matrix = {} + for part in self.freeCadRepository.getAllSolids(): + matrix[part.Label] = [] + touches = FreeCadRepository().objectHasTouches( + part=part, solidBodyPadding=solidBodyPadding + ) + matrix[part.Label].extend(touches) + if errorResult.__len__() == 0: + return Left( + AdjacencyMatrix( + all_parts=self.freeCadRepository.getAllLabelsSolids(), + first_detail=GetFirstDetailUseCase().call(), + matrix=matrix, + ) + ) + else: + return Right( + ErrorStringModel( + error="Solids bodies have no recounts: " + ",".join(errorResult) + ) + ) + except Exception as error: + print(error) + print("CheckObjectHasTouchesUseCase error") + return Right(ErrorStringModel(error="CheckObjectHasTouchesUseCase error")) + + +class CheckCadIntersectionObjects: + report = [] + + def call() -> bool: + FreeCadRepository().getAllSolids() + return False + + +class ExitFreeCadUseCase: + def call(): + import FreeCADGui as Gui + + App.ActiveDocument.clearDocument() + freecadQTWindow = Gui.getMainWindow() + freecadQTWindow.close() + + +# class CheckValidIntersectionUseCase: +# def call() -> ErrorStringModel: +# for part in FreeCadRepository().getAllSolids(): +# print(part) +# FreeCadRepository().obj +# pass + + +class EnvReaderUseCase: + def call() -> Either: + try: + return Left(Env.from_dict(FileSystemRepository.readJSON("env.json"))) + except: + print("env reader error") + return Right(None) + + +class OpenFreeCadDocumentUseCase: + def call(path: str) -> Either: + try: + FreeCadRepository().closeIfOpenDocument() + FreeCadRepository().openDocument(path) + return Left(None) + except: + print("OpenFreeCadDocumentUseCase error") + return Right(None) + + +class IntersectionGeometryUseCase: + def call(contacts, path): + intersection_geometry = {"status": True, "recalculations": None} + for el in contacts: + child = App.ActiveDocument.getObjectsByLabel(el.get("child"))[0] + parent = App.ActiveDocument.getObjectsByLabel(el.get("parent"))[0] + area = IntersectionComputedUseCase.call([child, parent]) + if area != 0.0: + if intersection_geometry.get("recalculations") == None: + intersection_geometry["status"] = False + intersection_geometry["recalculations"] = [] + intersection_geometry["recalculations"].append( + {"area": area, "connect": el.get("child") + " " + el.get("parent")} + ) + FileSystemRepository.writeFile( + json.dumps(intersection_geometry, ensure_ascii=False, indent=4), + path, + "intersection_geometry.json", + ) + + +def main(): + try: + EnvReaderUseCase.call().either( + leftF=lambda environment: ( + OpenFreeCadDocumentUseCase.call(environment.cadFilePath).either( + leftF=lambda _: ( + IsAllObjectSolidsCheckUseCase.call().either( + leftF=lambda _: ( + CheckObjectHasTouchesUseCase() + .call(environment.solidBodyPadding) + .either( + leftF=lambda adjaxedMatrix: ( + adjaxedMatrix.sequencesToFileSystem( + environment.outPath, + environment.sequencesFixed, + ), + IntersectionGeometryUseCase.call( + adjaxedMatrix.matrixGetUniqueContact(), + environment.outPath, + ), + adjaxedMatrix.matrixToFileSystem( + environment.outPath, + ), + ExitFreeCadUseCase.call(), + ), + rightF=lambda error: error.toFileSystem( + environment.outPath + ), + ), + ), + rightF=lambda error: error.toFileSystem( + environment.outPath + ), + ), + ), + rightF=lambda error: print(error), + ), + ), + rightF=lambda error: print(error), + ) + + except Exception as error: + print(error) + ExitFreeCadUseCase.call() + + +# main() + + +class ReadFileSystemAndGetInstanceModelUseCase: + def call(self, model, path): + if hasattr(model, "from_dict") is False: + return Right( + "ReadFileSystemAndGetInstanceModelUseCase error:" + + model + + "is not have method" + + "from_dict()" + ) + try: + return Left(model.from_dict(FileSystemRepository.readJSON(path))) + except: + error = str(model) + " " + "from dict error " + "path: " + path + print("ReadFileSystemAndGetInstanceModelUseCase error" + error) + return Right(error) + pass + + +class FreeCadTest: + testName: str + + def testHelper(self, testResult): + if isinstance(testResult, bool) is False: + print( + BackgroundConsoleColors.WARNING, + self.testName + + " expected a value of type Boolean, returned a value of type below ", + ) + print(testResult) + return + if testResult: + print(BackgroundConsoleColors.OKGREEN, self.testName + "is complete!") + else: + print(BackgroundConsoleColors.FAIL, self.testName + " is Error!") + pass + + +class FreeCadASPGenerationTestController(FreeCadTest): + testName: str + + def __init__(self, testName: str) -> None: + self.testName = testName + pass + + def test( + self, + assertFn, + documentPath: str, + modelName: str, + model, + execComposition, + outPath=os.path.dirname(__file__) + "/out/", + ): + try: + OpenFreeCadDocumentUseCase.call(documentPath).either( + leftF=lambda _: ( + execComposition(""), + ReadFileSystemAndGetInstanceModelUseCase() + .call(model=model, path=outPath + modelName) + .either( + leftF=lambda model: ( + self.testHelper( + assertFn(model), + ) + ), + rightF=lambda error: print(error), + ), + ), + rightF=lambda error: print(error), + ) + except Exception as inst: + print("FreeCadASPGenerationTestController error") + print(inst) + pass + + +def test(): + try: + mocksFolder = os.path.dirname(__file__) + "/mocks/" + outFolder = os.path.dirname(__file__) + "/out/" + + # FreeCadASPGenerationTestController("test adjaxed matrix simple cube").test( + # assertFn=lambda model: CoreList(model.all_parts).equal(["Куб", "Куб001"]), + # execComposition=lambda _: ( + # CheckObjectHasTouchesUseCase() + # .call(0) + # .either( + # leftF=lambda matrix: matrix.matrixToFileSystem(outFolder), + # rightF=lambda error: print(error), + # ) + # ), + # documentPath=mocksFolder + "simple_assembly_with_two_cubes.FCStd", + # modelName=AdjacencyMatrix.fileName, + # model=AdjacencyMatrix, + # ) + + FreeCadASPGenerationTestController( + "test adjaxed matrix vs structure of document" + ).test( + assertFn=lambda model: CoreDict(model.matrix).isEquivalentByKeys( + { + "Бутылочный домкрат 4т_Гильза": [ + "Бутылочный домкрат 4т_Тяга", + "Бутылочный домкрат 4т_Тяга001", + "Бутылочный домкрат 4т_Шток насоса", + "Бутылочный домкрат 4т_Шток", + "Бутылочный домкрат 4т_Вентиль", + ], + "Бутылочный домкрат 4т_Тяга": [ + "Бутылочный домкрат 4т_Гильза", + "Бутылочный домкрат 4т_Тяга001", + "Бутылочный домкрат 4т_Коромысло", + ], + "Бутылочный домкрат 4т_Тяга001": [ + "Бутылочный домкрат 4т_Гильза", + "Бутылочный домкрат 4т_Тяга", + "Бутылочный домкрат 4т_Коромысло", + ], + "Бутылочный домкрат 4т_Шток насоса": [ + "Бутылочный домкрат 4т_Гильза", + "Бутылочный домкрат 4т_Коромысло", + ], + "Бутылочный домкрат 4т_Коромысло": [ + "Бутылочный домкрат 4т_Тяга", + "Бутылочный домкрат 4т_Тяга001", + "Бутылочный домкрат 4т_Шток насоса", + ], + "Бутылочный домкрат 4т_Шток": [ + "Бутылочный домкрат 4т_Гильза", + "Бутылочный домкрат 4т_Винт штока", + ], + "Бутылочный домкрат 4т_Винт штока": ["Бутылочный домкрат 4т_Шток"], + "Бутылочный домкрат 4т_Вентиль": ["Бутылочный домкрат 4т_Гильза"], + } + ), + execComposition=lambda _: ( + CheckObjectHasTouchesUseCase() + .call(0) + .either( + leftF=lambda matrix: matrix.matrixToFileSystem(outFolder), + rightF=lambda error: print(error), + ) + ), + documentPath=mocksFolder + "bottle_jack.FCStd", + modelName=AdjacencyMatrix.fileName, + model=AdjacencyMatrix, + ) + + FreeCadASPGenerationTestController( + "test adjacency matrix keys vs allparts" + ).test( + assertFn=lambda model: CoreDict(model.matrix).isMatchByKeys( + ["Куб", "Куб001"] + ), + execComposition=lambda _: ( + CheckObjectHasTouchesUseCase() + .call(0) + .either( + leftF=lambda matrix: matrix.matrixToFileSystem(outFolder), + rightF=lambda error: print(error), + ) + ), + documentPath=mocksFolder + "simple_assembly_with_two_cubes.FCStd", + modelName=AdjacencyMatrix.fileName, + model=AdjacencyMatrix, + ) + + ExitFreeCadUseCase.call() + except: + print("test error") + ExitFreeCadUseCase.call() + pass + + +# test() + diff --git a/freecad_workbench/freecad/robossembler/importFreecadUsecase.py b/freecad_workbench/freecad/robossembler/importFreecadUsecase.py deleted file mode 100644 index 3ed4fae..0000000 --- a/freecad_workbench/freecad/robossembler/importFreecadUsecase.py +++ /dev/null @@ -1,81 +0,0 @@ -import FreeCAD -import Part -import ImportGui - -class GeometryValidateUseCase: - def __init__(self): - self.doc = None - - - - - def import_step_and_check(self, file_path): - - FreeCAD.closeDocument("Unnamed") - - - self.doc = FreeCAD.newDocument("Unnamed") - - - ImportGui.open(file_path, "Unnamed") - - - FreeCAD.ActiveDocument.recompute() - Gui.SendMsgToActiveView("ViewFit") - - - bodies = getBodies(self.doc) - - - intersections = self.check_intersections(bodies) - self.print_intersections(intersections) - - - zero_volume_bodies = self.check_zero_volume(bodies) - self.print_zero_volume(zero_volume_bodies) - - - self.save_checked_document(file_path) - - - def getBodies(doc): - bodies = [] - for obj in doc.Objects: - if hasattr(obj, 'TypeId') and obj.TypeId == "Part::Feature": - bodies.append(obj) - return bodies - - - - def check_intersections(self, bodies): - intersections = [] - for i in range(len(bodies)): - for j in range(i + 1, len(bodies)): - if bodies[i].intersects(bodies[j]): - intersections.append((i + 1, j + 1)) - return intersections - - def check_zero_volume(self, bodies): - zero_volume_bodies = [i + 1 for i, body in enumerate(bodies) if body.Volume == 0] - return zero_volume_bodies - - def print_intersections(self, intersections): - for i, j in intersections: - print("Тела пересекаются: Body {} и Body {}".format(i, j)) - - def print_zero_volume(self, zero_volume_bodies): - for i in zero_volume_bodies: - print("Тело {} имеет нулевой объем".format(i)) - - def save_checked_document(self, original_file_path): - checked_file_path = original_file_path.replace(".step", "_checked.FCStd") - FreeCAD.saveDocument(self.doc, checked_file_path) - print("Проверенная сборка сохранена в файл:", checked_file_path) - FreeCAD.closeDocument("Unnamed") - - - -use_case = GeometryValidateUseCase() -step_file_path = '/path/to/file' -use_case.import_step_and_check(step_file_path) - diff --git a/freecad_workbench/freecad/robossembler/init_gui.py b/freecad_workbench/freecad/robossembler/init_gui.py index 5cdbc87..ef93952 100644 --- a/freecad_workbench/freecad/robossembler/init_gui.py +++ b/freecad_workbench/freecad/robossembler/init_gui.py @@ -49,13 +49,7 @@ class Robossembler(Gui.Workbench): "Export_Entities", "ExportGazeboModels", "InsertGraspPose", - # "ASM4StructureParsing", - "PDDL_CreateTypes", - "PDDL_CreateParameters", - "PDDL_CreateAction", - "PDDL_CreatePredicate", - "PDDL_CreateDurativeAction", - + "Validate_Project", "Publish_Project" ] self.appendToolbar(f"{__class__.__name__} Frames", self.framecommands) diff --git a/freecad_workbench/freecad/robossembler/is_object_solid.py b/freecad_workbench/freecad/robossembler/is_object_solid.py deleted file mode 100644 index 1d69d3a..0000000 --- a/freecad_workbench/freecad/robossembler/is_object_solid.py +++ /dev/null @@ -1,29 +0,0 @@ -# coding: utf-8 -# Copyright (C) 2023 Ilia Kurochkin -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -''' -DESCRIPTION. -Simple FreeCAD's object test for manifold mawater-tight surface. -''' - -import FreeCAD - - -def is_object_solid(obj): - '''If obj is solid return True''' - if not isinstance(obj, FreeCAD.DocumentObject): - return False - - if not hasattr(obj, 'Shape'): - return False - - return obj.Shape.isClosed() diff --git a/freecad_workbench/freecad/robossembler/markupEntities.py b/freecad_workbench/freecad/robossembler/markup_entities.py similarity index 100% rename from freecad_workbench/freecad/robossembler/markupEntities.py rename to freecad_workbench/freecad/robossembler/markup_entities.py diff --git a/freecad_workbench/freecad/robossembler/poseGenerator.py b/freecad_workbench/freecad/robossembler/pose_generator.py similarity index 100% rename from freecad_workbench/freecad/robossembler/poseGenerator.py rename to freecad_workbench/freecad/robossembler/pose_generator.py diff --git a/freecad_workbench/freecad/robossembler/project_validator.py b/freecad_workbench/freecad/robossembler/project_validator.py new file mode 100644 index 0000000..e9913f1 --- /dev/null +++ b/freecad_workbench/freecad/robossembler/project_validator.py @@ -0,0 +1,184 @@ +import FreeCAD as App +import unicodedata +import numpy as np +from .helper.is_solid import is_object_solid + +## Программа, содержащая все проверки активного проекта FreeCAD в соответствии с issue#149 + +doc = App.ActiveDocument +Console = App.Console + + +#========== Проверка имен =============== +class FormalValidator: + def __init__(self, document): + self.document = document + + def is_unicode_safe(self, name): + normalized_name = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore').decode() + return name == normalized_name + + def fix_invalid_names(self): + for obj in self.document.Objects: + if not self.is_unicode_safe(obj.Label): + safe_name = unicodedata.normalize('NFKD', obj.Label).encode('ASCII', 'ignore').decode() + obj.Label = safe_name + self.document.recompute() + + #запуск проверки и исправления имен файлов + def validate(self): + invalid_names = [obj.Label for obj in self.document.Objects if not self.is_unicode_safe(obj.Label)] + if invalid_names: + print("Некорректные имена:") + for name in invalid_names: + print(name) + self.fix_invalid_names() + else: + print("Все имена корректны.") + + +class ModelValidator: + def __init__(self, document): + self.document = document + self.partlist = [obj for obj in self.document.Objects if (is_object_solid(obj) and not obj.TypeId == 'App::Part') ] + self.material_objects = [obj for obj in document.Objects if obj.TypeId == "App::MaterialObjectPython"] + + def get_parts(self): + pass + + + + def validate_geometry(self): + for obj in self.document.Objects: + if hasattr(obj, "Shape") and not is_object_solid(obj): + Console.PrintError(f"Объект '{obj.Name}' не является твердым телом.\n") + + def is_intersecting(self, obj1, obj2): + if obj1.Shape.common(obj2.Shape).Volume > 0.0: + return True + + def has_allow_intersections(self, obj): + if hasattr(obj, 'Base_Allowed_Intersection') and getattr(obj, 'Base_Allowed_Intersection', True): + return True + + def check_bodies_for_intersections(self): + bodies = self.partlist + num_bodies = len(bodies) + + for i in range(num_bodies): + for j in range(i + 1, num_bodies): + body1 = bodies[i] + body2 = bodies[j] + if self.is_intersecting(body1, body2): + if self.has_allow_intersections(body1) or self.has_allow_intersections(body2): + Console.PrintWarning( + f"Тела '{body1.Label}' и '{body2.Label}' ""предусмотренно пересекаются.\n") + else: + Console.PrintError( + f"Тела '{body1.Label}' и '{body2.Label}' ""непредусмотренно пересекаются.\n") + print('V = ' + str(body1.Shape.common(body2.Shape).Volume)) + + + def get_adjacency_matrix(self, padding = 1): + # print(int(padding)) + bodies = self.partlist + num_bodies = len(bodies) + adjacency_matrix = np.zeros((num_bodies, num_bodies), dtype=bool) + + for i in range(num_bodies): + shape1 = bodies[i].Shape + for j in range(i + 1, num_bodies): + shape2 = bodies[j].Shape + + if shape1.common(shape2).Volume > 0: + continue + + distance = shape1.distToShape(shape2) + if distance[0] < padding: + adjacency_matrix[i][j] = 1 + adjacency_matrix[j][i] = 1 + + return adjacency_matrix + + def check_bodies_without_contacts(self): + adjacency_matrix = self.get_adjacency_matrix() + bodies = self.partlist + + isolated_bodies = [] + num_bodies = len(bodies) + + for i in range(num_bodies): + # Проверяем, есть ли у тела касания с другими телами + if not any(adjacency_matrix[i]): + isolated_bodies.append(bodies[i].Label) + + if isolated_bodies: + App.Console.PrintWarning("Следующие тела не касаются других:\n") + for label in isolated_bodies: + App.Console.PrintWarning(f"- {label}\n") + else: + App.Console.PrintMessage("Все тела имеют контакты.\n") + + + def find_material_objects(document): + pass + + + + def check_bodies_with_multiple_materials(self): + material_objects = self.material_objects + material_references = {} + + for material in material_objects: + if hasattr(material, "References"): + for reference in material.References: + if reference in material_references: + material_references[reference].append(material.Label) + else: + material_references[reference] = [material.Label] + + conflicts = {k: v for k, v in material_references.items() if len(v) > 1} + + if conflicts: + App.Console.PrintWarning("Обнаружены конфликты между объектами материалов:\n") + for ref, materials in conflicts.items(): + App.Console.PrintWarning(f"Деталь '{ref[0].Label}' используется в материалах: {', '.join(materials)}\n") + else: + App.Console.PrintMessage("Конфликтов в ссылках материалов не обнаружено.\n") + + def check_bodies_without_material(self): + material_objects = self.material_objects + partlist = self.partlist + referenced_bodies = [] + for material in material_objects: + if hasattr(material, "References"): + for reference in material.References: + referenced_bodies.append(reference[0].Label) + + bodies_without_material = [] + for part in partlist: + if part.Label not in referenced_bodies: + bodies_without_material.append(part.Label) + + if bodies_without_material: + App.Console.PrintWarning("Следующие тела не имеют назначенного материала:\n") + for name in bodies_without_material: + App.Console.PrintWarning(f"- {name}\n") + else: + App.Console.PrintMessage("Все тела имеют назначенные материалы.\n") + + + + def validate(self): + self.validate_geometry() + self.check_bodies_for_intersections() + self.check_bodies_without_contacts() + self.check_bodies_without_material() + self.check_bodies_with_multiple_materials() + +def validate_project(): + doc = App.ActiveDocument + FormalValidator(doc).validate() + ModelValidator(doc).validate() + + diff --git a/freecad_workbench/freecad/update_workbench.sh b/freecad_workbench/freecad/update_workbench.sh new file mode 100755 index 0000000..3ac11df --- /dev/null +++ b/freecad_workbench/freecad/update_workbench.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Укажите путь к папке, которую нужно скопировать +SOURCE_DIR="/home/markvoltov/GitProjects/framework/freecad_workbench/freecad/robossembler" + +# Укажите путь к папке, которую нужно заменить +DEST_DIR="/home/markvoltov/.local/share/FreeCAD/Mod/freecad_workbench" + +# Проверка, что исходная папка существует +if [ ! -d "$SOURCE_DIR" ]; then + echo "Исходная папка не существует: $SOURCE_DIR" + exit 1 +fi + +# Удаление содержимого папки назначения +if [ -d "$DEST_DIR" ]; then + echo "Удаление содержимого папки назначения: $DEST_DIR" + rm -rf "$DEST_DIR/*" +else + # Создание папки назначения, если её нет + echo "Создание папки назначения: $DEST_DIR" + mkdir -p "$DEST_DIR" +fi + +# Копирование содержимого исходной папки в папку назначения +echo "Копирование содержимого из $SOURCE_DIR в $DEST_DIR" +cp -r "$SOURCE_DIR/." "$DEST_DIR/" + +echo "Копирование завершено" diff --git a/test_models/test_reductor.20240510-053606.FCBak b/test_models/test_reductor.20240510-053606.FCBak new file mode 100644 index 0000000..b57c1a1 Binary files /dev/null and b/test_models/test_reductor.20240510-053606.FCBak differ diff --git a/test_models/test_reductor.FCStd b/test_models/test_reductor.FCStd index 970e675..302c1f3 100644 Binary files a/test_models/test_reductor.FCStd and b/test_models/test_reductor.FCStd differ