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

543 lines
17 KiB
Python

"""
BuildLine
name: build_line.py
by: Gumyr
date: July 12th 2022
desc:
This python module is a library used to build lines in three dimensional space.
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 sin, cos, radians, sqrt
from typing import Union, Iterable
from cadquery import (
Edge,
Wire,
Vector,
Vertex,
Plane,
)
from cadquery.occ_impl.shapes import VectorLike
import cq_warehouse.extensions
from build123d_common import *
from build_sketch import BuildSketch
from build_part import BuildPart
class BuildLine:
"""BuildLine
Create lines (objects with length but not area or volume) from edges or wires.
Args:
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
"""
@property
def line_as_wire(self) -> Union[Wire, list[Wire]]:
"""Unify edges into one or more Wires"""
wires = Wire.combine(self.line)
return wires if len(wires) > 1 else wires[0]
def __init__(self, mode: Mode = Mode.ADDITION):
self.line = []
self.tags: dict[str, Edge] = {}
self.mode = mode
self.last_vertices = []
self.last_edges = []
def __enter__(self):
"""Upon entering BuildLine, add current BuildLine instance to context stack"""
context_stack.append(self)
return self
def __exit__(self, exception_type, exception_value, traceback):
"""Upon exiting BuildLine, transfer sketch to parent BuildSketch | BuildPart if available"""
context_stack.pop()
if context_stack:
if isinstance(context_stack[-1], BuildSketch):
BuildSketch.get_context().add_to_context(*self.line, mode=self.mode)
elif isinstance(context_stack[-1], BuildPart):
BuildPart.get_context().add_to_context(*self.line, mode=self.mode)
def vertices(self, select: Select = Select.ALL) -> list[Vertex]:
"""Return Vertices from Line
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
"""
if select == Select.ALL:
vertex_list = []
for edge in self.line:
vertex_list.extend(edge.Vertices())
vertex_list = list(set(vertex_list))
elif select == Select.LAST:
vertex_list = self.last_vertices
return vertex_list
def edges(self, select: Select = Select.ALL) -> list[Edge]:
"""Return Edges from Line
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.line
elif select == Select.LAST:
edge_list = self.last_edges
return edge_list
@staticmethod
def add_to_context(*edges: Edge, mode: Mode = Mode.ADDITION):
"""Add objects to BuildSketch instance
Core method to interface with BuildLine instance. Input sequence of edges are
combined with current line.
Each operation generates a list of vertices and edges 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:
edges (Edge): sequence of edges to add
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
"""
if context_stack and mode != Mode.PRIVATE:
for edge in edges:
edge.forConstruction = mode == Mode.CONSTRUCTION
context_stack[-1].line.append(edge)
context_stack[-1].last_edges = edges
context_stack[-1].last_vertices = list(
set(v for e in edges for v in e.Vertices())
)
@staticmethod
def get_context() -> "BuildLine":
"""Return the current BuildLine instance. Used by Object and Operation
classes to refer to the current context."""
return context_stack[-1]
#
# Operations
#
class MirrorToLine:
"""Line Operation: Mirror
Add the mirror of the provided sequence of edges about the given axis to line.
Args:
edges (Edge): sequence of edges to mirror
axis (Axis, optional): axis to mirror about. Defaults to Axis.X.
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
"""
def __init__(self, *edges: Edge, axis: Axis = Axis.X, mode: Mode = Mode.ADDITION):
mirrored_edges = Plane.named("XY").mirrorInPlane(edges, axis=axis.name)
BuildLine.add_to_context(*mirrored_edges, mode=mode)
#
# Objects
#
class CenterArc(Edge):
"""Line Object: Center Arc
Add center arc to the line.
Args:
center (VectorLike): center point of arc
radius (float): arc radius
start_angle (float): arc staring angle
arc_size (float): arc size
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
"""
def __init__(
self,
center: VectorLike,
radius: float,
start_angle: float,
arc_size: float,
mode: Mode = Mode.ADDITION,
):
points = []
if abs(arc_size) >= 360:
arc = Edge.makeCircle(
radius,
center,
angle1=start_angle,
angle2=start_angle,
orientation=arc_size > 0,
)
else:
center_point = Vector(center)
points.append(
center_point
+ radius * Vector(cos(radians(start_angle)), sin(radians(start_angle)))
)
points.append(
center_point
+ radius
* Vector(
cos(radians(start_angle + arc_size / 2)),
sin(radians(start_angle + arc_size / 2)),
)
)
points.append(
center_point
+ radius
* Vector(
cos(radians(start_angle + arc_size)),
sin(radians(start_angle + arc_size)),
)
)
arc = Edge.makeThreePointArc(*points)
BuildLine.add_to_context(arc, mode=mode)
super().__init__(arc.wrapped)
class Helix(Wire):
"""Line Object: Helix
Add a helix to the line.
Args:
pitch (float): distance between successive loops
height (float): helix size
radius (float): helix radius
center (VectorLike, optional): center point. Defaults to (0, 0, 0).
direction (VectorLike, optional): direction of central axis. Defaults to (0, 0, 1).
arc_size (float, optional): rotational angle. Defaults to 360.
lefhand (bool, optional): left handed helix. Defaults to False.
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
"""
def __init__(
self,
pitch: float,
height: float,
radius: float,
center: VectorLike = (0, 0, 0),
direction: VectorLike = (0, 0, 1),
arc_size: float = 360,
lefhand: bool = False,
mode: Mode = Mode.ADDITION,
):
helix = Wire.makeHelix(
pitch, height, radius, Vector(center), Vector(direction), arc_size, lefhand
)
BuildLine.add_to_context(*helix.Edges(), mode=mode)
super().__init__(helix.wrapped)
class Line(Edge):
"""Line Object: Line
Add a straight line defined by two end points.
Args:
pts (VectorLike): sequence of two points
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
Raises:
ValueError: Two point not provided
"""
def __init__(self, *pts: VectorLike, mode: Mode = Mode.ADDITION):
if len(pts) != 2:
raise ValueError("Line requires two pts")
lines_pts = [Vector(p) for p in pts]
new_edge = Edge.makeLine(lines_pts[0], lines_pts[1])
BuildLine.add_to_context(new_edge, mode=mode)
super().__init__(new_edge.wrapped)
class PolarLine(Edge):
"""Line Object: Polar Line
Add line defined by a start point, length and angle.
Args:
start (VectorLike): start point
length (float): line length
angle (float, optional): angle from +v X axis. Defaults to None.
direction (VectorLike, optional): vector direction. Defaults to None.
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
Raises:
ValueError: Either angle or direction must be provided
"""
def __init__(
self,
start: VectorLike,
length: float,
angle: float = None,
direction: VectorLike = None,
mode: Mode = Mode.ADDITION,
):
if angle is not None:
x = cos(radians(angle)) * length
y = sin(radians(angle)) * length
new_edge = Edge.makeLine(Vector(start), Vector(start) + Vector(x, y, 0))
elif direction is not None:
new_edge = Edge.makeLine(
Vector(start), Vector(start) + Vector(direction).normalized() * length
)
else:
raise ValueError("Either angle or direction must be provided")
BuildLine.add_to_context(new_edge, mode=mode)
super().__init__(new_edge.wrapped)
class Polyline(Wire):
"""Line Object: Polyline
Add a sequence of straight lines defined by successive point pairs.
Args:
pts (VectorLike): sequence of three or more points
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
Raises:
ValueError: Three or more points not provided
"""
def __init__(self, *pts: VectorLike, mode: Mode = Mode.ADDITION):
if len(pts) < 3:
raise ValueError("polyline requires three 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)
]
BuildLine.add_to_context(*new_edges, mode=mode)
super().__init__(Wire.combine(new_edges)[0].wrapped)
class RadiusArc(Edge):
"""Line Object: Radius Arc
Add an arc defined by two end points and a radius
Args:
start_point (VectorLike): start
end_point (VectorLike): end
radius (float): radius
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
Raises:
ValueError: Insufficient radius to connect end points
"""
def __init__(
self,
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 as e:
raise ValueError(
"Arc radius is not large enough to reach the end point."
) from e
# Return a sagitta arc
if radius > 0:
arc = SagittaArc(start, end, sagitta, mode=Mode.PRIVATE)
else:
arc = SagittaArc(start, end, -sagitta, mode=Mode.PRIVATE)
BuildLine.add_to_context(arc, mode=mode)
super().__init__(arc.wrapped)
class SagittaArc(Edge):
"""Line Object: Sagitta Arc
Add an arc defined by two points and the height of the arc (sagitta).
Args:
start_point (VectorLike): start
end_point (VectorLike): end
sagitta (float): arc height
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
"""
def __init__(
self,
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
arc = ThreePointArc(start, sag_point, end, mode=Mode.PRIVATE)
BuildLine.add_to_context(arc, mode=mode)
super().__init__(arc.wrapped)
class Spline(Edge):
"""Line Object: Spline
Add a spline through the provided points optionally constrained by tangents.
Args:
pts (VectorLike): sequence of two or more points
tangents (Iterable[VectorLike], optional): tangents at end points. Defaults to None.
tangent_scalars (Iterable[float], optional): change shape by amplifying tangent.
Defaults to None.
periodic (bool, optional): make the spline periodic. Defaults to False.
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
"""
def __init__(
self,
*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
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,
)
BuildLine.add_to_context(spline, mode=mode)
super().__init__(spline.wrapped)
class TangentArc(Edge):
"""Line Object: Tangent Arc
Add an arc defined by two points and a tangent.
Args:
pts (VectorLike): sequence of two points
tangent (VectorLike): tanget to constrain arc
tangent_from_first (bool, optional): apply tangent to first point. Note, applying
tangent to end point will flip the orientation of the arc. Defaults to True.
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
Raises:
ValueError: Two points are required
"""
def __init__(
self,
*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)
arc = Edge.makeTangentArc(
arc_pts[point_indices[0]], arc_tangent, arc_pts[point_indices[1]]
)
BuildLine.add_to_context(arc, mode=mode)
super().__init__(arc.wrapped)
class ThreePointArc(Edge):
"""Line Object: Three Point Arc
Add an arc generated by three points.
Args:
pts (VectorLike): sequence of three points
mode (Mode, optional): combination mode. Defaults to Mode.ADDITION.
Raises:
ValueError: Three points must be provided
"""
def __init__(self, *pts: VectorLike, mode: Mode = Mode.ADDITION):
if len(pts) != 3:
raise ValueError("ThreePointArc requires three points")
points = [Vector(p) for p in pts]
arc = Edge.makeThreePointArc(*points)
BuildLine.add_to_context(arc, mode=mode)
super().__init__(arc.wrapped)