Fixing typing
Some checks are pending
benchmarks / benchmarks (macos-13, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Run type checker / typecheck (3.10) (push) Waiting to run
Run type checker / typecheck (3.13) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-13, 3.10) (push) Waiting to run
tests / tests (macos-13, 3.13) (push) Waiting to run
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.13) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.13) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.13) (push) Waiting to run

This commit is contained in:
gumyr 2025-09-14 19:09:29 -04:00
parent f0f79fccd4
commit d8f7da348c
3 changed files with 68 additions and 52 deletions

View file

@ -37,11 +37,12 @@ from OCP.BRep import BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_Curve from OCP.BRepAdaptor import BRepAdaptor_Curve
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
from OCP.GCPnts import GCPnts_AbscissaPoint from OCP.GCPnts import GCPnts_AbscissaPoint
from OCP.Geom import Geom_Plane from OCP.Geom import Geom_Curve, Geom_Plane
from OCP.Geom2d import ( from OCP.Geom2d import (
Geom2d_CartesianPoint, Geom2d_CartesianPoint,
Geom2d_Circle, Geom2d_Circle,
Geom2d_Curve, Geom2d_Curve,
Geom2d_Point,
Geom2d_TrimmedCurve, Geom2d_TrimmedCurve,
) )
from OCP.Geom2dAdaptor import Geom2dAdaptor_Curve from OCP.Geom2dAdaptor import Geom2dAdaptor_Curve
@ -73,7 +74,7 @@ from OCP.TopoDS import TopoDS_Edge
from build123d.build_enums import Sagitta, Tangency from build123d.build_enums import Sagitta, Tangency
from build123d.geometry import TOLERANCE, Vector, VectorLike from build123d.geometry import TOLERANCE, Vector, VectorLike
from .zero_d import Vertex from .zero_d import Vertex
from .shape_core import ShapeList from .shape_core import ShapeList, downcast
if TYPE_CHECKING: if TYPE_CHECKING:
from build123d.topology.one_d import Edge from build123d.topology.one_d import Edge
@ -114,7 +115,7 @@ def _forward_delta(u1: float, u2: float, first: float, period: float) -> float:
# --------------------------- # ---------------------------
def _edge_to_qualified_2d( def _edge_to_qualified_2d(
edge: TopoDS_Edge, position_constaint: Tangency edge: TopoDS_Edge, position_constaint: Tangency
) -> tuple[Geom2dGcc_QualifiedCurve, Geom2d_Curve, float, float]: ) -> tuple[Geom2dGcc_QualifiedCurve, Geom2d_Curve, float, float, Geom2dAdaptor_Curve]:
"""Convert a TopoDS_Edge into 2d curve & extract properties""" """Convert a TopoDS_Edge into 2d curve & extract properties"""
# 1) Underlying curve + range (also retrieve location to be safe) # 1) Underlying curve + range (also retrieve location to be safe)
@ -128,7 +129,7 @@ def _edge_to_qualified_2d(
# 2) Apply location if the edge is positioned by a TopLoc_Location # 2) Apply location if the edge is positioned by a TopLoc_Location
if not loc.IsIdentity(): if not loc.IsIdentity():
trsf = loc.Transformation() trsf = loc.Transformation()
hcurve3d = hcurve3d.Transformed(trsf) hcurve3d = tcast(Geom_Curve, hcurve3d.Transformed(trsf))
# 3) Convert to 2D on Plane.XY (Z-up frame at origin) # 3) Convert to 2D on Plane.XY (Z-up frame at origin)
hcurve2d = GeomAPI.To2d_s(hcurve3d, _pln_xy) # -> Handle_Geom2d_Curve hcurve2d = GeomAPI.To2d_s(hcurve3d, _pln_xy) # -> Handle_Geom2d_Curve
@ -147,13 +148,17 @@ def _edge_from_circle(h2d_circle: Geom2d_Circle, u1: float, u2: float) -> TopoDS
return BRepBuilderAPI_MakeEdge(arc2d, _surf_xy).Edge() return BRepBuilderAPI_MakeEdge(arc2d, _surf_xy).Edge()
def _param_in_trim(u: float, first: float, last: float, h2d: Geom2d_Curve) -> bool: def _param_in_trim(
u: float | None, first: float | None, last: float | None, h2d: Geom2d_Curve | None
) -> bool:
"""Normalize (if periodic) then test [first, last] with tolerance.""" """Normalize (if periodic) then test [first, last] with tolerance."""
if u is None or first is None or last is None or h2d is None: # for typing
raise TypeError("Invalid parameters to _param_in_trim")
u = _norm_on_period(u, first, h2d.Period()) if h2d.IsPeriodic() else u u = _norm_on_period(u, first, h2d.Period()) if h2d.IsPeriodic() else u
return (u >= first - TOLERANCE) and (u <= last + TOLERANCE) return (u >= first - TOLERANCE) and (u <= last + TOLERANCE)
def _as_gcc_arg(obj: Edge | Vertex | VectorLike, constaint: Tangency) -> tuple[ def _as_gcc_arg(obj: Edge | Vector, constaint: Tangency) -> tuple[
Geom2dGcc_QualifiedCurve | Geom2d_CartesianPoint, Geom2dGcc_QualifiedCurve | Geom2d_CartesianPoint,
Geom2d_Curve | None, Geom2d_Curve | None,
float | None, float | None,
@ -166,16 +171,18 @@ def _as_gcc_arg(obj: Edge | Vertex | VectorLike, constaint: Tangency) -> tuple[
- Edge -> (QualifiedCurve, h2d, first, last, True) - Edge -> (QualifiedCurve, h2d, first, last, True)
- Vertex/VectorLike -> (CartesianPoint, None, None, None, False) - Vertex/VectorLike -> (CartesianPoint, None, None, None, False)
""" """
if obj.wrapped is None:
raise TypeError("Can't create a qualified curve from empty edge")
if isinstance(obj.wrapped, TopoDS_Edge): if isinstance(obj.wrapped, TopoDS_Edge):
return _edge_to_qualified_2d(obj.wrapped, constaint)[0:4] + (True,) return _edge_to_qualified_2d(obj.wrapped, constaint)[0:4] + (True,)
loc_xyz = obj.position if isinstance(obj, Vertex) else Vector()
try: try:
base = Vector(obj) base = Vector(obj)
except (TypeError, ValueError) as exc: except (TypeError, ValueError) as exc:
raise ValueError("Expected Edge | Vertex | VectorLike") from exc raise ValueError("Expected Edge | Vertex | VectorLike") from exc
gp_pnt = gp_Pnt2d(base.X + loc_xyz.X, base.Y + loc_xyz.Y) gp_pnt = gp_Pnt2d(base.X, base.Y)
return Geom2d_CartesianPoint(gp_pnt), None, None, None, False return Geom2d_CartesianPoint(gp_pnt), None, None, None, False
@ -230,8 +237,8 @@ def _make_2tan_rad_arcs(
*tangencies: tuple[Edge, Tangency] | Edge | Vector, # 2 *tangencies: tuple[Edge, Tangency] | Edge | Vector, # 2
radius: float, radius: float,
sagitta: Sagitta = Sagitta.SHORT, sagitta: Sagitta = Sagitta.SHORT,
edge_factory: Callable[[TopoDS_Edge], TWrap], edge_factory: Callable[[TopoDS_Edge], Edge],
) -> list[Edge]: ) -> ShapeList[Edge]:
""" """
Create all planar circular arcs of a given radius that are tangent/contacting Create all planar circular arcs of a given radius that are tangent/contacting
the two provided objects on the XY plane. the two provided objects on the XY plane.
@ -261,11 +268,9 @@ def _make_2tan_rad_arcs(
] ]
# Build inputs for GCC # Build inputs for GCC
q_o, h_e, e_first, e_last, is_edge = [[None] * 2 for _ in range(5)] results = [_as_gcc_arg(*t) for t in tangent_tuples]
for i in range(len(tangent_tuples)): q_o: tuple[Geom2dGcc_QualifiedCurve, Geom2dGcc_QualifiedCurve]
q_o[i], h_e[i], e_first[i], e_last[i], is_edge[i] = _as_gcc_arg( q_o, h_e, e_first, e_last, is_edge = map(tuple, zip(*results))
*tangent_tuples[i]
)
gcc = Geom2dGcc_Circ2d2TanRad(*q_o, radius, TOLERANCE) gcc = Geom2dGcc_Circ2d2TanRad(*q_o, radius, TOLERANCE)
if not gcc.IsDone() or gcc.NbSolutions() == 0: if not gcc.IsDone() or gcc.NbSolutions() == 0:
@ -280,7 +285,7 @@ def _make_2tan_rad_arcs(
# --------------------------- # ---------------------------
# Solutions # Solutions
# --------------------------- # ---------------------------
solutions: list[Edge] = [] solutions: list[TopoDS_Edge] = []
for i in range(1, gcc.NbSolutions() + 1): for i in range(1, gcc.NbSolutions() + 1):
circ = gcc.ThisSolution(i) # gp_Circ2d circ = gcc.ThisSolution(i) # gp_Circ2d
@ -322,7 +327,7 @@ def _make_2tan_on_arcs(
*tangencies: tuple[Edge, Tangency] | Edge | Vector, # 2 *tangencies: tuple[Edge, Tangency] | Edge | Vector, # 2
center_on: Edge, center_on: Edge,
sagitta: Sagitta = Sagitta.SHORT, sagitta: Sagitta = Sagitta.SHORT,
edge_factory: Callable[[TopoDS_Edge], TWrap], edge_factory: Callable[[TopoDS_Edge], Edge],
) -> ShapeList[Edge]: ) -> ShapeList[Edge]:
""" """
Create all planar circular arcs whose circle is tangent to two objects and whose Create all planar circular arcs whose circle is tangent to two objects and whose
@ -335,29 +340,29 @@ def _make_2tan_on_arcs(
# Unpack optional per-edge qualifiers (default UNQUALIFIED) # Unpack optional per-edge qualifiers (default UNQUALIFIED)
tangent_tuples = [ tangent_tuples = [
t if isinstance(t, tuple) else (t, Tangency.UNQUALIFIED) for t in tangencies t if isinstance(t, tuple) else (t, Tangency.UNQUALIFIED)
for t in list(tangencies) + [center_on]
] ]
# Build inputs for GCC # Build inputs for GCC
q_o, h_e, e_first, e_last, is_edge = [[None] * 3 for _ in range(5)] results = [_as_gcc_arg(*t) for t in tangent_tuples]
for i in range(len(tangent_tuples)): q_o: tuple[
q_o[i], h_e[i], e_first[i], e_last[i], is_edge[i] = _as_gcc_arg( Geom2dGcc_QualifiedCurve, Geom2dGcc_QualifiedCurve, Geom2dGcc_QualifiedCurve
*tangent_tuples[i] ]
) q_o, h_e, e_first, e_last, is_edge = map(tuple, zip(*results))
adapt_on = Geom2dAdaptor_Curve(h_e[2], e_first[2], e_last[2])
# Build center locus ("On") input
_, h_on2d, e_first[2], e_last[2], adapt_on = _edge_to_qualified_2d(
center_on.wrapped, Tangency.UNQUALIFIED
)
is_edge[2] = True
# Provide initial middle guess parameters for all of the edges # Provide initial middle guess parameters for all of the edges
guesses = [(e_last[i] - e_first[i]) / 2 + e_first[i] for i in range(len(is_edge))] guesses: tuple[float, float, float] = tuple(
[(e_last[i] - e_first[i]) / 2 + e_first[i] for i in range(len(is_edge))]
)
if sum(is_edge) > 1: if sum(is_edge) > 1:
gcc = Geom2dGcc_Circ2d2TanOn(*q_o[0:2], adapt_on, TOLERANCE, *guesses) gcc = Geom2dGcc_Circ2d2TanOn(q_o[0], q_o[1], adapt_on, TOLERANCE, *guesses)
else: else:
gcc = Geom2dGcc_Circ2d2TanOn(*q_o[0:2], adapt_on, TOLERANCE) assert isinstance(q_o[0], Geom2d_Point)
assert isinstance(q_o[1], Geom2d_Point)
gcc = Geom2dGcc_Circ2d2TanOn(q_o[0], q_o[1], adapt_on, TOLERANCE)
if not gcc.IsDone() or gcc.NbSolutions() == 0: if not gcc.IsDone() or gcc.NbSolutions() == 0:
raise RuntimeError("Unable to find a tangent arc with center_on constraint") raise RuntimeError("Unable to find a tangent arc with center_on constraint")
@ -391,7 +396,7 @@ def _make_2tan_on_arcs(
center2d = circ.Location() # gp_Pnt2d center2d = circ.Location() # gp_Pnt2d
# Project center onto the (trimmed) 2D locus # Project center onto the (trimmed) 2D locus
proj = Geom2dAPI_ProjectPointOnCurve(center2d, h_on2d) proj = Geom2dAPI_ProjectPointOnCurve(center2d, h_e[2])
if proj.NbPoints() == 0: if proj.NbPoints() == 0:
continue # no projection -> reject continue # no projection -> reject
@ -401,7 +406,7 @@ def _make_2tan_on_arcs(
continue continue
# Respect the trimmed interval (handles periodic curves too) # Respect the trimmed interval (handles periodic curves too)
if not _param_in_trim(u_on, e_first[2], e_last[2], h_on2d): if not _param_in_trim(u_on, e_first[2], e_last[2], h_e[2]):
continue continue
# Build sagitta arc(s) and select by LengthConstraint # Build sagitta arc(s) and select by LengthConstraint
@ -422,7 +427,7 @@ def _make_2tan_on_arcs(
def _make_3tan_arcs( def _make_3tan_arcs(
*tangencies: tuple[Edge, Tangency] | Edge | Vector, # 3 *tangencies: tuple[Edge, Tangency] | Edge | Vector, # 3
sagitta: Sagitta = Sagitta.SHORT, sagitta: Sagitta = Sagitta.SHORT,
edge_factory: Callable[[TopoDS_Edge], TWrap], edge_factory: Callable[[TopoDS_Edge], Edge],
) -> ShapeList[Edge]: ) -> ShapeList[Edge]:
""" """
Create planar circular arc(s) on XY tangent to three provided objects. Create planar circular arc(s) on XY tangent to three provided objects.
@ -439,14 +444,16 @@ def _make_3tan_arcs(
] ]
# Build inputs for GCC # Build inputs for GCC
q_o, h_e, e_first, e_last, is_edge = [[None] * 3 for _ in range(5)] results = [_as_gcc_arg(*t) for t in tangent_tuples]
for i in range(len(tangent_tuples)): q_o: tuple[
q_o[i], h_e[i], e_first[i], e_last[i], is_edge[i] = _as_gcc_arg( Geom2dGcc_QualifiedCurve, Geom2dGcc_QualifiedCurve, Geom2dGcc_QualifiedCurve
*tangent_tuples[i] ]
) q_o, h_e, e_first, e_last, is_edge = map(tuple, zip(*results))
# Provide initial middle guess parameters for all of the edges # Provide initial middle guess parameters for all of the edges
guesses = [(e_last[i] - e_first[i]) / 2 + e_first[i] for i in range(len(is_edge))] guesses: tuple[float, float, float] = tuple(
[(e_last[i] - e_first[i]) / 2 + e_first[i] for i in range(len(is_edge))]
)
# Generate all valid circles tangent to the 3 inputs # Generate all valid circles tangent to the 3 inputs
gcc = Geom2dGcc_Circ2d3Tan(*q_o, TOLERANCE, *guesses) gcc = Geom2dGcc_Circ2d3Tan(*q_o, TOLERANCE, *guesses)
@ -505,7 +512,7 @@ def _make_tan_cen_arcs(
tangency: tuple[Edge, Tangency] | Edge | Vector, tangency: tuple[Edge, Tangency] | Edge | Vector,
*, *,
center: VectorLike | Vertex, center: VectorLike | Vertex,
edge_factory: Callable[[TopoDS_Edge], TWrap], edge_factory: Callable[[TopoDS_Edge], Edge],
) -> ShapeList[Edge]: ) -> ShapeList[Edge]:
""" """
Create planar circle(s) on XY whose center is fixed and that are tangent/contacting Create planar circle(s) on XY whose center is fixed and that are tangent/contacting
@ -530,7 +537,7 @@ def _make_tan_cen_arcs(
# Build fixed center (gp_Pnt2d) # Build fixed center (gp_Pnt2d)
# --------------------------- # ---------------------------
if isinstance(center, Vertex): if isinstance(center, Vertex):
loc_xyz = center.position loc_xyz = center.position if center.position is not None else Vector(0, 0)
base = Vector(center) base = Vector(center)
c2d = gp_Pnt2d(base.X + loc_xyz.X, base.Y + loc_xyz.Y) c2d = gp_Pnt2d(base.X + loc_xyz.X, base.Y + loc_xyz.Y)
else: else:
@ -560,6 +567,7 @@ def _make_tan_cen_arcs(
solutions_topo.append(_edge_from_circle(h2d, 0.0, per)) solutions_topo.append(_edge_from_circle(h2d, 0.0, per))
else: else:
assert isinstance(q_o1, Geom2dGcc_QualifiedCurve)
# Case B: tangency target is a curve/edge (qualified curve) # Case B: tangency target is a curve/edge (qualified curve)
gcc = Geom2dGcc_Circ2dTanCen(q_o1, Geom2d_CartesianPoint(c2d), TOLERANCE) gcc = Geom2dGcc_Circ2dTanCen(q_o1, Geom2d_CartesianPoint(c2d), TOLERANCE)
if not gcc.IsDone() or gcc.NbSolutions() == 0: if not gcc.IsDone() or gcc.NbSolutions() == 0:
@ -589,7 +597,7 @@ def _make_tan_on_rad_arcs(
*, *,
center_on: Edge, center_on: Edge,
radius: float, radius: float,
edge_factory: Callable[[TopoDS_Edge], TWrap], edge_factory: Callable[[TopoDS_Edge], Edge],
) -> ShapeList[Edge]: ) -> ShapeList[Edge]:
""" """
Create planar circle(s) on XY that: Create planar circle(s) on XY that:

View file

@ -1741,7 +1741,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
tangency_args = [ tangency_args = [
t for t in (tangency_one, tangency_two, tangency_three) if t is not None t for t in (tangency_one, tangency_two, tangency_three) if t is not None
] ]
tangencies = [] tangencies: list[tuple[Edge, Tangency] | Edge | Vector] = []
for tangency_arg in tangency_args: for tangency_arg in tangency_args:
if isinstance(tangency_arg, Edge): if isinstance(tangency_arg, Edge):
tangencies.append(tangency_arg) tangencies.append(tangency_arg)
@ -1749,18 +1749,18 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
if isinstance(tangency_arg, tuple) and isinstance(tangency_arg[0], Edge): if isinstance(tangency_arg, tuple) and isinstance(tangency_arg[0], Edge):
tangencies.append(tangency_arg) tangencies.append(tangency_arg)
continue continue
# if not Edges or constrained Edges convert to Vectors if isinstance(tangency_arg, Vertex):
tangencies.append(Vector(tangency_arg) + tangency_arg.position)
continue
# if not Edges, constrained Edges or Vertex convert to Vectors
try: try:
tangencies.append(Vector(tangency_arg)) tangencies.append(Vector(tangency_arg))
except Exception as exc: except Exception as exc:
raise TypeError(f"Invalid tangency: {tangency_arg!r}") from exc raise TypeError(f"Invalid tangency: {tangency_arg!r}") from exc
# Sort the tangency inputs so points are always last # # Sort the tangency inputs so points are always last
tangent_tuples = [t if isinstance(t, tuple) else (t, None) for t in tangencies] tangencies = sorted(tangencies, key=lambda x: isinstance(x, Vector))
tangent_tuples = sorted(
tangent_tuples, key=lambda t: not issubclass(type(t[0]), Edge)
)
tangencies = [t[0] if t[1] is None else t for t in tangent_tuples]
tan_count = len(tangencies) tan_count = len(tangencies)
if not (1 <= tan_count <= 3): if not (1 <= tan_count <= 3):

View file

@ -114,6 +114,14 @@ def test_tan2_rad_arcs_3():
assert len(tan2_rad_edges) == 2 assert len(tan2_rad_edges) == 2
def test_tan2_rad_arcs_4():
"""edge & 1 points & radius"""
# the point should be automatically moved after the edge
e1 = Line((0, 0), (1, 0))
tan2_rad_edges = Edge.make_constrained_arcs((0, 0.5), e1, radius=0.5)
assert len(tan2_rad_edges) == 1
def test_tan2_center_on_1(): def test_tan2_center_on_1():
"""2 tangents & center on""" """2 tangents & center on"""
c1 = PolarLine((0, 0), 4, -20, length_mode=LengthMode.HORIZONTAL) c1 = PolarLine((0, 0), 4, -20, length_mode=LengthMode.HORIZONTAL)