"Project on Surface" using Scripting.
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
Be nice to others! Respect the FreeCAD code of conduct!
"Project on Surface" using Scripting.
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.
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/
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.
Blog: https://okkmkblog.wordpress.com/
-
- Veteran
- Posts: 3106
- Joined: Thu Sep 24, 2020 10:31 pm
- Location: Hawaii
- Contact:
Re: "Poject on Surface" using Scripting.
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.
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.
Re: "Project on Surface" using Scripting.
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.
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/
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.
Blog: https://okkmkblog.wordpress.com/
Re: "Project on Surface" using Scripting.
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
Markus
Re: "Project on Surface" using Scripting.
Thanks for the reply and for the warning.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).
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/
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.
Blog: https://okkmkblog.wordpress.com/
Re: "Project on Surface" using Scripting.
Code: Select all
help(Part.makeWireString)
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)
Re: "Project on Surface" using Scripting.
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.Chris_G wrote: ↑Wed Jul 27, 2022 10:09 amCode: Select all
help(Part.makeWireString)
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)
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:
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/
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.
Blog: https://okkmkblog.wordpress.com/
Re: "Project on Surface" using Scripting.
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.
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()
Re: "Project on Surface" using Scripting.
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.
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.
Re: "Project on Surface" using Scripting.
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/
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.
Blog: https://okkmkblog.wordpress.com/