diff --git a/doc/changelog.d/1753.maintenance.md b/doc/changelog.d/1753.maintenance.md new file mode 100644 index 0000000000..97e5671828 --- /dev/null +++ b/doc/changelog.d/1753.maintenance.md @@ -0,0 +1 @@ +update CHANGELOG for v0.9.0 \ No newline at end of file diff --git a/doc/changelog.d/2153.added.md b/doc/changelog.d/2153.added.md new file mode 100644 index 0000000000..f4ff49de0c --- /dev/null +++ b/doc/changelog.d/2153.added.md @@ -0,0 +1 @@ +Tracking boolean operations diff --git a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py index fdc862eaf7..756cc64584 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py @@ -24,7 +24,9 @@ import grpc import pint +from ansys.geometry.core import USE_TRACKER_TO_UPDATE_DESIGN from ansys.geometry.core.errors import protect_grpc +from ansys.geometry.core.misc.auxiliary import get_design_from_body from ansys.geometry.core.misc.measurements import DEFAULT_UNITS from ..base.bodies import GRPCBodyService @@ -700,14 +702,21 @@ def boolean(self, **kwargs) -> dict: # noqa: D102 # Call the gRPC service and build the requests accordingly resp = 0 + serialized_tracker_response = {} try: - resp = self.stub.Boolean( - request=BooleanRequest( - body1=kwargs["target"].id, - tool_bodies=[other.id for other in kwargs["other"]], - method=kwargs["method"], + request = BooleanRequest( + body1=kwargs["target"].id, + tool_bodies=[other.id for other in kwargs["other"]], + method=kwargs["method"], + ) + if USE_TRACKER_TO_UPDATE_DESIGN: + request.keep_other = kwargs["keep_other"] + resp = self.stub.Boolean(request=request) + if USE_TRACKER_TO_UPDATE_DESIGN: + parent_design = get_design_from_body(kwargs["target"]) + serialized_tracker_response = parent_design._serialize_tracker_command_response( + resp.response ) - ).empty_result except grpc.RpcError as err: # pragma: no cover # TODO: to be deleted - old versions did not have "tool_bodies" in the request # This is a temporary fix to support old versions of the server - should be deleted @@ -734,15 +743,15 @@ def boolean(self, **kwargs) -> dict: # noqa: D102 body2=kwargs["other"][0].id, method=kwargs["method"], ) - ).empty_result + ) else: raise err - if resp == 1: + if resp.empty_result == 1: raise ValueError( f"Boolean operation of type '{kwargs['method']}' failed: {kwargs['err_msg']}.\n" f"Involving bodies:{kwargs['target']}, {kwargs['other']}" ) # Return the response - formatted as a dictionary - return {} + return {"complete_command_response": serialized_tracker_response} diff --git a/src/ansys/geometry/core/_grpc/_services/v0/repair_tools.py b/src/ansys/geometry/core/_grpc/_services/v0/repair_tools.py index 8c25d5c7f8..e88684f90f 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/repair_tools.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/repair_tools.py @@ -344,7 +344,7 @@ def find_and_fix_simplify(self, **kwargs) -> dict: # noqa: D102 # Call the gRPC service response = self.stub.FindAndSimplify(request) - serialized_tracker_response = self._serialize_tracker_command_response( + serialized_tracker_response = kwargs["parent_design"]._serialize_tracker_command_response( response.complete_command_response ) @@ -383,7 +383,7 @@ def find_and_fix_stitch_faces(self, **kwargs) -> dict: # noqa: D102 # Call the gRPC service response = self.stub.FindAndFixStitchFaces(request) - serialized_tracker_response = self._serialize_tracker_command_response( + serialized_tracker_response = kwargs["parent_design"]._serialize_tracker_command_response( response.complete_command_response ) @@ -467,7 +467,7 @@ def find_and_fix_short_edges(self, **kwargs): # noqa: D102 # Call the gRPC service response = self.stub.FindAndFixShortEdges(request) - serialized_tracker_response = self._serialize_tracker_command_response( + serialized_tracker_response = kwargs["parent_design"]._serialize_tracker_command_response( response.complete_command_response ) @@ -494,7 +494,7 @@ def find_and_fix_extra_edges(self, **kwargs) -> dict: # noqa: D102 # Call the gRPC service response = self.stub.FindAndFixExtraEdges(request) - serialized_tracker_response = self._serialize_tracker_command_response( + serialized_tracker_response = kwargs["parent_design"]._serialize_tracker_command_response( response.complete_command_response ) @@ -525,7 +525,7 @@ def find_and_fix_split_edges(self, **kwargs) -> dict: # noqa: D102 # Call the gRPC service response = self.stub.FindAndFixSplitEdges(request) - serialized_tracker_response = self._serialize_tracker_command_response( + serialized_tracker_response = kwargs["parent_design"]._serialize_tracker_command_response( response.complete_command_response ) diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index c40589f5fd..2fb1ffd77c 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -43,6 +43,7 @@ ShellRequest, ) from ansys.api.geometry.v0.commands_pb2_grpc import CommandsStub +from ansys.geometry.core import USE_TRACKER_TO_UPDATE_DESIGN from ansys.geometry.core.connection.client import GrpcClient from ansys.geometry.core.connection.conversions import ( plane_to_grpc_plane, @@ -1948,7 +1949,9 @@ def __generic_boolean_command( # If USE_TRACKER_TO_UPDATE_DESIGN is True, we serialize the response # and update the parent design with the serialized response. tracker_response = response.result.complete_command_response - serialized_response = self._serialize_tracker_command_response(tracker_response) + serialized_response = parent_design._serialize_tracker_command_response( + tracker_response + ) parent_design._update_from_tracker(serialized_response) @reset_tessellation_cache @@ -1962,20 +1965,26 @@ def __generic_boolean_op( err_msg: str, ) -> None: grpc_other = other if isinstance(other, Iterable) else [other] - if keep_other: - # Make a copy of the other body to keep it... - # stored temporarily in the parent component - since it will be deleted - grpc_other = [b.copy(self.parent_component, f"BoolOpCopy_{b.name}") for b in grpc_other] - - self._template._grpc_client.services.bodies.boolean( - target=self, - other=grpc_other, - method=method, - err_msg=err_msg, + if not USE_TRACKER_TO_UPDATE_DESIGN: + if keep_other: + # Make a copy of the other body to keep it... + # stored temporarily in the parent component - since it will be deleted + grpc_other = [ + b.copy(self.parent_component, f"BoolOpCopy_{b.name}") for b in grpc_other + ] + + response = self._template._grpc_client.services.bodies.boolean( + target=self, other=grpc_other, method=method, err_msg=err_msg, keep_other=keep_other ) - for b in grpc_other: - b.parent_component.delete_body(b) + if not USE_TRACKER_TO_UPDATE_DESIGN: + for b in grpc_other: + b.parent_component.delete_body(b) + else: + # If USE_TRACKER_TO_UPDATE_DESIGN is True, we serialize the response + # and update the parent design with the serialized response. + parent_design = get_design_from_body(self) + parent_design._update_from_tracker(response["complete_command_response"]) def __repr__(self) -> str: """Represent the ``Body`` as a string.""" diff --git a/src/ansys/geometry/core/designer/component.py b/src/ansys/geometry/core/designer/component.py index d1bee6cf09..f6c786bebc 100644 --- a/src/ansys/geometry/core/designer/component.py +++ b/src/ansys/geometry/core/designer/component.py @@ -245,6 +245,7 @@ def __init__( self._name = new_component.component.name self._instance_name = new_component.component.instance_name else: + new_component = None self._name = name self._id = None self._instance_name = instance_name @@ -277,8 +278,14 @@ def __init__( elif not read_existing_comp: # This is an independent Component - Create new Part and MasterComponent - p = Part(uuid.uuid4(), f"p_{name}", [], []) - master = MasterComponent(uuid.uuid4(), f"master_{name}", p) + p = Part( + uuid.uuid4() if not new_component else new_component.template, f"p_{name}", [], [] + ) + master = MasterComponent( + uuid.uuid4() if not new_component else new_component.component.master_id, + f"master_{name}", + p, + ) self._master_component = master self._master_component.occurrences.append(self) diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 670c1e73e7..6088235b7e 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -1027,6 +1027,55 @@ def __repr__(self) -> str: lines.append(f" N Design Points : {len(self.design_points)}") return "\n".join(lines) + def _serialize_tracker_command_response(self, response) -> dict: + """Serialize a TrackerCommandResponse object into a dictionary. + + Parameters + ---------- + response : TrackerCommandResponse + The gRPC TrackerCommandResponse object to serialize. + + Returns + ------- + dict + A dictionary representation of the TrackerCommandResponse object. + """ + + def serialize_body(body): + return { + "id": body.id, + "name": body.name, + "can_suppress": body.can_suppress, + "transform_to_master": { + "m00": body.transform_to_master.m00, + "m11": body.transform_to_master.m11, + "m22": body.transform_to_master.m22, + "m33": body.transform_to_master.m33, + }, + "master_id": body.master_id, + "parent_id": body.parent_id, + } + + def serialize_entity_identifier(entity): + """Serialize an EntityIdentifier object into a dictionary.""" + return { + "id": entity.id, + } + + return { + "success": response.success, + "created_bodies": [ + serialize_body(body) for body in getattr(response, "created_bodies", []) + ], + "modified_bodies": [ + serialize_body(body) for body in getattr(response, "modified_bodies", []) + ], + "deleted_bodies": [ + serialize_entity_identifier(entity) + for entity in getattr(response, "deleted_bodies", []) + ], + } + def __read_existing_design(self) -> None: """Read an existing ``Design`` located on the server.""" # @@ -1325,7 +1374,12 @@ def _handle_deleted_bodies(self, deleted_bodies): for body in self.bodies: if body.id == body_id: body._is_alive = False - self.bodies.remove(body) + # self.bodies.remove(body) + for bd in self._master_component.part.bodies: + if bd.id == body_id: + self._master_component.part.bodies.remove(bd) + break + self._clear_cached_bodies() removed = True self._grpc_client.log.info( f"Deleted body (ID: {body_id}) removed from root level." @@ -1352,10 +1406,12 @@ def _handle_created_bodies(self, created_bodies): ) continue - added = any(self._find_and_add_body(body_info, self.components)) + added = self._find_and_add_body(body_info, self.components) if not added: new_body = MasterBody(body_id, body_name, self._grpc_client, is_surface=is_surface) - self.bodies.append(new_body) + # self.bodies.append(new_body) + self._master_component.part.bodies.append(new_body) + self._clear_cached_bodies() self._grpc_client.log.debug( f"Added new body '{body_name}' (ID: {body_id}) to root level." ) @@ -1370,14 +1426,17 @@ def _update_body(self, existing_body, body_info): def _find_and_add_body(self, body_info, components): for component in components: - if component.id == body_info["parent_id"]: + parent_id_for_body = component._master_component.part.id + if parent_id_for_body == body_info["parent_id"]: new_body = MasterBody( body_info["id"], body_info["name"], self._grpc_client, is_surface=body_info.get("is_surface", False), ) - component.bodies.append(new_body) + # component.bodies.append(new_body) + component._master_component.part.bodies.append(new_body) + component._clear_cached_bodies() self._grpc_client.log.debug( f"Added new body '{new_body.name}' (ID: {new_body.id}) " f"to component '{component.name}' (ID: {component.id})" @@ -1407,11 +1466,17 @@ def _find_and_update_body(self, body_info, component): def _find_and_remove_body(self, body_info, component): for body in component.bodies: - if body.id == body_info["id"]: + body_info_id = body_info["id"] + if body.id == f"{component.id}/{body_info_id}": body._is_alive = False - component.bodies.remove(body) + # component.bodies.remove(body) + for bd in component._master_component.part.bodies: + if bd.id == body_info_id: + component._master_component.part.bodies.remove(bd) + break + component._clear_cached_bodies() self._grpc_client.log.debug( - f"Removed body '{body_info['name']}' (ID: {body_info['id']}) from component " + f"Removed body (ID: {body_info['id']}) from component " f"'{component.name}' (ID: {component.id})" ) return True diff --git a/src/ansys/geometry/core/tools/problem_areas.py b/src/ansys/geometry/core/tools/problem_areas.py index cac2d552c5..11cf85a01d 100644 --- a/src/ansys/geometry/core/tools/problem_areas.py +++ b/src/ansys/geometry/core/tools/problem_areas.py @@ -83,58 +83,6 @@ def fix(self): """Fix problem area.""" raise NotImplementedError("Fix method is not implemented in the base class.") - def _serialize_tracker_command_response(self, response) -> dict: - """Serialize a TrackerCommandResponse object into a dictionary. - - Parameters - ---------- - response : TrackerCommandResponse - The gRPC TrackerCommandResponse object to serialize. - - Returns - ------- - dict - A dictionary representation of the TrackerCommandResponse object. - """ - - def serialize_body(body): - """Serialize a Body object into a dictionary.""" - return { - "id": body.id, - "name": body.name, - "can_suppress": body.can_suppress, - "transform_to_master": { - "m00": body.transform_to_master.m00, - "m11": body.transform_to_master.m11, - "m22": body.transform_to_master.m22, - "m33": body.transform_to_master.m33, - }, - "master_id": body.master_id, - "parent_id": body.parent_id, - "is_surface": body.is_surface, - } - - def serialize_entity_identifier(entity): - """Serialize an EntityIdentifier object into a dictionary.""" - return { - "id": entity.id, - } - - # Safely serialize each field, defaulting to an empty list if the field is missing - return { - "success": response.success, - "created_bodies": [ - serialize_body(body) for body in getattr(response, "created_bodies", []) - ], - "modified_bodies": [ - serialize_body(body) for body in getattr(response, "modified_bodies", []) - ], - "deleted_bodies": [ - serialize_entity_identifier(entity) - for entity in getattr(response, "deleted_bodies", []) - ], - } - class DuplicateFaceProblemAreas(ProblemArea): """Provides duplicate face problem area definition. @@ -189,7 +137,9 @@ def fix(self) -> RepairToolMessage: parent_design._update_design_inplace() else: tracker_response = response.result.complete_command_response - serialized_response = self._serialize_tracker_command_response(tracker_response) + serialized_response = parent_design._serialize_tracker_command_response( + tracker_response + ) parent_design._update_from_tracker(serialized_response) message = RepairToolMessage( @@ -247,7 +197,7 @@ def fix(self) -> RepairToolMessage: FixMissingFacesRequest(missing_face_problem_area_id=self._grpc_id) ) - serialized_response = self._serialize_tracker_command_response( + serialized_response = parent_design._serialize_tracker_command_response( response.result.complete_command_response ) @@ -319,7 +269,9 @@ def fix(self) -> RepairToolMessage: parent_design._update_design_inplace() else: tracker_response = response.result.complete_command_response - serialized_response = self._serialize_tracker_command_response(tracker_response) + serialized_response = parent_design._serialize_tracker_command_response( + tracker_response + ) parent_design._update_from_tracker(serialized_response) message = RepairToolMessage( @@ -381,7 +333,9 @@ def fix(self) -> RepairToolMessage: parent_design._update_design_inplace() else: tracker_response = response.result.complete_command_response - serialized_response = self._serialize_tracker_command_response(tracker_response) + serialized_response = parent_design._serialize_tracker_command_response( + tracker_response + ) parent_design._update_from_tracker(serialized_response) message = RepairToolMessage( @@ -444,7 +398,9 @@ def fix(self) -> RepairToolMessage: parent_design._update_design_inplace() else: tracker_response = response.result.complete_command_response - serialized_response = self._serialize_tracker_command_response(tracker_response) + serialized_response = parent_design._serialize_tracker_command_response( + tracker_response + ) parent_design._update_from_tracker(serialized_response) message = RepairToolMessage( @@ -510,7 +466,9 @@ def fix(self) -> RepairToolMessage: # If USE_TRACKER_TO_UPDATE_DESIGN is True, we serialize the response # and update the parent design with the serialized response. tracker_response = response.result.complete_command_response - serialized_response = self._serialize_tracker_command_response(tracker_response) + serialized_response = parent_design._serialize_tracker_command_response( + tracker_response + ) parent_design._update_from_tracker(serialized_response) message = RepairToolMessage( @@ -572,7 +530,7 @@ def fix(self) -> RepairToolMessage: parent_design._update_design_inplace() else: tracker_respone = response.result.complete_command_response - serialized_response = self._serialize_tracker_command_response(tracker_respone) + serialized_response = parent_design._serialize_tracker_command_response(tracker_respone) parent_design._update_from_tracker(serialized_response) message = RepairToolMessage( @@ -635,7 +593,7 @@ def fix(self) -> RepairToolMessage: parent_design._update_design_inplace() else: tracker_respone = response.result.complete_command_response - serialized_response = self._serialize_tracker_command_response(tracker_respone) + serialized_response = parent_design._serialize_tracker_command_response(tracker_respone) parent_design._update_from_tracker(serialized_response) message = RepairToolMessage( @@ -692,7 +650,7 @@ def fix(self) -> RepairToolMessage: parent_design._update_design_inplace() else: tracker_respone = response.result.complete_command_response - serialized_response = self._serialize_tracker_command_response(tracker_respone) + serialized_response = parent_design._serialize_tracker_command_response(tracker_respone) parent_design._update_from_tracker(serialized_response) message = RepairToolMessage( diff --git a/src/ansys/geometry/core/tools/repair_tools.py b/src/ansys/geometry/core/tools/repair_tools.py index 5742c97754..a5b4206350 100644 --- a/src/ansys/geometry/core/tools/repair_tools.py +++ b/src/ansys/geometry/core/tools/repair_tools.py @@ -523,16 +523,16 @@ def find_and_fix_short_edges( ) body_ids = [body.id for body in bodies] + parent_design = get_design_from_body(bodies[0]) response = self._grpc_client.services.repair_tools.find_and_fix_short_edges( selection=body_ids, + parent_design=parent_design, length=length, comprehensive_result=comprehensive_result, ) # Update existing design - parent_design = get_design_from_body(bodies[0]) - if not pyansys_geometry.USE_TRACKER_TO_UPDATE_DESIGN: parent_design._update_design_inplace() else: @@ -579,14 +579,14 @@ def find_and_fix_extra_edges( ) body_ids = [body.id for body in bodies] + parent_design = get_design_from_body(bodies[0]) response = self._grpc_client.services.repair_tools.find_and_fix_extra_edges( selection=body_ids, + parent_design=parent_design, comprehensive_result=comprehensive_result, ) # Update existing design - parent_design = get_design_from_body(bodies[0]) - if not pyansys_geometry.USE_TRACKER_TO_UPDATE_DESIGN: parent_design._update_design_inplace() else: @@ -641,17 +641,17 @@ def find_and_fix_split_edges( ) body_ids = [body.id for body in bodies] + parent_design = get_design_from_body(bodies[0]) response = self._grpc_client.services.repair_tools.find_and_fix_split_edges( bodies_or_faces=body_ids, + parent_design=parent_design, angle=angle, length=length, comprehensive_result=comprehensive_result, ) # Update existing design - parent_design = get_design_from_body(bodies[0]) - if not pyansys_geometry.USE_TRACKER_TO_UPDATE_DESIGN: parent_design._update_design_inplace() else: @@ -696,15 +696,15 @@ def find_and_fix_simplify( ) body_ids = [body.id for body in bodies] + parent_design = get_design_from_body(bodies[0]) response = self._grpc_client.services.repair_tools.find_and_fix_simplify( selection=body_ids, + parent_design=parent_design, comprehensive_result=comprehensive_result, ) # Update existing design - parent_design = get_design_from_body(bodies[0]) - if not pyansys_geometry.USE_TRACKER_TO_UPDATE_DESIGN: parent_design._update_design_inplace() else: @@ -766,9 +766,11 @@ def find_and_fix_stitch_faces( ) body_ids = [body.id for body in bodies] + parent_design = get_design_from_body(bodies[0]) response = self._grpc_client.services.repair_tools.find_and_fix_stitch_faces( body_ids=body_ids, + parent_design=parent_design, max_distance=max_distance, allow_multiple_bodies=allow_multiple_bodies, maintain_components=maintain_components, @@ -777,8 +779,6 @@ def find_and_fix_stitch_faces( ) # Update existing design - parent_design = get_design_from_body(bodies[0]) - if not pyansys_geometry.USE_TRACKER_TO_UPDATE_DESIGN: parent_design._update_design_inplace() else: