gumyr / build123d

A python CAD programming library
Apache License 2.0
521 stars 88 forks source link

Add a solver #169

Open gumyr opened 1 year ago

gumyr commented 1 year ago

Although not required frequently, having the capability to use a solver while creating a drawing (likely only BuildLine) would be an important addition.

@fpq473 shows a solvespace example:

Taken from the python-solvespace repo https://github.com/KmolYuan/solvespace/blob/b22a5c5d6fd3816ca6f3f4b4e8dcb41222faa0c2/cython/test/test_slvs.py#L122-L146:

I think this means: find p0,p1,p2,p3 such that:

  • p0 to p2 is horizontal
  • p3 is a specific distance from the line between p1 to p2
  • p0 is a specific distance from p1
  • p1 is a specific distance from p2
        h0 = 0.5
        b0 = 0.75
        r0 = 0.25
        n1 = 1.5
        n2 = 2.3
        l0 = 3.25

        sys = SolverSystem()
        wp = sys.create_2d_base()
        p0 = sys.add_point_2d(0, 0, wp)
        sys.dragged(p0, wp)

        p1 = sys.add_point_2d(2, 2, wp)
        p2 = sys.add_point_2d(2, 0, wp)
        line0 = sys.add_line_2d(p0, p2, wp)
        sys.horizontal(line0, wp)

        line1 = sys.add_line_2d(p1, p2, wp)
        p3 = sys.add_point_2d(b0 / 2, h0, wp)
        sys.dragged(p3, wp)
        sys.distance(p3, line1, r0, wp)
        sys.distance(p0, p1, n1, wp)
        sys.distance(p1, p2, n2, wp)

        result_flag = sys.solve()

where dragged refers to a list of parameters that are being dragged; these are the ones that we should put as close as possible to their initial positions.

Here are a couple proposals for how solvespace might be integrated in b3d: 1) From @johnmeacham:

W = newScalar()
 rectangle(w, 3)
Circle(w) 

Will create a rectangle and circle that are constrained to have the same width but that width isn't known yet and may be solved for later. Then you have mechanisms for expressing other constraints but for most equality constraints which are the most common something like this will work.

And primitives can have some extra methods to add constraints so maybe

R = rectangle(w,3)
Circle(w).tangent_to(r.top) 

will add a constraint that the top line of that rectangle is tangent to a circle of the same radius.

2) From @fpq473:

r = Rect(height=3)
c = Circ()
constrain_equal(r.width, c.radius)
constrain_tangent(c, r.top)

Initial Proposal

At first glance, it seems as though having BuildLine accept constraints as parameters such as points or radii in addition to fixed values would fit the best. All constraints would be evaluated when BuildLine exits resulting in concrete object. Getting constraints to work with all BuildLine objects could be challenging though.

It might look something like:

p1 = Constraint(c_type=C_Type.POINT)
p2 = Constraint(c_type=C_Type.POINT)
r = Constraint(c_type=C_Type.DISTANCE)
d = Constraint(c_type=C_Type.DISTANCE)
a1 = Constraint(c_type=C_Type.ANGLE)
a2 = Constraint(c_type=C_Type.ANGLE)

with BuildSketch() as sketch_builder:
    with BuildLine(workplane=Plane.XY) as solved_line:
        l1 = Line((0, 0),p1).constrain(Constraint.HORIZONTAL)
        l2 = PolarLine((0, 0), length=d, angle=a1)
        l3 = CenterArc(l1 @ 1, radius = r, start_angle=0, arc_size=a2)
        solved_line.constrain(l2@1, l3@1, Constraint.EQUAL)
        ...
    MakeFace()

The BuildLine.__exit__ could fail if there is no solution, otherwise sketch_builder would have a new Face to work with as normal.

Would this be the type of thing the community is looking for?

Dash-Lambda commented 10 months ago

A solver would be great to have.

One way to explicitly represent it in the builder API would be to have a dedicated "BuildConstrainedLine" context of some sort, with objects to create partially-specified shapes and relationships between them.

Using an example sketch from jamarzka on the Discord: constraint_example

You could write this sketch out something like this:

with BuildPart() as constraint_based_arrowhead:
    with BuildSketch():
        with BuildConstrainedLine():
            outer_left = FreeLine()
            outer_right = FreeLine()
            inner_left = FreeLine()
            inner_right = FreeLine()

            top_arc = FreeArc(
                center = (0, 1.5),
                radius = 0.125,
                dir = Direction.CLOCKWISE)
            left_arc = FreeArc(
                center = (None, -0.4375),
                radius = 0.1875,
                dir = Direction.CLOCKWISE)
            right_arc = FreeArc(
                center = (None, -0.4375),
                radius = 0.1875,
                dir = Direction.CLOCKWISE)
            middle_arc = FreeArc(
                center = (0, 0),
                radius = 0.25,
                dir = Direction.ANTICLOCKWISE)

            ConstrainTangent(left_arc @ 1, outer_left @ 0)
            ConstrainTangent(outer_left @ 1, top_arc @ 0)
            ConstrainTangent(top_arc @ 1, outer_right @ 0)
            ConstrainTangent(outer_right @ 1, right_arc @ 0)
            ConstrainTangent(right_arc @ 1, inner_right @ 0)
            ConstrainTangent(inner_right @ 1, middle_arc @ 0)
            ConstrainTangent(middle_arc @ 1, inner_left @ 0)
            ConstrainTangent(inner_left @ 1, left_arc @ 0)
            ConstrainNoOverlap(outer_left, outer_right, inner_left, inner_right)
            ConstrainDistance(left_arc, right_arc, 19,
                mode = DistanceMode.FARTHEST_POINT)
    extrude(amount = 1)
ZacharyKF commented 7 months ago

One of the defining features of constraints is that they don't exist in isolation; If a sketch is fully constrained except for one part with unconstrained geometry, that could be trivially found via math or use of a specific definition of the geometry (JernArc vs ThreePointArc for example).

The interesting things happen when two or more unconstrained geometries interact. I imagine the constraint API being defined as one of the following two options:

  1. Parameters noted as Fuzzy. So for example, JernArc(start=(0,0), tangent=(1,0), radius=10, arc_size=Fuzzy(300) )
    • Fuzzy parameters can also be unspecified, allowing for Fuzzy() & Fuzzy(x)
    • Fuzzy is inherited, so if a JernArc is fuzzy, and the @/%/^ operators are used, then the value is fuzzy.
  2. Distinct fuzzy versions of geometry FuzzyJernArc ect.

I'll use 1 for my example, but the idea is the same for 2. The general idea is that fuzzy geometry is tracked separately until enough parameters are provided to solve them.

with BuildPart() as bp:
    with BuildSketch() as bs:
        line_a = Line([(0,0), (1,0)])
        line_b = Line([(3,2), (4,2)])

        arc_a = JernArc(
            start = line_a @ 1,
            tangent = line_b % 1,
            radius = Fuzzy(1),
            arc_size = Fuzzy(90)
        )

        # Everything will resolve here since enough parameters have been provided
        arc_b = JernArc (
            start = line_b @ 0,
            tangent = line_b % 0,
            radius = Fuzzy(1),
            arc_size = Fuzzy(90),
            end_of_arc = arc_a @ 1,
        )

Internally this could be tracked by a flag on classes, and interactions between fuzzy and non-fuzzy geometry could throw an error.