From e28ffb04c2ac313bfe6787414d6a93110e86357a Mon Sep 17 00:00:00 2001 From: Romain FERRU Date: Thu, 7 Nov 2024 20:38:31 +0100 Subject: [PATCH 1/3] refactored loft at direct api level --- src/build123d/topology.py | 615 +++++++++++++------------------------- tests/test_direct_api.py | 380 +++++++---------------- 2 files changed, 313 insertions(+), 682 deletions(-) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 561093ba..4b15d86f 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -415,9 +415,7 @@ def param_at(self, distance: float) -> float: curve = self._geom_adaptor() length = GCPnts_AbscissaPoint.Length_s(curve) - return GCPnts_AbscissaPoint( - curve, length * distance, curve.FirstParameter() - ).Parameter() + return GCPnts_AbscissaPoint(curve, length * distance, curve.FirstParameter()).Parameter() def tangent_at( self, @@ -566,9 +564,7 @@ def common_plane(self, *lines: Union[Edge, Wire]) -> Union[None, Plane]: # Note: BRepLib_FindSurface is not helpful as it requires the # Edges to form a surface perimeter. points: list[Vector] = [] - all_lines: list[Edge, Wire] = [ - line for line in [self, *lines] if line is not None - ] + all_lines: list[Edge, Wire] = [line for line in [self, *lines] if line is not None] if any([not isinstance(line, (Edge, Wire)) for line in all_lines]): raise ValueError("Only Edges or Wires are valid") @@ -596,9 +592,7 @@ def common_plane(self, *lines: Union[Edge, Wire]) -> Union[None, Plane]: for line in all_lines: num_points = 2 if line.geom_type == GeomType.LINE else 8 - points.extend( - [line.position_at(i / (num_points - 1)) for i in range(num_points)] - ) + points.extend([line.position_at(i / (num_points - 1)) for i in range(num_points)]) points = list(set(points)) # unique points extreme_areas = {} for subset in combinations(points, 3): @@ -786,9 +780,7 @@ def locations( list[Location]: A list of Location objects representing local coordinate systems at the specified distances. """ - return [ - self.location_at(d, position_mode, frame_method, planar) for d in distances - ] + return [self.location_at(d, position_mode, frame_method, planar) for d in distances] def __matmul__(self: Union[Edge, Wire], position: float) -> Vector: """Position on wire operator @""" @@ -869,8 +861,7 @@ def offset_2d( i = 0 for edge in offset_edges: if edge.geom_type == GeomType.CIRCLE and ( - edge.arc_center == line.position_at(0) - or edge.arc_center == line.position_at(1) + edge.arc_center == line.position_at(0) or edge.arc_center == line.position_at(1) ): i += 1 else: @@ -878,10 +869,7 @@ def offset_2d( edges_to_keep[0] += edges_to_keep[2] wires = [Wire(edges) for edges in edges_to_keep[0:2]] centers = [w.position_at(0.5) for w in wires] - angles = [ - line.tangent_at(0).get_signed_angle(c - line.position_at(0)) - for c in centers - ] + angles = [line.tangent_at(0).get_signed_angle(c - line.position_at(0)) for c in centers] if side == Side.LEFT: offset_wire = wires[int(angles[0] > angles[1])] else: @@ -903,9 +891,7 @@ def offset_2d( offset_edges = offset_wire.edges() return offset_edges[0] if len(offset_edges) == 1 else offset_wire - def perpendicular_line( - self, length: float, u_value: float, plane: Plane = Plane.XY - ) -> Edge: + def perpendicular_line(self, length: float, u_value: float, plane: Plane = Plane.XY) -> Edge: """perpendicular_line Create a line on the given plane perpendicular to and centered on beginning of self @@ -919,9 +905,7 @@ def perpendicular_line( Edge: perpendicular line """ start = self.position_at(u_value) - local_plane = Plane( - origin=start, x_dir=self.tangent_at(u_value), z_dir=plane.z_dir - ) + local_plane = Plane(origin=start, x_dir=self.tangent_at(u_value), z_dir=plane.z_dir) line = Edge.make_line( start + local_plane.y_dir * length / 2, start - local_plane.y_dir * length / 2, @@ -942,9 +926,7 @@ def project( """ - bldr = BRepProj_Projection( - self.wrapped, face.wrapped, Vector(direction).to_dir() - ) + bldr = BRepProj_Projection(self.wrapped, face.wrapped, Vector(direction).to_dir()) shapes = Compound(bldr.Shape()) # select the closest projection if requested @@ -1060,9 +1042,7 @@ def __max_fillet(window_min: float, window_max: float, current_iteration: int): if window_mid - window_min <= tolerance: return_value = window_mid else: - return_value = __max_fillet( - window_mid, window_max, current_iteration + 1 - ) + return_value = __max_fillet(window_mid, window_max, current_iteration + 1) return return_value if not self.is_valid(): @@ -1112,9 +1092,7 @@ def chamfer( # make a edge --> faces mapping edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape() - TopExp.MapShapesAndAncestors_s( - self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map - ) + TopExp.MapShapesAndAncestors_s(self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map) # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) @@ -1141,9 +1119,7 @@ def chamfer( if not new_shape.is_valid(): raise Standard_Failure except (StdFail_NotDone, Standard_Failure) as err: - raise ValueError( - "Failed creating a chamfer, try a smaller length value(s)" - ) from err + raise ValueError("Failed creating a chamfer, try a smaller length value(s)") from err return new_shape @@ -1294,9 +1270,7 @@ def offset_3d( try: offset_occt_solid = offset_builder.Shape() except (StdFail_NotDone, Standard_Failure) as err: - raise RuntimeError( - "offset Error, an alternative kind may resolve this error" - ) from err + raise RuntimeError("offset Error, an alternative kind may resolve this error") from err offset_solid = self.__class__(offset_occt_solid) @@ -1499,9 +1473,7 @@ def copy_attributes_to(self, target: Shape, attributes: list[str]): if not getattr(target, attr): setattr(target, attr, getattr(self, attr)) elif getattr(self, attr): - warnings.warn( - f"Target does not have attribute '{attr}', skipping copy." - ) + warnings.warn(f"Target does not have attribute '{attr}', skipping copy.") else: raise ValueError(f"Source does not have attribute '{attr}'") @@ -1525,9 +1497,7 @@ def is_manifold(self) -> bool: # Fill the map with edges and their associated faces in the given shape. Each edge in # the map is associated with a list of faces that share that edge. - TopExp.MapShapesAndAncestors_s( - self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, shape_map - ) + TopExp.MapShapesAndAncestors_s(self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, shape_map) # Iterate over the edges in the map and checks if each edge is non-degenerate and has # exactly two faces associated with it. @@ -1602,9 +1572,7 @@ def _build_tree( parent_node = tree[-1] while iterator.More(): child = iterator.Value() - if Shape._ordered_shapes.index( - child.ShapeType() - ) <= Shape._ordered_shapes.index(limit): + if Shape._ordered_shapes.index(child.ShapeType()) <= Shape._ordered_shapes.index(limit): Shape._build_tree(child, tree, parent_node, limit) iterator.Next() return tree @@ -1618,15 +1586,10 @@ def _show_tree(root_node, show_center: bool) -> str: size_tuples.append((root_node.height, len(root_node.label))) # pylint: disable=cell-var-from-loop size_tuples_per_level = [ - list(filter(lambda ll: ll[0] == l, size_tuples)) - for l in range(root_node.height + 1) - ] - max_sizes_per_level = [ - max(4, max(l[1] for l in level)) for level in size_tuples_per_level - ] - level_sizes_per_level = [ - l + i * 4 for i, l in enumerate(reversed(max_sizes_per_level)) + list(filter(lambda ll: ll[0] == l, size_tuples)) for l in range(root_node.height + 1) ] + max_sizes_per_level = [max(4, max(l[1] for l in level)) for level in size_tuples_per_level] + level_sizes_per_level = [l + i * 4 for i, l in enumerate(reversed(max_sizes_per_level))] tree_label_width = max(level_sizes_per_level) + 1 # Build the tree line by line @@ -1690,9 +1653,7 @@ def show_topology( show_center = False if show_center is None else show_center result = Shape._show_tree(self, show_center) else: - tree = Shape._build_tree( - self.wrapped, tree=[], limit=inverse_shape_LUT[limit_class] - ) + tree = Shape._build_tree(self.wrapped, tree=[], limit=inverse_shape_LUT[limit_class]) show_center = True if show_center is None else show_center result = Shape._show_tree(tree[0], show_center) return result @@ -1727,11 +1688,7 @@ def __add__(self, other: Union[list[Shape], Shape]) -> Self: sum_shape = self.fuse(*summands) # Simplify Compounds if possible - sum_shape = ( - sum_shape.unwrap(fully=True) - if isinstance(sum_shape, Compound) - else sum_shape - ) + sum_shape = sum_shape.unwrap(fully=True) if isinstance(sum_shape, Compound) else sum_shape if SkipClean.clean: sum_shape = sum_shape.clean() @@ -1778,9 +1735,7 @@ def __sub__(self, other: Union[Shape, Iterable[Shape]]) -> Self: # Simplify Compounds if possible difference = ( - difference.unwrap(fully=True) - if isinstance(difference, Compound) - else difference + difference.unwrap(fully=True) if isinstance(difference, Compound) else difference ) # To allow the @, % and ^ operators to work 1D objects must be type Curve if minuend_dim == 1: @@ -1800,11 +1755,7 @@ def __and__(self, other: Shape) -> Self: new_shape = new_shape.clean() # Simplify Compounds if possible - new_shape = ( - new_shape.unwrap(fully=True) - if isinstance(new_shape, Compound) - else new_shape - ) + new_shape = new_shape.unwrap(fully=True) if isinstance(new_shape, Compound) else new_shape # To allow the @, % and ^ operators to work 1D objects must be type Curve if self._dim == 1: @@ -1818,9 +1769,7 @@ def __rmul__(self, other): isinstance(other, (list, tuple)) and all([isinstance(o, (Location, Plane)) for o in other]) ): - raise ValueError( - "shapes can only be multiplied list of locations or planes" - ) + raise ValueError("shapes can only be multiplied list of locations or planes") return [loc * self for loc in other] def center(self) -> Vector: @@ -1906,9 +1855,7 @@ def export_stl( Returns: bool: Success """ - mesh = BRepMesh_IncrementalMesh( - self.wrapped, tolerance, True, angular_tolerance, True - ) + mesh = BRepMesh_IncrementalMesh(self.wrapped, tolerance, True, angular_tolerance, True) mesh.Perform() writer = StlAPI_Writer() @@ -2058,9 +2005,7 @@ def bounding_box(self, tolerance: float = None, optimal: bool = True) -> BoundBo Returns: BoundBox: A box sized to contain this Shape """ - return BoundBox._from_topo_ds( - self.wrapped, tolerance=tolerance, optimal=optimal - ) + return BoundBox._from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal) def mirror(self, mirror_plane: Plane = None) -> Self: """ @@ -2076,16 +2021,12 @@ def mirror(self, mirror_plane: Plane = None) -> Self: mirror_plane = Plane.XY transformation = gp_Trsf() - transformation.SetMirror( - gp_Ax2(mirror_plane.origin.to_pnt(), mirror_plane.z_dir.to_dir()) - ) + transformation.SetMirror(gp_Ax2(mirror_plane.origin.to_pnt(), mirror_plane.z_dir.to_dir())) return self._apply_transform(transformation) @staticmethod - def combined_center( - objects: Iterable[Shape], center_of: CenterOf = CenterOf.MASS - ) -> Vector: + def combined_center(objects: Iterable[Shape], center_of: CenterOf = CenterOf.MASS) -> Vector: """combined center Calculates the center of a multiple objects. @@ -2158,16 +2099,12 @@ def _entities(self, topo_type: Shapes) -> list[TopoDS_Shape]: while explorer.More(): item = explorer.Current() - out[item.HashCode(HASH_CODE_MAX)] = ( - item # needed to avoid pseudo-duplicate entities - ) + out[item.HashCode(HASH_CODE_MAX)] = item # needed to avoid pseudo-duplicate entities explorer.Next() return list(out.values()) - def _entities_from( - self, child_type: Shapes, parent_type: Shapes - ) -> Dict[Shape, list[Shape]]: + def _entities_from(self, child_type: Shapes, parent_type: Shapes) -> Dict[Shape, list[Shape]]: """This function is very slow on M1 macs and is currently unused""" res = TopTools_IndexedDataMapOfShapeListOfShape() @@ -2180,17 +2117,13 @@ def _entities_from( out: Dict[Shape, list[Shape]] = {} for i in range(1, res.Extent() + 1): - out[Shape.cast(res.FindKey(i))] = [ - Shape.cast(el) for el in res.FindFromIndex(i) - ] + out[Shape.cast(res.FindKey(i))] = [Shape.cast(el) for el in res.FindFromIndex(i)] return out def vertices(self) -> ShapeList[Vertex]: """vertices - all the vertices in this Shape""" - vertex_list = ShapeList( - [Vertex(downcast(i)) for i in self._entities(Vertex.__name__)] - ) + vertex_list = ShapeList([Vertex(downcast(i)) for i in self._entities(Vertex.__name__)]) for vertex in vertex_list: vertex.topo_parent = self return vertex_list @@ -2781,11 +2714,7 @@ def split(self, surface: Union[Plane, Face], keep: Keep = Keep.TOP) -> Self: shape_list.Append(self.wrapped) # Define the splitting tool - tool = ( - Face.make_plane(surface).wrapped - if isinstance(surface, Plane) - else surface.wrapped - ) + tool = Face.make_plane(surface).wrapped if isinstance(surface, Plane) else surface.wrapped tool_list = TopTools_ListOfShape() tool_list.Append(tool) @@ -2821,9 +2750,7 @@ def split_by_perimeter( ) -> Union[Optional[Shell], Optional[Face]]: ... @overload - def split_by_perimeter( - self, perimeter: Union[Edge, Wire], keep: Literal[Keep.BOTH] - ) -> tuple[ + def split_by_perimeter(self, perimeter: Union[Edge, Wire], keep: Literal[Keep.BOTH]) -> tuple[ Union[Optional[Shell], Optional[Face]], Union[Optional[Shell], Optional[Face]], ]: ... @@ -2831,9 +2758,7 @@ def split_by_perimeter( def split_by_perimeter( self, perimeter: Union[Edge, Wire] ) -> Union[Optional[Shell], Optional[Face]]: ... - def split_by_perimeter( - self, perimeter: Union[Edge, Wire], keep: Keep = Keep.INSIDE - ): + def split_by_perimeter(self, perimeter: Union[Edge, Wire], keep: Keep = Keep.INSIDE): """split_by_perimeter Divide the faces of this object into those within the perimeter @@ -2870,9 +2795,7 @@ def get(los: TopTools_ListOfShape, shape_cls) -> list: return shapes if keep not in {Keep.INSIDE, Keep.OUTSIDE, Keep.BOTH}: - raise ValueError( - "keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH" - ) + raise ValueError("keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH") # Process the perimeter if not perimeter.is_closed: @@ -2896,12 +2819,8 @@ def get(los: TopTools_ListOfShape, shape_cls) -> list: # Is left or right the inside? perimeter_length = perimeter.length - left_perimeter_length = ( - sum(e.length for e in left.edges()) if not left is None else 0 - ) - right_perimeter_length = ( - sum(e.length for e in right.edges()) if not right is None else 0 - ) + left_perimeter_length = sum(e.length for e in left.edges()) if not left is None else 0 + right_perimeter_length = sum(e.length for e in right.edges()) if not right is None else 0 left_inside = abs(perimeter_length - left_perimeter_length) < abs( perimeter_length - right_perimeter_length ) @@ -2955,9 +2874,7 @@ def mesh(self, tolerance: float, angular_tolerance: float = 0.1): """ if not BRepTools.Triangulation_s(self.wrapped, tolerance): - BRepMesh_IncrementalMesh( - self.wrapped, tolerance, True, angular_tolerance, True - ) + BRepMesh_IncrementalMesh(self.wrapped, tolerance, True, angular_tolerance, True) def tessellate( self, tolerance: float, angular_tolerance: float = 0.1 @@ -2978,9 +2895,7 @@ def tessellate( # add vertices vertices += [ Vector(v.X(), v.Y(), v.Z()) - for v in ( - poly.Node(i).Transformed(trsf) for i in range(1, poly.NbNodes() + 1) - ) + for v in (poly.Node(i).Transformed(trsf) for i in range(1, poly.NbNodes() + 1)) ] # add triangles triangles += [ @@ -3004,9 +2919,7 @@ def tessellate( return vertices, triangles - def to_splines( - self, degree: int = 3, tolerance: float = 1e-3, nurbs: bool = False - ) -> T: + def to_splines(self, degree: int = 3, tolerance: float = 1e-3, nurbs: bool = False) -> T: """to_splines Approximate shape with b-splines of the specified degree. @@ -3109,9 +3022,7 @@ def _repr_javascript_(self): return display(self)._repr_javascript_() - def transformed( - self, rotate: VectorLike = (0, 0, 0), offset: VectorLike = (0, 0, 0) - ) -> Self: + def transformed(self, rotate: VectorLike = (0, 0, 0), offset: VectorLike = (0, 0, 0)) -> Self: """Transform Shape Rotate and translate the Shape by the three angles (in degrees) and offset. @@ -3228,15 +3139,11 @@ def project_faces( for face in faces: bbox = face.bounding_box() face_center_x = (bbox.min.X + bbox.max.X) / 2 - relative_position_on_wire = ( - start + (face_center_x - first_face_min_x) / path_length - ) + relative_position_on_wire = start + (face_center_x - first_face_min_x) / path_length path_position = path.position_at(relative_position_on_wire) path_tangent = path.tangent_at(relative_position_on_wire) projection_axis = Axis(path_position, shape_center - path_position) - (surface_point, surface_normal) = self.find_intersection_points( - projection_axis - )[0] + (surface_point, surface_normal) = self.find_intersection_points(projection_axis)[0] surface_normal_plane = Plane( origin=surface_point, x_dir=path_tangent, z_dir=surface_normal ) @@ -3245,17 +3152,13 @@ def project_faces( ) logger.debug("projecting face at %0.2f", relative_position_on_wire) - projected_faces.append( - projection_face.project_to_shape(self, surface_normal * -1)[0] - ) + projected_faces.append(projection_face.project_to_shape(self, surface_normal * -1)[0]) logger.debug("finished projecting '%d' faces", len(faces)) return Compound(projected_faces) - def _extrude( - self, direction: VectorLike - ) -> Union[Edge, Face, Shell, Solid, Compound]: + def _extrude(self, direction: VectorLike) -> Union[Edge, Face, Shell, Solid, Compound]: """_extrude Extrude self in the provided direction. @@ -3300,9 +3203,7 @@ def _extrude( return result @classmethod - def extrude( - cls, obj: Union[Vertex, Edge, Wire, Face, Shell], direction: VectorLike - ) -> Self: + def extrude(cls, obj: Union[Vertex, Edge, Wire, Face, Shell], direction: VectorLike) -> Self: """extrude Extrude a Shape in the provided direction. @@ -3368,9 +3269,7 @@ def extract_edges(compound): projection_dir: Vector = (viewport_origin - look_at).normalized() viewport_up = Vector(viewport_up).normalized() camera_coordinate_system = gp_Ax2() - camera_coordinate_system.SetAxis( - gp_Ax1(viewport_origin.to_pnt(), projection_dir.to_dir()) - ) + camera_coordinate_system.SetAxis(gp_Ax1(viewport_origin.to_pnt(), projection_dir.to_dir())) camera_coordinate_system.SetYDirection(viewport_up.to_dir()) projector = HLRAlgo_Projector(camera_coordinate_system) @@ -3563,30 +3462,22 @@ def filter_by_position( """ if inclusive == (True, True): objects = filter( - lambda o: minimum - <= axis.to_plane().to_local_coords(o).center().Z - <= maximum, + lambda o: minimum <= axis.to_plane().to_local_coords(o).center().Z <= maximum, self, ) elif inclusive == (True, False): objects = filter( - lambda o: minimum - <= axis.to_plane().to_local_coords(o).center().Z - < maximum, + lambda o: minimum <= axis.to_plane().to_local_coords(o).center().Z < maximum, self, ) elif inclusive == (False, True): objects = filter( - lambda o: minimum - < axis.to_plane().to_local_coords(o).center().Z - <= maximum, + lambda o: minimum < axis.to_plane().to_local_coords(o).center().Z <= maximum, self, ) elif inclusive == (False, False): objects = filter( - lambda o: minimum - < axis.to_plane().to_local_coords(o).center().Z - < maximum, + lambda o: minimum < axis.to_plane().to_local_coords(o).center().Z < maximum, self, ) @@ -3694,9 +3585,7 @@ def u_of_closest_center(obj) -> float: return sort_by.param_at_point(pnt1) # pylint: disable=unnecessary-lambda - objects = sorted( - self, key=lambda o: u_of_closest_center(o), reverse=reverse - ) + objects = sorted(self, key=lambda o: u_of_closest_center(o), reverse=reverse) elif isinstance(sort_by, SortBy): if sort_by == SortBy.LENGTH: @@ -3861,17 +3750,13 @@ def __or__(self, filter_by: Union[Axis, GeomType] = Axis.Z): def __eq__(self, other: object): """ShapeLists equality operator ==""" - return ( - set(self) == set(other) if isinstance(other, ShapeList) else NotImplemented - ) + return set(self) == set(other) if isinstance(other, ShapeList) else NotImplemented # Normally implementing __eq__ is enough, but ShapeList subclasses list, # which already implements __ne__, so we need to override it, too def __ne__(self, other: ShapeList): """ShapeLists inequality operator !=""" - return ( - set(self) != set(other) if isinstance(other, ShapeList) else NotImplemented - ) + return set(self) != set(other) if isinstance(other, ShapeList) else NotImplemented def __add__(self, other: ShapeList): """Combine two ShapeLists together operator +""" @@ -4032,13 +3917,13 @@ def __init__(self, *args, **kwargs): if args: l_a = len(args) if isinstance(args[0], TopoDS_Shape): - obj, label, color, material, joints, parent, children = args[:7] + ( - None, - ) * (7 - l_a) + obj, label, color, material, joints, parent, children = args[:7] + (None,) * ( + 7 - l_a + ) elif isinstance(args[0], Iterable): - shapes, label, color, material, joints, parent, children = args[:7] + ( - None, - ) * (7 - l_a) + shapes, label, color, material, joints, parent, children = args[:7] + (None,) * ( + 7 - l_a + ) unknown_args = ", ".join( set(kwargs.keys()).difference( @@ -4175,9 +4060,7 @@ def _post_detach(self, parent: Compound): """Method call after detaching from `parent`.""" logger.debug("Removing parent of %s (%s)", self.label, parent.label) if parent.children: - parent.wrapped = Compound._make_compound( - [c.wrapped for c in parent.children] - ) + parent.wrapped = Compound._make_compound([c.wrapped for c in parent.children]) else: parent.wrapped = None @@ -4235,12 +4118,9 @@ def do_children_intersect( if not include_parent: children.pop(0) # remove parent # children_bbox = [child.bounding_box().to_solid() for child in children] - children_bbox = [ - Solid.from_bounding_box(child.bounding_box()) for child in children - ] + children_bbox = [Solid.from_bounding_box(child.bounding_box()) for child in children] child_index_pairs = [ - tuple(map(int, comb)) - for comb in combinations(list(range(len(children))), 2) + tuple(map(int, comb)) for comb in combinations(list(range(len(children))), 2) ] for child_index_pair in child_index_pairs: # First check for bounding box intersections .. @@ -4252,9 +4132,7 @@ def do_children_intersect( ) if bbox_common_volume > tolerance: common_volume = ( - children[child_index_pair[0]] - .intersect(children[child_index_pair[1]]) - .volume + children[child_index_pair[0]].intersect(children[child_index_pair[1]]).volume ) if common_volume > tolerance: return ( @@ -4319,9 +4197,7 @@ def position_face(orig_face: "Face") -> "Face": """ bbox = orig_face.bounding_box() face_bottom_center = Vector((bbox.min.X + bbox.max.X) / 2, 0, 0) - relative_position_on_wire = ( - position_on_path + face_bottom_center.X / path_length - ) + relative_position_on_wire = position_on_path + face_bottom_center.X / path_length wire_tangent = text_path.tangent_at(relative_position_on_wire) wire_angle = Vector(1, 0, 0).get_signed_angle(wire_tangent) wire_position = text_path.position_at(relative_position_on_wire) @@ -4367,9 +4243,7 @@ def position_face(orig_face: "Face") -> "Face": # Align the text from the bounding box align = tuplify(align, 2) - text_flat = text_flat.translate( - Vector(*text_flat.bounding_box().to_align_offset(align)) - ) + text_flat = text_flat.translate(Vector(*text_flat.bounding_box().to_align_offset(align))) if text_path is not None: path_length = text_path.length @@ -4389,24 +4263,18 @@ def make_triad(cls, axes_scale: float) -> Compound: ) arrow = arrow_arc.fuse(copy.copy(arrow_arc).mirror(Plane.XZ)) x_label = ( - Compound.make_text( - "X", font_size=axes_scale / 4, align=(Align.MIN, Align.CENTER) - ) + Compound.make_text("X", font_size=axes_scale / 4, align=(Align.MIN, Align.CENTER)) .move(Location(x_axis @ 1)) .edges() ) y_label = ( - Compound.make_text( - "Y", font_size=axes_scale / 4, align=(Align.MIN, Align.CENTER) - ) + Compound.make_text("Y", font_size=axes_scale / 4, align=(Align.MIN, Align.CENTER)) .rotate(Axis.Z, 90) .move(Location(y_axis @ 1)) .edges() ) z_label = ( - Compound.make_text( - "Z", font_size=axes_scale / 4, align=(Align.CENTER, Align.MIN) - ) + Compound.make_text("Z", font_size=axes_scale / 4, align=(Align.CENTER, Align.MIN)) .rotate(Axis.Y, 90) .rotate(Axis.X, 90) .move(Location(z_axis @ 1)) @@ -4512,9 +4380,7 @@ def intersect(self, *to_intersect: Shape) -> Compound: def get_type( self, - obj_type: Union[ - Type[Vertex], Type[Edge], Type[Face], Type[Shell], Type[Solid], Type[Wire] - ], + obj_type: Union[Type[Vertex], Type[Edge], Type[Face], Type[Shell], Type[Solid], Type[Wire]], ) -> list[Union[Vertex, Edge, Face, Shell, Solid, Wire]]: """get_type @@ -4549,9 +4415,7 @@ def get_type( return results - def first_level_shapes( - self, _shapes: list[TopoDS_Shape] = None - ) -> ShapeList[Shape]: + def first_level_shapes(self, _shapes: list[TopoDS_Shape] = None) -> ShapeList[Shape]: """first_level_shapes This method iterates through the immediate children of the compound and @@ -4811,10 +4675,7 @@ def find_tangent( discontinuities = 0.0 for i in range(101 - periodic): tangent = self.tangent_angle_at(i / 100) + discontinuities * 360 - if ( - previous_tangent is not None - and abs(previous_tangent - tangent) > 300 - ): + if previous_tangent is not None and abs(previous_tangent - tangent) > 300: discontinuities = copysign(1.0, previous_tangent - tangent) tangent += 360 * discontinuities previous_tangent = tangent @@ -4840,9 +4701,7 @@ def find_tangent( def _intersect_with_edge(self, edge: Edge) -> Shape: # Find any intersection points - vertex_intersections = [ - Vertex(pnt) for pnt in self.find_intersection_points(edge) - ] + vertex_intersections = [Vertex(pnt) for pnt in self.find_intersection_points(edge)] # Find Edge/Edge overlaps intersect_op = BRepAlgoAPI_Common() @@ -4852,9 +4711,7 @@ def _intersect_with_edge(self, edge: Edge) -> Shape: def _intersect_with_axis(self, axis: Axis) -> Shape: # Find any intersection points - vertex_intersections = [ - Vertex(pnt) for pnt in self.find_intersection_points(axis) - ] + vertex_intersections = [Vertex(pnt) for pnt in self.find_intersection_points(axis)] # Find Edge/Edge overlaps intersect_op = BRepAlgoAPI_Common() @@ -4879,9 +4736,7 @@ def find_intersection_points( """ # Convert an Axis into an edge at least as large as self and Axis start point if isinstance(edge, Axis): - self_bbox_w_edge = self.bounding_box().add( - Vertex(edge.position).bounding_box() - ) + self_bbox_w_edge = self.bounding_box().add(Vertex(edge.position).bounding_box()) edge = Edge.make_line( edge.position + edge.direction * (-1 * self_bbox_w_edge.diagonal), edge.position + edge.direction * self_bbox_w_edge.diagonal, @@ -4907,9 +4762,7 @@ def find_intersection_points( edge.param_at(0), edge.param_at(1), ) - intersector = Geom2dAPI_InterCurveCurve( - self_2d_curve, edge_2d_curve, tolerance - ) + intersector = Geom2dAPI_InterCurveCurve(self_2d_curve, edge_2d_curve, tolerance) else: intersector = Geom2dAPI_InterCurveCurve(self_2d_curve, tolerance) @@ -4926,10 +4779,7 @@ def find_intersection_points( for pnt in crosses: try: if edge is not None: - if ( - self.distance_to(pnt) <= TOLERANCE - and edge.distance_to(pnt) <= TOLERANCE - ): + if self.distance_to(pnt) <= TOLERANCE and edge.distance_to(pnt) <= TOLERANCE: valid_crosses.append(pnt) else: if self.distance_to(pnt) <= TOLERANCE: @@ -5061,9 +4911,7 @@ def func(param: ndarray) -> float: return (self.position_at(param[0]) - point).length # Find the u value that results in a point within tolerance of the target - initial_guess = max( - 0.0, min(1.0, (point - self.position_at(0)).length / self.length) - ) + initial_guess = max(0.0, min(1.0, (point - self.position_at(0)).length / self.length)) result = minimize( func, x0=initial_guess, @@ -5095,9 +4943,7 @@ def make_bezier(cls, *cntl_pnts: VectorLike, weights: list[float] = None) -> Edg Edge: bezier curve """ if len(cntl_pnts) < 2: - raise ValueError( - "At least two control points must be provided (start, end)" - ) + raise ValueError("At least two control points must be provided (start, end)") if len(cntl_pnts) > 25: raise ValueError("The maximum number of control points is 25") if weights: @@ -5367,9 +5213,7 @@ def make_spline_approx( pnts.SetValue(i + 1, Vector(point).to_pnt()) if smoothing: - spline_builder = GeomAPI_PointsToBSpline( - pnts, *smoothing, DegMax=max_deg, Tol3D=tol - ) + spline_builder = GeomAPI_PointsToBSpline(pnts, *smoothing, DegMax=max_deg, Tol3D=tol) else: spline_builder = GeomAPI_PointsToBSpline( pnts, DegMin=min_deg, DegMax=max_deg, Tol3D=tol @@ -5405,9 +5249,7 @@ def make_three_point_arc( return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) @classmethod - def make_tangent_arc( - cls, start: VectorLike, tangent: VectorLike, end: VectorLike - ) -> Edge: + def make_tangent_arc(cls, start: VectorLike, tangent: VectorLike, end: VectorLike) -> Edge: """Tangent Arc Makes a tangent arc from point start, in the direction of tangent and ends at end. @@ -5438,11 +5280,7 @@ def make_line(cls, point1: VectorLike, point2: VectorLike) -> Edge: A linear edge between the two provided points """ - return cls( - BRepBuilderAPI_MakeEdge( - Vector(point1).to_pnt(), Vector(point2).to_pnt() - ).Edge() - ) + return cls(BRepBuilderAPI_MakeEdge(Vector(point1).to_pnt(), Vector(point2).to_pnt()).Edge()) @classmethod def make_helix( @@ -5496,9 +5334,7 @@ def make_helix( # Create an infinite 2d line in the direction of the helix helix_line = Geom2d_Line(gp_Pnt2d(0, 0), gp_Dir2d(line_dir.X, line_dir.Y)) # Trim the line to the desired length - helix_curve = Geom2d_TrimmedCurve( - helix_line, 0, line_len, theAdjustPeriodic=True - ) + helix_curve = Geom2d_TrimmedCurve(helix_line, 0, line_len, theAdjustPeriodic=True) # 3. Wrap the line around the surface edge_builder = BRepBuilderAPI_MakeEdge(helix_curve, geom_surf) @@ -5585,9 +5421,7 @@ def project_to_shape( def to_axis(self) -> Axis: """Translate a linear Edge to an Axis""" if self.geom_type != GeomType.LINE: - raise ValueError( - f"to_axis is only valid for linear Edges not {self.geom_type}" - ) + raise ValueError(f"to_axis is only valid for linear Edges not {self.geom_type}") return Axis(self.position_at(0), self.position_at(1) - self.position_at(0)) @@ -5650,9 +5484,7 @@ def __init__(self, *args, **kwargs): if isinstance(args[0], TopoDS_Shape): obj, label, color, parent = args[:4] + (None,) * (4 - l_a) elif isinstance(args[0], Wire): - outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * ( - 5 - l_a - ) + outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * (5 - l_a) unknown_args = ", ".join( set(kwargs.keys()).difference( @@ -5728,9 +5560,7 @@ def geometry(self) -> str: if len(flat_face_edges) == 4: edge_pairs = [] for vertex in flat_face_vertices: - edge_pairs.append( - [e for e in flat_face_edges if vertex in e.vertices()] - ) + edge_pairs.append([e for e in flat_face_edges if vertex in e.vertices()]) edge_pair_directions = [ [edge.tangent_at(0) for edge in pair] for pair in edge_pairs ] @@ -5828,9 +5658,7 @@ def normal_at(self, *args, **kwargs) -> Vector: if len(args) == 2 and isinstance(args[1], (int, float)): v = args[1] - unknown_args = ", ".join( - set(kwargs.keys()).difference(["surface_point", "u", "v"]) - ) + unknown_args = ", ".join(set(kwargs.keys()).difference(["surface_point", "u", "v"])) if unknown_args: raise ValueError(f"Unexpected argument(s) {unknown_args}") @@ -5851,9 +5679,7 @@ def normal_at(self, *args, **kwargs) -> Vector: v_val = v * (v_val0 + v_val1) else: # project point on surface - projector = GeomAPI_ProjectPointOnSurf( - Vector(surface_point).to_pnt(), surface - ) + projector = GeomAPI_ProjectPointOnSurf(Vector(surface_point).to_pnt(), surface) u_val, v_val = projector.LowerDistanceParameters() @@ -5907,9 +5733,7 @@ def center(self, center_of=CenterOf.GEOMETRY) -> Vector: Returns: Vector: center """ - if (center_of == CenterOf.MASS) or ( - center_of == CenterOf.GEOMETRY and self.is_planar - ): + if (center_of == CenterOf.MASS) or (center_of == CenterOf.GEOMETRY and self.is_planar): properties = GProp_GProps() BRepGProp.SurfaceProperties_s(self.wrapped, properties) center_point = properties.CentreOfMass() @@ -5975,22 +5799,16 @@ def make_plane( @overload @classmethod - def make_surface_from_curves( - cls, edge1: Edge, edge2: Edge - ) -> Face: # pragma: no cover + def make_surface_from_curves(cls, edge1: Edge, edge2: Edge) -> Face: # pragma: no cover ... @overload @classmethod - def make_surface_from_curves( - cls, wire1: Wire, wire2: Wire - ) -> Face: # pragma: no cover + def make_surface_from_curves(cls, wire1: Wire, wire2: Wire) -> Face: # pragma: no cover ... @classmethod - def make_surface_from_curves( - cls, curve1: Union[Edge, Wire], curve2: Union[Edge, Wire] - ) -> Face: + def make_surface_from_curves(cls, curve1: Union[Edge, Wire], curve2: Union[Edge, Wire]) -> Face: """make_surface_from_curves Create a ruled surface out of two edges or two wires. If wires are used then @@ -6010,9 +5828,7 @@ def make_surface_from_curves( return return_value @classmethod - def make_from_wires( - cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None - ) -> Face: + def make_from_wires(cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None) -> Face: """make_from_wires Makes a planar face from one or more wires @@ -6039,9 +5855,7 @@ def make_from_wires( return Face(Face._make_from_wires(outer_wire, inner_wires)) @classmethod - def _make_from_wires( - cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None - ) -> TopoDS_Shape: + def _make_from_wires(cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None) -> TopoDS_Shape: """make_from_wires Makes a planar face from one or more wires @@ -6065,9 +5879,7 @@ def _make_from_wires( # check if wires are coplanar verification_compound = Compound([outer_wire] + inner_wires) - if not BRepLib_FindSurface( - verification_compound.wrapped, OnlyPlane=True - ).Found(): + if not BRepLib_FindSurface(verification_compound.wrapped, OnlyPlane=True).Found(): raise ValueError("Cannot build face(s): wires not planar") # fix outer wire @@ -6134,9 +5946,7 @@ def sew_faces(cls, faces: Iterable[Face]) -> list[ShapeList[Face]]: elif isinstance(sewed_shape, TopoDS_Solid): sewn_faces = [Solid(sewed_shape).faces()] else: - raise RuntimeError( - f"SewedShape returned a {type(sewed_shape)} which was unexpected" - ) + raise RuntimeError(f"SewedShape returned a {type(sewed_shape)} which was unexpected") return sewn_faces @@ -6255,14 +6065,10 @@ def make_bezier_surface( Face: a potentially non-planar face """ if len(points) < 2 or len(points[0]) < 2: - raise ValueError( - "At least two control points must be provided (start, end)" - ) + raise ValueError("At least two control points must be provided (start, end)") if len(points) > 25 or len(points[0]) > 25: raise ValueError("The maximum number of control points is 25") - if weights and ( - len(points) != len(weights) or len(points[0]) != len(weights[0]) - ): + if weights and (len(points) != len(weights) or len(points[0]) != len(weights[0])): raise ValueError("A weight must be provided for each control point") points_ = TColgp_HArray2OfPnt(1, len(points), 1, len(points[0])) @@ -6338,9 +6144,7 @@ def make_surface( ) if isinstance(exterior, Wire): outside_edges = exterior.edges() - elif isinstance(exterior, Iterable) and all( - [isinstance(o, Edge) for o in exterior] - ): + elif isinstance(exterior, Iterable) and all([isinstance(o, Edge) for o in exterior]): outside_edges = exterior else: raise ValueError("exterior must be a Wire or list of Edges") @@ -6357,9 +6161,7 @@ def make_surface( Standard_NoSuchObject, Standard_ConstructionError, ) as err: - raise RuntimeError( - "Error building non-planar face with provided exterior" - ) from err + raise RuntimeError("Error building non-planar face with provided exterior") from err if surface_points: for point in surface_points: surface.Add(gp_Pnt(*point.to_tuple())) @@ -6480,8 +6282,7 @@ def is_coplanar(self, plane: Plane) -> bool: BRepGProp_Face(self.wrapped).Normal(u_val0, v_val0, gp_pnt, normal) return ( - plane.contains(Vector(gp_pnt)) - and 1 - abs(plane.z_dir.dot(Vector(normal))) < TOLERANCE + plane.contains(Vector(gp_pnt)) and 1 - abs(plane.z_dir.dot(Vector(normal))) < TOLERANCE ) def thicken(self, depth: float, normal_override: VectorLike = None) -> Solid: @@ -6616,8 +6417,7 @@ def get(los: TopTools_ListOfShape, shape_cls) -> list: def desired_faces(face_list: list[Face]) -> bool: return ( face_list - and face_list[0]._extrude(direction * -max_size).intersect(self).area - > TOLERANCE + and face_list[0]._extrude(direction * -max_size).intersect(self).area > TOLERANCE ) # @@ -6644,9 +6444,7 @@ def desired_faces(face_list: list[Face]) -> bool: if not edge_compound.IsNull(): target_edges_on_xy.extend(Compound(edge_compound).edges()) - target_edges = [ - projection_plane.from_local_coords(e) for e in target_edges_on_xy - ] + target_edges = [projection_plane.from_local_coords(e) for e in target_edges_on_xy] target_wires = edges_to_wires(target_edges) # return target_wires @@ -6668,9 +6466,7 @@ def desired_faces(face_list: list[Face]) -> bool: perimeter.wrapped, target_object.wrapped, direction.to_dir() ) # print(len(Compound(hlr_projector.Shape()).wires().sort_by(projection_axis))) - projected_wires = ( - Compound(hlr_projector.Shape()).wires().sort_by(projection_axis) - ) + projected_wires = Compound(hlr_projector.Shape()).wires().sort_by(projection_axis) # target_projected_wires = [] # for target_wire in target_wires: @@ -6977,6 +6773,27 @@ def sweep( builder.Build() return Shape.cast(builder.Shape()) + @classmethod + def make_loft(cls, objs: Iterable[Union[Vertex, Wire]], ruled: bool = False) -> Shell: + """make loft + + Makes a loft from a list of wires and vertices. + Vertices can appear only at the beginning or end of the list, but cannot appear consecutively within the list + nor between wires. + Wires may be closed or opened. + + Args: + objs (list[Vertex, Wire]): wire perimeters or vertices + ruled (bool, optional): stepped or smooth. Defaults to False (smooth). + + Raises: + ValueError: Too few wires + + Returns: + Shell: Lofted object + """ + return cls(_make_loft(objs, False, ruled)) + class Solid(Mixin3D, Shape): """A Solid in build123d represents a three-dimensional solid geometry @@ -7040,13 +6857,9 @@ def __init__(self, *args, **kwargs): if args: l_a = len(args) if isinstance(args[0], TopoDS_Shape): - obj, label, color, material, joints, parent = args[:6] + (None,) * ( - 6 - l_a - ) + obj, label, color, material, joints, parent = args[:6] + (None,) * (6 - l_a) elif isinstance(args[0], Shell): - shell, label, color, material, joints, parent = args[:6] + (None,) * ( - 6 - l_a - ) + shell, label, color, material, joints, parent = args[:6] + (None,) * (6 - l_a) unknown_args = ", ".join( set(kwargs.keys()).difference( @@ -7111,9 +6924,7 @@ def from_bounding_box(cls, bbox: BoundBox) -> Solid: return Solid.make_box(*bbox.size).locate(Location(bbox.min)) @classmethod - def make_box( - cls, length: float, width: float, height: float, plane: Plane = Plane.XY - ) -> Solid: + def make_box(cls, length: float, width: float, height: float, plane: Plane = Plane.XY) -> Solid: """make box Make a box at the origin of plane extending in positive direction of each axis. @@ -7235,13 +7046,12 @@ def make_torus( ) @classmethod - def make_loft( - cls, objs: Iterable[Union[Vertex, Wire]], ruled: bool = False - ) -> Solid: + def make_loft(cls, objs: Iterable[Union[Vertex, Wire]], ruled: bool = False) -> Solid: """make loft - Makes a loft from a list of wires and vertices, where vertices can be the first, - last, or first and last elements. + Makes a loft from a list of wires and vertices. + Vertices can appear only at the beginning or end of the list, but cannot appear consecutively within the list + nor between wires. Args: objs (list[Vertex, Wire]): wire perimeters or vertices @@ -7253,22 +7063,7 @@ def make_loft( Returns: Solid: Lofted object """ - - if len(objs) < 2: - raise ValueError("More than one wire, or a wire and a vertex is required") - - # the True flag requests building a solid instead of a shell. - loft_builder = BRepOffsetAPI_ThruSections(True, ruled) - - for obj in objs: - if isinstance(obj, Vertex): - loft_builder.AddVertex(obj.wrapped) - elif isinstance(obj, Wire): - loft_builder.AddWire(obj.wrapped) - - loft_builder.Build() - - return cls(loft_builder.Shape()) + return cls(_make_loft(objs, True, ruled)) @classmethod def make_wedge( @@ -7389,9 +7184,7 @@ def extrude_taper( outer = profile.outer_wire() local_outer: Wire = Plane(profile).to_local_coords(outer) - local_taper_outer = local_outer.offset_2d( - offset_amt, kind=Kind.INTERSECTION - ) + local_taper_outer = local_outer.offset_2d(offset_amt, kind=Kind.INTERSECTION) taper_outer = Plane(profile).from_local_coords(local_taper_outer) taper_outer.move(Location(direction)) @@ -7406,9 +7199,7 @@ def extrude_taper( taper.move(Location(direction)) taper_wires.append(taper) - solids = [ - Solid.make_loft([p, t]) for p, t in zip(profile_wires, taper_wires) - ] + solids = [Solid.make_loft([p, t]) for p, t in zip(profile_wires, taper_wires)] if len(solids) > 1: new_solid = solids[0].cut(*solids[1:]) else: @@ -7483,14 +7274,11 @@ def extrude_aux_spine( ).wrapped # extrude the outer wire - outer_solid = extrude_aux_spine( - outer_wire.wrapped, straight_spine_w, aux_spine_w - ) + outer_solid = extrude_aux_spine(outer_wire.wrapped, straight_spine_w, aux_spine_w) # extrude inner wires inner_solids = [ - Shape(extrude_aux_spine(w.wrapped, straight_spine_w, aux_spine_w)) - for w in inner_wires + Shape(extrude_aux_spine(w.wrapped, straight_spine_w, aux_spine_w)) for w in inner_wires ] # combine the inner solids into compound @@ -7532,9 +7320,7 @@ def extrude_until( max_dimension = Compound([section, target_object]).bounding_box().diagonal clipping_direction = ( - direction * max_dimension - if until == Until.NEXT - else -direction * max_dimension + direction * max_dimension if until == Until.NEXT else -direction * max_dimension ) direction_axis = Axis(section.center(), clipping_direction) # Create a linear extrusion to start @@ -7550,18 +7336,12 @@ def extrude_until( else: faces += face.faces() - clip_faces = [ - f - for f in faces - if not (f.is_planar and f.normal_at().dot(direction) == 0.0) - ] + clip_faces = [f for f in faces if not (f.is_planar and f.normal_at().dot(direction) == 0.0)] if not clip_faces: raise ValueError("provided face does not intersect target_object") # Create the objects that will clip the linear extrusion - clipping_objects = [ - Solid.extrude(f, clipping_direction).fix() for f in clip_faces - ] + clipping_objects = [Solid.extrude(f, clipping_direction).fix() for f in clip_faces] clipping_objects = [o for o in clipping_objects if o.volume > 1e-9] if until == Until.NEXT: @@ -7571,11 +7351,7 @@ def extrude_until( # thus they could be non manifold which results failed boolean operations # - so skip these objects try: - extrusion = ( - extrusion.cut(clipping_object) - .solids() - .sort_by(direction_axis)[0] - ) + extrusion = extrusion.cut(clipping_object).solids().sort_by(direction_axis)[0] except: # pylint: disable=bare-except warnings.warn("clipping error - extrusion may be incorrect") else: @@ -7583,9 +7359,7 @@ def extrude_until( for clipping_object in clipping_objects: try: extrusion_parts.append( - extrusion.intersect(clipping_object) - .solids() - .sort_by(direction_axis)[0] + extrusion.intersect(clipping_object).solids().sort_by(direction_axis)[0] ) except: # pylint: disable=bare-except warnings.warn("clipping error - extrusion may be incorrect") @@ -7759,9 +7533,7 @@ def sweep_multi( for profile in profiles: path_as_wire = ( - profile.wrapped - if isinstance(profile, Wire) - else profile.outer_wire().wrapped + profile.wrapped if isinstance(profile, Wire) else profile.outer_wire().wrapped ) builder.Add(path_as_wire, translate, rotate) @@ -7860,9 +7632,7 @@ def center(self) -> Vector: """The center of a vertex is itself!""" return Vector(self) - def __add__( - self, other: Union[Vertex, Vector, Tuple[float, float, float]] - ) -> Vertex: + def __add__(self, other: Union[Vertex, Vector, Tuple[float, float, float]]) -> Vertex: """Add Add to a Vertex with a Vertex, Vector or Tuple @@ -7886,9 +7656,7 @@ def __add__( new_vertex = Vertex(self.X + other.X, self.Y + other.Y, self.Z + other.Z) elif isinstance(other, (Vector, tuple)): new_other = Vector(other) - new_vertex = Vertex( - self.X + new_other.X, self.Y + new_other.Y, self.Z + new_other.Z - ) + new_vertex = Vertex(self.X + new_other.X, self.Y + new_other.Y, self.Z + new_other.Z) else: raise TypeError( "Vertex addition only supports Vertex,Vector or tuple(float,float,float) as input" @@ -7916,9 +7684,7 @@ def __sub__(self, other: Union[Vertex, Vector, tuple]) -> Vertex: new_vertex = Vertex(self.X - other.X, self.Y - other.Y, self.Z - other.Z) elif isinstance(other, (Vector, tuple)): new_other = Vector(other) - new_vertex = Vertex( - self.X - new_other.X, self.Y - new_other.Y, self.Z - new_other.Z - ) + new_vertex = Vertex(self.X - new_other.X, self.Y - new_other.Y, self.Z - new_other.Z) else: raise TypeError( "Vertex subtraction only supports Vertex,Vector or tuple(float,float,float)" @@ -8143,9 +7909,7 @@ def to_wire(self) -> Wire: return self @classmethod - def combine( - cls, wires: Iterable[Union[Wire, Edge]], tol: float = 1e-9 - ) -> ShapeList[Wire]: + def combine(cls, wires: Iterable[Union[Wire, Edge]], tol: float = 1e-9) -> ShapeList[Wire]: """combine Combine a list of wires and edges into a list of Wires. @@ -8267,16 +8031,8 @@ def trim(self: Wire, start: float, end: float) -> Wire: u = self.param_at_point(e.position_at(0)) v = self.param_at_point(e.position_at(1)) if self.is_closed: # Avoid two beginnings or ends - u = ( - 1 - u - if found_end_of_wire and (isclose_b(u, 0) or isclose_b(u, 1)) - else u - ) - v = ( - 1 - v - if found_end_of_wire and (isclose_b(v, 0) or isclose_b(v, 1)) - else v - ) + u = 1 - u if found_end_of_wire and (isclose_b(u, 0) or isclose_b(u, 1)) else u + v = 1 - v if found_end_of_wire and (isclose_b(v, 0) or isclose_b(v, 1)) else v found_end_of_wire = ( isclose_b(u, 0) or isclose_b(u, 1) @@ -8301,9 +8057,7 @@ def trim(self: Wire, start: float, end: float) -> Wire: elif start >= u and end <= v: # Wire trimmed to single Edge u_edge = e.param_at_point(self.position_at(start)) v_edge = e.param_at_point(self.position_at(end)) - u_edge, v_edge = ( - (v_edge, u_edge) if u_edge > v_edge else (u_edge, v_edge) - ) + u_edge, v_edge = (v_edge, u_edge) if u_edge > v_edge else (u_edge, v_edge) new_edges.append(e.trim(u_edge, v_edge)) elif start <= u: # keep start of Edge @@ -8320,9 +8074,7 @@ def trim(self: Wire, start: float, end: float) -> Wire: def order_edges(self) -> ShapeList[Edge]: """Return the edges in self ordered by wire direction and orientation""" - ordered_edges = [ - e if e.is_forward else e.reversed() for e in self.edges().sort_by(self) - ] + ordered_edges = [e if e.is_forward else e.reversed() for e in self.edges().sort_by(self)] return ShapeList(ordered_edges) @classmethod @@ -8679,15 +8431,11 @@ def make_convex_hull(cls, edges: Iterable[Edge], tolerance: float = 1e-3) -> Wir trim_data[edge] = f_points connecting_edges = [ - Edge.make_line( - edges[line[0][0]] @ line[0][1], edges[line[1][0]] @ line[1][1] - ) + Edge.make_line(edges[line[0][0]] @ line[0][1], edges[line[1][0]] @ line[1][1]) for line in connecting_edge_data ] trimmed_edges = [ - edges[edge].trim( - points_lookup[trim_pair[0]][1], points_lookup[trim_pair[1]][1] - ) + edges[edge].trim(points_lookup[trim_pair[0]][1], points_lookup[trim_pair[1]][1]) for edge, trim_pairs in trim_data.items() for trim_pair in trim_pairs ] @@ -8771,9 +8519,7 @@ def project_to_shape( for output_wire in output_wires: output_wire_center = output_wire.center() if direction_vector is not None: - output_wire_direction = ( - output_wire_center - planar_wire_center - ).normalized() + output_wire_direction = (output_wire_center - planar_wire_center).normalized() if output_wire_direction.dot(direction_vector) >= 0: output_wires_distances.append( ( @@ -8852,6 +8598,57 @@ def symbol(self) -> Compound: # pragma: no cover raise NotImplementedError +def _make_loft( + objs: Iterable[Union[Vertex, Wire]], + filled: bool, + ruled: bool = False, +) -> TopoDS_Shape: + """make loft + + Makes a loft from a list of wires and vertices. + Vertices can appear only at the beginning or end of the list, but cannot appear consecutively within the list + nor between wires. + + Args: + wires (list[Wire]): section perimeters + ruled (bool, optional): stepped or smooth. Defaults to False (smooth). + + Raises: + ValueError: Too few wires + + Returns: + TopoDS_Shape: Lofted object + """ + if len(objs) < 2: + raise ValueError("More than one wire is required") + vertices = [obj for obj in objs if isinstance(obj, Vertex)] + vertex_count = len(vertices) + + if vertex_count > 2: + raise ValueError("Only two vertices are allowed") + + if vertex_count == 1 and not (isinstance(objs[0], Vertex) or isinstance(objs[-1], Vertex)): + raise ValueError("The vertex must be either at the beginning or end of the list") + + if vertex_count == 2: + if len(objs) == 2: + raise ValueError("You can't have only 2 vertices to loft; try adding some wires") + if not (isinstance(objs[0], Vertex) and isinstance(objs[-1], Vertex)): + raise ValueError("The vertices must be at the beginning and end of the list") + + loft_builder = BRepOffsetAPI_ThruSections(filled, ruled) + + for obj in objs: + if isinstance(obj, Vertex): + loft_builder.AddVertex(obj.wrapped) + elif isinstance(obj, Wire): + loft_builder.AddWire(obj.wrapped) + + loft_builder.Build() + + return loft_builder.Shape() + + def downcast(obj: TopoDS_Shape) -> TopoDS_Shape: """Downcasts a TopoDS object to suitable specialized type diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index 1790861f..b9ffe1c4 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -296,9 +296,7 @@ def test_axis_is_parallel(self): def test_axis_angle_between(self): self.assertAlmostEqual(Axis.X.angle_between(Axis.Y), 90, 5) - self.assertAlmostEqual( - Axis.X.angle_between(Axis((1, 1, 1), (-1, 0, 0))), 180, 5 - ) + self.assertAlmostEqual(Axis.X.angle_between(Axis((1, 1, 1), (-1, 0, 0))), 180, 5) def test_axis_reverse(self): self.assertVectorAlmostEquals(Axis.X.reverse().direction, (-1, 0, 0), 5) @@ -426,9 +424,7 @@ def test_basic_bounding_box(self): def test_bounding_box_repr(self): bb = Solid.make_box(1, 1, 1).bounding_box() - self.assertEqual( - repr(bb), "bbox: 0.0 <= x <= 1.0, 0.0 <= y <= 1.0, 0.0 <= z <= 1.0" - ) + self.assertEqual(repr(bb), "bbox: 0.0 <= x <= 1.0, 0.0 <= y <= 1.0, 0.0 <= z <= 1.0") def test_center_of_boundbox(self): self.assertVectorAlmostEquals( @@ -719,22 +715,16 @@ def test_to_tuple(self): def test_hex(self): c = Color(0x996692) - self.assertTupleAlmostEquals( - tuple(c), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), 5 - ) + self.assertTupleAlmostEquals(tuple(c), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), 5) c = Color(0x006692, 0x80) - self.assertTupleAlmostEquals( - tuple(c), (0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF), 5 - ) + self.assertTupleAlmostEquals(tuple(c), (0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF), 5) c = Color(0x006692, alpha=0x80) self.assertTupleAlmostEquals(tuple(c), (0, 102 / 255, 146 / 255, 128 / 255), 5) c = Color(color_code=0x996692, alpha=0xCC) - self.assertTupleAlmostEquals( - tuple(c), (153 / 255, 102 / 255, 146 / 255, 204 / 255), 5 - ) + self.assertTupleAlmostEquals(tuple(c), (153 / 255, 102 / 255, 146 / 255, 204 / 255), 5) c = Color(0.0, 0.0, 1.0, 1.0) self.assertTupleAlmostEquals(tuple(c), (0, 0, 1, 1), 5) @@ -770,9 +760,7 @@ def test_make_text(self): arc = Edge.make_three_point_arc((-50, 0, 0), (0, 20, 0), (50, 0, 0)) text = Compound.make_text("test", 10, text_path=arc) self.assertEqual(len(text.faces()), 4) - text = Compound.make_text( - "test", 10, align=(Align.MAX, Align.MAX), text_path=arc - ) + text = Compound.make_text("test", 10, align=(Align.MAX, Align.MAX), text_path=arc) self.assertEqual(len(text.faces()), 4) def test_fuse(self): @@ -810,9 +798,7 @@ def test_center(self): ] ) self.assertVectorAlmostEquals(test_compound.center(CenterOf.MASS), (1, 0, 0), 5) - self.assertVectorAlmostEquals( - test_compound.center(CenterOf.BOUNDING_BOX), (4.25, 0, 0), 5 - ) + self.assertVectorAlmostEquals(test_compound.center(CenterOf.BOUNDING_BOX), (4.25, 0, 0), 5) with self.assertRaises(ValueError): test_compound.center(CenterOf.GEOMETRY) @@ -885,9 +871,7 @@ def test_first_level_shapes(self): class TestEdge(DirectApiTestCase): def test_close(self): - self.assertAlmostEqual( - Edge.make_circle(1, end_angle=180).close().length, math.pi + 2, 5 - ) + self.assertAlmostEqual(Edge.make_circle(1, end_angle=180).close().length, math.pi + 2, 5) self.assertAlmostEqual(Edge.make_circle(1).close().length, 2 * math.pi, 5) def test_make_half_circle(self): @@ -931,13 +915,9 @@ def test_spline_with_parameters(self): ) self.assertVectorAlmostEquals(spline.end_point(), (2, 0, 0), 5) with self.assertRaises(ValueError): - Edge.make_spline( - points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], parameters=[0.0, 1.0] - ) + Edge.make_spline(points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], parameters=[0.0, 1.0]) with self.assertRaises(ValueError): - Edge.make_spline( - points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], tangents=[(1, 1, 0)] - ) + Edge.make_spline(points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], tangents=[(1, 1, 0)]) def test_spline_approx(self): spline = Edge.make_spline_approx([(0, 0), (1, 1), (2, 1), (3, 0)]) @@ -1019,12 +999,8 @@ def test_find_intersection_points(self): def test_trim(self): line = Edge.make_line((-2, 0), (2, 0)) - self.assertVectorAlmostEquals( - line.trim(0.25, 0.75).position_at(0), (-1, 0, 0), 5 - ) - self.assertVectorAlmostEquals( - line.trim(0.25, 0.75).position_at(1), (1, 0, 0), 5 - ) + self.assertVectorAlmostEquals(line.trim(0.25, 0.75).position_at(0), (-1, 0, 0), 5) + self.assertVectorAlmostEquals(line.trim(0.25, 0.75).position_at(1), (1, 0, 0), 5) with self.assertRaises(ValueError): line.trim(0.75, 0.25) @@ -1041,9 +1017,7 @@ def test_trim_to_length(self): e2_trim.position_at(0), Vector(10, 0, 0).rotate(Axis.Z, 45), 5 ) - e3 = Edge.make_spline( - [(0, 10, 0), (-4, 5, 2), (0, 0, 0)], tangents=[(-1, 0), (1, 0)] - ) + e3 = Edge.make_spline([(0, 10, 0), (-4, 5, 2), (0, 0, 0)], tangents=[(-1, 0), (1, 0)]) e3_trim = e3.trim_to_length(0, 7) self.assertAlmostEqual(e3_trim.length, 7, 5) @@ -1088,9 +1062,7 @@ def test_distribute_locations2(self): def test_find_tangent(self): circle = Edge.make_circle(1) parm = circle.find_tangent(135)[0] - self.assertVectorAlmostEquals( - circle @ parm, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 5 - ) + self.assertVectorAlmostEquals(circle @ parm, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 5) line = Edge.make_line((0, 0), (1, 1)) parm = line.find_tangent(45)[0] self.assertAlmostEqual(parm, 0, 5) @@ -1156,9 +1128,7 @@ def test_make_surface_from_curves(self): def test_center(self): test_face = Face(Wire.make_polygon([(0, 0), (1, 0), (1, 1), (0, 0)])) - self.assertVectorAlmostEquals( - test_face.center(CenterOf.MASS), (2 / 3, 1 / 3, 0), 1 - ) + self.assertVectorAlmostEquals(test_face.center(CenterOf.MASS), (2 / 3, 1 / 3, 0), 1) self.assertVectorAlmostEquals( test_face.center(CenterOf.BOUNDING_BOX), (0.5, 0.5, 0), @@ -1171,18 +1141,14 @@ def test_face_volume(self): def test_chamfer_2d(self): test_face = Face.make_rect(10, 10) - test_face = test_face.chamfer_2d( - distance=1, distance2=2, vertices=test_face.vertices() - ) + test_face = test_face.chamfer_2d(distance=1, distance2=2, vertices=test_face.vertices()) self.assertAlmostEqual(test_face.area, 100 - 4 * 0.5 * 1 * 2) def test_chamfer_2d_reference(self): test_face = Face.make_rect(10, 10) edge = test_face.edges().sort_by(Axis.Y)[0] vertex = edge.vertices().sort_by(Axis.X)[0] - test_face = test_face.chamfer_2d( - distance=1, distance2=2, vertices=[vertex], edge=edge - ) + test_face = test_face.chamfer_2d(distance=1, distance2=2, vertices=[vertex], edge=edge) self.assertAlmostEqual(test_face.area, 100 - 0.5 * 1 * 2) self.assertAlmostEqual(test_face.edges().sort_by(Axis.Y)[0].length, 9) self.assertAlmostEqual(test_face.edges().sort_by(Axis.X)[0].length, 8) @@ -1191,9 +1157,7 @@ def test_chamfer_2d_reference_inverted(self): test_face = Face.make_rect(10, 10) edge = test_face.edges().sort_by(Axis.Y)[0] vertex = edge.vertices().sort_by(Axis.X)[0] - test_face = test_face.chamfer_2d( - distance=2, distance2=1, vertices=[vertex], edge=edge - ) + test_face = test_face.chamfer_2d(distance=2, distance2=1, vertices=[vertex], edge=edge) self.assertAlmostEqual(test_face.area, 100 - 0.5 * 1 * 2) self.assertAlmostEqual(test_face.edges().sort_by(Axis.Y)[0].length, 8) self.assertAlmostEqual(test_face.edges().sort_by(Axis.X)[0].length, 9) @@ -1236,8 +1200,7 @@ def test_is_planar(self): mount = Solid.make_loft( [ Rectangle((1 + 16 + 4), 20, align=(Align.MIN, Align.CENTER)).wire(), - Pos(1, 0, 4) - * Rectangle(16, 20, align=(Align.MIN, Align.CENTER)).wire(), + Pos(1, 0, 4) * Rectangle(16, 20, align=(Align.MIN, Align.CENTER)).wire(), ], ) self.assertTrue(all(f.is_planar for f in mount.faces())) @@ -1304,10 +1267,7 @@ def test_surface_from_array_of_points(self): def test_bezier_surface(self): points = [ - [ - (x, y, 2 if x == 0 and y == 0 else 1 if x == 0 or y == 0 else 0) - for x in range(-1, 2) - ] + [(x, y, 2 if x == 0 and y == 0 else 1 if x == 0 or y == 0 else 0) for x in range(-1, 2)] for y in range(-1, 2) ] surface = Face.make_bezier_surface(points) @@ -1316,9 +1276,7 @@ def test_bezier_surface(self): self.assertVectorAlmostEquals(bbox.max, (+1, +1, +1), 1) self.assertLess(bbox.max.Z, 1.0) - weights = [ - [2 if x == 0 or y == 0 else 1 for x in range(-1, 2)] for y in range(-1, 2) - ] + weights = [[2 if x == 0 or y == 0 else 1 for x in range(-1, 2)] for y in range(-1, 2)] surface = Face.make_bezier_surface(points, weights) bbox = surface.bounding_box() self.assertVectorAlmostEquals(bbox.min, (-1, -1, 0), 3) @@ -1361,14 +1319,10 @@ def test_make_holes(self): circumference = 2 * math.pi * radius hex_diagonal = 4 * (circumference / 10) / 3 cylinder = Solid.make_cylinder(radius, hex_diagonal * 5) - cylinder_wall: Face = cylinder.faces().filter_by(GeomType.PLANE, reverse=True)[ - 0 - ] + cylinder_wall: Face = cylinder.faces().filter_by(GeomType.PLANE, reverse=True)[0] with BuildSketch(Plane.XZ.offset(radius)) as hex: with Locations((0, hex_diagonal)): - RegularPolygon( - hex_diagonal * 0.4, 6, align=(Align.CENTER, Align.CENTER) - ) + RegularPolygon(hex_diagonal * 0.4, 6, align=(Align.CENTER, Align.CENTER)) hex_wire_vertical: Wire = hex.sketch.faces()[0].outer_wire() projected_wire: Wire = hex_wire_vertical.project_to_shape( @@ -1483,9 +1437,7 @@ def test_make_surface_error_checking(self): if platform.system() != "Darwin": with self.assertRaises(RuntimeError): - Face.make_surface( - [Edge.make_circle(50)], surface_points=[(0, 0, -50), (0, 0, 50)] - ) + Face.make_surface([Edge.make_circle(50)], surface_points=[(0, 0, -50), (0, 0, 50)]) with self.assertRaises(RuntimeError): Face.make_surface( @@ -1529,9 +1481,7 @@ def test_constructor(self): def test_normal_at(self): face = Face.make_rect(1, 1) self.assertVectorAlmostEquals(face.normal_at(0, 0), (0, 0, 1), 5) - self.assertVectorAlmostEquals( - face.normal_at(face.position_at(0, 0)), (0, 0, 1), 5 - ) + self.assertVectorAlmostEquals(face.normal_at(face.position_at(0, 0)), (0, 0, 1), 5) with self.assertRaises(ValueError): face.normal_at(0) with self.assertRaises(ValueError): @@ -1680,19 +1630,13 @@ def test_location(self): T = loc5.wrapped.Transformation().TranslationPart() self.assertTupleAlmostEquals((T.X(), T.Y(), T.Z()), (0, 0, 1), 6) - angle5 = ( - loc5.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG - ) + angle5 = loc5.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG self.assertAlmostEqual(15, angle5) - angle6 = ( - loc6.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG - ) + angle6 = loc6.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG self.assertAlmostEqual(30, angle6) - angle7 = ( - loc7.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG - ) + angle7 = loc7.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG self.assertAlmostEqual(30, angle7) # Test error handling on creation @@ -1766,9 +1710,7 @@ def test_location_parameters(self): Location(Intrinsic.XYZ) def test_location_repr_and_str(self): - self.assertEqual( - repr(Location()), "(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))" - ) + self.assertEqual(repr(Location()), "(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))") self.assertEqual( str(Location()), "Location: (position=(0.00, 0.00, 0.00), orientation=(-0.00, 0.00, -0.00))", @@ -2219,9 +2161,7 @@ def test_location_at(self): self.assertVectorAlmostEquals(loc.position, (0, 1, 0), 5) self.assertVectorAlmostEquals(loc.orientation, (0, -90, -90), 5) - loc = Edge.make_circle(1).location_at( - math.pi / 2, position_mode=PositionMode.LENGTH - ) + loc = Edge.make_circle(1).location_at(math.pi / 2, position_mode=PositionMode.LENGTH) self.assertVectorAlmostEquals(loc.position, (0, 1, 0), 5) self.assertVectorAlmostEquals(loc.orientation, (0, -90, -90), 5) @@ -2247,9 +2187,7 @@ def test_project(self): def test_project2(self): target = Cylinder(1, 10).faces().filter_by(GeomType.PLANE, reverse=True)[0] square = Wire.make_rect(1, 1, Plane.YZ).locate(Location((10, 0, 0))) - projections: list[Wire] = square.project( - target, direction=(-1, 0, 0), closest=False - ) + projections: list[Wire] = square.project(target, direction=(-1, 0, 0), closest=False) self.assertEqual(len(projections), 2) def test_is_forward(self): @@ -2268,10 +2206,7 @@ def test_offset_2d(self): self.assertEqual(len(offset_wire.edges().filter_by(GeomType.CIRCLE)), 2) offset_wire_right = base_wire.offset_2d(0.1, side=Side.RIGHT) self.assertAlmostEqual( - offset_wire_right.edges() - .filter_by(GeomType.CIRCLE) - .sort_by(SortBy.RADIUS)[-1] - .radius, + offset_wire_right.edges().filter_by(GeomType.CIRCLE).sort_by(SortBy.RADIUS)[-1].radius, 0.5, 4, ) @@ -2361,9 +2296,7 @@ def test_chamfer_asym_length_with_face(self): def test_chamfer_too_high_length(self): box = Solid.make_box(1, 1, 1) face = box.faces - self.assertRaises( - ValueError, box.chamfer, 2, None, box.edges().sort_by(Axis.Z)[-1:] - ) + self.assertRaises(ValueError, box.chamfer, 2, None, box.edges().sort_by(Axis.Z)[-1:]) def test_chamfer_edge_not_part_of_face(self): box = Solid.make_box(1, 1, 1) @@ -2383,9 +2316,7 @@ def test_is_inside(self): def test_dprism(self): # face f = Face.make_rect(0.5, 0.5) - d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism( - None, [f], additive=False - ) + d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(None, [f], additive=False) self.assertTrue(d.is_valid()) self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5) @@ -2408,9 +2339,7 @@ def test_dprism(self): # wire w = Face.make_rect(0.5, 0.5).outer_wire() - d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism( - None, [w], additive=False - ) + d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(None, [w], additive=False) self.assertTrue(d.is_valid()) self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5) @@ -2503,12 +2432,8 @@ def test_plane_init(self): p_from_named_loc = Plane(location=loc) for p in [p_from_loc, p_from_named_loc]: self.assertVectorAlmostEquals(p.origin, (0, 0, 0), 6) - self.assertVectorAlmostEquals( - p.x_dir, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6 - ) - self.assertVectorAlmostEquals( - p.y_dir, (-math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6 - ) + self.assertVectorAlmostEquals(p.x_dir, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6) + self.assertVectorAlmostEquals(p.y_dir, (-math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6) self.assertVectorAlmostEquals(p.z_dir, (0, 0, 1), 6) self.assertVectorAlmostEquals(loc.position, p.location.position, 6) self.assertVectorAlmostEquals(loc.orientation, p.location.orientation, 6) @@ -2518,12 +2443,8 @@ def test_plane_init(self): p = Plane(loc) self.assertVectorAlmostEquals(p.origin, (0, 2, -1), 6) self.assertVectorAlmostEquals(p.x_dir, (1, 0, 0), 6) - self.assertVectorAlmostEquals( - p.y_dir, (0, math.sqrt(2) / 2, math.sqrt(2) / 2), 6 - ) - self.assertVectorAlmostEquals( - p.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6 - ) + self.assertVectorAlmostEquals(p.y_dir, (0, math.sqrt(2) / 2, math.sqrt(2) / 2), 6) + self.assertVectorAlmostEquals(p.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6) self.assertVectorAlmostEquals(loc.position, p.location.position, 6) self.assertVectorAlmostEquals(loc.orientation, p.location.orientation, 6) @@ -2537,13 +2458,9 @@ def test_plane_init(self): self.assertVectorAlmostEquals(p.origin, (1, 2, 3), 6) self.assertVectorAlmostEquals(p.x_dir, (math.sqrt(2) / 2, 0.5, 0.5), 6) self.assertVectorAlmostEquals(p.y_dir, (-math.sqrt(2) / 2, 0.5, 0.5), 6) - self.assertVectorAlmostEquals( - p.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6 - ) + self.assertVectorAlmostEquals(p.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6) self.assertVectorAlmostEquals(f.location.position, p.location.position, 6) - self.assertVectorAlmostEquals( - f.location.orientation, p.location.orientation, 6 - ) + self.assertVectorAlmostEquals(f.location.orientation, p.location.orientation, 6) # from a face with x_dir f = Face.make_rect(1, 2) @@ -2573,48 +2490,32 @@ def test_plane_neg(self): self.assertVectorAlmostEquals(p2.origin, p.origin, 6) self.assertVectorAlmostEquals(p2.x_dir, p.x_dir, 6) self.assertVectorAlmostEquals(p2.z_dir, -p.z_dir, 6) - self.assertVectorAlmostEquals( - p2.y_dir, (-p.z_dir).cross(p.x_dir).normalized(), 6 - ) + self.assertVectorAlmostEquals(p2.y_dir, (-p.z_dir).cross(p.x_dir).normalized(), 6) p3 = p.reverse() self.assertVectorAlmostEquals(p3.origin, p.origin, 6) self.assertVectorAlmostEquals(p3.x_dir, p.x_dir, 6) self.assertVectorAlmostEquals(p3.z_dir, -p.z_dir, 6) - self.assertVectorAlmostEquals( - p3.y_dir, (-p.z_dir).cross(p.x_dir).normalized(), 6 - ) + self.assertVectorAlmostEquals(p3.y_dir, (-p.z_dir).cross(p.x_dir).normalized(), 6) def test_plane_mul(self): p = Plane(origin=(1, 2, 3), x_dir=(1, 0, 0), z_dir=(0, 0, 1)) p2 = p * Location((1, 2, -1), (0, 0, 45)) self.assertVectorAlmostEquals(p2.origin, (2, 4, 2), 6) - self.assertVectorAlmostEquals( - p2.x_dir, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6 - ) - self.assertVectorAlmostEquals( - p2.y_dir, (-math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6 - ) + self.assertVectorAlmostEquals(p2.x_dir, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6) + self.assertVectorAlmostEquals(p2.y_dir, (-math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6) self.assertVectorAlmostEquals(p2.z_dir, (0, 0, 1), 6) p2 = p * Location((1, 2, -1), (0, 45, 0)) self.assertVectorAlmostEquals(p2.origin, (2, 4, 2), 6) - self.assertVectorAlmostEquals( - p2.x_dir, (math.sqrt(2) / 2, 0, -math.sqrt(2) / 2), 6 - ) + self.assertVectorAlmostEquals(p2.x_dir, (math.sqrt(2) / 2, 0, -math.sqrt(2) / 2), 6) self.assertVectorAlmostEquals(p2.y_dir, (0, 1, 0), 6) - self.assertVectorAlmostEquals( - p2.z_dir, (math.sqrt(2) / 2, 0, math.sqrt(2) / 2), 6 - ) + self.assertVectorAlmostEquals(p2.z_dir, (math.sqrt(2) / 2, 0, math.sqrt(2) / 2), 6) p2 = p * Location((1, 2, -1), (45, 0, 0)) self.assertVectorAlmostEquals(p2.origin, (2, 4, 2), 6) self.assertVectorAlmostEquals(p2.x_dir, (1, 0, 0), 6) - self.assertVectorAlmostEquals( - p2.y_dir, (0, math.sqrt(2) / 2, math.sqrt(2) / 2), 6 - ) - self.assertVectorAlmostEquals( - p2.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6 - ) + self.assertVectorAlmostEquals(p2.y_dir, (0, math.sqrt(2) / 2, math.sqrt(2) / 2), 6) + self.assertVectorAlmostEquals(p2.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6) with self.assertRaises(TypeError): p2 * Vector(1, 1, 1) @@ -2669,9 +2570,7 @@ def test_shift_origin_axis(self): def test_shift_origin_vertex(self): box = Box(1, 1, 1, align=Align.MIN) front = box.faces().sort_by(Axis.X)[-1] - pln = Plane(front).shift_origin( - front.vertices().group_by(Axis.Z)[-1].sort_by(Axis.Y)[-1] - ) + pln = Plane(front).shift_origin(front.vertices().group_by(Axis.Z)[-1].sort_by(Axis.Y)[-1]) with BuildPart() as p: add(box) with BuildSketch(pln): @@ -2757,9 +2656,7 @@ def test_plane_equal(self): def test_plane_not_equal(self): # type difference for value in [None, 0, 1, "abc"]: - self.assertNotEqual( - Plane(origin=(0, 0, 0), x_dir=(1, 0, 0), z_dir=(0, 0, 1)), value - ) + self.assertNotEqual(Plane(origin=(0, 0, 0), x_dir=(1, 0, 0), z_dir=(0, 0, 1)), value) # origin difference self.assertNotEqual( Plane(origin=(0, 0, 0), x_dir=(1, 0, 0), z_dir=(0, 0, 1)), @@ -2782,9 +2679,7 @@ def test_to_location(self): self.assertVectorAlmostEquals(loc.orientation, (0, 0, 90), 5) def test_intersect(self): - self.assertVectorAlmostEquals( - Plane.XY.intersect(Axis((1, 2, 3), (0, 0, -1))), (1, 2, 0), 5 - ) + self.assertVectorAlmostEquals(Plane.XY.intersect(Axis((1, 2, 3), (0, 0, -1))), (1, 2, 0), 5) self.assertIsNone(Plane.XY.intersect(Axis((1, 2, 3), (0, 1, 0)))) self.assertEqual(Plane.XY.intersect(Plane.XZ), Axis.X) @@ -2801,9 +2696,7 @@ def test_from_non_planar_face(self): flat = Face.make_rect(1, 1) pln = Plane(flat) self.assertTrue(isinstance(pln, Plane)) - cyl = ( - Solid.make_cylinder(1, 4).faces().filter_by(GeomType.PLANE, reverse=True)[0] - ) + cyl = Solid.make_cylinder(1, 4).faces().filter_by(GeomType.PLANE, reverse=True)[0] with self.assertRaises(ValueError): pln = Plane(cyl) @@ -2854,8 +2747,7 @@ def test_flat_projection(self): .faces() ) projected_text_faces = [ - f.project_to_shape(sphere, projection_direction)[0] - for f in planar_text_faces + f.project_to_shape(sphere, projection_direction)[0] for f in planar_text_faces ] self.assertEqual(len(projected_text_faces), 4) @@ -2873,11 +2765,7 @@ def test_multiple_output_wires(self): def test_text_projection(self): sphere = Solid.make_sphere(50) arch_path = ( - sphere.cut( - Solid.make_cylinder( - 80, 100, Plane(origin=(-50, 0, -70), z_dir=(1, 0, 0)) - ) - ) + sphere.cut(Solid.make_cylinder(80, 100, Plane(origin=(-50, 0, -70), z_dir=(1, 0, 0)))) .edges() .sort_by(Axis.Z)[0] ) @@ -3038,9 +2926,7 @@ def test_split_by_perimeter(self): # Test 3 - Invalid, wire on shape edge target3 = Solid.make_cylinder(5, 10, Plane((0, 0, -5))) - square_projected = square.project_to_shape(target3, (-1, 0, 0))[0].unwrap( - fully=True - ) + square_projected = square.project_to_shape(target3, (-1, 0, 0))[0].unwrap(fully=True) project_perimeter = square_projected.outer_wire() inside3 = target3.split_by_perimeter(project_perimeter, Keep.INSIDE) self.assertIsNone(inside3) @@ -3075,9 +2961,7 @@ def test_max_fillet(self): max = test_object.max_fillet(test_object.edges()) self.assertAlmostEqual(max, max_values[i], 2) with self.assertRaises(RuntimeError): - test_solids[0].max_fillet( - test_solids[0].edges(), tolerance=1e-6, max_iterations=1 - ) + test_solids[0].max_fillet(test_solids[0].edges(), tolerance=1e-6, max_iterations=1) with self.assertRaises(ValueError): box = Solid.make_box(1, 1, 1) box.fillet(0.75, box.edges()) @@ -3157,9 +3041,7 @@ def test_distance_to(self): def test_intersection(self): box = Solid.make_box(1, 1, 1) - intersections = ( - box.intersect(Axis((0.5, 0.5, 4), (0, 0, -1))).vertices().sort_by(Axis.Z) - ) + intersections = box.intersect(Axis((0.5, 0.5, 4), (0, 0, -1))).vertices().sort_by(Axis.Z) self.assertVectorAlmostEquals(intersections[0], (0.5, 0.5, 0), 5) self.assertVectorAlmostEquals(intersections[1], (0.5, 0.5, 1), 5) @@ -3259,15 +3141,10 @@ def test_manifold(self): self.assertTrue(Solid.make_box(1, 1, 1).is_manifold) self.assertTrue(Solid.make_box(1, 1, 1).shell().is_manifold) self.assertFalse( - Solid.make_box(1, 1, 1) - .shell() - .cut(Solid.make_box(0.5, 0.5, 0.5)) - .is_manifold + Solid.make_box(1, 1, 1).shell().cut(Solid.make_box(0.5, 0.5, 0.5)).is_manifold ) self.assertTrue( - Compound( - children=[Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)] - ).is_manifold + Compound(children=[Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)]).is_manifold ) def test_inherit_color(self): @@ -3333,9 +3210,7 @@ def test_copy_attributes_to(self): box.topo_parent = box2 blank = Compound() - box.copy_attributes_to( - blank, ["color", "label", "joints", "children", "topo_parent"] - ) + box.copy_attributes_to(blank, ["color", "label", "joints", "children", "topo_parent"]) self.assertEqual(blank.label, "box") self.assertTrue(all(c1 == c2 for c1, c2 in zip(blank.color, Color("Red")))) self.assertTrue(all(j1 == j2 for j1, j2 in zip(blank.joints, ["j1", "j2"]))) @@ -3372,9 +3247,7 @@ def test_sort_by(self): self.assertAlmostEqual(faces[-1].area, 2, 5) def test_filter_by_geomtype(self): - non_planar_faces = ( - Solid.make_cylinder(1, 1).faces().filter_by(GeomType.PLANE, reverse=True) - ) + non_planar_faces = Solid.make_cylinder(1, 1).faces().filter_by(GeomType.PLANE, reverse=True) self.assertEqual(len(non_planar_faces), 1) self.assertAlmostEqual(non_planar_faces[0].area, 2 * math.pi, 5) @@ -3398,9 +3271,7 @@ def test_filter_by_callable_predicate(self): self.assertEqual(len(shapelist.filter_by(lambda s: s.label == "B")), 1) def test_first_last(self): - vertices = ( - Solid.make_box(1, 1, 1).vertices().sort_by(Axis((0, 0, 0), (1, 1, 1))) - ) + vertices = Solid.make_box(1, 1, 1).vertices().sort_by(Axis((0, 0, 0), (1, 1, 1))) self.assertVectorAlmostEquals(vertices.last, (1, 1, 1), 5) self.assertVectorAlmostEquals(vertices.first, (0, 0, 0), 5) @@ -3411,12 +3282,7 @@ def test_group_by(self): edges = Solid.make_box(1, 1, 1).edges().group_by(SortBy.LENGTH) self.assertEqual(len(edges[0]), 12) - edges = ( - Solid.make_cone(2, 1, 2) - .edges() - .filter_by(GeomType.CIRCLE) - .group_by(SortBy.RADIUS) - ) + edges = Solid.make_cone(2, 1, 2).edges().filter_by(GeomType.CIRCLE).group_by(SortBy.RADIUS) self.assertEqual(len(edges[0]), 1) edges = (Solid.make_cone(2, 1, 2).edges() | GeomType.CIRCLE) << SortBy.RADIUS @@ -3505,9 +3371,7 @@ def test_group_by_str_repr(self): " [," " ]]" ) - self.assertDunderReprEqual( - repr(nonagon.edges().group_by(Axis.X)), expected_repr - ) + self.assertDunderReprEqual(repr(nonagon.edges().group_by(Axis.X)), expected_repr) f = io.StringIO() p = pretty.PrettyPrinter(f) @@ -3520,9 +3384,7 @@ def test_distance(self): obj = (-0.2, 0.1, 0.5) edges = box.edges().sort_by_distance(obj) distances = [Vertex(*obj).distance_to(edge) for edge in edges] - self.assertTrue( - all([distances[i] >= distances[i - 1] for i in range(1, len(edges))]) - ) + self.assertTrue(all([distances[i] >= distances[i - 1] for i in range(1, len(edges))])) def test_distance_reverse(self): with BuildPart() as box: @@ -3530,9 +3392,7 @@ def test_distance_reverse(self): obj = (-0.2, 0.1, 0.5) edges = box.edges().sort_by_distance(obj, reverse=True) distances = [Vertex(*obj).distance_to(edge) for edge in edges] - self.assertTrue( - all([distances[i] <= distances[i - 1] for i in range(1, len(edges))]) - ) + self.assertTrue(all([distances[i] <= distances[i - 1] for i in range(1, len(edges))])) def test_distance_equal(self): with BuildPart() as box: @@ -3577,9 +3437,7 @@ def test_faces(self): self.assertEqual(len(sl.faces()), 9) def test_face(self): - sl = ShapeList( - [Vertex(1, 1, 1), Edge.make_line((0, 0), (1, 1)), Face.make_rect(2, 1)] - ) + sl = ShapeList([Vertex(1, 1, 1), Edge.make_line((0, 0), (1, 1)), Face.make_rect(2, 1)]) self.assertAlmostEqual(sl.face().area, 2 * 1, 5) sl = ShapeList([Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)]) with self.assertWarns(UserWarning): @@ -3686,6 +3544,14 @@ def test_sweep(self): self.assertEqual(len(sweep_c2_c1.faces()), 2) self.assertEqual(len(sweep_w_w.faces()), 4) self.assertEqual(len(sweep_c2_c2.faces()), 4) + + def test_loft(self): + r = 3 + h = 2 + loft = Shell.make_loft([Wire.make_circle(r,Plane((0,0,h))), Wire.make_circle(r) ]) + self.assertEqual(loft.volume, 0, "A shell has no volume") + cylinder_area = 2*math.pi*r*h + self.assertAlmostEqual(loft.area, cylinder_area) class TestSolid(DirectApiTestCase): @@ -3735,9 +3601,7 @@ def test_extrude_taper(self): for taper in [10, -10]: offset_amt = -direction.length * math.tan(math.radians(taper)) for face in [rect, flipped]: - with self.subTest( - f"{direction=}, {taper=}, flipped={face==flipped}" - ): + with self.subTest(f"{direction=}, {taper=}, flipped={face==flipped}"): taper_solid = Solid.extrude_taper(face, direction, taper) # V = 1/3 × h × (a² + b² + ab) h = Vector(direction).length @@ -3747,14 +3611,10 @@ def test_extrude_taper(self): bbox = taper_solid.bounding_box() size = max(1, b) / 2 if direction.Z > 0: - self.assertVectorAlmostEquals( - bbox.min, (-size, -size, 0), 1 - ) + self.assertVectorAlmostEquals(bbox.min, (-size, -size, 0), 1) self.assertVectorAlmostEquals(bbox.max, (size, size, h), 1) else: - self.assertVectorAlmostEquals( - bbox.min, (-size, -size, -h), 1 - ) + self.assertVectorAlmostEquals(bbox.min, (-size, -size, -h), 1) self.assertVectorAlmostEquals(bbox.max, (size, size, 0), 1) def test_extrude_taper_with_hole(self): @@ -3805,14 +3665,22 @@ def test_extrude_linear_with_rotation(self): self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5) def test_make_loft(self): - loft = Solid.make_loft( - [Wire.make_rect(2, 2), Wire.make_circle(1, Plane((0, 0, 1)))] - ) + loft = Solid.make_loft([Wire.make_rect(2, 2), Wire.make_circle(1, Plane((0, 0, 1)))]) self.assertAlmostEqual(loft.volume, (4 + math.pi) / 2, 1) with self.assertRaises(ValueError): Solid.make_loft([Wire.make_rect(1, 1)]) + def test_make_loft_with_vertices(self): + loft = Solid.make_loft([Vertex(0, 0, -1), Wire.make_rect(1, 1.5), Vertex(0, 0, 1)], True) + self.assertAlmostEqual(loft.volume, 1, 5) + + with self.assertRaises(ValueError): + Solid.make_loft([Wire.make_rect(1, 1), Vertex(0, 0, 1), Wire.make_rect(1, 1)]) + + with self.assertRaises(ValueError): + Solid.make_loft([Vertex(0, 0, 1), Vertex(0, 0, 2)]) + def test_extrude_until(self): square = Face.make_rect(1, 1) box = Solid.make_box(4, 4, 1, Plane((-2, -2, 3))) @@ -3889,9 +3757,7 @@ def test_vector_rotate(self): vector_x = Vector(1, 0, 1).rotate(Axis.X, 45) vector_y = Vector(1, 2, 1).rotate(Axis.Y, 45) vector_z = Vector(-1, -1, 3).rotate(Axis.Z, 45) - self.assertVectorAlmostEquals( - vector_x, (1, -math.sqrt(2) / 2, math.sqrt(2) / 2), 7 - ) + self.assertVectorAlmostEquals(vector_x, (1, -math.sqrt(2) / 2, math.sqrt(2) / 2), 7) self.assertVectorAlmostEquals(vector_y, (math.sqrt(2), 2, 0), 7) self.assertVectorAlmostEquals(vector_z, (0, -math.sqrt(2), 3), 7) @@ -4043,21 +3909,11 @@ def test_vector_transform(self): pxy = Plane.XY pxy_o1 = Plane.XY.offset(1) self.assertEqual(a.transform(pxy.forward_transform, is_direction=False), a) - self.assertEqual( - a.transform(pxy.forward_transform, is_direction=True), a.normalized() - ) - self.assertEqual( - a.transform(pxy_o1.forward_transform, is_direction=False), Vector(1, 2, 2) - ) - self.assertEqual( - a.transform(pxy_o1.forward_transform, is_direction=True), a.normalized() - ) - self.assertEqual( - a.transform(pxy_o1.reverse_transform, is_direction=False), Vector(1, 2, 4) - ) - self.assertEqual( - a.transform(pxy_o1.reverse_transform, is_direction=True), a.normalized() - ) + self.assertEqual(a.transform(pxy.forward_transform, is_direction=True), a.normalized()) + self.assertEqual(a.transform(pxy_o1.forward_transform, is_direction=False), Vector(1, 2, 2)) + self.assertEqual(a.transform(pxy_o1.forward_transform, is_direction=True), a.normalized()) + self.assertEqual(a.transform(pxy_o1.reverse_transform, is_direction=False), Vector(1, 2, 4)) + self.assertEqual(a.transform(pxy_o1.reverse_transform, is_direction=True), a.normalized()) def test_intersect(self): v1 = Vector(1, 2, 3) @@ -4073,12 +3929,8 @@ def test_intersect(self): self.assertVectorAlmostEquals(v1 & Plane((1, 2, 3)), (1, 2, 3), 5) self.assertIsNone(v1 & Plane.XY) - self.assertVectorAlmostEquals( - (v1 & Solid.make_box(2, 4, 5)).vertex(), (1, 2, 3), 5 - ) - self.assertTrue( - len(v1.intersect(Solid.make_box(0.5, 0.5, 0.5)).vertices()) == 0 - ) + self.assertVectorAlmostEquals((v1 & Solid.make_box(2, 4, 5)).vertex(), (1, 2, 3), 5) + self.assertTrue(len(v1.intersect(Solid.make_box(0.5, 0.5, 0.5)).vertices()) == 0) class TestVectorLike(DirectApiTestCase): @@ -4122,12 +3974,8 @@ def test_vertex_volume(self): def test_vertex_add(self): test_vertex = Vertex(0, 0, 0) - self.assertVectorAlmostEquals( - Vector(test_vertex + (100, -40, 10)), (100, -40, 10), 7 - ) - self.assertVectorAlmostEquals( - Vector(test_vertex + Vector(100, -40, 10)), (100, -40, 10), 7 - ) + self.assertVectorAlmostEquals(Vector(test_vertex + (100, -40, 10)), (100, -40, 10), 7) + self.assertVectorAlmostEquals(Vector(test_vertex + Vector(100, -40, 10)), (100, -40, 10), 7) self.assertVectorAlmostEquals( Vector(test_vertex + Vertex(100, -40, 10)), (100, -40, 10), @@ -4138,9 +3986,7 @@ def test_vertex_add(self): def test_vertex_sub(self): test_vertex = Vertex(0, 0, 0) - self.assertVectorAlmostEquals( - Vector(test_vertex - (100, -40, 10)), (-100, 40, -10), 7 - ) + self.assertVectorAlmostEquals(Vector(test_vertex - (100, -40, 10)), (-100, 40, -10), 7) self.assertVectorAlmostEquals( Vector(test_vertex - Vector(100, -40, 10)), (-100, 40, -10), 7 ) @@ -4175,42 +4021,30 @@ def test_no_intersect(self): class TestWire(DirectApiTestCase): def test_ellipse_arc(self): full_ellipse = Wire.make_ellipse(2, 1) - half_ellipse = Wire.make_ellipse( - 2, 1, start_angle=0, end_angle=180, closed=True - ) + half_ellipse = Wire.make_ellipse(2, 1, start_angle=0, end_angle=180, closed=True) self.assertAlmostEqual(full_ellipse.area / 2, half_ellipse.area, 5) def test_stitch(self): - half_ellipse1 = Wire.make_ellipse( - 2, 1, start_angle=0, end_angle=180, closed=False - ) - half_ellipse2 = Wire.make_ellipse( - 2, 1, start_angle=180, end_angle=360, closed=False - ) + half_ellipse1 = Wire.make_ellipse(2, 1, start_angle=0, end_angle=180, closed=False) + half_ellipse2 = Wire.make_ellipse(2, 1, start_angle=180, end_angle=360, closed=False) ellipse = half_ellipse1.stitch(half_ellipse2) self.assertEqual(len(ellipse.wires()), 1) def test_fillet_2d(self): square = Wire.make_rect(1, 1) squaroid = square.fillet_2d(0.1, square.vertices()) - self.assertAlmostEqual( - squaroid.length, 4 * (1 - 2 * 0.1) + 2 * math.pi * 0.1, 5 - ) + self.assertAlmostEqual(squaroid.length, 4 * (1 - 2 * 0.1) + 2 * math.pi * 0.1, 5) def test_chamfer_2d(self): square = Wire.make_rect(1, 1) squaroid = square.chamfer_2d(0.1, 0.1, square.vertices()) - self.assertAlmostEqual( - squaroid.length, 4 * (1 - 2 * 0.1 + 0.1 * math.sqrt(2)), 5 - ) + self.assertAlmostEqual(squaroid.length, 4 * (1 - 2 * 0.1 + 0.1 * math.sqrt(2)), 5) def test_chamfer_2d_edge(self): square = Wire.make_rect(1, 1) edge = square.edges().sort_by(Axis.Y)[0] vertex = edge.vertices().sort_by(Axis.X)[0] - square = square.chamfer_2d( - distance=0.1, distance2=0.2, vertices=[vertex], edge=edge - ) + square = square.chamfer_2d(distance=0.1, distance2=0.2, vertices=[vertex], edge=edge) self.assertAlmostEqual(square.edges().sort_by(Axis.Y)[0].length, 0.9) def test_make_convex_hull(self): From 2a00c1e454d1eb71be70641c1a7113995297b01f Mon Sep 17 00:00:00 2001 From: Romain FERRU Date: Fri, 8 Nov 2024 20:18:12 +0100 Subject: [PATCH 2/3] ran black at 88 char per line --- src/build123d/topology.py | 541 ++++++++++++++++++++++++++++---------- tests/test_direct_api.py | 382 ++++++++++++++++++++------- 2 files changed, 692 insertions(+), 231 deletions(-) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 4b15d86f..23998462 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -415,7 +415,9 @@ def param_at(self, distance: float) -> float: curve = self._geom_adaptor() length = GCPnts_AbscissaPoint.Length_s(curve) - return GCPnts_AbscissaPoint(curve, length * distance, curve.FirstParameter()).Parameter() + return GCPnts_AbscissaPoint( + curve, length * distance, curve.FirstParameter() + ).Parameter() def tangent_at( self, @@ -564,7 +566,9 @@ def common_plane(self, *lines: Union[Edge, Wire]) -> Union[None, Plane]: # Note: BRepLib_FindSurface is not helpful as it requires the # Edges to form a surface perimeter. points: list[Vector] = [] - all_lines: list[Edge, Wire] = [line for line in [self, *lines] if line is not None] + all_lines: list[Edge, Wire] = [ + line for line in [self, *lines] if line is not None + ] if any([not isinstance(line, (Edge, Wire)) for line in all_lines]): raise ValueError("Only Edges or Wires are valid") @@ -592,7 +596,9 @@ def common_plane(self, *lines: Union[Edge, Wire]) -> Union[None, Plane]: for line in all_lines: num_points = 2 if line.geom_type == GeomType.LINE else 8 - points.extend([line.position_at(i / (num_points - 1)) for i in range(num_points)]) + points.extend( + [line.position_at(i / (num_points - 1)) for i in range(num_points)] + ) points = list(set(points)) # unique points extreme_areas = {} for subset in combinations(points, 3): @@ -780,7 +786,9 @@ def locations( list[Location]: A list of Location objects representing local coordinate systems at the specified distances. """ - return [self.location_at(d, position_mode, frame_method, planar) for d in distances] + return [ + self.location_at(d, position_mode, frame_method, planar) for d in distances + ] def __matmul__(self: Union[Edge, Wire], position: float) -> Vector: """Position on wire operator @""" @@ -861,7 +869,8 @@ def offset_2d( i = 0 for edge in offset_edges: if edge.geom_type == GeomType.CIRCLE and ( - edge.arc_center == line.position_at(0) or edge.arc_center == line.position_at(1) + edge.arc_center == line.position_at(0) + or edge.arc_center == line.position_at(1) ): i += 1 else: @@ -869,7 +878,10 @@ def offset_2d( edges_to_keep[0] += edges_to_keep[2] wires = [Wire(edges) for edges in edges_to_keep[0:2]] centers = [w.position_at(0.5) for w in wires] - angles = [line.tangent_at(0).get_signed_angle(c - line.position_at(0)) for c in centers] + angles = [ + line.tangent_at(0).get_signed_angle(c - line.position_at(0)) + for c in centers + ] if side == Side.LEFT: offset_wire = wires[int(angles[0] > angles[1])] else: @@ -891,7 +903,9 @@ def offset_2d( offset_edges = offset_wire.edges() return offset_edges[0] if len(offset_edges) == 1 else offset_wire - def perpendicular_line(self, length: float, u_value: float, plane: Plane = Plane.XY) -> Edge: + def perpendicular_line( + self, length: float, u_value: float, plane: Plane = Plane.XY + ) -> Edge: """perpendicular_line Create a line on the given plane perpendicular to and centered on beginning of self @@ -905,7 +919,9 @@ def perpendicular_line(self, length: float, u_value: float, plane: Plane = Plane Edge: perpendicular line """ start = self.position_at(u_value) - local_plane = Plane(origin=start, x_dir=self.tangent_at(u_value), z_dir=plane.z_dir) + local_plane = Plane( + origin=start, x_dir=self.tangent_at(u_value), z_dir=plane.z_dir + ) line = Edge.make_line( start + local_plane.y_dir * length / 2, start - local_plane.y_dir * length / 2, @@ -926,7 +942,9 @@ def project( """ - bldr = BRepProj_Projection(self.wrapped, face.wrapped, Vector(direction).to_dir()) + bldr = BRepProj_Projection( + self.wrapped, face.wrapped, Vector(direction).to_dir() + ) shapes = Compound(bldr.Shape()) # select the closest projection if requested @@ -1042,7 +1060,9 @@ def __max_fillet(window_min: float, window_max: float, current_iteration: int): if window_mid - window_min <= tolerance: return_value = window_mid else: - return_value = __max_fillet(window_mid, window_max, current_iteration + 1) + return_value = __max_fillet( + window_mid, window_max, current_iteration + 1 + ) return return_value if not self.is_valid(): @@ -1092,7 +1112,9 @@ def chamfer( # make a edge --> faces mapping edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape() - TopExp.MapShapesAndAncestors_s(self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map) + TopExp.MapShapesAndAncestors_s( + self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map + ) # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) @@ -1119,7 +1141,9 @@ def chamfer( if not new_shape.is_valid(): raise Standard_Failure except (StdFail_NotDone, Standard_Failure) as err: - raise ValueError("Failed creating a chamfer, try a smaller length value(s)") from err + raise ValueError( + "Failed creating a chamfer, try a smaller length value(s)" + ) from err return new_shape @@ -1270,7 +1294,9 @@ def offset_3d( try: offset_occt_solid = offset_builder.Shape() except (StdFail_NotDone, Standard_Failure) as err: - raise RuntimeError("offset Error, an alternative kind may resolve this error") from err + raise RuntimeError( + "offset Error, an alternative kind may resolve this error" + ) from err offset_solid = self.__class__(offset_occt_solid) @@ -1473,7 +1499,9 @@ def copy_attributes_to(self, target: Shape, attributes: list[str]): if not getattr(target, attr): setattr(target, attr, getattr(self, attr)) elif getattr(self, attr): - warnings.warn(f"Target does not have attribute '{attr}', skipping copy.") + warnings.warn( + f"Target does not have attribute '{attr}', skipping copy." + ) else: raise ValueError(f"Source does not have attribute '{attr}'") @@ -1497,7 +1525,9 @@ def is_manifold(self) -> bool: # Fill the map with edges and their associated faces in the given shape. Each edge in # the map is associated with a list of faces that share that edge. - TopExp.MapShapesAndAncestors_s(self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, shape_map) + TopExp.MapShapesAndAncestors_s( + self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, shape_map + ) # Iterate over the edges in the map and checks if each edge is non-degenerate and has # exactly two faces associated with it. @@ -1572,7 +1602,9 @@ def _build_tree( parent_node = tree[-1] while iterator.More(): child = iterator.Value() - if Shape._ordered_shapes.index(child.ShapeType()) <= Shape._ordered_shapes.index(limit): + if Shape._ordered_shapes.index( + child.ShapeType() + ) <= Shape._ordered_shapes.index(limit): Shape._build_tree(child, tree, parent_node, limit) iterator.Next() return tree @@ -1586,10 +1618,15 @@ def _show_tree(root_node, show_center: bool) -> str: size_tuples.append((root_node.height, len(root_node.label))) # pylint: disable=cell-var-from-loop size_tuples_per_level = [ - list(filter(lambda ll: ll[0] == l, size_tuples)) for l in range(root_node.height + 1) + list(filter(lambda ll: ll[0] == l, size_tuples)) + for l in range(root_node.height + 1) + ] + max_sizes_per_level = [ + max(4, max(l[1] for l in level)) for level in size_tuples_per_level + ] + level_sizes_per_level = [ + l + i * 4 for i, l in enumerate(reversed(max_sizes_per_level)) ] - max_sizes_per_level = [max(4, max(l[1] for l in level)) for level in size_tuples_per_level] - level_sizes_per_level = [l + i * 4 for i, l in enumerate(reversed(max_sizes_per_level))] tree_label_width = max(level_sizes_per_level) + 1 # Build the tree line by line @@ -1653,7 +1690,9 @@ def show_topology( show_center = False if show_center is None else show_center result = Shape._show_tree(self, show_center) else: - tree = Shape._build_tree(self.wrapped, tree=[], limit=inverse_shape_LUT[limit_class]) + tree = Shape._build_tree( + self.wrapped, tree=[], limit=inverse_shape_LUT[limit_class] + ) show_center = True if show_center is None else show_center result = Shape._show_tree(tree[0], show_center) return result @@ -1688,7 +1727,11 @@ def __add__(self, other: Union[list[Shape], Shape]) -> Self: sum_shape = self.fuse(*summands) # Simplify Compounds if possible - sum_shape = sum_shape.unwrap(fully=True) if isinstance(sum_shape, Compound) else sum_shape + sum_shape = ( + sum_shape.unwrap(fully=True) + if isinstance(sum_shape, Compound) + else sum_shape + ) if SkipClean.clean: sum_shape = sum_shape.clean() @@ -1735,7 +1778,9 @@ def __sub__(self, other: Union[Shape, Iterable[Shape]]) -> Self: # Simplify Compounds if possible difference = ( - difference.unwrap(fully=True) if isinstance(difference, Compound) else difference + difference.unwrap(fully=True) + if isinstance(difference, Compound) + else difference ) # To allow the @, % and ^ operators to work 1D objects must be type Curve if minuend_dim == 1: @@ -1755,7 +1800,11 @@ def __and__(self, other: Shape) -> Self: new_shape = new_shape.clean() # Simplify Compounds if possible - new_shape = new_shape.unwrap(fully=True) if isinstance(new_shape, Compound) else new_shape + new_shape = ( + new_shape.unwrap(fully=True) + if isinstance(new_shape, Compound) + else new_shape + ) # To allow the @, % and ^ operators to work 1D objects must be type Curve if self._dim == 1: @@ -1769,7 +1818,9 @@ def __rmul__(self, other): isinstance(other, (list, tuple)) and all([isinstance(o, (Location, Plane)) for o in other]) ): - raise ValueError("shapes can only be multiplied list of locations or planes") + raise ValueError( + "shapes can only be multiplied list of locations or planes" + ) return [loc * self for loc in other] def center(self) -> Vector: @@ -1855,7 +1906,9 @@ def export_stl( Returns: bool: Success """ - mesh = BRepMesh_IncrementalMesh(self.wrapped, tolerance, True, angular_tolerance, True) + mesh = BRepMesh_IncrementalMesh( + self.wrapped, tolerance, True, angular_tolerance, True + ) mesh.Perform() writer = StlAPI_Writer() @@ -2005,7 +2058,9 @@ def bounding_box(self, tolerance: float = None, optimal: bool = True) -> BoundBo Returns: BoundBox: A box sized to contain this Shape """ - return BoundBox._from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal) + return BoundBox._from_topo_ds( + self.wrapped, tolerance=tolerance, optimal=optimal + ) def mirror(self, mirror_plane: Plane = None) -> Self: """ @@ -2021,12 +2076,16 @@ def mirror(self, mirror_plane: Plane = None) -> Self: mirror_plane = Plane.XY transformation = gp_Trsf() - transformation.SetMirror(gp_Ax2(mirror_plane.origin.to_pnt(), mirror_plane.z_dir.to_dir())) + transformation.SetMirror( + gp_Ax2(mirror_plane.origin.to_pnt(), mirror_plane.z_dir.to_dir()) + ) return self._apply_transform(transformation) @staticmethod - def combined_center(objects: Iterable[Shape], center_of: CenterOf = CenterOf.MASS) -> Vector: + def combined_center( + objects: Iterable[Shape], center_of: CenterOf = CenterOf.MASS + ) -> Vector: """combined center Calculates the center of a multiple objects. @@ -2099,12 +2158,16 @@ def _entities(self, topo_type: Shapes) -> list[TopoDS_Shape]: while explorer.More(): item = explorer.Current() - out[item.HashCode(HASH_CODE_MAX)] = item # needed to avoid pseudo-duplicate entities + out[item.HashCode(HASH_CODE_MAX)] = ( + item # needed to avoid pseudo-duplicate entities + ) explorer.Next() return list(out.values()) - def _entities_from(self, child_type: Shapes, parent_type: Shapes) -> Dict[Shape, list[Shape]]: + def _entities_from( + self, child_type: Shapes, parent_type: Shapes + ) -> Dict[Shape, list[Shape]]: """This function is very slow on M1 macs and is currently unused""" res = TopTools_IndexedDataMapOfShapeListOfShape() @@ -2117,13 +2180,17 @@ def _entities_from(self, child_type: Shapes, parent_type: Shapes) -> Dict[Shape, out: Dict[Shape, list[Shape]] = {} for i in range(1, res.Extent() + 1): - out[Shape.cast(res.FindKey(i))] = [Shape.cast(el) for el in res.FindFromIndex(i)] + out[Shape.cast(res.FindKey(i))] = [ + Shape.cast(el) for el in res.FindFromIndex(i) + ] return out def vertices(self) -> ShapeList[Vertex]: """vertices - all the vertices in this Shape""" - vertex_list = ShapeList([Vertex(downcast(i)) for i in self._entities(Vertex.__name__)]) + vertex_list = ShapeList( + [Vertex(downcast(i)) for i in self._entities(Vertex.__name__)] + ) for vertex in vertex_list: vertex.topo_parent = self return vertex_list @@ -2714,7 +2781,11 @@ def split(self, surface: Union[Plane, Face], keep: Keep = Keep.TOP) -> Self: shape_list.Append(self.wrapped) # Define the splitting tool - tool = Face.make_plane(surface).wrapped if isinstance(surface, Plane) else surface.wrapped + tool = ( + Face.make_plane(surface).wrapped + if isinstance(surface, Plane) + else surface.wrapped + ) tool_list = TopTools_ListOfShape() tool_list.Append(tool) @@ -2750,7 +2821,9 @@ def split_by_perimeter( ) -> Union[Optional[Shell], Optional[Face]]: ... @overload - def split_by_perimeter(self, perimeter: Union[Edge, Wire], keep: Literal[Keep.BOTH]) -> tuple[ + def split_by_perimeter( + self, perimeter: Union[Edge, Wire], keep: Literal[Keep.BOTH] + ) -> tuple[ Union[Optional[Shell], Optional[Face]], Union[Optional[Shell], Optional[Face]], ]: ... @@ -2758,7 +2831,9 @@ def split_by_perimeter(self, perimeter: Union[Edge, Wire], keep: Literal[Keep.BO def split_by_perimeter( self, perimeter: Union[Edge, Wire] ) -> Union[Optional[Shell], Optional[Face]]: ... - def split_by_perimeter(self, perimeter: Union[Edge, Wire], keep: Keep = Keep.INSIDE): + def split_by_perimeter( + self, perimeter: Union[Edge, Wire], keep: Keep = Keep.INSIDE + ): """split_by_perimeter Divide the faces of this object into those within the perimeter @@ -2795,7 +2870,9 @@ def get(los: TopTools_ListOfShape, shape_cls) -> list: return shapes if keep not in {Keep.INSIDE, Keep.OUTSIDE, Keep.BOTH}: - raise ValueError("keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH") + raise ValueError( + "keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH" + ) # Process the perimeter if not perimeter.is_closed: @@ -2819,8 +2896,12 @@ def get(los: TopTools_ListOfShape, shape_cls) -> list: # Is left or right the inside? perimeter_length = perimeter.length - left_perimeter_length = sum(e.length for e in left.edges()) if not left is None else 0 - right_perimeter_length = sum(e.length for e in right.edges()) if not right is None else 0 + left_perimeter_length = ( + sum(e.length for e in left.edges()) if not left is None else 0 + ) + right_perimeter_length = ( + sum(e.length for e in right.edges()) if not right is None else 0 + ) left_inside = abs(perimeter_length - left_perimeter_length) < abs( perimeter_length - right_perimeter_length ) @@ -2874,7 +2955,9 @@ def mesh(self, tolerance: float, angular_tolerance: float = 0.1): """ if not BRepTools.Triangulation_s(self.wrapped, tolerance): - BRepMesh_IncrementalMesh(self.wrapped, tolerance, True, angular_tolerance, True) + BRepMesh_IncrementalMesh( + self.wrapped, tolerance, True, angular_tolerance, True + ) def tessellate( self, tolerance: float, angular_tolerance: float = 0.1 @@ -2895,7 +2978,9 @@ def tessellate( # add vertices vertices += [ Vector(v.X(), v.Y(), v.Z()) - for v in (poly.Node(i).Transformed(trsf) for i in range(1, poly.NbNodes() + 1)) + for v in ( + poly.Node(i).Transformed(trsf) for i in range(1, poly.NbNodes() + 1) + ) ] # add triangles triangles += [ @@ -2919,7 +3004,9 @@ def tessellate( return vertices, triangles - def to_splines(self, degree: int = 3, tolerance: float = 1e-3, nurbs: bool = False) -> T: + def to_splines( + self, degree: int = 3, tolerance: float = 1e-3, nurbs: bool = False + ) -> T: """to_splines Approximate shape with b-splines of the specified degree. @@ -3022,7 +3109,9 @@ def _repr_javascript_(self): return display(self)._repr_javascript_() - def transformed(self, rotate: VectorLike = (0, 0, 0), offset: VectorLike = (0, 0, 0)) -> Self: + def transformed( + self, rotate: VectorLike = (0, 0, 0), offset: VectorLike = (0, 0, 0) + ) -> Self: """Transform Shape Rotate and translate the Shape by the three angles (in degrees) and offset. @@ -3139,11 +3228,15 @@ def project_faces( for face in faces: bbox = face.bounding_box() face_center_x = (bbox.min.X + bbox.max.X) / 2 - relative_position_on_wire = start + (face_center_x - first_face_min_x) / path_length + relative_position_on_wire = ( + start + (face_center_x - first_face_min_x) / path_length + ) path_position = path.position_at(relative_position_on_wire) path_tangent = path.tangent_at(relative_position_on_wire) projection_axis = Axis(path_position, shape_center - path_position) - (surface_point, surface_normal) = self.find_intersection_points(projection_axis)[0] + (surface_point, surface_normal) = self.find_intersection_points( + projection_axis + )[0] surface_normal_plane = Plane( origin=surface_point, x_dir=path_tangent, z_dir=surface_normal ) @@ -3152,13 +3245,17 @@ def project_faces( ) logger.debug("projecting face at %0.2f", relative_position_on_wire) - projected_faces.append(projection_face.project_to_shape(self, surface_normal * -1)[0]) + projected_faces.append( + projection_face.project_to_shape(self, surface_normal * -1)[0] + ) logger.debug("finished projecting '%d' faces", len(faces)) return Compound(projected_faces) - def _extrude(self, direction: VectorLike) -> Union[Edge, Face, Shell, Solid, Compound]: + def _extrude( + self, direction: VectorLike + ) -> Union[Edge, Face, Shell, Solid, Compound]: """_extrude Extrude self in the provided direction. @@ -3203,7 +3300,9 @@ def _extrude(self, direction: VectorLike) -> Union[Edge, Face, Shell, Solid, Com return result @classmethod - def extrude(cls, obj: Union[Vertex, Edge, Wire, Face, Shell], direction: VectorLike) -> Self: + def extrude( + cls, obj: Union[Vertex, Edge, Wire, Face, Shell], direction: VectorLike + ) -> Self: """extrude Extrude a Shape in the provided direction. @@ -3269,7 +3368,9 @@ def extract_edges(compound): projection_dir: Vector = (viewport_origin - look_at).normalized() viewport_up = Vector(viewport_up).normalized() camera_coordinate_system = gp_Ax2() - camera_coordinate_system.SetAxis(gp_Ax1(viewport_origin.to_pnt(), projection_dir.to_dir())) + camera_coordinate_system.SetAxis( + gp_Ax1(viewport_origin.to_pnt(), projection_dir.to_dir()) + ) camera_coordinate_system.SetYDirection(viewport_up.to_dir()) projector = HLRAlgo_Projector(camera_coordinate_system) @@ -3462,22 +3563,30 @@ def filter_by_position( """ if inclusive == (True, True): objects = filter( - lambda o: minimum <= axis.to_plane().to_local_coords(o).center().Z <= maximum, + lambda o: minimum + <= axis.to_plane().to_local_coords(o).center().Z + <= maximum, self, ) elif inclusive == (True, False): objects = filter( - lambda o: minimum <= axis.to_plane().to_local_coords(o).center().Z < maximum, + lambda o: minimum + <= axis.to_plane().to_local_coords(o).center().Z + < maximum, self, ) elif inclusive == (False, True): objects = filter( - lambda o: minimum < axis.to_plane().to_local_coords(o).center().Z <= maximum, + lambda o: minimum + < axis.to_plane().to_local_coords(o).center().Z + <= maximum, self, ) elif inclusive == (False, False): objects = filter( - lambda o: minimum < axis.to_plane().to_local_coords(o).center().Z < maximum, + lambda o: minimum + < axis.to_plane().to_local_coords(o).center().Z + < maximum, self, ) @@ -3585,7 +3694,9 @@ def u_of_closest_center(obj) -> float: return sort_by.param_at_point(pnt1) # pylint: disable=unnecessary-lambda - objects = sorted(self, key=lambda o: u_of_closest_center(o), reverse=reverse) + objects = sorted( + self, key=lambda o: u_of_closest_center(o), reverse=reverse + ) elif isinstance(sort_by, SortBy): if sort_by == SortBy.LENGTH: @@ -3750,13 +3861,17 @@ def __or__(self, filter_by: Union[Axis, GeomType] = Axis.Z): def __eq__(self, other: object): """ShapeLists equality operator ==""" - return set(self) == set(other) if isinstance(other, ShapeList) else NotImplemented + return ( + set(self) == set(other) if isinstance(other, ShapeList) else NotImplemented + ) # Normally implementing __eq__ is enough, but ShapeList subclasses list, # which already implements __ne__, so we need to override it, too def __ne__(self, other: ShapeList): """ShapeLists inequality operator !=""" - return set(self) != set(other) if isinstance(other, ShapeList) else NotImplemented + return ( + set(self) != set(other) if isinstance(other, ShapeList) else NotImplemented + ) def __add__(self, other: ShapeList): """Combine two ShapeLists together operator +""" @@ -3917,13 +4032,13 @@ def __init__(self, *args, **kwargs): if args: l_a = len(args) if isinstance(args[0], TopoDS_Shape): - obj, label, color, material, joints, parent, children = args[:7] + (None,) * ( - 7 - l_a - ) + obj, label, color, material, joints, parent, children = args[:7] + ( + None, + ) * (7 - l_a) elif isinstance(args[0], Iterable): - shapes, label, color, material, joints, parent, children = args[:7] + (None,) * ( - 7 - l_a - ) + shapes, label, color, material, joints, parent, children = args[:7] + ( + None, + ) * (7 - l_a) unknown_args = ", ".join( set(kwargs.keys()).difference( @@ -4060,7 +4175,9 @@ def _post_detach(self, parent: Compound): """Method call after detaching from `parent`.""" logger.debug("Removing parent of %s (%s)", self.label, parent.label) if parent.children: - parent.wrapped = Compound._make_compound([c.wrapped for c in parent.children]) + parent.wrapped = Compound._make_compound( + [c.wrapped for c in parent.children] + ) else: parent.wrapped = None @@ -4118,9 +4235,12 @@ def do_children_intersect( if not include_parent: children.pop(0) # remove parent # children_bbox = [child.bounding_box().to_solid() for child in children] - children_bbox = [Solid.from_bounding_box(child.bounding_box()) for child in children] + children_bbox = [ + Solid.from_bounding_box(child.bounding_box()) for child in children + ] child_index_pairs = [ - tuple(map(int, comb)) for comb in combinations(list(range(len(children))), 2) + tuple(map(int, comb)) + for comb in combinations(list(range(len(children))), 2) ] for child_index_pair in child_index_pairs: # First check for bounding box intersections .. @@ -4132,7 +4252,9 @@ def do_children_intersect( ) if bbox_common_volume > tolerance: common_volume = ( - children[child_index_pair[0]].intersect(children[child_index_pair[1]]).volume + children[child_index_pair[0]] + .intersect(children[child_index_pair[1]]) + .volume ) if common_volume > tolerance: return ( @@ -4197,7 +4319,9 @@ def position_face(orig_face: "Face") -> "Face": """ bbox = orig_face.bounding_box() face_bottom_center = Vector((bbox.min.X + bbox.max.X) / 2, 0, 0) - relative_position_on_wire = position_on_path + face_bottom_center.X / path_length + relative_position_on_wire = ( + position_on_path + face_bottom_center.X / path_length + ) wire_tangent = text_path.tangent_at(relative_position_on_wire) wire_angle = Vector(1, 0, 0).get_signed_angle(wire_tangent) wire_position = text_path.position_at(relative_position_on_wire) @@ -4243,7 +4367,9 @@ def position_face(orig_face: "Face") -> "Face": # Align the text from the bounding box align = tuplify(align, 2) - text_flat = text_flat.translate(Vector(*text_flat.bounding_box().to_align_offset(align))) + text_flat = text_flat.translate( + Vector(*text_flat.bounding_box().to_align_offset(align)) + ) if text_path is not None: path_length = text_path.length @@ -4263,18 +4389,24 @@ def make_triad(cls, axes_scale: float) -> Compound: ) arrow = arrow_arc.fuse(copy.copy(arrow_arc).mirror(Plane.XZ)) x_label = ( - Compound.make_text("X", font_size=axes_scale / 4, align=(Align.MIN, Align.CENTER)) + Compound.make_text( + "X", font_size=axes_scale / 4, align=(Align.MIN, Align.CENTER) + ) .move(Location(x_axis @ 1)) .edges() ) y_label = ( - Compound.make_text("Y", font_size=axes_scale / 4, align=(Align.MIN, Align.CENTER)) + Compound.make_text( + "Y", font_size=axes_scale / 4, align=(Align.MIN, Align.CENTER) + ) .rotate(Axis.Z, 90) .move(Location(y_axis @ 1)) .edges() ) z_label = ( - Compound.make_text("Z", font_size=axes_scale / 4, align=(Align.CENTER, Align.MIN)) + Compound.make_text( + "Z", font_size=axes_scale / 4, align=(Align.CENTER, Align.MIN) + ) .rotate(Axis.Y, 90) .rotate(Axis.X, 90) .move(Location(z_axis @ 1)) @@ -4380,7 +4512,9 @@ def intersect(self, *to_intersect: Shape) -> Compound: def get_type( self, - obj_type: Union[Type[Vertex], Type[Edge], Type[Face], Type[Shell], Type[Solid], Type[Wire]], + obj_type: Union[ + Type[Vertex], Type[Edge], Type[Face], Type[Shell], Type[Solid], Type[Wire] + ], ) -> list[Union[Vertex, Edge, Face, Shell, Solid, Wire]]: """get_type @@ -4415,7 +4549,9 @@ def get_type( return results - def first_level_shapes(self, _shapes: list[TopoDS_Shape] = None) -> ShapeList[Shape]: + def first_level_shapes( + self, _shapes: list[TopoDS_Shape] = None + ) -> ShapeList[Shape]: """first_level_shapes This method iterates through the immediate children of the compound and @@ -4675,7 +4811,10 @@ def find_tangent( discontinuities = 0.0 for i in range(101 - periodic): tangent = self.tangent_angle_at(i / 100) + discontinuities * 360 - if previous_tangent is not None and abs(previous_tangent - tangent) > 300: + if ( + previous_tangent is not None + and abs(previous_tangent - tangent) > 300 + ): discontinuities = copysign(1.0, previous_tangent - tangent) tangent += 360 * discontinuities previous_tangent = tangent @@ -4701,7 +4840,9 @@ def find_tangent( def _intersect_with_edge(self, edge: Edge) -> Shape: # Find any intersection points - vertex_intersections = [Vertex(pnt) for pnt in self.find_intersection_points(edge)] + vertex_intersections = [ + Vertex(pnt) for pnt in self.find_intersection_points(edge) + ] # Find Edge/Edge overlaps intersect_op = BRepAlgoAPI_Common() @@ -4711,7 +4852,9 @@ def _intersect_with_edge(self, edge: Edge) -> Shape: def _intersect_with_axis(self, axis: Axis) -> Shape: # Find any intersection points - vertex_intersections = [Vertex(pnt) for pnt in self.find_intersection_points(axis)] + vertex_intersections = [ + Vertex(pnt) for pnt in self.find_intersection_points(axis) + ] # Find Edge/Edge overlaps intersect_op = BRepAlgoAPI_Common() @@ -4736,7 +4879,9 @@ def find_intersection_points( """ # Convert an Axis into an edge at least as large as self and Axis start point if isinstance(edge, Axis): - self_bbox_w_edge = self.bounding_box().add(Vertex(edge.position).bounding_box()) + self_bbox_w_edge = self.bounding_box().add( + Vertex(edge.position).bounding_box() + ) edge = Edge.make_line( edge.position + edge.direction * (-1 * self_bbox_w_edge.diagonal), edge.position + edge.direction * self_bbox_w_edge.diagonal, @@ -4762,7 +4907,9 @@ def find_intersection_points( edge.param_at(0), edge.param_at(1), ) - intersector = Geom2dAPI_InterCurveCurve(self_2d_curve, edge_2d_curve, tolerance) + intersector = Geom2dAPI_InterCurveCurve( + self_2d_curve, edge_2d_curve, tolerance + ) else: intersector = Geom2dAPI_InterCurveCurve(self_2d_curve, tolerance) @@ -4779,7 +4926,10 @@ def find_intersection_points( for pnt in crosses: try: if edge is not None: - if self.distance_to(pnt) <= TOLERANCE and edge.distance_to(pnt) <= TOLERANCE: + if ( + self.distance_to(pnt) <= TOLERANCE + and edge.distance_to(pnt) <= TOLERANCE + ): valid_crosses.append(pnt) else: if self.distance_to(pnt) <= TOLERANCE: @@ -4911,7 +5061,9 @@ def func(param: ndarray) -> float: return (self.position_at(param[0]) - point).length # Find the u value that results in a point within tolerance of the target - initial_guess = max(0.0, min(1.0, (point - self.position_at(0)).length / self.length)) + initial_guess = max( + 0.0, min(1.0, (point - self.position_at(0)).length / self.length) + ) result = minimize( func, x0=initial_guess, @@ -4943,7 +5095,9 @@ def make_bezier(cls, *cntl_pnts: VectorLike, weights: list[float] = None) -> Edg Edge: bezier curve """ if len(cntl_pnts) < 2: - raise ValueError("At least two control points must be provided (start, end)") + raise ValueError( + "At least two control points must be provided (start, end)" + ) if len(cntl_pnts) > 25: raise ValueError("The maximum number of control points is 25") if weights: @@ -5213,7 +5367,9 @@ def make_spline_approx( pnts.SetValue(i + 1, Vector(point).to_pnt()) if smoothing: - spline_builder = GeomAPI_PointsToBSpline(pnts, *smoothing, DegMax=max_deg, Tol3D=tol) + spline_builder = GeomAPI_PointsToBSpline( + pnts, *smoothing, DegMax=max_deg, Tol3D=tol + ) else: spline_builder = GeomAPI_PointsToBSpline( pnts, DegMin=min_deg, DegMax=max_deg, Tol3D=tol @@ -5249,7 +5405,9 @@ def make_three_point_arc( return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) @classmethod - def make_tangent_arc(cls, start: VectorLike, tangent: VectorLike, end: VectorLike) -> Edge: + def make_tangent_arc( + cls, start: VectorLike, tangent: VectorLike, end: VectorLike + ) -> Edge: """Tangent Arc Makes a tangent arc from point start, in the direction of tangent and ends at end. @@ -5280,7 +5438,11 @@ def make_line(cls, point1: VectorLike, point2: VectorLike) -> Edge: A linear edge between the two provided points """ - return cls(BRepBuilderAPI_MakeEdge(Vector(point1).to_pnt(), Vector(point2).to_pnt()).Edge()) + return cls( + BRepBuilderAPI_MakeEdge( + Vector(point1).to_pnt(), Vector(point2).to_pnt() + ).Edge() + ) @classmethod def make_helix( @@ -5334,7 +5496,9 @@ def make_helix( # Create an infinite 2d line in the direction of the helix helix_line = Geom2d_Line(gp_Pnt2d(0, 0), gp_Dir2d(line_dir.X, line_dir.Y)) # Trim the line to the desired length - helix_curve = Geom2d_TrimmedCurve(helix_line, 0, line_len, theAdjustPeriodic=True) + helix_curve = Geom2d_TrimmedCurve( + helix_line, 0, line_len, theAdjustPeriodic=True + ) # 3. Wrap the line around the surface edge_builder = BRepBuilderAPI_MakeEdge(helix_curve, geom_surf) @@ -5421,7 +5585,9 @@ def project_to_shape( def to_axis(self) -> Axis: """Translate a linear Edge to an Axis""" if self.geom_type != GeomType.LINE: - raise ValueError(f"to_axis is only valid for linear Edges not {self.geom_type}") + raise ValueError( + f"to_axis is only valid for linear Edges not {self.geom_type}" + ) return Axis(self.position_at(0), self.position_at(1) - self.position_at(0)) @@ -5484,7 +5650,9 @@ def __init__(self, *args, **kwargs): if isinstance(args[0], TopoDS_Shape): obj, label, color, parent = args[:4] + (None,) * (4 - l_a) elif isinstance(args[0], Wire): - outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * (5 - l_a) + outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * ( + 5 - l_a + ) unknown_args = ", ".join( set(kwargs.keys()).difference( @@ -5560,7 +5728,9 @@ def geometry(self) -> str: if len(flat_face_edges) == 4: edge_pairs = [] for vertex in flat_face_vertices: - edge_pairs.append([e for e in flat_face_edges if vertex in e.vertices()]) + edge_pairs.append( + [e for e in flat_face_edges if vertex in e.vertices()] + ) edge_pair_directions = [ [edge.tangent_at(0) for edge in pair] for pair in edge_pairs ] @@ -5658,7 +5828,9 @@ def normal_at(self, *args, **kwargs) -> Vector: if len(args) == 2 and isinstance(args[1], (int, float)): v = args[1] - unknown_args = ", ".join(set(kwargs.keys()).difference(["surface_point", "u", "v"])) + unknown_args = ", ".join( + set(kwargs.keys()).difference(["surface_point", "u", "v"]) + ) if unknown_args: raise ValueError(f"Unexpected argument(s) {unknown_args}") @@ -5679,7 +5851,9 @@ def normal_at(self, *args, **kwargs) -> Vector: v_val = v * (v_val0 + v_val1) else: # project point on surface - projector = GeomAPI_ProjectPointOnSurf(Vector(surface_point).to_pnt(), surface) + projector = GeomAPI_ProjectPointOnSurf( + Vector(surface_point).to_pnt(), surface + ) u_val, v_val = projector.LowerDistanceParameters() @@ -5733,7 +5907,9 @@ def center(self, center_of=CenterOf.GEOMETRY) -> Vector: Returns: Vector: center """ - if (center_of == CenterOf.MASS) or (center_of == CenterOf.GEOMETRY and self.is_planar): + if (center_of == CenterOf.MASS) or ( + center_of == CenterOf.GEOMETRY and self.is_planar + ): properties = GProp_GProps() BRepGProp.SurfaceProperties_s(self.wrapped, properties) center_point = properties.CentreOfMass() @@ -5799,16 +5975,22 @@ def make_plane( @overload @classmethod - def make_surface_from_curves(cls, edge1: Edge, edge2: Edge) -> Face: # pragma: no cover + def make_surface_from_curves( + cls, edge1: Edge, edge2: Edge + ) -> Face: # pragma: no cover ... @overload @classmethod - def make_surface_from_curves(cls, wire1: Wire, wire2: Wire) -> Face: # pragma: no cover + def make_surface_from_curves( + cls, wire1: Wire, wire2: Wire + ) -> Face: # pragma: no cover ... @classmethod - def make_surface_from_curves(cls, curve1: Union[Edge, Wire], curve2: Union[Edge, Wire]) -> Face: + def make_surface_from_curves( + cls, curve1: Union[Edge, Wire], curve2: Union[Edge, Wire] + ) -> Face: """make_surface_from_curves Create a ruled surface out of two edges or two wires. If wires are used then @@ -5828,7 +6010,9 @@ def make_surface_from_curves(cls, curve1: Union[Edge, Wire], curve2: Union[Edge, return return_value @classmethod - def make_from_wires(cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None) -> Face: + def make_from_wires( + cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None + ) -> Face: """make_from_wires Makes a planar face from one or more wires @@ -5855,7 +6039,9 @@ def make_from_wires(cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None) - return Face(Face._make_from_wires(outer_wire, inner_wires)) @classmethod - def _make_from_wires(cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None) -> TopoDS_Shape: + def _make_from_wires( + cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None + ) -> TopoDS_Shape: """make_from_wires Makes a planar face from one or more wires @@ -5879,7 +6065,9 @@ def _make_from_wires(cls, outer_wire: Wire, inner_wires: Iterable[Wire] = None) # check if wires are coplanar verification_compound = Compound([outer_wire] + inner_wires) - if not BRepLib_FindSurface(verification_compound.wrapped, OnlyPlane=True).Found(): + if not BRepLib_FindSurface( + verification_compound.wrapped, OnlyPlane=True + ).Found(): raise ValueError("Cannot build face(s): wires not planar") # fix outer wire @@ -5946,7 +6134,9 @@ def sew_faces(cls, faces: Iterable[Face]) -> list[ShapeList[Face]]: elif isinstance(sewed_shape, TopoDS_Solid): sewn_faces = [Solid(sewed_shape).faces()] else: - raise RuntimeError(f"SewedShape returned a {type(sewed_shape)} which was unexpected") + raise RuntimeError( + f"SewedShape returned a {type(sewed_shape)} which was unexpected" + ) return sewn_faces @@ -6065,10 +6255,14 @@ def make_bezier_surface( Face: a potentially non-planar face """ if len(points) < 2 or len(points[0]) < 2: - raise ValueError("At least two control points must be provided (start, end)") + raise ValueError( + "At least two control points must be provided (start, end)" + ) if len(points) > 25 or len(points[0]) > 25: raise ValueError("The maximum number of control points is 25") - if weights and (len(points) != len(weights) or len(points[0]) != len(weights[0])): + if weights and ( + len(points) != len(weights) or len(points[0]) != len(weights[0]) + ): raise ValueError("A weight must be provided for each control point") points_ = TColgp_HArray2OfPnt(1, len(points), 1, len(points[0])) @@ -6144,7 +6338,9 @@ def make_surface( ) if isinstance(exterior, Wire): outside_edges = exterior.edges() - elif isinstance(exterior, Iterable) and all([isinstance(o, Edge) for o in exterior]): + elif isinstance(exterior, Iterable) and all( + [isinstance(o, Edge) for o in exterior] + ): outside_edges = exterior else: raise ValueError("exterior must be a Wire or list of Edges") @@ -6161,7 +6357,9 @@ def make_surface( Standard_NoSuchObject, Standard_ConstructionError, ) as err: - raise RuntimeError("Error building non-planar face with provided exterior") from err + raise RuntimeError( + "Error building non-planar face with provided exterior" + ) from err if surface_points: for point in surface_points: surface.Add(gp_Pnt(*point.to_tuple())) @@ -6282,7 +6480,8 @@ def is_coplanar(self, plane: Plane) -> bool: BRepGProp_Face(self.wrapped).Normal(u_val0, v_val0, gp_pnt, normal) return ( - plane.contains(Vector(gp_pnt)) and 1 - abs(plane.z_dir.dot(Vector(normal))) < TOLERANCE + plane.contains(Vector(gp_pnt)) + and 1 - abs(plane.z_dir.dot(Vector(normal))) < TOLERANCE ) def thicken(self, depth: float, normal_override: VectorLike = None) -> Solid: @@ -6417,7 +6616,8 @@ def get(los: TopTools_ListOfShape, shape_cls) -> list: def desired_faces(face_list: list[Face]) -> bool: return ( face_list - and face_list[0]._extrude(direction * -max_size).intersect(self).area > TOLERANCE + and face_list[0]._extrude(direction * -max_size).intersect(self).area + > TOLERANCE ) # @@ -6444,7 +6644,9 @@ def desired_faces(face_list: list[Face]) -> bool: if not edge_compound.IsNull(): target_edges_on_xy.extend(Compound(edge_compound).edges()) - target_edges = [projection_plane.from_local_coords(e) for e in target_edges_on_xy] + target_edges = [ + projection_plane.from_local_coords(e) for e in target_edges_on_xy + ] target_wires = edges_to_wires(target_edges) # return target_wires @@ -6466,7 +6668,9 @@ def desired_faces(face_list: list[Face]) -> bool: perimeter.wrapped, target_object.wrapped, direction.to_dir() ) # print(len(Compound(hlr_projector.Shape()).wires().sort_by(projection_axis))) - projected_wires = Compound(hlr_projector.Shape()).wires().sort_by(projection_axis) + projected_wires = ( + Compound(hlr_projector.Shape()).wires().sort_by(projection_axis) + ) # target_projected_wires = [] # for target_wire in target_wires: @@ -6774,7 +6978,9 @@ def sweep( return Shape.cast(builder.Shape()) @classmethod - def make_loft(cls, objs: Iterable[Union[Vertex, Wire]], ruled: bool = False) -> Shell: + def make_loft( + cls, objs: Iterable[Union[Vertex, Wire]], ruled: bool = False + ) -> Shell: """make loft Makes a loft from a list of wires and vertices. @@ -6857,9 +7063,13 @@ def __init__(self, *args, **kwargs): if args: l_a = len(args) if isinstance(args[0], TopoDS_Shape): - obj, label, color, material, joints, parent = args[:6] + (None,) * (6 - l_a) + obj, label, color, material, joints, parent = args[:6] + (None,) * ( + 6 - l_a + ) elif isinstance(args[0], Shell): - shell, label, color, material, joints, parent = args[:6] + (None,) * (6 - l_a) + shell, label, color, material, joints, parent = args[:6] + (None,) * ( + 6 - l_a + ) unknown_args = ", ".join( set(kwargs.keys()).difference( @@ -6924,7 +7134,9 @@ def from_bounding_box(cls, bbox: BoundBox) -> Solid: return Solid.make_box(*bbox.size).locate(Location(bbox.min)) @classmethod - def make_box(cls, length: float, width: float, height: float, plane: Plane = Plane.XY) -> Solid: + def make_box( + cls, length: float, width: float, height: float, plane: Plane = Plane.XY + ) -> Solid: """make box Make a box at the origin of plane extending in positive direction of each axis. @@ -7046,7 +7258,9 @@ def make_torus( ) @classmethod - def make_loft(cls, objs: Iterable[Union[Vertex, Wire]], ruled: bool = False) -> Solid: + def make_loft( + cls, objs: Iterable[Union[Vertex, Wire]], ruled: bool = False + ) -> Solid: """make loft Makes a loft from a list of wires and vertices. @@ -7184,7 +7398,9 @@ def extrude_taper( outer = profile.outer_wire() local_outer: Wire = Plane(profile).to_local_coords(outer) - local_taper_outer = local_outer.offset_2d(offset_amt, kind=Kind.INTERSECTION) + local_taper_outer = local_outer.offset_2d( + offset_amt, kind=Kind.INTERSECTION + ) taper_outer = Plane(profile).from_local_coords(local_taper_outer) taper_outer.move(Location(direction)) @@ -7199,7 +7415,9 @@ def extrude_taper( taper.move(Location(direction)) taper_wires.append(taper) - solids = [Solid.make_loft([p, t]) for p, t in zip(profile_wires, taper_wires)] + solids = [ + Solid.make_loft([p, t]) for p, t in zip(profile_wires, taper_wires) + ] if len(solids) > 1: new_solid = solids[0].cut(*solids[1:]) else: @@ -7274,11 +7492,14 @@ def extrude_aux_spine( ).wrapped # extrude the outer wire - outer_solid = extrude_aux_spine(outer_wire.wrapped, straight_spine_w, aux_spine_w) + outer_solid = extrude_aux_spine( + outer_wire.wrapped, straight_spine_w, aux_spine_w + ) # extrude inner wires inner_solids = [ - Shape(extrude_aux_spine(w.wrapped, straight_spine_w, aux_spine_w)) for w in inner_wires + Shape(extrude_aux_spine(w.wrapped, straight_spine_w, aux_spine_w)) + for w in inner_wires ] # combine the inner solids into compound @@ -7320,7 +7541,9 @@ def extrude_until( max_dimension = Compound([section, target_object]).bounding_box().diagonal clipping_direction = ( - direction * max_dimension if until == Until.NEXT else -direction * max_dimension + direction * max_dimension + if until == Until.NEXT + else -direction * max_dimension ) direction_axis = Axis(section.center(), clipping_direction) # Create a linear extrusion to start @@ -7336,12 +7559,18 @@ def extrude_until( else: faces += face.faces() - clip_faces = [f for f in faces if not (f.is_planar and f.normal_at().dot(direction) == 0.0)] + clip_faces = [ + f + for f in faces + if not (f.is_planar and f.normal_at().dot(direction) == 0.0) + ] if not clip_faces: raise ValueError("provided face does not intersect target_object") # Create the objects that will clip the linear extrusion - clipping_objects = [Solid.extrude(f, clipping_direction).fix() for f in clip_faces] + clipping_objects = [ + Solid.extrude(f, clipping_direction).fix() for f in clip_faces + ] clipping_objects = [o for o in clipping_objects if o.volume > 1e-9] if until == Until.NEXT: @@ -7351,7 +7580,11 @@ def extrude_until( # thus they could be non manifold which results failed boolean operations # - so skip these objects try: - extrusion = extrusion.cut(clipping_object).solids().sort_by(direction_axis)[0] + extrusion = ( + extrusion.cut(clipping_object) + .solids() + .sort_by(direction_axis)[0] + ) except: # pylint: disable=bare-except warnings.warn("clipping error - extrusion may be incorrect") else: @@ -7359,7 +7592,9 @@ def extrude_until( for clipping_object in clipping_objects: try: extrusion_parts.append( - extrusion.intersect(clipping_object).solids().sort_by(direction_axis)[0] + extrusion.intersect(clipping_object) + .solids() + .sort_by(direction_axis)[0] ) except: # pylint: disable=bare-except warnings.warn("clipping error - extrusion may be incorrect") @@ -7533,7 +7768,9 @@ def sweep_multi( for profile in profiles: path_as_wire = ( - profile.wrapped if isinstance(profile, Wire) else profile.outer_wire().wrapped + profile.wrapped + if isinstance(profile, Wire) + else profile.outer_wire().wrapped ) builder.Add(path_as_wire, translate, rotate) @@ -7632,7 +7869,9 @@ def center(self) -> Vector: """The center of a vertex is itself!""" return Vector(self) - def __add__(self, other: Union[Vertex, Vector, Tuple[float, float, float]]) -> Vertex: + def __add__( + self, other: Union[Vertex, Vector, Tuple[float, float, float]] + ) -> Vertex: """Add Add to a Vertex with a Vertex, Vector or Tuple @@ -7656,7 +7895,9 @@ def __add__(self, other: Union[Vertex, Vector, Tuple[float, float, float]]) -> V new_vertex = Vertex(self.X + other.X, self.Y + other.Y, self.Z + other.Z) elif isinstance(other, (Vector, tuple)): new_other = Vector(other) - new_vertex = Vertex(self.X + new_other.X, self.Y + new_other.Y, self.Z + new_other.Z) + new_vertex = Vertex( + self.X + new_other.X, self.Y + new_other.Y, self.Z + new_other.Z + ) else: raise TypeError( "Vertex addition only supports Vertex,Vector or tuple(float,float,float) as input" @@ -7684,7 +7925,9 @@ def __sub__(self, other: Union[Vertex, Vector, tuple]) -> Vertex: new_vertex = Vertex(self.X - other.X, self.Y - other.Y, self.Z - other.Z) elif isinstance(other, (Vector, tuple)): new_other = Vector(other) - new_vertex = Vertex(self.X - new_other.X, self.Y - new_other.Y, self.Z - new_other.Z) + new_vertex = Vertex( + self.X - new_other.X, self.Y - new_other.Y, self.Z - new_other.Z + ) else: raise TypeError( "Vertex subtraction only supports Vertex,Vector or tuple(float,float,float)" @@ -7909,7 +8152,9 @@ def to_wire(self) -> Wire: return self @classmethod - def combine(cls, wires: Iterable[Union[Wire, Edge]], tol: float = 1e-9) -> ShapeList[Wire]: + def combine( + cls, wires: Iterable[Union[Wire, Edge]], tol: float = 1e-9 + ) -> ShapeList[Wire]: """combine Combine a list of wires and edges into a list of Wires. @@ -8031,8 +8276,16 @@ def trim(self: Wire, start: float, end: float) -> Wire: u = self.param_at_point(e.position_at(0)) v = self.param_at_point(e.position_at(1)) if self.is_closed: # Avoid two beginnings or ends - u = 1 - u if found_end_of_wire and (isclose_b(u, 0) or isclose_b(u, 1)) else u - v = 1 - v if found_end_of_wire and (isclose_b(v, 0) or isclose_b(v, 1)) else v + u = ( + 1 - u + if found_end_of_wire and (isclose_b(u, 0) or isclose_b(u, 1)) + else u + ) + v = ( + 1 - v + if found_end_of_wire and (isclose_b(v, 0) or isclose_b(v, 1)) + else v + ) found_end_of_wire = ( isclose_b(u, 0) or isclose_b(u, 1) @@ -8057,7 +8310,9 @@ def trim(self: Wire, start: float, end: float) -> Wire: elif start >= u and end <= v: # Wire trimmed to single Edge u_edge = e.param_at_point(self.position_at(start)) v_edge = e.param_at_point(self.position_at(end)) - u_edge, v_edge = (v_edge, u_edge) if u_edge > v_edge else (u_edge, v_edge) + u_edge, v_edge = ( + (v_edge, u_edge) if u_edge > v_edge else (u_edge, v_edge) + ) new_edges.append(e.trim(u_edge, v_edge)) elif start <= u: # keep start of Edge @@ -8074,7 +8329,9 @@ def trim(self: Wire, start: float, end: float) -> Wire: def order_edges(self) -> ShapeList[Edge]: """Return the edges in self ordered by wire direction and orientation""" - ordered_edges = [e if e.is_forward else e.reversed() for e in self.edges().sort_by(self)] + ordered_edges = [ + e if e.is_forward else e.reversed() for e in self.edges().sort_by(self) + ] return ShapeList(ordered_edges) @classmethod @@ -8431,11 +8688,15 @@ def make_convex_hull(cls, edges: Iterable[Edge], tolerance: float = 1e-3) -> Wir trim_data[edge] = f_points connecting_edges = [ - Edge.make_line(edges[line[0][0]] @ line[0][1], edges[line[1][0]] @ line[1][1]) + Edge.make_line( + edges[line[0][0]] @ line[0][1], edges[line[1][0]] @ line[1][1] + ) for line in connecting_edge_data ] trimmed_edges = [ - edges[edge].trim(points_lookup[trim_pair[0]][1], points_lookup[trim_pair[1]][1]) + edges[edge].trim( + points_lookup[trim_pair[0]][1], points_lookup[trim_pair[1]][1] + ) for edge, trim_pairs in trim_data.items() for trim_pair in trim_pairs ] @@ -8519,7 +8780,9 @@ def project_to_shape( for output_wire in output_wires: output_wire_center = output_wire.center() if direction_vector is not None: - output_wire_direction = (output_wire_center - planar_wire_center).normalized() + output_wire_direction = ( + output_wire_center - planar_wire_center + ).normalized() if output_wire_direction.dot(direction_vector) >= 0: output_wires_distances.append( ( @@ -8627,14 +8890,22 @@ def _make_loft( if vertex_count > 2: raise ValueError("Only two vertices are allowed") - if vertex_count == 1 and not (isinstance(objs[0], Vertex) or isinstance(objs[-1], Vertex)): - raise ValueError("The vertex must be either at the beginning or end of the list") + if vertex_count == 1 and not ( + isinstance(objs[0], Vertex) or isinstance(objs[-1], Vertex) + ): + raise ValueError( + "The vertex must be either at the beginning or end of the list" + ) if vertex_count == 2: if len(objs) == 2: - raise ValueError("You can't have only 2 vertices to loft; try adding some wires") + raise ValueError( + "You can't have only 2 vertices to loft; try adding some wires" + ) if not (isinstance(objs[0], Vertex) and isinstance(objs[-1], Vertex)): - raise ValueError("The vertices must be at the beginning and end of the list") + raise ValueError( + "The vertices must be at the beginning and end of the list" + ) loft_builder = BRepOffsetAPI_ThruSections(filled, ruled) diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index b9ffe1c4..596567b7 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -296,7 +296,9 @@ def test_axis_is_parallel(self): def test_axis_angle_between(self): self.assertAlmostEqual(Axis.X.angle_between(Axis.Y), 90, 5) - self.assertAlmostEqual(Axis.X.angle_between(Axis((1, 1, 1), (-1, 0, 0))), 180, 5) + self.assertAlmostEqual( + Axis.X.angle_between(Axis((1, 1, 1), (-1, 0, 0))), 180, 5 + ) def test_axis_reverse(self): self.assertVectorAlmostEquals(Axis.X.reverse().direction, (-1, 0, 0), 5) @@ -424,7 +426,9 @@ def test_basic_bounding_box(self): def test_bounding_box_repr(self): bb = Solid.make_box(1, 1, 1).bounding_box() - self.assertEqual(repr(bb), "bbox: 0.0 <= x <= 1.0, 0.0 <= y <= 1.0, 0.0 <= z <= 1.0") + self.assertEqual( + repr(bb), "bbox: 0.0 <= x <= 1.0, 0.0 <= y <= 1.0, 0.0 <= z <= 1.0" + ) def test_center_of_boundbox(self): self.assertVectorAlmostEquals( @@ -715,16 +719,22 @@ def test_to_tuple(self): def test_hex(self): c = Color(0x996692) - self.assertTupleAlmostEquals(tuple(c), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), 5) + self.assertTupleAlmostEquals( + tuple(c), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), 5 + ) c = Color(0x006692, 0x80) - self.assertTupleAlmostEquals(tuple(c), (0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF), 5) + self.assertTupleAlmostEquals( + tuple(c), (0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF), 5 + ) c = Color(0x006692, alpha=0x80) self.assertTupleAlmostEquals(tuple(c), (0, 102 / 255, 146 / 255, 128 / 255), 5) c = Color(color_code=0x996692, alpha=0xCC) - self.assertTupleAlmostEquals(tuple(c), (153 / 255, 102 / 255, 146 / 255, 204 / 255), 5) + self.assertTupleAlmostEquals( + tuple(c), (153 / 255, 102 / 255, 146 / 255, 204 / 255), 5 + ) c = Color(0.0, 0.0, 1.0, 1.0) self.assertTupleAlmostEquals(tuple(c), (0, 0, 1, 1), 5) @@ -760,7 +770,9 @@ def test_make_text(self): arc = Edge.make_three_point_arc((-50, 0, 0), (0, 20, 0), (50, 0, 0)) text = Compound.make_text("test", 10, text_path=arc) self.assertEqual(len(text.faces()), 4) - text = Compound.make_text("test", 10, align=(Align.MAX, Align.MAX), text_path=arc) + text = Compound.make_text( + "test", 10, align=(Align.MAX, Align.MAX), text_path=arc + ) self.assertEqual(len(text.faces()), 4) def test_fuse(self): @@ -798,7 +810,9 @@ def test_center(self): ] ) self.assertVectorAlmostEquals(test_compound.center(CenterOf.MASS), (1, 0, 0), 5) - self.assertVectorAlmostEquals(test_compound.center(CenterOf.BOUNDING_BOX), (4.25, 0, 0), 5) + self.assertVectorAlmostEquals( + test_compound.center(CenterOf.BOUNDING_BOX), (4.25, 0, 0), 5 + ) with self.assertRaises(ValueError): test_compound.center(CenterOf.GEOMETRY) @@ -871,7 +885,9 @@ def test_first_level_shapes(self): class TestEdge(DirectApiTestCase): def test_close(self): - self.assertAlmostEqual(Edge.make_circle(1, end_angle=180).close().length, math.pi + 2, 5) + self.assertAlmostEqual( + Edge.make_circle(1, end_angle=180).close().length, math.pi + 2, 5 + ) self.assertAlmostEqual(Edge.make_circle(1).close().length, 2 * math.pi, 5) def test_make_half_circle(self): @@ -915,9 +931,13 @@ def test_spline_with_parameters(self): ) self.assertVectorAlmostEquals(spline.end_point(), (2, 0, 0), 5) with self.assertRaises(ValueError): - Edge.make_spline(points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], parameters=[0.0, 1.0]) + Edge.make_spline( + points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], parameters=[0.0, 1.0] + ) with self.assertRaises(ValueError): - Edge.make_spline(points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], tangents=[(1, 1, 0)]) + Edge.make_spline( + points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)], tangents=[(1, 1, 0)] + ) def test_spline_approx(self): spline = Edge.make_spline_approx([(0, 0), (1, 1), (2, 1), (3, 0)]) @@ -999,8 +1019,12 @@ def test_find_intersection_points(self): def test_trim(self): line = Edge.make_line((-2, 0), (2, 0)) - self.assertVectorAlmostEquals(line.trim(0.25, 0.75).position_at(0), (-1, 0, 0), 5) - self.assertVectorAlmostEquals(line.trim(0.25, 0.75).position_at(1), (1, 0, 0), 5) + self.assertVectorAlmostEquals( + line.trim(0.25, 0.75).position_at(0), (-1, 0, 0), 5 + ) + self.assertVectorAlmostEquals( + line.trim(0.25, 0.75).position_at(1), (1, 0, 0), 5 + ) with self.assertRaises(ValueError): line.trim(0.75, 0.25) @@ -1017,7 +1041,9 @@ def test_trim_to_length(self): e2_trim.position_at(0), Vector(10, 0, 0).rotate(Axis.Z, 45), 5 ) - e3 = Edge.make_spline([(0, 10, 0), (-4, 5, 2), (0, 0, 0)], tangents=[(-1, 0), (1, 0)]) + e3 = Edge.make_spline( + [(0, 10, 0), (-4, 5, 2), (0, 0, 0)], tangents=[(-1, 0), (1, 0)] + ) e3_trim = e3.trim_to_length(0, 7) self.assertAlmostEqual(e3_trim.length, 7, 5) @@ -1062,7 +1088,9 @@ def test_distribute_locations2(self): def test_find_tangent(self): circle = Edge.make_circle(1) parm = circle.find_tangent(135)[0] - self.assertVectorAlmostEquals(circle @ parm, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 5) + self.assertVectorAlmostEquals( + circle @ parm, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 5 + ) line = Edge.make_line((0, 0), (1, 1)) parm = line.find_tangent(45)[0] self.assertAlmostEqual(parm, 0, 5) @@ -1128,7 +1156,9 @@ def test_make_surface_from_curves(self): def test_center(self): test_face = Face(Wire.make_polygon([(0, 0), (1, 0), (1, 1), (0, 0)])) - self.assertVectorAlmostEquals(test_face.center(CenterOf.MASS), (2 / 3, 1 / 3, 0), 1) + self.assertVectorAlmostEquals( + test_face.center(CenterOf.MASS), (2 / 3, 1 / 3, 0), 1 + ) self.assertVectorAlmostEquals( test_face.center(CenterOf.BOUNDING_BOX), (0.5, 0.5, 0), @@ -1141,14 +1171,18 @@ def test_face_volume(self): def test_chamfer_2d(self): test_face = Face.make_rect(10, 10) - test_face = test_face.chamfer_2d(distance=1, distance2=2, vertices=test_face.vertices()) + test_face = test_face.chamfer_2d( + distance=1, distance2=2, vertices=test_face.vertices() + ) self.assertAlmostEqual(test_face.area, 100 - 4 * 0.5 * 1 * 2) def test_chamfer_2d_reference(self): test_face = Face.make_rect(10, 10) edge = test_face.edges().sort_by(Axis.Y)[0] vertex = edge.vertices().sort_by(Axis.X)[0] - test_face = test_face.chamfer_2d(distance=1, distance2=2, vertices=[vertex], edge=edge) + test_face = test_face.chamfer_2d( + distance=1, distance2=2, vertices=[vertex], edge=edge + ) self.assertAlmostEqual(test_face.area, 100 - 0.5 * 1 * 2) self.assertAlmostEqual(test_face.edges().sort_by(Axis.Y)[0].length, 9) self.assertAlmostEqual(test_face.edges().sort_by(Axis.X)[0].length, 8) @@ -1157,7 +1191,9 @@ def test_chamfer_2d_reference_inverted(self): test_face = Face.make_rect(10, 10) edge = test_face.edges().sort_by(Axis.Y)[0] vertex = edge.vertices().sort_by(Axis.X)[0] - test_face = test_face.chamfer_2d(distance=2, distance2=1, vertices=[vertex], edge=edge) + test_face = test_face.chamfer_2d( + distance=2, distance2=1, vertices=[vertex], edge=edge + ) self.assertAlmostEqual(test_face.area, 100 - 0.5 * 1 * 2) self.assertAlmostEqual(test_face.edges().sort_by(Axis.Y)[0].length, 8) self.assertAlmostEqual(test_face.edges().sort_by(Axis.X)[0].length, 9) @@ -1200,7 +1236,8 @@ def test_is_planar(self): mount = Solid.make_loft( [ Rectangle((1 + 16 + 4), 20, align=(Align.MIN, Align.CENTER)).wire(), - Pos(1, 0, 4) * Rectangle(16, 20, align=(Align.MIN, Align.CENTER)).wire(), + Pos(1, 0, 4) + * Rectangle(16, 20, align=(Align.MIN, Align.CENTER)).wire(), ], ) self.assertTrue(all(f.is_planar for f in mount.faces())) @@ -1267,7 +1304,10 @@ def test_surface_from_array_of_points(self): def test_bezier_surface(self): points = [ - [(x, y, 2 if x == 0 and y == 0 else 1 if x == 0 or y == 0 else 0) for x in range(-1, 2)] + [ + (x, y, 2 if x == 0 and y == 0 else 1 if x == 0 or y == 0 else 0) + for x in range(-1, 2) + ] for y in range(-1, 2) ] surface = Face.make_bezier_surface(points) @@ -1276,7 +1316,9 @@ def test_bezier_surface(self): self.assertVectorAlmostEquals(bbox.max, (+1, +1, +1), 1) self.assertLess(bbox.max.Z, 1.0) - weights = [[2 if x == 0 or y == 0 else 1 for x in range(-1, 2)] for y in range(-1, 2)] + weights = [ + [2 if x == 0 or y == 0 else 1 for x in range(-1, 2)] for y in range(-1, 2) + ] surface = Face.make_bezier_surface(points, weights) bbox = surface.bounding_box() self.assertVectorAlmostEquals(bbox.min, (-1, -1, 0), 3) @@ -1319,10 +1361,14 @@ def test_make_holes(self): circumference = 2 * math.pi * radius hex_diagonal = 4 * (circumference / 10) / 3 cylinder = Solid.make_cylinder(radius, hex_diagonal * 5) - cylinder_wall: Face = cylinder.faces().filter_by(GeomType.PLANE, reverse=True)[0] + cylinder_wall: Face = cylinder.faces().filter_by(GeomType.PLANE, reverse=True)[ + 0 + ] with BuildSketch(Plane.XZ.offset(radius)) as hex: with Locations((0, hex_diagonal)): - RegularPolygon(hex_diagonal * 0.4, 6, align=(Align.CENTER, Align.CENTER)) + RegularPolygon( + hex_diagonal * 0.4, 6, align=(Align.CENTER, Align.CENTER) + ) hex_wire_vertical: Wire = hex.sketch.faces()[0].outer_wire() projected_wire: Wire = hex_wire_vertical.project_to_shape( @@ -1437,7 +1483,9 @@ def test_make_surface_error_checking(self): if platform.system() != "Darwin": with self.assertRaises(RuntimeError): - Face.make_surface([Edge.make_circle(50)], surface_points=[(0, 0, -50), (0, 0, 50)]) + Face.make_surface( + [Edge.make_circle(50)], surface_points=[(0, 0, -50), (0, 0, 50)] + ) with self.assertRaises(RuntimeError): Face.make_surface( @@ -1481,7 +1529,9 @@ def test_constructor(self): def test_normal_at(self): face = Face.make_rect(1, 1) self.assertVectorAlmostEquals(face.normal_at(0, 0), (0, 0, 1), 5) - self.assertVectorAlmostEquals(face.normal_at(face.position_at(0, 0)), (0, 0, 1), 5) + self.assertVectorAlmostEquals( + face.normal_at(face.position_at(0, 0)), (0, 0, 1), 5 + ) with self.assertRaises(ValueError): face.normal_at(0) with self.assertRaises(ValueError): @@ -1630,13 +1680,19 @@ def test_location(self): T = loc5.wrapped.Transformation().TranslationPart() self.assertTupleAlmostEquals((T.X(), T.Y(), T.Z()), (0, 0, 1), 6) - angle5 = loc5.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG + angle5 = ( + loc5.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG + ) self.assertAlmostEqual(15, angle5) - angle6 = loc6.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG + angle6 = ( + loc6.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG + ) self.assertAlmostEqual(30, angle6) - angle7 = loc7.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG + angle7 = ( + loc7.wrapped.Transformation().GetRotation().GetRotationAngle() * RAD2DEG + ) self.assertAlmostEqual(30, angle7) # Test error handling on creation @@ -1710,7 +1766,9 @@ def test_location_parameters(self): Location(Intrinsic.XYZ) def test_location_repr_and_str(self): - self.assertEqual(repr(Location()), "(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))") + self.assertEqual( + repr(Location()), "(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))" + ) self.assertEqual( str(Location()), "Location: (position=(0.00, 0.00, 0.00), orientation=(-0.00, 0.00, -0.00))", @@ -2161,7 +2219,9 @@ def test_location_at(self): self.assertVectorAlmostEquals(loc.position, (0, 1, 0), 5) self.assertVectorAlmostEquals(loc.orientation, (0, -90, -90), 5) - loc = Edge.make_circle(1).location_at(math.pi / 2, position_mode=PositionMode.LENGTH) + loc = Edge.make_circle(1).location_at( + math.pi / 2, position_mode=PositionMode.LENGTH + ) self.assertVectorAlmostEquals(loc.position, (0, 1, 0), 5) self.assertVectorAlmostEquals(loc.orientation, (0, -90, -90), 5) @@ -2187,7 +2247,9 @@ def test_project(self): def test_project2(self): target = Cylinder(1, 10).faces().filter_by(GeomType.PLANE, reverse=True)[0] square = Wire.make_rect(1, 1, Plane.YZ).locate(Location((10, 0, 0))) - projections: list[Wire] = square.project(target, direction=(-1, 0, 0), closest=False) + projections: list[Wire] = square.project( + target, direction=(-1, 0, 0), closest=False + ) self.assertEqual(len(projections), 2) def test_is_forward(self): @@ -2206,7 +2268,10 @@ def test_offset_2d(self): self.assertEqual(len(offset_wire.edges().filter_by(GeomType.CIRCLE)), 2) offset_wire_right = base_wire.offset_2d(0.1, side=Side.RIGHT) self.assertAlmostEqual( - offset_wire_right.edges().filter_by(GeomType.CIRCLE).sort_by(SortBy.RADIUS)[-1].radius, + offset_wire_right.edges() + .filter_by(GeomType.CIRCLE) + .sort_by(SortBy.RADIUS)[-1] + .radius, 0.5, 4, ) @@ -2296,7 +2361,9 @@ def test_chamfer_asym_length_with_face(self): def test_chamfer_too_high_length(self): box = Solid.make_box(1, 1, 1) face = box.faces - self.assertRaises(ValueError, box.chamfer, 2, None, box.edges().sort_by(Axis.Z)[-1:]) + self.assertRaises( + ValueError, box.chamfer, 2, None, box.edges().sort_by(Axis.Z)[-1:] + ) def test_chamfer_edge_not_part_of_face(self): box = Solid.make_box(1, 1, 1) @@ -2316,7 +2383,9 @@ def test_is_inside(self): def test_dprism(self): # face f = Face.make_rect(0.5, 0.5) - d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(None, [f], additive=False) + d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism( + None, [f], additive=False + ) self.assertTrue(d.is_valid()) self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5) @@ -2339,7 +2408,9 @@ def test_dprism(self): # wire w = Face.make_rect(0.5, 0.5).outer_wire() - d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(None, [w], additive=False) + d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism( + None, [w], additive=False + ) self.assertTrue(d.is_valid()) self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5) @@ -2432,8 +2503,12 @@ def test_plane_init(self): p_from_named_loc = Plane(location=loc) for p in [p_from_loc, p_from_named_loc]: self.assertVectorAlmostEquals(p.origin, (0, 0, 0), 6) - self.assertVectorAlmostEquals(p.x_dir, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6) - self.assertVectorAlmostEquals(p.y_dir, (-math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6) + self.assertVectorAlmostEquals( + p.x_dir, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6 + ) + self.assertVectorAlmostEquals( + p.y_dir, (-math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6 + ) self.assertVectorAlmostEquals(p.z_dir, (0, 0, 1), 6) self.assertVectorAlmostEquals(loc.position, p.location.position, 6) self.assertVectorAlmostEquals(loc.orientation, p.location.orientation, 6) @@ -2443,8 +2518,12 @@ def test_plane_init(self): p = Plane(loc) self.assertVectorAlmostEquals(p.origin, (0, 2, -1), 6) self.assertVectorAlmostEquals(p.x_dir, (1, 0, 0), 6) - self.assertVectorAlmostEquals(p.y_dir, (0, math.sqrt(2) / 2, math.sqrt(2) / 2), 6) - self.assertVectorAlmostEquals(p.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6) + self.assertVectorAlmostEquals( + p.y_dir, (0, math.sqrt(2) / 2, math.sqrt(2) / 2), 6 + ) + self.assertVectorAlmostEquals( + p.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6 + ) self.assertVectorAlmostEquals(loc.position, p.location.position, 6) self.assertVectorAlmostEquals(loc.orientation, p.location.orientation, 6) @@ -2458,9 +2537,13 @@ def test_plane_init(self): self.assertVectorAlmostEquals(p.origin, (1, 2, 3), 6) self.assertVectorAlmostEquals(p.x_dir, (math.sqrt(2) / 2, 0.5, 0.5), 6) self.assertVectorAlmostEquals(p.y_dir, (-math.sqrt(2) / 2, 0.5, 0.5), 6) - self.assertVectorAlmostEquals(p.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6) + self.assertVectorAlmostEquals( + p.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6 + ) self.assertVectorAlmostEquals(f.location.position, p.location.position, 6) - self.assertVectorAlmostEquals(f.location.orientation, p.location.orientation, 6) + self.assertVectorAlmostEquals( + f.location.orientation, p.location.orientation, 6 + ) # from a face with x_dir f = Face.make_rect(1, 2) @@ -2490,32 +2573,48 @@ def test_plane_neg(self): self.assertVectorAlmostEquals(p2.origin, p.origin, 6) self.assertVectorAlmostEquals(p2.x_dir, p.x_dir, 6) self.assertVectorAlmostEquals(p2.z_dir, -p.z_dir, 6) - self.assertVectorAlmostEquals(p2.y_dir, (-p.z_dir).cross(p.x_dir).normalized(), 6) + self.assertVectorAlmostEquals( + p2.y_dir, (-p.z_dir).cross(p.x_dir).normalized(), 6 + ) p3 = p.reverse() self.assertVectorAlmostEquals(p3.origin, p.origin, 6) self.assertVectorAlmostEquals(p3.x_dir, p.x_dir, 6) self.assertVectorAlmostEquals(p3.z_dir, -p.z_dir, 6) - self.assertVectorAlmostEquals(p3.y_dir, (-p.z_dir).cross(p.x_dir).normalized(), 6) + self.assertVectorAlmostEquals( + p3.y_dir, (-p.z_dir).cross(p.x_dir).normalized(), 6 + ) def test_plane_mul(self): p = Plane(origin=(1, 2, 3), x_dir=(1, 0, 0), z_dir=(0, 0, 1)) p2 = p * Location((1, 2, -1), (0, 0, 45)) self.assertVectorAlmostEquals(p2.origin, (2, 4, 2), 6) - self.assertVectorAlmostEquals(p2.x_dir, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6) - self.assertVectorAlmostEquals(p2.y_dir, (-math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6) + self.assertVectorAlmostEquals( + p2.x_dir, (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6 + ) + self.assertVectorAlmostEquals( + p2.y_dir, (-math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 6 + ) self.assertVectorAlmostEquals(p2.z_dir, (0, 0, 1), 6) p2 = p * Location((1, 2, -1), (0, 45, 0)) self.assertVectorAlmostEquals(p2.origin, (2, 4, 2), 6) - self.assertVectorAlmostEquals(p2.x_dir, (math.sqrt(2) / 2, 0, -math.sqrt(2) / 2), 6) + self.assertVectorAlmostEquals( + p2.x_dir, (math.sqrt(2) / 2, 0, -math.sqrt(2) / 2), 6 + ) self.assertVectorAlmostEquals(p2.y_dir, (0, 1, 0), 6) - self.assertVectorAlmostEquals(p2.z_dir, (math.sqrt(2) / 2, 0, math.sqrt(2) / 2), 6) + self.assertVectorAlmostEquals( + p2.z_dir, (math.sqrt(2) / 2, 0, math.sqrt(2) / 2), 6 + ) p2 = p * Location((1, 2, -1), (45, 0, 0)) self.assertVectorAlmostEquals(p2.origin, (2, 4, 2), 6) self.assertVectorAlmostEquals(p2.x_dir, (1, 0, 0), 6) - self.assertVectorAlmostEquals(p2.y_dir, (0, math.sqrt(2) / 2, math.sqrt(2) / 2), 6) - self.assertVectorAlmostEquals(p2.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6) + self.assertVectorAlmostEquals( + p2.y_dir, (0, math.sqrt(2) / 2, math.sqrt(2) / 2), 6 + ) + self.assertVectorAlmostEquals( + p2.z_dir, (0, -math.sqrt(2) / 2, math.sqrt(2) / 2), 6 + ) with self.assertRaises(TypeError): p2 * Vector(1, 1, 1) @@ -2570,7 +2669,9 @@ def test_shift_origin_axis(self): def test_shift_origin_vertex(self): box = Box(1, 1, 1, align=Align.MIN) front = box.faces().sort_by(Axis.X)[-1] - pln = Plane(front).shift_origin(front.vertices().group_by(Axis.Z)[-1].sort_by(Axis.Y)[-1]) + pln = Plane(front).shift_origin( + front.vertices().group_by(Axis.Z)[-1].sort_by(Axis.Y)[-1] + ) with BuildPart() as p: add(box) with BuildSketch(pln): @@ -2656,7 +2757,9 @@ def test_plane_equal(self): def test_plane_not_equal(self): # type difference for value in [None, 0, 1, "abc"]: - self.assertNotEqual(Plane(origin=(0, 0, 0), x_dir=(1, 0, 0), z_dir=(0, 0, 1)), value) + self.assertNotEqual( + Plane(origin=(0, 0, 0), x_dir=(1, 0, 0), z_dir=(0, 0, 1)), value + ) # origin difference self.assertNotEqual( Plane(origin=(0, 0, 0), x_dir=(1, 0, 0), z_dir=(0, 0, 1)), @@ -2679,7 +2782,9 @@ def test_to_location(self): self.assertVectorAlmostEquals(loc.orientation, (0, 0, 90), 5) def test_intersect(self): - self.assertVectorAlmostEquals(Plane.XY.intersect(Axis((1, 2, 3), (0, 0, -1))), (1, 2, 0), 5) + self.assertVectorAlmostEquals( + Plane.XY.intersect(Axis((1, 2, 3), (0, 0, -1))), (1, 2, 0), 5 + ) self.assertIsNone(Plane.XY.intersect(Axis((1, 2, 3), (0, 1, 0)))) self.assertEqual(Plane.XY.intersect(Plane.XZ), Axis.X) @@ -2696,7 +2801,9 @@ def test_from_non_planar_face(self): flat = Face.make_rect(1, 1) pln = Plane(flat) self.assertTrue(isinstance(pln, Plane)) - cyl = Solid.make_cylinder(1, 4).faces().filter_by(GeomType.PLANE, reverse=True)[0] + cyl = ( + Solid.make_cylinder(1, 4).faces().filter_by(GeomType.PLANE, reverse=True)[0] + ) with self.assertRaises(ValueError): pln = Plane(cyl) @@ -2747,7 +2854,8 @@ def test_flat_projection(self): .faces() ) projected_text_faces = [ - f.project_to_shape(sphere, projection_direction)[0] for f in planar_text_faces + f.project_to_shape(sphere, projection_direction)[0] + for f in planar_text_faces ] self.assertEqual(len(projected_text_faces), 4) @@ -2765,7 +2873,11 @@ def test_multiple_output_wires(self): def test_text_projection(self): sphere = Solid.make_sphere(50) arch_path = ( - sphere.cut(Solid.make_cylinder(80, 100, Plane(origin=(-50, 0, -70), z_dir=(1, 0, 0)))) + sphere.cut( + Solid.make_cylinder( + 80, 100, Plane(origin=(-50, 0, -70), z_dir=(1, 0, 0)) + ) + ) .edges() .sort_by(Axis.Z)[0] ) @@ -2926,7 +3038,9 @@ def test_split_by_perimeter(self): # Test 3 - Invalid, wire on shape edge target3 = Solid.make_cylinder(5, 10, Plane((0, 0, -5))) - square_projected = square.project_to_shape(target3, (-1, 0, 0))[0].unwrap(fully=True) + square_projected = square.project_to_shape(target3, (-1, 0, 0))[0].unwrap( + fully=True + ) project_perimeter = square_projected.outer_wire() inside3 = target3.split_by_perimeter(project_perimeter, Keep.INSIDE) self.assertIsNone(inside3) @@ -2961,7 +3075,9 @@ def test_max_fillet(self): max = test_object.max_fillet(test_object.edges()) self.assertAlmostEqual(max, max_values[i], 2) with self.assertRaises(RuntimeError): - test_solids[0].max_fillet(test_solids[0].edges(), tolerance=1e-6, max_iterations=1) + test_solids[0].max_fillet( + test_solids[0].edges(), tolerance=1e-6, max_iterations=1 + ) with self.assertRaises(ValueError): box = Solid.make_box(1, 1, 1) box.fillet(0.75, box.edges()) @@ -3041,7 +3157,9 @@ def test_distance_to(self): def test_intersection(self): box = Solid.make_box(1, 1, 1) - intersections = box.intersect(Axis((0.5, 0.5, 4), (0, 0, -1))).vertices().sort_by(Axis.Z) + intersections = ( + box.intersect(Axis((0.5, 0.5, 4), (0, 0, -1))).vertices().sort_by(Axis.Z) + ) self.assertVectorAlmostEquals(intersections[0], (0.5, 0.5, 0), 5) self.assertVectorAlmostEquals(intersections[1], (0.5, 0.5, 1), 5) @@ -3141,10 +3259,15 @@ def test_manifold(self): self.assertTrue(Solid.make_box(1, 1, 1).is_manifold) self.assertTrue(Solid.make_box(1, 1, 1).shell().is_manifold) self.assertFalse( - Solid.make_box(1, 1, 1).shell().cut(Solid.make_box(0.5, 0.5, 0.5)).is_manifold + Solid.make_box(1, 1, 1) + .shell() + .cut(Solid.make_box(0.5, 0.5, 0.5)) + .is_manifold ) self.assertTrue( - Compound(children=[Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)]).is_manifold + Compound( + children=[Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)] + ).is_manifold ) def test_inherit_color(self): @@ -3210,7 +3333,9 @@ def test_copy_attributes_to(self): box.topo_parent = box2 blank = Compound() - box.copy_attributes_to(blank, ["color", "label", "joints", "children", "topo_parent"]) + box.copy_attributes_to( + blank, ["color", "label", "joints", "children", "topo_parent"] + ) self.assertEqual(blank.label, "box") self.assertTrue(all(c1 == c2 for c1, c2 in zip(blank.color, Color("Red")))) self.assertTrue(all(j1 == j2 for j1, j2 in zip(blank.joints, ["j1", "j2"]))) @@ -3247,7 +3372,9 @@ def test_sort_by(self): self.assertAlmostEqual(faces[-1].area, 2, 5) def test_filter_by_geomtype(self): - non_planar_faces = Solid.make_cylinder(1, 1).faces().filter_by(GeomType.PLANE, reverse=True) + non_planar_faces = ( + Solid.make_cylinder(1, 1).faces().filter_by(GeomType.PLANE, reverse=True) + ) self.assertEqual(len(non_planar_faces), 1) self.assertAlmostEqual(non_planar_faces[0].area, 2 * math.pi, 5) @@ -3271,7 +3398,9 @@ def test_filter_by_callable_predicate(self): self.assertEqual(len(shapelist.filter_by(lambda s: s.label == "B")), 1) def test_first_last(self): - vertices = Solid.make_box(1, 1, 1).vertices().sort_by(Axis((0, 0, 0), (1, 1, 1))) + vertices = ( + Solid.make_box(1, 1, 1).vertices().sort_by(Axis((0, 0, 0), (1, 1, 1))) + ) self.assertVectorAlmostEquals(vertices.last, (1, 1, 1), 5) self.assertVectorAlmostEquals(vertices.first, (0, 0, 0), 5) @@ -3282,7 +3411,12 @@ def test_group_by(self): edges = Solid.make_box(1, 1, 1).edges().group_by(SortBy.LENGTH) self.assertEqual(len(edges[0]), 12) - edges = Solid.make_cone(2, 1, 2).edges().filter_by(GeomType.CIRCLE).group_by(SortBy.RADIUS) + edges = ( + Solid.make_cone(2, 1, 2) + .edges() + .filter_by(GeomType.CIRCLE) + .group_by(SortBy.RADIUS) + ) self.assertEqual(len(edges[0]), 1) edges = (Solid.make_cone(2, 1, 2).edges() | GeomType.CIRCLE) << SortBy.RADIUS @@ -3371,7 +3505,9 @@ def test_group_by_str_repr(self): " [," " ]]" ) - self.assertDunderReprEqual(repr(nonagon.edges().group_by(Axis.X)), expected_repr) + self.assertDunderReprEqual( + repr(nonagon.edges().group_by(Axis.X)), expected_repr + ) f = io.StringIO() p = pretty.PrettyPrinter(f) @@ -3384,7 +3520,9 @@ def test_distance(self): obj = (-0.2, 0.1, 0.5) edges = box.edges().sort_by_distance(obj) distances = [Vertex(*obj).distance_to(edge) for edge in edges] - self.assertTrue(all([distances[i] >= distances[i - 1] for i in range(1, len(edges))])) + self.assertTrue( + all([distances[i] >= distances[i - 1] for i in range(1, len(edges))]) + ) def test_distance_reverse(self): with BuildPart() as box: @@ -3392,7 +3530,9 @@ def test_distance_reverse(self): obj = (-0.2, 0.1, 0.5) edges = box.edges().sort_by_distance(obj, reverse=True) distances = [Vertex(*obj).distance_to(edge) for edge in edges] - self.assertTrue(all([distances[i] <= distances[i - 1] for i in range(1, len(edges))])) + self.assertTrue( + all([distances[i] <= distances[i - 1] for i in range(1, len(edges))]) + ) def test_distance_equal(self): with BuildPart() as box: @@ -3437,7 +3577,9 @@ def test_faces(self): self.assertEqual(len(sl.faces()), 9) def test_face(self): - sl = ShapeList([Vertex(1, 1, 1), Edge.make_line((0, 0), (1, 1)), Face.make_rect(2, 1)]) + sl = ShapeList( + [Vertex(1, 1, 1), Edge.make_line((0, 0), (1, 1)), Face.make_rect(2, 1)] + ) self.assertAlmostEqual(sl.face().area, 2 * 1, 5) sl = ShapeList([Solid.make_box(1, 1, 1), Solid.make_cylinder(1, 1)]) with self.assertWarns(UserWarning): @@ -3544,13 +3686,15 @@ def test_sweep(self): self.assertEqual(len(sweep_c2_c1.faces()), 2) self.assertEqual(len(sweep_w_w.faces()), 4) self.assertEqual(len(sweep_c2_c2.faces()), 4) - + def test_loft(self): r = 3 h = 2 - loft = Shell.make_loft([Wire.make_circle(r,Plane((0,0,h))), Wire.make_circle(r) ]) + loft = Shell.make_loft( + [Wire.make_circle(r, Plane((0, 0, h))), Wire.make_circle(r)] + ) self.assertEqual(loft.volume, 0, "A shell has no volume") - cylinder_area = 2*math.pi*r*h + cylinder_area = 2 * math.pi * r * h self.assertAlmostEqual(loft.area, cylinder_area) @@ -3601,7 +3745,9 @@ def test_extrude_taper(self): for taper in [10, -10]: offset_amt = -direction.length * math.tan(math.radians(taper)) for face in [rect, flipped]: - with self.subTest(f"{direction=}, {taper=}, flipped={face==flipped}"): + with self.subTest( + f"{direction=}, {taper=}, flipped={face==flipped}" + ): taper_solid = Solid.extrude_taper(face, direction, taper) # V = 1/3 × h × (a² + b² + ab) h = Vector(direction).length @@ -3611,10 +3757,14 @@ def test_extrude_taper(self): bbox = taper_solid.bounding_box() size = max(1, b) / 2 if direction.Z > 0: - self.assertVectorAlmostEquals(bbox.min, (-size, -size, 0), 1) + self.assertVectorAlmostEquals( + bbox.min, (-size, -size, 0), 1 + ) self.assertVectorAlmostEquals(bbox.max, (size, size, h), 1) else: - self.assertVectorAlmostEquals(bbox.min, (-size, -size, -h), 1) + self.assertVectorAlmostEquals( + bbox.min, (-size, -size, -h), 1 + ) self.assertVectorAlmostEquals(bbox.max, (size, size, 0), 1) def test_extrude_taper_with_hole(self): @@ -3665,21 +3815,27 @@ def test_extrude_linear_with_rotation(self): self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5) def test_make_loft(self): - loft = Solid.make_loft([Wire.make_rect(2, 2), Wire.make_circle(1, Plane((0, 0, 1)))]) + loft = Solid.make_loft( + [Wire.make_rect(2, 2), Wire.make_circle(1, Plane((0, 0, 1)))] + ) self.assertAlmostEqual(loft.volume, (4 + math.pi) / 2, 1) with self.assertRaises(ValueError): Solid.make_loft([Wire.make_rect(1, 1)]) def test_make_loft_with_vertices(self): - loft = Solid.make_loft([Vertex(0, 0, -1), Wire.make_rect(1, 1.5), Vertex(0, 0, 1)], True) + loft = Solid.make_loft( + [Vertex(0, 0, -1), Wire.make_rect(1, 1.5), Vertex(0, 0, 1)], True + ) self.assertAlmostEqual(loft.volume, 1, 5) with self.assertRaises(ValueError): - Solid.make_loft([Wire.make_rect(1, 1), Vertex(0, 0, 1), Wire.make_rect(1, 1)]) + Solid.make_loft( + [Wire.make_rect(1, 1), Vertex(0, 0, 1), Wire.make_rect(1, 1)] + ) - with self.assertRaises(ValueError): - Solid.make_loft([Vertex(0, 0, 1), Vertex(0, 0, 2)]) + with self.assertRaises(ValueError): + Solid.make_loft([Vertex(0, 0, 1), Vertex(0, 0, 2)]) def test_extrude_until(self): square = Face.make_rect(1, 1) @@ -3757,7 +3913,9 @@ def test_vector_rotate(self): vector_x = Vector(1, 0, 1).rotate(Axis.X, 45) vector_y = Vector(1, 2, 1).rotate(Axis.Y, 45) vector_z = Vector(-1, -1, 3).rotate(Axis.Z, 45) - self.assertVectorAlmostEquals(vector_x, (1, -math.sqrt(2) / 2, math.sqrt(2) / 2), 7) + self.assertVectorAlmostEquals( + vector_x, (1, -math.sqrt(2) / 2, math.sqrt(2) / 2), 7 + ) self.assertVectorAlmostEquals(vector_y, (math.sqrt(2), 2, 0), 7) self.assertVectorAlmostEquals(vector_z, (0, -math.sqrt(2), 3), 7) @@ -3909,11 +4067,21 @@ def test_vector_transform(self): pxy = Plane.XY pxy_o1 = Plane.XY.offset(1) self.assertEqual(a.transform(pxy.forward_transform, is_direction=False), a) - self.assertEqual(a.transform(pxy.forward_transform, is_direction=True), a.normalized()) - self.assertEqual(a.transform(pxy_o1.forward_transform, is_direction=False), Vector(1, 2, 2)) - self.assertEqual(a.transform(pxy_o1.forward_transform, is_direction=True), a.normalized()) - self.assertEqual(a.transform(pxy_o1.reverse_transform, is_direction=False), Vector(1, 2, 4)) - self.assertEqual(a.transform(pxy_o1.reverse_transform, is_direction=True), a.normalized()) + self.assertEqual( + a.transform(pxy.forward_transform, is_direction=True), a.normalized() + ) + self.assertEqual( + a.transform(pxy_o1.forward_transform, is_direction=False), Vector(1, 2, 2) + ) + self.assertEqual( + a.transform(pxy_o1.forward_transform, is_direction=True), a.normalized() + ) + self.assertEqual( + a.transform(pxy_o1.reverse_transform, is_direction=False), Vector(1, 2, 4) + ) + self.assertEqual( + a.transform(pxy_o1.reverse_transform, is_direction=True), a.normalized() + ) def test_intersect(self): v1 = Vector(1, 2, 3) @@ -3929,8 +4097,12 @@ def test_intersect(self): self.assertVectorAlmostEquals(v1 & Plane((1, 2, 3)), (1, 2, 3), 5) self.assertIsNone(v1 & Plane.XY) - self.assertVectorAlmostEquals((v1 & Solid.make_box(2, 4, 5)).vertex(), (1, 2, 3), 5) - self.assertTrue(len(v1.intersect(Solid.make_box(0.5, 0.5, 0.5)).vertices()) == 0) + self.assertVectorAlmostEquals( + (v1 & Solid.make_box(2, 4, 5)).vertex(), (1, 2, 3), 5 + ) + self.assertTrue( + len(v1.intersect(Solid.make_box(0.5, 0.5, 0.5)).vertices()) == 0 + ) class TestVectorLike(DirectApiTestCase): @@ -3974,8 +4146,12 @@ def test_vertex_volume(self): def test_vertex_add(self): test_vertex = Vertex(0, 0, 0) - self.assertVectorAlmostEquals(Vector(test_vertex + (100, -40, 10)), (100, -40, 10), 7) - self.assertVectorAlmostEquals(Vector(test_vertex + Vector(100, -40, 10)), (100, -40, 10), 7) + self.assertVectorAlmostEquals( + Vector(test_vertex + (100, -40, 10)), (100, -40, 10), 7 + ) + self.assertVectorAlmostEquals( + Vector(test_vertex + Vector(100, -40, 10)), (100, -40, 10), 7 + ) self.assertVectorAlmostEquals( Vector(test_vertex + Vertex(100, -40, 10)), (100, -40, 10), @@ -3986,7 +4162,9 @@ def test_vertex_add(self): def test_vertex_sub(self): test_vertex = Vertex(0, 0, 0) - self.assertVectorAlmostEquals(Vector(test_vertex - (100, -40, 10)), (-100, 40, -10), 7) + self.assertVectorAlmostEquals( + Vector(test_vertex - (100, -40, 10)), (-100, 40, -10), 7 + ) self.assertVectorAlmostEquals( Vector(test_vertex - Vector(100, -40, 10)), (-100, 40, -10), 7 ) @@ -4021,30 +4199,42 @@ def test_no_intersect(self): class TestWire(DirectApiTestCase): def test_ellipse_arc(self): full_ellipse = Wire.make_ellipse(2, 1) - half_ellipse = Wire.make_ellipse(2, 1, start_angle=0, end_angle=180, closed=True) + half_ellipse = Wire.make_ellipse( + 2, 1, start_angle=0, end_angle=180, closed=True + ) self.assertAlmostEqual(full_ellipse.area / 2, half_ellipse.area, 5) def test_stitch(self): - half_ellipse1 = Wire.make_ellipse(2, 1, start_angle=0, end_angle=180, closed=False) - half_ellipse2 = Wire.make_ellipse(2, 1, start_angle=180, end_angle=360, closed=False) + half_ellipse1 = Wire.make_ellipse( + 2, 1, start_angle=0, end_angle=180, closed=False + ) + half_ellipse2 = Wire.make_ellipse( + 2, 1, start_angle=180, end_angle=360, closed=False + ) ellipse = half_ellipse1.stitch(half_ellipse2) self.assertEqual(len(ellipse.wires()), 1) def test_fillet_2d(self): square = Wire.make_rect(1, 1) squaroid = square.fillet_2d(0.1, square.vertices()) - self.assertAlmostEqual(squaroid.length, 4 * (1 - 2 * 0.1) + 2 * math.pi * 0.1, 5) + self.assertAlmostEqual( + squaroid.length, 4 * (1 - 2 * 0.1) + 2 * math.pi * 0.1, 5 + ) def test_chamfer_2d(self): square = Wire.make_rect(1, 1) squaroid = square.chamfer_2d(0.1, 0.1, square.vertices()) - self.assertAlmostEqual(squaroid.length, 4 * (1 - 2 * 0.1 + 0.1 * math.sqrt(2)), 5) + self.assertAlmostEqual( + squaroid.length, 4 * (1 - 2 * 0.1 + 0.1 * math.sqrt(2)), 5 + ) def test_chamfer_2d_edge(self): square = Wire.make_rect(1, 1) edge = square.edges().sort_by(Axis.Y)[0] vertex = edge.vertices().sort_by(Axis.X)[0] - square = square.chamfer_2d(distance=0.1, distance2=0.2, vertices=[vertex], edge=edge) + square = square.chamfer_2d( + distance=0.1, distance2=0.2, vertices=[vertex], edge=edge + ) self.assertAlmostEqual(square.edges().sort_by(Axis.Y)[0].length, 0.9) def test_make_convex_hull(self): From 8fd5a6775e5cd5fa5b155ac21ddd4e5f4fd1c303 Mon Sep 17 00:00:00 2001 From: Romain FERRU Date: Fri, 8 Nov 2024 20:29:20 +0100 Subject: [PATCH 3/3] added > 2 vertices test --- tests/test_direct_api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index 596567b7..413a135f 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -3687,7 +3687,7 @@ def test_sweep(self): self.assertEqual(len(sweep_w_w.faces()), 4) self.assertEqual(len(sweep_c2_c2.faces()), 4) - def test_loft(self): + def test_make_loft(self): r = 3 h = 2 loft = Shell.make_loft( @@ -3697,7 +3697,6 @@ def test_loft(self): cylinder_area = 2 * math.pi * r * h self.assertAlmostEqual(loft.area, cylinder_area) - class TestSolid(DirectApiTestCase): def test_make_solid(self): box_faces = Solid.make_box(1, 1, 1).faces() @@ -3837,6 +3836,9 @@ def test_make_loft_with_vertices(self): with self.assertRaises(ValueError): Solid.make_loft([Vertex(0, 0, 1), Vertex(0, 0, 2)]) + with self.assertRaises(ValueError): + Solid.make_loft([Vertex(0, 0, 1),Wire.make_rect(1, 1), Vertex(0, 0, 2), Vertex(0, 0, 3)]) + def test_extrude_until(self): square = Face.make_rect(1, 1) box = Solid.make_box(4, 4, 1, Plane((-2, -2, 3)))