Refactored to be class based

This commit is contained in:
Roger Maitland 2022-07-08 11:52:29 -04:00
parent b8c76c7954
commit b2d19f8e13

View file

@ -1,8 +1,7 @@
from math import pi, sin, cos, radians, sqrt from math import pi, sin, cos, radians, sqrt
from typing import Union, Iterable, Sequence, Callable from typing import Union, Iterable, Callable
from enum import Enum, auto from enum import Enum, auto
import cadquery as cq import cadquery as cq
from cadquery.hull import find_hull
from cadquery import ( from cadquery import (
Edge, Edge,
Face, Face,
@ -26,64 +25,46 @@ class BuildPart:
return len(self.workplanes) return len(self.workplanes)
@property @property
def pending_face_count(self) -> int: def pending_faces_count(self) -> int:
return len(self.pending_faces.values()) return len(self.pending_faces.values())
@property @property
def pending_edge_count(self) -> int: def pending_edges_count(self) -> int:
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:
return len(self.locations.values()) return len(self.locations.values())
def __init__( def __init__(
self, self,
parent: BuildAssembly = None,
mode: Mode = Mode.ADDITION, mode: Mode = Mode.ADDITION,
workplane: Plane = Plane.named("XY"), workplane: Plane = Plane.named("XY"),
): ):
self.parent = parent self.part: Compound = None
self.part: Solid = None
self.workplanes: list[Plane] = [workplane] self.workplanes: list[Plane] = [workplane]
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: dict[int : list[Location]] = {0: []} self.locations: dict[int : list[Location]] = {0: []}
self.last_operation: dict[CqObject : list[Shape]] = {} self.last_operation: dict[CqObject : list[Shape]] = {}
self.mode = mode
self.last_vertices = []
self.last_edges = []
self.last_faces = []
self.last_solids = []
def __enter__(self): def __enter__(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 pass
def push_points(self, *pts: Union[VectorLike, Location]):
new_locations = [
Location(Vector(pt)) if not isinstance(pt, Location) else pt for pt in pts
]
for i in range(len(self.workplanes)):
self.locations[i].extend(new_locations)
print(f"{len(self.locations[i])=}")
return new_locations[0] if len(new_locations) == 1 else new_locations
def add(self, obj: Union[Edge, Face], mode: Mode = Mode.ADDITION):
for i, workplane in enumerate(self.workplanes):
# If no locations have been defined, add one to the workplane center
if not self.locations[i]:
self.locations[i].append(Location(Vector()))
for loc in self.locations[i]:
localized_obj = workplane.fromLocalCoords(obj.moved(loc))
if i in self.pending_faces:
if isinstance(obj, Face):
self.pending_faces[i].append(localized_obj)
else:
self.pending_edges[i].append(localized_obj)
else:
if isinstance(obj, Face):
self.pending_faces[i] = [localized_obj]
else:
self.pending_edges[i] = [localized_obj]
def workplane(self, workplane: Plane = Plane.named("XY"), replace=True): def workplane(self, workplane: Plane = Plane.named("XY"), replace=True):
if replace: if replace:
self.workplanes = [workplane] self.workplanes = [workplane]
@ -92,7 +73,7 @@ class BuildPart:
self.locations[len(self.workplanes) - 1] = [Location()] self.locations[len(self.workplanes) - 1] = [Location()]
return workplane return workplane
def faces_to_workplanes(self, *faces: Sequence[Face], replace=False): def faces_to_workplanes(self, *faces: Face, replace=False):
new_planes = [] new_planes = []
for face in faces: for face in faces:
new_plane = Plane(origin=face.Center(), normal=face.normalAt(face.Center())) new_plane = Plane(origin=face.Center(), normal=face.normalAt(face.Center()))
@ -100,192 +81,189 @@ class BuildPart:
self.workplane(new_plane, replace) self.workplane(new_plane, replace)
return new_planes[0] if len(new_planes) == 1 else new_planes return new_planes[0] if len(new_planes) == 1 else new_planes
def edges(self, sort_by: SortBy = SortBy.NONE, reverse: bool = False) -> list[Edge]: def vertices(self, select: Select = Select.ALL) -> list[Vertex]:
if sort_by == SortBy.NONE: vertex_list = []
edges = self.part.Edges() if select == Select.ALL:
elif sort_by == SortBy.X: for e in self.part.Edges():
edges = sorted( vertex_list.extend(e.Vertices())
self.part.Edges(), elif select == Select.LAST:
key=lambda obj: obj.Center().x, vertex_list = self.last_vertices
reverse=reverse, return list(set(vertex_list))
)
elif sort_by == SortBy.Y:
edges = sorted(
self.part.Edges(),
key=lambda obj: obj.Center().y,
reverse=reverse,
)
elif sort_by == SortBy.Z:
edges = sorted(
self.part.Edges(),
key=lambda obj: obj.Center().z,
reverse=reverse,
)
elif sort_by == SortBy.LENGTH:
edges = sorted(
self.part.Edges(),
key=lambda obj: obj.Length(),
reverse=reverse,
)
elif sort_by == SortBy.RADIUS:
edges = sorted(
self.part.Edges(),
key=lambda obj: obj.radius(),
reverse=reverse,
)
elif sort_by == SortBy.DISTANCE:
edges = sorted(
self.part.Edges(),
key=lambda obj: obj.Center().Length,
reverse=reverse,
)
else:
raise ValueError(f"Unable to sort edges by {sort_by}")
return edges def edges(self, select: Select = Select.ALL) -> list[Edge]:
if select == Select.ALL:
edge_list = self.part.Edges()
elif select == Select.LAST:
edge_list = self.last_edges
return edge_list
def faces(self, sort_by: SortBy = SortBy.NONE, reverse: bool = False) -> list[Face]: def faces(self, select: Select = Select.ALL) -> list[Face]:
if sort_by == SortBy.NONE: if select == Select.ALL:
faces = self.part.Faces() face_list = self.part.Faces()
elif sort_by == SortBy.X: elif select == Select.LAST:
faces = sorted( face_list = self.last_edges
self.part.Faces(), return face_list
key=lambda obj: obj.Center().x,
reverse=reverse,
)
elif sort_by == SortBy.Y:
faces = sorted(
self.part.Faces(),
key=lambda obj: obj.Center().y,
reverse=reverse,
)
elif sort_by == SortBy.Z:
faces = sorted(
self.part.Faces(),
key=lambda obj: obj.Center().z,
reverse=reverse,
)
elif sort_by == SortBy.AREA:
faces = sorted(
self.part.Faces(), key=lambda obj: obj.Area(), reverse=reverse
)
elif sort_by == SortBy.DISTANCE:
faces = sorted(
self.part.Faces(),
key=lambda obj: obj.Center().Length,
reverse=reverse,
)
else:
raise ValueError(f"Unable to sort edges by {sort_by}")
return faces
def vertices( def solids(self, select: Select = Select.ALL) -> list[Solid]:
self, sort_by: SortBy = SortBy.NONE, reverse: bool = False if select == Select.ALL:
) -> list[Vertex]: solid_list = self.part.Solids()
if sort_by == SortBy.NONE: elif select == Select.LAST:
vertices = self.part.Vertices() solid_list = self.last_solids
elif sort_by == SortBy.X: return solid_list
vertices = sorted(
self.part.Vertices(),
key=lambda obj: obj.Center().x,
reverse=reverse,
)
elif sort_by == SortBy.Y:
vertices = sorted(
self.part.Vertices(),
key=lambda obj: obj.Center().y,
reverse=reverse,
)
elif sort_by == SortBy.Z:
vertices = sorted(
self.part.Vertices(),
key=lambda obj: obj.Center().z,
reverse=reverse,
)
elif sort_by == SortBy.DISTANCE:
vertices = sorted(
self.part.Vertices(),
key=lambda obj: obj.Center().Length,
reverse=reverse,
)
else:
raise ValueError(f"Unable to sort edges by {sort_by}")
return vertices
def place_solids( @staticmethod
def get_context() -> "BuildPart":
return context_stack[-1]
def add_to_pending(self, *objects: Union[Edge, Face]):
for obj in objects:
for i, workplane in enumerate(self.workplanes):
# If no locations have been defined, add one to the workplane center
if not self.locations[i]:
self.locations[i].append(Location(Vector()))
for loc in self.locations[i]:
localized_obj = workplane.fromLocalCoords(obj.moved(loc))
if i in self.pending_faces:
if isinstance(obj, Face):
self.pending_faces[i].append(localized_obj)
else:
self.pending_edges[i].append(localized_obj)
else:
if isinstance(obj, Face):
self.pending_faces[i] = [localized_obj]
else:
self.pending_edges[i] = [localized_obj]
def add_to_context(
self, self,
new_solids: list[Solid, Compound], *objects: Union[Edge, Wire, Face, Solid, Compound],
mode: Mode = Mode.ADDITION, mode: Mode = Mode.ADDITION,
clean: bool = True,
): ):
if context_stack and mode != Mode.PRIVATE:
# Sort the provided objects into edges, faces and solids
new_faces = [obj for obj in objects if isinstance(obj, Face)]
new_solids = [obj for obj in objects if isinstance(obj, Solid)]
for compound in filter(lambda o: isinstance(o, Compound), objects):
new_faces.extend(compound.Faces())
new_solids.extend(compound.Solids())
new_edges = [obj for obj in objects if isinstance(obj, Edge)]
for compound in filter(lambda o: isinstance(o, Wire), objects):
new_edges.extend(compound.Edges())
Solid.clean_op = Solid.clean if clean else Solid.null pre_vertices = set() if self.part is None else set(self.part.Vertices())
Compound.clean_op = Compound.clean if clean else Compound.null pre_edges = set() if self.part is None else set(self.part.Edges())
pre_faces = set() if self.part is None else set(self.part.Faces())
pre_solids = set() if self.part is None else set(self.part.Solids())
before_vertices = set() if self.part is None else set(self.part.Vertices()) if mode == Mode.ADDITION:
before_edges = set() if self.part is None else set(self.part.Edges()) if self.part is None:
before_faces = set() if self.part is None else set(self.part.Faces()) if len(new_solids) == 1:
self.part = new_solids[0]
if mode == Mode.ADDITION: else:
if self.part is None: self.part = new_solids.pop().fuse(*new_solids)
if len(new_solids) == 1:
self.part = new_solids[0]
else: else:
self.part = new_solids.pop().fuse(*new_solids) self.part = self.part.fuse(*new_solids).clean_op()
elif mode == Mode.SUBTRACTION:
if self.part is None:
raise ValueError("Nothing to subtract from")
self.part = self.part.cut(*new_solids).clean_op()
elif mode == Mode.INTERSECTION:
if self.part is None:
raise ValueError("Nothing to intersect with")
self.part = self.part.intersect(*new_solids).clean_op()
elif mode == Mode.CONSTRUCTION:
pass
else: else:
self.part = self.part.fuse(*new_solids).clean_op() raise ValueError(f"Invalid mode: {mode}")
elif mode == Mode.SUBTRACTION:
if self.part is None:
raise ValueError("Nothing to subtract from")
self.part = self.part.cut(*new_solids).clean_op()
elif mode == Mode.INTERSECTION:
if self.part is None:
raise ValueError("Nothing to intersect with")
self.part = self.part.intersect(*new_solids).clean_op()
self.last_operation[CqObject.VERTEX] = list( post_vertices = set(self.part.Vertices())
set(self.part.Vertices()) - before_vertices post_edges = set(self.part.Edges())
) post_faces = set(self.part.Faces())
self.last_operation[CqObject.EDGE] = list(set(self.part.Edges()) - before_edges) post_solids = set(self.part.Solids())
self.last_operation[CqObject.FACE] = list(set(self.part.Faces()) - before_faces) self.last_vertices = list(post_vertices - pre_vertices)
self.last_edges = list(post_edges - pre_edges)
self.last_faces = list(post_faces - pre_faces)
self.last_solids = list(post_solids - pre_solids)
def extrude( self.add_to_pending(*new_edges)
self.add_to_pending(*new_faces)
class AddPart(Compound):
def __init__(
self,
*objects: Union[Edge, Wire, Face, Solid, Compound],
mode: Mode = Mode.ADDITION,
):
new_faces = [obj for obj in objects if isinstance(obj, Face)]
new_solids = [obj for obj in objects if isinstance(obj, Solid)]
for compound in filter(lambda o: isinstance(o, Compound), objects):
new_faces.extend(compound.Faces())
new_solids.extend(compound.Solids())
new_edges = [obj for obj in objects if isinstance(obj, Edge)]
for compound in filter(lambda o: isinstance(o, Wire), objects):
new_edges.extend(compound.Edges())
# Add to pending faces and edges
BuildPart.get_context().add_to_pending(new_faces)
BuildPart.get_context().add_to_pending(new_edges)
# Locate the solids to the predefined positions
locations = [
location for location in BuildPart.get_context().locations.values()
]
# If no locations have been specified, use the origin
if not locations:
locations = [Location(Vector())]
located_solids = [
solid.moved(location) for solid in new_solids for location in locations
]
BuildPart.get_context().add_to_context(*located_solids, mode=mode)
super().__init__(Compound.makeCompound(located_solids).wrapped)
class Extrude(Compound):
def __init__(
self, self,
until: Union[float, Until, Face], until: Union[float, Until, Face],
both: bool = False, both: bool = False,
taper: float = None, taper: float = None,
mode: Mode = Mode.ADDITION, mode: Mode = Mode.ADDITION,
clean: bool = True,
): ):
new_solids: list[Solid] = [] new_solids: list[Solid] = []
for plane_index, faces in self.pending_faces.items(): for plane_index, faces in BuildPart.get_context().pending_faces.items():
for face in faces: for face in faces:
new_solids.append( new_solids.append(
Solid.extrudeLinear( Solid.extrudeLinear(
face, self.workplanes[plane_index].zDir * until, 0 face,
BuildPart.get_context().workplanes[plane_index].zDir * until,
0,
) )
) )
if both: if both:
new_solids.append( new_solids.append(
Solid.extrudeLinear( Solid.extrudeLinear(
face, face,
self.workplanes[plane_index].zDir * until * -1.0, BuildPart.get_context().workplanes[plane_index].zDir
* until
* -1.0,
0, 0,
) )
) )
self.place_solids(new_solids, mode, clean) BuildPart.get_context().add_to_context(*new_solids, mode=mode)
super().__init__(Compound.makeCompound(new_solids).wrapped)
return new_solids[0] if len(new_solids) == 1 else new_solids
def revolve( class Revolve(Compound):
def __init__(
self, self,
angle_degrees: float = 360.0, angle_degrees: float = 360.0,
axis_start: VectorLike = None, axis_start: VectorLike = None,
axis_end: VectorLike = None, axis_end: VectorLike = None,
mode: Mode = Mode.ADDITION, mode: Mode = Mode.ADDITION,
clean: bool = True,
): ):
# Make sure we account for users specifying angles larger than 360 degrees, and # Make sure we account for users specifying angles larger than 360 degrees, and
# for OCCT not assuming that a 0 degree revolve means a 360 degree revolve # for OCCT not assuming that a 0 degree revolve means a 360 degree revolve
@ -293,7 +271,7 @@ class BuildPart:
angle = 360.0 if angle == 0 else angle angle = 360.0 if angle == 0 else angle
new_solids = [] new_solids = []
for i, workplane in enumerate(self.workplanes): for i, workplane in enumerate(BuildPart.get_context().workplanes):
axis = [] axis = []
if axis_start is None: if axis_start is None:
axis.append(workplane.fromLocalCoords(Vector(0, 0, 0))) axis.append(workplane.fromLocalCoords(Vector(0, 0, 0)))
@ -306,25 +284,28 @@ class BuildPart:
axis.append(workplane.fromLocalCoords(Vector(axis_end))) axis.append(workplane.fromLocalCoords(Vector(axis_end)))
print(f"Revolve: {axis=}") print(f"Revolve: {axis=}")
for face in self.pending_faces[i]: for face in BuildPart.get_context().pending_faces[i]:
new_solids.append(Solid.revolve(face, angle, *axis)) new_solids.append(Solid.revolve(face, angle, *axis))
self.place_solids(new_solids, mode, clean) BuildPart.get_context().add_to_context(*new_solids, mode=mode)
super().__init__(Compound.makeCompound(new_solids).wrapped)
return new_solids[0] if len(new_solids) == 1 else new_solids
def loft(self, ruled: bool = False, mode: Mode = Mode.ADDITION, clean: bool = True): class Loft(Solid):
def __init__(self, ruled: bool = False, mode: Mode = Mode.ADDITION):
loft_wires = [] loft_wires = []
for i in range(len(self.workplanes)): for i in range(len(BuildPart.get_context().workplanes)):
for face in self.pending_faces[i]: for face in BuildPart.get_context().pending_faces[i]:
loft_wires.append(face.outerWire()) loft_wires.append(face.outerWire())
new_solid = Solid.makeLoft(loft_wires, ruled) new_solid = Solid.makeLoft(loft_wires, ruled)
self.place_solids([new_solid], mode, clean)
return new_solid BuildPart.get_context().add_to_context(new_solid, mode=mode)
super().__init__(new_solid.wrapped)
def sweep(
class Sweep(Compound):
def __init__(
self, self,
path: Union[Edge, Wire], path: Union[Edge, Wire],
multisection: bool = False, multisection: bool = False,
@ -334,7 +315,6 @@ class BuildPart:
normal: VectorLike = None, normal: VectorLike = None,
binormal: Union[Edge, Wire] = None, binormal: Union[Edge, Wire] = None,
mode: Mode = Mode.ADDITION, mode: Mode = Mode.ADDITION,
clean: bool = True,
): ):
path_wire = Wire.assembleEdges([path]) if isinstance(path, Edge) else path path_wire = Wire.assembleEdges([path]) if isinstance(path, Edge) else path
@ -346,9 +326,9 @@ class BuildPart:
binormal_mode = binormal binormal_mode = binormal
new_solids = [] new_solids = []
for i, workplane in enumerate(self.workplanes): for i, workplane in enumerate(BuildPart.get_context().workplanes):
if not multisection: if not multisection:
for face in self.pending_faces[i]: for face in BuildPart.get_context().pending_faces[i]:
new_solids.append( new_solids.append(
Solid.sweep( Solid.sweep(
face, face,
@ -360,19 +340,39 @@ class BuildPart:
) )
) )
else: else:
sections = [face.outerWire() for face in self.pending_faces[i]] sections = [
face.outerWire()
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
) )
) )
self.place_solids(new_solids, mode, clean) BuildPart.get_context().add_to_context(*new_solids, mode=mode)
super().__init__(Compound.makeCompound(new_solids).wrapped)
return new_solids
def fillet(self, *edges: Sequence[Edge], radius: float): class FilletPart(Compound):
self.part = self.part.fillet(radius, [e for e in edges]) def __init__(self, *edges: Edge, radius: float):
new_part = BuildPart.get_context().part.fillet(radius, list(edges))
BuildPart.get_context().part = new_part
super().__init__(new_part.wrapped)
def chamfer(self, *edges: Sequence[Edge], length1: float, length2: float = None):
self.part = self.part.chamfer(length1, length2, list(edges)) class ChamferPart(Compound):
def __init__(self, *edges: Edge, length1: float, length2: float = None):
new_part = BuildPart.get_context().part.chamfer(length1, length2, list(edges))
BuildPart.get_context().part = new_part
super().__init__(new_part.wrapped)
class PushPointsPart:
def __init__(self, *pts: Union[VectorLike, Location]):
new_locations = [
Location(Vector(pt)) if not isinstance(pt, Location) else pt for pt in pts
]
for i in range(len(BuildPart.get_context().workplanes)):
BuildPart.get_context().locations[i].extend(new_locations)
print(f"{len(BuildPart.get_context().locations[i])=}")