Open Windfisch opened 8 months ago
I've got another similar bug:
from build123d import *
from ocp_vscode import *
ZPOS=5
sphere = Location((0,0,ZPOS)) * Sphere(10)
sphere.color = "#88ff8888"
foo = project(sphere.faces(), workplane = Plane.XY)
show(foo, sphere)
yields
Changing ZPOS = 11
yields
Traceback (most recent call last):
File "/home/flo/kruschkram/b3d-workspace/bugs/project.py", line 8, in <module>
foo = project(sphere.faces(), workplane = Plane.XY)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/flo/bin/cadquery/lib/python3.11/site-packages/build123d/operations_generic.py", line 777, in project
projection = obj.project_to_shape(target, projection_direction)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/flo/bin/cadquery/lib/python3.11/site-packages/build123d/topology.py", line 5982, in project_to_shape
sewed_face_list = Face.sew_faces(intersected_faces)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/flo/bin/cadquery/lib/python3.11/site-packages/build123d/topology.py", line 5578, in sew_faces
sewed_shape = downcast(shell_builder.SewedShape())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/flo/bin/cadquery/lib/python3.11/site-packages/build123d/topology.py", line 8054, in downcast
f_downcast: Any = downcast_LUT[shapetype(obj)]
^^^^^^^^^^^^^^
File "/home/flo/bin/cadquery/lib/python3.11/site-packages/build123d/topology.py", line 8104, in shapetype
raise ValueError("Null TopoDS_Shape object")
ValueError: Null TopoDS_Shape object
Expected result:
The result of show(sphere, Circle(10))
:
I'm quite confident that there is a bug in project
lines 774 & 792 where the to_local_coords
should be from_local_coords
but haven't fully confirmed the change yet. The direction of projection needs to be calculated correctly which is the problem.
Getting perpendicular faces to return an Edge is going to be difficult to implement I expect - at least until the work on intersections is complete (OCCT doesn't handle intersections consistently - there are many difficult procedures that are need to cover all the possibilities).
@Windfisch You're welcome to look into the problem(s) further - any help would be appreciated.
Just looking into this.
It appears to me that project_to_shape
works by extruding the face-to-be-projected, and intersecting this extrusion with the target face.
This has two issues:
-1
.zerovolume.intersect(target)
returns just target
, instead of an empty solid)As for 1., I would like to fix this by always extruding bidirectionally. What do you think about this?
As for 2, i'm investigating what's going on there...
I don't agree with 1) but there is no "correct" answer here. There could easily be part geometries where projecting in both directions would result in Faces on multiple sections of a part where only one is desired. This may require a potentially difficult selection process for the user as well as potentially exposing more OCCT problems. In addition, project with a center (which can only be in one direction) would be different than project with a direction which would be in two directions.
2) is a real problem. Unfortunately, intersect
is quite complex as there are many different types of OCCT intersect classes/methods and the correct one needs to be used depending on the types of objects involved. I've started the Intersect Everything issue (https://github.com/gumyr/build123d/issues/328) but haven't completed the work. I don't doubt there are build123d issues here too, it's difficult to design and test for all circumstances. Any help is appreciated.
for 1), I agree that there is no "correct" answer; however, what build123d currently does is hard to predict and highly unintuitive: It's not like we're projecting to the first hit of the target face. Since we're extruding the face-to-be-projected by the diagonal of the bounding box that contains the face and the target, this could or could not hit the target face once, twice or more (with a wide but curved target face, for example).
If we had a clean "extrudes up to the first hit of the target face, but not more", I would agree, but IIUC we don't. Or am I wrong here?
for 2): it seems that the intersection operation (BRepAlgoApi_Common
) is an operation that modifies its first argument. Or does not, in case an error occurred, which is likely the case here. And in that case, nothing is changed, leaving the old face in place.
The most obvious solution to me appears to check the HasErrors
method... But it does not exist in the python bindings. @gumyr can you help me here?
Also, is there a way to return a valid but empty object? Or would that mess up subsequent operations too much?
a-ha! the missing HasError
method is pulled in via using SomeProtectedBase::method
from a protected base class, and is not defined as a bool normalMethod() { ... }
. Maybe this confuses pybind?
But uh, that's definitely outside of my python-fu.
Edit: oh maybe this could help: https://github.com/CadQuery/OCP/blob/4b98a5dc79fa900f7429975708f6a8c2e41cecd1/ocp.toml#L596-L606
@gumyr at this point, i'd like to have a quick sanity check from your side :D
If we had a clean "extrudes up to the first hit of the target face, but not more", I would agree, but IIUC we don't. Or am I wrong here? build123d does have an
extrude_until
but it is complex and slow and has similar limitations to the above.
Although using HasErrors would be convenient getting Adam to add it to the OCP API is likely to be difficult based on past experience - you're welcome to try though. Checking for empty objects is possible which would avoid this.
Which respect to the direction of projection, when using the project
operation this is determined for the user. Consider the following example:
with BuildPart() as test1:
Box(10, 10, 1)
with BuildSketch(Plane.XY.offset(2)) as front:
Text("F", font_size=8)
p_f = project()
thicken(amount=-0.1, mode=Mode.SUBTRACT)
with BuildSketch(Plane.XY.offset(-2)) as back:
Text("B", font_size=8)
p_b = project()
thicken(amount=-0.1, mode=Mode.SUBTRACT)
However, as previously mentioned, there is a bug to be fixed.
There are several issues here. The above commit fixes the problem when the source and target object were within 1 of each other.
The projection of sphere problem is a different problem and related to how OCCT's BRepPrimAPI_MakePrism works (or doesn't) for closed faces. When the sphere's face is extruded, a cylinder with domed ends is expected, but that is not what is generated - a shifted sphere + the original is the result. Thus the intersection fails in the gap. A new strategy is required for closed faces.
face_extruded = Solid.extrude((Pos(0, 0, 11) * Sphere(10)).face(), Vector(0, 0, -25))
Here is a proposal for how to do projection of non-planar Faces or Solids better - the complexity is likely why OCCT has limited related functionality:
visible, hidden = shape.project_to_viewport(viewport_origin=s.center() + projection_direction)
to create a list of visible Edges on Plane.XY. for e1, e2 in itertools.combinations(visible, 2):
print(topo_explore_common_vertex(e1, e2))
print(e1.intersect(e2).vertices())
edges_to_wires
doesn't do this well enough). Edges that are not part of a closed Wire are discarded.These new Faces are then extruded and intersected with the target Shape as done currently. Extrude Until.NEXT could be used to avoid internal Faces however one has to account for the extrusion protruding past the boundary of the target object.
A potential alternative solution:
This would be an approximation of the target surface and could cause issues when combined with the target (close but not close enough?)
I've made progress on a re-implementation of project_to_shape
(see https://github.com/gumyr/build123d/blob/dev/src/build123d/topology.py#L6057) that will hopefully avoid the issues with the current algorithm but I've found there are a whole set of new problems. Happily the OCCT BRepFeat_SplitShape
implements the functionality of steps 4 & 5 from the second list above which is great - which enables the isolation of projected faces as shown here:
However, handling of self projection is more problematic. Although I've been able to project the visible edges of the target object onto the plane of the object to project, when these edges are projected back onto the target the result is a segmentation fault. This is required to handle the hole in the above part which is not appropriately clipped.
I'm now experimenting with dividing a face into "front" and "back" depending on the direction of the normal to aid in this self clipping problem.
Discord's Alexer suggests that HLRTopoBRep_OutLiner
might provide the edges delineating "front" and "back".
Hi,
Calling
project
on the individual faces of aBox
creates wrong results or crashes.Reproducer
Actual results
for
NUM=0
:for
NUM=1
:for
NUM=2
:for
NUM = 3
,4
or5
, or forfoo = project(box.faces(), workplane=Plane.XY)
:Expected results
for
NUM == 0, 1, 2
, i.e. for all "sides" of the box that are parallel to the Z axis, I would expect a Line to be projected, but not a Quad three times the cube's size.(Note that this is how
project()
works internally: It creates a quad three times the input's bounding box size, and projects everything onto that quad. Apparently it fails to do so, though)for those sides that are the top or bottom of the cube, i would expect something similar to
show(Rect(10,10))
.For the whole
box.faces()
, I would expect the same.