Introduced Axis Class and Sort/Filter Operators

This commit is contained in:
Roger Maitland 2022-09-27 13:39:32 -04:00
parent b37945f5ab
commit b50d6d5fe1
11 changed files with 326 additions and 163 deletions

View file

@ -29,12 +29,12 @@ import cadquery as cq
with BuildSketch() as logo_text: with BuildSketch() as logo_text:
Text("123d", fontsize=10, valign=Valign.BOTTOM) 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: with BuildSketch() as build_text:
Text("build", fontsize=5, halign=Halign.CENTER) Text("build", fontsize=5, halign=Halign.CENTER)
build_bb = BoundingBox(build_text.sketch, mode=Mode.PRIVATE) 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 build_width = build_vertices[-1].x - build_vertices[0].x
with BuildLine() as one: with BuildLine() as one:
@ -50,7 +50,7 @@ with BuildPart() as three_d:
with BuildSketch(): with BuildSketch():
Text("3d", fontsize=10, valign=Valign.BOTTOM) Text("3d", fontsize=10, valign=Valign.BOTTOM)
Extrude(amount=font_height * 0.3) 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: with BuildLine() as arrow_left:
t1 = TangentArc((0, 0), (1, 0.75), tangent=(1, 0)) t1 = TangentArc((0, 0), (1, 0.75), tangent=(1, 0))

View file

@ -60,7 +60,7 @@ with BuildPart() as single_multiple:
with BuildPart() as non_planar: with BuildPart() as non_planar:
Cylinder(10, 20, rotation=(90, 0, 0), centered=(True, False, True)) Cylinder(10, 20, rotation=(90, 0, 0), centered=(True, False, True))
Box(10, 10, 10, centered=(True, True, False), mode=Mode.INTERSECT) 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 # Taper Extrude and Extrude to "next" while creating a Cherry MX key cap
# See: https://www.cherrymx.de/en/dev.html # See: https://www.cherrymx.de/en/dev.html

View file

@ -47,7 +47,7 @@ with BuildPart() as recessed_counter_sink:
with BuildPart() as flush_counter_sink: with BuildPart() as flush_counter_sink:
with Locations((10, 10)): with Locations((10, 10)):
Cylinder(radius=3, height=2) 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) CounterSinkHole(radius=1, counter_sink_radius=1.5)
if "show_object" in locals(): if "show_object" in locals():

View file

@ -8,6 +8,7 @@ desc:
This example creates a model of a double wide lego block with a This example creates a model of a double wide lego block with a
parametric length (pip_count). parametric length (pip_count).
*** Don't edit this file without checking the lego tutorial ***
license: license:
@ -26,7 +27,6 @@ license:
limitations under the License. limitations under the License.
""" """
from build123d import * from build123d import *
from cadquery import Plane
pip_count = 6 pip_count = 6
@ -74,7 +74,7 @@ with BuildPart() as lego:
wall_thickness, wall_thickness,
centered=(True, True, False), 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): with GridLocations(lego_unit_size, lego_unit_size, pip_count, 2):
Cylinder( Cylinder(
radius=pip_diameter / 2, height=pip_height, centered=(True, True, False) radius=pip_diameter / 2, height=pip_height, centered=(True, True, False)

View file

@ -47,13 +47,14 @@ with BuildPart() as vase:
l1 @ 0, l1 @ 0,
) )
BuildFace() BuildFace()
Revolve(axis_origin=(0, 0, 0), axis_direction=(0, 1, 0)) Revolve(axis=Axis.Y)
Offset(openings=vase.faces().filter_by_axis(Axis.Y)[-1], amount=-1) # Offset(openings=vase.faces().filter_by_axis(Axis.Y)[-1], amount=-1)
Offset(openings=(vase.faces() | Axis.Y) >> Axis.Y, amount=-1)
top_edges = ( top_edges = (
vase.edges().filter_by_position(Axis.Y, 60, 62).filter_by_type(Type.CIRCLE) vase.edges().filter_by_position(Axis.Y, 60, 62).filter_by_type(Type.CIRCLE)
) )
Fillet(*top_edges, radius=0.25) 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(): if "show_object" in locals():

View file

@ -29,10 +29,11 @@ license:
limitations under the License. limitations under the License.
""" """
from __future__ import annotations
import contextvars import contextvars
from itertools import product from itertools import product
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from math import radians, sqrt from math import radians, sqrt, pi
from typing import Iterable, Union from typing import Iterable, Union
from enum import Enum, auto from enum import Enum, auto
from cadquery import ( from cadquery import (
@ -104,8 +105,6 @@ Vector.X = property(_vector_x)
Vector.Y = property(_vector_y) Vector.Y = property(_vector_y)
Vector.Z = property(_vector_z) Vector.Z = property(_vector_z)
z_axis = (Vector(0, 0, 0), Vector(0, 0, 1))
def vertex_eq_(self: Vertex, other: Vertex) -> bool: def vertex_eq_(self: Vertex, other: Vertex) -> bool:
"""True if the distance between the two vertices is lower than their tolerance""" """True if the distance between the two vertices is lower than their tolerance"""
@ -175,6 +174,9 @@ class Select(Enum):
ALL = auto() ALL = auto()
LAST = auto() LAST = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
class Kind(Enum): class Kind(Enum):
"""Offset corner transition""" """Offset corner transition"""
@ -183,6 +185,9 @@ class Kind(Enum):
INTERSECTION = auto() INTERSECTION = auto()
TANGENT = auto() TANGENT = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
class Keep(Enum): class Keep(Enum):
"""Split options""" """Split options"""
@ -191,6 +196,9 @@ class Keep(Enum):
BOTTOM = auto() BOTTOM = auto()
BOTH = auto() BOTH = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
class Mode(Enum): class Mode(Enum):
"""Combination Mode""" """Combination Mode"""
@ -201,6 +209,9 @@ class Mode(Enum):
REPLACE = auto() REPLACE = auto()
PRIVATE = auto() PRIVATE = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
class Transition(Enum): class Transition(Enum):
"""Sweep discontinuity handling option""" """Sweep discontinuity handling option"""
@ -209,6 +220,9 @@ class Transition(Enum):
ROUND = auto() ROUND = auto()
TRANSFORMED = auto() TRANSFORMED = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
class FontStyle(Enum): class FontStyle(Enum):
"""Text Font Styles""" """Text Font Styles"""
@ -217,6 +231,9 @@ class FontStyle(Enum):
BOLD = auto() BOLD = auto()
ITALIC = auto() ITALIC = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
class Halign(Enum): class Halign(Enum):
"""Text Horizontal Alignment""" """Text Horizontal Alignment"""
@ -225,6 +242,9 @@ class Halign(Enum):
LEFT = auto() LEFT = auto()
RIGHT = auto() RIGHT = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
class Valign(Enum): class Valign(Enum):
"""Text Vertical Alignment""" """Text Vertical Alignment"""
@ -233,6 +253,9 @@ class Valign(Enum):
TOP = auto() TOP = auto()
BOTTOM = auto() BOTTOM = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
class Until(Enum): class Until(Enum):
"""Extrude limit""" """Extrude limit"""
@ -240,27 +263,22 @@ class Until(Enum):
NEXT = auto() NEXT = auto()
LAST = auto() LAST = auto()
def __repr__(self):
class Axis(Enum): return "<%s.%s>" % (self.__class__.__name__, self.name)
"""One of the three dimensions"""
X = auto()
Y = auto()
Z = auto()
class SortBy(Enum): class SortBy(Enum):
"""Sorting criteria""" """Sorting criteria"""
X = auto()
Y = auto()
Z = auto()
LENGTH = auto() LENGTH = auto()
RADIUS = auto() RADIUS = auto()
AREA = auto() AREA = auto()
VOLUME = auto() VOLUME = auto()
DISTANCE = auto() DISTANCE = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
class Type(Enum): class Type(Enum):
"""CAD object type""" """CAD object type"""
@ -282,6 +300,9 @@ class Type(Enum):
PARABOLA = auto() PARABOLA = auto()
OTHER = auto() OTHER = auto()
def __repr__(self):
return "<%s.%s>" % (self.__class__.__name__, self.name)
def validate_inputs(validating_class, builder_context, objects=[]): def validate_inputs(validating_class, builder_context, objects=[]):
"""Validate that objects/operations and parameters apply""" """Validate that objects/operations and parameters apply"""
@ -355,15 +376,164 @@ RotationLike = Union[tuple[float, float, float], Rotation]
PlaneLike = Union[str, Plane] 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): class ShapeList(list):
"""Subclass of list with custom filter and sort methods appropriate to CAD""" """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: def __init_subclass__(cls) -> None:
super().__init_subclass__() super().__init_subclass__()
@ -388,40 +558,26 @@ class ShapeList(list):
lambda o: isinstance(o, Edge) and o.geomType() == "LINE", self lambda o: isinstance(o, Edge) and o.geomType() == "LINE", self
) )
result = []
result = list( result = list(
filter( filter(
lambda o: ( lambda o: axis.is_parallel(
o.normalAt(None) - Vector(*ShapeList.axis_map[axis][0]) Axis(o.Center(), o.normalAt(None)), tolerance
).Length ),
<= tolerance
or (o.normalAt(None) - Vector(*ShapeList.axis_map[axis][1])).Length
<= tolerance,
planar_faces, planar_faces,
) )
) )
result.extend( result.extend(
list( list(
filter( filter(
lambda o: ( lambda o: axis.is_parallel(
o.tangentAt(0) - Vector(*ShapeList.axis_map[axis][0]) Axis(o.positionAt(0), o.tangentAt(0)), tolerance
).Length ),
<= tolerance
or (o.tangentAt(0) - Vector(*ShapeList.axis_map[axis][1])).Length
<= tolerance,
linear_edges, 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( def filter_by_position(
self, self,
@ -444,38 +600,25 @@ class ShapeList(list):
Returns: Returns:
ShapeList: filtered object list ShapeList: filtered object list
""" """
if axis == Axis.X: if inclusive == (True, True):
if inclusive == (True, True): objects = filter(
result = filter(lambda o: min <= o.Center().x <= max, self) lambda o: min <= axis.to_plane().toLocalCoords(o).Center().z <= max,
elif inclusive == (True, False): self,
result = filter(lambda o: min <= o.Center().x < max, self) )
elif inclusive == (False, True): elif inclusive == (True, False):
result = filter(lambda o: min < o.Center().x <= max, self) objects = filter(
elif inclusive == (False, False): lambda o: min <= axis.to_plane().toLocalCoords(o).Center().z < max, self
result = filter(lambda o: min < o.Center().x < max, self) )
result = sorted(result, key=lambda obj: obj.Center().x) elif inclusive == (False, True):
elif axis == Axis.Y: objects = filter(
if inclusive == (True, True): lambda o: min < axis.to_plane().toLocalCoords(o).Center().z <= max, self
result = filter(lambda o: min <= o.Center().y <= max, self) )
elif inclusive == (True, False): elif inclusive == (False, False):
result = filter(lambda o: min <= o.Center().y < max, self) objects = filter(
elif inclusive == (False, True): lambda o: min < axis.to_plane().toLocalCoords(o).Center().z < max, self
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)
return ShapeList(result) return ShapeList(objects).sort_by(axis)
def filter_by_type( def filter_by_type(
self, self,
@ -495,7 +638,7 @@ class ShapeList(list):
result = filter(lambda o: o.geomType() == type.name, self) result = filter(lambda o: o.geomType() == type.name, self)
return ShapeList(result) 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 by
Sort objects by provided criteria. Note that not all sort_by criteria apply to all Sort objects by provided criteria. Note that not all sort_by criteria apply to all
@ -508,57 +651,73 @@ class ShapeList(list):
Returns: Returns:
ShapeList: sorted list of objects ShapeList: sorted list of objects
""" """
if sort_by == SortBy.X: if isinstance(sort_by, Axis):
objects = sorted( objects = sorted(
self, self,
key=lambda obj: obj.Center().x, key=lambda o: sort_by.to_plane().toLocalCoords(o).Center().z,
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(),
reverse=reverse, 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) 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]: def _vertices(self: Shape) -> ShapeList[Vertex]:
"""Return ShapeList of Vertex in self""" """Return ShapeList of Vertex in self"""

View file

@ -623,8 +623,7 @@ class Revolve(Compound):
Args: Args:
profiles (Face, optional): sequence of 2D profile to revolve. profiles (Face, optional): sequence of 2D profile to revolve.
axis_origin (VectorLike, optional): axis start in local coordinates. Defaults to (0, 0, 0). axis (Axis): axis of rotation.
axis_direction (VectorLike, optional): axis direction. Defaults to (0, 1, 0).
revolution_arc (float, optional): angular size of revolution. Defaults to 360.0. revolution_arc (float, optional): angular size of revolution. Defaults to 360.0.
mode (Mode, optional): combination mode. Defaults to Mode.ADD. mode (Mode, optional): combination mode. Defaults to Mode.ADD.
@ -635,8 +634,7 @@ class Revolve(Compound):
def __init__( def __init__(
self, self,
*profiles: Face, *profiles: Face,
axis_origin: VectorLike, axis: Axis,
axis_direction: VectorLike,
revolution_arc: float = 360.0, revolution_arc: float = 360.0,
mode: Mode = Mode.ADD, mode: Mode = Mode.ADD,
): ):
@ -653,27 +651,23 @@ class Revolve(Compound):
profiles = context.pending_faces profiles = context.pending_faces
context.pending_faces = [] context.pending_faces = []
axis_origin = Vector(axis_origin)
axis_direction = Vector(axis_direction)
self.profiles = profiles self.profiles = profiles
self.axis_origin = axis_origin self.axis = axis
self.axis_direction = axis_direction
self.revolution_arc = revolution_arc self.revolution_arc = revolution_arc
self.mode = mode self.mode = mode
new_solids = [] new_solids = []
for profile in profiles: 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( face_occt_pln = gp_Pln(
profile.Center().toPnt(), profile.normalAt(profile.Center()).toDir() 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( 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( 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( raise ValueError(
"axis must be in the same plane as the face to revolve" "axis must be in the same plane as the face to revolve"
@ -682,8 +676,8 @@ class Revolve(Compound):
new_solid = Solid.revolve( new_solid = Solid.revolve(
profile, profile,
angle, angle,
axis_origin, axis.position,
axis_origin + axis_direction, axis.position + axis.direction,
) )
new_solids.extend( new_solids.extend(
[ [

View file

@ -300,7 +300,7 @@ class Circle(Compound):
0 if centered[0] else radius, 0 if centered[0] else radius,
0 if centered[1] 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) Location(center_offset)
) )
new_faces = [ new_faces = [
@ -381,7 +381,9 @@ class Polygon(Compound):
context: BuildSketch = BuildSketch._get_context() context: BuildSketch = BuildSketch._get_context()
validate_inputs(self, context) validate_inputs(self, context)
poly_pts = [Vector(p) for p in pts] 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() bounding_box = face.BoundingBox()
center_offset = Vector( center_offset = Vector(
0 if centered[0] else bounding_box.xlen / 2, 0 if centered[0] else bounding_box.xlen / 2,
@ -420,7 +422,7 @@ class Rectangle(Compound):
context: BuildSketch = BuildSketch._get_context() context: BuildSketch = BuildSketch._get_context()
validate_inputs(self, 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() bounding_box = face.BoundingBox()
center_offset = Vector( center_offset = Vector(
0 if centered[0] else bounding_box.xlen / 2, 0 if centered[0] else bounding_box.xlen / 2,
@ -466,7 +468,9 @@ class RegularPolygon(Compound):
) )
for i in range(side_count + 1) 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() bounding_box = face.BoundingBox()
center_offset = Vector( center_offset = Vector(
0 if centered[0] else bounding_box.xlen / 2, 0 if centered[0] else bounding_box.xlen / 2,
@ -509,7 +513,9 @@ class SlotArc(Compound):
if isinstance(arc, Edge): if isinstance(arc, Edge):
raise ValueError("Bug - Edges aren't supported by offset") raise ValueError("Bug - Edges aren't supported by offset")
# arc_wire = arc if isinstance(arc, Wire) else Wire.assembleEdges([arc]) # 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 = [ new_faces = [
face.moved(location) for location in LocationList._get_context().locations 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), Edge.makeLine(center_v, center_v - half_line),
] ]
)[0].offset2D(height / 2)[0] )[0].offset2D(height / 2)[0]
).rotate(*z_axis, rotation) ).rotate((0, 0, 0), (0, 0, 1), rotation)
new_faces = [ new_faces = [
face.moved(location) for location in LocationList._get_context().locations 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)), Edge.makeLine(Vector(), Vector(+center_separation / 2, 0, 0)),
] ]
).offset2D(height / 2)[0] ).offset2D(height / 2)[0]
).rotate(*z_axis, rotation) ).rotate((0, 0, 0), (0, 0, 1), rotation)
new_faces = [ new_faces = [
face.moved(location) for location in LocationList._get_context().locations 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)), Edge.makeLine(Vector(), Vector(+width / 2 - height / 2, 0, 0)),
] ]
).offset2D(height / 2)[0] ).offset2D(height / 2)[0]
).rotate(*z_axis, rotation) ).rotate((0, 0, 0), (0, 0, 1), rotation)
new_faces = [ new_faces = [
face.moved(location) for location in LocationList._get_context().locations face.moved(location) for location in LocationList._get_context().locations
] ]
@ -741,7 +747,9 @@ class Trapezoid(Compound):
) )
) )
pts.append(pts[0]) 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() bounding_box = face.BoundingBox()
center_offset = Vector( center_offset = Vector(
0 if centered[0] else bounding_box.xlen / 2, 0 if centered[0] else bounding_box.xlen / 2,

View file

@ -58,9 +58,10 @@ class TestProperties(unittest.TestCase):
self.assertTupleAlmostEquals((v.x, v.y, v.z), (1, 2, 3), 5) self.assertTupleAlmostEquals((v.x, v.y, v.z), (1, 2, 3), 5)
def test_vector_properties(self): 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) self.assertTupleAlmostEquals((v.X, v.Y, v.Z), (1, 2, 3), 5)
class TestRotation(unittest.TestCase): class TestRotation(unittest.TestCase):
"""Test the Rotation derived class of Location""" """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[0].radius(), 0.5)
self.assertEqual(edges[-1].radius(), 1) self.assertEqual(edges[-1].radius(), 1)
with self.subTest(sort_by=SortBy.X): with self.subTest(sort_by="X"):
with BuildPart() as test: with BuildPart() as test:
Box(1, 1, 1) 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[0].Center().x, -0.5)
self.assertEqual(edges[-1].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: with BuildPart() as test:
Box(1, 1, 1) 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[0].Center().y, -0.5)
self.assertEqual(edges[-1].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: with BuildPart() as test:
Box(1, 1, 1) 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[0].Center().z, -0.5)
self.assertEqual(edges[-1].Center().z, 0.5) self.assertEqual(edges[-1].Center().z, 0.5)

View file

@ -160,7 +160,7 @@ class TestOffset(unittest.TestCase):
Box(10, 10, 10) Box(10, 10, 10)
Offset( Offset(
amount=-1, amount=-1,
openings=test.faces().sort_by()[0], openings=test.faces() >> Axis.Z,
kind=Kind.INTERSECTION, kind=Kind.INTERSECTION,
) )
self.assertAlmostEqual(test.part.Volume(), 10**3 - 8**2 * 9, 5) self.assertAlmostEqual(test.part.Volume(), 10**3 - 8**2 * 9, 5)
@ -173,7 +173,7 @@ class BoundingBoxTests(unittest.TestCase):
Circle(10) Circle(10)
with BuildSketch(mode=Mode.PRIVATE) as bb: with BuildSketch(mode=Mode.PRIVATE) as bb:
BoundingBox(*mickey.faces()) BoundingBox(*mickey.faces())
ears = bb.vertices().sort_by(SortBy.Y)[:-2] ears = (bb.vertices() > Axis.Y)[:-2]
with Locations(*ears): with Locations(*ears):
Circle(7) Circle(7)
self.assertAlmostEqual(mickey.sketch.Area(), 586.1521145312807, 5) self.assertAlmostEqual(mickey.sketch.Area(), 586.1521145312807, 5)

View file

@ -296,7 +296,7 @@ class TestRevolve(unittest.TestCase):
l1 @ 0, l1 @ 0,
) )
BuildFace() 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.assertLess(test.part.Volume(), 22**2 * pi * 50, 5)
self.assertGreater(test.part.Volume(), 144 * 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)) l3 = Line(l2 @ 1, (20, 0))
l4 = Line(l3 @ 1, l1 @ 0) l4 = Line(l3 @ 1, l1 @ 0)
BuildFace() 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.assertLess(test.part.Volume(), 244 * pi * 20, 5)
self.assertGreater(test.part.Volume(), 100 * pi * 20, 5) self.assertGreater(test.part.Volume(), 100 * pi * 20, 5)
@ -318,14 +318,14 @@ class TestRevolve(unittest.TestCase):
with BuildSketch(): with BuildSketch():
Rectangle(1, 1, centered=(False, False)) Rectangle(1, 1, centered=(False, False))
with self.assertRaises(ValueError): 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): def test_invalid_axis_direction(self):
with BuildPart(): with BuildPart():
with BuildSketch(): with BuildSketch():
Rectangle(1, 1, centered=(False, False)) Rectangle(1, 1, centered=(False, False))
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Revolve(axis_origin=(0, 0, 0), axis_direction=(0, 0, 1)) Revolve(axis=Axis.Z)
class TestSection(unittest.TestCase): class TestSection(unittest.TestCase):