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.

What it does

  1. Creates a new document and a PartDesign Body.

  2. Opens a sketch on the XY_Plane in the Sketcher workbench and draws a fully constrained 1000 mm x 1000 mm square, one line at a time.

  3. Pads the square 200 mm and sets the Body’s material to Steel.

  4. 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).

  5. Pockets the letters 50 mm deep.

  6. 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")