Skip to content

Instantly share code, notes, and snippets.

@aavogt
Last active May 22, 2025 14:22
Show Gist options
  • Save aavogt/d4b78be6bb4c64c2b59a2d29795e6dd2 to your computer and use it in GitHub Desktop.
Save aavogt/d4b78be6bb4c64c2b59a2d29795e6dd2 to your computer and use it in GitHub Desktop.
freecad export stls (on save)
#!/usr/bin/env -S freecad --console
import os
import sys
import textwrap
import FreeCAD
import PartDesign
import tempfile
import shutil
from typing import TypeVar
from collections.abc import Sequence
def files_equal(file1_path, file2_path):
try:
with open(file1_path, 'rb') as file1, open(file2_path, 'rb') as file2:
content1 = file1.read()
content2 = file2.read()
return content1 == content2
except FileNotFoundError:
return False
# copilot can translate it to argparse, but freecad intercepts arguments
# Environment variables are one way, so then DO_ALL=true fc2stl.py ...
# is an option.
# To get the usual fc2stl.py -a example.FCStd, I would have to
# make an intermediate script that calls the freecad --console
# with the correct environment variables, which for now seems to
# bee more work and complication for the future use of this script
# than I want.
# so for now I just set doAll = True here and the newer feature of
# not updating identical files seems to reduce the need for doAll=False
doAll = True
T = TypeVar("T", bound=FreeCAD.DocumentObject)
def findObjects(t: type[T], doc: FreeCAD.Document) -> Sequence[T]:
return doc.findObjects(t.__qualname__.replace(".", "::"))
# This is a workaround for the fact that PartDesign.Body is not defined in the PartDesign module,
# even though there are objects that claim to be a PartDesign.Body.
if not hasattr(PartDesign, 'Body'):
class Body:
__qualname__ = "PartDesign.Body"
PartDesign.Body = Body
def export_bodies_to_stl(doc_name: str):
doc = FreeCAD.open(os.path.join(os.curdir, doc_name))
base_name = os.path.splitext(doc_name)[0]
result : list[str] = []
for i, obj in enumerate(findObjects(PartDesign.Body, doc)):
if doAll or obj.Visibility:
with tempfile.NamedTemporaryFile() as tmp:
stl_file_path = os.path.join(os.curdir, f"{base_name}-{obj.Label}.stl")
obj.Shape.exportStl(tmp.name)
if not files_equal(stl_file_path, tmp.name):
shutil.copy(tmp.name, stl_file_path)
os.system(f"env -u PYTHONHOME recently_used.py {stl_file_path}")
result += [stl_file_path]
else:
print(f"{stl_file_path} equal to {tmp.name}: leaving {stl_file_path} unchanged")
return result
try:
result : list[str] = []
if len(sys.argv) < 4:
raise ValueError(textwrap.dedent("""\
Usage:
$ fc2stl.py example.FCStd
Or to also rerun when it changes:
$ x=example.FCStd; entr fc2stl.py $x <<< $x
which is done by fc2stl example.FCStd
This exports all visible bodies in ./example.FCStd to STL files.
./example-Body.stl, ./example-Body001.stl, etc. are created.
Consider adding to ~/.zshrc:
_fcstd_files() {
compadd *.FCStd
}
compdef _fcstd_files fc2stl.py
"""))
for x in sys.argv[3:]:
print(f"Exporting {x} to STL")
result += export_bodies_to_stl(x)
print("Exported:")
print(" ".join(result))
except Exception as e:
print(e)
sys.exit(1)
sys.exit(0)
# put into ~/.zshrc
# or source it:
# . path/to/fc2stl.zsh
# intercalate "-" . inits . splitOn "-"
intercalate_inits () {
local IFS="-"
parts=($=1)
reply=()
for ((i=1; i<=${#parts[@]}; i++)); do
reply[i]+=${parts[1,i]}
done
}
test_intercalate_inits () {
intercalate_inits "abc-def-ghci.stl"
if [[ ${reply[3]} != "abc-def-ghci.stl" ]]; then
echo "Test failed: ${reply[3]} != abc-def-ghci.stl"
return 1
fi
if [[ ${reply[2]} != "abc-def" ]]; then
echo "Test failed: ${reply[2]} != abc-def"
return 1
fi
if [[ ${reply[1]} != "abc" ]]; then
echo "Test failed: ${reply[1]} != abc"
return 1
fi
echo "All tests passed"
return 0
}
# test_intercalate_inits
# fc2stl/fc2stl.py-generated files
# pc4-m4-wye-Body.stl
# are not completed if there is a pc4-m4-wye.FCStd
_freecad_files() {
local files=(*.FCStd *.stl *.3mf *.step)
local filtered_files=()
for file in "${files[@]}"; do
if [[ $file == *.stl ]]; then
intercalate_inits "${file%.stl}"
found=false
for r in "${reply[@]}"; do
if [[ -e "$r.FCStd" ]]; then
found=true
break
fi
done
if [[ $found == false ]]; then
filtered_files+=("$file")
fi
else
filtered_files+=("$file")
fi
done
compadd "${filtered_files[@]}"
}
_fcstd_files() {
compadd *.FCStd
}
fc2stl () {
ls $* | entr fc2stl.py $*
}
compdef _fcstd_files fc2stl.py
compdef _fcstd_files fc2stl
compdef _freecad_files freecad
_slicer_files() {
compadd *.stl *.3mf
}
compdef _slicer_files prusa-slicer
#!/usr/bin/env python3
# https://unix.stackexchange.com/a/509417
import gi
import sys
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio, GLib
def add(file_paths):
rec_mgr = Gtk.RecentManager.get_default()
for path in file_paths:
rec_mgr.add_item(Gio.File.new_for_path(path).get_uri())
GLib.idle_add(Gtk.main_quit)
Gtk.main()
if __name__ == "__main__":
add(sys.argv[1:])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment