mirror of
https://github.com/gumyr/build123d.git
synced 2026-03-10 08:42:06 -07:00
Merge branch 'gumyr:dev' into publishPyPI
This commit is contained in:
commit
2fcaecb673
11 changed files with 289 additions and 82 deletions
|
|
@ -103,6 +103,30 @@ G = 1
|
|||
KG = 1000 * G
|
||||
LB = 453.59237 * G
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def flatten_sequence(*obj: T) -> list[T]:
|
||||
"""Convert a sequence of object potentially containing iterables into a flat list"""
|
||||
|
||||
def is_point(obj):
|
||||
"""Identify points as tuples of numbers"""
|
||||
return isinstance(obj, tuple) and all(
|
||||
isinstance(item, (int, float)) for item in obj
|
||||
)
|
||||
|
||||
flat_list = []
|
||||
for item in obj:
|
||||
# Note: an Iterable can't be used here as it will match with Vector & Vertex
|
||||
# and break them into a list of floats.
|
||||
if isinstance(item, (list, tuple, filter, set)) and not is_point(item):
|
||||
flat_list.extend(item)
|
||||
else:
|
||||
flat_list.append(item)
|
||||
|
||||
return flat_list
|
||||
|
||||
|
||||
operations_apply_to = {
|
||||
"add": ["BuildPart", "BuildSketch", "BuildLine"],
|
||||
"bounding_box": ["BuildPart", "BuildSketch", "BuildLine"],
|
||||
|
|
@ -942,16 +966,28 @@ class Locations(LocationList):
|
|||
Creates a context of locations for Part or Sketch
|
||||
|
||||
Args:
|
||||
pts (Union[VectorLike, Vertex, Location]): sequence of points to push
|
||||
pts (Union[VectorLike, Vertex, Location, Face, Plane, Axis] or iterable of same):
|
||||
sequence of points to push
|
||||
|
||||
Attributes:
|
||||
local_locations (list{Location}): locations relative to workplane
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *pts: Union[VectorLike, Vertex, Location, Face, Plane, Axis]):
|
||||
def __init__(
|
||||
self,
|
||||
*pts: Union[
|
||||
VectorLike,
|
||||
Vertex,
|
||||
Location,
|
||||
Face,
|
||||
Plane,
|
||||
Axis,
|
||||
Iterable[VectorLike, Vertex, Location, Face, Plane, Axis],
|
||||
],
|
||||
):
|
||||
local_locations = []
|
||||
for point in pts:
|
||||
for point in flatten_sequence(*pts):
|
||||
if isinstance(point, Location):
|
||||
local_locations.append(point)
|
||||
elif isinstance(point, Vector):
|
||||
|
|
@ -1108,6 +1144,7 @@ class WorkplaneList:
|
|||
@staticmethod
|
||||
def _convert_to_planes(objs: Iterable[Union[Face, Plane, Location]]) -> list[Plane]:
|
||||
"""Translate objects to planes"""
|
||||
objs = flatten_sequence(*objs)
|
||||
planes = []
|
||||
for obj in objs:
|
||||
if isinstance(obj, Plane):
|
||||
|
|
@ -1186,7 +1223,6 @@ class WorkplaneList:
|
|||
return result
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ class BuildLine(Builder):
|
|||
):
|
||||
self.line: Curve = None
|
||||
super().__init__(workplane, mode=mode)
|
||||
if len(self.workplanes) > 1:
|
||||
raise ValueError("BuildLine only accepts one workplane")
|
||||
|
||||
def __exit__(self, exception_type, exception_value, traceback):
|
||||
"""Upon exiting restore context and send object to parent"""
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import copy
|
|||
from math import copysign, cos, radians, sin, sqrt
|
||||
from typing import Iterable, Union
|
||||
|
||||
from build123d.build_common import WorkplaneList, validate_inputs
|
||||
from build123d.build_common import WorkplaneList, flatten_sequence, validate_inputs
|
||||
from build123d.build_enums import AngularDirection, GeomType, LengthMode, Mode
|
||||
from build123d.build_line import BuildLine
|
||||
from build123d.geometry import Axis, Plane, Vector, VectorLike
|
||||
|
|
@ -90,6 +90,7 @@ class Bezier(BaseLineObject):
|
|||
context: BuildLine = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
cntl_pnts = flatten_sequence(*cntl_pnts)
|
||||
polls = WorkplaneList.localize(*cntl_pnts)
|
||||
curve = Edge.make_bezier(*polls, weights=weights)
|
||||
|
||||
|
|
@ -353,7 +354,7 @@ class FilletPolyline(BaseLineObject):
|
|||
are filleted to a given radius.
|
||||
|
||||
Args:
|
||||
pts (VectorLike): sequence of three or more points
|
||||
pts (Union[VectorLike, Iterable[VectorLike]]): sequence of three or more points
|
||||
radius (float): radius of filleted corners
|
||||
close (bool, optional): close by generating an extra Edge. Defaults to False.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
|
|
@ -367,7 +368,7 @@ class FilletPolyline(BaseLineObject):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*pts: VectorLike,
|
||||
*pts: Union[VectorLike, Iterable[VectorLike]],
|
||||
radius: float,
|
||||
close: bool = False,
|
||||
mode: Mode = Mode.ADD,
|
||||
|
|
@ -375,6 +376,8 @@ class FilletPolyline(BaseLineObject):
|
|||
context: BuildLine = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
pts = flatten_sequence(*pts)
|
||||
|
||||
if len(pts) < 3:
|
||||
raise ValueError("filletpolyline requires three or more pts")
|
||||
if radius <= 0:
|
||||
|
|
@ -506,7 +509,7 @@ class Line(BaseLineObject):
|
|||
Add a straight line defined by two end points.
|
||||
|
||||
Args:
|
||||
pts (VectorLike): sequence of two points
|
||||
pts (Union[VectorLike, Iterable[VectorLike]]): sequence of two points
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
|
||||
Raises:
|
||||
|
|
@ -515,7 +518,10 @@ class Line(BaseLineObject):
|
|||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
||||
def __init__(self, *pts: VectorLike, mode: Mode = Mode.ADD):
|
||||
def __init__(
|
||||
self, *pts: Union[VectorLike, Iterable[VectorLike]], mode: Mode = Mode.ADD
|
||||
):
|
||||
pts = flatten_sequence(*pts)
|
||||
if len(pts) != 2:
|
||||
raise ValueError("Line requires two pts")
|
||||
|
||||
|
|
@ -637,7 +643,7 @@ class Polyline(BaseLineObject):
|
|||
Add a sequence of straight lines defined by successive point pairs.
|
||||
|
||||
Args:
|
||||
pts (VectorLike): sequence of three or more points
|
||||
pts (Union[VectorLike, Iterable[VectorLike]]): sequence of three or more points
|
||||
close (bool, optional): close by generating an extra Edge. Defaults to False.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
|
||||
|
|
@ -647,10 +653,16 @@ class Polyline(BaseLineObject):
|
|||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
||||
def __init__(self, *pts: VectorLike, close: bool = False, mode: Mode = Mode.ADD):
|
||||
def __init__(
|
||||
self,
|
||||
*pts: Union[VectorLike, Iterable[VectorLike]],
|
||||
close: bool = False,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
context: BuildLine = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
pts = flatten_sequence(*pts)
|
||||
if len(pts) < 3:
|
||||
raise ValueError("polyline requires three or more pts")
|
||||
|
||||
|
|
@ -766,7 +778,7 @@ class Spline(BaseLineObject):
|
|||
Add a spline through the provided points optionally constrained by tangents.
|
||||
|
||||
Args:
|
||||
pts (VectorLike): sequence of two or more points
|
||||
pts (Union[VectorLike, Iterable[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.
|
||||
|
|
@ -778,12 +790,13 @@ class Spline(BaseLineObject):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*pts: VectorLike,
|
||||
*pts: Union[VectorLike, Iterable[VectorLike]],
|
||||
tangents: Iterable[VectorLike] = None,
|
||||
tangent_scalars: Iterable[float] = None,
|
||||
periodic: bool = False,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
pts = flatten_sequence(*pts)
|
||||
context: BuildLine = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
|
|
@ -821,7 +834,7 @@ class TangentArc(BaseLineObject):
|
|||
Add an arc defined by two points and a tangent.
|
||||
|
||||
Args:
|
||||
pts (VectorLike): sequence of two points
|
||||
pts (Union[VectorLike, Iterable[VectorLike]]): sequence of two points
|
||||
tangent (VectorLike): tangent 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.
|
||||
|
|
@ -835,11 +848,12 @@ class TangentArc(BaseLineObject):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*pts: VectorLike,
|
||||
*pts: Union[VectorLike, Iterable[VectorLike]],
|
||||
tangent: VectorLike,
|
||||
tangent_from_first: bool = True,
|
||||
mode: Mode = Mode.ADD,
|
||||
):
|
||||
pts = flatten_sequence(*pts)
|
||||
context: BuildLine = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
|
|
@ -862,7 +876,7 @@ class ThreePointArc(BaseLineObject):
|
|||
Add an arc generated by three points.
|
||||
|
||||
Args:
|
||||
pts (VectorLike): sequence of three points
|
||||
pts (Union[VectorLike, Iterable[VectorLike]]): sequence of three points
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
|
||||
Raises:
|
||||
|
|
@ -871,10 +885,13 @@ class ThreePointArc(BaseLineObject):
|
|||
|
||||
_applies_to = [BuildLine._tag]
|
||||
|
||||
def __init__(self, *pts: VectorLike, mode: Mode = Mode.ADD):
|
||||
def __init__(
|
||||
self, *pts: Union[VectorLike, Iterable[VectorLike]], mode: Mode = Mode.ADD
|
||||
):
|
||||
context: BuildLine = BuildLine._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
pts = flatten_sequence(*pts)
|
||||
if len(pts) != 3:
|
||||
raise ValueError("ThreePointArc requires three points")
|
||||
points = WorkplaneList.localize(*pts)
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ license:
|
|||
from __future__ import annotations
|
||||
|
||||
from math import cos, pi, radians, sin, tan
|
||||
from typing import Union
|
||||
from typing import Iterable, Union
|
||||
|
||||
from build123d.build_common import LocationList, validate_inputs
|
||||
from build123d.build_common import LocationList, flatten_sequence, validate_inputs
|
||||
from build123d.build_enums import Align, FontStyle, Mode
|
||||
from build123d.build_sketch import BuildSketch
|
||||
from build123d.geometry import Axis, Location, Rotation, Vector, VectorLike
|
||||
|
|
@ -155,7 +155,8 @@ class Polygon(BaseSketchObject):
|
|||
Add polygon(s) defined by given sequence of points to sketch.
|
||||
|
||||
Args:
|
||||
pts (VectorLike): sequence of points defining the vertices of polygon
|
||||
pts (Union[VectorLike, Iterable[VectorLike]]): sequence of points defining the
|
||||
vertices of polygon
|
||||
rotation (float, optional): angles to rotate objects. Defaults to 0.
|
||||
align (Union[Align, tuple[Align, Align]], optional): align min, center, or max of object.
|
||||
Defaults to (Align.CENTER, Align.CENTER).
|
||||
|
|
@ -166,7 +167,7 @@ class Polygon(BaseSketchObject):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*pts: VectorLike,
|
||||
*pts: Union[VectorLike, Iterable[VectorLike]],
|
||||
rotation: float = 0,
|
||||
align: Union[Align, tuple[Align, Align]] = (Align.CENTER, Align.CENTER),
|
||||
mode: Mode = Mode.ADD,
|
||||
|
|
@ -174,6 +175,7 @@ class Polygon(BaseSketchObject):
|
|||
context = BuildSketch._get_context(self)
|
||||
validate_inputs(context, self)
|
||||
|
||||
pts = flatten_sequence(*pts)
|
||||
self.pts = pts
|
||||
self.align = tuplify(align, 2)
|
||||
|
||||
|
|
@ -495,7 +497,7 @@ class SlotOverall(BaseSketchObject):
|
|||
).offset_2d(height / 2)
|
||||
)
|
||||
else:
|
||||
face = Circle(width/2, mode=mode).face()
|
||||
face = Circle(width / 2, mode=mode).face()
|
||||
super().__init__(face, rotation, align, mode)
|
||||
|
||||
|
||||
|
|
@ -518,6 +520,7 @@ class Text(BaseSketchObject):
|
|||
rotation (float, optional): angles to rotate objects. Defaults to 0.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
_applies_to = [BuildSketch._tag]
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,13 @@ import logging
|
|||
from math import radians, tan
|
||||
from typing import Union, Iterable
|
||||
|
||||
from build123d.build_common import Builder, LocationList, WorkplaneList, validate_inputs
|
||||
from build123d.build_common import (
|
||||
Builder,
|
||||
LocationList,
|
||||
WorkplaneList,
|
||||
flatten_sequence,
|
||||
validate_inputs,
|
||||
)
|
||||
from build123d.build_enums import Keep, Kind, Mode, Side, Transition
|
||||
from build123d.build_line import BuildLine
|
||||
from build123d.build_part import BuildPart
|
||||
|
|
@ -206,9 +212,8 @@ def bounding_box(
|
|||
raise ValueError("objects must be provided")
|
||||
object_list = [context._obj]
|
||||
else:
|
||||
object_list = (
|
||||
[*objects] if isinstance(objects, (list, tuple, filter)) else [objects]
|
||||
)
|
||||
object_list = flatten_sequence(objects)
|
||||
|
||||
validate_inputs(context, "bounding_box", object_list)
|
||||
|
||||
if all([obj._dim == 2 for obj in object_list]):
|
||||
|
|
@ -300,9 +305,7 @@ def chamfer(
|
|||
):
|
||||
raise ValueError("No objects provided")
|
||||
|
||||
object_list = (
|
||||
[*objects] if isinstance(objects, (list, tuple, filter)) else [objects]
|
||||
)
|
||||
object_list = flatten_sequence(objects)
|
||||
|
||||
validate_inputs(context, "chamfer", object_list)
|
||||
|
||||
|
|
@ -397,9 +400,8 @@ def fillet(
|
|||
):
|
||||
raise ValueError("No objects provided")
|
||||
|
||||
object_list = (
|
||||
[*objects] if isinstance(objects, (list, tuple, filter)) else [objects]
|
||||
)
|
||||
object_list = flatten_sequence(objects)
|
||||
|
||||
validate_inputs(context, "fillet", object_list)
|
||||
if context is not None:
|
||||
target = context._obj
|
||||
|
|
@ -495,9 +497,8 @@ def mirror(
|
|||
raise ValueError("objects must be provided")
|
||||
object_list = [context._obj]
|
||||
else:
|
||||
object_list = (
|
||||
[*objects] if isinstance(objects, (list, tuple, filter)) else [objects]
|
||||
)
|
||||
object_list = flatten_sequence(objects)
|
||||
|
||||
validate_inputs(context, "mirror", object_list)
|
||||
|
||||
mirrored = [copy.deepcopy(o).mirror(about) for o in object_list]
|
||||
|
|
@ -562,9 +563,8 @@ def offset(
|
|||
raise ValueError("objects must be provided")
|
||||
object_list = [context._obj]
|
||||
else:
|
||||
object_list = (
|
||||
[*objects] if isinstance(objects, (list, tuple, filter)) else [objects]
|
||||
)
|
||||
object_list = flatten_sequence(objects)
|
||||
|
||||
validate_inputs(context, "offset", object_list)
|
||||
|
||||
edges: list[Edge] = []
|
||||
|
|
@ -701,9 +701,7 @@ def project(
|
|||
else:
|
||||
workplane = context.exit_workplanes[0]
|
||||
else:
|
||||
object_list = (
|
||||
[*objects] if isinstance(objects, (list, tuple, filter)) else [objects]
|
||||
)
|
||||
object_list = flatten_sequence(objects)
|
||||
|
||||
# The size of the object determines the size of the target projection screen
|
||||
# as the screen is normal to the direction of parallel projection
|
||||
|
|
@ -826,9 +824,8 @@ def scale(
|
|||
raise ValueError("objects must be provided")
|
||||
object_list = [context._obj]
|
||||
else:
|
||||
object_list = (
|
||||
[*objects] if isinstance(objects, (list, tuple, filter)) else [objects]
|
||||
)
|
||||
object_list = flatten_sequence(objects)
|
||||
|
||||
validate_inputs(context, "scale", object_list)
|
||||
|
||||
if isinstance(by, (int, float)):
|
||||
|
|
@ -907,9 +904,8 @@ def split(
|
|||
raise ValueError("objects must be provided")
|
||||
object_list = [context._obj]
|
||||
else:
|
||||
object_list = (
|
||||
[*objects] if isinstance(objects, (list, tuple, filter)) else [objects]
|
||||
)
|
||||
object_list = flatten_sequence(objects)
|
||||
|
||||
validate_inputs(context, "split", object_list)
|
||||
|
||||
new_objects = []
|
||||
|
|
|
|||
|
|
@ -44,13 +44,18 @@ from build123d.topology import (
|
|||
Vertex,
|
||||
)
|
||||
|
||||
from build123d.build_common import logger, WorkplaneList, validate_inputs
|
||||
from build123d.build_common import (
|
||||
logger,
|
||||
WorkplaneList,
|
||||
flatten_sequence,
|
||||
validate_inputs,
|
||||
)
|
||||
|
||||
|
||||
def extrude(
|
||||
to_extrude: Union[Face, Sketch] = None,
|
||||
amount: float = None,
|
||||
dir: VectorLike = None, # pylint: disable=redefined-builtin
|
||||
dir: VectorLike = None, # pylint: disable=redefined-builtin
|
||||
until: Until = None,
|
||||
target: Union[Compound, Solid] = None,
|
||||
both: bool = False,
|
||||
|
|
@ -175,7 +180,7 @@ def extrude(
|
|||
|
||||
|
||||
def loft(
|
||||
sections: Union[Face, Iterable[Face]] = None,
|
||||
sections: Union[Face, Sketch, Iterable[Union[Vertex, Face, Sketch]]] = None,
|
||||
ruled: bool = False,
|
||||
clean: bool = True,
|
||||
mode: Mode = Mode.ADD,
|
||||
|
|
@ -185,17 +190,16 @@ def loft(
|
|||
Loft the pending sketches/faces, across all workplanes, into a solid.
|
||||
|
||||
Args:
|
||||
sections (Face): slices to loft into object. If not provided, pending_faces
|
||||
will be used.
|
||||
sections (Vertex, Face, Sketch): slices to loft into object. If not provided, pending_faces
|
||||
will be used. If vertices are to be used, a vertex can be the first, last, or
|
||||
first and last elements.
|
||||
ruled (bool, optional): discontiguous layer tangents. Defaults to False.
|
||||
clean (bool, optional): Remove extraneous internal structure. Defaults to True.
|
||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD.
|
||||
"""
|
||||
context: BuildPart = BuildPart._get_context("loft")
|
||||
|
||||
section_list = (
|
||||
[*sections] if isinstance(sections, (list, tuple, filter)) else [sections]
|
||||
)
|
||||
section_list = flatten_sequence(sections)
|
||||
validate_inputs(context, "loft", section_list)
|
||||
|
||||
if all([s is None for s in section_list]):
|
||||
|
|
@ -205,9 +209,32 @@ def loft(
|
|||
context.pending_faces = []
|
||||
context.pending_face_planes = []
|
||||
else:
|
||||
loft_wires = [
|
||||
face.outer_wire() for section in section_list for face in section.faces()
|
||||
]
|
||||
if all(isinstance(s, (Face, Sketch)) for s in section_list):
|
||||
loft_wires = [
|
||||
face.outer_wire()
|
||||
for section in section_list
|
||||
for face in section.faces()
|
||||
]
|
||||
elif any(isinstance(s, Vertex) for s in section_list) and any(
|
||||
isinstance(s, (Face, Sketch)) for s in section_list
|
||||
):
|
||||
if any(isinstance(s, Vertex) for s in section_list[1:-1]):
|
||||
raise ValueError(
|
||||
"Vertices must be the first, last, or first and last elements"
|
||||
)
|
||||
loft_wires = []
|
||||
for s in section_list:
|
||||
if isinstance(s, Vertex):
|
||||
loft_wires.append(s)
|
||||
elif isinstance(s, Face):
|
||||
loft_wires.append(s.outer_wire())
|
||||
elif isinstance(s, Sketch):
|
||||
loft_wires.append(s.face().outer_wire())
|
||||
elif all(isinstance(s, Vertex) for s in section_list):
|
||||
raise ValueError(
|
||||
"At least one face/sketch is required if vertices are the first, last, or first and last elements"
|
||||
)
|
||||
|
||||
new_solid = Solid.make_loft(loft_wires, ruled)
|
||||
|
||||
# Try to recover an invalid loft
|
||||
|
|
@ -411,9 +438,8 @@ def revolve(
|
|||
"""
|
||||
context: BuildPart = BuildPart._get_context("revolve")
|
||||
|
||||
profile_list = (
|
||||
[*profiles] if isinstance(profiles, (list, tuple, filter)) else [profiles]
|
||||
)
|
||||
profile_list = flatten_sequence(profiles)
|
||||
|
||||
validate_inputs(context, "revolve", profile_list)
|
||||
|
||||
# Make sure we account for users specifying angles larger than 360 degrees, and
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ from __future__ import annotations
|
|||
from typing import Iterable, Union
|
||||
from build123d.build_enums import Mode
|
||||
from build123d.topology import Compound, Curve, Edge, Face, ShapeList, Wire, Sketch
|
||||
from build123d.build_common import validate_inputs
|
||||
from build123d.build_common import flatten_sequence, validate_inputs
|
||||
from build123d.build_sketch import BuildSketch
|
||||
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ def make_face(
|
|||
context: BuildSketch = BuildSketch._get_context("make_face")
|
||||
|
||||
if edges is not None:
|
||||
outer_edges = [*edges] if isinstance(edges, (list, tuple, filter)) else [edges]
|
||||
outer_edges = flatten_sequence(edges)
|
||||
elif context is not None:
|
||||
outer_edges = context.pending_edges
|
||||
else:
|
||||
|
|
@ -84,7 +84,7 @@ def make_hull(
|
|||
context: BuildSketch = BuildSketch._get_context("make_hull")
|
||||
|
||||
if edges is not None:
|
||||
hull_edges = [*edges] if isinstance(edges, (list, tuple, filter)) else [edges]
|
||||
hull_edges = flatten_sequence(edges)
|
||||
elif context is not None:
|
||||
hull_edges = context.pending_edges
|
||||
if context.sketch_local is not None:
|
||||
|
|
@ -131,7 +131,7 @@ def trace(
|
|||
context: BuildSketch = BuildSketch._get_context("trace")
|
||||
|
||||
if lines is not None:
|
||||
trace_lines = [*lines] if isinstance(lines, (list, tuple, filter)) else [lines]
|
||||
trace_lines = flatten_sequence(lines)
|
||||
trace_edges = [e for l in trace_lines for e in l.edges()]
|
||||
elif context is not None:
|
||||
trace_edges = context.pending_edges
|
||||
|
|
|
|||
|
|
@ -1390,6 +1390,7 @@ class Shape(NodeMixin):
|
|||
topo_parent (Shape): assembly parent of this object
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-public-methods
|
||||
|
||||
_dim = None
|
||||
|
|
@ -1763,7 +1764,7 @@ class Shape(NodeMixin):
|
|||
try:
|
||||
upgrader.Build()
|
||||
self.wrapped = downcast(upgrader.Shape())
|
||||
except: # pylint: disable=bare-except
|
||||
except: # pylint: disable=bare-except
|
||||
warnings.warn(f"Unable to clean {self}")
|
||||
return self
|
||||
|
||||
|
|
@ -3256,6 +3257,7 @@ class ShapePredicate(Protocol):
|
|||
|
||||
class ShapeList(list[T]):
|
||||
"""Subclass of list with custom filter and sort methods appropriate to CAD"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
@property
|
||||
|
|
@ -4244,6 +4246,7 @@ class Curve(Compound):
|
|||
|
||||
class Edge(Mixin1D, Shape):
|
||||
"""A trimmed curve that represents the border of a face"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
_dim = 1
|
||||
|
|
@ -4999,6 +5002,7 @@ class Edge(Mixin1D, Shape):
|
|||
|
||||
class Face(Shape):
|
||||
"""a bounded surface that represents part of the boundary of a solid"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
_dim = 2
|
||||
|
|
@ -5935,13 +5939,15 @@ class Solid(Mixin3D, Shape):
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def make_loft(cls, wires: list[Wire], ruled: bool = False) -> Solid:
|
||||
def make_loft(
|
||||
cls, objs: Iterable[Union[Vertex, Wire]], ruled: bool = False
|
||||
) -> Solid:
|
||||
"""make loft
|
||||
|
||||
Makes a loft from a list of wires.
|
||||
Makes a loft from a list of wires and vertices, where vertices can be the first, last, or first and last elements.
|
||||
|
||||
Args:
|
||||
wires (list[Wire]): section perimeters
|
||||
objs (list[Vertex, Wire]): wire perimeters or vertices
|
||||
ruled (bool, optional): stepped or smooth. Defaults to False (smooth).
|
||||
|
||||
Raises:
|
||||
|
|
@ -5950,13 +5956,18 @@ class Solid(Mixin3D, Shape):
|
|||
Returns:
|
||||
Solid: Lofted object
|
||||
"""
|
||||
|
||||
if len(objs) < 2:
|
||||
raise ValueError("More than one wire, or a wire and a vertex is required")
|
||||
|
||||
# the True flag requests building a solid instead of a shell.
|
||||
if len(wires) < 2:
|
||||
raise ValueError("More than one wire is required")
|
||||
loft_builder = BRepOffsetAPI_ThruSections(True, ruled)
|
||||
|
||||
for wire in wires:
|
||||
loft_builder.AddWire(wire.wrapped)
|
||||
for obj in objs:
|
||||
if isinstance(obj, Vertex):
|
||||
loft_builder.AddVertex(obj.wrapped)
|
||||
elif isinstance(obj, Wire):
|
||||
loft_builder.AddWire(obj.wrapped)
|
||||
|
||||
loft_builder.Build()
|
||||
|
||||
|
|
@ -6274,7 +6285,7 @@ class Solid(Mixin3D, Shape):
|
|||
.solids()
|
||||
.sort_by(direction_axis)[0]
|
||||
)
|
||||
except: # pylint: disable=bare-except
|
||||
except: # pylint: disable=bare-except
|
||||
warnings.warn("clipping error - extrusion may be incorrect")
|
||||
else:
|
||||
extrusion_parts = [extrusion.intersect(target_object)]
|
||||
|
|
@ -6285,7 +6296,7 @@ class Solid(Mixin3D, Shape):
|
|||
.solids()
|
||||
.sort_by(direction_axis)[0]
|
||||
)
|
||||
except: # pylint: disable=bare-except
|
||||
except: # pylint: disable=bare-except
|
||||
warnings.warn("clipping error - extrusion may be incorrect")
|
||||
extrusion = Shape.fuse(*extrusion_parts)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ license:
|
|||
import unittest
|
||||
from math import pi
|
||||
from build123d import *
|
||||
from build123d import Builder, WorkplaneList, LocationList
|
||||
from build123d import WorkplaneList, flatten_sequence
|
||||
|
||||
|
||||
def _assertTupleAlmostEquals(self, expected, actual, places, msg=None):
|
||||
|
|
@ -40,6 +40,38 @@ def _assertTupleAlmostEquals(self, expected, actual, places, msg=None):
|
|||
unittest.TestCase.assertTupleAlmostEquals = _assertTupleAlmostEquals
|
||||
|
||||
|
||||
class TestFlattenSequence(unittest.TestCase):
|
||||
"""Test the flatten_sequence helper function"""
|
||||
|
||||
def test_single_object(self):
|
||||
self.assertListEqual(flatten_sequence("a"), ["a"])
|
||||
|
||||
def test_sequence(self):
|
||||
self.assertListEqual(flatten_sequence("a", "b", "c"), ["a", "b", "c"])
|
||||
|
||||
def test_list(self):
|
||||
self.assertListEqual(flatten_sequence(["a", "b", "c"]), ["a", "b", "c"])
|
||||
|
||||
def test_list_sequence(self):
|
||||
self.assertListEqual(
|
||||
flatten_sequence(["a", "b", "c"], "d"), ["a", "b", "c", "d"]
|
||||
)
|
||||
|
||||
def test_sequence_tuple(self):
|
||||
self.assertListEqual(
|
||||
flatten_sequence("a", ("b", "c", "d"), "e"), ["a", "b", "c", "d", "e"]
|
||||
)
|
||||
|
||||
def test_points(self):
|
||||
self.assertListEqual(
|
||||
flatten_sequence("a", (1, 2, 3), "e"), ["a", (1, 2, 3), "e"]
|
||||
)
|
||||
|
||||
self.assertListEqual(
|
||||
flatten_sequence("a", (1.0, 2.0, 3.0), "e"), ["a", (1.0, 2.0, 3.0), "e"]
|
||||
)
|
||||
|
||||
|
||||
class TestBuilder(unittest.TestCase):
|
||||
"""Test the Builder base class"""
|
||||
|
||||
|
|
@ -123,6 +155,18 @@ class TestBuilder(unittest.TestCase):
|
|||
with self.assertWarns(UserWarning):
|
||||
p.solid()
|
||||
|
||||
def test_workplanes_as_list(self):
|
||||
with BuildPart() as p:
|
||||
Box(1, 1, 1)
|
||||
with BuildSketch(p.faces() >> Axis.Z):
|
||||
Rectangle(0.25, 0.25)
|
||||
extrude(amount=0.25)
|
||||
self.assertAlmostEqual(p.part.volume, 1**3 + 0.25**3, 5)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
with BuildLine([Plane.XY, Plane.XZ]):
|
||||
Line((0, 0), (1, 1))
|
||||
|
||||
|
||||
class TestBuilderExit(unittest.TestCase):
|
||||
def test_multiple(self):
|
||||
|
|
@ -306,6 +350,22 @@ class TestLocations(unittest.TestCase):
|
|||
self.assertTupleAlmostEquals(grid.min.to_tuple(), (-5, -15, 0), 5)
|
||||
self.assertTupleAlmostEquals(grid.max.to_tuple(), (5, 15, 0), 5)
|
||||
|
||||
def test_mixed_sequence_list(self):
|
||||
locs = Locations((0, 1), [(2, 3), (4, 5)], (6, 7))
|
||||
self.assertEqual(len(locs.locations), 4)
|
||||
self.assertTupleAlmostEquals(
|
||||
locs.locations[0].position.to_tuple(), (0, 1, 0), 5
|
||||
)
|
||||
self.assertTupleAlmostEquals(
|
||||
locs.locations[1].position.to_tuple(), (2, 3, 0), 5
|
||||
)
|
||||
self.assertTupleAlmostEquals(
|
||||
locs.locations[2].position.to_tuple(), (4, 5, 0), 5
|
||||
)
|
||||
self.assertTupleAlmostEquals(
|
||||
locs.locations[3].position.to_tuple(), (6, 7, 0), 5
|
||||
)
|
||||
|
||||
|
||||
class TestProperties(unittest.TestCase):
|
||||
def test_vector_properties(self):
|
||||
|
|
@ -609,9 +669,6 @@ class TestValidateInputs(unittest.TestCase):
|
|||
with BuildPart() as p:
|
||||
Box(1, 1, 1)
|
||||
fillet(4, radius=1)
|
||||
self.assertEqual(
|
||||
"fillet doesn't accept int, did you intend <keyword>=4?", str(rte.exception)
|
||||
)
|
||||
|
||||
|
||||
class TestVectorExtensions(unittest.TestCase):
|
||||
|
|
@ -695,20 +752,20 @@ class TestWorkplaneStorage(unittest.TestCase):
|
|||
class TestContextAwareSelectors(unittest.TestCase):
|
||||
def test_context_aware_selectors(self):
|
||||
with BuildPart() as p:
|
||||
Box(1,1,1)
|
||||
Box(1, 1, 1)
|
||||
self.assertEqual(solids(), p.solids())
|
||||
self.assertEqual(faces(), p.faces())
|
||||
self.assertEqual(wires(), p.wires())
|
||||
self.assertEqual(edges(), p.edges())
|
||||
self.assertEqual(vertices(), p.vertices())
|
||||
with BuildSketch() as p:
|
||||
Rectangle(1,1)
|
||||
Rectangle(1, 1)
|
||||
self.assertEqual(faces(), p.faces())
|
||||
self.assertEqual(wires(), p.wires())
|
||||
self.assertEqual(edges(), p.edges())
|
||||
self.assertEqual(vertices(), p.vertices())
|
||||
with BuildLine() as p:
|
||||
Line((0,0), (1,0))
|
||||
Line((0, 0), (1, 0))
|
||||
self.assertEqual(edges(), p.edges())
|
||||
self.assertEqual(vertices(), p.vertices())
|
||||
with BuildSketch() as p:
|
||||
|
|
|
|||
|
|
@ -280,6 +280,21 @@ class BuildLineTests(unittest.TestCase):
|
|||
self.assertEqual(len(test.edges()), 4)
|
||||
self.assertAlmostEqual(test.wires()[0].length, 4)
|
||||
|
||||
def test_polyline_with_list(self):
|
||||
"""Test edge generation and close"""
|
||||
with BuildLine() as test:
|
||||
Polyline((0, 0), [(1, 0), (1, 1)], (0, 1), close=True)
|
||||
self.assertAlmostEqual(
|
||||
(test.edges()[0] @ 0 - test.edges()[-1] @ 1).length, 0, 5
|
||||
)
|
||||
self.assertEqual(len(test.edges()), 4)
|
||||
self.assertAlmostEqual(test.wires()[0].length, 4)
|
||||
|
||||
def test_line_with_list(self):
|
||||
"""Test line with a list of points"""
|
||||
l = Line([(0, 0), (10, 0)])
|
||||
self.assertAlmostEqual(l.length, 10, 5)
|
||||
|
||||
def test_wires_select_last(self):
|
||||
with BuildLine() as test:
|
||||
Line((0, 0), (0, 1))
|
||||
|
|
|
|||
|
|
@ -325,6 +325,50 @@ class TestLoft(unittest.TestCase):
|
|||
self.assertLess(test.part.volume, 225 * pi * 30, 5)
|
||||
self.assertGreater(test.part.volume, 25 * pi * 30, 5)
|
||||
|
||||
def test_loft_vertex(self):
|
||||
with BuildPart() as test:
|
||||
v1 = Vertex(0, 0, 3)
|
||||
with BuildSketch() as s:
|
||||
Rectangle(1, 1)
|
||||
loft(sections=[s.sketch, v1], ruled=True)
|
||||
self.assertAlmostEqual(test.part.volume, 1, 5)
|
||||
|
||||
def test_loft_vertices(self):
|
||||
with BuildPart() as test:
|
||||
v1 = Vertex(0, 0, 3)
|
||||
v2 = Vertex(0, 0, -3)
|
||||
with BuildSketch() as s:
|
||||
Rectangle(1, 1)
|
||||
loft(sections=[v2, s.sketch, v1], ruled=True)
|
||||
self.assertAlmostEqual(test.part.volume, 2, 5)
|
||||
|
||||
def test_loft_vertex_face(self):
|
||||
v1 = Vertex(0, 0, 3)
|
||||
r = Rectangle(1, 1)
|
||||
test = loft(sections=[r.face(), v1], ruled=True)
|
||||
self.assertAlmostEqual(test.volume, 1, 5)
|
||||
|
||||
def test_loft_no_sections_assert(self):
|
||||
with BuildPart() as test:
|
||||
with self.assertRaises(ValueError):
|
||||
loft(sections=[None])
|
||||
|
||||
def test_loft_all_vertices_assert(self):
|
||||
with BuildPart() as test:
|
||||
v1 = Vertex(0, 0, -1)
|
||||
v2 = Vertex(0, 0, 2)
|
||||
with self.assertRaises(ValueError):
|
||||
loft(sections=[v1, v2])
|
||||
|
||||
def test_loft_vertex_middle_assert(self):
|
||||
with BuildPart() as test:
|
||||
v1 = Vertex(0, 0, -1)
|
||||
v2 = Vertex(0, 0, 2)
|
||||
with BuildSketch() as s:
|
||||
Circle(1)
|
||||
with self.assertRaises(ValueError):
|
||||
loft(sections=[v1, v2, s.sketch])
|
||||
|
||||
|
||||
class TestRevolve(unittest.TestCase):
|
||||
def test_simple_revolve(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue