faceting splines

84 views
Skip to first unread message

Jonathan Shimwell

unread,
Jun 13, 2020, 9:30:29 AM6/13/20
to CadQuery


Hi all.

I've been making shapes using a combination of edges including splines that reflect the true shape of the object, like the attached image.

I am then trying to uses those shapes in a package that only supports CSG geometry.

The package is able to automatically convert some cad geometry into CSG geometry but naturally it can't do splines.

So I'm wondering if cad query can be used to re-calculate / approximate splines using straight lines. I guess it would be similar to faceting the spline edge.

Is anyone aware of a feature in CADQuery where a spline edge can be converted into a series of straight edges (ideally with a with a tolerance setting) ?

I continue to find this package fantastic and will be sharing a project on the shared work thread soon. Thanks for making the project I think it really has the potential to change the way design is done.

Best

Jon
Screenshot from 2020-06-13 14-23-27.png

Adam Urbanczyk

unread,
Jun 13, 2020, 2:38:45 PM6/13/20
to CadQuery
Hi Jonathan,

glad to hear that you find CQ useful. Please do share your work, it is always nice to see what other people are using CQ for!

Regarding spline approximation, you could use the approach from the SVG exporter: https://github.com/CadQuery/cadquery/blob/f9fe7b1b842bce3a590ec8e6a1becdc2f04bd054/cadquery/occ_impl/exporters.py#L230 . But this means going down all the way to the CAD kernel level. May I ask what format are you using? If it is not STEP, then all the edges are discretized anyway.

HTH,
Adam

Jonathan Shimwell

unread,
Jun 13, 2020, 4:05:20 PM6/13/20
to Adam Urbanczyk, CadQuery
Thanks for the suggestions Adam

I think I am understanding what to do, that makeSVGedge looks similar to what I am after.

The geometry I am trying to make is STEP files but without splines, let's call it SplineLessSTEP and has a niche use in fusion neutronics geometry

There is another stage afterwards that converts that into CSG geometry that can handle non spline surfaces. So when the converter comes across a spline it doesn't know what to do with it and is able to convert that solid.

Using your makeSVGedge I am thinking of something like this, but I'm not quite sure if one can detect the surface types of an edge.

import cadquery as cq
from cadquery import exporters

def facetet_edges_if_spline(edge):
    print("start point " + str(edge.startPoint()) + " end point " + str(edge.endPoint()))   
    # find a way to detect edge type and then make a new edge
    #if edge type is spline (not straights or circle) then
    new_edge = exporters.makeSVGedge(edge) # implement something similar that accepts tolerance as an argument
    return new_edge

solid = (
    cq.Workplane('XY')
    .polyline([(1,1), (1,2)])
    .spline([(1,2),(2,4),(3,2)])
    .polyline([(3,2), (3,1)])
    .threePointArc((2,0),(1,1))
    .close()
    .extrude(1)
)
edges = solid.faces().edges()
edges.each(facetet_edges_if_spline)
solid #render the solid in jupyter notebook



--
cadquery home: https://github.com/dcowden/cadquery
post issues at https://github.com/dcowden/cadquery/issues
run it at home at : https://github.com/jmwright/cadquery-freecad-module
see it in action at http://www.parametricparts.com
---
You received this message because you are subscribed to the Google Groups "CadQuery" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cadquery+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/cadquery/9bb50069-0729-477a-94c7-03fa860e4d2do%40googlegroups.com.

Adam Urbanczyk

unread,
Jun 14, 2020, 11:24:43 AM6/14/20
to CadQuery
I'm afraid that it is more complicated. Here is an example of something you might be after. The final solid s has egdes that are either of type CIRCLE or LINE. That is not the case for the non-planar faces. That could be solved as well, but it will be more complicated.

import cadquery as cq

from OCP.GCPnts import GCPnts_QuasiUniformDeflection
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
from OCP.TopTools import TopTools_HSequenceOfShape
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace

from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeFilling
from OCP.GeomAbs import GeomAbs_C0, GeomAbs_C1

def split_faces(obj):
   
    rv_spline
= []
    rv_regular
= []
   
   
for f in obj.Faces():
       
if any(e.geomType()=='BSPLINE' for e in f.Edges()):
            rv_spline
.append(f)
       
else:
            rv_regular
.append(f)
   
   
return rv_spline,rv_regular

def transform_bspline(e,tol=1e-3):
   
    curve
= e._geomAdaptor()  # adapt the edge into curve
    start
= curve.FirstParameter()
   
end = curve.LastParameter()

    points
= GCPnts_QuasiUniformDeflection(curve, tol, start, end)
   
return cq.Wire.makePolygon((cq.Vector(points.Value(i + 1)) for i in range(points.NbPoints())))

def makeNSidedSurface(
        face
,
        edges
,
        continuity
=GeomAbs_C0,
        degree
=2,
        nbPtsOnCur
=15,
        nbIter
=2,
        anisotropy
=False,
        tol2d
=0.00001,
        tol3d
=0.0001,
        tolAng
=0.01,
        tolCurv
=0.1,
        maxDeg
=8,
        maxSegments
=9,
   
):

        n_sided
= BRepOffsetAPI_MakeFilling(
            degree
,
            nbPtsOnCur
,
            nbIter
,
            anisotropy
,
            tol2d
,
            tol3d
,
            tolAng
,
            tolCurv
,
            maxDeg
,
            maxSegments
,
       
)
       
for edge in edges:
            n_sided
.Add(edge.wrapped, continuity)
       
        n_sided
.LoadInitSurface(face)
        n_sided
.Build()
        face
= n_sided.Shape()
       
return cq.Shape.cast(face).fix()

def transform_spline_face(f):
   
    edges
= []
   
   
for e in f.Edges():
         
if e.geomType() != 'BSPLINE':
            edges
.append(e)
         
else:
            edges
.extend(transform_bspline(e).Edges())
       
   
# edges to wires
    wires_out
= TopTools_HSequenceOfShape()
    edges_in
= TopTools_HSequenceOfShape()
   
for el in edges: edges_in.Append(el.wrapped)
   
ShapeAnalysis_FreeBounds.ConnectEdgesToWires_s(edges_in, 1e-6, False, wires_out)

    wires
= [cq.Shape.cast(el) for el in wires_out]    
   
    bldr
= BRepBuilderAPI_MakeFace(wires[0].wrapped,False)
   
   
for w in wires[1:]:
        bldr
.Add(w.wrapped)
   
    bldr
.Build()
   
if bldr.IsDone():
       
return cq.Shape.cast(bldr.Shape())
   
else:
       
return makeNSidedSurface(f.wrapped,wires[0].Edges(),[],degree=2)

   
   

solid
= (
    cq
.Workplane('XY')
   
.polyline([(1,1), (1,2)])
   
.spline([(1,2),(2,4),(3,2)])
   
.polyline([(3,2), (3,1)])
   
.threePointArc((2,0),(1,1))
   
.close()
   
.extrude(1)
)


spline_faces
,regular_faces = split_faces(solid.val())

transformed_faces
= [transform_spline_face(f) for f in spline_faces]

sh
= cq.Shell.makeShell(transformed_faces+regular_faces)
s
= cq.Solid.makeSolid(sh)

show_object
(s)


Adam Urbanczyk

unread,
Jun 14, 2020, 12:23:54 PM6/14/20
to CadQuery
I made a mistake in the code, here is a working version:

       
return makeNSidedSurface(f.wrapped,wires[0].Edges(),degree=2)

Jonathan Shimwell

unread,
Jun 14, 2020, 1:27:35 PM6/14/20
to Adam Urbanczyk, CadQuery
That works nicely, using your script it was possible to save a spet file with splines and another on with spline replacements. This is a really useful feature for those of us that want to save STEP files in a manner that facilitates conversion to CSG at a later stage. Quite niche but super super helpful.

Thanks very much for your help

Jon

--
cadquery home: https://github.com/dcowden/cadquery
post issues at https://github.com/dcowden/cadquery/issues
run it at home at : https://github.com/jmwright/cadquery-freecad-module
see it in action at http://www.parametricparts.com
---
You received this message because you are subscribed to the Google Groups "CadQuery" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cadquery+u...@googlegroups.com.
Screenshot from 2020-06-14 18-08-43.png
Screenshot from 2020-06-14 18-08-48.png

Jonathan Shimwell

unread,
May 29, 2024, 9:02:47 AMMay 29
to CadQuery
Revisiting the script in this post and I've now noticed that although the edges are straight lines I think the n sided face is not spline free.

I noticed some ```B_SPLINE_SURFACE_WITH_KNOTS``` appear in the step file when I load it up in a text editor (screen shot attached)

When viewing the face the N sided surface quite curved (most noticeable when setting tolerance to 0.05) (screen shot attached)

I'm looking into replacing the N sided surface with a series of smaller planar surfaces. The approach I was thinking of taking is to tessellate the spline curved face and then make planar faces for each triangle. I think this will be similar to the STL mesh of a single surface and be quite a general solution to replacing the spline curved face with a series of smaller planar faces.

Just wondering if anyone has done something like this before or has any alternative suggestions.

All the best

Jon
Screenshot from 2024-05-29 13-49-38.png
Screenshot from 2024-05-29 13-47-19.png

Jeremy Wright

unread,
May 29, 2024, 9:30:01 AMMay 29
to Jonathan Shimwell, CadQuery
I've done some tessellation work before when generating customized meshes. You have to be careful to get the triangle normals correct.


I have worked with a 3D scanned mesh that I then converted to STEP and it got pretty messy. It works however, and after some post-processing the file could be imported in CadQuery without causing a hang (the mesh had millions of triangles). If you want to see the internals of that STEP file, you can download it here: https://codeberg.org/7BIndustries/poc-stereoscope/src/commit/847dd415d1d0aa8804f3ef010bdaec239165ac4b/stereoscope/components/cameras/camera_trap_simplified_10k.step

Can your package that only supports CSG import STL, AMF, 3MF or ThreeJS? CadQuery supports all of those mesh-based formats, and that might be a way to avoid doing the tessellation yourself.


---
You received this message because you are subscribed to the Google Groups "CadQuery" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cadquery+u...@googlegroups.com.
browning_hp5_horizontal_shim.png

Jonathan Shimwell

unread,
May 29, 2024, 12:26:24 PMMay 29
to Jeremy Wright, CadQuery
Thanks Jeremy

I've taken a good look at your code, thanks for sharing that. If I've understood correctly you tessellate the entire solid and then make planes from each triangle and combine them to make a new workplane which is pretty much what I'm trying to achieve.

The one difference is that I'm trying to avoid tesselating faces that don't have splines such as the ones with circles or straight edges (as these can be converted to CSG primitives)

I've had a go at following your method with my simple shape and I end up tesselating each face with spines on it and then the interfaces between tessellated faces and non tessellated faces results in gaps which I had not considered previously.

For example the circle edge which connects a face with splines and a face without splines has gaps in it.

So I might have a go at decomposing the workplane first or try adding tessellation more selectively or combine your method with Adam.

Many thanks for the help. I'm going to keep going with this and see how much closer I can get.


```
import cadquery as cq

from OCP.GCPnts import GCPnts_QuasiUniformDeflection
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
from OCP.TopTools import TopTools_HSequenceOfShape
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace

from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeFilling
from OCP.GeomAbs import GeomAbs_C0, GeomAbs_C1


def split_faces(obj):
   
    rv_spline = []
    rv_regular = []
   
    for f in obj.Faces():
        if any(e.geomType()=='BSPLINE' for e in f.Edges()):
            rv_spline.append(f)
        else:
            rv_regular.append(f)
   
    return rv_spline,rv_regular


solid = (
    cq.Workplane('XY')
    .polyline([(1,1), (1,2)])
    .spline([(1,2),(2,4),(3,2)])
    .polyline([(3,2), (3,1)])
    .threePointArc((2,0),(1,1))
    .close()
    .extrude(1)
)

# looping through the edges on each face and printing their geomType to show there are no splines
for face in solid.val().Faces():
    for e in face.edges():
        if e.geomType() != 'LINE':
            print('edge which is not a line found in original workplane', e.geomType())


spline_faces,regular_faces = split_faces(solid.val())
print(f'{len(spline_faces)} spline containing faces found')
print(f'{len(spline_faces)} non spline containing faces found')

new_faces = []
for spline_face in spline_faces:
    tess = spline_face.tessellate(tolerance=5)
    for triangle in tess[1]:
        # todo check if these should be clockwise or anticlockwise
        edge1 = cq.Edge.makeLine(tess[0][triangle[0]], tess[0][triangle[1]])
        edge2 = cq.Edge.makeLine(tess[0][triangle[1]], tess[0][triangle[2]])
        edge3 = cq.Edge.makeLine(tess[0][triangle[0]], tess[0][triangle[2]])
        wire = cq.Wire.combine([edge1,edge2, edge3])
        new_face = cq.occ_impl.shapes.wiresToFaces([wire[0].close()])
        new_faces.append(new_face[0])


sh = cq.Shell.makeShell(new_faces+regular_faces)
desplined_solid = cq.Solid.makeSolid(sh)
desplined_solid.exportStep('tess.stp')

# looping through the edges on each face and printing their geomType to show there are no splines
for face in desplined_solid.faces():
    for e in face.edges():
        if e.geomType() != 'LINE':
            print('edge which is not a line found in final workplane', e.geomType())

from jupyter_cadquery import show
show(desplined_solid)
```


Screenshot from 2024-05-29 16-58-40.png

Adam Urbanczyk

unread,
May 30, 2024, 3:16:40 PMMay 30
to CadQuery
You might want to check out cq.Shape.toSplines. Setting the degree to 1 will do almost what you want. You'd "only" need to split the resulting piecewise planar surface into strictly planar pieces.

Jonathan Shimwell

unread,
May 30, 2024, 9:32:06 PMMay 30
to Adam Urbanczyk, CadQuery
Nice, I've rewritten the code to make use of Shape.toSpines on the spline and I've got something that visually looks correct.

Opening the stp file and I have some simple splines in there

Is there any way to retrieve the listOfVector that gets used to create the spline Edge. I can't see it in the Edge.vertices attribute?

If I can get that list then I can make straight edges to match and replaced that straight edge spline with the line geomtype

import cadquery as cq

solid = (
    cq.Workplane('XY')
    .polyline([(1,1), (1,2)])
    .spline([(1,2),(2,4),(3,2)])
    .polyline([(3,2), (3,1)])
    .threePointArc((2,0),(1,1))
    .close()
    .extrude(1)
)
new_faces = []
for face in solid.val().Faces():
    spline_found=False
    edges=[]
    for e in face.edges():
        if e.geomType() == 'BSPLINE':
            edges.append(e.toSplines(degree=1, tolerance=0.1))
            spline_found=True
        else:
            edges.append(e)
    if spline_found == True:
        wire = cq.Wire.combine(edges)
        try: # please excuse the nasty try except, I shall find a nicer way of checking if it is planar

            new_face = cq.occ_impl.shapes.wiresToFaces([wire[0].close()])
            new_faces.append(new_face[0])
        except:
            print('failed wires not planar')
            new_face = face.toSplines(degree=1, tolerance=0.1)
            new_faces.append(new_facewires not planar)
    else:
        new_faces.append(face)

sh = cq.Shell.makeShell(new_faces)
desplined_solid = cq.Solid.makeSolid(sh)
desplined_solid





You received this message because you are subscribed to a topic in the Google Groups "CadQuery" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/cadquery/Ia84dtcnCo0/unsubscribe.
To unsubscribe from this group and all its topics, send an email to cadquery+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/cadquery/603e564a-a82a-4660-9240-636158770dabn%40googlegroups.com.
Screenshot from 2024-05-31 02-04-26.png

Jeremy Wright

unread,
May 30, 2024, 9:37:43 PMMay 30
to Jonathan Shimwell, Adam Urbanczyk, CadQuery

Adam Urbanczyk

unread,
May 31, 2024, 1:37:41 AMMay 31
to CadQuery
There seems to be a function in OCCT for this. Not sure how well it works in general so YMMV.

from cadquery.occ_impl.shapes import *
from OCP.ShapeUpgrade import ShapeUpgrade_ShapeDivideContinuity

c = circle(5)
f = fill(c,[(0,0,3)]).toSplines(1,0.2)

su = ShapeUpgrade_ShapeDivideContinuity(f.wrapped)
su.Perform()

res = cq.Shape(su.Result())

show_object(res)
Reply all
Reply to author
Forward
0 new messages