mirror of
https://github.com/gumyr/build123d.git
synced 2026-05-10 14:13:45 -07:00
parent
11a017ead7
commit
7dea02e814
5 changed files with 390 additions and 36 deletions
|
|
@ -55,6 +55,7 @@ import copy
|
|||
import warnings
|
||||
from bisect import bisect_right
|
||||
from collections.abc import Iterable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from itertools import combinations
|
||||
from math import atan2, ceil, copysign, cos, floor, inf, isclose, pi, radians
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
|
|
@ -62,6 +63,7 @@ from typing import cast as tcast
|
|||
|
||||
import numpy as np
|
||||
import OCP.TopAbs as ta
|
||||
from OCP.BOPAlgo import BOPAlgo_Splitter
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.BRepAdaptor import BRepAdaptor_CompCurve, BRepAdaptor_Curve
|
||||
from OCP.BRepAlgoAPI import (
|
||||
|
|
@ -69,6 +71,7 @@ from OCP.BRepAlgoAPI import (
|
|||
BRepAlgoAPI_Section,
|
||||
BRepAlgoAPI_Splitter,
|
||||
)
|
||||
|
||||
from OCP.BRepBuilderAPI import (
|
||||
BRepBuilderAPI_DisconnectedWire,
|
||||
BRepBuilderAPI_EmptyWire,
|
||||
|
|
@ -78,8 +81,10 @@ from OCP.BRepBuilderAPI import (
|
|||
BRepBuilderAPI_MakePolygon,
|
||||
BRepBuilderAPI_MakeWire,
|
||||
BRepBuilderAPI_NonManifoldWire,
|
||||
BRepBuilderAPI_MakeVertex,
|
||||
)
|
||||
from OCP.BRepExtrema import BRepExtrema_DistShapeShape, BRepExtrema_SupportType
|
||||
from OCP.BRepFeat import BRepFeat_SplitShape
|
||||
from OCP.BRepFilletAPI import BRepFilletAPI_MakeFillet2d
|
||||
from OCP.BRepGProp import BRepGProp, BRepGProp_Face
|
||||
from OCP.BRepLib import BRepLib, BRepLib_FindSurface
|
||||
|
|
@ -89,6 +94,7 @@ from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeOffset
|
|||
from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
|
||||
from OCP.BRepProj import BRepProj_Projection
|
||||
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
|
||||
from OCP.ChFi2d import ChFi2d_FilletAlgo
|
||||
from OCP.Extrema import Extrema_ExtPC
|
||||
from OCP.GC import (
|
||||
GC_MakeArcOfCircle,
|
||||
|
|
@ -267,6 +273,222 @@ if TYPE_CHECKING: # pragma: no cover
|
|||
from .two_d import Face, Shell # pylint: disable=R0801
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _WireFilletCorner:
|
||||
"""Context needed to fillet a single planar wire corner."""
|
||||
|
||||
wire: Wire
|
||||
vertex: Vertex
|
||||
surface: Geom_Plane
|
||||
all_edges: ShapeList[Edge]
|
||||
connected_edges: ShapeList[Edge]
|
||||
connected_edge_indices: list[int]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _WireFilletSolution:
|
||||
"""Replacement edges for a filleted wire corner."""
|
||||
|
||||
trimmed_topods_edges: list[TopoDS_Edge]
|
||||
fillet_topods_edge: TopoDS_Edge
|
||||
|
||||
|
||||
def _analyze_wire_fillet_corner(wire: Wire, vertex: Vertex) -> _WireFilletCorner:
|
||||
"""Collect the topology needed to fillet a single wire corner."""
|
||||
|
||||
find_surface = BRepLib_FindSurface(wire.wrapped, OnlyPlane=True)
|
||||
surf = find_surface.Surface()
|
||||
if not isinstance(surf, Geom_Plane):
|
||||
raise ValueError(f"Wire is not planar {wire}")
|
||||
|
||||
all_edges = wire.edges()
|
||||
connected_edges = all_edges.filter_by(
|
||||
lambda edge: any(
|
||||
edge_vertex.wrapped.IsSame(vertex.wrapped)
|
||||
for edge_vertex in edge.vertices()
|
||||
)
|
||||
)
|
||||
vertex_label = str(vertex)
|
||||
if not connected_edges:
|
||||
raise ValueError(f"Could not find shared vertex on wire: {vertex_label}")
|
||||
if len(connected_edges) != 2:
|
||||
raise ValueError(f"Vertex must connect exactly two edges: {vertex_label}")
|
||||
|
||||
connected_edge_indices = [all_edges.index(e) for e in connected_edges]
|
||||
|
||||
return _WireFilletCorner(
|
||||
wire=wire,
|
||||
vertex=vertex,
|
||||
surface=surf,
|
||||
all_edges=all_edges,
|
||||
connected_edges=connected_edges,
|
||||
connected_edge_indices=connected_edge_indices,
|
||||
)
|
||||
|
||||
|
||||
def _solve_wire_fillet_corner_chfi2d(
|
||||
corner: _WireFilletCorner, radius: float
|
||||
) -> _WireFilletSolution | None:
|
||||
"""Try to fillet a planar wire corner with ``ChFi2d_FilletAlgo``."""
|
||||
|
||||
fillet_builder = ChFi2d_FilletAlgo()
|
||||
fillet_builder.Init(
|
||||
corner.connected_edges[0].wrapped,
|
||||
corner.connected_edges[1].wrapped,
|
||||
corner.surface.Pln(),
|
||||
)
|
||||
|
||||
vertex_point = BRep_Tool.Pnt_s(corner.vertex.wrapped)
|
||||
if (
|
||||
not fillet_builder.Perform(radius)
|
||||
or fillet_builder.NbResults(vertex_point) == 0
|
||||
):
|
||||
return None
|
||||
|
||||
trimmed_topods_edge0, trimmed_topods_edge1 = TopoDS_Edge(), TopoDS_Edge()
|
||||
fillet_topods_edge = fillet_builder.Result(
|
||||
vertex_point, trimmed_topods_edge0, trimmed_topods_edge1
|
||||
)
|
||||
|
||||
return _WireFilletSolution(
|
||||
trimmed_topods_edges=[trimmed_topods_edge0, trimmed_topods_edge1],
|
||||
fillet_topods_edge=fillet_topods_edge,
|
||||
)
|
||||
|
||||
|
||||
def _topods_edge_contains_vertex(
|
||||
topods_edge: TopoDS_Edge, topods_vertex: TopoDS_Vertex
|
||||
) -> bool:
|
||||
"""Check whether an edge has the given vertex as one of its endpoints."""
|
||||
|
||||
edge_vertex0 = TopExp.FirstVertex_s(topods_edge)
|
||||
edge_vertex1 = TopExp.LastVertex_s(topods_edge)
|
||||
vertex_point = BRep_Tool.Pnt_s(topods_vertex)
|
||||
return (
|
||||
BRep_Tool.Pnt_s(edge_vertex0).Distance(vertex_point) < TOLERANCE
|
||||
or BRep_Tool.Pnt_s(edge_vertex1).Distance(vertex_point) < TOLERANCE
|
||||
)
|
||||
|
||||
|
||||
def _split_edge_at_vertex(edge: Edge, split_vertex: Vertex) -> list[TopoDS_Edge]:
|
||||
"""Split an edge at a vertex and return the resulting edge segments."""
|
||||
|
||||
splitter = BOPAlgo_Splitter()
|
||||
splitter.AddArgument(edge.wrapped)
|
||||
splitter.AddTool(split_vertex.wrapped)
|
||||
splitter.Perform()
|
||||
|
||||
split_shape = splitter.Shape()
|
||||
split_edges = []
|
||||
explorer = TopExp_Explorer(split_shape, ta.TopAbs_EDGE)
|
||||
while explorer.More():
|
||||
split_edges.append(TopoDS.Edge(explorer.Current()))
|
||||
explorer.Next()
|
||||
|
||||
return split_edges
|
||||
|
||||
|
||||
def _wire_fillet_corner_is_tangent_continuous(corner: _WireFilletCorner) -> bool:
|
||||
"""Determine if two incident edges are tangent-continuous at a corner."""
|
||||
|
||||
tangent0 = corner.connected_edges[0].tangent_at(corner.vertex)
|
||||
tangent1 = corner.connected_edges[1].tangent_at(corner.vertex)
|
||||
return tangent0.cross(tangent1).length <= TOLERANCE
|
||||
|
||||
|
||||
def _solve_wire_fillet_corner_geom2dgcc_circ2d2tanrad(
|
||||
corner: _WireFilletCorner, radius: float
|
||||
) -> _WireFilletSolution | None:
|
||||
"""Fallback fillet using the ``Geom2dGcc_Circ2d2TanRad`` tangent-arc solver."""
|
||||
|
||||
fillet_arcs = _make_2tan_rad_arcs(
|
||||
*corner.connected_edges,
|
||||
radius=radius,
|
||||
sagitta=Sagitta.BOTH,
|
||||
edge_factory=Edge,
|
||||
)
|
||||
if not fillet_arcs:
|
||||
return None
|
||||
|
||||
fillet_arc = fillet_arcs.sort_by_distance(corner.vertex)[0]
|
||||
|
||||
trimmed_topods_edges = []
|
||||
for connected_edge in corner.connected_edges:
|
||||
other_vertex = [v for v in connected_edge.vertices() if v != corner.vertex][0]
|
||||
fillet_vertex = fillet_arc.vertices().sort_by_distance(connected_edge)[0]
|
||||
split_vertex = Vertex(
|
||||
BRepBuilderAPI_MakeVertex(Vector(fillet_vertex).to_pnt()).Vertex()
|
||||
)
|
||||
split_edges = _split_edge_at_vertex(copy.deepcopy(connected_edge), split_vertex)
|
||||
trimmed_topods_edges.append(
|
||||
next(
|
||||
edge
|
||||
for edge in split_edges
|
||||
if _topods_edge_contains_vertex(edge, other_vertex.wrapped)
|
||||
)
|
||||
)
|
||||
|
||||
return _WireFilletSolution(
|
||||
trimmed_topods_edges=trimmed_topods_edges,
|
||||
fillet_topods_edge=fillet_arc.wrapped,
|
||||
)
|
||||
|
||||
|
||||
def _splice_wire_fillet_corner(
|
||||
corner: _WireFilletCorner, solution: _WireFilletSolution
|
||||
) -> Wire:
|
||||
"""Replace two connected edges with a fillet and rebuild the wire."""
|
||||
|
||||
all_topods_edges = [edge.wrapped for edge in corner.all_edges]
|
||||
|
||||
# Flip any edges that were reversed during trimming
|
||||
for i in range(2):
|
||||
if (
|
||||
solution.trimmed_topods_edges[i].Orientation()
|
||||
!= corner.connected_edges[i].wrapped.Orientation()
|
||||
):
|
||||
solution.trimmed_topods_edges[i].Reverse()
|
||||
|
||||
for i in range(2):
|
||||
all_topods_edges[corner.connected_edge_indices[i]] = (
|
||||
solution.trimmed_topods_edges[i]
|
||||
)
|
||||
|
||||
n = len(all_topods_edges)
|
||||
if corner.connected_edge_indices[1] == (corner.connected_edge_indices[0] + 1) % n:
|
||||
insert_index = corner.connected_edge_indices[0] + 1
|
||||
else:
|
||||
insert_index = corner.connected_edge_indices[1] + 1
|
||||
|
||||
all_topods_edges.insert(insert_index, solution.fillet_topods_edge)
|
||||
|
||||
combined_edges = TopTools_ListOfShape()
|
||||
for topods_edge in all_topods_edges:
|
||||
combined_edges.Append(topods_edge)
|
||||
wire_builder = BRepBuilderAPI_MakeWire()
|
||||
wire_builder.Add(combined_edges)
|
||||
wire_builder.Build()
|
||||
|
||||
return Wire(wire_builder.Wire())
|
||||
|
||||
|
||||
def _fillet_wire_corner(wire: Wire, vertex: Vertex, radius: float) -> Wire:
|
||||
"""Fillet a single planar wire corner with the available 2D fillet solvers."""
|
||||
|
||||
corner = _analyze_wire_fillet_corner(wire, vertex)
|
||||
if _wire_fillet_corner_is_tangent_continuous(corner):
|
||||
return wire
|
||||
vertex_label = str(vertex)
|
||||
solution = _solve_wire_fillet_corner_chfi2d(corner, radius)
|
||||
if solution is None:
|
||||
solution = _solve_wire_fillet_corner_geom2dgcc_circ2d2tanrad(corner, radius)
|
||||
if solution is None:
|
||||
raise ValueError(
|
||||
f"Fillet algorithm failed for {vertex_label} with radius {radius}"
|
||||
)
|
||||
return _splice_wire_fillet_corner(corner, solution)
|
||||
|
||||
|
||||
class Mixin1D(Shape[TOPODS]):
|
||||
"""Methods to add to the Edge and Wire classes"""
|
||||
|
||||
|
|
@ -3785,20 +4007,18 @@ class Wire(Mixin1D[TopoDS_Wire]):
|
|||
"""
|
||||
if self._wrapped is None:
|
||||
raise ValueError("Can't fillet an empty wire")
|
||||
|
||||
# Create a face to fillet
|
||||
unfilleted_face = _make_topods_face_from_wires(self.wrapped)
|
||||
# Fillet the face
|
||||
fillet_builder = BRepFilletAPI_MakeFillet2d(unfilleted_face)
|
||||
filleted_wire = self
|
||||
for vertex in vertices:
|
||||
if vertex.wrapped is not None:
|
||||
fillet_builder.AddFillet(vertex.wrapped, radius)
|
||||
fillet_builder.Build()
|
||||
filleted_face = downcast(fillet_builder.Shape())
|
||||
if not isinstance(filleted_face, TopoDS_Face):
|
||||
raise RuntimeError("An internal error occured creating the fillet")
|
||||
# Return the outer wire
|
||||
return Wire(BRepTools.OuterWire_s(filleted_face))
|
||||
if vertex.wrapped is None:
|
||||
continue
|
||||
current_vertices = filleted_wire.vertices().sort_by_distance(vertex)
|
||||
if not current_vertices:
|
||||
raise ValueError(f"Could not find fillet vertex on wire: {vertex}")
|
||||
current_vertex = current_vertices[0]
|
||||
if not isclose_b((Vector(current_vertex) - Vector(vertex)).length, 0.0):
|
||||
raise ValueError(f"Could not find fillet vertex on wire: {vertex}")
|
||||
filleted_wire = _fillet_wire_corner(filleted_wire, current_vertex, radius)
|
||||
return filleted_wire
|
||||
|
||||
def fix_degenerate_edges(self, precision: float) -> Wire:
|
||||
"""fix_degenerate_edges
|
||||
|
|
|
|||
|
|
@ -1836,15 +1836,32 @@ class Face(Mixin2D[TopoDS_Face]):
|
|||
Returns:
|
||||
|
||||
"""
|
||||
vertices = [vertex for vertex in vertices if vertex.wrapped is not None]
|
||||
if not vertices:
|
||||
return self
|
||||
|
||||
fillet_builder = BRepFilletAPI_MakeFillet2d(self.wrapped)
|
||||
outer_wire = self.outer_wire()
|
||||
inner_wires = self.inner_wires()
|
||||
filleted_wires: list[Wire] = []
|
||||
|
||||
for vertex in vertices:
|
||||
fillet_builder.AddFillet(vertex.wrapped, radius)
|
||||
for wire in [outer_wire, *inner_wires]:
|
||||
vertices_in_wire = [
|
||||
vertex
|
||||
for vertex in vertices
|
||||
if any(
|
||||
wire_vertex.wrapped.IsSame(vertex.wrapped)
|
||||
for wire_vertex in wire.vertices()
|
||||
)
|
||||
]
|
||||
filleted_wires.append(
|
||||
wire.fillet_2d(radius, vertices_in_wire) if vertices_in_wire else wire
|
||||
)
|
||||
|
||||
fillet_builder.Build()
|
||||
filleted_face = self.__class__(filleted_wires[0], filleted_wires[1:])
|
||||
if self.normal_at() != filleted_face.normal_at():
|
||||
filleted_face = -filleted_face
|
||||
|
||||
return self.__class__.cast(fillet_builder.Shape())
|
||||
return filleted_face
|
||||
|
||||
def geom_adaptor(self) -> Geom_Surface:
|
||||
"""Return the Geom Surface for this Face"""
|
||||
|
|
|
|||
|
|
@ -594,8 +594,8 @@ class OffsetTests(unittest.TestCase):
|
|||
]
|
||||
line = FilletPolyline(*pts, radius=3.177)
|
||||
self.assertEqual(len(line.edges()), 11)
|
||||
o_line = offset(line, amount=3.177)
|
||||
self.assertEqual(len(o_line.edges()), 19)
|
||||
o_line = offset(line, amount=2)
|
||||
self.assertEqual(len(o_line.edges()), 26)
|
||||
|
||||
def test_offset_face_with_inner_wire(self):
|
||||
# offset amount causes the inner wire to have zero length
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ from OCP.Geom import Geom_RectangularTrimmedSurface
|
|||
from OCP.GeomAPI import GeomAPI_ExtremaCurveCurve
|
||||
from OCP.Geom import Geom_CylindricalSurface, Geom_OffsetSurface
|
||||
|
||||
from build123d.build_common import Locations, PolarLocations
|
||||
from build123d.build_enums import Align, CenterOf, ContinuityLevel, GeomType, Keep
|
||||
from build123d.build_common import GridLocations, Locations, PolarLocations
|
||||
from build123d.build_enums import Align, CenterOf, ContinuityLevel, GeomType, Keep, Mode
|
||||
from build123d.build_line import BuildLine
|
||||
from build123d.build_part import BuildPart
|
||||
from build123d.build_sketch import BuildSketch
|
||||
|
|
@ -61,7 +61,7 @@ from build123d.objects_sketch import (
|
|||
from build123d.operations_generic import fillet, offset
|
||||
from build123d.operations_part import extrude
|
||||
from build123d.operations_sketch import make_face
|
||||
from build123d.topology import Edge, Face, Shell, Solid, Wire
|
||||
from build123d.topology import Edge, Face, Shell, Sketch, Solid, Wire
|
||||
|
||||
|
||||
class TestFace(unittest.TestCase):
|
||||
|
|
@ -133,6 +133,45 @@ class TestFace(unittest.TestCase):
|
|||
distance=1, distance2=2, vertices=[vertex], edge=other_edge
|
||||
)
|
||||
|
||||
def test_fillet_2d_mixed_profile_case(self):
|
||||
sketch = Sketch() + Rectangle(10, 20) + Ellipse(20, 5)
|
||||
vertex = sketch.vertices().group_by(Axis.X)[0].sort_by(Axis.Y)[0]
|
||||
|
||||
filleted = sketch.faces()[0].fillet_2d(1.0, [vertex])
|
||||
|
||||
self.assertTrue(filleted.is_valid)
|
||||
self.assertGreater(filleted.area, 0)
|
||||
self.assertGreaterEqual(len(filleted.edges().filter_by(GeomType.CIRCLE)), 1)
|
||||
|
||||
def test_fillet_2d_mixed_profile_regression(self):
|
||||
sketch = Sketch() + Rectangle(10, 20) + Ellipse(20, 5)
|
||||
vertex = sketch.vertices().group_by(Axis.X)[1].sort_by(Axis.Y)[1]
|
||||
|
||||
filleted = sketch.faces()[0].fillet_2d(1.0, [vertex])
|
||||
|
||||
self.assertTrue(filleted.is_valid)
|
||||
self.assertGreater(filleted.area, 0)
|
||||
self.assertGreaterEqual(len(filleted.edges().filter_by(GeomType.CIRCLE)), 1)
|
||||
|
||||
def test_fillet_2d_holes_regression(self):
|
||||
with BuildSketch() as sketch_builder:
|
||||
Ellipse(x_radius=74 / 2, y_radius=54 / 2)
|
||||
with GridLocations(49, 32, 2, 2):
|
||||
Circle(12 / 2, mode=Mode.SUBTRACT)
|
||||
vertex = sketch_builder.vertices().sort_by_distance((30, 20))[0]
|
||||
original = sketch_builder.face()
|
||||
|
||||
filleted = original.fillet_2d(2.0, [vertex])
|
||||
|
||||
self.assertTrue(filleted.is_valid)
|
||||
self.assertGreater(filleted.area, 0)
|
||||
self.assertLess(filleted.area, original.area)
|
||||
|
||||
def test_fillet_geom2dgcc_circ2d2tanrad_algorithm(self):
|
||||
r = Rectangle(6, 6) - Pos(1, 1) * Circle(2) - Pos(3, 3) * Rectangle(4, 4)
|
||||
filleted = r.face().fillet_2d(0.2, r.vertices())
|
||||
self.assertEqual(len(filleted.edges().filter_by(GeomType.CIRCLE)), 6)
|
||||
|
||||
def test_plane_as_face(self):
|
||||
test_face = Face(Plane.XY)
|
||||
self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5)
|
||||
|
|
|
|||
|
|
@ -33,14 +33,15 @@ from unittest.mock import patch, MagicMock
|
|||
|
||||
import numpy as np
|
||||
from build123d.topology.shape_core import TOLERANCE
|
||||
import build123d.topology.one_d as one_d
|
||||
|
||||
from build123d.build_enums import GeomType, PositionMode, Side
|
||||
from build123d.build_line import BuildLine
|
||||
from build123d.geometry import Axis, Color, Location, Plane, Vector
|
||||
from build123d.objects_curve import Curve, Line, PolarLine, Polyline, Spline
|
||||
from build123d.objects_curve import Curve, Line, JernArc, PolarLine, Polyline, Spline
|
||||
from build123d.objects_sketch import Circle, Rectangle, RegularPolygon
|
||||
from build123d.operations_generic import fillet
|
||||
from build123d.topology import Edge, Face, Wire
|
||||
from build123d.topology import Edge, Face, Vertex, Wire
|
||||
from OCP.BRepAdaptor import BRepAdaptor_CompCurve
|
||||
from OCP.gp import gp_Pnt
|
||||
|
||||
|
|
@ -69,6 +70,16 @@ class TestWire(unittest.TestCase):
|
|||
self.assertAlmostEqual(
|
||||
squaroid.length, 4 * (1 - 2 * 0.1) + 2 * math.pi * 0.1, 5
|
||||
)
|
||||
straight_wire = Wire(
|
||||
[
|
||||
Edge.make_line((0, 0), (1, 0)),
|
||||
Edge.make_line((1, 0), (2, 0)),
|
||||
]
|
||||
)
|
||||
straight_vertex = straight_wire.vertices().sort_by_distance((1, 0, 0))[0]
|
||||
unmodified_wire = straight_wire.fillet_2d(0.1, [straight_vertex])
|
||||
self.assertAlmostEqual(unmodified_wire.length, straight_wire.length, 5)
|
||||
self.assertEqual(len(unmodified_wire.edges()), 2)
|
||||
square.wrapped = None
|
||||
with self.assertRaises(ValueError):
|
||||
square.fillet_2d(0.1, square.vertices())
|
||||
|
|
@ -224,18 +235,14 @@ class TestWire(unittest.TestCase):
|
|||
self.assertAlmostEqual(param, i / 20, 6)
|
||||
|
||||
def test_tangent_at_reversed_edges(self):
|
||||
with BuildLine(Plane.YZ) as wing_line:
|
||||
l1 = Line((0, 65), (80 / 2 + 1.526 * 4, 65))
|
||||
PolarLine(
|
||||
l1 @ 1, 20.371288916, direction=Vector(0, 1, 0).rotate(Axis.X, -75)
|
||||
)
|
||||
fillet(wing_line.vertices(), 7)
|
||||
|
||||
w = wing_line.wire()
|
||||
self.assertAlmostEqual(
|
||||
w.tangent_at(0), (0, -0.2588190451025, 0.9659258262891), 6
|
||||
w = Wire(
|
||||
[
|
||||
Line((0, 0), (0, 1)),
|
||||
JernArc((0, 1), (0, 1), 1, -90).reversed(reconstruct=True),
|
||||
]
|
||||
)
|
||||
self.assertAlmostEqual(w.tangent_at(1), (0, -1, 0), 6)
|
||||
self.assertAlmostEqual(w.tangent_at(0), (0, 1, 0), 6)
|
||||
self.assertAlmostEqual(w.tangent_at(1), (1, 0, 0), 6)
|
||||
|
||||
def test_order_edges(self):
|
||||
w1 = Wire(
|
||||
|
|
@ -288,6 +295,77 @@ class TestWire(unittest.TestCase):
|
|||
Wire(bob="fred")
|
||||
|
||||
|
||||
class TestWireFilletHelpers(unittest.TestCase):
|
||||
def test_analyze_wire_fillet_corner_non_planar(self):
|
||||
wire = Wire(
|
||||
[
|
||||
Edge.make_line((0, 0, 0), (1, 0, 0)),
|
||||
Edge.make_line((1, 0, 0), (1, 1, 1)),
|
||||
]
|
||||
)
|
||||
vertex = wire.vertices().sort_by_distance((1, 0, 0))[0]
|
||||
|
||||
mock_find_surface = MagicMock()
|
||||
mock_find_surface.Surface.return_value = object()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"build123d.topology.one_d.BRepLib_FindSurface",
|
||||
return_value=mock_find_surface,
|
||||
),
|
||||
self.assertRaises(ValueError) as ctx,
|
||||
):
|
||||
one_d._analyze_wire_fillet_corner(wire, vertex)
|
||||
self.assertIn("Wire is not planar", str(ctx.exception))
|
||||
|
||||
def test_analyze_wire_fillet_corner_missing_vertex(self):
|
||||
wire = Wire.make_rect(1, 1)
|
||||
vertex = Vertex((10, 10, 0))
|
||||
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
one_d._analyze_wire_fillet_corner(wire, vertex)
|
||||
self.assertIn("Could not find shared vertex on wire", str(ctx.exception))
|
||||
|
||||
def test_analyze_wire_fillet_corner_vertex_not_degree_two(self):
|
||||
wire = Wire.make_rect(1, 1)
|
||||
shared_vertex = wire.vertices()[0]
|
||||
mock_edges = MagicMock()
|
||||
mock_edges.filter_by.return_value = [MagicMock(), MagicMock(), MagicMock()]
|
||||
|
||||
with (
|
||||
patch.object(wire, "edges", return_value=mock_edges),
|
||||
self.assertRaises(ValueError) as ctx,
|
||||
):
|
||||
one_d._analyze_wire_fillet_corner(wire, shared_vertex)
|
||||
self.assertIn("Vertex must connect exactly two edges", str(ctx.exception))
|
||||
|
||||
def test_solve_wire_fillet_corner_geom2dgcc_circ2d2tanrad_no_solution(self):
|
||||
wire = Wire.make_rect(1, 1)
|
||||
corner = one_d._analyze_wire_fillet_corner(wire, wire.vertices()[0])
|
||||
self.assertIsNone(
|
||||
one_d._solve_wire_fillet_corner_geom2dgcc_circ2d2tanrad(corner, 10)
|
||||
)
|
||||
|
||||
def test_fillet_wire_corner_failure_when_all_solvers_fail(self):
|
||||
wire = Wire.make_rect(1, 1)
|
||||
vertex = wire.vertices()[0]
|
||||
|
||||
with (
|
||||
patch(
|
||||
"build123d.topology.one_d._solve_wire_fillet_corner_chfi2d",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"build123d.topology.one_d._solve_wire_fillet_corner_geom2dgcc_circ2d2tanrad",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
one_d._fillet_wire_corner(wire, vertex, 0.1)
|
||||
|
||||
self.assertIn("Fillet algorithm failed", str(ctx.exception))
|
||||
|
||||
|
||||
class TestWireToBSpline(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# A simple rectilinear, multi-segment wire:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue