Last active
May 22, 2025 14:22
-
-
Save aavogt/d4b78be6bb4c64c2b59a2d29795e6dd2 to your computer and use it in GitHub Desktop.
freecad export stls (on save)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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