commit ccd48612c7f6a5c06c9193a548c6076e2779b177 Author: Roger Maitland Date: Fri Jul 1 11:27:14 2022 -0400 Initial prototype diff --git a/cq3.py b/cq3.py new file mode 100644 index 0000000..3e99f9d --- /dev/null +++ b/cq3.py @@ -0,0 +1,741 @@ +import builtins +from math import pi, sin +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 + +z_axis = (Vector(0, 0, 0), Vector(0, 0, 1)) + + +def __matmul__custom(e: Union[Edge, Wire], p: float): + return e.positionAt(p) + + +def __mod__custom(e: Union[Edge, Wire], p: float): + return e.tangentAt(p) + + +Edge.__matmul__ = __matmul__custom +Edge.__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=}") + + +def by_x(obj: Shape) -> float: + 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 SortBy(Enum): + NONE = auto() + X = auto() + Y = auto() + Z = auto() + LENGTH = auto() + RADIUS = auto() + AREA = auto() + VOLUME = auto() + DISTANCE = auto() + + +class Mode(Enum): + """Combination Mode""" + + ADDITION = auto() + SUBTRACTION = auto() + INTERSECTION = auto() + CONSTRUCTION = auto() + + +class BuildAssembly: + def add(self): + pass + + +def _null(self): + return self + + +Solid.null = _null +Compound.null = _null + + +class Until(Enum): + NEXT = auto() + LAST = auto() + + +class CqObject(Enum): + EDGE = auto() + FACE = auto() + VERTEX = auto() + + +class Build3D: + @property + def workplane_count(self) -> int: + return len(self.workplanes) + + @property + def pending_face_count(self) -> int: + return len(self.pending_faces) + + def __init__( + self, + parent: BuildAssembly = None, + mode: Mode = Mode.ADDITION, + workplane: Plane = Plane.named("XY"), + ): + self.parent = parent + self.working_solid: 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]] = {} + # self.last_operation_edges: list[Edge] = [] + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + pass + + def push_locations(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): + print(f"Add before: {self.locations=}") + print(f"Add before: {self.pending_faces=}") + for i, workplane in enumerate(self.workplanes): + if not self.locations: + self.locations[i] = Location(Vector()) + for loc in self.locations[i]: + # located_loc = workplane.fromLocalCoords(loc) + print(f"{loc=}") + localized_location = loc * Location(workplane) + print(f"{localized_location=}") + # if i in self.workplanes: + if isinstance(obj, Face): + self.pending_faces[i].append( + # workplane.fromLocalCoords(obj.located(localized_location)) + obj.located(localized_location) + ) + else: + self.pending_edges[i].append( + # workplane.fromLocalCoords(obj.located(localized_location)) + obj.located(localized_location) + ) + # else: + # if isinstance(obj, Face): + # self.pending_faces[i] = [ + # # workplane.fromLocalCoords(obj.located(localized_location)) + # obj.located(localized_location) + # ] + # else: + # self.pending_edges[i] = [ + # # workplane.fromLocalCoords(obj.located(localized_location)) + # obj.located(localized_location) + # ] + print(f"Add after: {self.pending_faces=}") + + 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.working_solid.Edges() + elif sort_by == SortBy.X: + edges = sorted( + self.working_solid.Edges(), + key=lambda obj: obj.Center().x, + reverse=reverse, + ) + elif sort_by == SortBy.Y: + edges = sorted( + self.working_solid.Edges(), + key=lambda obj: obj.Center().y, + reverse=reverse, + ) + elif sort_by == SortBy.Z: + edges = sorted( + self.working_solid.Edges(), + key=lambda obj: obj.Center().z, + reverse=reverse, + ) + elif sort_by == SortBy.LENGTH: + edges = sorted( + self.working_solid.Edges(), + key=lambda obj: obj.Length(), + reverse=reverse, + ) + elif sort_by == SortBy.RADIUS: + edges = sorted( + self.working_solid.Edges(), + key=lambda obj: obj.radius(), + reverse=reverse, + ) + elif sort_by == SortBy.DISTANCE: + edges = sorted( + self.working_solid.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.working_solid.Faces() + elif sort_by == SortBy.X: + faces = sorted( + self.working_solid.Faces(), + key=lambda obj: obj.Center().x, + reverse=reverse, + ) + elif sort_by == SortBy.Y: + faces = sorted( + self.working_solid.Faces(), + key=lambda obj: obj.Center().y, + reverse=reverse, + ) + elif sort_by == SortBy.Z: + faces = sorted( + self.working_solid.Faces(), + key=lambda obj: obj.Center().z, + reverse=reverse, + ) + elif sort_by == SortBy.AREA: + faces = sorted( + self.working_solid.Faces(), key=lambda obj: obj.Area(), reverse=reverse + ) + elif sort_by == SortBy.DISTANCE: + faces = sorted( + self.working_solid.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.working_solid.Vertices() + elif sort_by == SortBy.X: + vertices = sorted( + self.working_solid.Vertices(), + key=lambda obj: obj.Center().x, + reverse=reverse, + ) + elif sort_by == SortBy.Y: + vertices = sorted( + self.working_solid.Vertices(), + key=lambda obj: obj.Center().y, + reverse=reverse, + ) + elif sort_by == SortBy.Z: + vertices = sorted( + self.working_solid.Vertices(), + key=lambda obj: obj.Center().z, + reverse=reverse, + ) + elif sort_by == SortBy.DISTANCE: + vertices = sorted( + self.working_solid.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.working_solid is None else set(self.working_solid.Vertices()) + ) + before_edges = ( + set() if self.working_solid is None else set(self.working_solid.Edges()) + ) + before_faces = ( + set() if self.working_solid is None else set(self.working_solid.Faces()) + ) + + if mode == Mode.ADDITION: + if self.working_solid is None: + if len(new_solids) == 1: + self.working_solid = new_solids[0] + else: + self.working_solid = new_solids.pop().fuse(*new_solids) + else: + self.working_solid = self.working_solid.fuse(*new_solids).clean_op() + elif mode == Mode.SUBTRACTION: + if self.working_solid is None: + raise ValueError("Nothing to subtract from") + self.working_solid = self.working_solid.cut(*new_solids).clean_op() + elif mode == Mode.INTERSECTION: + if self.working_solid is None: + raise ValueError("Nothing to intersect with") + self.working_solid = self.working_solid.intersect(*new_solids).clean_op() + + self.last_operation[CqObject.VERTEX] = list( + set(self.working_solid.Vertices()) - before_vertices + ) + self.last_operation[CqObject.EDGE] = list( + set(self.working_solid.Edges()) - before_edges + ) + self.last_operation[CqObject.FACE] = list( + set(self.working_solid.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]: + print(f"{type(face)=}") + print(f"{face.Area()=}") + print(f"{face.Center()=}") + print(f"{face.normalAt(face.Center())=}") + 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): + + new_solids = [] + for i in range(len(self.workplanes)): + new_wires = [] + for face in self.faces[i]: + new_wires.append(face.outerWire()) + print(f"{len(new_wires)=}") + new_solids.append(Solid.makeLoft(new_wires, ruled)) + + self.place_solids(new_solids, mode, clean) + + return new_solids[0] if len(new_solids) == 1 else new_solids + + def fillet(self, *edges: Sequence[Edge], radius: float): + self.working_solid = self.working_solid.fillet(radius, [e for e in edges]) + + +class Build2D: + def __init__(self, parent: Build3D = None, mode: Mode = Mode.ADDITION): + self.working_surface = Compound.makeCompound(()) + self.pending_edges: list[Edge] = [] + # self.tags: dict[str, Face] = {} + self.parent = parent + self.locations: list[Location] = [] + self.mode = mode + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + print(f"Exit: Area of generated Face: {self.working_surface.Area()}") + if self.parent is not None: + self.parent.add(self.working_surface, self.mode) + + def add(self, f: Face, mode: Mode = Mode.ADDITION): + new_faces = self.place_face(f, mode) + return new_faces if len(new_faces) > 1 else new_faces[0] + + def push_locations(self, *pts: Sequence[Union[VectorLike, Location]]): + new_locations = [ + Location(Vector(pt)) if not isinstance(pt, Location) else pt for pt in pts + ] + self.locations.extend(new_locations) + return new_locations + + def assemble_edges(self, mode: Mode = Mode.ADDITION, tag: str = None) -> Face: + pending_face = Face.makeFromWires(Wire.assembleEdges(self.pending_edges)) + self.add(pending_face, mode, tag) + self.pending_edges = [] + # print(f"Area of generated Face: {pending_face.Area()}") + return pending_face + + def hull_edges(self, mode: Mode = Mode.ADDITION, tag: str = None) -> Face: + pending_face = find_hull(self.pending_edges) + self.add(pending_face, mode, tag) + self.pending_edges = [] + # print(f"Area of generated Face: {pending_face.Area()}") + return pending_face + + def rect( + self, + width: float, + height: float, + angle: float = 0, + mode: Mode = Mode.ADDITION, + ) -> Face: + """ + Construct a rectangular face. + """ + + new_faces = self.place_face( + Face.makePlane(height, width).rotate(*z_axis, angle), mode + ) + + return new_faces if len(new_faces) > 1 else new_faces[0] + + def circle(self, radius: float, mode: Mode = Mode.ADDITION) -> Face: + """ + Construct a circular face. + """ + + new_faces = self.place_face( + Face.makeFromWires(Wire.makeCircle(radius, *z_axis)), mode + ) + + return new_faces if len(new_faces) > 1 else new_faces[0] + + def place_face(self, face: Face, mode: Mode = Mode.ADDITION): + + if not self.locations: + self.locations = [Location(Vector())] + new_faces = [face.located(location) for location in self.locations] + + if mode == Mode.ADDITION: + self.working_surface = self.working_surface.fuse(*new_faces).clean() + elif mode == Mode.SUBTRACTION: + self.working_surface = self.working_surface.cut(*new_faces).clean() + elif mode == Mode.INTERSECTION: + self.working_surface = self.working_surface.intersect(*new_faces).clean() + elif mode == Mode.CONSTRUCTION: + pass + else: + raise ValueError(f"Invalid mode: {mode}") + + self.locations = [] + return new_faces + + +class Build1D: + def __init__(self, parent: Build2D = None, mode: Mode = Mode.ADDITION): + self.edge_list = [] + self.tags: dict[str, Edge] = {} + self.parent = parent + self.mode = mode + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + pending_face = Face.makeFromWires(Wire.assembleEdges(self.edge_list)) + print(f"Exit: Area of generated Face: {pending_face.Area()}") + # print(self.tags) + self.parent.add(pending_face, self.mode) + + def edges(self) -> list[Edge]: + return self.edge_list + + def vertices(self) -> list[Vertex]: + vertex_list = [] + for e in self.edge_list: + vertex_list.extend(e.Vertices()) + return list(set(vertex_list)) + + def polyline( + self, + *pts: VectorLike, + mode: Mode = Mode.ADDITION, + tag: str = None, + ): + if len(pts) < 2: + raise ValueError("polyline requires two or more pts") + + lines_pts = [Vector(p) for p in pts] + + new_edges = [ + Edge.makeLine(lines_pts[i], lines_pts[i + 1]) + for i in range(len(lines_pts) - 1) + ] + + for e in new_edges: + e.forConstruction = mode == Mode.CONSTRUCTION + self.edge_list.extend(new_edges) + + return_value = ( + new_edges[0] if len(new_edges) == 1 else Wire.assembleEdges(new_edges) + ) + + if tag: + if len(new_edges) > 1: + for i, edge in enumerate(new_edges): + self.tags[f"{tag}-{i}"] = edge + else: + self.tags[tag] = new_edges[0] + + return return_value + + +# with Build2D() as f: +# # Start with a central circle with a square quarter +# c6 = f.circle(6) +# print(f"{type(c6)=}, {c6.Center()=}") +# f.push((3, 3)) +# r6 = f.rect(6, 6) +# print(f"{type(r6)=}, {r6.Center()=}") +# # Create some locations for the cutouts +# polar_locations = [ +# Location(Vector(3, 0, 0).rotateZ(a), Vector(0, 0, 1), a) +# for a in range(0, 360, 45) +# ] +# f.push(*polar_locations) +# # Cutout a set of diamonds +# with Build1D(parent=f, mode=Mode.SUBTRACTION) as e: +# # Instantiate a simple line +# l = e.polyline((0, 0), (1, 1)) +# print(f"Type of line: {type(l)}") +# # Instantiate a polyline +# m = e.polyline(l.endPoint(), (2, 0), (1, -1)) +# print(f"Type of polyline: {type(m)}") +# # Create another line but don't assign a global to it +# e.polyline(m.endPoint(), l.startPoint()) +# # Extract all of the vertices - two for each Edge +# all_vertices = e.vertices() +# print(f"Type of vertices: {type(all_vertices)}") +# print(f"Total number of vertices: {len(all_vertices)}") +# # Sort these vertices by Y value +# corners_sorted_by_Y = sorted(all_vertices, key=lambda v: v.Y) +# # Filter the sorted value to extract just those on the X axis +# side_corners = list(filter(lambda v: abs(v.Y) < 1e-5, corners_sorted_by_Y)) +# print(f"Number of vertices after filter: {len(side_corners)}") +# print("Corner vertices at X axis:") +# for v in side_corners: +# print(v.toTuple()) + +# with Build2D() as f2: +# with Build1D(parent=f2) as c2: +# pts = [Vector(10, 0, 0).rotateZ(a) for a in range(0, 360, 60)] +# c2.polyline(*pts) +# print(f"{type(c2.face)=}") + +# with Build3D() as s1: +# with Build2D(s1) as f1: +# f1.rect(10, 10) +# box = s1.extrude(10) +# s1.faces_to_workplanes(*box.Faces()) +# with Build2D(s1) as f2: +# f2.circle(3) +# s1.extrude(-1, mode=Mode.SUBTRACTION) +# # edges_by_z = sorted(s1.edges(), key=by_z, reverse=True)[0:1] +# # edges_by_z = s1.edges(sort_by=SortBy.Z, reverse=True)[0:1] +# edges_by_z = s1.edges(SortBy.Z, reverse=True)[0:1] +# # print(f"{edges_by_z}") +# # top_circle = sorted(s1.last_operation_edges, key=by_z, reverse=True)[0] +# # top_circle = filter(lambda f: abs(f.by_z() - 5) < 1e-5, s1.last_operation_edges) +# # s1.fillet(*s1.last_operation[CqObject.EDGE], radius=0.2) +# s1.fillet(*edges_by_z, radius=0.2) + +# print(s1.solid.Volume()) + +# with Build2D() as f1: +# f1.push_locations((-5, -5), (-5, 5), (5, 5), (5, -5)) +# # f1.rect(1, 1) +# f1.circle(1) +# faces = [f for list in s1.faces.values() for f in list] + +# with Build3D() as s2: +# with Build2D(s2) as f1: +# f1.push_locations((4, 3)) +# rect = f1.rect(6, 6) +# revolve = s2.pending_faces +# s2.revolve() + +with Build3D() as s2: + # s2.push_locations((-5, -5), (-5, 5), (5, 5), (5, -5)) + with Build2D(s2) as f1: + f1.circle(1) + + +# with Build3D() as s3: +# for i in range(21): +# r = 10 * sin(i * pi / 20) + 5 +# s3.push((0, 0, i)) +# with Build2D(s3) as f1: +# f1.circle(r) +# print(f"{len(s3.faces[0])=}") +# s3.loft() + +if "show_object" in locals(): + show_object(s2.pending_faces[0]) + # show_object(rect, name="rect") + # show_object(f1.working_surface, name="working_surface") + # show_object(revolve, name="revolve") + # show_object(f1.face_list) + # show_object(s1.working_solid, name="s1") + # show_object(s1.last_operation_edges, name="last edges") + # show_object(s2.solid, name="s2") + # show_object(f1.faces, name="f1") + # show_object(rface, name="rface") + # show_object(faces, name="circles") + # show_object(bumps, name="bumps") + # show_object(f.faces, name="f") + # show_object(f2.faces, name="f2") diff --git a/rtree.py b/rtree.py new file mode 100644 index 0000000..4916ee4 --- /dev/null +++ b/rtree.py @@ -0,0 +1,39 @@ +def get_bb_coords(shape: Shape) -> Tuple[float, float, float, float, float, float]: + bb = shape.BoundingBox() + return (bb.xmin, bb.ymin, bb.zmin, bb.xmax, bb.ymax, bb.zmax) + + +def build_tree(shapes: Iterable[Shape]): + import rtree.index + + p = rtree.index.Property(dimension=3) + t = rtree.index.RtreeContainer(properties=p) + for shape in shapes: + t.insert(shape, get_bb_coords(shape)) + return t + + +def face_outside(f: Face, ff: Face) -> bool: + """f exists outside of ff""" + # TODO: shortcut True if bbox intersection is 1d and f.Area() > 0 + return f.cut(ff).Area() > 0 + + +def new_faces(self: T) -> T: + """Find faces not present in parent""" + old_faces = self.parent.findSolid().Faces() + old_set = set(old_faces) + new = [f for f in self.findSolid().Faces() if f not in old_set] + new_tree = build_tree(new) + old_tree = build_tree( + f for f in old_faces if any(new_tree.intersection(get_bb_coords(f))) + ) + new = [ + f + for f in new + if all(face_outside(f, ff) for ff in old_tree.intersection(get_bb_coords(f))) + ] + return self.newObject(new) + + +cq.Workplane.new_faces = new_faces