mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Merge pull request #1158 from gumyr/pr1140
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
Some checks failed
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (macos-15-intel, 3.10) (push) Has been cancelled
tests / tests (macos-15-intel, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
Pr1140 with typing and test coverage improvements
This commit is contained in:
commit
ad77bf5f7f
2 changed files with 123 additions and 36 deletions
|
|
@ -50,7 +50,7 @@ from build123d.build_enums import (
|
||||||
)
|
)
|
||||||
from build123d.build_line import BuildLine
|
from build123d.build_line import BuildLine
|
||||||
from build123d.geometry import Axis, Plane, Vector, VectorLike, TOLERANCE
|
from build123d.geometry import Axis, Plane, Vector, VectorLike, TOLERANCE
|
||||||
from build123d.topology import Edge, Face, Wire, Curve
|
from build123d.topology import Curve, Edge, Face, Vertex, Wire
|
||||||
from build123d.topology.shape_core import ShapeList
|
from build123d.topology.shape_core import ShapeList
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -787,20 +787,22 @@ class Helix(BaseEdgeObject):
|
||||||
|
|
||||||
class FilletPolyline(BaseLineObject):
|
class FilletPolyline(BaseLineObject):
|
||||||
"""Line Object: Fillet Polyline
|
"""Line Object: Fillet Polyline
|
||||||
|
|
||||||
Create a sequence of straight lines defined by successive points that are filleted
|
Create a sequence of straight lines defined by successive points that are filleted
|
||||||
to a given radius.
|
to a given radius.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pts (VectorLike | Iterable[VectorLike]): sequence of two or more points
|
pts (VectorLike | Iterable[VectorLike]): sequence of two or more points
|
||||||
radius (float | Iterable[float]): radius to fillet at each vertex or a single value for all vertices
|
radius (float | Iterable[float]): radius to fillet at each vertex or a single value for all vertices.
|
||||||
|
A radius of 0 will create a sharp corner (vertex without fillet).
|
||||||
|
|
||||||
close (bool, optional): close end points with extra Edge and corner fillets.
|
close (bool, optional): close end points with extra Edge and corner fillets.
|
||||||
Defaults to False
|
Defaults to False
|
||||||
|
|
||||||
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
mode (Mode, optional): combination mode. Defaults to Mode.ADD
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: Two or more points not provided
|
ValueError: Two or more points not provided
|
||||||
ValueError: radius must be positive
|
ValueError: radius must be non-negative
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_applies_to = [BuildLine._tag]
|
_applies_to = [BuildLine._tag]
|
||||||
|
|
@ -812,9 +814,9 @@ class FilletPolyline(BaseLineObject):
|
||||||
close: bool = False,
|
close: bool = False,
|
||||||
mode: Mode = Mode.ADD,
|
mode: Mode = Mode.ADD,
|
||||||
):
|
):
|
||||||
|
|
||||||
context: BuildLine | None = BuildLine._get_context(self)
|
context: BuildLine | None = BuildLine._get_context(self)
|
||||||
validate_inputs(context, self)
|
validate_inputs(context, self)
|
||||||
|
|
||||||
points = flatten_sequence(*pts)
|
points = flatten_sequence(*pts)
|
||||||
|
|
||||||
if len(points) < 2:
|
if len(points) < 2:
|
||||||
|
|
@ -822,30 +824,35 @@ class FilletPolyline(BaseLineObject):
|
||||||
|
|
||||||
if isinstance(radius, (int, float)):
|
if isinstance(radius, (int, float)):
|
||||||
radius_list = [radius] * len(points) # Single radius for all points
|
radius_list = [radius] * len(points) # Single radius for all points
|
||||||
|
|
||||||
else:
|
else:
|
||||||
radius_list = list(radius)
|
radius_list = list(radius)
|
||||||
if len(radius_list) != len(points) - int(not close) * 2:
|
if len(radius_list) != len(points) - int(not close) * 2:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"radius list length ({len(radius_list)}) must match angle count ({ len(points) - int(not close) * 2})"
|
f"radius list length ({len(radius_list)}) must match angle count ({ len(points) - int(not close) * 2})"
|
||||||
)
|
)
|
||||||
|
|
||||||
for r in radius_list:
|
for r in radius_list:
|
||||||
if r <= 0:
|
if r < 0:
|
||||||
raise ValueError(f"radius {r} must be positive")
|
raise ValueError(f"radius {r} must be non-negative")
|
||||||
|
|
||||||
lines_pts = WorkplaneList.localize(*points)
|
lines_pts = WorkplaneList.localize(*points)
|
||||||
|
|
||||||
# Create the polyline
|
# Create the polyline
|
||||||
|
|
||||||
new_edges = [
|
new_edges = [
|
||||||
Edge.make_line(lines_pts[i], lines_pts[i + 1])
|
Edge.make_line(lines_pts[i], lines_pts[i + 1])
|
||||||
for i in range(len(lines_pts) - 1)
|
for i in range(len(lines_pts) - 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
if close and (new_edges[0] @ 0 - new_edges[-1] @ 1).length > 1e-5:
|
if close and (new_edges[0] @ 0 - new_edges[-1] @ 1).length > 1e-5:
|
||||||
new_edges.append(Edge.make_line(new_edges[-1] @ 1, new_edges[0] @ 0))
|
new_edges.append(Edge.make_line(new_edges[-1] @ 1, new_edges[0] @ 0))
|
||||||
|
|
||||||
wire_of_lines = Wire(new_edges)
|
wire_of_lines = Wire(new_edges)
|
||||||
|
|
||||||
# Create a list of vertices from wire_of_lines in the same order as
|
# Create a list of vertices from wire_of_lines in the same order as
|
||||||
# the original points so the resulting fillet edges are ordered
|
# the original points so the resulting fillet edges are ordered
|
||||||
ordered_vertices = []
|
ordered_vertices: list[Vertex] = []
|
||||||
|
|
||||||
for pnts in lines_pts:
|
for pnts in lines_pts:
|
||||||
distance = {
|
distance = {
|
||||||
v: (Vector(pnts) - Vector(*v)).length for v in wire_of_lines.vertices()
|
v: (Vector(pnts) - Vector(*v)).length for v in wire_of_lines.vertices()
|
||||||
|
|
@ -853,42 +860,93 @@ class FilletPolyline(BaseLineObject):
|
||||||
ordered_vertices.append(sorted(distance.items(), key=lambda x: x[1])[0][0])
|
ordered_vertices.append(sorted(distance.items(), key=lambda x: x[1])[0][0])
|
||||||
|
|
||||||
# Fillet the corners
|
# Fillet the corners
|
||||||
|
|
||||||
# Create a map of vertices to edges containing that vertex
|
# Create a map of vertices to edges containing that vertex
|
||||||
vertex_to_edges = {
|
vertex_to_edges = {
|
||||||
v: [e for e in wire_of_lines.edges() if v in e.vertices()]
|
v: [e for e in wire_of_lines.edges() if v in e.vertices()]
|
||||||
for v in ordered_vertices
|
for v in ordered_vertices
|
||||||
}
|
}
|
||||||
|
|
||||||
# For each corner vertex create a new fillet Edge
|
# For each corner vertex create a new fillet Edge (or keep as vertex if radius is 0)
|
||||||
fillets = []
|
fillets: list[None | Edge] = []
|
||||||
|
|
||||||
for i, (vertex, edges) in enumerate(vertex_to_edges.items()):
|
for i, (vertex, edges) in enumerate(vertex_to_edges.items()):
|
||||||
if len(edges) != 2:
|
if len(edges) != 2:
|
||||||
continue
|
continue
|
||||||
other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex}
|
current_radius = radius_list[i - int(not close)]
|
||||||
third_edge = Edge.make_line(*[v for v in other_vertices])
|
|
||||||
fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(
|
if current_radius == 0:
|
||||||
radius_list[i - int(not close)], [vertex]
|
# For 0 radius, store the vertex as a marker for a sharp corner
|
||||||
)
|
fillets.append(None)
|
||||||
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
|
|
||||||
|
else:
|
||||||
|
other_vertices = {
|
||||||
|
ve for e in edges for ve in e.vertices() if ve != vertex
|
||||||
|
}
|
||||||
|
third_edge = Edge.make_line(*[v for v in other_vertices])
|
||||||
|
fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(
|
||||||
|
current_radius, [vertex]
|
||||||
|
)
|
||||||
|
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
|
||||||
|
|
||||||
# Create the Edges that join the fillets
|
# Create the Edges that join the fillets
|
||||||
if close:
|
if close:
|
||||||
interior_edges = [
|
interior_edges = []
|
||||||
Edge.make_line(fillets[i - 1] @ 1, fillets[i] @ 0)
|
|
||||||
for i in range(len(fillets))
|
|
||||||
]
|
|
||||||
end_edges = []
|
|
||||||
else:
|
|
||||||
interior_edges = [
|
|
||||||
Edge.make_line(fillets[i] @ 1, f @ 0) for i, f in enumerate(fillets[1:])
|
|
||||||
]
|
|
||||||
end_edges = [
|
|
||||||
Edge.make_line(wire_of_lines @ 0, fillets[0] @ 0),
|
|
||||||
Edge.make_line(fillets[-1] @ 1, wire_of_lines @ 1),
|
|
||||||
]
|
|
||||||
|
|
||||||
new_wire = Wire(end_edges + interior_edges + fillets)
|
for i in range(len(fillets)):
|
||||||
|
prev_fillet = fillets[i - 1]
|
||||||
|
curr_fillet = fillets[i]
|
||||||
|
prev_idx = i - 1
|
||||||
|
curr_idx = i
|
||||||
|
# Determine start and end points
|
||||||
|
if prev_fillet is None:
|
||||||
|
start_pt: Vertex | Vector = ordered_vertices[prev_idx]
|
||||||
|
else:
|
||||||
|
start_pt = prev_fillet @ 1
|
||||||
|
|
||||||
|
if curr_fillet is None:
|
||||||
|
end_pt: Vertex | Vector = ordered_vertices[curr_idx]
|
||||||
|
else:
|
||||||
|
end_pt = curr_fillet @ 0
|
||||||
|
interior_edges.append(Edge.make_line(start_pt, end_pt))
|
||||||
|
|
||||||
|
end_edges = []
|
||||||
|
|
||||||
|
else:
|
||||||
|
interior_edges = []
|
||||||
|
for i in range(len(fillets) - 1):
|
||||||
|
next_fillet = fillets[i + 1]
|
||||||
|
curr_fillet = fillets[i]
|
||||||
|
curr_idx = i
|
||||||
|
next_idx = i + 1
|
||||||
|
# Determine start and end points
|
||||||
|
if curr_fillet is None:
|
||||||
|
start_pt = ordered_vertices[
|
||||||
|
curr_idx + 1
|
||||||
|
] # +1 because first vertex has no fillet
|
||||||
|
else:
|
||||||
|
start_pt = curr_fillet @ 1
|
||||||
|
|
||||||
|
if next_fillet is None:
|
||||||
|
end_pt = ordered_vertices[next_idx + 1]
|
||||||
|
else:
|
||||||
|
end_pt = next_fillet @ 0
|
||||||
|
interior_edges.append(Edge.make_line(start_pt, end_pt))
|
||||||
|
|
||||||
|
# Handle end edges
|
||||||
|
if fillets[0] is None:
|
||||||
|
start_edge = Edge.make_line(wire_of_lines @ 0, ordered_vertices[1])
|
||||||
|
else:
|
||||||
|
start_edge = Edge.make_line(wire_of_lines @ 0, fillets[0] @ 0)
|
||||||
|
|
||||||
|
if fillets[-1] is None:
|
||||||
|
end_edge = Edge.make_line(ordered_vertices[-2], wire_of_lines @ 1)
|
||||||
|
else:
|
||||||
|
end_edge = Edge.make_line(fillets[-1] @ 1, wire_of_lines @ 1)
|
||||||
|
end_edges = [start_edge, end_edge]
|
||||||
|
|
||||||
|
# Filter out None values from fillets (these are 0-radius corners)
|
||||||
|
actual_fillets = [f for f in fillets if f is not None]
|
||||||
|
new_wire = Wire(end_edges + interior_edges + actual_fillets)
|
||||||
|
|
||||||
super().__init__(new_wire, mode=mode)
|
super().__init__(new_wire, mode=mode)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ class BuildLineTests(unittest.TestCase):
|
||||||
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
|
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
|
||||||
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3)
|
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3)
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with BuildLine(Plane.YZ):
|
||||||
p = FilletPolyline(
|
p = FilletPolyline(
|
||||||
(0, 0),
|
(0, 0),
|
||||||
(10, 0),
|
(10, 0),
|
||||||
|
|
@ -192,6 +192,8 @@ class BuildLineTests(unittest.TestCase):
|
||||||
radius=(1, 2, 3, 0),
|
radius=(1, 2, 3, 0),
|
||||||
close=True,
|
close=True,
|
||||||
)
|
)
|
||||||
|
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 3)
|
||||||
|
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 4)
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
p = FilletPolyline(
|
p = FilletPolyline(
|
||||||
|
|
@ -250,6 +252,33 @@ class BuildLineTests(unittest.TestCase):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
FilletPolyline((0, 0), (1, 0), (1, 1), radius=-1)
|
FilletPolyline((0, 0), (1, 0), (1, 1), radius=-1)
|
||||||
|
|
||||||
|
# test filletpolyline curr_fillet None
|
||||||
|
# Middle corner radius = 0 → curr_fillet is None
|
||||||
|
with BuildLine():
|
||||||
|
p = FilletPolyline(
|
||||||
|
(0, 0),
|
||||||
|
(10, 0),
|
||||||
|
(10, 10),
|
||||||
|
(20, 10),
|
||||||
|
radius=(0, 1), # middle corner is sharp
|
||||||
|
close=False,
|
||||||
|
)
|
||||||
|
# 1 circular fillet, 3 line fillets
|
||||||
|
assert len(p.edges().filter_by(GeomType.CIRCLE)) == 1
|
||||||
|
|
||||||
|
# test filletpolyline next_fillet None:
|
||||||
|
# Second corner is sharp (radius 0) → next_fillet is None
|
||||||
|
with BuildLine():
|
||||||
|
p = FilletPolyline(
|
||||||
|
(0, 0),
|
||||||
|
(10, 0),
|
||||||
|
(10, 10),
|
||||||
|
(0, 10),
|
||||||
|
radius=(1, 0), # next_fillet is None at last interior corner
|
||||||
|
close=False,
|
||||||
|
)
|
||||||
|
assert len(p.edges()) > 0
|
||||||
|
|
||||||
def test_intersecting_line(self):
|
def test_intersecting_line(self):
|
||||||
with BuildLine():
|
with BuildLine():
|
||||||
l1 = Line((0, 0), (10, 0))
|
l1 = Line((0, 0), (10, 0))
|
||||||
|
|
@ -858,9 +887,9 @@ class BuildLineTests(unittest.TestCase):
|
||||||
min_r = 0 if case[2][0] is None else (flip_min * case[0] + case[2][0]) / 2
|
min_r = 0 if case[2][0] is None else (flip_min * case[0] + case[2][0]) / 2
|
||||||
max_r = 1e6 if case[2][1] is None else (flip_max * case[0] + case[2][1]) / 2
|
max_r = 1e6 if case[2][1] is None else (flip_max * case[0] + case[2][1]) / 2
|
||||||
|
|
||||||
print(case[1], min_r, max_r, case[0])
|
# print(case[1], min_r, max_r, case[0])
|
||||||
print(min_r + 0.01, min_r * 0.99, max_r - 0.01, max_r + 0.01)
|
# print(min_r + 0.01, min_r * 0.99, max_r - 0.01, max_r + 0.01)
|
||||||
print((case[0] - 1 * (r1 + r2)) / 2)
|
# print((case[0] - 1 * (r1 + r2)) / 2)
|
||||||
|
|
||||||
# Greater than min
|
# Greater than min
|
||||||
l1 = ArcArcTangentArc(start_arc, end_arc, min_r + 0.01, keep=case[1])
|
l1 = ArcArcTangentArc(start_arc, end_arc, min_r + 0.01, keep=case[1])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue