gumyr / build123d

A python CAD programming library
Apache License 2.0
402 stars 75 forks source link

Possible integration of Joints in builders #226

Closed JonnyHaystack closed 11 months ago

JonnyHaystack commented 1 year ago

Would be cool to be able to add Joints within builders, rather than having to do it afterwards.

For example, instead of the following

with BuildPart() as plate:
    Box(50, 20, 8)

    with BuildSketch(plate.faces().sort_by(Axis.Z).first) as sk:
            with GridLocations(40, 15, 2, 2) as screw_holes:
                Circle(1.3)
                screw_locations = screw_holes.locations
    extrude(amount=-8, mode=Mode.SUBTRACT)

with BuildPart() as screw:
    with BuildSketch() as screw_sk:
        Circle(1.2)
    extrude(amount=8)

screws = []
for idx, loc in enumerate(screw_locations):
    screw_copy = copy.copy(screw.part)
    screw_copy.label = f"Screw {idx}"
    screw_hole_joint = RigidJoint(f"screw_hole{idx}", plate.part, loc)
    screw_head_joint = LinearJoint("head", screw_copy, linear_range=(-4, 8))
    screw_hole_joint.connect_to(screw_head_joint, position=0)
    screws.append(screw_copy)

plate.part.label = "Plate"

assembly = Compound(
    label="Assembly",
    children=[plate.part, *screws],
)

It would be nice if you could just declare the Joint inside a locations context in a builder, and have that added to the resulting part, something like this:

with BuildPart() as plate:
    Box(50, 20, 8)

    with BuildSketch(plate.faces().sort_by(Axis.Z).first) as sk:
            with GridLocations(40, 15, 2, 2) as screw_holes:
                Circle(1.3)
                RigidJoint("screw_hole") # Automatically gets part and location from context
    extrude(amount=-8, mode=Mode.SUBTRACT)

with BuildPart() as screw:
    with BuildSketch() as screw_sk:
        Circle(1.2)
    extrude(amount=8)
    LinearJoint("head", linear_range=(-4, 8))

screws = []
for idx, screw_hole_joint in enumerate(plate.part.joints["screw_hole"]):
    screw_copy = copy.copy(screw.part)
    screw_copy.label = f"Screw {idx}"
    screw_hole_joint.connect_to(screw_copy.joints["head"][0], position=0)
    screws.append(screw_copy)

plate.part.label = "Plate"

assembly = Compound(
    label="Assembly",
    children=[plate.part, *screws],
)

This example assumes a solution where each value in the joints dict is a list of Joints, or something like a JointList, not a single Joint. Maybe someone can think of a better way, that's just the idea that I had off the top of my head. I think this could make it easier to read builder code using joints because the joints would be localised to the part of the model code that the joint is actually associated with, and it also would reduce the need to save Locations inside builder/location contexts and then iterate over those locations them outside, as is done in the first example.

gumyr commented 1 year ago

Related to Issue #83

gumyr commented 1 year ago

This turns out to be a surprisingly difficult problem to solve due to the structure of the code. Joints are defined in the low level topology.py module where there is no knowledge of the builders. Changing the code structure isn't easy as import loops get generated; however, somehow this needs to be improved.

gumyr commented 11 months ago

Completed with commit 63340c1b7d77c678f043b9fcaf974d48805c5529 Joints can be added within a BuildPart scope without specifying the to_part parameter and don't need to be at the end of the builder. When the builder exits it copies the joints to the part. The https://build123d.readthedocs.io/en/latest/joints.html section of the docs describes this new behaviour.