Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

STEP Missing Surfaces? #745

Open
Spectre5 opened this issue Oct 20, 2024 · 17 comments
Open

STEP Missing Surfaces? #745

Spectre5 opened this issue Oct 20, 2024 · 17 comments
Labels
occt A bug with the OpenCascade CAD core wontfix This will not be worked on

Comments

@Spectre5
Copy link
Contributor

I'm not sure where to post this and I'm not sure if this is a bug in build123d, FreeCAD, or something else. So I'm looking for some guidance in debugging I guess.

I have a part that seems to build find and also displays as expected in Codium using ocp_vscode. However, when I export the file to STEP and import it into FreeCAD, a bunch of surfaces appear to be missing.

Has anyone run into this? Does anyone have any recommendations to debug this?

For reference, here is the part in ocp_vscode:

image

And here it is imported into FreeCAD (latest version, 0.21.2):

image

Notice that the top surfaces are missing.

@gumyr gumyr added wontfix This will not be worked on occt A bug with the OpenCascade CAD core labels Oct 20, 2024
@gumyr gumyr added this to the Not Gating Release 1.0.0 milestone Oct 20, 2024
@gumyr
Copy link
Owner

gumyr commented Oct 20, 2024

Nice box - an electronics case I assume.

The problem is almost certainly that although it looks like a valid shape it isn't. It's possible for the OCCT CAD kernel to create invalid shapes like this without warning as described in the docs here: can't get there from here.

To debug the problem I'd suggest combining intermediate steps with a Box and visually checking if all of the surfaces are correct. Alternatively, you could export intermediate versions to STEP and check if the problem occurs to isolate what part of the code is causing the invalid shape.

@Spectre5
Copy link
Contributor Author

Thanks for the comment. I knew about potentially invalid shapes, but I thought this one was fine since ocp_vscode shows the part fine. Also, part.is_valid() from build123d returns True. Lastly, when I export to an STL file, it loads in FreeCAD fine. I understand of course that an STL is very different than STEP since it is meshed, but it still works fine. My friend printed the STL for me as well and it printed fine. Everything except loading the STEP in FreeCAD seems to indicate that the part is fine.

@gumyr
Copy link
Owner

gumyr commented Oct 20, 2024

As you've found is_valid isn't all that trust worthy, one of my top few things I'd like fixed in OpenCascade.

Sometimes the STL exporter will still work with invalid objects as you've found which can be a good way to just get the part out if all you want to do is print it.

Good luck with your project.

@jdegenstein
Copy link
Collaborator

What does is_manifold return? I find that the manifold check is a lot more reliable than is_valid is (although still not perfect).

@Spectre5
Copy link
Contributor Author

is_manifold does return False. However, I've found a simpler geometry where is_manifold and is_valid() both return True but for which the STEP file isn't as expected. I'm going to try to create a minimal example of it to share.

@Spectre5
Copy link
Contributor Author

Ok, I've got a fairly minimal example to demonstrate my issue. In this example, I just create a small shell, and then try to add an extrusion to the inside of the "top" face. I'm running Linux, if it matters.

Python Version and Pip Freeze
$ python --version
Python 3.12.7
$ pip freeze
anytree==2.12.1
asttokens==2.4.1
build123d==0.7.0
cachetools==5.5.0
cadquery-ocp==7.7.2
certifi==2024.8.30
charset-normalizer==3.4.0
comm==0.2.2
debugpy==1.8.7
decorator==5.1.1
executing==2.1.0
ezdxf==1.3.4
fonttools==4.54.1
idna==3.10
imagesize==1.4.1
ipykernel==6.29.5
ipython==8.28.0
jedi==0.19.1
jupyter_client==8.6.3
jupyter_core==5.7.2
matplotlib-inline==0.1.7
nest-asyncio==1.6.0
numpy==1.26.4
ocp-tessellate==3.0.7
ocp_vscode==2.5.0
ocpsvg==0.2.1
orjson==3.10.8
packaging==24.1
parso==0.8.4
pexpect==4.9.0
platformdirs==4.3.6
prompt_toolkit==3.0.48
psutil==6.1.0
ptyprocess==0.7.0
pure_eval==0.2.3
py-lib3mf==2.3.1
Pygments==2.18.0
pyparsing==3.2.0
python-dateutil==2.9.0.post0
pyzmq==26.2.0
requests==2.32.3
scipy==1.14.1
six==1.16.0
stack-data==0.6.3
svgelements==1.9.6
svgpathtools==1.6.1
svgwrite==1.4.3
tornado==6.4.1
traitlets==5.14.3
trianglesolver==1.2
typing_extensions==4.12.2
urllib3==2.2.3
wcwidth==0.2.13
webcolors==24.8.0
websockets==13.1

Contents of issue_745.py:

from build123d import IN
import build123d as cad
from ocp_vscode import show_object

# base part shell
shell_thickness = 1 / 16 * IN
sketch = cad.Sketch() + [
    cad.Pos(Z=0.00 * IN) * cad.Rectangle(6.0 * IN, 4.0 * IN),
    cad.Pos(Z=1.00 * IN) * cad.Rectangle(5.0 * IN, 3.0 * IN),
]
solid = cad.loft(sketch, ruled=True)
bottom = solid.faces().sort_by()[0]
shell = cad.offset(solid, amount=-shell_thickness, openings=bottom)

# create a a boss on the inside surface
face = shell.faces().sort_by()[-2]
outer = cad.offset(face, amount=-0.25 * IN)
shell += cad.extrude(outer, 0.50 * IN)

show_object(shell)
# shell = shell.clean()  # does not help
print(shell.is_valid())
print(shell.is_manifold)
show_object(shell)
cad.export_step(shell, 'issue_745.step')
cad.export_stl(shell, 'issue_745.stl')

When viewed in ocv_vscode, I get the expected shell with an extrusion on the inside:
image

But opening the result STEP in FreeCAD 0.21.2 only shows two planes instead of the inside boss/extrusion:
image

@Spectre5
Copy link
Contributor Author

Spectre5 commented Oct 22, 2024

I did find out 1 more detail that may be part of the problem. If I remove * IN from all of the dimensions, then it seems to display correctly in FreeCAD and ocp_vscode (but of course the dimensions are off by a factor of 25.4). So most likely there is some issue with the floating point math due to that conversion.

@gumyr gumyr added wontfix This will not be worked on and removed wontfix This will not be worked on labels Oct 22, 2024
@gumyr
Copy link
Owner

gumyr commented Oct 22, 2024

The problem is within OCCT - the boss is not being fused into the shell successfully probably due to some small floating point issue. I was able to get the fuse to work by extruding the offset version of very top surface face = shell.faces().sort_by()[-1] down which makes the two solids overlap.

Although not specific to this problem, you might find it convenient to design in "units" and scale the entire model at the end shell = scale(shell, IN) to get to inches.

Although I'd like these problems to be fixed, OCCT issues are outside the scope of build123d unless a work-around can be found. In this case all the work arounds I've tried failed.

@Spectre5
Copy link
Contributor Author

Spectre5 commented Oct 22, 2024

Interestingly, just using regular dimensions and then scaling with shell = cad.scale(shell, IN) did work here as well, even though it would theoretically give the same thing. This is definitely a floating point issue. I also found a solution with ignoring the IN units altogether and then setting the step to export the model as inches using the lines below. Although the downside here is that it only affects the STEP and not the STL or other outputs.

from OCP.Interface import Interface_Static
Interface_Static.SetCVal_s("write.step.unit", 'INCH')

Your idea of starting on the top surface and extruding "inward" (plus the extra thickness of the top) makes sense to ensure overlapping geometry, though in some cases this may not be possible.

I wonder if there could be some solution in extending the extrusion in the opposite direction a little bit. However, doing this fully automatically is probably very difficult and riddled with corner cases. Maybe a small improvement would be to allow extruding a different amount in each direction. extrude already has the both keyword, so maybe it could be edited to allow am amount in each direction. Then my example could potentially just be changed in the extrusions to be something like

shell += cad.extrude(outer, 0.50 * IN, dir2=-1e-8 * IN)

where that dir2 parameter could be just half the thickness of the top in my case, or else something suitably small in more complicated cases. Or maybe a "backwards target" to be extruded to? Then I could select that face as the backwards target.

@Spectre5
Copy link
Contributor Author

I also still have a hard time understanding why ocp_vscode displays it "correctly" even when FreeCAD does not. My understanding is that both of them using Open CASCADE?

@Spectre5
Copy link
Contributor Author

Looking at the STEP file that has an issue and the alternative (where I just used shell = cad.scale(shell, IN) at the end and did not multiple anything else by IN), I see that the outputs are fairly different.

In the problematic one, it interestingly has some B_SPLINEs, which shouldn't be needed. Note that some of the points end with -49.02512104286 and others with -49.02512104285 (last digit difference). The correctly rendering one does not have any splines with more than 2 points used in the definition. So, again, this points to a floating point issue.

#244 = SURFACE_CURVE('',#245,(#278,#314),.PCURVE_S1.);
#245 = B_SPLINE_CURVE_WITH_KNOTS('',6,(#246,#247,#248,#249,#250,#251,
    #252,#253,#254,#255,#256,#257,#258,#259,#260,#261,#262,#263,#264,
    #265,#266,#267,#268,#269,#270,#271,#272,#273,#274,#275,#276,#277),
  .UNSPECIFIED.,.F.,.F.,(7,5,5,5,5,5,7),(0.,0.165254218546,
    0.175246461201,0.499996321046,0.831740793256,0.841734145636,1.),
  .UNSPECIFIED.);
#246 = CARTESIAN_POINT('',(113.94502420857,-49.02512104286,0.));
#247 = CARTESIAN_POINT('',(107.64378014471,-49.02512104286,0.));
#248 = CARTESIAN_POINT('',(101.34828111535,-49.02512104286,0.));
#249 = CARTESIAN_POINT('',(95.05855355311,-49.02512104286,0.));
#250 = CARTESIAN_POINT('',(88.77462409614,-49.02512104286,0.));
#251 = CARTESIAN_POINT('',(82.496519588153,-49.02512104286,0.));
#252 = CARTESIAN_POINT('',(75.845009776396,-49.02512104286,0.));
#253 = CARTESIAN_POINT('',(75.465799479008,-49.02512104286,0.));
#254 = CARTESIAN_POINT('',(75.086579880094,-49.02512104286,0.));
#255 = CARTESIAN_POINT('',(74.707348460808,-49.02512104285,0.));
#256 = CARTESIAN_POINT('',(74.328121419791,-49.02512104286,0.));
#257 = CARTESIAN_POINT('',(61.624082169465,-49.02512104286,0.));
#258 = CARTESIAN_POINT('',(49.299265735572,-49.02512104285,0.));
#259 = CARTESIAN_POINT('',(36.974449301679,-49.02512104285,0.));
#260 = CARTESIAN_POINT('',(24.649632867786,-49.02512104285,0.));
#261 = CARTESIAN_POINT('',(12.324816433893,-49.02512104286,0.));
#262 = CARTESIAN_POINT('',(-12.59027401862,-49.02512104286,0.));
#263 = CARTESIAN_POINT('',(-25.18054803724,-49.02512104286,0.));
#264 = CARTESIAN_POINT('',(-37.77082205586,-49.02512104286,0.));
#265 = CARTESIAN_POINT('',(-50.36109607449,-49.02512104286,0.));
#266 = CARTESIAN_POINT('',(-62.95137009311,-49.02512104286,0.));
#267 = CARTESIAN_POINT('',(-75.92090904412,-49.02512104286,0.));
#268 = CARTESIAN_POINT('',(-76.30016437005,-49.02512104285,0.));
#269 = CARTESIAN_POINT('',(-76.67947064724,-49.02512104286,0.));
#270 = CARTESIAN_POINT('',(-77.05877964725,-49.02512104285,0.));
#271 = CARTESIAN_POINT('',(-77.43811713417,-49.02512104286,0.));
#272 = CARTESIAN_POINT('',(-83.82540456841,-49.02512104286,0.));
#273 = CARTESIAN_POINT('',(-89.83869721972,-49.02512104286,0.));
#274 = CARTESIAN_POINT('',(-95.85732898468,-49.02512104286,0.));
#275 = CARTESIAN_POINT('',(-101.8812763016,-49.02512104286,0.));
#276 = CARTESIAN_POINT('',(-107.9105157815,-49.02512104286,0.));
#277 = CARTESIAN_POINT('',(-113.9450242085,-49.02512104286,0.));

Would it be possible to have some global flag to round all values to within some user defined tolerance? If the values were rounded to to 6 (or even 10 digits here) then this would likely go away?

@Spectre5
Copy link
Contributor Author

Spectre5 commented Oct 23, 2024

Sorry for the barrage of comments. But was is interesting is that even when I "force" there to be good clearance, it still doesn't work here.

shell += cad.extrude(outer, 0.50 * IN)
# change the line above to one of these:
shell += cad.extrude(outer.located(cad.Pos(Z=0.25 * IN)), 0.50 * IN)
shell += cad.extrude(cad.Pos(Z=0.25 * IN) * outer, 0.50 * IN)
# try using glue and/or tol in the fuse method
shell = shell.fuse(cad.extrude(outer, 0.50 * IN), glue=True, tol=0.01 * IN)

In this case, outer is moved up higher and the extrusion should fully intersect the top surface and displays correctly in ocd_vscode, but still has the issues in FreeCAD. I also tried with a smaller offset so that it wouldn't penetrate above the part, but the issue remained. So it doesn't then seem to be only a floating point issue. Something else is (also) going on I think.

@gumyr
Copy link
Owner

gumyr commented Oct 23, 2024

Although I can't say for sure what the problem is as it's within the OCCT code base, my guess is that a floating point difference is causing OCCT to interpret a planar surface as a non-planar surface which breaks the logic of fusing the extrusion to the shell. As you mention, it isn't feasible to start extruding in both directions generally as build123d must handle arbitrary shapes.

The viewer uses a different system to display object than the OCCT STEP file generator so one shouldn't assume that the results are going to be exactly the same.

Unfortunately, this S/W (all S/W?) has bugs and sometimes the only thing to do is avoid them.

@Spectre5
Copy link
Contributor Author

Spectre5 commented Oct 23, 2024

Ya, the issue is even more fundamental than I realized. I spent a bit of time going down the path of investigating the fusing operation, but actually even just the "boss" extruded box itself does not work.

from build123d import IN
import build123d as cad
from ocp_vscode import show_object

# base part shell
shell_thickness = 1 / 16 * IN
sketch = cad.Sketch() + [
    cad.Pos(Z=0.00 * IN) * cad.Rectangle(6.0 * IN, 4.0 * IN),
    cad.Pos(Z=1.00 * IN) * cad.Rectangle(5.0 * IN, 3.0 * IN),
]
solid = cad.loft(sketch, ruled=True)
bottom = solid.faces().sort_by()[0]
shell = cad.offset(solid, amount=-shell_thickness, openings=bottom)

# create a a boss on the inside surface
face = shell.faces().sort_by()[-2]
outer = cad.offset(-face, amount=-0.25 * IN)
# tried this too, since we know there is only 1 face in the sketch, but also fails:
# outer = cad.offset(-face, amount=-0.25 * IN).faces()[0]
box = cad.extrude(outer, -0.50 * IN)

# this works though, so the issue gets introduced in the offset operation
# box = cad.extrude(face, -0.50 * IN)

# save the box extrusion only
# box = box.clean()  # does not help
print(box.is_valid())
print(box.is_manifold)
show_object(box)
cad.export_step(box, 'issue_745_2.step')
cad.export_stl(box, 'issue_745_2.stl')

Saving just this boss extrusion, which should just be a simple shape, fails to open correctly in FreeCAD. It only shows two of the planes. Using the face within the sketch still fails, but it does work if we use the inside face directly (see code comments). So it appear that the issue is introduced in the offset operation.

@gumyr
Copy link
Owner

gumyr commented Oct 23, 2024

Yes, the offset face is causing the problem but when I looked at the vertices of the offset they all look good. For a while I thought I could potentially "fix" the offset but after more investigation I failed to find a way to do so.

@Spectre5
Copy link
Contributor Author

Spectre5 commented Oct 24, 2024

I've found that all of my examples above (the box, the simplified model, and the original complex model) all display correctly in FreeCAD if I un-comment the line below to enable SetApprox within the offset_2d method in topology.py:

offset_builder.SetApprox(True)

So perhaps we can add a boolean to the main offset method to be passed to offset_2d and enable this?

@gumyr
Copy link
Owner

gumyr commented Oct 24, 2024

There already is an issue on this: #547 I'll comment further about this feature there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
occt A bug with the OpenCascade CAD core wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

3 participants