Split module, refactored build1d from methods to classes

This commit is contained in:
Roger Maitland 2022-07-02 20:34:03 -04:00
parent 1d029e8d34
commit 30c0045a6d
6 changed files with 1158 additions and 0 deletions

219
build123d_common.py Normal file
View file

@ -0,0 +1,219 @@
"""
OK, fwiw, the reason translate() creates a copy is because the use of copy=True in Shape._apply_transform(): https://github.com/CadQuery/cadquery/blob/c9d3f1e693d8d3b59054c8f10027c46a55342b22/cadquery/occ_impl/shapes.py#L846. I tried setting it to False and my original example passes.
Thanks. Playing around a bit more, it seems like translate() makes the underlying TShapes unequal, but Shape.moved() preserves TShape. This returns true, which could be useful:
x1 = cq.Workplane().box(3,4,5)
x2 = cq.Workplane(x1.findSolid().moved(cq.Location(cq.Vector(1,2,3),cq.Vector(4,5,6),7)))
f1 = x1.faces(">Z").val()
f2 = x2.faces(">Z").val()
f1.wrapped.TShape() == f2.wrapped.TShape() <=== TRUE
@fpq473 - cadquery newbie
Thanks. Playing around a bit more, it seems like translate() makes the underlying TShapes unequal, but Shape.moved() preserves TShape. This returns true, which could be useful: x1 = cq.Workplane().box(3,4,5) x2 = cq.Workplane(x1.findSolid().moved(cq.Location(cq.Vector(1,2,3),cq.Vector(4,5,6),7))) f1 = x1.faces(">Z").val() f2 = x2.faces(">Z").val() f1.wrapped.TShape() == f2.wrapped.TShape() <=== TRUE
"""
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
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
Wire.__matmul__ = __matmul__custom
Wire.__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 Transition(Enum):
RIGHT = auto()
ROUND = auto()
TRANSFORMED = auto()
class Font_Style(Enum):
"""Text Font Styles"""
REGULAR = auto()
BOLD = auto()
ITALIC = auto()
def legacy(font_style: "Font_Style") -> str:
return {
Font_Style.REGULAR: "regular",
Font_Style.BOLD: "bold",
Font_Style.ITALIC: "italic",
}[font_style]
class Halign(Enum):
"""Horizontal Alignment"""
CENTER = auto()
LEFT = auto()
RIGHT = auto()
def legacy(halign: "Halign") -> str:
return {
Halign.LEFT: "left",
Halign.RIGHT: "right",
Halign.CENTER: "center",
}[halign]
class Valign(Enum):
"""Vertical Alignment"""
CENTER = auto()
TOP = auto()
BOTTOM = auto()
def legacy(valign: "Valign") -> str:
return {
Valign.TOP: "top",
Valign.BOTTOM: "bottom",
Valign.CENTER: "center",
}[valign]
class Until(Enum):
NEXT = auto()
LAST = auto()
class CqObject(Enum):
EDGE = auto()
FACE = auto()
VERTEX = auto()
class BuildAssembly:
def add(self):
pass
def _null(self):
return self
Solid.null = _null
Compound.null = _null

314
build1d.py Normal file
View file

@ -0,0 +1,314 @@
from math import pi, sin, cos, radians, sqrt
from typing import Union, Iterable, Sequence, Callable
from enum import Enum, auto
from abc import ABC, abstractmethod
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 *
from build2d import Build2D
from build3d import Build3D
class Build1D:
@property
def working_line(self) -> Wire:
return Wire.assembleEdges(self.edge_list)
def __init__(
self, parent: Union[Build2D, Build3D] = 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):
if self.parent is not None:
self.parent.add(*self.edge_list, mode=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))
class Build1DObject(ABC):
@property
@abstractmethod
def object(self):
"""Each derived class must provide the object created"""
return NotImplementedError
@staticmethod
def add(context: Build1D, *edges: Edge, mode: Mode = Mode.ADDITION):
if context is not None and mode != Mode.CONSTRUCTION:
for edge in edges:
if not isinstance(edge, Edge):
raise ValueError("Build1D.add only accepts edges")
context.edge_list.append(edge)
print(f"{context.edge_list=}")
class Polyline(Build1DObject):
@property
def object(self) -> Union[Edge, Wire]:
if len(self.new_edges) == 1:
return self.new_edges[0]
else:
return Wire.assembleEdges(self.new_edges)
def __init__(self, context: Build1D, *pts: VectorLike, mode: Mode = Mode.ADDITION):
if len(pts) < 2:
raise ValueError("polyline requires two or more pts")
lines_pts = [Vector(p) for p in pts]
self.new_edges = [
Edge.makeLine(lines_pts[i], lines_pts[i + 1])
for i in range(len(lines_pts) - 1)
]
Build1DObject.add(context, *self.new_edges, mode=mode)
class Spline(Build1DObject):
@property
def object(self) -> Edge:
return self.spline
def __init__(
self,
context: Build1D,
*pts: VectorLike,
tangents: Iterable[VectorLike] = None,
tangent_scalars: Iterable[float] = None,
periodic: bool = False,
mode: Mode = Mode.ADDITION,
):
spline_pts = [Vector(pt) for pt in pts]
if tangents:
spline_tangents = [Vector(tangent) for tangent in tangents]
else:
spline_tangents = None
if tangents and not tangent_scalars:
scalars = [1.0] * len(tangents)
else:
scalars = tangent_scalars
self.spline = Edge.makeSpline(
[p if isinstance(p, Vector) else Vector(*p) for p in spline_pts],
tangents=[
t * s if isinstance(t, Vector) else Vector(*t) * s
for t, s in zip(spline_tangents, scalars)
]
if spline_tangents
else None,
periodic=periodic,
scale=tangent_scalars is None,
)
Build1DObject.add(context, self.spline, mode=mode)
class CenterArc(Build1DObject):
@property
def object(self) -> Edge:
return self.arc
def __init__(
self,
context: Build1D,
center: VectorLike,
radius: float,
start_angle: float,
arc_size: float,
mode: Mode = Mode.ADDITION,
):
if abs(arc_size) >= 360:
self.arc = Edge.makeCircle(
radius,
center,
angle1=start_angle,
angle2=start_angle,
orientation=arc_size > 0,
)
else:
p0 = center
p1 = p0 + radius * Vector(
cos(radians(start_angle)), sin(radians(start_angle))
)
p2 = p0 + radius * Vector(
cos(radians(start_angle + arc_size / 2)),
sin(radians(start_angle + arc_size / 2)),
)
p3 = p0 + radius * Vector(
cos(radians(start_angle + arc_size)),
sin(radians(start_angle + arc_size)),
)
self.arc = Edge.makeThreePointArc(p1, p2, p3)
Build1DObject.add(context, self.arc, mode=mode)
class ThreePointArc(Build1DObject):
@property
def object(self) -> Edge:
return self.arc
def __init__(self, context: Build1D, *pts: VectorLike, mode: Mode = Mode.ADDITION):
if len(pts) != 3:
raise ValueError("ThreePointArc requires three points")
points = [Vector(p) for p in pts]
self.arc = Edge.makeThreePointArc(*points)
Build1DObject.add(context, self.arc, mode=mode)
class TangentArc(Build1DObject):
@property
def object(self) -> Edge:
return self.arc
def __init__(
self,
context: Build1D,
*pts: VectorLike,
tangent: VectorLike,
tangent_from_first: bool = True,
mode: Mode = Mode.ADDITION,
):
arc_pts = [Vector(p) for p in pts]
if len(arc_pts) != 2:
raise ValueError("tangent_arc requires two points")
arc_tangent = Vector(tangent)
point_indices = (0, -1) if tangent_from_first else (-1, 0)
self.arc = Edge.makeTangentArc(
arc_pts[point_indices[0]], arc_tangent, arc_pts[point_indices[1]]
)
Build1DObject.add(context, self.arc, mode=mode)
class RadiusArc(Build1DObject):
@property
def object(self) -> Edge:
return self.arc
def __init__(
self,
context: Build1D,
start_point: VectorLike,
end_point: VectorLike,
radius: float,
mode: Mode = Mode.ADDITION,
):
start = Vector(start_point)
end = Vector(end_point)
# Calculate the sagitta from the radius
length = end.sub(start).Length / 2.0
try:
sagitta = abs(radius) - sqrt(radius**2 - length**2)
except ValueError:
raise ValueError("Arc radius is not large enough to reach the end point.")
# Return a sagitta arc
if radius > 0:
self.arc = SagittaArc(
context, start, end, sagitta, mode=Mode.CONSTRUCTION
).object
else:
self.arc = SagittaArc(
context, start, end, -sagitta, mode=Mode.CONSTRUCTION
).object
Build1DObject.add(context, self.arc, mode=mode)
class SagittaArc(Build1DObject):
@property
def object(self) -> Edge:
return self.arc
def __init__(
self,
context: Build1D,
start_point: VectorLike,
end_point: VectorLike,
sagitta: float,
mode: Mode = Mode.ADDITION,
):
start = Vector(start_point)
end = Vector(end_point)
mid_point = (end + start) * 0.5
sagitta_vector = (end - start).normalized() * abs(sagitta)
if sagitta > 0:
sagitta_vector.x, sagitta_vector.y = (
-sagitta_vector.y,
sagitta_vector.x,
) # Rotate sagitta_vector +90 deg
else:
sagitta_vector.x, sagitta_vector.y = (
sagitta_vector.y,
-sagitta_vector.x,
) # Rotate sagitta_vector -90 deg
sag_point = mid_point + sagitta_vector
self.arc = ThreePointArc(
context, start, sag_point, end, mode=Mode.CONSTRUCTION
).object
Build1DObject.add(context, self.arc, mode=mode)
class MirrorX(Build1DObject):
@property
def object(self) -> Union[Edge, list[Edge]]:
return (
self.mirrored_edges[0]
if len(self.mirrored_edges) == 1
else self.mirrored_edges
)
def __init__(self, context: Build1D, *edges: Edge, mode: Mode = Mode.ADDITION):
self.mirrored_edges = Plane.named("XY").mirrorInPlane(edges, axis="X")
Build1DObject.add(context, *self.mirrored_edges, mode=mode)
class MirrorY(Build1DObject):
@property
def object(self) -> Union[Edge, list[Edge]]:
return (
self.mirrored_edges[0]
if len(self.mirrored_edges) == 1
else self.mirrored_edges
)
def __init__(self, context: Build1D, *edges: Edge, mode: Mode = Mode.ADDITION):
self.mirrored_edges = Plane.named("XY").mirrorInPlane(edges, axis="Y")
Build1DObject.add(context, *self.mirrored_edges, mode=mode)

25
build1d_test.py Normal file
View file

@ -0,0 +1,25 @@
from cadquery import Vector
from build123d_common import *
import build1d as b
with b.Build1D() as ml:
l1 = b.Polyline(ml, (0.0000, 0.0771), (0.0187, 0.0771), (0.0094, 0.2569)).object
l2 = b.Polyline(ml, (0.0325, 0.2773), (0.2115, 0.2458), (0.1873, 0.3125)).object
b.RadiusArc(ml, l1 @ 1, l2 @ 0, 0.0271)
l3 = b.Polyline(ml, (0.1915, 0.3277), (0.3875, 0.4865), (0.3433, 0.5071)).object
b.TangentArc(ml, l2 @ 1, l3 @ 0, tangent=l2 % 1)
l4 = b.Polyline(ml, (0.3362, 0.5235), (0.375, 0.6427), (0.2621, 0.6188)).object
b.SagittaArc(ml, l3 @ 1, l4 @ 0, 0.003)
l5 = b.Polyline(ml, (0.2469, 0.6267), (0.225, 0.6781), (0.1369, 0.5835)).object
b.ThreePointArc(
ml, l4 @ 1, (l4 @ 1 + l5 @ 0) * 0.5 + Vector(-0.002, -0.002), l5 @ 0
)
l6 = b.Polyline(ml, (0.1138, 0.5954), (0.1562, 0.8146), (0.0881, 0.7752)).object
b.Spline(ml, l5 @ 1, l6 @ 0, tangents=(l5 % 1, l6 % 0), tangent_scalars=(2, 2))
l7 = b.Polyline(ml, (0.0692, 0.7808), (0.0000, 0.9167)).object
b.TangentArc(ml, l6 @ 1, l7 @ 0, tangent=l6 % 1)
b.MirrorY(ml, *ml.edges())
if "show_object" in locals():
show_object(ml.edge_list, "maple leaf")

162
build2d.py Normal file
View file

@ -0,0 +1,162 @@
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 *
from build3d import Build3D
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 add(self, *objects: Union[Edge, Face], mode: Mode = Mode.ADDITION):
new_faces = [obj for obj in objects if isinstance(obj, Face)]
new_edges = [obj for obj in objects if isinstance(obj, Edge)]
placed_faces = []
for face in new_faces:
placed_faces.extend(self.place_face(face, mode))
placed_edges = []
if not self.locations:
self.locations = [Location(Vector())]
for location in self.locations:
placed_edges.extend([edge.moved(location) for edge in new_edges])
self.pending_edges.extend(placed_edges)
self.locations = []
placed_objects = placed_faces + placed_edges
return placed_objects[0] if len(placed_objects) == 1 else placed_objects
def push_points(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 text(
self,
txt: str,
fontsize: float,
font: str = "Arial",
font_path: str = None,
font_style: Font_Style = Font_Style.REGULAR,
halign: Halign = Halign.LEFT,
valign: Valign = Valign.CENTER,
path: Union[Edge, Wire] = None,
position_on_path: float = 0.0,
angle: float = 0,
mode: Mode = Mode.ADDITION,
tag: str = None,
) -> Compound:
text_string = Compound.make2DText(
txt,
fontsize,
font,
font_path,
Font_Style.legacy(font_style),
Halign.legacy(halign),
Valign.legacy(valign),
position_on_path,
path,
).rotate(Vector(), Vector(0, 0, 1), angle)
new_faces = self.place_face(text_string, 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.moved(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}")
return new_faces

391
build3d.py Normal file
View file

@ -0,0 +1,391 @@
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 Build3D:
@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.working_volume: 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_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.working_volume.Edges()
elif sort_by == SortBy.X:
edges = sorted(
self.working_volume.Edges(),
key=lambda obj: obj.Center().x,
reverse=reverse,
)
elif sort_by == SortBy.Y:
edges = sorted(
self.working_volume.Edges(),
key=lambda obj: obj.Center().y,
reverse=reverse,
)
elif sort_by == SortBy.Z:
edges = sorted(
self.working_volume.Edges(),
key=lambda obj: obj.Center().z,
reverse=reverse,
)
elif sort_by == SortBy.LENGTH:
edges = sorted(
self.working_volume.Edges(),
key=lambda obj: obj.Length(),
reverse=reverse,
)
elif sort_by == SortBy.RADIUS:
edges = sorted(
self.working_volume.Edges(),
key=lambda obj: obj.radius(),
reverse=reverse,
)
elif sort_by == SortBy.DISTANCE:
edges = sorted(
self.working_volume.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_volume.Faces()
elif sort_by == SortBy.X:
faces = sorted(
self.working_volume.Faces(),
key=lambda obj: obj.Center().x,
reverse=reverse,
)
elif sort_by == SortBy.Y:
faces = sorted(
self.working_volume.Faces(),
key=lambda obj: obj.Center().y,
reverse=reverse,
)
elif sort_by == SortBy.Z:
faces = sorted(
self.working_volume.Faces(),
key=lambda obj: obj.Center().z,
reverse=reverse,
)
elif sort_by == SortBy.AREA:
faces = sorted(
self.working_volume.Faces(), key=lambda obj: obj.Area(), reverse=reverse
)
elif sort_by == SortBy.DISTANCE:
faces = sorted(
self.working_volume.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_volume.Vertices()
elif sort_by == SortBy.X:
vertices = sorted(
self.working_volume.Vertices(),
key=lambda obj: obj.Center().x,
reverse=reverse,
)
elif sort_by == SortBy.Y:
vertices = sorted(
self.working_volume.Vertices(),
key=lambda obj: obj.Center().y,
reverse=reverse,
)
elif sort_by == SortBy.Z:
vertices = sorted(
self.working_volume.Vertices(),
key=lambda obj: obj.Center().z,
reverse=reverse,
)
elif sort_by == SortBy.DISTANCE:
vertices = sorted(
self.working_volume.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_volume is None
else set(self.working_volume.Vertices())
)
before_edges = (
set() if self.working_volume is None else set(self.working_volume.Edges())
)
before_faces = (
set() if self.working_volume is None else set(self.working_volume.Faces())
)
if mode == Mode.ADDITION:
if self.working_volume is None:
if len(new_solids) == 1:
self.working_volume = new_solids[0]
else:
self.working_volume = new_solids.pop().fuse(*new_solids)
else:
self.working_volume = self.working_volume.fuse(*new_solids).clean_op()
elif mode == Mode.SUBTRACTION:
if self.working_volume is None:
raise ValueError("Nothing to subtract from")
self.working_volume = self.working_volume.cut(*new_solids).clean_op()
elif mode == Mode.INTERSECTION:
if self.working_volume is None:
raise ValueError("Nothing to intersect with")
self.working_volume = self.working_volume.intersect(*new_solids).clean_op()
self.last_operation[CqObject.VERTEX] = list(
set(self.working_volume.Vertices()) - before_vertices
)
self.last_operation[CqObject.EDGE] = list(
set(self.working_volume.Edges()) - before_edges
)
self.last_operation[CqObject.FACE] = list(
set(self.working_volume.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.working_volume = self.working_volume.fillet(radius, [e for e in edges])
def chamfer(self, *edges: Sequence[Edge], length1: float, length2: float = None):
self.working_volume = self.working_volume.chamfer(length1, length2, list(edges))

47
cq3.py
View file

@ -1093,6 +1093,53 @@ with Build1D() as ml:
# # f3.circle(next(radii))
# s3.loft()
class ThreePointArc(Edge):
@property
def edge(self) -> Edge:
return self.arc
def __init__(self, context: Build1D, *pts: VectorLike):
# def __init__(self, *pts: VectorLike):
self.context = context
self.points = [Vector(p) for p in pts]
if len(self.points) != 3:
raise ValueError("ThreePointArc requires three points")
self.arc = Edge.makeThreePointArc(*self.points)
if context is not None:
context.edge_list.append(self.arc)
# def __call__(self, context: Build1D) -> Edge:
# # def __call__(self) -> Edge:
# return self.arc
def three_point_arc(context: Build1D, *pts: VectorLike) -> Edge:
points = [Vector(p) for p in pts]
if len(points) != 3:
raise ValueError("ThreePointArc requires three points")
arc = Edge.makeThreePointArc(*points)
if context is not None:
context.edge_list.append(arc)
return arc
with Build1D() as b1:
three_point_arc(b1, (0, 0), (1, 0), (0, 1))
a1 = three_point_arc(b1, (0, 0), (1, 0), (0, 1))
print(f"{type(a1)=}")
c1 = Build1D()
# e = ThreePointArc((0, 0), (1, 0), (0, 1))(c1)
e2 = ThreePointArc(c1, (0, 0), (1, 0), (0, 1)).edge
# print(type(e))
# print(e @ 1)
print(c1.edge_list)
print(type(e2))
if "show_object" in locals():
# show_object(s2.pending_faces[0], "pending_face")
# show_object(rect, name="rect")