build123d/build_part.py

378 lines
13 KiB
Python

from math import pi, sin, cos, radians, sqrt
from typing import Union, Iterable, Sequence, Callable
from enum import Enum, auto
import cadquery as cq
from cadquery.hull import find_hull
from cadquery import (
Edge,
Face,
Wire,
Vector,
Shape,
Location,
Vertex,
Compound,
Solid,
Plane,
)
from cadquery.occ_impl.shapes import VectorLike, Real
import cq_warehouse.extensions
from build123d_common import *
class BuildPart:
@property
def workplane_count(self) -> int:
return len(self.workplanes)
@property
def pending_face_count(self) -> int:
return len(self.pending_faces.values())
@property
def pending_edge_count(self) -> int:
return len(self.pending_edges.values())
@property
def pending_location_count(self) -> int:
return len(self.locations.values())
def __init__(
self,
parent: BuildAssembly = None,
mode: Mode = Mode.ADDITION,
workplane: Plane = Plane.named("XY"),
):
self.parent = parent
self.part: Solid = None
self.workplanes: list[Plane] = [workplane]
self.pending_faces: dict[int : list[Face]] = {0: []}
self.pending_edges: dict[int : list[Edge]] = {0: []}
self.locations: dict[int : list[Location]] = {0: []}
self.last_operation: dict[CqObject : list[Shape]] = {}
def __enter__(self):
return self
def __exit__(self, exception_type, exception_value, traceback):
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):
if replace:
self.workplanes = [workplane]
else:
self.workplanes.append(workplane)
self.locations[len(self.workplanes) - 1] = [Location()]
return workplane
def faces_to_workplanes(self, *faces: Sequence[Face], replace=False):
new_planes = []
for face in faces:
new_plane = Plane(origin=face.Center(), normal=face.normalAt(face.Center()))
new_planes.append(new_plane)
self.workplane(new_plane, replace)
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]:
if sort_by == SortBy.NONE:
edges = self.part.Edges()
elif sort_by == SortBy.X:
edges = sorted(
self.part.Edges(),
key=lambda obj: obj.Center().x,
reverse=reverse,
)
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 faces(self, sort_by: SortBy = SortBy.NONE, reverse: bool = False) -> list[Face]:
if sort_by == SortBy.NONE:
faces = self.part.Faces()
elif sort_by == SortBy.X:
faces = sorted(
self.part.Faces(),
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(
self, sort_by: SortBy = SortBy.NONE, reverse: bool = False
) -> list[Vertex]:
if sort_by == SortBy.NONE:
vertices = self.part.Vertices()
elif sort_by == SortBy.X:
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(
self,
new_solids: list[Solid, Compound],
mode: Mode = Mode.ADDITION,
clean: bool = True,
):
Solid.clean_op = Solid.clean if clean else Solid.null
Compound.clean_op = Compound.clean if clean else Compound.null
before_vertices = set() if self.part is None else set(self.part.Vertices())
before_edges = set() if self.part is None else set(self.part.Edges())
before_faces = set() if self.part is None else set(self.part.Faces())
if mode == Mode.ADDITION:
if self.part is None:
if len(new_solids) == 1:
self.part = new_solids[0]
else:
self.part = new_solids.pop().fuse(*new_solids)
else:
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()
self.last_operation[CqObject.VERTEX] = list(
set(self.part.Vertices()) - before_vertices
)
self.last_operation[CqObject.EDGE] = list(set(self.part.Edges()) - before_edges)
self.last_operation[CqObject.FACE] = list(set(self.part.Faces()) - before_faces)
def extrude(
self,
until: Union[float, Until, Face],
both: bool = False,
taper: float = None,
mode: Mode = Mode.ADDITION,
clean: bool = True,
):
new_solids: list[Solid] = []
for plane_index, faces in self.pending_faces.items():
for face in faces:
new_solids.append(
Solid.extrudeLinear(
face, self.workplanes[plane_index].zDir * until, 0
)
)
if both:
new_solids.append(
Solid.extrudeLinear(
face,
self.workplanes[plane_index].zDir * until * -1.0,
0,
)
)
self.place_solids(new_solids, mode, clean)
return new_solids[0] if len(new_solids) == 1 else new_solids
def revolve(
self,
angle_degrees: float = 360.0,
axis_start: VectorLike = None,
axis_end: VectorLike = None,
mode: Mode = Mode.ADDITION,
clean: bool = True,
):
# 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
angle = angle_degrees % 360.0
angle = 360.0 if angle == 0 else angle
new_solids = []
for i, workplane in enumerate(self.workplanes):
axis = []
if axis_start is None:
axis.append(workplane.fromLocalCoords(Vector(0, 0, 0)))
else:
axis.append(workplane.fromLocalCoords(Vector(axis_start)))
if axis_end is None:
axis.append(workplane.fromLocalCoords(Vector(0, 1, 0)))
else:
axis.append(workplane.fromLocalCoords(Vector(axis_end)))
print(f"Revolve: {axis=}")
for face in self.pending_faces[i]:
new_solids.append(Solid.revolve(face, angle, *axis))
self.place_solids(new_solids, mode, clean)
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):
loft_wires = []
for i in range(len(self.workplanes)):
for face in self.pending_faces[i]:
loft_wires.append(face.outerWire())
new_solid = Solid.makeLoft(loft_wires, ruled)
self.place_solids([new_solid], mode, clean)
return new_solid
def sweep(
self,
path: Union[Edge, Wire],
multisection: bool = False,
make_solid: bool = True,
is_frenet: bool = False,
transition: Transition = Transition.RIGHT,
normal: VectorLike = None,
binormal: Union[Edge, Wire] = None,
mode: Mode = Mode.ADDITION,
clean: bool = True,
):
path_wire = Wire.assembleEdges([path]) if isinstance(path, Edge) else path
if binormal is None:
binormal_mode = Vector(normal)
elif isinstance(binormal, Edge):
binormal_mode = Wire.assembleEdges([binormal])
else:
binormal_mode = binormal
new_solids = []
for i, workplane in enumerate(self.workplanes):
if not multisection:
for face in self.pending_faces[i]:
new_solids.append(
Solid.sweep(
face,
path_wire,
make_solid,
is_frenet,
binormal_mode,
transition,
)
)
else:
sections = [face.outerWire() for face in self.pending_faces[i]]
new_solids.append(
Solid.sweep_multi(
sections, path_wire, make_solid, is_frenet, binormal_mode
)
)
self.place_solids(new_solids, mode, clean)
return new_solids
def fillet(self, *edges: Sequence[Edge], radius: float):
self.part = self.part.fillet(radius, [e for e in edges])
def chamfer(self, *edges: Sequence[Edge], length1: float, length2: float = None):
self.part = self.part.chamfer(length1, length2, list(edges))