mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Introduced Axis Class and Sort/Filter Operators
This commit is contained in:
parent
b37945f5ab
commit
b50d6d5fe1
11 changed files with 326 additions and 163 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue