Completed docstrings

This commit is contained in:
Roger Maitland 2022-07-11 20:50:36 -04:00
parent da093c25d6
commit 9b6831ee95
2 changed files with 163 additions and 174 deletions

View file

@ -14,55 +14,28 @@ f1.wrapped.TShape() == f2.wrapped.TShape() <=== TRUE
Thanks. Playing around a bit more, it seems like translate() makes the underlying TShapes unequal, but Shape.moved() preserves TShape. This returns true, which could be useful: x1 = cq.Workplane().box(3,4,5) x2 = cq.Workplane(x1.findSolid().moved(cq.Location(cq.Vector(1,2,3),cq.Vector(4,5,6),7))) f1 = x1.faces(">Z").val() f2 = x2.faces(">Z").val() f1.wrapped.TShape() == f2.wrapped.TShape() <=== TRUE Thanks. Playing around a bit more, it seems like translate() makes the underlying TShapes unequal, but Shape.moved() preserves TShape. This returns true, which could be useful: x1 = cq.Workplane().box(3,4,5) x2 = cq.Workplane(x1.findSolid().moved(cq.Location(cq.Vector(1,2,3),cq.Vector(4,5,6),7))) f1 = x1.faces(">Z").val() f2 = x2.faces(">Z").val() f1.wrapped.TShape() == f2.wrapped.TShape() <=== TRUE
""" """
import logging from math import radians
from functools import partial from typing import Union
from math import pi, sin, cos, radians, sqrt
from pstats import SortKey
from typing import Union, Iterable, Sequence, Callable
import builtins
from enum import Enum, auto from enum import Enum, auto
import cadquery as cq
from cadquery.hull import find_hull
from cadquery import ( from cadquery import (
Edge, Edge,
Face,
Wire, Wire,
Vector, Vector,
Shape, Shape,
Location, Location,
Vertex,
Compound, Compound,
Solid, Solid,
Plane,
) )
from cadquery.occ_impl.shapes import VectorLike, Real from cadquery.occ_impl.shapes import VectorLike, Real
from OCP.gp import gp_Vec, gp_Pnt, gp_Ax1, gp_Dir, gp_Trsf from OCP.gp import gp_Vec, gp_Pnt, gp_Ax1, gp_Dir, gp_Trsf
import cq_warehouse.extensions import cq_warehouse.extensions
class Rotation(Location):
def __init__(self, about_x: float = 0, about_y: float = 0, about_z: float = 0):
self.about_x = about_x
self.about_y = about_y
self.about_z = about_z
# Compute rotation matrix.
rx = gp_Trsf()
rx.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), radians(about_x))
ry = gp_Trsf()
ry.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 1, 0)), radians(about_y))
rz = gp_Trsf()
rz.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), radians(about_z))
super().__init__(Location(rx * ry * rz).wrapped)
RotationLike = Union[tuple[float, float, float], Rotation]
z_axis = (Vector(0, 0, 0), Vector(0, 0, 1)) z_axis = (Vector(0, 0, 0), Vector(0, 0, 1))
_context_stack = [] #
# Operators
#
def __matmul__custom(e: Union[Edge, Wire], p: float): def __matmul__custom(e: Union[Edge, Wire], p: float):
return e.positionAt(p) return e.positionAt(p)
@ -75,81 +48,12 @@ Edge.__matmul__ = __matmul__custom
Edge.__mod__ = __mod__custom Edge.__mod__ = __mod__custom
Wire.__matmul__ = __matmul__custom Wire.__matmul__ = __matmul__custom
Wire.__mod__ = __mod__custom Wire.__mod__ = __mod__custom
line = Edge.makeLine(Vector(0, 0, 0), Vector(10, 0, 0))
# print(f"position of line at 1/2: {line @ 0.5=}")
# print(f"tangent of line at 1/2: {line % 0.5=}")
context_stack = [] context_stack = []
#
def by_x(obj: Shape) -> float: # ENUMs
return obj.Center().x #
def _by_x_shape(self) -> float:
return self.Center().x
Shape.by_x = _by_x_shape
def by_y(obj: Shape) -> float:
return obj.Center().y
def _by_y_shape(self) -> float:
return self.Center().y
Shape.by_y = _by_y_shape
def by_z(obj: Shape) -> float:
return obj.Center().z
def _by_z_shape(self) -> float:
return self.Center().z
Shape.by_z = _by_z_shape
def by_length(obj: Union[Edge, Wire]) -> float:
return obj.Length()
def _by_length_edge_or_wire(self) -> float:
return self.Length()
Edge.by_length = _by_length_edge_or_wire
Wire.by_length = _by_length_edge_or_wire
def by_radius(obj: Union[Edge, Wire]) -> float:
return obj.radius()
def _by_radius_edge_or_wire(self) -> float:
return self.radius()
Edge.by_radius = _by_radius_edge_or_wire
Wire.by_radius = _by_radius_edge_or_wire
def by_area(obj: cq.Shape) -> float:
return obj.Area()
def _by_area_shape(self) -> float:
return self.Area()
Shape.by_area = _by_area_shape
class Select(Enum): class Select(Enum):
ALL = auto() ALL = auto()
LAST = auto() LAST = auto()
@ -232,24 +136,6 @@ class BuildAssembly:
pass pass
def _null(self):
return self
Solid.null = _null
Compound.null = _null
def pts_to_locations(*pts: Union[Vector, Location]) -> list[Location]:
if pts:
locations = [
pt if isinstance(pt, Location) else Location(Vector(pt)) for pt in pts
]
else:
locations = [Location(Vector())]
return locations
class SortBy(Enum): class SortBy(Enum):
X = auto() X = auto()
Y = auto() Y = auto()
@ -261,6 +147,28 @@ class SortBy(Enum):
DISTANCE = auto() DISTANCE = auto()
#
# DirectAPI Classes
#
class Rotation(Location):
def __init__(self, about_x: float = 0, about_y: float = 0, about_z: float = 0):
self.about_x = about_x
self.about_y = about_y
self.about_z = about_z
# Compute rotation matrix.
rx = gp_Trsf()
rx.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), radians(about_x))
ry = gp_Trsf()
ry.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 1, 0)), radians(about_y))
rz = gp_Trsf()
rz.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), radians(about_z))
super().__init__(Location(rx * ry * rz).wrapped)
RotationLike = Union[tuple[float, float, float], Rotation]
class ShapeList(list): class ShapeList(list):
axis_map = { axis_map = {
Axis.X: ((1, 0, 0), (-1, 0, 0)), Axis.X: ((1, 0, 0), (-1, 0, 0)),

View file

@ -6,13 +6,11 @@ TODO:
""" """
from math import radians, tan from math import radians, tan
from typing import Union from typing import Union
import cadquery as cq
from cadquery import ( from cadquery import (
Edge, Edge,
Face, Face,
Wire, Wire,
Vector, Vector,
Shape,
Location, Location,
Vertex, Vertex,
Compound, Compound,
@ -23,34 +21,43 @@ from cadquery.occ_impl.shapes import VectorLike
import cq_warehouse.extensions import cq_warehouse.extensions
from build123d_common import * from build123d_common import *
logging.basicConfig( # logging.basicConfig(
filename="build123D.log", # filename="build123D.log",
encoding="utf-8", # encoding="utf-8",
level=logging.DEBUG, # level=logging.DEBUG,
# level=logging.CRITICAL, # # level=logging.CRITICAL,
format="%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)s - %(funcName)20s() ] - %(message)s", # format="%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)s - %(funcName)20s() ] - %(message)s",
) # )
class BuildPart: class BuildPart:
"""BuildPart
Create 3D parts (objects with the property of volume) from sketches or 3D objects.
Args:
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
workplane (Plane, optional): initial plane to work on. Defaults to Plane.named("XY").
"""
@property @property
def workplane_count(self) -> int: def workplane_count(self) -> int:
"""Number of active workplanes"""
return len(self.workplanes) return len(self.workplanes)
@property @property
def pending_faces_count(self) -> int: def pending_faces_count(self) -> int:
"""Number of pending faces"""
return len(self.pending_faces.values()) return len(self.pending_faces.values())
@property @property
def pending_edges_count(self) -> int: def pending_edges_count(self) -> int:
"""Number of pending edges"""
return len(self.pending_edges.values()) return len(self.pending_edges.values())
@property
def pending_solids_count(self) -> int:
return len(self.pending_solids.values())
@property @property
def pending_location_count(self) -> int: def pending_location_count(self) -> int:
"""Number of current locations"""
return len(self.locations) return len(self.locations)
def __init__( def __init__(
@ -60,11 +67,9 @@ class BuildPart:
): ):
self.part: Compound = None self.part: Compound = None
self.workplanes: list[Plane] = [workplane] self.workplanes: list[Plane] = [workplane]
self.locations: list[Location] = [Location(workplane.origin)]
self.pending_faces: dict[int : list[Face]] = {0: []} self.pending_faces: dict[int : list[Face]] = {0: []}
self.pending_edges: dict[int : list[Edge]] = {0: []} self.pending_edges: dict[int : list[Edge]] = {0: []}
self.pending_solids: dict[int : list[Solid]] = {0: []}
self.locations: list[Location] = [Location(workplane.origin)]
self.last_operation: dict[CqObject : list[Shape]] = {}
self.mode = mode self.mode = mode
self.last_vertices = [] self.last_vertices = []
self.last_edges = [] self.last_edges = []
@ -72,30 +77,57 @@ class BuildPart:
self.last_solids = [] self.last_solids = []
def __enter__(self): def __enter__(self):
"""Upon entering BuildPart, add current BuildPart instance to context stack"""
context_stack.append(self) context_stack.append(self)
return self return self
def __exit__(self, exception_type, exception_value, traceback): def __exit__(self, exception_type, exception_value, traceback):
pass """Upon exiting BuildPart - do nothing"""
def workplane(self, *workplanes: Plane, replace=True): def workplane(self, *workplanes: Plane, replace=True):
"""Create Workplane(s)
Add a sequence of planes as workplanes possibly replacing existing workplanes.
Args:
workplanes (Plane): a sequence of planes to add as workplanes
replace (bool, optional): replace existing workplanes. Defaults to True.
"""
if replace: if replace:
self.workplanes = [] self.workplanes = []
# self.locations: list[Location] = {0: []}
for plane in workplanes: for plane in workplanes:
self.workplanes.append(plane) self.workplanes.append(plane)
# self.locations[len(self.workplanes) - 1] = [Location()]
def vertices(self, select: Select = Select.ALL) -> VertexList[Vertex]: def vertices(self, select: Select = Select.ALL) -> VertexList[Vertex]:
"""Return Vertices from Part
Return either all or the vertices created during the last operation.
Args:
select (Select, optional): Vertex selector. Defaults to Select.ALL.
Returns:
VertexList[Vertex]: Vertices extracted
"""
vertex_list = [] vertex_list = []
if select == Select.ALL: if select == Select.ALL:
for e in self.part.Edges(): for edge in self.part.Edges():
vertex_list.extend(e.Vertices()) vertex_list.extend(edge.Vertices())
elif select == Select.LAST: elif select == Select.LAST:
vertex_list = self.last_vertices vertex_list = self.last_vertices
return VertexList(set(vertex_list)) return VertexList(set(vertex_list))
def edges(self, select: Select = Select.ALL) -> ShapeList[Edge]: def edges(self, select: Select = Select.ALL) -> ShapeList[Edge]:
"""Return Edges from Part
Return either all or the edges created during the last operation.
Args:
select (Select, optional): Edge selector. Defaults to Select.ALL.
Returns:
ShapeList[Edge]: Edges extracted
"""
if select == Select.ALL: if select == Select.ALL:
edge_list = self.part.Edges() edge_list = self.part.Edges()
elif select == Select.LAST: elif select == Select.LAST:
@ -103,6 +135,16 @@ class BuildPart:
return ShapeList(edge_list) return ShapeList(edge_list)
def faces(self, select: Select = Select.ALL) -> ShapeList[Face]: def faces(self, select: Select = Select.ALL) -> ShapeList[Face]:
"""Return Faces from Part
Return either all or the faces created during the last operation.
Args:
select (Select, optional): Face selector. Defaults to Select.ALL.
Returns:
ShapeList[Face]: Faces extracted
"""
if select == Select.ALL: if select == Select.ALL:
face_list = self.part.Faces() face_list = self.part.Faces()
elif select == Select.LAST: elif select == Select.LAST:
@ -110,6 +152,16 @@ class BuildPart:
return ShapeList(face_list) return ShapeList(face_list)
def solids(self, select: Select = Select.ALL) -> ShapeList[Solid]: def solids(self, select: Select = Select.ALL) -> ShapeList[Solid]:
"""Return Solids from Part
Return either all or the solids created during the last operation.
Args:
select (Select, optional): Solid selector. Defaults to Select.ALL.
Returns:
ShapeList[Solid]: Solids extracted
"""
if select == Select.ALL: if select == Select.ALL:
solid_list = self.part.Solids() solid_list = self.part.Solids()
elif select == Select.LAST: elif select == Select.LAST:
@ -118,9 +170,12 @@ class BuildPart:
@staticmethod @staticmethod
def get_context() -> "BuildPart": def get_context() -> "BuildPart":
"""Return the current BuildPart instance. Used by Object and Operation
classes to refer to the current context."""
return context_stack[-1] return context_stack[-1]
def add_to_pending(self, *objects: Union[Edge, Face]): def add_to_pending(self, *objects: Union[Edge, Face]):
"""Add objects to BuildPart pending lists"""
for obj in objects: for obj in objects:
for i, workplane in enumerate(self.workplanes): for i, workplane in enumerate(self.workplanes):
for loc in self.locations: for loc in self.locations:
@ -141,6 +196,25 @@ class BuildPart:
*objects: Union[Edge, Wire, Face, Solid, Compound], *objects: Union[Edge, Wire, Face, Solid, Compound],
mode: Mode = Mode.ADDITION, mode: Mode = Mode.ADDITION,
): ):
"""Add objects to BuildPart instance
Core method to interface with BuildPart instance. Input sequence of objects is
parsed into lists of edges, faces, and solids. Edges and faces are added to pending
lists. Solids are combined with current part.
Each operation generates a list of vertices, edges, faces, and solids that have
changed during this operation. These lists are only guaranteed to be valid up until
the next operation as subsequent operations can element these objects.
Args:
objects (Union[Edge, Wire, Face, Solid, Compound]): sequence of objects to add
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
Raises:
ValueError: Nothing to subtract from
ValueError: Nothing to intersect with
ValueError: Invalid mode
"""
if context_stack and mode != Mode.PRIVATE: if context_stack and mode != Mode.PRIVATE:
# Sort the provided objects into edges, faces and solids # Sort the provided objects into edges, faces and solids
new_faces = [obj for obj in objects if isinstance(obj, Face)] new_faces = [obj for obj in objects if isinstance(obj, Face)]
@ -192,6 +266,7 @@ class BuildPart:
self.add_to_pending(*new_faces) self.add_to_pending(*new_faces)
def get_and_clear_locations(self) -> list: def get_and_clear_locations(self) -> list:
"""Return position and planes from current points and workplanes and clear locations."""
position_planes = [] position_planes = []
for workplane in self.workplanes: for workplane in self.workplanes:
position_planes.extend( position_planes.extend(
@ -200,14 +275,13 @@ class BuildPart:
for location in self.locations for location in self.locations
] ]
) )
# Clear used locations
self.locations = [Location(Vector())] self.locations = [Location(Vector())]
return position_planes return position_planes
""" #
Operations # Operations
""" #
class ChamferPart(Compound): class ChamferPart(Compound):
@ -426,15 +500,16 @@ class Loft(Solid):
class PushPointsToPart: class PushPointsToPart:
"""Part Operation: Push Points
Push the sequence of tuples, Vectors or Locations to builder internal structure,
replacing existing locations.
Args:
pts (Union[VectorLike, Location]): sequence of points
"""
def __init__(self, *pts: Union[VectorLike, Location]): def __init__(self, *pts: Union[VectorLike, Location]):
"""Part Operation: Push Points
Push the sequence of tuples, Vectors or Locations to builder internal structure,
replacing existing locations.
Args:
pts (Union[VectorLike, Location]): sequence of points
"""
new_locations = [ new_locations = [
pt if isinstance(pt, Location) else Location(Vector(pt)) for pt in pts pt if isinstance(pt, Location) else Location(Vector(pt)) for pt in pts
] ]
@ -546,7 +621,8 @@ class Sweep(Compound):
multisection (bool, optional): sweep multiple on path. Defaults to False. multisection (bool, optional): sweep multiple on path. Defaults to False.
make_solid (bool, optional): create solid instead of face. Defaults to True. make_solid (bool, optional): create solid instead of face. Defaults to True.
is_frenet (bool, optional): use freenet algorithm. Defaults to False. is_frenet (bool, optional): use freenet algorithm. Defaults to False.
transition (Transition, optional): discontinuity handling option. Defaults to Transition.RIGHT. transition (Transition, optional): discontinuity handling option.
Defaults to Transition.RIGHT.
normal (VectorLike, optional): fixed normal. Defaults to None. normal (VectorLike, optional): fixed normal. Defaults to None.
binormal (Union[Edge, Wire], optional): guide rotation along path. Defaults to None. binormal (Union[Edge, Wire], optional): guide rotation along path. Defaults to None.
mode (Mode, optional): combination. Defaults to Mode.ADDITION. mode (Mode, optional): combination. Defaults to Mode.ADDITION.
@ -572,7 +648,7 @@ class Sweep(Compound):
binormal_mode = binormal binormal_mode = binormal
new_solids = [] new_solids = []
for i, workplane in enumerate(BuildPart.get_context().workplanes): for i in range(BuildPart.get_context().workplane_count):
if not multisection: if not multisection:
for face in BuildPart.get_context().pending_faces[i]: for face in BuildPart.get_context().pending_faces[i]:
new_solids.append( new_solids.append(
@ -585,16 +661,16 @@ class Sweep(Compound):
transition, transition,
) )
) )
else: else:
sections = [ sections = [
face.outerWire() face.outerWire()
for face in BuildPart.get_context().pending_faces[i] for face in BuildPart.get_context().pending_faces[i]
] ]
new_solids.append( new_solids.append(
Solid.sweep_multi( Solid.sweep_multi(
sections, path_wire, make_solid, is_frenet, binormal_mode sections, path_wire, make_solid, is_frenet, binormal_mode
)
) )
)
BuildPart.get_context().pending_faces = {0: []} BuildPart.get_context().pending_faces = {0: []}
BuildPart.get_context().add_to_context(*new_solids, mode=mode) BuildPart.get_context().add_to_context(*new_solids, mode=mode)
@ -620,9 +696,9 @@ class WorkplanesFromFaces:
BuildPart.get_context().workplane(*new_planes, replace=replace) BuildPart.get_context().workplane(*new_planes, replace=replace)
""" #
Objects # Objects
""" #
class AddToPart(Compound): class AddToPart(Compound):
@ -681,7 +757,8 @@ class Box(Compound):
width (float): box size width (float): box size
height (float): box size height (float): box size
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0). rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
centered (tuple[bool, bool, bool], optional): center about axes. Defaults to (True, True, True). centered (tuple[bool, bool, bool], optional): center about axes.
Defaults to (True, True, True).
mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. mode (Mode, optional): combine mode. Defaults to Mode.ADDITION.
""" """
@ -722,7 +799,8 @@ class Cone(Compound):
height (float): cone size height (float): cone size
arc_size (float, optional): angular size of cone. Defaults to 360. arc_size (float, optional): angular size of cone. Defaults to 360.
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0). rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
centered (tuple[bool, bool, bool], optional): center about axes. Defaults to (True, True, True). centered (tuple[bool, bool, bool], optional): center about axes.
Defaults to (True, True, True).
mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. mode (Mode, optional): combine mode. Defaults to Mode.ADDITION.
""" """
@ -768,7 +846,8 @@ class Cylinder(Compound):
height (float): cylinder size height (float): cylinder size
arc_size (float, optional): angular size of cone. Defaults to 360. arc_size (float, optional): angular size of cone. Defaults to 360.
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0). rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
centered (tuple[bool, bool, bool], optional): center about axes. Defaults to (True, True, True). centered (tuple[bool, bool, bool], optional): center about axes.
Defaults to (True, True, True).
mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. mode (Mode, optional): combine mode. Defaults to Mode.ADDITION.
""" """
@ -809,7 +888,8 @@ class Sphere(Compound):
arc_size2 (float, optional): angular size of sphere. Defaults to 90. arc_size2 (float, optional): angular size of sphere. Defaults to 90.
arc_size3 (float, optional): angular size of sphere. Defaults to 360. arc_size3 (float, optional): angular size of sphere. Defaults to 360.
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0). rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
centered (tuple[bool, bool, bool], optional): center about axes. Defaults to (True, True, True). centered (tuple[bool, bool, bool], optional): center about axes.
Defaults to (True, True, True).
mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. mode (Mode, optional): combine mode. Defaults to Mode.ADDITION.
""" """
@ -852,7 +932,8 @@ class Torus(Compound):
major_arc_size (float, optional): angular size or torus. Defaults to 0. major_arc_size (float, optional): angular size or torus. Defaults to 0.
minor_arc_size (float, optional): angular size or torus. Defaults to 360. minor_arc_size (float, optional): angular size or torus. Defaults to 360.
rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0). rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0).
centered (tuple[bool, bool, bool], optional): center about axes. Defaults to (True, True, True). centered (tuple[bool, bool, bool], optional): center about axes.
Defaults to (True, True, True).
mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. mode (Mode, optional): combine mode. Defaults to Mode.ADDITION.
""" """