""" BuildPart name: build_part.py by: Gumyr date: July 12th 2022 desc: This python module is a library used to build 3D parts. TODO: - add TwistExtrude, ProjectText - add centered to wedge - add Mode.REPLACE for operations like fillet that change the part - add a Workplane class with a Plane input license: Copyright 2022 Gumyr Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from math import radians, sin, cos, tan from tkinter import TOP from typing import Union from itertools import product from cadquery import ( Edge, Face, Wire, Vector, Location, Vertex, Compound, Solid, Plane, ) from cadquery.occ_impl.shapes import VectorLike import cq_warehouse.extensions from build123d_common import * 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 def workplane_count(self) -> int: """Number of active workplanes""" return len(self.workplanes) @property def pending_faces_count(self) -> int: """Number of pending faces""" return len(self.pending_faces.values()) @property def pending_edges_count(self) -> int: """Number of pending edges""" return len(self.pending_edges.values()) @property def pending_location_count(self) -> int: """Number of current locations""" return len(self.locations) def __init__( self, mode: Mode = Mode.ADDITION, workplane: Plane = Plane.named("XY"), ): self.part: Compound = None self.workplanes: list[Plane] = [workplane] self.locations: list[Location] = [Location(workplane.origin)] self.pending_faces: dict[int : list[Face]] = {0: []} self.pending_edges: dict[int : list[Edge]] = {0: []} self.mode = mode self.last_vertices = [] self.last_edges = [] self.last_faces = [] self.last_solids = [] def __enter__(self): """Upon entering BuildPart, add current BuildPart instance to context stack""" if context_stack: raise RuntimeError("BuildPart can't be nested") context_stack.append(self) return self def __exit__(self, exception_type, exception_value, traceback): """Upon exiting BuildPart - do nothing""" context_stack.pop() 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: self.workplanes = [] for plane in workplanes: self.workplanes.append(plane) 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 = [] if select == Select.ALL: for edge in self.part.Edges(): vertex_list.extend(edge.Vertices()) elif select == Select.LAST: vertex_list = self.last_vertices return VertexList(set(vertex_list)) 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: edge_list = self.part.Edges() elif select == Select.LAST: edge_list = self.last_edges return ShapeList(edge_list) 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: face_list = self.part.Faces() elif select == Select.LAST: face_list = self.last_edges return ShapeList(face_list) 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: solid_list = self.part.Solids() elif select == Select.LAST: solid_list = self.last_solids return ShapeList(solid_list) def add_to_pending(self, *objects: Union[Edge, Face]): """Add objects to BuildPart pending lists Args: objects (Union[Edge, Face]): sequence of objects to add """ for obj in objects: for i, workplane in enumerate(self.workplanes): for loc in self.locations: 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, *objects: Union[Edge, Wire, Face, Solid, Compound], 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 eliminate 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: # 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()) pre_vertices = set() if self.part is None else set(self.part.Vertices()) 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()) if new_solids: 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() elif mode == Mode.SUBTRACTION: if self.part is None: raise ValueError("Nothing to subtract from") self.part = self.part.cut(*new_solids).clean() elif mode == Mode.INTERSECTION: if self.part is None: raise ValueError("Nothing to intersect with") self.part = self.part.intersect(*new_solids).clean() elif mode == Mode.CONSTRUCTION: pass else: raise ValueError(f"Invalid mode: {mode}") post_vertices = set() if self.part is None else set(self.part.Vertices()) post_edges = set() if self.part is None else set(self.part.Edges()) post_faces = set() if self.part is None else set(self.part.Faces()) post_solids = set() if self.part is None else set(self.part.Solids()) 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) self.add_to_pending(*new_edges) self.add_to_pending(*new_faces) def get_and_clear_locations(self) -> list: """Return location and planes from current points and workplanes and clear locations.""" location_planes = [] for workplane in self.workplanes: location_planes.extend( [ ((Location(workplane) * location), workplane) for location in self.locations ] ) self.locations = [Location(Vector())] return location_planes @staticmethod 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] # # Operations # class ChamferPart(Compound): """Part Operation: Chamfer Chamfer the given sequence of edges. Args: edges (Edge): sequence of edges to chamfer length1 (float): chamfer size length2 (float, optional): asymmetric chamfer size. Defaults to None. """ 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 CounterBoreHole(Compound): """Part Operation: Counter Bore Hole Create a counter bore hole in part. Args: radius (float): hole size counter_bore_radius (float): counter bore size counter_bore_depth (float): counter bore depth depth (float, optional): hole depth - None implies through part. Defaults to None. mode (Mode, optional): combination mode. Defaults to Mode.SUBTRACTION. """ def __init__( self, radius: float, counter_bore_radius: float, counter_bore_depth: float, depth: float = None, mode: Mode = Mode.SUBTRACTION, ): hole_depth = ( BuildPart.get_context().part.BoundingBox().DiagonalLength if depth is None else depth ) location_planes = BuildPart.get_context().get_and_clear_locations() new_solids = [ Solid.makeCylinder( radius, hole_depth, loc.position(), plane.zDir * -1.0 ).fuse( Solid.makeCylinder( counter_bore_radius, counter_bore_depth, loc.position(), plane.zDir * -1.0, ) ) for loc, plane in location_planes ] BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class CounterSinkHole(Compound): """Part Operation: Counter Sink Hole Create a counter sink hole in part. Args: radius (float): hole size counter_sink_radius (float): counter sink size depth (float, optional): hole depth - None implies through part. Defaults to None. counter_sink_angle (float, optional): cone angle. Defaults to 82. mode (Mode, optional): combination mode. Defaults to Mode.SUBTRACTION. """ def __init__( self, radius: float, counter_sink_radius: float, depth: float = None, counter_sink_angle: float = 82, # Common tip angle mode: Mode = Mode.SUBTRACTION, ): hole_depth = ( BuildPart.get_context().part.BoundingBox().DiagonalLength if depth is None else depth ) cone_height = counter_sink_radius / tan(radians(counter_sink_angle / 2.0)) location_planes = BuildPart.get_context().get_and_clear_locations() new_solids = [ Solid.makeCylinder( radius, hole_depth, loc.position(), plane.zDir * -1.0 ).fuse( Solid.makeCone( counter_sink_radius, 0.0, cone_height, loc, plane.zDir * -1.0, ) ) for loc, plane in location_planes ] BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class Extrude(Compound): """Part Operation: Extrude Extrude a sketch/face and combine with part. Args: until (Union[float, Until, Face]): depth of extrude or extrude limit both (bool, optional): extrude in both directions. Defaults to False. taper (float, optional): taper during extrusion. Defaults to None. mode (Mode, optional): combination mode. Defaults to Mode.ADDITION. """ def __init__( self, until: Union[float, Until, Face], both: bool = False, taper: float = None, mode: Mode = Mode.ADDITION, ): new_solids: list[Solid] = [] for plane_index, faces in BuildPart.get_context().pending_faces.items(): for face in faces: new_solids.append( Solid.extrudeLinear( face, BuildPart.get_context().workplanes[plane_index].zDir * until, 0, ) ) if both: new_solids.append( Solid.extrudeLinear( face, BuildPart.get_context().workplanes[plane_index].zDir * until * -1.0, 0, ) ) BuildPart.get_context().pending_faces = {0: []} BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class FilletPart(Compound): """Part Operation: Fillet Fillet the given sequence of edges. Args: edges (Edge): sequence of edges to fillet radius (float): fillet size - must be less than 1/2 local width """ 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) class Hole(Compound): """Part Operation: Hole Create a hole in part. Args: radius (float): hole size depth (float, optional): hole depth - None implies through part. Defaults to None. mode (Mode, optional): combination mode. Defaults to Mode.SUBTRACTION. """ def __init__( self, radius: float, depth: float = None, mode: Mode = Mode.SUBTRACTION, ): hole_depth = ( BuildPart.get_context().part.BoundingBox().DiagonalLength if depth is None else depth ) location_planes = BuildPart.get_context().get_and_clear_locations() new_solids = [ Solid.makeCylinder( radius, hole_depth, loc.position(), plane.zDir * -1.0, 360 ) for loc, plane in location_planes ] BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class Loft(Solid): """Part Operation: Loft Loft the pending sketches/faces, across all workplanes, into a solid. Args: ruled (bool, optional): discontiguous layer tangents. Defaults to False. mode (Mode, optional): combination mode. Defaults to Mode.ADDITION. """ def __init__(self, ruled: bool = False, mode: Mode = Mode.ADDITION): loft_wires = [] for i in range(len(BuildPart.get_context().workplanes)): for face in BuildPart.get_context().pending_faces[i]: loft_wires.append(face.outerWire()) new_solid = Solid.makeLoft(loft_wires, ruled) BuildPart.get_context().pending_faces = {0: []} BuildPart.get_context().add_to_context(new_solid, mode=mode) super().__init__(new_solid.wrapped) class PolarArrayToPart: """Part Operation: Polar Array Push a polar array of locations to BuildPart Args: radius (float): array radius start_angle (float): angle to first point from +ve X axis stop_angle (float): angle to last point from +ve X axis count (int): Number of points to push rotate (bool, optional): Align locations with arc tangents. Defaults to True. Raises: ValueError: Count must be greater than or equal to 1 """ def __init__( self, radius: float, start_angle: float, stop_angle: float, count: int, rotate: bool = True, ): if count < 1: raise ValueError(f"At least 1 elements required, requested {count}") x = radius * sin(radians(start_angle)) y = radius * cos(radians(start_angle)) if rotate: loc = Location(Vector(x, y), Vector(0, 0, 1), -start_angle) else: loc = Location(Vector(x, y)) new_locations = [loc] angle = (stop_angle - start_angle) / (count - 1) for i in range(1, count): phi = start_angle + (angle * i) x = radius * sin(radians(phi)) y = radius * cos(radians(phi)) if rotate: loc = Location(Vector(x, y), Vector(0, 0, 1), -phi) else: loc = Location(Vector(x, y)) new_locations.append(loc) BuildPart.get_context().locations = new_locations 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]): new_locations = [ pt if isinstance(pt, Location) else Location(Vector(pt)) for pt in pts ] BuildPart.get_context().locations = new_locations class RectangularArrayToPart: """Part Operation: Rectangular Array Push a rectangular array of locations to BuildPart Args: x_spacing (float): horizontal spacing y_spacing (float): vertical spacing x_count (int): number of horizontal points y_count (int): number of vertical points Raises: ValueError: Either x or y count must be greater than or equal to one. """ def __init__(self, x_spacing: float, y_spacing: float, x_count: int, y_count: int): if x_count < 1 or y_count < 1: raise ValueError( f"At least 1 elements required, requested {x_count}, {y_count}" ) new_locations = [] offset = Vector((x_count - 1) * x_spacing, (y_count - 1) * y_spacing) * 0.5 for i, j in product(range(x_count), range(y_count)): new_locations.append( Location(Vector(i * x_spacing, j * y_spacing) - offset) ) BuildPart.get_context().locations = new_locations class Revolve(Compound): """Part Operation: Revolve Revolve the pending sketches/faces about the given local axis. Args: revolution_arc (float, optional): angular size of revolution. Defaults to 360.0. axis_start (VectorLike, optional): axis start in local coordinates. Defaults to None. axis_end (VectorLike, optional): axis end in local coordinates. Defaults to None. mode (Mode, optional): combination mode. Defaults to Mode.ADDITION. """ def __init__( self, revolution_arc: float = 360.0, axis_start: VectorLike = None, axis_end: VectorLike = None, mode: Mode = Mode.ADDITION, ): # 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 = revolution_arc % 360.0 angle = 360.0 if angle == 0 else angle new_solids = [] for i, workplane in enumerate(BuildPart.get_context().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))) for face in BuildPart.get_context().pending_faces[i]: new_solids.append(Solid.revolve(face, angle, *axis)) BuildPart.get_context().pending_faces = {0: []} BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class Shell(Compound): """Part Operation: Shell Create a hollow shell from part with provided open faces. Args: faces (Face): sequence of faces to open thickness (float): thickness of shell - positive values shell outwards, negative inwards. kind (Kind, optional): edge construction option. Defaults to Kind.ARC. """ def __init__( self, *faces: Face, thickness: float, kind: Kind = Kind.ARC, ): new_part = BuildPart.get_context().part.shell( faces, thickness, kind=kind.name.lower() ) BuildPart.get_context().part = new_part super().__init__(new_part.wrapped) class Split(Compound): """Part Operation: Split Bisect part with plane and keep either top, bottom or both. Args: bisect_by (Plane, optional): plane to segment part. Defaults to Plane.named("XZ"). keep (Keep, optional): selector for which segment to keep. Defaults to Keep.TOP. """ def __init__(self, bisect_by: Plane = Plane.named("XZ"), keep: Keep = Keep.TOP): max_size = BuildPart.get_context().BoundingBox().DiagonalLength def build_cutter(keep: Keep) -> Solid: cutter_center = ( Vector(-max_size, -max_size, 0) if keep == Keep.TOP else Vector(-max_size, -max_size, -2 * max_size) ) return bisect_by.fromLocalCoords( Solid.makeBox(2 * max_size, 2 * max_size, 2 * max_size).moved( Location(cutter_center) ) ) cutters = [] if keep == Keep.BOTH: cutters.append(build_cutter(Keep.TOP)) cutters.append(build_cutter(Keep.BOTTOM)) else: cutters.append(build_cutter(keep)) BuildPart.get_context().add_to_context(*cutters, mode=Mode.INTERSECTION) super().__init__(BuildPart.get_context().part.wrapped) class Sweep(Compound): """Part Operation: Sweep Sweep pending sketches/faces along path. Args: path (Union[Edge, Wire]): path to follow multisection (bool, optional): sweep multiple on path. Defaults to False. make_solid (bool, optional): create solid instead of face. Defaults to True. is_frenet (bool, optional): use freenet algorithm. Defaults to False. transition (Transition, optional): discontinuity handling option. Defaults to Transition.RIGHT. normal (VectorLike, optional): fixed normal. Defaults to None. binormal (Union[Edge, Wire], optional): guide rotation along path. Defaults to None. mode (Mode, optional): combination. Defaults to Mode.ADDITION. """ def __init__( 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, ): 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 in range(BuildPart.get_context().workplane_count): if not multisection: for face in BuildPart.get_context().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 BuildPart.get_context().pending_faces[i] ] new_solids.append( Solid.sweep_multi( sections, path_wire, make_solid, is_frenet, binormal_mode ) ) BuildPart.get_context().pending_faces = {0: []} BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class WorkplanesFromFaces: """Part Operation: Workplanes from Faces Create workplanes from the given sequence of faces, optionally replacing existing workplanes. The workplane origin is aligned to the center of the face. Args: faces (Face): sequence of faces to convert to workplanes. replace (bool, optional): replace existing workplanes. Defaults to True. """ def __init__(self, *faces: Face, replace=True): new_planes = [ Plane(origin=face.Center(), normal=face.normalAt(face.Center())) for face in faces ] BuildPart.get_context().workplane(*new_planes, replace=replace) # # Objects # class AddToPart(Compound): """Part Object: Add Object to Part Add an object to the part. Edges and Wires are added to pending_edges. Compounds of Face are added to pending_faces. Solids or Compounds of Solid are combined into the part. Args: objects (Union[Edge, Wire, Face, Solid, Compound]): sequence of objects to add rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0). mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. """ def __init__( self, *objects: Union[Edge, Wire, Face, Solid, Compound], rotation: RotationLike = (0, 0, 0), mode: Mode = Mode.ADDITION, ): rotate = Rotation(*rotation) if isinstance(rotation, tuple) else rotation new_faces = [obj for obj in objects if isinstance(obj, Face)] new_solids = [obj.moved(rotate) 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) # Can't use get_and_clear_locations because the solid needs to be # oriented to the workplane after being moved to a local location located_solids = [ workplane.fromLocalCoords(solid.moved(location)) for solid in new_solids for workplane in BuildPart.get_context().workplanes for location in BuildPart.get_context().locations ] BuildPart.get_context().locations = [Location(Vector())] BuildPart.get_context().add_to_context(*located_solids, mode=mode) super().__init__(Compound.makeCompound(located_solids).wrapped) class Box(Compound): """Part Object: Box Create a box(es) and combine with part. Args: length (float): box size width (float): box size height (float): box size 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). mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. """ def __init__( self, length: float, width: float, height: float, rotation: RotationLike = (0, 0, 0), centered: tuple[bool, bool, bool] = (True, True, True), mode: Mode = Mode.ADDITION, ): rotate = Rotation(*rotation) if isinstance(rotation, tuple) else rotation location_planes = BuildPart.get_context().get_and_clear_locations() center_offset = Vector( -length / 2 if centered[0] else 0, -width / 2 if centered[1] else 0, -height / 2 if centered[2] else 0, ) new_solids = [ Solid.makeBox( length, width, height, loc.position() + plane.fromLocalCoords(center_offset), plane.zDir, ).moved(rotate) for loc, plane in location_planes ] BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class Cone(Compound): """Part Object: Cone Create a cone(s) and combine with part. Args: bottom_radius (float): cone size top_radius (float): top size, could be zero height (float): cone size arc_size (float, optional): angular size of cone. Defaults to 360. 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). mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. """ def __init__( self, bottom_radius: float, top_radius: float, height: float, arc_size: float = 360, rotation: RotationLike = (0, 0, 0), centered: tuple[bool, bool, bool] = (True, True, True), mode: Mode = Mode.ADDITION, ): rotate = Rotation(*rotation) if isinstance(rotation, tuple) else rotation location_planes = BuildPart.get_context().get_and_clear_locations() center_offset = Vector( 0 if centered[0] else max(bottom_radius, top_radius), 0 if centered[1] else max(bottom_radius, top_radius), -height / 2 if centered[2] else 0, ) new_solids = [ Solid.makeCone( bottom_radius, top_radius, height, loc.position() + plane.fromLocalCoords(center_offset), plane.zDir, arc_size, ).moved(rotate) for loc, plane in location_planes ] BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class Cylinder(Compound): """Part Object: Cylinder Create a cylinder(s) and combine with part. Args: radius (float): cylinder size height (float): cylinder size arc_size (float, optional): angular size of cone. Defaults to 360. 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). mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. """ def __init__( self, radius: float, height: float, arc_size: float = 360, rotation: RotationLike = (0, 0, 0), centered: tuple[bool, bool, bool] = (True, True, True), mode: Mode = Mode.ADDITION, ): rotate = Rotation(*rotation) if isinstance(rotation, tuple) else rotation location_planes = BuildPart.get_context().get_and_clear_locations() center_offset = Vector( 0 if centered[0] else radius, 0 if centered[1] else radius, -height / 2 if centered[2] else 0, ) new_solids = [ Solid.makeCylinder( radius, height, loc.position() + plane.fromLocalCoords(center_offset), plane.zDir, arc_size, ).moved(rotate) for loc, plane in location_planes ] BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class Sphere(Compound): """Part Object: Sphere Create a sphere(s) and combine with part. Args: radius (float): sphere size arc_size1 (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. 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). mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. """ def __init__( self, radius: float, arc_size1: float = -90, arc_size2: float = 90, arc_size3: float = 360, rotation: RotationLike = (0, 0, 0), centered: tuple[bool, bool, bool] = (True, True, True), mode: Mode = Mode.ADDITION, ): rotate = Rotation(*rotation) if isinstance(rotation, tuple) else rotation location_planes = BuildPart.get_context().get_and_clear_locations() center_offset = Vector( 0 if centered[0] else radius, 0 if centered[1] else radius, 0 if centered[2] else radius, ) new_solids = [ Solid.makeSphere( radius, loc.position() + plane.fromLocalCoords(center_offset), plane.zDir, arc_size1, arc_size2, arc_size3, ).moved(rotate) for loc, plane in location_planes ] BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class Torus(Compound): """Part Object: Torus Create a torus(es) and combine with part. Args: major_radius (float): torus size minor_radius (float): torus size major_arc_size (float, optional): angular size or torus. Defaults to 0. 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). centered (tuple[bool, bool, bool], optional): center about axes. Defaults to (True, True, True). mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. """ def __init__( self, major_radius: float, minor_radius: float, major_arc_size: float = 0, minor_arc_size: float = 360, rotation: RotationLike = (0, 0, 0), centered: tuple[bool, bool, bool] = (True, True, True), mode: Mode = Mode.ADDITION, ): rotate = Rotation(*rotation) if isinstance(rotation, tuple) else rotation location_planes = BuildPart.get_context().get_and_clear_locations() center_offset = Vector( 0 if centered[0] else major_radius, 0 if centered[1] else major_radius, 0 if centered[2] else minor_radius, ) new_solids = [ Solid.makeTorus( major_radius, minor_radius, loc.position() + plane.fromLocalCoords(center_offset), plane.zDir, major_arc_size, minor_arc_size, ).moved(rotate) for loc, plane in location_planes ] BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped) class Wedge(Compound): """Part Object: Wedge Create a wedge(s) and combine with part. Args: dx (float): distance along the X axis dy (float): distance along the Y axis dz (float): distance along the Z axis xmin (float): minimum X location zmin (float): minimum Z location xmax (float): maximum X location zmax (float): maximum Z location rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0). mode (Mode, optional): combine mode. Defaults to Mode.ADDITION. """ def __init__( self, dx: float, dy: float, dz: float, xmin: float, zmin: float, xmax: float, zmax: float, rotation: RotationLike = (0, 0, 0), mode: Mode = Mode.ADDITION, ): rotate = Rotation(*rotation) if isinstance(rotation, tuple) else rotation location_planes = BuildPart.get_context().get_and_clear_locations() new_solids = [ Solid.makeWedge( dx, dy, dz, xmin, zmin, xmax, zmax, loc.position(), plane.zDir ).moved(rotate) for loc, plane in location_planes ] BuildPart.get_context().add_to_context(*new_solids, mode=mode) super().__init__(Compound.makeCompound(new_solids).wrapped)