"Project on Surface" using Scripting.

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: 6144
Joined: Sat Jan 17, 2015 7:48 am
Contact:

"Project on Surface" using Scripting.

Post by onekk »

Hello, I've seen this thread:

https://forum.freecadweb.org/viewtopic.php?f=3&t=70515

And I've seen the solution from @domad .

I've had not done similar things, but it seem interesting to know, if this could be achieved with scripting and how.

I know that is "very general" question, but about fonts and similar thing I know very little:

1) how to obtain solids from fonts
2) how to project them on a curved surface, and related problems

I have no code to start from, even if I've seen in the past something about this arguments in macros, and honestly I've not done a proper search.

Someone know even some link to study?

TIA and Regards

Carlo D.
Last edited by onekk on Wed Jul 27, 2022 8:30 am, edited 1 time 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/
edwilliams16
Veteran
Posts: 3106
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: "Poject on Surface" using Scripting.

Post by edwilliams16 »

I'd watch https://www.youtube.com/watch?v=7q5eo9lE6m8 and follow through on the Python Console. Need to create a Sketch_On_Surface object.
By hand, it will require you to map the shapestring on the XY_plane with a .toShape(face)

https://github.com/tomate44/CurvesWB/bl ... Surface.py for clues, maybe.
User avatar
onekk
Veteran
Posts: 6144
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: "Project on Surface" using Scripting.

Post by onekk »

Thanks, I have seen the video, probably it is possible to make use of direct use of methods in Curves WB, the code in the creation phase is not complicated

1) to obtain a shapestring, I have to use Draft, I have to see if is possible to script this passage
2) I have to see if it is possible to assign properties using Scripting to the curvesOnSurface method
3) I will try to put together some script code.

Thanks again and Regards

Carlo D.
Last edited by onekk on Wed Jul 27, 2022 9:21 am, edited 1 time 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
mfro
Posts: 662
Joined: Sat Sep 23, 2017 8:15 am

Re: "Project on Surface" using Scripting.

Post by mfro »

onekk wrote: Wed Jul 27, 2022 8:21 am Thanks, I have seen the video, probably it is possible to make use of direct use of methods in Curves WB, the code in the creation phase is not complicated
Beware: Curves SketchOnSurface and the solution shown in the linked thread follow completely different approaches.

To my understanding, the former projects along the target surface normals while the latter projects along the view direction. While the former inevitably enforces a distortion of the projection if the target surface is "noisy" (in terms of surface normal variation), the latter can only project to the visible part of the target surface (i.e. you can't map a sketch to a cylinder circumference, for example).
Cheers,
Markus
User avatar
onekk
Veteran
Posts: 6144
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: "Project on Surface" using Scripting.

Post by onekk »

mfro wrote: Wed Jul 27, 2022 8:39 am To my understanding, the former projects along the target surface normals while the latter projects along the view direction. While the former inevitably enforces a distortion of the projection if the target surface is "noisy" (in terms of surface normal variation), the latter can only project to the visible part of the target surface (i.e. you can't map a sketch to a cylinder circumference, for example).
Thanks for the reply and for the warning.
EDIT: I have taken the wrong code to analyse

If you intend curveOnSurface from Curves WB it seems that view direction is not taken in account, the surface.


More study to do.

END EDIT

Or maybe I've interpreted wrong your reply, if this is the case, sorry.

I know limitations of surfaces, especially when they are generated in wrong manners of trying to achieve some strange effects that will lead of problems
related to normals that are non uniforms or messed ups, like when you try to obtain lofts or sweeps that lead to intersecting surfaces as in the problem in this forum post:

https://forum.freecadweb.org/viewtopic.php?f=22&t=65467

I've quite experimented using scripting, as I have modelled many things, using different techniques.

My goal is to develop a way to make almost all my modelling using scripting, so projecting a Shapestring on a surface could be very handy on some works, like 3d printing models and similar tasks.

For this I've opened this topis, as usually my workflow is modelling the entire structure with scritping, using the Gui, is somewhat difficult, as sometimes it is almost impossible to have a scripted representation of the object ready to be placed in script that will create all the shapes from the script itself.

I know this is an "unusual way" to use FC, but in the past three years I've made many projects this way.

I hope this is feasible, so one more tools in my toolbox would be ready to use for future projects.

I know also that this workflow could be seen as "strange", but code is clear and with some comment could be a very "help for memory" when doing complex things, using GUI it is easy to forgot some setting and take note of all the things, is difficult, once you have written a test code, even a complex one you could ever with some study and if you have put relevant comment in it reconstruct the complex work done and reuse it.

Not counting that if you make a "big mistake" after having worked with the GUI you have more hassles than maintain some copies of the "more small" script file.

But this is my "very personal way" of doing things.

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
Chris_G
Veteran
Posts: 2578
Joined: Tue Dec 31, 2013 4:10 pm
Location: France
Contact:

Re: "Project on Surface" using Scripting.

Post by Chris_G »

onekk wrote: Wed Jul 27, 2022 8:21 am 1) to obtain a shapestring, I have to use Draft, I have to see if is possible to script this passage

Code: Select all

help(Part.makeWireString)
onekk wrote: Wed Jul 27, 2022 8:21 am 2) I have to see if it is possible to assign properties using Scripting to the curvesOnSurface method
Not sure it is the right path. This is an old tool, that is not really useful.

I see two possible, but different, ways :

- map the shapestring to a target surface, by converting to 2D curves, and applying them on target with toShape(target_surface). The shapestring will have the same deformation as the target surface, (this is the Sketch_On_Surface approach)

- project the shapestring on the target, with the_shapestring.makeParallelProjection(). The shapestring will not be deformed, and this may also work on a shape made of multiple faces. (this is the Part.Projection_On_Surface approach)
User avatar
onekk
Veteran
Posts: 6144
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: "Project on Surface" using Scripting.

Post by onekk »

Chris_G wrote: Wed Jul 27, 2022 10:09 am
onekk wrote: Wed Jul 27, 2022 8:21 am 1) to obtain a shapestring, I have to use Draft, I have to see if is possible to script this passage

Code: Select all

help(Part.makeWireString)
onekk wrote: Wed Jul 27, 2022 8:21 am 2) I have to see if it is possible to assign properties using Scripting to the curvesOnSurface method
Not sure it is the right path. This is an old tool, that is not really useful.

I see two possible, but different, ways :

- map the shapestring to a target surface, by converting to 2D curves, and applying them on target with toShape(target_surface). The shapestring will have the same deformation as the target surface, (this is the Sketch_On_Surface approach)

- project the shapestring on the target, with the_shapestring.makeParallelProjection(). The shapestring will not be deformed, and this may also work on a shape made of multiple faces. (this is the Part.Projection_On_Surface approach)
Many Thanks @Chris_G I have seen the code and it is difficult to use with a Script, without rewriting most of the code of Sketch_On_Surface to be used without the GUI part.


I have used Part.makeWireString in my code now I could guess that I have to map all the wires to the destination surface, say as example a surface derived from a cylinder to make thing easier in the example code:
20220727-text_on_surface.py
(3.29 KiB) Downloaded 35 times
Could I abuse of your kindness?

I could guess from your description in case map the shapestring to a target surface this seems not to different from other things I've done

I have to:

1) convert to 2D curves but after some research I've not found a clever way of doing it for every wire extracted in the code, probably I'm missing something, or most probably I've already seen the way of doing this but I've forgotten to note it for "future use"


Point 2) seems not posing problems, once I have obtained "wires 2D" from the wires returned from makeWireString

Code: Select all

obj.toShape(cyl1)

my only concern is if what is the way to move things around to properly position on the destination surface (UV map) as it seems that there is no a Placement property for Geom2D objects.

TIA and 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
Chris_G
Veteran
Posts: 2578
Joined: Tue Dec 31, 2013 4:10 pm
Location: France
Contact:

Re: "Project on Surface" using Scripting.

Post by Chris_G »

Here is a script with explained workflow.
This is roughly what is done in Sketch On Surface.
The main trick is to use a BSpline rectangle (quad) as the projection surface, whose geometry extends around the wirestring, but whose parametric space is the same as the target face.
So, the 2d curves extracted from the projection will be in the parametric space of the target surface.

Code: Select all

"""Project text on surface

This code was written as an sample code

Name: 20220727-text_on_surface.py

Author: Carlo Dormeletti
Copyright: 2022
Licence: CC BY-NC-ND 4.0 IT
"""

import os
import sys  # noqa
import FreeCAD
import FreeCADGui
from FreeCAD import Placement, Rotation, Vector # noqa
import Part # noqa

from math import pi, sin, cos # noqa

V2d = FreeCAD.Base.Vector2d

DOC_NAME = "text_on_surface"


def activate_doc():
    """Activate document."""
    FreeCAD.setActiveDocument(DOC_NAME)
    FreeCAD.ActiveDocument = FreeCAD.getDocument(DOC_NAME)
    FreeCADGui.ActiveDocument = FreeCADGui.getDocument(DOC_NAME)
    print("{0} activated".format(DOC_NAME))


def setview():
    """Rearrange View."""
    DOC.recompute()
    VIEW.viewAxometric()
    VIEW.setAxisCross(True)
    VIEW.fitAll()


def deleteObject(obj):
    """Delete documentObject."""
    if hasattr(obj, "InList") and len(obj.InList) > 0:
        for o in obj.InList:
            deleteObject(o)
            try:
                DOC.removeObject(o.Name)
            except RuntimeError as rte:
                errorMsg = str(rte)
                if errorMsg != "This object is currently not part of a document":
                    FreeCAD.Console.PrintError(errorMsg)
                    return False
    return True


def clear_DOC():
    """Clear ActiveDocument deleting all the objects."""
    while DOC.Objects:
        obj = DOC.Objects[0]
        name = obj.Name

        if not hasattr(DOC, name):
            continue

        if not deleteObject(obj):
            FreeCAD.Console.PrintError("Exiting on error")
            os.sys.exit()

        DOC.removeObject(obj.Name)

        DOC.recompute()


if FreeCAD.ActiveDocument is None:
    FreeCAD.newDocument(DOC_NAME)
    print("Document: {0} Created".format(DOC_NAME))

# test if there is an active document with a "proper" name
if FreeCAD.ActiveDocument.Name == DOC_NAME:
    print("DOC_NAME exist")
else:
    print("DOC_NAME is not active")
    # test if there is a document with a "proper" name
    try:
        FreeCAD.getDocument(DOC_NAME)
    except NameError:
        print("No Document: {0}".format(DOC_NAME))
        FreeCAD.newDocument(DOC_NAME)
        print("Document {} Created".format(DOC_NAME))

DOC = FreeCAD.getDocument(DOC_NAME)
GUI = FreeCADGui.getDocument(DOC_NAME)
VIEW = GUI.ActiveView
# print("DOC : {0} GUI : {1}".format(DOC, GUI))
activate_doc()
# print(FreeCAD.ActiveDocument.Name)
clear_DOC()

# Handy abbreviations

VZOR = Vector(0, 0, 0)
ROT0 = Rotation(0, 0, 0)

# Tolerances
EPS = 0.01

# it permit to apply half of the above tolerance, useful when cutting
# pipes using a length and EPS as quantity to add to inner cylinder

EPS_C = EPS * -0.5

# CODE START HERE
sh_s = "String"
fnt_dir = "/usr/share/fonts/TTF/"
fnt_nm = "DejaVuSerif.ttf"
fnt_size = 10

wire_s = Part.makeWireString(sh_s, fnt_dir, fnt_nm, fnt_size)

print(wire_s)

for s_idx, s_wire in enumerate(wire_s):
    wire_n = f"s_wire_{s_idx}"
    if len(s_wire) > 1:
        for sw_idx, sub_wire in enumerate(s_wire):
            Part.show(sub_wire, f"{wire_n}_{sw_idx}")
    else:
        Part.show(s_wire[0], wire_n)


c_rad = 20
c_heig = fnt_size * 1.5

cyl1 = Part.makeCylinder(c_rad, c_heig, VZOR, Vector(0, 0, 1), 180)

# Part.show(cyl1, "cilinder_1")

surf = cyl1.Faces[0]

Part.show(surf, "surface")

'''************  Map a wirestring on a face
- get a compound of the wirestring edges
- get its bounding box
- create a BSpline rectangle around the wirestring (with some margins)
- get the parameter range of the target face
- assign this parameter range to the BSpline rectangle
- project the wirestring on the BSpline rectangle
- get the 2d curves from the projection
- map these 2d curves on the cylinder
'''

edges = []
for s_idx, s_wire in enumerate(wire_s):
    for sw_idx, sub_wire in enumerate(s_wire):
        for e in sub_wire.Edges:
            edges.append(e)

comp = Part.Compound(edges)
bb = comp.BoundBox

margin_U_neg = 5
margin_U_pos = 10
margin_V_neg = 3
margin_V_pos = 20

p00 = FreeCAD.Vector(bb.XMin - margin_U_neg, bb.YMin - margin_V_neg, 0)
p01 = FreeCAD.Vector(bb.XMin - margin_U_neg, bb.YMax + margin_V_pos, 0)
p10 = FreeCAD.Vector(bb.XMax + margin_U_pos, bb.YMin - margin_V_neg, 0)
p11 = FreeCAD.Vector(bb.XMax + margin_U_pos, bb.YMax + margin_V_pos, 0)

quad = Part.BSplineSurface()
quad.setPole(1, 1, p00)
quad.setPole(1, 2, p01)
quad.setPole(2, 1, p10)
quad.setPole(2, 2, p11)

u0, u1, v0, v1 = surf.ParameterRange

quad.setUKnot(1, u0)
quad.setUKnot(2, u1)
quad.setVKnot(1, v0)
quad.setVKnot(2, v1)

quad_face = quad.toShape()

def map_wire(wire, surface, quad):
    """Map wire on target surface, using quad"""
    proj = quad.project(wire.Edges)

    mapped_edges = []
    for e in proj.Edges:
        c, fp, lp = quad.curveOnSurface(e)
        mapped_edges.append(c.toShape(surf, fp, lp))

    return Part.Wire(mapped_edges)

for s_idx, s_wire in enumerate(wire_s):
    wire_n = f"m_wire_{s_idx}"
    if len(s_wire) > 1:
        for sw_idx, sub_wire in enumerate(s_wire):
            mw = map_wire(sub_wire, surf.Surface, quad_face)
            Part.show(mw, f"{wire_n}_{sw_idx}")
    else:
        mw = map_wire(s_wire[0], surf.Surface, quad_face)
        Part.show(mw, wire_n)


DOC.recompute()
setview()
User avatar
Chris_G
Veteran
Posts: 2578
Joined: Tue Dec 31, 2013 4:10 pm
Location: France
Contact:

Re: "Project on Surface" using Scripting.

Post by Chris_G »

Here is another method.
It uses a function that was added to FC yesterday, so you need a fresh build.
This time, the target surface is converted to BSpline and its parametric space is stretched to match the wirestring "real world" dimensions.
So it skips the intermediate projection on the BSpline rectangle.

Code: Select all

"""Project text on surface

This code was written as an sample code

Name: 20220727-text_on_surface.py

Author: Carlo Dormeletti
Copyright: 2022
Licence: CC BY-NC-ND 4.0 IT
"""

import os
import sys  # noqa
import FreeCAD
import FreeCADGui
from FreeCAD import Placement, Rotation, Vector # noqa
import Part # noqa

from math import pi, sin, cos # noqa

V2d = FreeCAD.Base.Vector2d

DOC_NAME = "text_on_surface"


def activate_doc():
    """Activate document."""
    FreeCAD.setActiveDocument(DOC_NAME)
    FreeCAD.ActiveDocument = FreeCAD.getDocument(DOC_NAME)
    FreeCADGui.ActiveDocument = FreeCADGui.getDocument(DOC_NAME)
    print("{0} activated".format(DOC_NAME))


def setview():
    """Rearrange View."""
    DOC.recompute()
    VIEW.viewAxometric()
    VIEW.setAxisCross(True)
    VIEW.fitAll()


def deleteObject(obj):
    """Delete documentObject."""
    if hasattr(obj, "InList") and len(obj.InList) > 0:
        for o in obj.InList:
            deleteObject(o)
            try:
                DOC.removeObject(o.Name)
            except RuntimeError as rte:
                errorMsg = str(rte)
                if errorMsg != "This object is currently not part of a document":
                    FreeCAD.Console.PrintError(errorMsg)
                    return False
    return True


def clear_DOC():
    """Clear ActiveDocument deleting all the objects."""
    while DOC.Objects:
        obj = DOC.Objects[0]
        name = obj.Name

        if not hasattr(DOC, name):
            continue

        if not deleteObject(obj):
            FreeCAD.Console.PrintError("Exiting on error")
            os.sys.exit()

        DOC.removeObject(obj.Name)

        DOC.recompute()


if FreeCAD.ActiveDocument is None:
    FreeCAD.newDocument(DOC_NAME)
    print("Document: {0} Created".format(DOC_NAME))

# test if there is an active document with a "proper" name
if FreeCAD.ActiveDocument.Name == DOC_NAME:
    print("DOC_NAME exist")
else:
    print("DOC_NAME is not active")
    # test if there is a document with a "proper" name
    try:
        FreeCAD.getDocument(DOC_NAME)
    except NameError:
        print("No Document: {0}".format(DOC_NAME))
        FreeCAD.newDocument(DOC_NAME)
        print("Document {} Created".format(DOC_NAME))

DOC = FreeCAD.getDocument(DOC_NAME)
GUI = FreeCADGui.getDocument(DOC_NAME)
VIEW = GUI.ActiveView
# print("DOC : {0} GUI : {1}".format(DOC, GUI))
activate_doc()
# print(FreeCAD.ActiveDocument.Name)
clear_DOC()

# Handy abbreviations

VZOR = Vector(0, 0, 0)
ROT0 = Rotation(0, 0, 0)

# Tolerances
EPS = 0.01

# it permit to apply half of the above tolerance, useful when cutting
# pipes using a length and EPS as quantity to add to inner cylinder

EPS_C = EPS * -0.5

# CODE START HERE
sh_s = "String"
fnt_dir = "/usr/share/fonts/TTF/"
fnt_nm = "DejaVuSerif.ttf"
fnt_size = 10

wire_s = Part.makeWireString(sh_s, fnt_dir, fnt_nm, fnt_size)

print(wire_s)

for s_idx, s_wire in enumerate(wire_s):
    wire_n = f"s_wire_{s_idx}"
    if len(s_wire) > 1:
        for sw_idx, sub_wire in enumerate(s_wire):
            Part.show(sub_wire, f"{wire_n}_{sw_idx}")
    else:
        Part.show(s_wire[0], wire_n)


c_rad = 20
c_heig = fnt_size * 1.5

cyl1 = Part.makeCylinder(c_rad, c_heig, VZOR, Vector(0, 0, 1), 180)

# Part.show(cyl1, "cilinder_1")

surf = cyl1.Faces[0]

Part.show(surf, "surface")

'''************  Map a wirestring on a face --- Method 2
- get a compound of the wirestring edges
- get its bounding box
- convert target face to BSpline surface
- set parametric space of BSpline surface to bounding box + some margins
- get the 2d curves of the wirestring in the XY plane
- map these 2d curves on the BSpline surface
'''

edges = []
for s_idx, s_wire in enumerate(wire_s):
    for sw_idx, sub_wire in enumerate(s_wire):
        for e in sub_wire.Edges:
            edges.append(e)

comp = Part.Compound(edges)
bb = comp.BoundBox

margin_U_neg = 5
margin_U_pos = 10
margin_V_neg = 3
margin_V_pos = 20

umin = bb.XMin - margin_U_neg
umax = bb.XMax + margin_U_pos
vmin = bb.YMin - margin_V_neg
vmax = bb.YMax + margin_V_pos

u0, u1, v0, v1 = surf.ParameterRange
rts = Part.RectangularTrimmedSurface(surf.Surface, u0, u1, v0, v1)
bs = rts.toBSpline()

bs.scaleKnotsToBounds(umin, umax, vmin, vmax)  # added to FC master on 2022/07/27


def map_wire(wire, surface):
    """Map wire on target surface
    Input wire must be on XY plane"""
    plane = Part.Plane().toShape()
    mapped_edges = []
    for e in wire.Edges:
        c, fp, lp = plane.curveOnSurface(e)
        mapped_edges.append(c.toShape(surface, fp, lp))
    return Part.Wire(mapped_edges)


for s_idx, s_wire in enumerate(wire_s):
    wire_n = f"m_wire_{s_idx}"
    if len(s_wire) > 1:
        for sw_idx, sub_wire in enumerate(s_wire):
            mw = map_wire(sub_wire, bs)
            Part.show(mw, f"{wire_n}_{sw_idx}")
    else:
        mw = map_wire(s_wire[0], bs)
        Part.show(mw, wire_n)


DOC.recompute()
setview()
Last edited by Chris_G on Tue Aug 30, 2022 9:24 pm, edited 1 time in total.
User avatar
onekk
Veteran
Posts: 6144
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: "Project on Surface" using Scripting.

Post by onekk »

Chris_G wrote: Thu Jul 28, 2022 9:22 am ....
Many thanks I will study the code for 0.20

Edit:
I will probably wait to test second version until there will be a "proper" update of conda build I updated today but version is the following:

Code: Select all

OS: Artix Linux (openbox)
Word size of FreeCAD: 64-bit
Version: 0.21.29393 (Git)
Build type: Release
Branch: master
Hash: 6820e0a9ec85203a6f342ca72a2ff8fd417beaf1
Python 3.9.13, Qt 5.12.9, Coin 4.0.0, Vtk 9.1.0, OCC 7.5.3
Locale: Italian/Italy (it_IT)
Installed mods: 
  * test_wb
  * fcgear 1.0.0
  * Curves 0.4.1
  * Help 1.0.3

Regards

Carlo D.
Last edited by onekk on Mon Jan 09, 2023 4:31 pm, 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/
Post Reply