From de041b6cbc2e978adede2a539188aef50dc452d2 Mon Sep 17 00:00:00 2001 From: javimixet Date: Mon, 4 May 2026 12:39:44 +0200 Subject: [PATCH] fix: adjust tangency parameters for enclosed circles in tangent arc Fixes [#1294](https://github.com/gumyr/build123d/issues/1294). When generating constrained tangent arcs, the tangency parameter on the solution circle must be shifted by `pi` if the generated circle is fully enclosed within one of the target tangent circular edges. * Added `_enclosed_circ_param_offset` to automatically detect when a solution circle is enclosed by a circular tangency target and apply the necessary `pi` offset to its parameters. * Applied the offset correction to the `_make_2tan_rad_arcs` and `_make_3tan_arcs` solver functions. * Added a guard in `_make_3tan_arcs` to raise a `RuntimeError` if no common tangent arcs can be generated after evaluating all GCC solutions. * Imported `pi` from `math` and `GeomAbs_CurveType` from `OCP.GeomAbs` to support the new geometric checks. --- src/build123d/topology/constrained_lines.py | 34 ++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/build123d/topology/constrained_lines.py b/src/build123d/topology/constrained_lines.py index ff1b4e05..ea662da4 100644 --- a/src/build123d/topology/constrained_lines.py +++ b/src/build123d/topology/constrained_lines.py @@ -29,7 +29,7 @@ license: from __future__ import annotations -from math import atan2, cos, isnan, sin +from math import atan2, cos, isnan, sin, pi from typing import overload, TYPE_CHECKING, Callable, TypeVar from typing import cast as tcast @@ -59,6 +59,7 @@ from OCP.Geom2dGcc import ( Geom2dGcc_Lin2d2Tan, Geom2dGcc_QualifiedCurve, ) +from OCP.GeomAbs import GeomAbs_CurveType from OCP.GeomAPI import GeomAPI from OCP.gp import ( gp_Ax2d, @@ -270,6 +271,30 @@ def _qstr(q) -> str: # pragma: no cover return str(int(q)) +def _enclosed_circ_param_offset( + tangent_tuples: list[tuple[Edge, Tangency]], + circ: gp_Circ2d, + params: list[float], +) -> list[float]: + """ + Adjusts the circle parameters by adding pi if the solution circle is + enclosed within a tangent circular edge. + """ + center_pnt = circ.Location() + center_vrt = Vector(center_pnt.X(), center_pnt.Y(), 0) + + pars = list(params) + for i, par in enumerate(params): + edg = tangent_tuples[i][0] + if isinstance(edg.wrapped, TopoDS_Edge): + adapt = BRepAdaptor_Curve(edg.wrapped) + if adapt.GetType() == GeomAbs_CurveType.GeomAbs_Circle: + if (center_vrt - edg.arc_center).length < edg.radius: + pars[i] = par + pi + + return pars + + def _make_2tan_rad_arcs( *tangencies: tuple[Edge, Tangency] | Edge | Vector, # 2 radius: float, @@ -431,6 +456,8 @@ def _make_2tan_on_arcs( if not _ok(1, u_arg2): continue + u_circ1, u_circ2 = _enclosed_circ_param_offset(tangent_tuples, circ, [u_circ1, u_circ2]) + # Build sagitta arc(s) and select by LengthConstraint if sagitta == Sagitta.BOTH: solutions.extend(_two_arc_edges_from_params(circ, u_circ1, u_circ2)) @@ -521,6 +548,8 @@ def _make_3tan_arcs( if not _ok(2, u_arg3): continue + u_circ1, u_circ2, _u_circ3 = _enclosed_circ_param_offset(tangent_tuples, circ, [u_circ1, u_circ2, _u_circ3]) + # Build arc(s) between u_circ1 and u_circ2 per LengthConstraint if sagitta == Sagitta.BOTH: out_topos.extend(_two_arc_edges_from_params(circ, u_circ1, u_circ2)) @@ -532,6 +561,9 @@ def _make_3tan_arcs( ) out_topos.append(arcs[sagitta.value]) + if len(out_topos) == 0: + raise RuntimeError("Unable to find common tangent arc(s)") + return ShapeList([edge_factory(e) for e in out_topos])