mirror of
https://github.com/gumyr/build123d.git
synced 2026-03-10 08:42:06 -07:00
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 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
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-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.14) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.14) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.14) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.14) (push) Waiting to run
Run type checking / typecheck (3.10) (push) Waiting to run
Run type checking / typecheck (3.14) (push) Waiting to run
773 lines
26 KiB
ReStructuredText
773 lines
26 KiB
ReStructuredText
########################
|
|
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 <https://github.com/bernhard-42/vscode-ocp-cad-viewer>`_ 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<radius>``
|
|
- 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.
|