mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
543 lines
17 KiB
Python
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)
|