CadQuery / cadquery

A python parametric CAD scripting framework based on OCCT
https://cadquery.readthedocs.io
Other
3.2k stars 289 forks source link

Cutting a cylinder with helix sweep result in "Compound is empty" #1437

Open realfun opened 11 months ago

realfun commented 11 months ago

Hi I am trying to create an auger-shaped marble run lift with the following code, but when running it with latest cadquery it always yield an error: ValueError: Compound is empty

The code:

import cadquery as cq
from ocp_vscode import *

r = 18 # Radius of the helix
p = 18 # Pitch of the helix
h = 30 # Height of the helix

wire = cq.Wire.makeHelix(pitch=p, height=h, radius=r)
helix = cq.Workplane(obj=wire)
marble_track = (
    cq.Workplane('XZ')
    .center(r, 0)
    .circle(8)
    .sweep(helix)
)

post = cq.Workplane().cylinder(h, 22)

result = post.cut(marble_track)

show(result)

I visually inspected the marble_track & the post individually, they seem to be correct, but don't know why cutting the marble_track from the post would return empty compound.

I tried to cut the other way around to figure out what's going on, I.E. result = marble_track.cut(post), this time it yields something really weird.

image

After getting some help from anderson.pa in discord #general channel, change the post to be z-flush would make it render: post = cq.Workplane().cylinder(h, 22, centered=(True, True, False)), but change wire to a bigger height or change the post to smaller radius(18) would cause it to fail.

So it seems to be a bug particular to the helix sweep.

lorenzncode commented 11 months ago

I find the cut is successful for a range of rotation angles of the post cylinder around its center.

fail: 0 to ~79 success: ~80 to 360

import cadquery as cq
from cadquery.vis import show

r = 18  # Radius of the helix
p = 18  # Pitch of the helix
h = 30  # Height of the helix

wire = cq.Wire.makeHelix(pitch=p, height=h, radius=r)
helix = cq.Workplane(obj=wire)
marble_track = (
    cq.Workplane('XZ')
    .center(r, 0)
    .circle(8)
    .sweep(helix)
)

post = cq.Workplane().cylinder(h, 22).rotateAboutCenter((0, 0, 1), 80)

result = post.cut(marble_track)

show(result)

Of course I'd rather not consider the rotation of the cylinder in order for a successful cut. It appears to be an OCCT CAD kernel issue.

realfun commented 11 months ago

Thanks for looking into this! Curious how do you ever think of trying rotateAboutCenter? This never crossed my mind.

Indeed it works after rotateAboutCenter 80, though it breaks again if h is 31, giving no error and just a cylinder without the coil cut. But if the cylinder is created Z-flush with centered=(1, 1, 0), then it starts working again! Weird, and cumbersome, but it works now. Really appreciated the help. Here is the updated code as a successful work around.

import cadquery as cq
from ocp_vscode import *

r = 18  # Radius of the helix
p = 18  # Pitch of the helix
h = 200  # Height of the helix

wire = cq.Wire.makeHelix(pitch=p, height=h, radius=r)
helix = cq.Workplane(obj=wire)
marble_track = (
    cq.Workplane('XZ')
    .center(r, 0)
    .circle(8)
    .sweep(helix)
)

post = cq.Workplane().cylinder(h, 22, centered=(1, 1, 0)).rotateAboutCenter((0, 0, 1), 80)
result = post.cut(marble_track)

show(result)

I noticed there are some weird line every several rounds: image

lorenzncode commented 11 months ago

Curious how do you ever think of trying rotateAboutCenter?

The boundary representation (BREP) of the cylinder includes a seam. The cylinder is not only defined by its origin, radius, height at the kernel level so I experimented with rotation to see if it affected the result.

Take a circle for example. https://dev.opencascade.org/doc/refman/html/classgp___circ.html "Describes a circle in 3D space. A circle is defined by its radius and positioned in space with a coordinate system"

The resulting edge or wire contains a vertex as shown here depending on the defined coordinate system.

import cadquery as cq
from cadquery.vis import show

circle = cq.Workplane().circle(10)
# mark the vertex
mark = cq.Workplane(origin=circle.vertices().val().Center()).sphere(1.0)

show(circle, mark)

Screenshot from 2023-11-11 20-12-27

though it breaks again if h is 31

Yes, modifying h or centered changes the problem and I find I had to play with the angle again (89 to 360 in case of h=31).

there are some weird line every several rounds:

That is from the intersection of the seam of the marble_track. See here:

Screenshot from 2023-11-11 21-24-20

realfun commented 11 months ago

Thanks for the detailed explaination, today I learnt :)

For anyone who searched to here, notice that in the original code a vertical circle is used to create the coil sweep, which is slightly off. To be truly perpendicular, it needs to be in a special angle that has to do with pitch and radius, updated code below:

import cadquery as cq
from ocp_vscode import *
from math import pi, atan, degrees

r = 18  # Radius of the helix
p = 18  # Pitch of the helix
h = 200  # Height of the helix

wire = cq.Wire.makeHelix(pitch=p, height=h, radius=r)
helix = cq.Workplane(obj=wire)
c = p / (pi * 2)
angle = -degrees(atan(r/c))  # Helix starting angle, with help of Wolfram Alpha
# print(angle)
marble_track = (
    cq.Workplane()
    .center(r, 0)
    .transformed(rotate=(angle, 0, 0))
    .circle(8)
    .sweep(helix)
)

post = cq.Workplane().cylinder(h, 22, centered=(1, 1, 0)).rotateAboutCenter((0, 0, 1), 80)
result = post.cut(marble_track)

show(result)