build123d/build_part.py
2022-07-13 16:13:33 -04:00

1151 lines
40 KiB
Python

"""
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)