gumyr / build123d

A python CAD programming library
Apache License 2.0
550 stars 93 forks source link

Infinite loop for a sweep along a wire that joins with the existing part. #687

Closed PaulBone closed 2 days ago

PaulBone commented 2 months ago

The following program gets stuck forever. I'm using build123d 0.6.1, Python 3.10 on Linux x86_64.

from build123d import *
from ocp_vscode import show, show_object, reset_show, set_port, set_defaults, get_defaults
set_port(3939)

with BuildPart() as p:
    Box(40, 40, 28)
    fillet(edges().filter_by(Axis.Z),
           radius=4)

    # Create a rim by sweeping an object around the top of the box.
    topw = faces().sort_by(Axis.Z)[-1].outer_wire()
    with BuildSketch(Plane(topw@0, z_dir=topw%0, x_dir=(1, 0, 0))):
        Rectangle(3, 3,
                  align=(Align.MIN, Align.MIN))
    sweep(path=topw)

show_object(p.part)

If I remove either the fillet, change the sweep to an extrude, or move the "rim" outwards so that its face doesn't touch the box (but its edge does) then the program works.

The more complete code for the shape I'm trying to create is below (but it's still simplified). I'm providing this so you can see why I'm creating a rim using a sweep and the top face's wire. I'm making a stacking lip on a bin whose shape is not a basic rounded rectangle.

from build123d import *
from ocp_vscode import show, show_object, reset_show, set_port, set_defaults, get_defaults
set_port(3939)

with BuildPart() as p:
    with Locations((0, 0, 0),
                   (40, 0, 0),
                   (0, 40, 0)):
       Box(40, 40, 28)

    fillet(edges().filter_by(Axis.Z),
           radius=4)
    offset(amount=-1.2,
           openings=faces().sort_by(Axis.Z)[-1])
    topw = faces().sort_by(Axis.Z)[-1].outer_wire()
    with BuildSketch(Plane(topw@0, z_dir=topw%0, x_dir=(1, 0, 0))):
        Rectangle(3, 3,
                  align=(Align.MIN, Align.MIN))
    sweep(path=topw)

show_object(p.part)

Thank you.

gumyr commented 2 months ago

The problem is with fusing the swept part with the rest. If this is suppressed with:

with BuildPart() as p:
    with Locations((0, 0, 0), (40, 0, 0), (0, 40, 0)):
        Box(40, 40, 28)

    fillet(edges().filter_by(Axis.Z), radius=4)
    offset(amount=-1.2, openings=faces().sort_by(Axis.Z)[-1])
    topw = faces().sort_by(Axis.Z)[-1].outer_wire()
    with BuildSketch(Plane(topw @ 0, z_dir=topw % 0, x_dir=(1, 0, 0))):
        Rectangle(3, 3, align=(Align.MIN, Align.MIN))
    s = sweep(path=topw, mode=Mode.PRIVATE)

image the part p and the sweep s both look independently correct. The problem is in the OCCT CAD kernel and has to do with parts that have coincident sides which should be okay but occasionally isn't. With some minor changes one can see this:

with BuildPart() as p:
    with Locations((0, 0, 0), (40, 0, 0), (0, 40, 0)):
        Box(40, 40, 28)

    fillet(edges().filter_by(Axis.Z), radius=4)
    offset(amount=-1.2, openings=faces().sort_by(Axis.Z)[-1])
    topw = faces().sort_by(Axis.Z)[-1].outer_wire()
    with BuildSketch(Plane(topw @ 0, z_dir=topw % 0, x_dir=(1, 0, 0))):
        with Locations((0.00001, 0)):
            Rectangle(3, 3, align=(Align.MIN, Align.MIN))
    s = sweep(path=topw)

image which moves the sweeping Rectangle slightly off the base part and the fuse works fine. Unfortunately, the best I can offer is to avoid the problem - possibly like this:

with BuildPart() as p:
    with Locations((0, 0, 0), (40, 0, 0), (0, 40, 0)):
        Box(40, 40, 31)

    fillet(edges().filter_by(Axis.Z), radius=4)
    inside = offset(amount=-1.2, mode=Mode.PRIVATE)
    inside = split(inside, Plane.XY.offset(12.5), keep=Keep.BOTTOM, mode=Mode.PRIVATE)
    add(inside, mode=Mode.SUBTRACT)
    top = p.faces().sort_by(Axis.Z)[-1]
    with BuildSketch(Plane.XY.offset(top.bounding_box().max.Z)) as cutout:
        add(top)
        offset(amount=-3)
    extrude(amount=-3, mode=Mode.SUBTRACT)

image where the lip is created by creating a custom opening and extruding it into the hollow part.

jdegenstein commented 2 months ago

I have an alternative fix from @gumyr, although I agree that the problem seems to be coincident cylindrical faces on the sweep and the base part.

image

with BuildPart() as p:
    with Locations((0, 0, 0),
                   (40, 0, 0),
                   (0, 40, 0)):
       Box(40, 40, 28)

    fillet(edges().filter_by(Axis.Z),
           radius=4)
    offset(amount=-1.2,
           openings=faces().sort_by(Axis.Z)[-1])
    topw = faces().sort_by(Axis.Z)[-1].outer_wire()
    with BuildSketch(topw^0) as s:     
        Rectangle(3, 3,
                align=(Align.MAX, Align.MIN))
    zz=sweep(path=topw, clean=False)

In this case, the default clean=True option appears to be preventing the fuse somehow. Ultimately this seems to be mostly a kernel issue and not sure how much can be done at the build123d-level.

PaulBone commented 2 months ago

Thanks for the quick responses and suggestions. I tried some of these solutions on my more complex shape and it still failed. The full program is here https://github.com/PaulBone/gfthings/tree/debug_sweep (the debug_sweep branch). Run the Bin.py module as a stand-alone program (I press F5 in visual studio), there are comments with "XXX" that direct you to the problematic part.

It's probably still the same bug but I guess for some reason the work-arounds don't avoid it in this case.

gumyr commented 2 months ago

Cool Gridfinity project. I was able to avoid the fusion of coincident faces from sweeping by changing strategies and creating the walls in a single BuildLine/BuildSketch as follows:

bin_size = 42
plate_height_a = 0.8
plate_height_b = 1.8
plate_height_c = 2.15
plate_base_height = 2.9
plate_height = plate_height_a + plate_height_b + plate_height_c
bin_clearance = 0.5 / 2
base_outer_dia = 8

height_units = 4
height = 7 * height_units + 0
depth = 2
width = 2
wall_thickness = 1.2
wall_height = height - plate_height
box_width = width * bin_size - bin_clearance * 2
box_depth = depth * bin_size - bin_clearance * 2

with BuildPart() as funky_bin:
    with BuildSketch() as plan:
        Rectangle(2 * bin_size, bin_size, align=Align.MIN)
        Rectangle(bin_size, 2 * bin_size, align=Align.MIN)
        fillet(plan.vertices(), base_outer_dia / 2)
    sweep_path = plan.face().outer_wire()

    with BuildSketch(sweep_path ^ 0) as profile:
        with BuildLine() as l:
            a, b, c = 1.9, 1.8, 0.7
            Polyline(
                (0, 0),
                (wall_height, 0),
                (wall_height - a, -a),
                (wall_height - (a + b), -a),
                (wall_height - (a + b + c), -a - c),
                (wall_height - (2 * a + b + c) - c, -wall_thickness),
                (0, -wall_thickness),
                close=True,
            )

        make_face()
    sweep(profile.sketch, sweep_path)
    extrude(plan.sketch, amount=wall_thickness)

image