[SOLVED] How do I make a solid from a B-Spline surface?

Need help, or want to share a macro? Post here!
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
User avatar
onekk
Veteran
Posts: 6222
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: How do I make a solid from a B-Spline surface?

Post by onekk »

sph wrote: Mon Aug 02, 2021 8:21 pm The problem with this approach is, that my real life case is a bit more complicated and I cannot use a loft.
...
But it strikes me a bit complicated and I wonder, if there isn't a way, to fill a 3d curve with a surface (other CAD packages have something called ‘Boundary Surface’ or ‘Filled Surface’ which does exactly that).
Ok also FreeCAD has other methods, the "problem" is to obtain the exact curves from the "generating pointlist".

The makeLoft approach is using as you can see directly approximated bspline curves

Code: Select all

        sec = Part.BSplineCurve()
        sec.approximate(line)

        sections.append(sec)
and for these you have no problem, it is easy to obtain the faces:

if you use the makeLoft code, you could obtain the face as simply as doing:

Code: Select all

top_face = Part.Face(sections[0])
show_curve(top_face,"top")

bot_face = Part.Face(sections[-1])
show_curve(bot_face,"bottom")

apart from the difficulties to reverse the proper face (I have encountered some problems using this approach, and I don't know why.

The other step is to close the "vertical face" and for that you have to obtain from the BSplineSurface obtained from the pointlist the two vertical segments, the top and bottom lines (boundaries) are simply the lines connecting the start and end point of the top and bottom bsplinecurves.
once obtained these edges you could simply use:

Code: Select all

>>> help(Part.makeFilledFace)
Help on built-in function makeFilledFace:

makeFilledFace(...) method of builtins.tuple instance
    makeFilledFace(list) -- Create a face out of a list of edges.

Regards

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
User avatar
onekk
Veteran
Posts: 6222
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: How do I make a solid from a B-Spline surface?

Post by onekk »

Thos code will obtain the three surfaces, the top has to be reversed to be correct as the normal has to point downward.

Code: Select all

sections = []


def close_spline(sec):
    sec_sp = sec.StartPoint
    sec_ep = sec.EndPoint
    
    c_ln = Part.LineSegment(sec_ep, sec_sp)

    edges = Part.sortEdges([c_ln.toShape(), sec.toShape()])[0]

    prof = Part.Wire(edges)
    return prof




for line in array:
    sec = Part.BSplineCurve()
    sec.approximate(line)
        
    sections.append(sec)

    
bs1 = Part.BSplineSurface()
bs1.buildFromNSections(sections)
bs1.exchangeUV()

alt_surf = bs1.toShape()
show_curve(alt_surf, "alt_surf")

bot_face = Part.Face(close_spline(sections[0]))
show_curve(bot_face,"bottom")

top_face = Part.Face(close_spline(sections[-1]))
top_face.reverse()
show_curve(top_face,"top")

note also:

Code: Select all

bs1.exchangeUV()
EDIT: code corrected


to correctly orient the bsplinesurface.

Regards

Carlo D.
Last edited by onekk on Tue Aug 03, 2021 11:08 am, edited 3 times in total.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
User avatar
onekk
Veteran
Posts: 6222
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: How do I make a solid from a B-Spline surface?

Post by onekk »

This code added at the bottom of the code in previous post, will create the missing face:

Code: Select all


# sections[0] is the bottom face
sec1_sp = sections[0].StartPoint
sec1_ep = sections[0].EndPoint

# sections[-1] is the top face
sec2_sp = sections[-1].StartPoint
sec2_ep = sections[-1].EndPoint

for edge in alt_surf.Edges:
    pt1 = sec1_sp.distanceToPoint(edge.Curve.StartPoint)
    pt2 = sec2_sp.distanceToPoint(edge.Curve.EndPoint)

    if pt1 == 0.0 and pt2 == 0.0:
        edge1 = edge

    pt3 = sec1_ep.distanceToPoint(edge.Curve.StartPoint)
    pt4 = sec2_ep.distanceToPoint(edge.Curve.EndPoint)

    if pt3 == 0.0 and pt4 == 0.0:
        edge2 = edge


ln1 = Part.LineSegment(sec1_sp, sec1_ep)
ln2 = Part.LineSegment(sec2_sp, sec2_ep)

DOC.recompute()

cl_surf = Part.makeFilledFace(Part.__sortEdges__([edge1, edge2, ln1.toShape(), ln2.toShape()]))

show_curve(cl_surf, "closing surface")

DOC.recompute()

Note the top and bottom face description in the code above was wrong, I have corrected the post


it simply test the distance to the start and ending point of the first and last curve and when both are 0.0 from the edge starting and ending point this is the proper edge, provided that the edge is one continuous curve, but it seems ok with the provided data.

Now the last step is to close the shell and form a solid


Regards

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
User avatar
onekk
Veteran
Posts: 6222
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: How do I make a solid from a B-Spline surface?

Post by onekk »

Code: Select all

shell = Part.makeShell([bot_face, alt_surf, top_face, cl_surf])

solid = Part.makeSolid(shell)

show_curve(solid, "final_solid")

DOC.recompute()


This seems to work as the part "check geometry" is telling is a valid solid.


This is the final code with some polishing around

Code: Select all

# ----code begin here ----

a_tol = 0.001

NO_X = 20
NO_Z = 15

X = 1
Y = 0.1
Z = 2


def ellipse(x, a, b):
    """Calculate the y-coordinate of an ellipse with the one apex at (0,0) and one at (2a,0)."""
    return b/a * math.sqrt(a**2 - (x-a)**2)


def parabola(x, a, b):
    """Calculate the y-coordinate of an parabola through the points (0,0), (a,b) and (2a,0)."""
    return b * (1 - ((x-a)/a)**2)


def gen_points():
    """Generate the points of our surface."""
    array = []
    for j in range(NO_Z):
        z = (j+10)/NO_Z * Z
        b = Z/10 - 0.02*z
        line = []
        # First side (back)
        for i in range(1, NO_X):
            x = i/NO_X * X
            y = parabola(x, X/2, 2*b) + ellipse(x, X/2, b)
            point = Vector(x, y, z)
            line.append(point)

        # Second side (face)
        for i in range(NO_X, 0, -1):
            x = i/NO_X * X
            y = parabola(x, X/2, 2*b) - ellipse(x, X/2, b)
            point = Vector(x, y, z)
            line.append(point)

        array.append(line)

    return array

array = gen_points()

show_points = False

if show_points == True:
    for l_idx,line in enumerate(array):
        for p_idx, pt in enumerate(line):
            show_point(pt, l_idx, p_idx)

DOC.recompute()

sections = []

def close_spline(sec):
    sec_sp = sec.StartPoint
    sec_ep = sec.EndPoint
    
    c_ln = Part.LineSegment(sec_ep, sec_sp)

    edges = Part.sortEdges([c_ln.toShape(), sec.toShape()])[0]

    prof = Part.Wire(edges)
    return prof


for line in array:
    sec = Part.BSplineCurve()
    sec.approximate(line)
        
    sections.append(sec)

    
bs1 = Part.BSplineSurface()
bs1.buildFromNSections(sections)
bs1.exchangeUV()

alt_surf = bs1.toShape()
#show_curve(alt_surf, "alt_surf")

bot_face = Part.Face(close_spline(sections[0]))
#show_curve(bot_face,"bottom")

top_face = Part.Face(close_spline(sections[-1]))
top_face.reverse()
#show_curve(top_face,"top")

DOC.recompute()

sec1_sp = sections[0].StartPoint
sec1_ep = sections[0].EndPoint

sec2_sp = sections[-1].StartPoint
sec2_ep = sections[-1].EndPoint

for edge in alt_surf.Edges:
    pt1 = sec1_sp.distanceToPoint(edge.Curve.StartPoint)
    pt2 = sec2_sp.distanceToPoint(edge.Curve.EndPoint)

    if pt1 == 0.0 and pt2 == 0.0:
        edge1 = edge

    pt3 = sec1_ep.distanceToPoint(edge.Curve.StartPoint)
    pt4 = sec2_ep.distanceToPoint(edge.Curve.EndPoint)

    if pt3 == 0.0 and pt4 == 0.0:
        edge2 = edge


ln1 = Part.LineSegment(sec1_sp, sec1_ep)
ln2 = Part.LineSegment(sec2_sp, sec2_ep)

#show_curve(ln1, "line 1")
#show_curve(ln2, "line 2")

DOC.recompute()

cl_surf = Part.makeFilledFace(Part.__sortEdges__([edge1, edge2, ln1.toShape(), ln2.toShape()]))

#show_curve(cl_surf, "closing surface")

shell = Part.makeShell([bot_face, alt_surf, top_face, cl_surf])

solid = Part.makeSolid(shell)

show_curve(solid, "final_solid")

DOC.recompute()

Regards


Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
sph
Posts: 19
Joined: Sun Jul 25, 2021 6:18 pm

Re: How do I make a solid from a B-Spline surface?

Post by sph »

Thanks, Carlo, with your help I managed to get what I wanted!

What helped me was the mention of the

Code: Select all

Part.makeFilledFace()
method!

I believe, your

Code: Select all

bot_face = Part.Face(close_spline(sections[0]))
top_face = Part.Face(close_spline(sections[-1]))
works only for the special case of flat bottom and top sections and not in the general case of arbitrary section in the 3d space (as mentioned in my very first email).

Anyway, here is my code for anyone, who has the same (or a similar) problem. This code can even export the solid in four different file formats (see commented section near the bottom of the file):

Code: Select all

#!/usr/bin/env python3

import math

from OCC.Core import gp
from OCC.Core import TColgp
from OCC.Core import GeomAPI
from OCC.Core import GeomFill
from OCC.Core import GeomAbs
from OCC.Core import BRepFill
from OCC.Core import BRepBuilderAPI
from OCC.Core import GProp
from OCC.Core import BRepGProp

from OCC.Display.SimpleGui import init_display


NO_X = 10
NO_Z = 10

X = 1
Y = 0.1
Z = 2


def ellipse(x, a, b):
    """Calculate the y-coordinate of an ellipse with the one apex at (0,0) and one at (2a,0)."""
    return b/a * math.sqrt(a**2 - (x-a)**2)


def parabola(x, a, b):
    """Calculate the y-coordinate of an parabola through the points (0,0), (a,b) and (2a,0)."""
    return b * (1 - ((x-a)/a)**2)


def points():
    """Generate the points of our surface."""
    array = TColgp.TColgp_Array2OfPnt(1, 2*NO_X-1, 1, NO_Z)
    for j in range(NO_Z):
        z = (j+10)/NO_Z * Z
        b = Z/10 - 0.02*z

        # First side (back)
        for i in range(1, NO_X):
            x = i/NO_X * X
            y = parabola(x, X/2, 2*b) + ellipse(x, X/2, b)
            point = gp.gp_Pnt(x, y, z)
            array.SetValue(i, j+1, point)

        # Second side (face)
        k = i
        for i in range(NO_X, 0, -1):
            x = i/NO_X * X
            y = parabola(x, X/2, 2*b) - ellipse(x, X/2, b)
            point = gp.gp_Pnt(x, y, z)
            k += 1
            array.SetValue(k, j+1, point)

    return array


# Fit a surface to our points
pts = points()
surface = GeomAPI.GeomAPI_PointsToBSplineSurface()
surface.Interpolate(pts, False)
surface = surface.Surface()

# Boundary lines to our surface
u_back, u_face, v_bottom, v_top = surface.Bounds()
bl_back = surface.UIso(u_back)
bl_face = surface.UIso(u_face)
bl_bottom = surface.VIso(v_bottom)
bl_top = surface.VIso(v_top)

# Fit a surface to the open aft end of our surface
surface_aft = GeomFill.geomfill_Surface(bl_back, bl_face)

# Boundary lines to the aft surface
u_bottom2, u_top2, v_face2, v_back2 = surface_aft.Bounds()
bl_bottom2 = surface_aft.UIso(u_bottom2)
bl_top2 = surface_aft.UIso(u_top2)

# Fit a surface to the open bottom of our surface
edge_bottom = BRepBuilderAPI.BRepBuilderAPI_MakeEdge(bl_bottom)
edge_bottom2 = BRepBuilderAPI.BRepBuilderAPI_MakeEdge(bl_bottom2)
filling_bottom = BRepFill.BRepFill_Filling()
filling_bottom.Add(edge_bottom.Edge(), GeomAbs.GeomAbs_C0)
filling_bottom.Add(edge_bottom2.Edge(), GeomAbs.GeomAbs_C0)
filling_bottom.Build()

# Fit a surface to the open top of our surface
edge_top = BRepBuilderAPI.BRepBuilderAPI_MakeEdge(bl_top)
edge_top2 = BRepBuilderAPI.BRepBuilderAPI_MakeEdge(bl_top2)
filling_top = BRepFill.BRepFill_Filling()
filling_top.Add(edge_top.Edge(), GeomAbs.GeomAbs_C0)
filling_top.Add(edge_top2.Edge(), GeomAbs.GeomAbs_C0)
filling_top.Build()

# Build a shell from all surfaces by sewing them together
shell = BRepBuilderAPI.BRepBuilderAPI_Sewing()
face_surface = BRepBuilderAPI.BRepBuilderAPI_MakeFace(surface, 0.001)
shell.Add(face_surface.Face())
face_aft = BRepBuilderAPI.BRepBuilderAPI_MakeFace(surface_aft, 0.001)
shell.Add(face_aft.Face())
shell.Add(filling_bottom.Face())
shell.Add(filling_top.Face())
shell.Perform()

# Reverse the shell, so that we get a positive volume
shell_shape = shell.SewedShape()
shell_shape.Complement()

# Build a solid
solid = BRepBuilderAPI.BRepBuilderAPI_MakeSolid(shell_shape)
solid.Build()

# Get the volume (which is the mass, if no density is defined)
gprop = GProp.GProp_GProps()
BRepGProp.brepgprop_VolumeProperties(solid.Shape(), gprop)
assert gprop.Mass() > 0, 'Shell is oriented the wrong way!'
print(f'Volume = {gprop.Mass()}')
print()

# # Export to .brep file
# from OCC.Core import BRepTools
# BRepTools.breptools_Write(solid, 'solid.brep')
# # Export other formats
# from OCC.Extend import DataExchange
# # Write STEP file
# DataExchange.write_step_file(solid.Shape(), 'solid.step')
# # Write STL file
# DataExchange.write_stl_file(
#     solid.Shape(), 'solid.stl', linear_deflection=X/NO_X/10)
# # Write IGES file
# DataExchange.write_iges_file(solid.Shape(), 'solid.iges')
# print()

# Display the surfaces
display, start_display, _, _ = init_display()
display.DisplayShape(solid.Shape())
display.DisplayShape(gprop.CentreOfMass())
display.FitAll()
start_display()
Maybe all this is easier using FreeCAD's API, but I wanted to keep the number (and size) of the dependencies small.
User avatar
onekk
Veteran
Posts: 6222
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: How do I make a solid from a B-Spline surface?

Post by onekk »

sph wrote: Wed Aug 04, 2021 8:12 pm Thanks, Carlo, with your help I managed to get what I wanted!

What helped me was the mention of the

Code: Select all

Part.makeFilledFace()
method!

I believe, your

Code: Select all

bot_face = Part.Face(close_spline(sections[0]))
top_face = Part.Face(close_spline(sections[-1]))
works only for the special case of flat bottom and top sections and not in the general case of arbitrary section in the 3d space (as mentioned in my very first email).
Its is quite general, if you costruct the surface using a set of sections as open curves and store them in a list:

Code: Select all

sections[0]
is the first section.

Code: Select all

sections[-1]
is the last section index -1 in python mean last element of the list.
sph wrote: Wed Aug 04, 2021 8:12 pm Maybe all this is easier using FreeCAD's API, but I wanted to keep the number (and size) of the dependencies small.
This affirmation seem to be wrong, using FreeCAD API you have only these dependencies:

Code: Select all

import FreeCAD
import Part
import math

from FreeCAD import Vector, Rotation
the latter is only to shorten methods writings.

Regards

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
sph
Posts: 19
Joined: Sun Jul 25, 2021 6:18 pm

Re: [SOLVED] How do I make a solid from a B-Spline surface?

Post by sph »

I beg to differ: I don't want to install the whole FreeCAD ecosystem with all its dependencies, I just want to generate a brep/step/iges file.

And with a bit of metaclass magic, my import statements boil down to one:

Code: Select all

from occ import occ
:geek:
User avatar
onekk
Veteran
Posts: 6222
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: [SOLVED] How do I make a solid from a B-Spline surface?

Post by onekk »

Thanks for the last post explaining the scope.

So you were searching information about pythonOCC and not FreeCAD.

As PythonOCC is more near the way OCCT is behaving, maybe searching directly on the OCCT pages will help you more, as FreeCAD is not so near the OCCT code, some things are implemented in a more "high level way" than PythonOCC even if the methods names are similar.

When porting FreeCAD code to PythonOCC this thing has to be clear, there are other things that simply than a "python bridge" to OCCT "C++ libraries.

So "your mileage may vary".

Regards

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
sph
Posts: 19
Joined: Sun Jul 25, 2021 6:18 pm

Re: [SOLVED] How do I make a solid from a B-Spline surface?

Post by sph »

IIRC, I mentioned all the disclaimers about opencascade/pythonocc in my very first post, so I wonder, why we discuss this here … I also mentioned, that I had troubles getting the information on opencascade's forum.

I was hoping to either get a solution directly in opencascade/pythonocc or also in FreeCAD, which would help me by looking at FreeCAD's code. And that worked! With your valuable help I managed to figure out, how to do it (in FreeCAD and then in opencascade/pythonocc). Your input was very valuable, indeed. Thanks!!
User avatar
onekk
Veteran
Posts: 6222
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: [SOLVED] How do I make a solid from a B-Spline surface?

Post by onekk »

You are welcome.

I'm only an user, but as I'm struggling with something similar to you problem I'm happy to share some knowledge (even it is not so deep).

Regards.

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
Post Reply