diff --git a/examples/build123d_logo.py b/examples/build123d_logo.py index d85b29a..9333a53 100644 --- a/examples/build123d_logo.py +++ b/examples/build123d_logo.py @@ -29,12 +29,12 @@ import cadquery as cq with BuildSketch() as logo_text: Text("123d", fontsize=10, valign=Valign.BOTTOM) - font_height = logo_text.vertices().sort_by(SortBy.Y)[-1].y + font_height = (logo_text.vertices() >> Axis.Y).y with BuildSketch() as build_text: Text("build", fontsize=5, halign=Halign.CENTER) build_bb = BoundingBox(build_text.sketch, mode=Mode.PRIVATE) - build_vertices = build_bb.vertices().sort_by(SortBy.X) + build_vertices = build_bb.vertices() > Axis.X build_width = build_vertices[-1].x - build_vertices[0].x with BuildLine() as one: @@ -50,7 +50,7 @@ with BuildPart() as three_d: with BuildSketch(): Text("3d", fontsize=10, valign=Valign.BOTTOM) Extrude(amount=font_height * 0.3) - logo_width = three_d.vertices().sort_by(SortBy.X)[-1].x + logo_width = (three_d.vertices() >> Axis.X).x with BuildLine() as arrow_left: t1 = TangentArc((0, 0), (1, 0.75), tangent=(1, 0)) diff --git a/examples/extrude.py b/examples/extrude.py index b47c770..69310d0 100644 --- a/examples/extrude.py +++ b/examples/extrude.py @@ -60,7 +60,7 @@ with BuildPart() as single_multiple: with BuildPart() as non_planar: Cylinder(10, 20, rotation=(90, 0, 0), centered=(True, False, True)) Box(10, 10, 10, centered=(True, True, False), mode=Mode.INTERSECT) - Extrude(non_planar.part.faces().sort_by(SortBy.Z)[0], amount=2, mode=Mode.REPLACE) + Extrude(non_planar.part.faces() << Axis.Z, amount=2, mode=Mode.REPLACE) # Taper Extrude and Extrude to "next" while creating a Cherry MX key cap # See: https://www.cherrymx.de/en/dev.html diff --git a/examples/holes.py b/examples/holes.py index 84c5d77..a43e078 100644 --- a/examples/holes.py +++ b/examples/holes.py @@ -47,7 +47,7 @@ with BuildPart() as recessed_counter_sink: with BuildPart() as flush_counter_sink: with Locations((10, 10)): Cylinder(radius=3, height=2) - with Workplanes(flush_counter_sink.part.faces().sort_by(SortBy.Z)[-1]): + with Workplanes(flush_counter_sink.part.faces() >> Axis.Z): CounterSinkHole(radius=1, counter_sink_radius=1.5) if "show_object" in locals(): diff --git a/examples/lego.py b/examples/lego.py index 347ac3d..56a62bb 100644 --- a/examples/lego.py +++ b/examples/lego.py @@ -8,6 +8,7 @@ desc: This example creates a model of a double wide lego block with a parametric length (pip_count). + *** Don't edit this file without checking the lego tutorial *** license: @@ -26,7 +27,6 @@ license: limitations under the License. """ from build123d import * -from cadquery import Plane pip_count = 6 @@ -74,7 +74,7 @@ with BuildPart() as lego: wall_thickness, centered=(True, True, False), ) - with Workplanes(lego.faces().sort_by(SortBy.Z)[-1]): + with Workplanes(lego.faces() >> Axis.Z): with GridLocations(lego_unit_size, lego_unit_size, pip_count, 2): Cylinder( radius=pip_diameter / 2, height=pip_height, centered=(True, True, False) diff --git a/examples/vase.py b/examples/vase.py index df72b0c..d029f32 100644 --- a/examples/vase.py +++ b/examples/vase.py @@ -47,13 +47,14 @@ with BuildPart() as vase: l1 @ 0, ) BuildFace() - Revolve(axis_origin=(0, 0, 0), axis_direction=(0, 1, 0)) - Offset(openings=vase.faces().filter_by_axis(Axis.Y)[-1], amount=-1) + Revolve(axis=Axis.Y) + # Offset(openings=vase.faces().filter_by_axis(Axis.Y)[-1], amount=-1) + Offset(openings=(vase.faces() | Axis.Y) >> Axis.Y, amount=-1) top_edges = ( vase.edges().filter_by_position(Axis.Y, 60, 62).filter_by_type(Type.CIRCLE) ) Fillet(*top_edges, radius=0.25) - Fillet(vase.edges().sort_by(SortBy.Y)[0], radius=0.5) + Fillet(vase.edges() << Axis.Y, radius=0.5) if "show_object" in locals(): diff --git a/src/build123d/build_common.py b/src/build123d/build_common.py index 809c27e..750448d 100644 --- a/src/build123d/build_common.py +++ b/src/build123d/build_common.py @@ -29,10 +29,11 @@ license: limitations under the License. """ +from __future__ import annotations import contextvars from itertools import product from abc import ABC, abstractmethod -from math import radians, sqrt +from math import radians, sqrt, pi from typing import Iterable, Union from enum import Enum, auto from cadquery import ( @@ -104,8 +105,6 @@ Vector.X = property(_vector_x) Vector.Y = property(_vector_y) Vector.Z = property(_vector_z) -z_axis = (Vector(0, 0, 0), Vector(0, 0, 1)) - def vertex_eq_(self: Vertex, other: Vertex) -> bool: """True if the distance between the two vertices is lower than their tolerance""" @@ -175,6 +174,9 @@ class Select(Enum): ALL = auto() LAST = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + class Kind(Enum): """Offset corner transition""" @@ -183,6 +185,9 @@ class Kind(Enum): INTERSECTION = auto() TANGENT = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + class Keep(Enum): """Split options""" @@ -191,6 +196,9 @@ class Keep(Enum): BOTTOM = auto() BOTH = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + class Mode(Enum): """Combination Mode""" @@ -201,6 +209,9 @@ class Mode(Enum): REPLACE = auto() PRIVATE = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + class Transition(Enum): """Sweep discontinuity handling option""" @@ -209,6 +220,9 @@ class Transition(Enum): ROUND = auto() TRANSFORMED = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + class FontStyle(Enum): """Text Font Styles""" @@ -217,6 +231,9 @@ class FontStyle(Enum): BOLD = auto() ITALIC = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + class Halign(Enum): """Text Horizontal Alignment""" @@ -225,6 +242,9 @@ class Halign(Enum): LEFT = auto() RIGHT = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + class Valign(Enum): """Text Vertical Alignment""" @@ -233,6 +253,9 @@ class Valign(Enum): TOP = auto() BOTTOM = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + class Until(Enum): """Extrude limit""" @@ -240,27 +263,22 @@ class Until(Enum): NEXT = auto() LAST = auto() - -class Axis(Enum): - """One of the three dimensions""" - - X = auto() - Y = auto() - Z = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) class SortBy(Enum): """Sorting criteria""" - X = auto() - Y = auto() - Z = auto() LENGTH = auto() RADIUS = auto() AREA = auto() VOLUME = auto() DISTANCE = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + class Type(Enum): """CAD object type""" @@ -282,6 +300,9 @@ class Type(Enum): PARABOLA = auto() OTHER = auto() + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + def validate_inputs(validating_class, builder_context, objects=[]): """Validate that objects/operations and parameters apply""" @@ -355,15 +376,164 @@ RotationLike = Union[tuple[float, float, float], Rotation] PlaneLike = Union[str, Plane] +class Axis: + """Axis defined by point and direction""" + + @classmethod + @property + def X(self) -> Axis: + return Axis((0, 0, 0), (1, 0, 0)) + + @classmethod + @property + def Y(self) -> Axis: + return Axis((0, 0, 0), (0, 1, 0)) + + @classmethod + @property + def Z(self) -> Axis: + return Axis((0, 0, 0), (0, 0, 1)) + + def __init__(self, origin: VectorLike, direction: VectorLike): + self.wrapped = gp_Ax1( + Vector(origin).toPnt(), gp_Dir(*Vector(direction).normalized().toTuple()) + ) + self.position = Vector( + self.wrapped.Location().X(), + self.wrapped.Location().Y(), + self.wrapped.Location().Z(), + ) + self.direction = Vector( + self.wrapped.Direction().X(), + self.wrapped.Direction().Y(), + self.wrapped.Direction().Z(), + ) + + @classmethod + def from_occt(cls, axis: gp_Ax1) -> Axis: + """Create an Axis instance from the occt object""" + position = ( + axis.Location().X(), + axis.Location().Y(), + axis.Location().Z(), + ) + direction = ( + axis.Direction().X(), + axis.Direction().Y(), + axis.Direction().Z(), + ) + return Axis(position, direction) + + def __repr__(self) -> str: + return f"({self.position.toTuple()},{self.direction.toTuple()})" + + def __str__(self) -> str: + return f"Axis: ({self.position.toTuple()},{self.direction.toTuple()})" + + def copy(self) -> Axis: + """Return copy of self""" + # Doesn't support sub-classing + return Axis(self.position, self.direction) + + def to_location(self) -> Location: + """Return self as Location""" + return Location(Plane(origin=self.position, normal=self.direction)) + + def to_plane(self) -> Plane: + """Return self as Plane""" + return Plane(origin=self.position, normal=self.direction) + + def is_coaxial( + self, + other: Axis, + angular_tolerance: float = 1e-5, + linear_tolerance: float = 1e-5, + ) -> bool: + """are axes coaxial + + True if the angle between self and other is lower or equal to angular_tolerance and + the distance between self and other is lower or equal to linear_tolerance. + + Args: + other (Axis): axis to compare to + angular_tolerance (float, optional): max angular deviation. Defaults to 1e-5. + linear_tolerance (float, optional): max linear deviation. Defaults to 1e-5. + + Returns: + bool: axes are coaxial + """ + return self.wrapped.IsCoaxial( + other.wrapped, angular_tolerance * (pi / 180), linear_tolerance + ) + + def is_normal(self, other: Axis, angular_tolerance: float = 1e-5) -> bool: + """are axes normal + + Returns True if the direction of this and another axis are normal to each other. That is, + if the angle between the two axes is equal to 90° within the angular_tolerance. + + Args: + other (Axis): axis to compare to + angular_tolerance (float, optional): max angular deviation. Defaults to 1e-5. + + Returns: + bool: axes are normal + """ + return self.wrapped.IsNormal(other.wrapped, angular_tolerance * (pi / 180)) + + def is_opposite(self, other: Axis, angular_tolerance: float = 1e-5) -> bool: + """are axes opposite + + Returns True if the direction of this and another axis are parallel with opposite orientation. + That is, if the angle between the two axes is equal to 180° within the angular_tolerance. + + Args: + other (Axis): axis to compare to + angular_tolerance (float, optional): max angular deviation. Defaults to 1e-5. + + Returns: + bool: axes are opposite + """ + return self.wrapped.IsOpposite(other.wrapped, angular_tolerance * (pi / 180)) + + def is_parallel(self, other: Axis, angular_tolerance: float = 1e-5) -> bool: + """are axes parallel + + Returns True if the direction of this and another axis are parallel with same + orientation or opposite orientation. That is, if the angle between the two axes is + equal to 0° or 180° within the angular_tolerance. + + Args: + other (Axis): axis to compare to + angular_tolerance (float, optional): max angular deviation. Defaults to 1e-5. + + Returns: + bool: axes are parallel + """ + return self.wrapped.IsParallel(other.wrapped, angular_tolerance * (pi / 180)) + + def angle_between(self, other: Axis) -> float: + """calculate angle between axes + + Computes the angular value, in degrees, between the direction of self and other + between 0° and 360°. + + Args: + other (Axis): axis to compare to + + Returns: + float: angle between axes + """ + return self.wrapped.Angle(other.wrapped) * 180 / pi + + def reversed(self) -> Axis: + """Return a copy of self with the direction reversed""" + return Axis.from_occt(self.wrapped.Reversed()) + + class ShapeList(list): """Subclass of list with custom filter and sort methods appropriate to CAD""" - axis_map = { - Axis.X: ((1, 0, 0), (-1, 0, 0)), - Axis.Y: ((0, 1, 0), (0, -1, 0)), - Axis.Z: ((0, 0, 1), (0, 0, -1)), - } - def __init_subclass__(cls) -> None: super().__init_subclass__() @@ -388,40 +558,26 @@ class ShapeList(list): lambda o: isinstance(o, Edge) and o.geomType() == "LINE", self ) - result = [] - result = list( filter( - lambda o: ( - o.normalAt(None) - Vector(*ShapeList.axis_map[axis][0]) - ).Length - <= tolerance - or (o.normalAt(None) - Vector(*ShapeList.axis_map[axis][1])).Length - <= tolerance, + lambda o: axis.is_parallel( + Axis(o.Center(), o.normalAt(None)), tolerance + ), planar_faces, ) ) result.extend( list( filter( - lambda o: ( - o.tangentAt(0) - Vector(*ShapeList.axis_map[axis][0]) - ).Length - <= tolerance - or (o.tangentAt(0) - Vector(*ShapeList.axis_map[axis][1])).Length - <= tolerance, + lambda o: axis.is_parallel( + Axis(o.positionAt(0), o.tangentAt(0)), tolerance + ), linear_edges, ) ) ) - if axis == Axis.X: - result = sorted(result, key=lambda obj: obj.Center().x) - elif axis == Axis.Y: - result = sorted(result, key=lambda obj: obj.Center().y) - elif axis == Axis.Z: - result = sorted(result, key=lambda obj: obj.Center().z) - return ShapeList(result) + return ShapeList(result).sort_by(axis) def filter_by_position( self, @@ -444,38 +600,25 @@ class ShapeList(list): Returns: ShapeList: filtered object list """ - if axis == Axis.X: - if inclusive == (True, True): - result = filter(lambda o: min <= o.Center().x <= max, self) - elif inclusive == (True, False): - result = filter(lambda o: min <= o.Center().x < max, self) - elif inclusive == (False, True): - result = filter(lambda o: min < o.Center().x <= max, self) - elif inclusive == (False, False): - result = filter(lambda o: min < o.Center().x < max, self) - result = sorted(result, key=lambda obj: obj.Center().x) - elif axis == Axis.Y: - if inclusive == (True, True): - result = filter(lambda o: min <= o.Center().y <= max, self) - elif inclusive == (True, False): - result = filter(lambda o: min <= o.Center().y < max, self) - elif inclusive == (False, True): - result = filter(lambda o: min < o.Center().y <= max, self) - elif inclusive == (False, False): - result = filter(lambda o: min < o.Center().y < max, self) - result = sorted(result, key=lambda obj: obj.Center().y) - elif axis == Axis.Z: - if inclusive == (True, True): - result = filter(lambda o: min <= o.Center().z <= max, self) - elif inclusive == (True, False): - result = filter(lambda o: min <= o.Center().z < max, self) - elif inclusive == (False, True): - result = filter(lambda o: min < o.Center().z <= max, self) - elif inclusive == (False, False): - result = filter(lambda o: min < o.Center().z < max, self) - result = sorted(result, key=lambda obj: obj.Center().z) + if inclusive == (True, True): + objects = filter( + lambda o: min <= axis.to_plane().toLocalCoords(o).Center().z <= max, + self, + ) + elif inclusive == (True, False): + objects = filter( + lambda o: min <= axis.to_plane().toLocalCoords(o).Center().z < max, self + ) + elif inclusive == (False, True): + objects = filter( + lambda o: min < axis.to_plane().toLocalCoords(o).Center().z <= max, self + ) + elif inclusive == (False, False): + objects = filter( + lambda o: min < axis.to_plane().toLocalCoords(o).Center().z < max, self + ) - return ShapeList(result) + return ShapeList(objects).sort_by(axis) def filter_by_type( self, @@ -495,7 +638,7 @@ class ShapeList(list): result = filter(lambda o: o.geomType() == type.name, self) return ShapeList(result) - def sort_by(self, sort_by: SortBy = SortBy.Z, reverse: bool = False): + def sort_by(self, sort_by: Union[Axis, SortBy] = Axis.Z, reverse: bool = False): """sort by Sort objects by provided criteria. Note that not all sort_by criteria apply to all @@ -508,57 +651,73 @@ class ShapeList(list): Returns: ShapeList: sorted list of objects """ - if sort_by == SortBy.X: + if isinstance(sort_by, Axis): objects = sorted( self, - key=lambda obj: obj.Center().x, - reverse=reverse, - ) - elif sort_by == SortBy.Y: - objects = sorted( - self, - key=lambda obj: obj.Center().y, - reverse=reverse, - ) - elif sort_by == SortBy.Z: - objects = sorted( - self, - key=lambda obj: obj.Center().z, - reverse=reverse, - ) - elif sort_by == SortBy.LENGTH: - objects = sorted( - self, - key=lambda obj: obj.Length(), - reverse=reverse, - ) - elif sort_by == SortBy.RADIUS: - objects = sorted( - self, - key=lambda obj: obj.radius(), - reverse=reverse, - ) - elif sort_by == SortBy.DISTANCE: - objects = sorted( - self, - key=lambda obj: obj.Center().Length, - reverse=reverse, - ) - elif sort_by == SortBy.AREA: - objects = sorted( - self, - key=lambda obj: obj.Area(), - reverse=reverse, - ) - elif sort_by == SortBy.VOLUME: - objects = sorted( - self, - key=lambda obj: obj.Volume(), + key=lambda o: sort_by.to_plane().toLocalCoords(o).Center().z, reverse=reverse, ) + elif isinstance(sort_by, SortBy): + if sort_by == SortBy.LENGTH: + objects = sorted( + self, + key=lambda obj: obj.Length(), + reverse=reverse, + ) + elif sort_by == SortBy.RADIUS: + objects = sorted( + self, + key=lambda obj: obj.radius(), + reverse=reverse, + ) + elif sort_by == SortBy.DISTANCE: + objects = sorted( + self, + key=lambda obj: obj.Center().Length, + reverse=reverse, + ) + elif sort_by == SortBy.AREA: + objects = sorted( + self, + key=lambda obj: obj.Area(), + reverse=reverse, + ) + elif sort_by == SortBy.VOLUME: + objects = sorted( + self, + key=lambda obj: obj.Volume(), + reverse=reverse, + ) + else: + raise ValueError(f"Sort by {type(sort_by)} unsupported") + return ShapeList(objects) + def __gt__(self, sort_by: Union[Axis, SortBy] = Axis.Z): + """Sort operator""" + return self.sort_by(sort_by) + + def __lt__(self, sort_by: Union[Axis, SortBy] = Axis.Z): + """Reverse sort operator""" + return self.sort_by(sort_by, reverse=True) + + def __rshift__(self, sort_by: Union[Axis, SortBy] = Axis.Z): + """Sort and select largest element operator""" + return self.sort_by(sort_by)[-1] + + def __lshift__(self, sort_by: Union[Axis, SortBy] = Axis.Z): + """Sort and select smallest element operator""" + return self.sort_by(sort_by)[0] + + def __or__(self, axis: Axis = Axis.Z): + """Filter by axis operator""" + return self.filter_by_axis(axis) + + def __mod__(self, type: Type): + """Filter by type operator""" + return self.filter_by_type(type) + def _vertices(self: Shape) -> ShapeList[Vertex]: """Return ShapeList of Vertex in self""" diff --git a/src/build123d/build_part.py b/src/build123d/build_part.py index a7bb4ed..9b17a0f 100644 --- a/src/build123d/build_part.py +++ b/src/build123d/build_part.py @@ -623,8 +623,7 @@ class Revolve(Compound): Args: profiles (Face, optional): sequence of 2D profile to revolve. - axis_origin (VectorLike, optional): axis start in local coordinates. Defaults to (0, 0, 0). - axis_direction (VectorLike, optional): axis direction. Defaults to (0, 1, 0). + axis (Axis): axis of rotation. revolution_arc (float, optional): angular size of revolution. Defaults to 360.0. mode (Mode, optional): combination mode. Defaults to Mode.ADD. @@ -635,8 +634,7 @@ class Revolve(Compound): def __init__( self, *profiles: Face, - axis_origin: VectorLike, - axis_direction: VectorLike, + axis: Axis, revolution_arc: float = 360.0, mode: Mode = Mode.ADD, ): @@ -653,27 +651,23 @@ class Revolve(Compound): profiles = context.pending_faces context.pending_faces = [] - axis_origin = Vector(axis_origin) - axis_direction = Vector(axis_direction) - self.profiles = profiles - self.axis_origin = axis_origin - self.axis_direction = axis_direction + self.axis = axis self.revolution_arc = revolution_arc self.mode = mode new_solids = [] for profile in profiles: - # axis_origin must be on the same plane as profile + # axis origin must be on the same plane as profile face_occt_pln = gp_Pln( profile.Center().toPnt(), profile.normalAt(profile.Center()).toDir() ) - if not face_occt_pln.Contains(axis_origin.toPnt(), 1e-5): + if not face_occt_pln.Contains(axis.position.toPnt(), 1e-5): raise ValueError( - "axis_origin must be on the same plane as the face to revolve" + "axis origin must be on the same plane as the face to revolve" ) if not face_occt_pln.Contains( - gp_Lin(axis_origin.toPnt(), axis_direction.toDir()), 1e-5, 1e-5 + gp_Lin(axis.position.toPnt(), axis.direction.toDir()), 1e-5, 1e-5 ): raise ValueError( "axis must be in the same plane as the face to revolve" @@ -682,8 +676,8 @@ class Revolve(Compound): new_solid = Solid.revolve( profile, angle, - axis_origin, - axis_origin + axis_direction, + axis.position, + axis.position + axis.direction, ) new_solids.extend( [ diff --git a/src/build123d/build_sketch.py b/src/build123d/build_sketch.py index 706adf4..4efb0fa 100644 --- a/src/build123d/build_sketch.py +++ b/src/build123d/build_sketch.py @@ -300,7 +300,7 @@ class Circle(Compound): 0 if centered[0] else radius, 0 if centered[1] else radius, ) - face = Face.makeFromWires(Wire.makeCircle(radius, *z_axis)).moved( + face = Face.makeFromWires(Wire.makeCircle(radius, (0, 0, 0), (0, 0, 1))).moved( Location(center_offset) ) new_faces = [ @@ -381,7 +381,9 @@ class Polygon(Compound): context: BuildSketch = BuildSketch._get_context() validate_inputs(self, context) poly_pts = [Vector(p) for p in pts] - face = Face.makeFromWires(Wire.makePolygon(poly_pts)).rotate(*z_axis, rotation) + face = Face.makeFromWires(Wire.makePolygon(poly_pts)).rotate( + (0, 0, 0), (0, 0, 1), rotation + ) bounding_box = face.BoundingBox() center_offset = Vector( 0 if centered[0] else bounding_box.xlen / 2, @@ -420,7 +422,7 @@ class Rectangle(Compound): context: BuildSketch = BuildSketch._get_context() validate_inputs(self, context) - face = Face.makePlane(height, width).rotate(*z_axis, rotation) + face = Face.makePlane(height, width).rotate((0, 0, 0), (0, 0, 1), rotation) bounding_box = face.BoundingBox() center_offset = Vector( 0 if centered[0] else bounding_box.xlen / 2, @@ -466,7 +468,9 @@ class RegularPolygon(Compound): ) for i in range(side_count + 1) ] - face = Face.makeFromWires(Wire.makePolygon(pts)).rotate(*z_axis, rotation) + face = Face.makeFromWires(Wire.makePolygon(pts)).rotate( + (0, 0, 0), (0, 0, 1), rotation + ) bounding_box = face.BoundingBox() center_offset = Vector( 0 if centered[0] else bounding_box.xlen / 2, @@ -509,7 +513,9 @@ class SlotArc(Compound): if isinstance(arc, Edge): raise ValueError("Bug - Edges aren't supported by offset") # arc_wire = arc if isinstance(arc, Wire) else Wire.assembleEdges([arc]) - face = Face.makeFromWires(arc.offset2D(height / 2)[0]).rotate(*z_axis, rotation) + face = Face.makeFromWires(arc.offset2D(height / 2)[0]).rotate( + (0, 0, 0), (0, 0, 1), rotation + ) new_faces = [ face.moved(location) for location in LocationList._get_context().locations ] @@ -553,7 +559,7 @@ class SlotCenterPoint(Compound): Edge.makeLine(center_v, center_v - half_line), ] )[0].offset2D(height / 2)[0] - ).rotate(*z_axis, rotation) + ).rotate((0, 0, 0), (0, 0, 1), rotation) new_faces = [ face.moved(location) for location in LocationList._get_context().locations ] @@ -591,7 +597,7 @@ class SlotCenterToCenter(Compound): Edge.makeLine(Vector(), Vector(+center_separation / 2, 0, 0)), ] ).offset2D(height / 2)[0] - ).rotate(*z_axis, rotation) + ).rotate((0, 0, 0), (0, 0, 1), rotation) new_faces = [ face.moved(location) for location in LocationList._get_context().locations ] @@ -628,7 +634,7 @@ class SlotOverall(Compound): Edge.makeLine(Vector(), Vector(+width / 2 - height / 2, 0, 0)), ] ).offset2D(height / 2)[0] - ).rotate(*z_axis, rotation) + ).rotate((0, 0, 0), (0, 0, 1), rotation) new_faces = [ face.moved(location) for location in LocationList._get_context().locations ] @@ -741,7 +747,9 @@ class Trapezoid(Compound): ) ) pts.append(pts[0]) - face = Face.makeFromWires(Wire.makePolygon(pts)).rotate(*z_axis, rotation) + face = Face.makeFromWires(Wire.makePolygon(pts)).rotate( + (0, 0, 0), (0, 0, 1), rotation + ) bounding_box = face.BoundingBox() center_offset = Vector( 0 if centered[0] else bounding_box.xlen / 2, diff --git a/tests/build_common_tests.py b/tests/build_common_tests.py index 556bffa..ffbb227 100644 --- a/tests/build_common_tests.py +++ b/tests/build_common_tests.py @@ -58,9 +58,10 @@ class TestProperties(unittest.TestCase): self.assertTupleAlmostEquals((v.x, v.y, v.z), (1, 2, 3), 5) def test_vector_properties(self): - v = Vector(1,2,3) + v = Vector(1, 2, 3) self.assertTupleAlmostEquals((v.X, v.Y, v.Z), (1, 2, 3), 5) + class TestRotation(unittest.TestCase): """Test the Rotation derived class of Location""" @@ -209,24 +210,24 @@ class TestShapeList(unittest.TestCase): self.assertEqual(edges[0].radius(), 0.5) self.assertEqual(edges[-1].radius(), 1) - with self.subTest(sort_by=SortBy.X): + with self.subTest(sort_by="X"): with BuildPart() as test: Box(1, 1, 1) - edges = test.edges().sort_by(SortBy.X) + edges = test.edges() > Axis.X self.assertEqual(edges[0].Center().x, -0.5) self.assertEqual(edges[-1].Center().x, 0.5) - with self.subTest(sort_by=SortBy.Y): + with self.subTest(sort_by="Y"): with BuildPart() as test: Box(1, 1, 1) - edges = test.edges().sort_by(SortBy.Y) + edges = test.edges() > Axis.Y self.assertEqual(edges[0].Center().y, -0.5) self.assertEqual(edges[-1].Center().y, 0.5) - with self.subTest(sort_by=SortBy.Z): + with self.subTest(sort_by="Z"): with BuildPart() as test: Box(1, 1, 1) - edges = test.edges().sort_by(SortBy.Z) + edges = test.edges() > Axis.Z self.assertEqual(edges[0].Center().z, -0.5) self.assertEqual(edges[-1].Center().z, 0.5) diff --git a/tests/build_generic_tests.py b/tests/build_generic_tests.py index e2aae6b..57298f0 100644 --- a/tests/build_generic_tests.py +++ b/tests/build_generic_tests.py @@ -160,7 +160,7 @@ class TestOffset(unittest.TestCase): Box(10, 10, 10) Offset( amount=-1, - openings=test.faces().sort_by()[0], + openings=test.faces() >> Axis.Z, kind=Kind.INTERSECTION, ) self.assertAlmostEqual(test.part.Volume(), 10**3 - 8**2 * 9, 5) @@ -173,7 +173,7 @@ class BoundingBoxTests(unittest.TestCase): Circle(10) with BuildSketch(mode=Mode.PRIVATE) as bb: BoundingBox(*mickey.faces()) - ears = bb.vertices().sort_by(SortBy.Y)[:-2] + ears = (bb.vertices() > Axis.Y)[:-2] with Locations(*ears): Circle(7) self.assertAlmostEqual(mickey.sketch.Area(), 586.1521145312807, 5) diff --git a/tests/build_part_tests.py b/tests/build_part_tests.py index cb74abd..b53b257 100644 --- a/tests/build_part_tests.py +++ b/tests/build_part_tests.py @@ -296,7 +296,7 @@ class TestRevolve(unittest.TestCase): l1 @ 0, ) BuildFace() - Revolve(axis_origin=(0, 0, 0), axis_direction=(0, 1, 0)) + Revolve(axis=Axis.Y) self.assertLess(test.part.Volume(), 22**2 * pi * 50, 5) self.assertGreater(test.part.Volume(), 144 * pi * 50, 5) @@ -309,7 +309,7 @@ class TestRevolve(unittest.TestCase): l3 = Line(l2 @ 1, (20, 0)) l4 = Line(l3 @ 1, l1 @ 0) BuildFace() - Revolve(axis_origin=(0, 0, 0), axis_direction=(1, 0, 0)) + Revolve(axis=Axis.X) self.assertLess(test.part.Volume(), 244 * pi * 20, 5) self.assertGreater(test.part.Volume(), 100 * pi * 20, 5) @@ -318,14 +318,14 @@ class TestRevolve(unittest.TestCase): with BuildSketch(): Rectangle(1, 1, centered=(False, False)) with self.assertRaises(ValueError): - Revolve(axis_origin=(1, 1, 1), axis_direction=(0, 1, 0)) + Revolve(axis=Axis((1, 1, 1), (0, 1, 0))) def test_invalid_axis_direction(self): with BuildPart(): with BuildSketch(): Rectangle(1, 1, centered=(False, False)) with self.assertRaises(ValueError): - Revolve(axis_origin=(0, 0, 0), axis_direction=(0, 0, 1)) + Revolve(axis=Axis.Z) class TestSection(unittest.TestCase):