diff --git a/docs/assets/blend_curve_ex.svg b/docs/assets/blend_curve_ex.svg new file mode 100644 index 0000000..9eaa3b6 --- /dev/null +++ b/docs/assets/blend_curve_ex.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/coincident_ex.svg b/docs/assets/coincident_ex.svg new file mode 100644 index 0000000..9411907 --- /dev/null +++ b/docs/assets/coincident_ex.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/complex_ex.svg b/docs/assets/complex_ex.svg new file mode 100644 index 0000000..c1c60a7 --- /dev/null +++ b/docs/assets/complex_ex.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/complex_sketch.png b/docs/assets/complex_sketch.png new file mode 100644 index 0000000..232216d Binary files /dev/null and b/docs/assets/complex_sketch.png differ diff --git a/docs/assets/enclosing_ex.svg b/docs/assets/enclosing_ex.svg new file mode 100644 index 0000000..7254e40 --- /dev/null +++ b/docs/assets/enclosing_ex.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/intersect_ex.svg b/docs/assets/intersect_ex.svg new file mode 100644 index 0000000..5e114a6 --- /dev/null +++ b/docs/assets/intersect_ex.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/lines_angle_ex.svg b/docs/assets/lines_angle_ex.svg new file mode 100644 index 0000000..19771b9 --- /dev/null +++ b/docs/assets/lines_angle_ex.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/lines_tan2_ex.svg b/docs/assets/lines_tan2_ex.svg new file mode 100644 index 0000000..905c839 --- /dev/null +++ b/docs/assets/lines_tan2_ex.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/lines_tan_pnt_ex.svg b/docs/assets/lines_tan_pnt_ex.svg new file mode 100644 index 0000000..65d6b21 --- /dev/null +++ b/docs/assets/lines_tan_pnt_ex.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/offset_ex.svg b/docs/assets/offset_ex.svg new file mode 100644 index 0000000..54e39c2 --- /dev/null +++ b/docs/assets/offset_ex.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/perpendicular_ex.svg b/docs/assets/perpendicular_ex.svg new file mode 100644 index 0000000..7037b4a --- /dev/null +++ b/docs/assets/perpendicular_ex.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/pnt_center_ex.svg b/docs/assets/pnt_center_ex.svg new file mode 100644 index 0000000..d03e416 --- /dev/null +++ b/docs/assets/pnt_center_ex.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tan2_on_ex.svg b/docs/assets/tan2_on_ex.svg new file mode 100644 index 0000000..ef8d12e --- /dev/null +++ b/docs/assets/tan2_on_ex.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tan2_rad_ex.svg b/docs/assets/tan2_rad_ex.svg new file mode 100644 index 0000000..4794c1a --- /dev/null +++ b/docs/assets/tan2_rad_ex.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tan3_ex.svg b/docs/assets/tan3_ex.svg new file mode 100644 index 0000000..30b0b03 --- /dev/null +++ b/docs/assets/tan3_ex.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tan_rad_on_ex.svg b/docs/assets/tan_rad_on_ex.svg new file mode 100644 index 0000000..b9afecf --- /dev/null +++ b/docs/assets/tan_rad_on_ex.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tangent_circles.svg b/docs/assets/tangent_circles.svg new file mode 100644 index 0000000..4373883 --- /dev/null +++ b/docs/assets/tangent_circles.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tangent_ex.svg b/docs/assets/tangent_ex.svg new file mode 100644 index 0000000..f872fbe --- /dev/null +++ b/docs/assets/tangent_ex.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/tutorial_constraints.rst b/docs/tutorial_constraints.rst new file mode 100644 index 0000000..88c5278 --- /dev/null +++ b/docs/tutorial_constraints.rst @@ -0,0 +1,773 @@ +######################## +Drawing with Constraints +######################## + +************ +Introduction +************ + +CAD constraints are geometric and dimensional rules that define how sketch or assembly entities +relate to one another. They control degrees of freedom (for example, parallel, +perpendicular, tangent, coincident, distance, or angle), so edits preserve design intent instead +of introducing unintended shape changes. This is the foundation of parametric +modeling: behavior is driven by explicit relationships, not fixed manually drawn geometry. +This section only addresses sketch constraints. + +In graphical CAD systems, sketching is usually a two-step workflow: first draw approximate +geometry, then add dimensions and constraints so a global solver can infer exact positions. +That model works well for interactive drawing, but it also encourages tightly coupled +constraint networks that can become difficult to predict and maintain as a design evolves. +It is also typically strongest for lines and circular arcs, with more limited and less +robust behavior for ellipses, splines, and other higher-order curves. + +In build123d, the primary workflow is different. Geometry is defined precisely at creation +time using coordinates, parameters, and explicit geometric relationships in code. Instead of +building a large interdependent constraint graph and asking a global solver to resolve it, +you express intent directly: mirror about a plane, construct tangent features, derive points +and frames from existing topology, and compose operations deterministically. + +This does not eliminate constrained construction; it scopes it. build123d provides targeted +geometric local solvers for common high-value problems, including objects such as +:class:`~objects_curve.BlendCurve`, :class:`~objects_curve.ConstrainedLines`, +:class:`~objects_curve.ConstrainedArcs`, :class:`~objects_curve.IntersectingLine`, and +:class:`~objects_sketch.Triangle`. +It also provides geometric operations that enforce clear relationships +directly, such as :class:`~operations_sketch.make_hull`, :class:`~operations_generic.mirror`, +and :class:`~operations_generic.offset`. Together, these tools solve specific +constraint patterns while keeping model behavior explicit, deterministic, and readable in +code. + +The result is a practical hybrid approach: precise programmatic modeling by default, with +specialized constrained constructors when they provide clear leverage. For most production +parts, this yields robust, maintainable sketches without the overhead and fragility of a +general-purpose sketch solver. + +**************** +Constraint Types +**************** + +build123d supports several practical forms of constrained construction. Rather than relying on +a single global sketch solver, it provides targeted tools that enforce specific geometric +relationships directly and predictably. + +Analytical Constraints +====================== + +:class:`~objects_sketch.Triangle` + Constructs a triangle from any three parameters (side lengths and/or interior angles) + and solves for the others. Angle naming follows standard convention: side ``a`` is opposite + angle ``A``, side ``b`` is opposite angle ``B``, and side ``c`` is opposite angle ``C``. + +Continuity Constraints +====================== + +:class:`~objects_curve.BlendCurve` + Creates a smooth Bézier transition between two existing edges. + + In this context, *continuity* describes how smoothly the new blend joins the input edges + at each endpoint: + + - C0 (positional continuity): endpoints meet, but direction may kink. + - C1 (tangent continuity): endpoints and tangent directions match, giving a visually smooth join with no corner. + - C2 (curvature continuity): endpoints, tangents, and curvature trend match, reducing curvature jumps and producing a smoother fairing. + + :class:`~objects_curve.BlendCurve` builds a Bézier curve that satisfies these endpoint constraints: + + - cubic Bézier for C1 blending (position + first derivative), + - quintic Bézier for C2 blending (position + first and second derivatives). + + The derivatives are sampled from the two source edges at the selected connection points, + then converted into Bézier control points that enforce the requested continuity. + Optional tangent scaling factors let you tune how strongly the blend departs from each + source edge, which adjusts perceived tension and transition shape without changing the + endpoint constraints. + +Geometric Relationship Constraints +================================== + +``@`` and ``%`` operators + Use ``@`` (position-at) and ``%`` (tangent-at) to construct geometry relative to existing + geometry. Typical uses include starting a new edge at an exact point on another edge, + or aligning a new edge direction to a sampled tangent. + +:class:`~operations_generic.mirror` + Enforces symmetry by reflecting geometry about a plane, producing mirrored entities with + exact geometric correspondence to the source. + +Intersection Constraints +======================== + +:class:`~objects_curve.IntersectingLine` + Constructs a line from a point/direction definition and an intersection condition against + another line-like reference, ensuring the resulting geometry satisfies the intersection + requirement. + + +Offset / Equidistance Constraints +================================= + +:class:`~operations_generic.offset` + Creates geometry at a constant normal distance from a source edge or wire. + + This enforces an equidistance relationship commonly used for wall thickness, clearances, + toolpaths, and parallel profile construction. Join behavior (for example at corners) can + be controlled to match the design intent. + +Tangency Constraints +==================== + +:class:`~objects_curve.ConstrainedArcs` and :class:`~objects_curve.ConstrainedLines` + Provide local constrained solving for 2D + line-and-circle constructions. These APIs solve common geometric construction problems + from explicit numeric and geometric constraints relative to existing curves. + + Supported constraint patterns include: + + - circle with specified radius, + - line at a specified angle to another line, + - tangency of a line or circle to a reference curve, + - line or circle passing through a point, + - circle center constrained to a point or to lie on a curve. + + For example, you can construct a circle with a given radius whose center lies on a + specified line and which is tangent to another circle. This style of targeted solving + covers high-value sketch workflows while keeping branch selection explicit and + deterministic in code. + +Multiple Solutions and Qualification +------------------------------------ + +Tangency construction is typically multi-solution. A single problem statement can produce +several valid geometric branches depending on where the solution lies relative to the +reference entities. + +For example, a circle of fixed radius tangent to two secant circles can produce up to +eight valid solutions as shown below. This is expected behavior, not an error. + +.. figure:: ./assets/tangent_circles.svg + :align: center + +To reduce ambiguity, tangency constraints support **qualification** of relative position: + +- ``Tangency.ENCLOSING``: the solution must enclose the argument. +- ``Tangency.ENCLOSED``: the solution must be enclosed by the argument. +- ``Tangency.OUTSIDE``: the solution and argument must be external to each other. +- ``Tangency.UNQUALIFIED``: no positional filtering; all valid branches are returned. + +These qualifiers are intuitive for circles (inside/outside). For general oriented curves, +interior is defined as the left-hand side of the curve with respect to its orientation. + +Even with qualification, more than one solution may remain. In that case, use a +``selector`` to choose deterministic outputs. + +Selecting results +----------------- + +In Algebra mode, select from returned edges after construction: + +.. code-block:: build123d + + arcs = ConstrainedArcs(..., sagitta=Sagitta.BOTH) + chosen = arcs.edges().sort_by(Edge.length)[0] + +In Builder mode, prefer the constructor ``selector`` argument so only desired branches +are added to the active context: + +.. code-block:: build123d + + with BuildLine(): + ConstrainedArcs( + ..., + selector=lambda edges: edges.sort_by_distance((0, 0))[0], + ) + +This combination of qualification + selection gives robust, explicit control over +tangency branch choice. + +****************** +Practical Examples +****************** + +The following examples show how each constraint type is used in production-style sketching. +Each example is intentionally small, with construction geometry kept visible in code so the +relationship logic is explicit and reusable. + +Analytical Constraints +====================== + +build123d includes a built-in :class:`~objects_sketch.Triangle` object that has an internal solver such that one can +specify any three parameters of a triangle and solve for the others. For example: + +.. code-block:: build123d + + >>> isosceles = Triangle(a=30, b=30, C=60) + >>> isosceles.c + 29.999999999999996 + >>> isosceles.A + 60.00000000000001 + >>> isosceles.B + 60.00000000000001 + >>> isosceles.vertex_A + Vertex(-1.7763568394002505e-15, 17.32050807568877, 0.0) + +In this example, side lengths ``a`` and ``b`` with included angle ``C`` are provided. +The object then computes the remaining side, angles, and vertices. This is useful when a +design intent is naturally expressed as triangle dimensions instead of explicit coordinates. + +One can easily use external solvers, say the symbolic solver ``sympy``, within your build123d code +as follows: + +.. code-block:: build123d + + from math import sin, cos, tan, radians + from build123d import * + from ocp_vscode import * + import sympy + + # This problem uses the sympy symbolic math solver + + # Define the symbols for the unknowns + # - the center of the radius 30 arc (x30, y30) + # - the center of the radius 66 arc (x66, y66) + # - end of the 8° line (l8x, l8y) + # - the point with the radius 30 and 66 arc meet i30_66 + # - the start of the horizontal line lh + y30, x66, xl8, yl8 = sympy.symbols("y30 x66 xl8 yl8") + x30 = 77 - 55 / 2 + y66 = 66 + 32 + + # There are 4 unknowns so we need 4 equations + equations = [ + (x66 - x30) ** 2 + (y66 - y30) ** 2 - (66 + 30) ** 2, # distance between centers + xl8 - (x30 + 30 * sin(radians(8))), # 8 degree slope + yl8 - (y30 + 30 * cos(radians(8))), # 8 degree slope + (yl8 - 50) / (55 / 2 - xl8) - tan(radians(8)), # 8 degree slope + ] + # There are two solutions but we want the 2nd one + solution = {k: float(v) for k,v in sympy.solve(equations, dict=True)[1].items()} + + # Create the critical points + c30 = Vector(x30, solution[y30]) + c66 = Vector(solution[x66], y66) + l8 = Vector(solution[xl8], solution[yl8]) + + ... + +This pattern is useful when the governing relationships are algebraic but awkward to +construct directly with primitives. Solve unknown parameters first, then feed the solved +values into standard build123d geometry construction. + + +Continuity Constraints +====================== + +One may want to join two curves with a third curve such that the connector satisfies a +given continuity where they meet as shown here where a semi-circle (on the left) is joined +to a spline (on the right). + +.. code-block:: build123d + + m1 = CenterArc((-2, 0.6), 1, -10, 200).reversed() + m2 = Spline((0.4, -0.6), (1, -1.6), (2, 0)) + connector = BlendCurve(m1, m2, tangent_scalars=(2, 1), continuity=ContinuityLevel.C2) + comb = Curve(Wire([m1, connector, m2]).curvature_comb(200)) + +.. figure:: ./assets/blend_curve_ex.svg + :align: center + +The key call is ``BlendCurve(..., continuity=ContinuityLevel.C2)``. ``C2`` continuity +matches endpoint curvature trend in addition to position and tangent, which reduces visible +fairness breaks at joins. ``tangent_scalars`` controls how strongly the connector departs +from each source curve. + +``curvature_comb`` is used here as a diagnostic. The normal "comb" lines represent local +curvature magnitude; smoother transitions produce gradual comb variation rather than abrupt +spikes. + +Geometric Relationship Constraints +================================== + +Coincident +---------- + +.. code-block:: build123d + + with BuildLine() as coincident_ex: + l1 = Line((0, 0), (1, 2)) + l2 = Line(l1 @ 1, l1 @ 1 + (1, 0)) + +.. figure:: ./assets/coincident_ex.svg + :align: center + +The second line starts at ``l1 @ 1`` (the end of ``l1``), creating an exact coincident +relationship without a separate constraint object. + +Tangent +------- + +.. code-block:: build123d + + with BuildLine() as tangent_ex: + l1 = Line((0, 0), (1, 1)) + l2 = JernArc(start=l1 @ 1, tangent=l1 % 1, radius=1, arc_size=70) + +.. figure:: ./assets/tangent_ex.svg + :align: center + +The arc starts at the line endpoint and uses ``l1 % 1`` as its initial tangent direction. +This is a direct tangent construction: continuity is encoded in the creation call. + +Perpendicular +------------- + +.. code-block:: build123d + + with BuildLine() as perpendicular_ex: + l1 = CenterArc((0, 0), 1.5, 0, 45) + l2 = PolarLine( + start=l1 @ 1, length=1, direction=l1.tangent_at(1).rotate(Axis.Z, -90) + ) + +.. figure:: ./assets/perpendicular_ex.svg + :align: center + +The direction vector is built from ``l1.tangent_at(1)`` rotated by 90 degrees, giving an +explicit perpendicular relationship relative to curve orientation. + +Intersection Constraints +======================== + +.. code-block:: build123d + + with BuildLine() as intersect_ex: + c1 = EllipticalCenterArc((0, 0), 1.2, 1.8, 0, 90, mode=Mode.PRIVATE) + l1 = IntersectingLine( + start=(0, 0), direction=Vector(1, 0).rotate(Axis.Z, 10), other=c1 + ) + l2 = IntersectingLine( + start=(0, 0), direction=Vector(1, 0).rotate(Axis.Z, 80), other=c1 + ) + l3 = add(c1.trim(l1 @ 1, l2 @ 1)) + +.. figure:: ./assets/intersect_ex.svg + :align: center + +:class:`~objects_curve.IntersectingLine` creates each line from a point and direction, then trims it to the +intersection with the ellipse. This is often cleaner than creating long helper lines and +manually trimming afterward. + +Offset / Equidistance Constraints +================================= + +.. code-block:: build123d + + inside = FilletPolyline((1.5, 0), (1.5, 1), (-1.5, 1), (-1.5, 0), radius=0.2) + perimeter = offset(inside, amount=0.2, side=Side.RIGHT) + + +.. figure:: ./assets/offset_ex.svg + :align: center + +:class:`~operations_generic.offset` preserves the source profile shape while enforcing constant wall thickness. +This is a common pattern for clearances, shells, and manufacturing margins. + +Tangency Constraints +==================== + +Both :class:`~objects_curve.ConstrainedArcs` and :class:`~objects_curve.ConstrainedLines` +return a :class:`~topology.Curve` containing one or more :class:`~topology.Edge` objects. + +These constructors solve tangent/contact problems from mixed numeric and geometric inputs. +Because tangency is often ambiguous, multiple valid branches are expected. + + +Multiple solutions +------------------ + +Constraint systems often yield multiple valid results. The ``selector`` callback is the +main tool for choosing the subset to keep. + +.. code-block:: build123d + + # Keep all solutions + ConstrainedArcs(..., selector=lambda arcs: arcs) + + # Keep first + ConstrainedArcs(..., selector=lambda arcs: arcs[0]) + + # Keep shortest + ConstrainedArcs(..., selector=lambda arcs: arcs.sort_by(Edge.length)[0]) + +In Builder mode, omitting ``selector`` can add all solutions to context, which is often +not what you want for production sketches. + +Tangency qualifiers +~~~~~~~~~~~~~~~~~~~ + +Tangency qualifiers come from OCCT and are exposed as ``Tangency``: + +- ``Tangency.UNQUALIFIED``: + no side preference (OCCT ``Unqualified``). +- ``Tangency.OUTSIDE``: + tangent on the exterior side of the target (OCCT ``Outside``). +- ``Tangency.ENCLOSING``: + solution encloses/includes the target (OCCT ``Enclosing``). +- ``Tangency.ENCLOSED``: + solution is enclosed/included by the target (OCCT ``Enclosed``). + +These semantics are most visible for curve-vs-curve constraints (for example circle +to circle, line to circle). In many practical cases, ``UNQUALIFIED`` is a good default +followed by filtering via ``selector``. + +.. code-block:: build123d + + with BuildLine() as egg_plant: + # Construction Geometry + c1 = CenterArc((-2, 0), 0.75, 80, 240, mode=Mode.PRIVATE) + c2 = CenterArc((2, 0), 1, 220, 250, mode=Mode.PRIVATE) + + # egg_plant perimeter + l1 = ConstrainedArcs((c2, Tangency.OUTSIDE), (c1, Tangency.OUTSIDE), radius=6) + l2 = ConstrainedArcs( + (c2, Tangency.ENCLOSING), + (c1, Tangency.ENCLOSING), + radius=8, + selector=lambda a: a.sort_by(Axis.Y)[-1], + ) + l3 = add(c1.trim(l1 @ 1, l2 @ 1)) + l4 = add(c2.trim(l1 @ 0, l2 @ 0)) + +.. figure:: ./assets/enclosing_ex.svg + :align: center + +In the "egg-plant" example, ``Tangency.OUTSIDE`` and ``Tangency.ENCLOSING`` reduce the +candidate branches to the intended outer profile. The selector on ``l2`` then resolves +the remaining ambiguity deterministically by choosing the highest branch in ``Y``. + +OCCT defines exterior/interior using orientation: + +- Circle: exterior is on the right side when traversing by its orientation + (interior/material is on the left side). +- Line/open curve: interior is the left side with respect to traversal direction, + exterior is the opposite side. + +Because of this, changing an input edge direction can change which branches satisfy +``OUTSIDE``/``ENCLOSING``/``ENCLOSED``. + +If qualifier behavior appears inverted, inspect input edge orientation first. + +ConstrainedArcs +--------------- + +Overview +~~~~~~~~ + +:class:`~objects_curve.ConstrainedArcs` supports several signature families for planar circular arcs: + +1. Two tangency/contact objects + fixed radius +2. Two tangency/contact objects + center constrained on a locus +3. Three tangency/contact objects +4. One tangency/contact object + fixed center +5. One tangency/contact object + fixed radius + center constrained on a locus + +``sagitta`` selects short/long/both arc branches: + +- ``Sagitta.SHORT`` +- ``Sagitta.LONG`` +- ``Sagitta.BOTH`` + +In practice, use qualifiers and ``sagitta`` to reduce branch count, then finalize with +``selector`` for deterministic output. + +Signature A: Two constraints + ``radius`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: build123d + + ConstrainedArcs( + tangency_one, + tangency_two, + radius=..., + sagitta=Sagitta.SHORT, + selector=lambda arcs: arcs, + ) + +.. figure:: ./assets/tan2_rad_ex.svg + :align: center + +Use when radius is known and arc must satisfy two contact/tangency conditions. + +Signature B: Two constraints + ``center_on`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: build123d + + ConstrainedArcs( + tangency_one, + tangency_two, + center_on=Axis(...), # or Edge + sagitta=Sagitta.SHORT, + selector=lambda arcs: arcs, + ) + +.. figure:: ./assets/tan2_on_ex.svg + :align: center + +Use when center must lie on a specific line/curve rather than radius being fixed. + +Signature C: Three constraints +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: build123d + + ConstrainedArcs( + tangency_one, + tangency_two, + tangency_three, + sagitta=Sagitta.BOTH, + selector=lambda arcs: arcs, + ) + +.. figure:: ./assets/tan3_ex.svg + :align: center + +Use for "arc tangent/contact to three entities". This can produce several branches; +always consider using ``selector``. + +Signature D: One constraint + fixed ``center`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: build123d + + ConstrainedArcs( + tangency_one, + center=(x, y), + selector=lambda arcs: arcs[0], + ) + +.. figure:: ./assets/pnt_center_ex.svg + :align: center + +Useful for "center-known" constructions. + +Signature E: One constraint + radius + ``center_on`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: build123d + + ConstrainedArcs( + tangency_one, + radius=..., + center_on=some_edge, + selector=lambda arcs: arcs, + ) + +.. figure:: ./assets/tan_rad_on_ex.svg + :align: center + +Useful for guided-center constructions with fixed radius. + +Allowed constraint objects +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For arc constraints, accepted objects include: + +- :class:`~topology.Edge` +- :class:`~geometry.Axis` +- :class:`~geometry.Vertex` / :class:`~geometry.VectorLike` point +- optional qualifier wrapper: ``(object, Tangency.XXX)`` + +ConstrainedLines +---------------- + +Overview +~~~~~~~~ + +:class:`~objects_curve.ConstrainedLines` supports these signature families: + +1. Tangent/contact to two objects +2. Tangent/contact to one object and passing through a fixed point +3. Tangent/contact to one object with fixed orientation (``angle`` or ``direction``) + +Signature A: Two constraints +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: build123d + + ConstrainedLines( + tangency_one, + tangency_two, + selector=lambda lines: lines, + ) + +.. figure:: ./assets/lines_tan2_ex.svg + :align: center + + +Signature B: One constraint + through point +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: build123d + + ConstrainedLines( + tangency_one, + (x, y), # through point + selector=lambda lines: lines, + ) + +.. figure:: ./assets/lines_tan_pnt_ex.svg + :align: center + +Signature C: One constraint + fixed orientation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: build123d + + ConstrainedLines( + tangency_one, + Axis.Y, + angle=30, # OR direction=(dx, dy) + selector=lambda lines: lines, + ) + +.. figure:: ./assets/lines_angle_ex.svg + :align: center + +Exactly one of ``angle`` or ``direction`` should be provided. + +For all signatures, qualifiers can be attached to tangency inputs when side selection +must be controlled. + +Builder vs Algebra mode +----------------------- + +Algebra mode +~~~~~~~~~~~~ + +Use direct assignment and post-selection: + +.. code-block:: build123d + + arcs = ConstrainedArcs(..., sagitta=Sagitta.BOTH) + chosen = arcs.edges().sort_by(Edge.length)[0] + +Builder mode +~~~~~~~~~~~~ + +Prefer selecting inside the call to avoid adding unwanted candidates to context: + +.. code-block:: build123d + + with BuildLine() as bl: + ConstrainedArcs( + ..., + sagitta=Sagitta.BOTH, + selector=lambda arcs: arcs.sort_by(Edge.length)[0], + ) + +Selection recipes +----------------- + +.. code-block:: build123d + + # Nearest to point + selector=lambda edges: edges.sort_by_distance((0, 0))[0] + + # Longest + selector=lambda edges: edges.sort_by(Edge.length)[-1] + + # Right most + selector=lambda edges: edges.sort_by(Axis.X)[-1] + + # Keep two branches + selector=lambda edges: edges[:2] + +Prefer geometric selection criteria (distance, axis ordering, length) over positional +indexing when upstream geometry may change. + +*********************** +Complex Drawing Example +*********************** + +This example pulls many of the techniques described above into a single example +where the following full constrained, complex sketch is converted into build123d code. + +.. figure:: ./assets/complex_sketch.png + :align: center + +When working with a drawing such as this one, the ``ImageFace`` functionality of the +`ocp-vscode `_ viewer is very handy as +it allows the image to be used as a visual guide when creating the sketch. + +Within the following code the following conventions are used: + +- construction geometry is labeled with a ``c_...`` +- arcs are labeled with a ``a`` +- lines and polylines are labeled with a ``l...`` + +The code starts immediately above the origin (arbitrarily set to the origin of the circle) +where a straight line 10° off the x-axis originates. The code then walks around the diagram +clockwise creating the perimeter of the object. + +.. code-block:: build123d + + image = ImageFace( + "complex_sketch.png", + scale=29 / 264, + origin_pixels=(297, 390), + location=Location((0, 0, -0.1)), + ) + + with BuildSketch() as sketch: + with BuildLine() as perimeter: + c_l1 = PolarLine((0, 32 - 14), 50, -10, mode=Mode.PRIVATE) + a19 = ConstrainedArcs(c_l1, (-14 + 81 - 29, -14 - 19 + 57), radius=19) + l2 = Polyline(a19 @ 1, a19 @ 1 + (29 - 5, 0), a19 @ 1 + (29, -5), (-14 + 81, 0)) + l3 = Line(l2 @ 1, (-14 + 81 - 29, (-14 - 19))) + c_l4 = Line((-14, -14), (-14 + 81, -14), mode=Mode.PRIVATE) + c_a29_arc_center = l3.intersect(c_l4)[0] + c_a29 = CenterArc(c_a29_arc_center, 29, 180, 50, mode=Mode.PRIVATE) + l5 = IntersectingLine(l3 @ 1, (-1, 0), c_a29) + a5 = ConstrainedArcs( + c_a29, c_l4, radius=5, selector=lambda a: a.sort_by(Axis.X)[0] + ) + a29 = add(c_a29.trim(l5 @ 1, a5 @ 0)) + l6 = Polyline( + a5 @ 1, + (-14 + 7, -14), + (-14, -14 + 7), + (-14, -14 + 32 - 7), + (-14 + 7, -14 + 32), + (0, -14 + 32), + a19 @ 0, + ) + make_face() + a14 = Circle(14 / 2, mode=Mode.SUBTRACT) + +.. figure:: ./assets/complex_ex.svg + :align: center + +Implementation notes: + +1. Build in traversal order around the perimeter. This keeps references local and makes + later edits easier because each segment depends on nearby geometry. +2. Keep helper entities private (``mode=Mode.PRIVATE``) so only final profile edges + contribute to the resulting face. +3. Use named construction geometry (``c_...``) for intersections and arc centers; this + improves readability and debugability. +4. Use constrained constructors only where they add value (for example :class:`~objects_curve.ConstrainedArcs`), + and use direct primitives elsewhere. +5. Create a :class:`~topology.Face` (``make_face`` then center-hole subtraction) only after + the perimeter is fully defined. + +Troubleshooting +=============== + +- Too many results: + add qualifiers and a stricter ``selector``. +- No results: + relax qualifier (start with ``UNQUALIFIED``) and verify geometry is coplanar. +- Unstable branch selection: + avoid index-only selection when topology changes; prefer geometric sorting. +- Builder mode unexpectedly adds many edges: + provide ``selector`` explicitly in the constructor call. diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 5ae7c96..d32800d 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -11,6 +11,7 @@ as later tutorials build on the concepts introduced in earlier ones. tutorial_design.rst tutorial_selectors.rst + tutorial_constraints.rst tutorial_lego.rst tutorial_joints.rst examples_1.rst