FreeCAD Macro
The model on the main page (a 1 m x 1 m x 20 cm steel sheet with the initials “GM” carved 5 cm deep) was built by this FreeCAD macro, build_gm_sheet.FCMacro. The macro ran inside the FreeCAD GUI while recordmydesktop recorded the screen, so every step appears in the video.
Download:
build_gm_sheet.FCMacroView on GitHub: docs/build_gm_sheet.FCMacro
What it does
Creates a new document and a PartDesign
Body.Opens a sketch on the
XY_Planein the Sketcher workbench and draws a fully constrained 1000 mm x 1000 mm square, one line at a time.Pads the square 200 mm and sets the Body’s material to Steel.
Opens a second sketch on the
XY_Plane, offset 200 mm so it sits on the top face, and draws the outlines of block letters G and M (500 mm tall, 80 mm strokes).Pockets the letters 50 mm deep.
Checks the result (valid solid, 1000 x 1000 x 200 mm bounding box, removed volume equals letter area x 50 mm), saves
gm-eco-libre-volunteer-test.FCStd, and takes the screenshot shown on the main page.
In the GUI it pauses between steps and opens the Pad and Pocket dialogs, so you can watch each step happen. Run headless, it skips the pauses.
How to run it
In the FreeCAD GUI, use Macro -> Macros…, choose the folder containing the macro, select it, and click Execute. You can also pass it on the command line (FreeCAD from Flathub shown):
# in the GUI
flatpak run org.freecad.FreeCAD build_gm_sheet.FCMacro
# headless
flatpak run --command=FreeCADCmd org.freecad.FreeCAD build_gm_sheet.FCMacro
It saves its output next to the macro, or in $GM_OUT_DIR if set. It was tested with FreeCAD 1.1.4.
Note
The lines that set the document’s license metadata (CC BY-SA 4.0) and the default output folder were added after the recording. The committed .FCStd had its license set to match.
Source
1# Eco-Libre Volunteer Test: 1 m x 1 m x 20 cm metal sheet with the initials
2# "GM" carved 5 cm deep using a Sketcher sketch + PartDesign Pocket.
3#
4# Run in the FreeCAD GUI (visible, paced for a screen recording):
5# freecad build_gm_sheet.FCMacro (or Macro -> Macros... -> Execute)
6# or headless:
7# FreeCADCmd build_gm_sheet.FCMacro
8#
9# Optional environment variables:
10# GM_OUT_DIR where to save the .FCStd and screenshot (default: next to this macro)
11# GM_START_TRIGGER wait until this file exists before modelling (to start a screen recorder first)
12# GM_DONE_MARKER file to create when finished
13#
14# SPDX-License-Identifier: GPL-3.0-or-later
15import os
16import time
17
18import FreeCAD as App
19import Part
20import Sketcher
21
22try:
23 _HERE = os.path.dirname(os.path.abspath(__file__))
24except NameError: # __file__ isn't always set when run from the Macro dialog
25 _HERE = os.path.expanduser("~")
26OUT_DIR = os.environ.get("GM_OUT_DIR", _HERE)
27FCSTD = os.path.join(OUT_DIR, "gm-eco-libre-volunteer-test.FCStd")
28RENDER = os.path.join(OUT_DIR, "gm-eco-libre-volunteer-test-render.png")
29SCREENSHOT = os.path.join(OUT_DIR, "gm-eco-libre-volunteer-test-screenshot.png")
30DONE_MARKER = os.environ.get("GM_DONE_MARKER", "")
31
32GUI = App.GuiUp
33if GUI:
34 import FreeCADGui as Gui
35 from PySide import QtGui
36
37
38def pause(seconds):
39 """Let the GUI repaint for a while so each step is visible in the video."""
40 if not GUI:
41 return
42 end = time.time() + seconds
43 while time.time() < end:
44 QtGui.QApplication.processEvents()
45 time.sleep(0.02)
46
47
48def fit(seconds=1.0):
49 if GUI:
50 Gui.SendMsgToActiveView("ViewFit")
51 pause(seconds)
52
53
54def steel_look(*objs):
55 if not GUI:
56 return
57 look = App.Material(DiffuseColor=(0.72, 0.74, 0.77), AmbientColor=(0.35, 0.36, 0.38),
58 SpecularColor=(0.55, 0.55, 0.55), Shininess=0.35)
59 for o in objs:
60 o.ViewObject.ShapeAppearance = [look]
61
62
63def oblique(view, spin=0):
64 """Tilted front view that keeps the initials readable."""
65 tilt = App.Rotation(App.Vector(1, 0, 0), 50)
66 view.setCameraOrientation(App.Rotation(App.Vector(0, 0, 1), spin).multiply(tilt))
67 Gui.SendMsgToActiveView("ViewFit")
68
69
70def add_polygon(sketch, pts, x0, y0, step_pause):
71 """Add a closed polygon of line segments, constraining shared endpoints."""
72 first = None
73 prev = None
74 for i in range(len(pts)):
75 a = pts[i]
76 b = pts[(i + 1) % len(pts)]
77 gid = sketch.addGeometry(Part.LineSegment(
78 App.Vector(x0 + a[0], y0 + a[1], 0), App.Vector(x0 + b[0], y0 + b[1], 0)), False)
79 if prev is not None:
80 sketch.addConstraint(Sketcher.Constraint("Coincident", prev, 2, gid, 1))
81 else:
82 first = gid
83 prev = gid
84 if GUI:
85 App.ActiveDocument.recompute()
86 pause(step_pause)
87 sketch.addConstraint(Sketcher.Constraint("Coincident", prev, 2, first, 1))
88
89
90# Block-letter outlines in mm, origin at each letter's lower-left corner.
91# Letters are 500 mm tall with 80 mm strokes.
92LETTER_G = [
93 (0, 0), (350, 0), (350, 250), (175, 250), (175, 170), (270, 170),
94 (270, 80), (80, 80), (80, 420), (350, 420), (350, 500), (0, 500),
95]
96LETTER_M = [
97 (0, 0), (80, 0), (80, 330), (200, 180), (320, 330), (320, 0),
98 (400, 0), (400, 500), (320, 500), (200, 330), (80, 500), (0, 500),
99]
100
101# Optionally wait (with FreeCAD already on screen) until the screen recorder
102# has started and created this trigger file.
103START_TRIGGER = os.environ.get("GM_START_TRIGGER", "")
104if GUI:
105 mw = Gui.getMainWindow()
106 mw.showMaximized()
107 while START_TRIGGER and not os.path.exists(START_TRIGGER):
108 pause(0.2)
109
110# --- New document -----------------------------------------------------------
111doc = App.newDocument("GM_metal_sheet")
112doc.CreatedBy = "Greyson Miller"
113doc.License = "Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)"
114doc.LicenseURL = "https://creativecommons.org/licenses/by-sa/4.0/"
115if GUI:
116 Gui.activateWorkbench("PartDesignWorkbench")
117 pause(2.0)
118
119body = doc.addObject("PartDesign::Body", "Body")
120if GUI:
121 Gui.ActiveDocument.ActiveView.setActiveObject("pdbody", body)
122 Gui.ActiveDocument.ActiveView.viewIsometric()
123pause(1.0)
124
125xy_plane = body.Origin.OriginFeatures[3] # XY_Plane
126assert xy_plane.Role == "XY_Plane", xy_plane.Role
127
128# --- Sketch 1: 1000 x 1000 mm outline of the metal sheet ----------------------
129sheet_sketch = body.newObject("Sketcher::SketchObject", "SheetSketch")
130sheet_sketch.AttachmentSupport = (xy_plane, [""])
131sheet_sketch.MapMode = "FlatFace"
132doc.recompute()
133if GUI:
134 Gui.ActiveDocument.setEdit(sheet_sketch) # switches to the Sketcher workbench
135 Gui.ActiveDocument.ActiveView.viewTop()
136 pause(1.5)
137
138add_polygon(sheet_sketch, [(0, 0), (1000, 0), (1000, 1000), (0, 1000)], -500, -500, 2.0)
139# Fully constrain the square: horizontal/vertical edges, 1000 mm sides, centred.
140sheet_sketch.addConstraint(Sketcher.Constraint("Horizontal", 0))
141sheet_sketch.addConstraint(Sketcher.Constraint("Horizontal", 2))
142sheet_sketch.addConstraint(Sketcher.Constraint("Vertical", 1))
143sheet_sketch.addConstraint(Sketcher.Constraint("Vertical", 3))
144sheet_sketch.addConstraint(Sketcher.Constraint("DistanceX", 0, 1, 0, 2, 1000.0))
145sheet_sketch.addConstraint(Sketcher.Constraint("DistanceY", 1, 1, 1, 2, 1000.0))
146sheet_sketch.addConstraint(Sketcher.Constraint("Symmetric", 0, 1, 1, 2, -1, 1))
147doc.recompute()
148fit(1.5)
149if GUI:
150 Gui.ActiveDocument.resetEdit()
151pause(1.0)
152
153# --- Pad 200 mm: the 1 m x 1 m x 20 cm sheet --------------------------------
154pad = body.newObject("PartDesign::Pad", "Sheet")
155pad.Profile = sheet_sketch
156pad.Length = 200.0 # 20 cm
157doc.recompute()
158if GUI:
159 Gui.ActiveDocument.setEdit(pad)
160 Gui.ActiveDocument.ActiveView.viewIsometric()
161 fit(4.0)
162 Gui.ActiveDocument.resetEdit()
163 sheet_sketch.Visibility = False
164
165# Steel material on the body, so it is literally a sheet of metal.
166try:
167 import Materials
168 mm = Materials.MaterialManager()
169 steel = next(m for m in mm.Materials.values() if m.Name in ("Steel-Generic", "Steel"))
170 body.ShapeMaterial = steel
171except Exception as e: # material library differs between FreeCAD versions
172 App.Console.PrintWarning("Could not assign steel material: {}\n".format(e))
173doc.recompute()
174steel_look(body, pad)
175if GUI:
176 Gui.ActiveDocument.ActiveView.viewIsometric()
177fit(2.5)
178
179# --- Sketch 2: initials "GM" on the top face (z = 200 mm) -------------------
180letters = body.newObject("Sketcher::SketchObject", "InitialsSketch")
181letters.AttachmentSupport = (xy_plane, [""])
182letters.MapMode = "FlatFace"
183letters.AttachmentOffset = App.Placement(App.Vector(0, 0, 200), App.Rotation())
184doc.recompute()
185if GUI:
186 Gui.ActiveDocument.setEdit(letters)
187 Gui.ActiveDocument.ActiveView.viewTop()
188 fit(1.5)
189
190add_polygon(letters, LETTER_G, -420, -250, 1.5)
191add_polygon(letters, LETTER_M, 30, -250, 1.5)
192doc.recompute()
193pause(1.5)
194if GUI:
195 Gui.ActiveDocument.resetEdit()
196pause(1.0)
197
198# --- Pocket 50 mm: carve the initials 5 cm deep -----------------------------
199pocket = body.newObject("PartDesign::Pocket", "CarvedInitials")
200pocket.Profile = letters
201pocket.Length = 50.0 # 5 cm
202doc.recompute()
203if GUI:
204 Gui.ActiveDocument.setEdit(pocket)
205 Gui.ActiveDocument.ActiveView.viewIsometric()
206 fit(4.0)
207 Gui.ActiveDocument.resetEdit()
208 letters.Visibility = False
209steel_look(body, pad, pocket)
210if GUI:
211 oblique(Gui.ActiveDocument.ActiveView)
212pause(2.0)
213
214# --- Sanity checks ------------------------------------------------------------
215shape = body.Shape
216bb = shape.BoundBox
217sheet_volume = 1000.0 * 1000.0 * 200.0
218removed = sheet_volume - shape.Volume
219App.Console.PrintMessage(
220 "GM_RESULT valid={} bbox=({:.1f} x {:.1f} x {:.1f}) mm removed={:.0f} mm^3 "
221 "pocket_depth={:.1f} mm\n".format(
222 shape.isValid(), bb.XLength, bb.YLength, bb.ZLength, removed, pocket.Length.Value))
223
224doc.saveAs(FCSTD)
225
226if GUI:
227 # Rotate a little so the 5 cm depth of the carving is visible, then capture.
228 view = Gui.ActiveDocument.ActiveView
229 Gui.Selection.clearSelection()
230 oblique(view, spin=-20)
231 pause(2.0)
232 view.saveImage(RENDER, 1920, 1080, "White")
233 shot = QtGui.QApplication.primaryScreen().grabWindow(mw.winId())
234 shot.save(SCREENSHOT, "PNG")
235 pause(1.0)
236
237 # Slow orbit around the finished part to show the 5 cm deep carving.
238 for angle in range(-20, 341, 2):
239 oblique(view, spin=angle)
240 pause(0.05)
241 pause(3.0)
242
243if DONE_MARKER:
244 with open(DONE_MARKER, "w") as f:
245 f.write("done\n")