TechDraw TemplateGenerator: Difference between revisions

From FreeCAD Documentation
mNo edit summary
m (description improved)
 
(38 intermediate revisions by 3 users not shown)
Line 4: Line 4:
<!--T:1-->
<!--T:1-->
{{TutorialInfo
{{TutorialInfo
|Topic=Template generation with Python macro
|Topic=Create a TechDraw template using a Python macro
|Level=Basic skills of Python and svg-structures are helpful
|Level=Basic skills of Python and svg-structures are helpful
|FCVersion=0.19.1 and later
|FCVersion=0.21 and later
|Time=(I don't know yet)
|Time=(I don't know yet)
|Author=[[User:FBXL5|FBXL5]]
|Author=[[User:FBXL5|FBXL5]]
|SeeAlso=[[Macro_TemplateHelper|Macro TemplateHelper]]
}}
}}


Line 17: Line 18:


<!--T:4-->
<!--T:4-->
Any text editor can be used to code. My choice is Atom, but FreeCAD's built-in editor works well, too.
Any text editor can be used to code. My choice is Atom, but FreeCAD's built-in editor works well, too.


<!--T:5-->
<!--T:5-->
Line 27: Line 28:
<!--T:7-->
<!--T:7-->
The templates are svg-files and so a macro has to compose some lines of svg code (which is a subset of xml code).
The templates are svg-files and so a macro has to compose some lines of svg code (which is a subset of xml code).

<!--T:51-->
'''Note:''' When FreeCAD was migrated from '''freecadweb.org''' to '''freecad.org''' this page was updated accordingly and the resulting SVG code is no longer compatible with FreeCAD versions older than v0.21. For those versions you need to manually change {{Incode|freecad.org}} to {{Incode|freecadweb.org}} on the namespace declaration line in the resulting SVG code, otherwise the editable texts are not recognized.


== Structure of a simple blank page == <!--T:8-->
== Structure of a simple blank page == <!--T:8-->
Line 33: Line 37:
The SVG format is a subset of the XML format.
The SVG format is a subset of the XML format.
That is why an SVG file, like any XML file, consists of two parts:
That is why an SVG file, like any XML file, consists of two parts:
* Head, a format declaration
* Head, a format declaration
* Body, containing the information what to show and where to place it
* Body, containing the information what to show and where to place it
: (I don't know why there should be a headline, the svg-file is still a valid template file without it...)
: (I don't know why there should be a headline, the svg-file is still a valid template file without it...)
Line 57: Line 61:
<svg
<svg
xmlns="http://www.w3.org/2000/svg" version="1.1"
xmlns="http://www.w3.org/2000/svg" version="1.1"
xmlns:freecad="http://www.freecadweb.org/wiki/index.php?title=Svg_Namespace"
xmlns:freecad="http://www.freecad.org/wiki/index.php?title=Svg_Namespace"
width="420mm"
width="420mm"
height="297mm"
height="297mm"
Line 68: Line 72:
: '''xmlns='''"http://www.w3.org/2000/svg": External link to the xml name space to look up standard xml commands
: '''xmlns='''"http://www.w3.org/2000/svg": External link to the xml name space to look up standard xml commands
: '''version='''"1.1": Used xml version is 1.1
: '''version='''"1.1": Used xml version is 1.1
: '''xmlns:freecad='''"[https://wiki.freecadweb.org/index.php?title=Svg_Namespace Svg Namespace]": External link to FreeCAD's name space extension
: '''xmlns:freecad='''"...=Svg_Namespace": External link to FreeCAD's [[Svg_Namespace|Svg Namespace]] wiki page. The link is not used to retrieve information or values at runtime, but it is the key to activate the custom attributes, e.g. those for editable texts.
: "freecad:" will be prefixed to said custom attribute names.
:: to look up special commands that are only used inside a FreeCAD environment e.g. for editable texts
: '''width='''"420mm": Width of the drawing area
: '''width='''"420mm": Width of the drawing area
: '''height='''"297mm": Height of the drawing area
: '''height='''"297mm": Height of the drawing area
: '''viewBox='''"0 0 420 297": Position of the upper left corner (0;0) and the lower right corner (420;297) in the svg construction space (in svg units).
: '''viewBox='''"0 0 420 297": Position of the upper left corner (0;0) and the lower right corner (420;297) in the svg construction space (in svg units).
: Width, height, and viewBox in this combination set 1 svg-unit to 1 mm for the whole document. A dimensional unit can be omitted from now on.
: Width, height, and viewBox in this combination set 1 svg-unit to 1 mm for the whole document. A dimensional unit can be omitted from now on.
: In this case 420 and 297 give an A3 page. Customise these values to generate other page sizes
: In this case 420 and 297 give an A3 page. Customise these values to generate other page sizes.


<!--T:15-->
<!--T:15-->
Line 84: Line 88:
<svg
<svg
xmlns="http://www.w3.org/2000/svg" version="1.1"
xmlns="http://www.w3.org/2000/svg" version="1.1"
xmlns:freecad="http://www.freecadweb.org/wiki/index.php?title=Svg_Namespace"
xmlns:freecad="http://www.freecad.org/wiki/index.php?title=Svg_Namespace"
width="420mm"
width="420mm"
height="297mm"
height="297mm"
Line 93: Line 97:


== Python code... == <!--T:16-->
== Python code... == <!--T:16-->

<!--T:52-->
Coding starts with a framework containing a separate main function:

</translate>
{{Code| |code=
#! python
# -*- coding: utf-8 -*-
# (c) 2024 Your name LGPL


def main():
"""Here is where the magic happens"""
return

if __name__ == '__main__':
# This will be true only if the file is "executed"
# but not if imported as module
main()
}}
<translate>

=== ... to create a blank page === <!--T:22-->

<!--T:25-->
The process of creating a template consists of:
* Locate the template folder.
* Open a file for writing to create an svg-file from scratch, write the header line and close the file as a first step.
* And then repeatedly open the file to append further segments, closing it again each time.

<!--T:56-->
The macro is made of several functions which are called from the main section.<br>
Additional functions could be inserted before the EndSvg function and the needed calls are put before the EndSvg() call.<br>
We need to have an eye on the number of spaces used with the write operations for a correct indentation.

==== pathToTemplate() ==== <!--T:57-->


<!--T:17-->
<!--T:17-->
Before any code is generated a folder is needed to store the new template file.
Before any code is generated a folder is needed to store the new template file, and a file name has to be set.


<!--T:18-->
<!--T:18-->
Line 101: Line 141:
It is not necessary to know where the preferences are store, because FreeCAD has commands to address needed parameters directly.
It is not necessary to know where the preferences are store, because FreeCAD has commands to address needed parameters directly.


</translate>
<!--T:19-->
{{Code| |code=
{{Code| |code=
def pathToTemplate(template_name):
parameter_path = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/Files")
"""Link a given template name to the path of the template folder"""
template_path = parameter_path.GetString("TemplateDir")
#- Get the path to the template folder that is set in the FreeCAD parameters
template_name = "MyTemplate.svg"
parameter_path = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/Files")
template_file = os.path.join(template_path, template_name)
template_path = parameter_path.GetString("TemplateDir")
#- Link template_path and template_name for any OS
path_to_file = os.path.join(template_path, template_name) # to join path segments OS neutral
return path_to_file
}}
}}
<translate>


<!--T:20-->
<!--T:20-->
'''parameter_path''' receives the path to the "folder" within the configuration file where "TemplateDir" parameter can be found. </br>
'''parameter_path''' receives the path to the "folder" within the configuration file where "TemplateDir" parameter can be found. </br>
'''template_path''' receives the content of "TemplateDir" which is the path to the template directory. </br>
'''template_path''' receives the content of "TemplateDir" which is the path to the template directory.<br>
'''template_name''' recieves the name of the template to be created.
'''template_name''' recieves the name of the template to be created.


<!--T:21-->
<!--T:21-->
Now the template name needs to be linked to the template path in a way that is compatible to unix based OSs and Windows.</br>
Now the template name needs to be linked to the template path in a way that is compatible to unix based OSs and Windows.</br>
This is done with the "os.path.join" command and stored into the '''template_file'''.
This is done with the "os.path.join" command and stored into the '''template_file'''. To enable this command an "import os" instruction is required.


=== ... to create a blank page === <!--T:22-->
==== createSvgFile() ==== <!--T:58-->

<!--T:59-->
This creates a new Template file and saves the xml head line.

</translate>
{{Code| |code=
def createSvgFile(file_path):
# Create a file and insert a header line (with t as the space saving variant of template)
t = open(file_path, "w") # w = write, overwrites existing files
t.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
t.close
}}
<translate>

==== startSvg() ==== <!--T:61-->

<!--T:62-->
This appends the template file and creates the svg opening tag including its attributes.<br>
Each write instruction contains one single line of svg code ending with "\n", the marker for CR/LF.

</translate>
{{Code| |code=
def startSvg(file_path, sheet_width, sheet_height):
# Create svg-tag including namespace and format definitions
t = open(file_path, "a", encoding="utf-8")
# a = append, new lines are added at the end of an existing file
# encoding="utf-8", helps with special characters if the Python interpreter is in ASCII mode
t.write("\n" + "\n")
t.write("<svg\n")
#- Namespace declarations
t.write(" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"\n")
t.write(" xmlns:freecad=\"http://www.freecad.org/wiki/index.php?title=Svg_Namespace\"\n")
#- Format definition
t.write(" width =\"" + sheet_width + "mm\"\n")
t.write(" height=\"" + sheet_height + "mm\"\n")
t.write(" viewBox=\"0 0 " + sheet_width + " " + sheet_height + "\">\n")
# identical values for width and height and Viewbox' width and height
# will synchronise mm and svg-units
t.close
}}
<translate>

==== endSvg() ==== <!--T:64-->

<!--T:65-->
This further appends the template file and creates the svg closing tag; This eventually finishes the template code.

</translate>
{{Code| |code=
def endSvg(file_path):
# Create closing svg-tag
t = open(file_path, "a", encoding="utf-8")
t.write("</svg>")
t.close
}}
<translate>

==== main() ==== <!--T:67-->

<!--T:68-->
The main() function calls the functions and hands over some parameters.

</translate>
{{Code| |code=
def main():
"""This one creates an empty A3 template"""
#- Set the name of the template file and get its location
template_file = pathToTemplate("MyTemplate.svg") # Change the template name here
#- Here starts the compiling of the svg file
createSvgFile(template_file) # overwrites existing File
#- Set sheet format (DIN A3)
format_width = "420"
format_height = "297"
startSvg(template_file, format_width, format_height) # adds svg start tag
endSvg(template_file) # adds svg end tag
# At this point a new SVG-file is generated and saved
return
}}
<translate>

<!--T:70-->
In this example {{Incode|format_width}} and {{Incode|format_height}} are hard coded dimensions, both unnecessary lines mark the points where other means of retrieving format data could put their content.


<!--T:23-->
<!--T:23-->
<div class="mw-collapsible mw-collapsed toccolours">
<div class="mw-collapsible mw-collapsed toccolours">
==== The complete blank page macro ====
This macro shows the principle how an svg-file can be put together. (Format is A3)

<!--T:71-->
This macro consists of the above code segments, ready to run.
<div class="mw-collapsible-content">
<div class="mw-collapsible-content">


Line 129: Line 259:
#! python
#! python
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL
# (c) 2024 Your name LGPL
#
#
#- Get the path to the template folder that is set in the FreeCAD parameters
parameter_path = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/Files")
template_path = parameter_path.GetString("TemplateDir")
template_name = "MyTemplate.svg"
#- Link template_path and template_name for any OS
template_file = os.path.join(template_path, template_name)


import os # to enable the use of os.path.join()
# - SVG creation -



#- Create a file and insert a header line
# - SVG creation -
# (with t as the space saving variant of template)
def CreateSvgFile(filePath):
def createSvgFile(file_path):
# Create a file and insert a header line (with t as the space saving variant of template)
t=open(filePath,"w") # w = write, overwrites existing files
t = open(file_path, "w") # w = write, overwrites existing files
t.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
t.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
t.close
t.close


def startSvg(file_path, sheet_width, sheet_height):
#- Create opening svg-tag
# Create svg-tag including namespace and format definitions
# Namespace section
t = open(file_path, "a", encoding="utf-8")
def StartSvg(filePath):
t=open(filePath,"a") # a = append, new lines are added at the end of an existing file
# a = append, new lines are added at the end of an existing file
# encoding="utf-8", helps with special characters if the Python interpreter is in ASCII mode
t.write("\n"+"\n")
t.write("\n" + "\n")
t.write("<svg\n")
t.write("<svg\n")
#- Namespace declarations
t.write(" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"\n")
t.write(" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"\n")
t.write(" xmlns:freecad=\"http://www.freecadweb.org/wiki/index.php?title=Svg_Namespace\"\n")
t.write(" xmlns:freecad=\"http://www.freecad.org/wiki/index.php?title=Svg_Namespace\"\n")
#- Format definition
t.write(" width =\"" + sheet_width + "mm\"\n")
t.write(" height=\"" + sheet_height + "mm\"\n")
t.write(" viewBox=\"0 0 " + sheet_width + " " + sheet_height + "\">\n")
# identical values for width and height and Viewbox' width and height
# will synchronise mm and svg-units
t.close
t.close
# Sheet size section
def CreateSheet(filePath,shWidth,shHeight):
t=open(filePath,"a")
t.write(" width =\""+shWidth+"mm\"\n")
t.write(" height=\""+shHeight+"mm\"\n")
t.write(" viewBox=\"0 0 "+shWidth+" "+shHeight+"\">\n")
t.close
# identical values for width and height and Viewbox' width and height will synchronise mm and svg-units


def endSvg(file_path):
#- Create closing svg-tag
# Create closing svg-tag
def EndSvg(filePath):
t=open(filePath,"a")
t = open(file_path, "a", encoding="utf-8")
t.write("</svg>")
t.write("</svg>")
t.close
t.close


def pathToTemplate(template_name):
# --- Main section ---
"""Link a given template name to the path of the template folder"""
#- Get the path to the template folder that is set in the FreeCAD parameters
parameter_path = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/Files")
template_path = parameter_path.GetString("TemplateDir")
#- Link template_path and template_name for any OS
path_to_file = os.path.join(template_path, template_name) # to join path segments OS neutral
return path_to_file


def main():
CreateSvgFile(template_file) # overwrites existing File
"""This one creates an empty A3 template"""
#- Set the name of the template file and get its location
template_file = pathToTemplate("MyTemplate.svg") # Change the template name here
#- Here starts the compiling of the svg file
createSvgFile(template_file) # overwrites existing File
#- Set sheet format (DIN A3)
format_width = "420"
format_height = "297"
startSvg(template_file, format_width, format_height) # adds svg start tag
endSvg(template_file) # adds svg end tag
# At this point a new SVG-file is generated and saved
return


if __name__ == '__main__':
# Set sheet format (DIN A3)
# This will be true only if the file is "executed"
formatWidth = "420"
# but not if imported as module
formatHeight = "297"
main()

StartSvg(template_file) # adds Start tag and namespaces
CreateSheet(template_file, formatWidth, formatHeight)
EndSvg(template_file)
# At this point a new SVG-file is generated and saved
}}
}}
<translate>
<translate>
Line 191: Line 329:
</div> <!-- End of collapsible element for ... section. Do not remove! -->
</div> <!-- End of collapsible element for ... section. Do not remove! -->


<!--T:25-->
: The main principle is:
:* to open a file for writing and so start an svg-file from scratch, write the header line and close the file as a first step.
:* and then to repeatedly open the file to append further segments and then close it again after appending.
:
:The macro is made of several functions which are called from the main section.
:Additional functions could be inserted before the EndSvg function and the needed calls are put before the EndSvg() call.
:We need to have an eye on the number of spaces used with the write operations for a correct indentation.


=== ... to create a page with some ink === <!--T:26-->
=== ... to create a page with some ink === <!--T:26-->
Line 204: Line 334:
<!--T:27-->
<!--T:27-->
To make a drawing out of a blank page we need:
To make a drawing out of a blank page we need:
:* Frames i.e. rectangles created with the '''rect''' instruction
* Frames i.e. rectangles created with the '''rect''' instruction.
:* a title block and more made of lines created with the '''path''' instruction
* A title block and more made of lines created with the '''path''' instruction.
:* simple texts for indexes and labeling title block cells
* Simple texts for indexes and labeling title block cells.
:* editable texts like part number or part name
* Editable texts like part number or part name.


<!--T:28-->
<!--T:28-->
Normally these graphical elements are used repeatedly and so the generating code is put into functions.
Normally these graphical elements are used repeatedly and so the generating code is put into four functions:
* svgRect() for rectangular frame elements.
* svgPath() for straight line elements.
* svgText() for static texts.
* ediText() for editable texts.


=== svgrect function === <!--T:29-->
<!--T:72-->
All functions are placed inside the above macro before the main() function and their related function calls are inserted in the main() function between {{Incode|startSvg(...)}} and {{Incode|endSvg(...)}}.

==== svgRect() ==== <!--T:29-->


<!--T:30-->
<!--T:30-->
To draw a rectangle we just need to call the '''svgrect''' function and hand over the values for width, height, and position of the upper left corner. That is better readable than the content of the svgline which had to be used instead.
To draw a rectangle we just need to call the '''svgRect''' function and hand over the values for width, height, and position of the upper left corner. That is better readable in this place than if svg_line would contain the whole svg code line.


</translate>
</translate>
{{Code| |code=
{{Code| |code=
def svgRect(width, height, x, y):
#- Function to generate an svg-instruction to draw a rectangle with the given values
# Generates an svg-instruction to draw a rectangle with the given values
def svgrect(width,height,x,y):
svg_line = (
svgLine=("<rect width=\""+width+"\" height=\""+height+"\" x=\""+x+"\" y=\""+y+"\" />")
"<rect width=\"" + width + "\" height=\"" + height + "\" x=\"" + x
return svgLine
+ "\" y=\"" + y + "\" />"
)
return svg_line
}}
}}
<translate>
<translate>


=== svgpath function === <!--T:31-->
<!--T:73-->
The svg instruction is split to stay within the recommended Python line length, it will result in a single svg line nevertheless.

==== svgPath() ==== <!--T:31-->


<!--T:32-->
<!--T:32-->
To draw a line we just need to call the '''svgpath''' function and hand over the coordinates of the start point and the endpoint of a line.
To draw a line we just need to call the '''svgPath''' function and hand over the coordinates of the start point and the endpoint of a line.


<!--T:33-->
<!--T:33-->
Instead of the end point coordinates we can hand over a tag to draw a horizontal (h) or vertical (v) line followed by the length of the line or the tag to draw a horizontal (H) or vertical (V) line followed by the x or y ordinate of the end point.
Instead of the end point coordinates we can hand over a tag to draw a horizontal (h) or vertical (v) line followed by the length of the line or the tag to draw a horizontal (H) or vertical (V) line followed by the x or y ordinate of the end point.


<!--T:34-->
<!--T:34-->
Line 239: Line 382:
</translate>
</translate>
{{Code| |code=
{{Code| |code=
def svgPath(x1, y1, x2, y2):
#- Function to generate an svg-instruction to draw a path element (line) with the given values
# Generates an svg-instruction to draw a path element (line) with the given values
def svgpath(x1,y1,x2,y2):
if x2=="v" or x2=="V" or x2=="h" or x2=="H":
if x2 == "v" or x2 == "V" or x2 == "h" or x2 == "H":
svgLine=("<path d=\"m "+x1+","+y1+" "+x2+" "+y2+"\" />")
svg_line = ("<path d=\"m " + x1 + "," + y1 + " " + x2 + " " + y2 + "\" />")
else:
else:
svgLine=("<path d=\"m "+x1+","+y1+" l "+x2+","+y2+"\" />")
svg_line = ("<path d=\"m " + x1 + "," + y1 + " l " + x2 + "," + y2 + "\" />")
return svgLine
return svg_line
}}
}}
<translate>
<translate>


=== svgtext function === <!--T:35-->
==== svgText() ==== <!--T:35-->


<!--T:36-->
<!--T:36-->
To draw a piece of text we just need to call the '''svgtext''' function and hand over the coordinates of the text's anchor point and the text string itself.
To draw a piece of text we just need to call the '''svgText''' function and hand over the coordinates of the text's anchor point, the text string itself, and optionally an angle for rotated text. To rotate texts a transformation instruction has to be inserted in each text tag separately; the rotation centers are set to the same coordinates as the related text anchor points.


<!--T:37-->
<!--T:37-->
(The text alignment is controlled by the surrounding group tag or left-aligned as default).
(The text alignment is controlled by the surrounding group tag or left-aligned as default).


</translate>
</translate>
{{Code| |code=
{{Code| |code=
def svgText(x, y, str_value, str_angle="0"):
#- Function to generate an svg-instruction to place a text element with the given values
"""
def svgtext(posX,posY,strValue):
Generates an svg-instruction to place a text element with the given values.
svgLine=("<text x=\""+posX+"\" y=\""+posY+"\">"+strValue+"</text>")
Optional str_angle enables vertical and arbitrarily rotated texts
return svgLine
"""
if str_angle == "0":
svg_line = ("<text x=\"" + x + "\" y=\"" + y + "\">" + str_value + "</text>")
else:
svg_line = (
"<text x=\"" + x + "\" y=\"" + y + "\" transform=\"rotate(" + str_angle
+ "," + x + "," + y + ")\">" + str_value + "</text>"
)
return svg_line
}}
}}
<translate>
<translate>


=== FCeditext function === <!--T:38-->
==== ediText() ==== <!--T:38-->


<!--T:39-->
<!--T:39-->
To draw a piece of editable text we just need to call the '''FCeditext''' function and hand over a name, the coordinates of the text's anchor point, and the text string itself.
To draw a piece of editable text we just need to call the '''ediText''' function and hand over a name, the coordinates of the text's anchor point, the text string itself, and optionally an angle for rotated text. To rotate texts a transformation instruction has to be inserted in each text tag separately; the rotation centers are set to the same coordinates as the related text anchor points.


<!--T:40-->
<!--T:40-->
Line 279: Line 431:
</translate>
</translate>
{{Code| |code=
{{Code| |code=
def ediText(entry_name, x, y, str_value, str_angle="0"):
#- Function to generate an svg-instruction to place an editable text element with the given values
"""
def FCeditext(entryName,posX,posY,strValue):
Generates an svg-instruction to place an editable text element with the given values.
svgLine=("<text freecad:editable=\""+entryName+"\" x=\""+posX+"\" y=\""+posY \
Optional str_angle enables vertical and arbitrarily rotated editable texts
+"\"> <tspan>"+strValue+"</tspan> </text>")
return svgLine
"""
if str_angle == "0":
svg_line = (
"<text freecad:editable=\"" + entry_name + "\" x=\"" + x + "\" y=\"" + y
+ "\"> <tspan>" + str_value + "</tspan> </text>"
)
else:
svg_line = (
"<text freecad:editable=\"" + entry_name + "\" x=\"" + x + "\" y=\"" + y
+ "\" transform=\"rotate(" + str_angle + "," + x + "," + y + ")\"> <tspan>"
+ str_value + "</tspan> </text>"
)
return svg_line
}}
}}
<translate>
<translate>


=== CreateXxxx functions === <!--T:42-->
=== createXxxx functions === <!--T:42-->


<!--T:43-->
<!--T:43-->
Line 293: Line 457:


<!--T:44-->
<!--T:44-->
Then follows a section to set and calculate values and with write instructions that call the above functions to generate svg-code.
Then follows a section to set and calculate values, and with write instructions that call the above functions to generate svg-code.


<!--T:45-->
<!--T:45-->
Line 300: Line 464:
</translate>
</translate>
{{Code| |code=
{{Code| |code=
def createFrame(file_path, sheet_width, sheet_height):
#- Frame creation
# Creates rectangles for sheet frame and drawing area
def CreateFrame(filePath,shWidth,shHeight):
t=open(filePath,"a")
t = open(file_path, "a", encoding="utf-8")
t.write(" <g id=\"drawing-frame\"\n")
t.write(" <g id=\"drawing-frame\"\n")
t.write(" style=\"fill:none;stroke:#000000;stroke-width:0.5;\
t.write(" style=\"fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round\">\n")
#- upper left corner of inner Frame, drawing area
stroke-linecap:round\">\n")
frame_x = str(20)
# inner Frame, drawing area
#- upper left corner
frame_y = str(10)
drawingX=str(20)
drawingY=str(10)
#- frame dimensions
#- frame dimensions
drawingWidth=str(int(shWidth)-20-10)
frame_width = str(int(sheet_width) - 20 - 10)
drawingHeight=str(int(shHeight)-10-10)
frame_height = str(int(sheet_height) - 10 - 10)
#- frame rectangle
#- frame rectangle
t.write(" "+svgrect(drawingWidth,drawingHeight,drawingX,drawingY)+"\n")
t.write(" " + svgRect(frame_width, frame_height, frame_x, frame_y) + "\n")
# outer frame
#- upper left corner outer frame, sheet frame
#- upper left corner
frame_x = str(15)
drawingX=str(15)
frame_y = str(5)
drawingY=str(5)
#- frame dimensions
#- frame dimensions
drawingWidth=str(int(shWidth)-20)
frame_width = str(int(sheet_width)-20)
drawingHeight=str(int(shHeight)-10)
frame_height = str(int(sheet_height)-10)
#- frame rectangle
#- frame rectangle
t.write(" "+svgrect(drawingWidth,drawingHeight,drawingX,drawingY)+"\n")
t.write(" " + svgRect(frame_width, frame_height, frame_x, frame_y) + "\n")
t.write(" </g>\n\n")
t.write(" </g>\n\n")
t.close
t.close
Line 340: Line 501:
#! python
#! python
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL
# (c) 2024 Your name LGPL
#
#
#- Get the path to the template folder that is set in the FreeCAD parameters
parameter_path = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/Files")
template_path = parameter_path.GetString("TemplateDir")
template_name = "MyTemplate.svg"
#- Link template_path and template_name for any OS
template_file = os.path.join(template_path, template_name)


import os # to enable the use of os.path.join()
# - SVG creation -



#- Create a file and insert a header line
# - SVG creation -
# (with t as the space saving variant of template)
def CreateSvgFile(filePath):
def createSvgFile(file_path):
# Create a file and insert a header line (with t as the space saving variant of template)
t=open(filePath,"w") # w = write, overwrites existing files
t = open(file_path, "w") # w = write, overwrites existing files
t.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
t.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
t.close
t.close


def startSvg(file_path, sheet_width, sheet_height):
#- Create opening svg-tag
# Create svg-tag including namespace and format definitions
# Namespace section
t = open(file_path, "a", encoding="utf-8")
def StartSvg(filePath):
t=open(filePath,"a") # a = append, new lines are added at the end of an existing file
# a = append, new lines are added at the end of an existing file
# encoding="utf-8", helps with special characters if the Python interpreter is in ASCII mode
t.write("\n"+"\n")
t.write("\n" + "\n")
t.write("<svg\n")
t.write("<svg\n")
#- Namespace declarations
t.write(" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"\n")
t.write(" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"\n")
t.write(" xmlns:freecad=\"http://www.freecadweb.org/wiki/index.php?title=Svg_Namespace\"\n")
t.write(" xmlns:freecad=\"http://www.freecad.org/wiki/index.php?title=Svg_Namespace\"\n")
#- Format definition
t.write(" width =\"" + sheet_width + "mm\"\n")
t.write(" height=\"" + sheet_height + "mm\"\n")
t.write(" viewBox=\"0 0 " + sheet_width + " " + sheet_height + "\">\n")
# identical values for width and height and Viewbox' width and height
# will synchronise mm and svg-units
t.close
t.close
# Sheet size section
def CreateSheet(filePath,shWidth,shHeight):
t=open(filePath,"a")
t.write(" width =\""+shWidth+"mm\"\n")
t.write(" height=\""+shHeight+"mm\"\n")
t.write(" viewBox=\"0 0 "+shWidth+" "+shHeight+"\">\n")
t.close
# identical values for width and height and Viewbox' width and height will synchronise mm and svg-units


def endSvg(file_path):
#- Create closing svg-tag
# Create closing svg-tag
def EndSvg(filePath):
t=open(filePath,"a")
t = open(file_path, "a", encoding="utf-8")
t.write("</svg>")
t.write("</svg>")
t.close
t.close


def svgRect(width, height, x, y):
#- Function to generate an svg-instruction to draw a rectangle with the given values
# Ggenerates an svg-instruction to draw a rectangle with the given values
def svgrect(width,height,x,y):
svg_line = (
svgLine=("<rect width=\""+width+"\" height=\""+height+"\" x=\""+x+"\" y=\""+y+"\" />")
"<rect width=\"" + width + "\" height=\"" + height + "\" x=\"" + x
return svgLine
+ "\" y=\"" + y + "\" />"
)
return svg_line


def svgPath(x1, y1, x2, y2):
#- Function to generate an svg-instruction to draw a path element (line) with the given values
# Generates an svg-instruction to draw a path element (line) with the given values
def svgpath(x1,y1,x2,y2):
if x2=="v" or x2=="V" or x2=="h" or x2=="H":
if x2 == "v" or x2 == "V" or x2 == "h" or x2 == "H":
svgLine=("<path d=\"m "+x1+","+y1+" "+x2+" "+y2+"\" />")
svg_line = ("<path d=\"m " + x1 + "," + y1 + " " + x2 + " " + y2 + "\" />")
else:
else:
svgLine=("<path d=\"m "+x1+","+y1+" l "+x2+","+y2+"\" />")
svg_line = ("<path d=\"m " + x1 + "," + y1 + " l " + x2 + "," + y2 + "\" />")
return svgLine
return svg_line


def svgText(x, y, str_value, str_angle="0"):
#- Function to generate an svg-instruction to place a text element with the given values
"""
def svgtext(posX,posY,strValue):
Generates an svg-instruction to place a text element with the given values.
svgLine=("<text x=\""+posX+"\" y=\""+posY+"\">"+strValue+"</text>")
Optional str_angle enables vertical and arbitrarily rotated texts
return svgLine
"""
if str_angle == "0":
svg_line = ("<text x=\"" + x + "\" y=\"" + y + "\">" + str_value + "</text>")
else:
svg_line = (
"<text x=\"" + x + "\" y=\"" + y + "\" transform=\"rotate(" + str_angle
+ "," + x + "," + y + ")\">" + str_value + "</text>"
)
return svg_line


def ediText(entry_name, x, y, str_value, str_angle="0"):
#- Function to generate an svg-instruction to place an editable text element with the given values
"""
def FCeditext(entryName,posX,posY,strValue):
Generates an svg-instruction to place an editable text element with the given values.
svgLine=("<text freecad:editable=\""+entryName+"\" x=\""+posX+"\" y=\""+posY \
Optional str_angle enables vertical and arbitrarily rotated editable texts
+"\"> <tspan>"+strValue+"</tspan> </text>")
return svgLine
"""
if str_angle == "0":
svg_line = (
"<text freecad:editable=\"" + entry_name + "\" x=\"" + x + "\" y=\"" + y
+ "\"> <tspan>" + str_value + "</tspan> </text>"
)
else:
svg_line = (
"<text freecad:editable=\"" + entry_name + "\" x=\"" + x + "\" y=\"" + y
+ "\" transform=\"rotate(" + str_angle + "," + x + "," + y + ")\"> <tspan>"
+ str_value + "</tspan> </text>"
)
return svg_line


def createFrame(file_path, sheet_width, sheet_height):
#- Frame creation
# Creates rectangles for sheet frame and drawing area
def CreateFrame(filePath,shWidth,shHeight):
t=open(filePath,"a")
t = open(file_path, "a", encoding="utf-8")
t.write(" <g id=\"drawing-frame\"\n")
t.write(" <g id=\"drawing-frame\"\n")
t.write(" style=\"fill:none;stroke:#000000;stroke-width:0.5;\
t.write(" style=\"fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round\">\n")
#- calculate upper left corner of inner Frame, drawing area
stroke-linecap:round\">\n")
frame_x = str(20)
# inner Frame, drawing area
#- upper left corner
frame_y = str(10)
drawingX=str(20)
drawingY=str(10)
#- frame dimensions
#- frame dimensions
drawingWidth=str(int(shWidth)-20-10)
frame_width = str(int(sheet_width) - 20 - 10)
drawingHeight=str(int(shHeight)-10-10)
frame_height = str(int(sheet_height) - 10 - 10)
#- frame rectangle
#- frame rectangle
t.write(" "+svgrect(drawingWidth,drawingHeight,drawingX,drawingY)+"\n")
t.write(" " + svgRect(frame_width, frame_height, frame_x, frame_y) + "\n")
# outer frame
#- calculate upper left corner outer frame, sheet frame
#- upper left corner
frame_x = str(15)
drawingX=str(15)
frame_y = str(5)
drawingY=str(5)
#- frame dimensions
#- frame dimensions
drawingWidth=str(int(shWidth)-20)
frame_width = str(int(sheet_width)-20)
drawingHeight=str(int(shHeight)-10)
frame_height = str(int(sheet_height)-10)
#- frame rectangle
#- frame rectangle
t.write(" "+svgrect(drawingWidth,drawingHeight,drawingX,drawingY)+"\n")
t.write(" " + svgRect(frame_width, frame_height, frame_x, frame_y) + "\n")
t.write(" </g>\n\n")
t.write(" </g>\n\n")
t.close
t.close


def createTitleBlock(file_path, sheet_width, sheet_height):
#- Title block movable
"""Creates a title block and transfers it to the position according to the sheet dimensions"""
def CreateTitleBlock(filePath,shWidth,shHeight):
#- calculate title block origin (lower left corner), offset from page origin

tbX = str(int(sheet_width) - 10 - 180) # 180 according to DIN EN ISO 7200
#- lower left corner
tbX=str(int(shWidth)-10-180) # 180 according to DIN EN ISO 7200
tbY = str(int(sheet_height) - 10)
t = open(file_path, "a", encoding="utf-8")
tbY=str(int(shHeight)-10)
#- group to transform all elements at once
#- group to transfer all included title block elements at once
t=open(filePath,"a")
t.write(" <g id=\"titleblock\"\n")
t.write(" <g id=\"titleblock\"\n")
t.write(" transform=\"translate("+tbX+","+tbY+")\">\n")
t.write(" transform=\"translate(" + tbX + "," + tbY + ")\">\n")
#- title block
#- sub-group of title block line framework
t.write(" <g id=\"titleblock-frame\"\n")
t.write(" <g id=\"titleblock-frame\"\n")
t.write(" style=\"fill:none;stroke:#000000;stroke-width:0.35;\
t.write(" style=\"fill:none;stroke:#000000;stroke-width:0.35;\
stroke-linecap:miter;stroke-miterlimit:4\">\n")
stroke-linecap:miter;stroke-miterlimit:4\">\n")
t.write(" "+svgpath(" 0"," 0"," 0","-63")+"\n")
t.write(" " + svgPath(" 0"," 0"," 0","-63") + "\n")
t.write(" "+svgpath(" 0","-63","180"," 0")+"\n")
t.write(" " + svgPath(" 0","-63","180"," 0") + "\n")
t.write(" "+svgpath(" 0","-30","h","155")+"\n")
t.write(" " + svgPath(" 0","-30","h","155") + "\n")
t.write(" "+svgpath("155"," 0","v","-63")+"\n")
t.write(" " + svgPath("155"," 0","v","-63") + "\n")
t.write(" </g>\n")
t.write(" </g>\n")
#- small texts, left-aligned
#- sub-group of title block static texts (left-aligned by default)
t.write(" <g id=\"titleblock-text-non-editable\"\n")
t.write(" <g id=\"titleblock-text-non-editable\"\n")
t.write(" style=\"font-size:5.0;text-anchor:start;fill:#000000;\
t.write(" style=\"font-size:5.0;text-anchor:start;fill:#000000;\
font-family:osifont\">\n")
font-family:osifont\">\n")
t.write(" "+svgtext(" 4.5","-43.5 ","Some static text")+"\n")
t.write(" " + svgText(" 4.5","-43.5 ","Some static text") + "\n")
t.write(" "+svgtext(" 4.5","-13.5 ","More static text")+"\n")
t.write(" " + svgText(" 4.5","-13.5 ","More static text") + "\n")
t.write(" " + svgText("162.5","-3.5 ","Vertical static text","-90") + "\n")
t.write(" </g>\n")
t.write(" </g>\n")

t.write(" </g>\n\n")
t.write(" </g>\n\n")
t.close
t.close


def createEditableText(file_path, sheet_width, sheet_height):
#- Title block editable texts
"""Creates editable texts positioned according to the page origin"""
def CreateEditableText(filePath,shWidth,shHeight):
#- calculate offset to titleblock origin

edX = int(sheet_width) - 10 - 180 # 180 according to DIN EN ISO 7200
#- offsets for editable texts
edY = int(sheet_height) - 10
edX=int(shWidth)-10-180 # 180 according to DIN EN ISO 7200
t = open(file_path, "a", encoding="utf-8")
edY=int(shHeight)-10
#- group editable texts using the same attributes

t=open(filePath,"a")
t.write(" <g id=\"titleblock-editable-texts\"\n")
t.write(" <g id=\"titleblock-editable-texts\"\n")
t.write(" style=\"font-size:7.0;text-anchor:start;fill:#0000d0;\
t.write(" style=\"font-size:7.0;text-anchor:start;fill:#0000d0;\
font-family:osifont\">\n")
font-family:osifont\">\n")
t.write(
t.write(" "+FCeditext("EdiText-1",str(edX+60),str(edY-43.5),"Some editable text")+"\n")
t.write(" "+FCeditext("EdiText-2",str(edX+60),str(edY-13.5),"More editable text")+"\n")
" " + ediText("EdiText-1",str(edX + 60),str(edY - 43.5),"Some editable text") + "\n"
)
t.write(
" " + ediText("EdiText-2",str(edX + 60),str(edY - 13.5),"More editable text") + "\n"
)
t.write(
" " + ediText("EdiText-3",str(edX + 173),str(edY - 4.5),"90° editable text","-90")
+ "\n"
)
t.write(" </g>\n\n")
t.write(" </g>\n\n")
t.close
t.close


def pathToTemplate(template_name):
# --- Main section ---
"""Link a given template name to the path of the template folder"""

#- Get the path to the template folder that is set in the FreeCAD parameters
CreateSvgFile(template_file) # overwrites existing File
parameter_path = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/Files")
template_path = parameter_path.GetString("TemplateDir")
#- Link template_path and template_name for any OS
path_to_file = os.path.join(template_path, template_name) # to join path segments OS neutral
return path_to_file


def main():
# Set sheet format (A3)
"""This one creates an A3 template with simple frame and title block"""
formatWidth = "420"
#- Set the name of the template file and get its location
formatHeight = "297"
template_file = pathToTemplate("MyTemplate.svg") # Change the template name here
#- Here starts the compiling of the svg file
createSvgFile(template_file) # overwrites existing File
#- Set sheet format (DIN A3)
format_width = "420"
format_height = "297"
startSvg(template_file, format_width, format_height) # adds svg start tag
createFrame(template_file, format_width, format_height)
createTitleBlock(template_file, format_width, format_height)
createEditableText(template_file, format_width, format_height)
endSvg(template_file) # adds svg end tag
# At this point a new SVG-file is generated and saved
return


if __name__ == '__main__':
StartSvg(template_file) # adds Start tag and namespaces
# This will be true only if the file is "executed"
CreateSheet(template_file, formatWidth, formatHeight)
# but not if imported as module
CreateFrame(template_file, formatWidth, formatHeight)
main()
CreateTitleBlock(template_file, formatWidth, formatHeight)
CreateEditableText(template_file, formatWidth, formatHeight)
EndSvg(template_file)
# At this point a new SVG-file is generated and saved
}}
}}
<translate>
<translate>
Line 507: Line 703:
<svg
<svg
xmlns="http://www.w3.org/2000/svg" version="1.1"
xmlns="http://www.w3.org/2000/svg" version="1.1"
xmlns:freecad="http://www.freecadweb.org/wiki/index.php?title=Svg_Namespace"
xmlns:freecad="http://www.freecad.org/wiki/index.php?title=Svg_Namespace"
width ="420mm"
width ="420mm"
height="297mm"
height="297mm"
Line 530: Line 726:
<text x=" 4.5" y="-43.5 ">Some static text</text>
<text x=" 4.5" y="-43.5 ">Some static text</text>
<text x=" 4.5" y="-13.5 ">More static text</text>
<text x=" 4.5" y="-13.5 ">More static text</text>
<text x="162.5" y="-3.5 " transform="rotate(-90,162.5,-3.5 )">Vertical static text</text>
</g>
</g>
</g>
</g>
Line 537: Line 734:
<text freecad:editable="EdiText-1" x="290" y="243.5"> <tspan>Some editable text</tspan> </text>
<text freecad:editable="EdiText-1" x="290" y="243.5"> <tspan>Some editable text</tspan> </text>
<text freecad:editable="EdiText-2" x="290" y="273.5"> <tspan>More editable text</tspan> </text>
<text freecad:editable="EdiText-2" x="290" y="273.5"> <tspan>More editable text</tspan> </text>
<text freecad:editable="EdiText-3" x="403" y="282.5" transform="rotate(-90,403,282.5)"> <tspan>90° editable text</tspan> </text>
</g>
</g>


Line 551: Line 749:


</translate>
</translate>
[[Image:TechDraw TemplateGenerator.png|TechDraw TemplateGenerator.png]]
[[Image:TechDraw TemplateGenerator.png|600px]]
<translate>

<!--T:74-->
To color the editable texts in blue is just a personal choice to easier distinguish static and editable texts on the finished drawing.

</translate>

Latest revision as of 11:04, 1 February 2024

Other languages:
Tutorial
Topic
Create a TechDraw template using a Python macro
Level
Basic skills of Python and svg-structures are helpful
Time to complete
(I don't know yet)
Authors
FBXL5
FreeCAD version
0.21 and later
Example files
None
See also
Macro TemplateHelper

Introduction

This tutorial describes how to generate a simple template to use with the TechDraw workbench out of some lines of Python code.

Any text editor can be used to code. My choice is Atom, but FreeCAD's built-in editor works well, too.

The following code examples can be copied and pasted into an empty text file and then saved under a name of your choice as a *.py or *.FCMacro file.

A template provides a background for the drawing tasks and its dimensions are used by the printer drivers to scale the drawing correctly.

The templates are svg-files and so a macro has to compose some lines of svg code (which is a subset of xml code).

Note: When FreeCAD was migrated from freecadweb.org to freecad.org this page was updated accordingly and the resulting SVG code is no longer compatible with FreeCAD versions older than v0.21. For those versions you need to manually change freecad.org to freecadweb.org on the namespace declaration line in the resulting SVG code, otherwise the editable texts are not recognized.

Structure of a simple blank page

The SVG format is a subset of the XML format. That is why an SVG file, like any XML file, consists of two parts:

  • Head, a format declaration
  • Body, containing the information what to show and where to place it
(I don't know why there should be a headline, the svg-file is still a valid template file without it...)

Head

The head is just one line to declare which version of the XML language an interpreter should use to handle the instructions in the body.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

Body

The Body starts with an opening tag which contains information about name spaces and about the size of the template and where to place it. And it finishes with a closing tag.

<svg
  xmlns="http://www.w3.org/2000/svg" version="1.1"
  xmlns:freecad="http://www.freecad.org/wiki/index.php?title=Svg_Namespace"
  width="420mm"
  height="297mm"
  viewBox="0 0 420 297">
</svg>
xmlns="http://www.w3.org/2000/svg": External link to the xml name space to look up standard xml commands
version="1.1": Used xml version is 1.1
xmlns:freecad="...=Svg_Namespace": External link to FreeCAD's Svg Namespace wiki page. The link is not used to retrieve information or values at runtime, but it is the key to activate the custom attributes, e.g. those for editable texts.
"freecad:" will be prefixed to said custom attribute names.
width="420mm": Width of the drawing area
height="297mm": Height of the drawing area
viewBox="0 0 420 297": Position of the upper left corner (0;0) and the lower right corner (420;297) in the svg construction space (in svg units).
Width, height, and viewBox in this combination set 1 svg-unit to 1 mm for the whole document. A dimensional unit can be omitted from now on.
In this case 420 and 297 give an A3 page. Customise these values to generate other page sizes.

For a blank page size DIN A3 in landscape orientation that's all.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
  xmlns="http://www.w3.org/2000/svg" version="1.1"
  xmlns:freecad="http://www.freecad.org/wiki/index.php?title=Svg_Namespace"
  width="420mm"
  height="297mm"
  viewBox="0 0 420 297">
</svg>

Python code...

Coding starts with a framework containing a separate main function:

#! python
# -*- coding: utf-8 -*-
# (c) 2024 Your name LGPL


def main():
    """Here is where the magic happens"""
    return

if __name__ == '__main__':
    # This will be true only if the file is "executed"
    # but not if imported as module
    main()

... to create a blank page

The process of creating a template consists of:

  • Locate the template folder.
  • Open a file for writing to create an svg-file from scratch, write the header line and close the file as a first step.
  • And then repeatedly open the file to append further segments, closing it again each time.

The macro is made of several functions which are called from the main section.
Additional functions could be inserted before the EndSvg function and the needed calls are put before the EndSvg() call.
We need to have an eye on the number of spaces used with the write operations for a correct indentation.

pathToTemplate()

Before any code is generated a folder is needed to store the new template file, and a file name has to be set.

The user should have selected a template folder. Its path is then stored in the TechDraw preferences.
It is not necessary to know where the preferences are store, because FreeCAD has commands to address needed parameters directly.

def pathToTemplate(template_name):
    """Link a given template name to the path of the template folder"""
    #- Get the path to the template folder that is set in the FreeCAD parameters
    parameter_path = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/Files")
    template_path = parameter_path.GetString("TemplateDir")
    #- Link template_path and template_name for any OS
    path_to_file = os.path.join(template_path, template_name)  # to join path segments OS neutral
    return path_to_file

parameter_path receives the path to the "folder" within the configuration file where "TemplateDir" parameter can be found.
template_path receives the content of "TemplateDir" which is the path to the template directory.
template_name recieves the name of the template to be created.

Now the template name needs to be linked to the template path in a way that is compatible to unix based OSs and Windows.
This is done with the "os.path.join" command and stored into the template_file. To enable this command an "import os" instruction is required.

createSvgFile()

This creates a new Template file and saves the xml head line.

def createSvgFile(file_path):
    # Create a file and insert a header line (with t as the space saving variant of template)
    t = open(file_path, "w")  # w = write, overwrites existing files
    t.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
    t.close

startSvg()

This appends the template file and creates the svg opening tag including its attributes.
Each write instruction contains one single line of svg code ending with "\n", the marker for CR/LF.

def startSvg(file_path, sheet_width, sheet_height):
    # Create svg-tag including namespace and format definitions
    t = open(file_path, "a", encoding="utf-8")
    # a = append, new lines are added at the end of an existing file
    # encoding="utf-8", helps with special characters if the Python interpreter is in ASCII mode
    t.write("\n" + "\n")
    t.write("<svg\n")
    #- Namespace declarations
    t.write("  xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"\n")
    t.write("  xmlns:freecad=\"http://www.freecad.org/wiki/index.php?title=Svg_Namespace\"\n")
    #- Format definition
    t.write("  width =\"" + sheet_width + "mm\"\n")
    t.write("  height=\"" + sheet_height + "mm\"\n")
    t.write("  viewBox=\"0 0 " + sheet_width + " " + sheet_height + "\">\n")
    # identical values for width and height and Viewbox' width and height
    # will synchronise mm and svg-units
    t.close

endSvg()

This further appends the template file and creates the svg closing tag; This eventually finishes the template code.

def endSvg(file_path):
    # Create closing svg-tag
    t = open(file_path, "a", encoding="utf-8")
    t.write("</svg>")
    t.close

main()

The main() function calls the functions and hands over some parameters.

def main():
    """This one creates an empty A3 template"""
    #- Set the name of the template file and get its location
    template_file = pathToTemplate("MyTemplate.svg")  # Change the template name here
    #- Here starts the compiling of the svg file
    createSvgFile(template_file)  # overwrites existing File
    #- Set sheet format (DIN A3)
    format_width  = "420"
    format_height = "297"
    startSvg(template_file, format_width, format_height)  # adds svg start tag
    endSvg(template_file)  # adds svg end tag
    # At this point a new SVG-file is generated and saved
    return

In this example format_width and format_height are hard coded dimensions, both unnecessary lines mark the points where other means of retrieving format data could put their content.

The complete blank page macro

This macro consists of the above code segments, ready to run.

#! python
# -*- coding: utf-8 -*-
# (c) 2024 Your name LGPL

import os  # to enable the use of os.path.join()


# - SVG creation -
def createSvgFile(file_path):
    # Create a file and insert a header line (with t as the space saving variant of template)
    t = open(file_path, "w")  # w = write, overwrites existing files
    t.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
    t.close

def startSvg(file_path, sheet_width, sheet_height):
    # Create svg-tag including namespace and format definitions
    t = open(file_path, "a", encoding="utf-8")
    # a = append, new lines are added at the end of an existing file
    # encoding="utf-8", helps with special characters if the Python interpreter is in ASCII mode
    t.write("\n" + "\n")
    t.write("<svg\n")
    #- Namespace declarations
    t.write("  xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"\n")
    t.write("  xmlns:freecad=\"http://www.freecad.org/wiki/index.php?title=Svg_Namespace\"\n")
    #- Format definition
    t.write("  width =\"" + sheet_width + "mm\"\n")
    t.write("  height=\"" + sheet_height + "mm\"\n")
    t.write("  viewBox=\"0 0 " + sheet_width + " " + sheet_height + "\">\n")
    # identical values for width and height and Viewbox' width and height
    # will synchronise mm and svg-units
    t.close

def endSvg(file_path):
    # Create closing svg-tag
    t = open(file_path, "a", encoding="utf-8")
    t.write("</svg>")
    t.close

def pathToTemplate(template_name):
    """Link a given template name to the path of the template folder"""
    #- Get the path to the template folder that is set in the FreeCAD parameters
    parameter_path = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/Files")
    template_path = parameter_path.GetString("TemplateDir")
    #- Link template_path and template_name for any OS
    path_to_file = os.path.join(template_path, template_name)  # to join path segments OS neutral
    return path_to_file

def main():
    """This one creates an empty A3 template"""
    #- Set the name of the template file and get its location
    template_file = pathToTemplate("MyTemplate.svg")  # Change the template name here
    #- Here starts the compiling of the svg file
    createSvgFile(template_file)  # overwrites existing File
    #- Set sheet format (DIN A3)
    format_width  = "420"
    format_height = "297"
    startSvg(template_file, format_width, format_height)  # adds svg start tag
    endSvg(template_file)  # adds svg end tag
    # At this point a new SVG-file is generated and saved
    return

if __name__ == '__main__':
    # This will be true only if the file is "executed"
    # but not if imported as module
    main()


... to create a page with some ink

To make a drawing out of a blank page we need:

  • Frames i.e. rectangles created with the rect instruction.
  • A title block and more made of lines created with the path instruction.
  • Simple texts for indexes and labeling title block cells.
  • Editable texts like part number or part name.

Normally these graphical elements are used repeatedly and so the generating code is put into four functions:

  • svgRect() for rectangular frame elements.
  • svgPath() for straight line elements.
  • svgText() for static texts.
  • ediText() for editable texts.

All functions are placed inside the above macro before the main() function and their related function calls are inserted in the main() function between startSvg(...) and endSvg(...).

svgRect()

To draw a rectangle we just need to call the svgRect function and hand over the values for width, height, and position of the upper left corner. That is better readable in this place than if svg_line would contain the whole svg code line.

def svgRect(width, height, x, y):
    # Generates an svg-instruction to draw a rectangle with the given values
    svg_line = (
        "<rect width=\"" + width + "\" height=\"" + height + "\" x=\"" + x
        + "\" y=\"" + y + "\" />"
        )
    return svg_line

The svg instruction is split to stay within the recommended Python line length, it will result in a single svg line nevertheless.

svgPath()

To draw a line we just need to call the svgPath function and hand over the coordinates of the start point and the endpoint of a line.

Instead of the end point coordinates we can hand over a tag to draw a horizontal (h) or vertical (v) line followed by the length of the line or the tag to draw a horizontal (H) or vertical (V) line followed by the x or y ordinate of the end point.

Each path starts at the origin and the first action is a movement with "raised pen" (not drawing) to the start point. In this case the relative movement and the absolute movement are the same and so it is irrelevant whether the m-tag is upper or lower case.

def svgPath(x1, y1, x2, y2):
    # Generates an svg-instruction to draw a path element (line) with the given values
    if x2 == "v" or x2 == "V" or x2 == "h" or x2 == "H":
        svg_line = ("<path d=\"m " + x1 + "," + y1 + " " + x2 + " " + y2 + "\" />")
    else:
        svg_line = ("<path d=\"m " + x1 + "," + y1 + " l " + x2 + "," + y2 + "\" />")
    return svg_line

svgText()

To draw a piece of text we just need to call the svgText function and hand over the coordinates of the text's anchor point, the text string itself, and optionally an angle for rotated text. To rotate texts a transformation instruction has to be inserted in each text tag separately; the rotation centers are set to the same coordinates as the related text anchor points.

(The text alignment is controlled by the surrounding group tag or left-aligned as default).

def svgText(x, y, str_value, str_angle="0"):
    """
    Generates an svg-instruction to place a text element with the given values.
    Optional str_angle enables vertical and arbitrarily rotated texts
    """
    if str_angle == "0":
        svg_line = ("<text x=\"" + x + "\" y=\"" + y + "\">" + str_value + "</text>")
    else:
        svg_line = (
            "<text x=\"" + x + "\" y=\"" + y + "\" transform=\"rotate(" + str_angle
            + "," + x + "," + y + ")\">" + str_value + "</text>"
            )
    return svg_line

ediText()

To draw a piece of editable text we just need to call the ediText function and hand over a name, the coordinates of the text's anchor point, the text string itself, and optionally an angle for rotated text. To rotate texts a transformation instruction has to be inserted in each text tag separately; the rotation centers are set to the same coordinates as the related text anchor points.

FreeCAD creates a dictionary object with every inserted template, and each entry has a name (key) and a value.

(The text alignment is controlled by the surrounding group tag or left-aligned as default).

def ediText(entry_name, x, y, str_value, str_angle="0"):
    """
    Generates an svg-instruction to place an editable text element with the given values.
    Optional str_angle enables vertical and arbitrarily rotated editable texts
    """
    if str_angle == "0":
        svg_line = (
            "<text freecad:editable=\"" + entry_name + "\" x=\"" + x + "\" y=\"" + y
            + "\">  <tspan>" + str_value + "</tspan>  </text>"
            )
    else:
        svg_line = (
            "<text freecad:editable=\"" + entry_name + "\" x=\"" + x + "\" y=\"" + y
            + "\" transform=\"rotate(" + str_angle + "," + x + "," + y + ")\">  <tspan>"
            + str_value + "</tspan>  </text>"
            )
    return svg_line

createXxxx functions

These functions start with code to open a file in append mode and to write the group opening tag.

Then follows a section to set and calculate values, and with write instructions that call the above functions to generate svg-code.

And finally the group closing tag followed by an instruction to close the file.

def createFrame(file_path, sheet_width, sheet_height):
    # Creates rectangles for sheet frame and drawing area
    t = open(file_path, "a", encoding="utf-8")
    t.write("    <g id=\"drawing-frame\"\n")
    t.write("      style=\"fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round\">\n")
    #- upper left corner of inner Frame, drawing area
    frame_x = str(20)
    frame_y = str(10)
    #- frame dimensions
    frame_width = str(int(sheet_width) - 20 - 10)
    frame_height = str(int(sheet_height) - 10 - 10)
    #- frame rectangle
    t.write("      " + svgRect(frame_width, frame_height, frame_x, frame_y) + "\n")
    #- upper left corner outer frame, sheet frame
    frame_x = str(15)
    frame_y = str(5)
    #- frame dimensions
    frame_width = str(int(sheet_width)-20)
    frame_height = str(int(sheet_height)-10)
    #- frame rectangle
    t.write("      " + svgRect(frame_width, frame_height, frame_x, frame_y) + "\n")
    t.write("    </g>\n\n")
    t.close

Resulting macro

This macro adds some basic graphical elements needed for proper templates i.e. line elements, texts, and editable texts.

#! python
# -*- coding: utf-8 -*-
# (c) 2024 Your name LGPL

import os  # to enable the use of os.path.join()


# - SVG creation -
def createSvgFile(file_path):
    # Create a file and insert a header line (with t as the space saving variant of template)
    t = open(file_path, "w")  # w = write, overwrites existing files
    t.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
    t.close

def startSvg(file_path, sheet_width, sheet_height):
    # Create svg-tag including namespace and format definitions
    t = open(file_path, "a", encoding="utf-8")
    # a = append, new lines are added at the end of an existing file
    # encoding="utf-8", helps with special characters if the Python interpreter is in ASCII mode
    t.write("\n" + "\n")
    t.write("<svg\n")
    #- Namespace declarations
    t.write("  xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\"\n")
    t.write("  xmlns:freecad=\"http://www.freecad.org/wiki/index.php?title=Svg_Namespace\"\n")
    #- Format definition
    t.write("  width =\"" + sheet_width + "mm\"\n")
    t.write("  height=\"" + sheet_height + "mm\"\n")
    t.write("  viewBox=\"0 0 " + sheet_width + " " + sheet_height + "\">\n")
    # identical values for width and height and Viewbox' width and height
    # will synchronise mm and svg-units
    t.close

def endSvg(file_path):
    # Create closing svg-tag
    t = open(file_path, "a", encoding="utf-8")
    t.write("</svg>")
    t.close

def svgRect(width, height, x, y):
    # Ggenerates an svg-instruction to draw a rectangle with the given values
    svg_line = (
        "<rect width=\"" + width + "\" height=\"" + height + "\" x=\"" + x
        + "\" y=\"" + y + "\" />"
        )
    return svg_line

def svgPath(x1, y1, x2, y2):
    # Generates an svg-instruction to draw a path element (line) with the given values
    if x2 == "v" or x2 == "V" or x2 == "h" or x2 == "H":
        svg_line = ("<path d=\"m " + x1 + "," + y1 + " " + x2 + " " + y2 + "\" />")
    else:
        svg_line = ("<path d=\"m " + x1 + "," + y1 + " l " + x2 + "," + y2 + "\" />")
    return svg_line

def svgText(x, y, str_value, str_angle="0"):
    """
    Generates an svg-instruction to place a text element with the given values.
    Optional str_angle enables vertical and arbitrarily rotated texts
    """
    if str_angle == "0":
        svg_line = ("<text x=\"" + x + "\" y=\"" + y + "\">" + str_value + "</text>")
    else:
        svg_line = (
            "<text x=\"" + x + "\" y=\"" + y + "\" transform=\"rotate(" + str_angle
            + "," + x + "," + y + ")\">" + str_value + "</text>"
            )
    return svg_line

def ediText(entry_name, x, y, str_value, str_angle="0"):
    """
    Generates an svg-instruction to place an editable text element with the given values.
    Optional str_angle enables vertical and arbitrarily rotated editable texts
    """
    if str_angle == "0":
        svg_line = (
            "<text freecad:editable=\"" + entry_name + "\" x=\"" + x + "\" y=\"" + y
            + "\">  <tspan>" + str_value + "</tspan>  </text>"
            )
    else:
        svg_line = (
            "<text freecad:editable=\"" + entry_name + "\" x=\"" + x + "\" y=\"" + y
            + "\" transform=\"rotate(" + str_angle + "," + x + "," + y + ")\">  <tspan>"
            + str_value + "</tspan>  </text>"
            )
    return svg_line

def createFrame(file_path, sheet_width, sheet_height):
    # Creates rectangles for sheet frame and drawing area
    t = open(file_path, "a", encoding="utf-8")
    t.write("    <g id=\"drawing-frame\"\n")
    t.write("      style=\"fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round\">\n")
    #- calculate upper left corner of inner Frame, drawing area
    frame_x = str(20)
    frame_y = str(10)
    #- frame dimensions
    frame_width = str(int(sheet_width) - 20 - 10)
    frame_height = str(int(sheet_height) - 10 - 10)
    #- frame rectangle
    t.write("      " + svgRect(frame_width, frame_height, frame_x, frame_y) + "\n")
    #- calculate upper left corner outer frame, sheet frame
    frame_x = str(15)
    frame_y = str(5)
    #- frame dimensions
    frame_width = str(int(sheet_width)-20)
    frame_height = str(int(sheet_height)-10)
    #- frame rectangle
    t.write("      " + svgRect(frame_width, frame_height, frame_x, frame_y) + "\n")
    t.write("    </g>\n\n")
    t.close

def createTitleBlock(file_path, sheet_width, sheet_height):
    """Creates a title block and transfers it to the position according to the sheet dimensions"""
    #- calculate title block origin (lower left corner), offset from page origin
    tbX = str(int(sheet_width) - 10 - 180)  # 180 according to DIN EN ISO 7200
    tbY = str(int(sheet_height) - 10)
    t = open(file_path, "a", encoding="utf-8")
    #- group to transfer all included title block elements at once
    t.write("    <g id=\"titleblock\"\n")
    t.write("      transform=\"translate(" + tbX + "," + tbY + ")\">\n")
    #- sub-group of title block line framework
    t.write("      <g id=\"titleblock-frame\"\n")
    t.write("        style=\"fill:none;stroke:#000000;stroke-width:0.35;\
stroke-linecap:miter;stroke-miterlimit:4\">\n")
    t.write("        " + svgPath("  0","  0","  0","-63") + "\n")
    t.write("        " + svgPath("  0","-63","180","  0") + "\n")
    t.write("        " + svgPath("  0","-30","h","155") + "\n")
    t.write("        " + svgPath("155","  0","v","-63") + "\n")
    t.write("      </g>\n")
    #- sub-group of title block static texts (left-aligned by default)
    t.write("      <g id=\"titleblock-text-non-editable\"\n")
    t.write("        style=\"font-size:5.0;text-anchor:start;fill:#000000;\
font-family:osifont\">\n")
    t.write("        " + svgText("  4.5","-43.5 ","Some static text") + "\n")
    t.write("        " + svgText("  4.5","-13.5 ","More static text") + "\n")
    t.write("        " + svgText("162.5","-3.5 ","Vertical static text","-90") + "\n")
    t.write("      </g>\n")
    t.write("    </g>\n\n")
    t.close

def createEditableText(file_path, sheet_width, sheet_height):
    """Creates editable texts positioned according to the page origin"""
    #- calculate offset to titleblock origin
    edX = int(sheet_width) - 10 - 180 # 180 according to DIN EN ISO 7200
    edY = int(sheet_height) - 10
    t = open(file_path, "a", encoding="utf-8")
    #- group editable texts using the same attributes
    t.write("    <g id=\"titleblock-editable-texts\"\n")
    t.write("      style=\"font-size:7.0;text-anchor:start;fill:#0000d0;\
font-family:osifont\">\n")
    t.write(
        "      " + ediText("EdiText-1",str(edX + 60),str(edY - 43.5),"Some editable text") + "\n"
        )
    t.write(
        "      " + ediText("EdiText-2",str(edX + 60),str(edY - 13.5),"More editable text") + "\n"
        )
    t.write(
        "      " + ediText("EdiText-3",str(edX + 173),str(edY - 4.5),"90° editable text","-90")
        + "\n"
        )
    t.write("    </g>\n\n")
    t.close

def pathToTemplate(template_name):
    """Link a given template name to the path of the template folder"""
    #- Get the path to the template folder that is set in the FreeCAD parameters
    parameter_path = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/TechDraw/Files")
    template_path = parameter_path.GetString("TemplateDir")
    #- Link template_path and template_name for any OS
    path_to_file = os.path.join(template_path, template_name)  # to join path segments OS neutral
    return path_to_file

def main():
    """This one creates an A3 template with simple frame and title block"""
    #- Set the name of the template file and get its location
    template_file = pathToTemplate("MyTemplate.svg")  # Change the template name here
    #- Here starts the compiling of the svg file
    createSvgFile(template_file)  # overwrites existing File
    #- Set sheet format (DIN A3)
    format_width  = "420"
    format_height = "297"
    startSvg(template_file, format_width, format_height)  # adds svg start tag
    createFrame(template_file, format_width, format_height)
    createTitleBlock(template_file, format_width, format_height)
    createEditableText(template_file, format_width, format_height)
    endSvg(template_file)  # adds svg end tag
    # At this point a new SVG-file is generated and saved
    return

if __name__ == '__main__':
    # This will be true only if the file is "executed"
    # but not if imported as module
    main()

And this is the svg-code coming out of this macro:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<svg
  xmlns="http://www.w3.org/2000/svg" version="1.1"
  xmlns:freecad="http://www.freecad.org/wiki/index.php?title=Svg_Namespace"
  width ="420mm"
  height="297mm"
  viewBox="0 0 420 297">
    <g id="drawing-frame"
      style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round">
      <rect width="390" height="277" x="20" y="10" />
      <rect width="400" height="287" x="15" y="5" />
    </g>

    <g id="titleblock"
      transform="translate(230,287)">
      <g id="titleblock-frame"
        style="fill:none;stroke:#000000;stroke-width:0.35;stroke-linecap:miter;stroke-miterlimit:4">
        <path d="m   0,  0 l   0,-63" />
        <path d="m   0,-63 l 180,  0" />
        <path d="m   0,-30 h 155" />
        <path d="m 155,  0 v -63" />
      </g>
      <g id="titleblock-text-non-editable"
        style="font-size:5.0;text-anchor:start;fill:#000000;font-family:osifont">
        <text x="  4.5" y="-43.5 ">Some static text</text>
        <text x="  4.5" y="-13.5 ">More static text</text>
        <text x="162.5" y="-3.5 " transform="rotate(-90,162.5,-3.5 )">Vertical static text</text>
      </g>
    </g>

    <g id="titleblock-editable-texts"
      style="font-size:7.0;text-anchor:start;fill:#0000d0;font-family:osifont">
      <text freecad:editable="EdiText-1" x="290" y="243.5">  <tspan>Some editable text</tspan>  </text>
      <text freecad:editable="EdiText-2" x="290" y="273.5">  <tspan>More editable text</tspan>  </text>
      <text freecad:editable="EdiText-3" x="403" y="282.5" transform="rotate(-90,403,282.5)">  <tspan>90° editable text</tspan>  </text>
    </g>

</svg>

And what it should look like when inserted (plus magnified title block):

To color the editable texts in blue is just a personal choice to easier distinguish static and editable texts on the finished drawing.