Open Spectre5 opened 1 month ago
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.
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.
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.
What does is_manifold
return? I find that the manifold check is a lot more reliable than is_valid
is (although still not perfect).
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.
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.
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:
But opening the result STEP in FreeCAD 0.21.2 only shows two planes instead of the inside boss/extrusion:
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.
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.
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.
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?
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?
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.
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.
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.
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.
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?
There already is an issue on this: https://github.com/gumyr/build123d/issues/547 I'll comment further about this feature there.
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:
And here it is imported into FreeCAD (latest version, 0.21.2):
Notice that the top surfaces are missing.