Skip to content

Instantly share code, notes, and snippets.

@jmwright
Last active December 12, 2023 15:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmwright/996c074c7730aad8cb4666683cfd78cd to your computer and use it in GitHub Desktop.
Save jmwright/996c074c7730aad8cb4666683cfd78cd to your computer and use it in GitHub Desktop.
FreeCAD Import in CadQuery
import os, sys
import glob
import zipfile
import tempfile
import cadquery as cq
def _fc_path():
"""
Pulled from cadquery-freecad-module
Find FreeCAD installation
"""
# Look for FREECAD_LIB env variable
_PATH = os.environ.get('FREECAD_LIB', '')
if _PATH and os.path.exists(_PATH):
return _PATH
# Try to guess if using Anaconda
if 'env' in sys.prefix:
if sys.platform.startswith('linux') or sys.platform.startswith('darwin'):
_PATH = os.path.join(sys.prefix,'lib')
# return PATH if FreeCAD.[so,pyd] is present
if len(glob.glob(os.path.join(_PATH,'FreeCAD.so'))) > 0:
return _PATH
elif sys.platform.startswith('win'):
_PATH = os.path.join(sys.prefix,'Library','bin')
# return PATH if FreeCAD.[so,pyd] is present
if len(glob.glob(os.path.join(_PATH,'FreeCAD.pyd'))) > 0:
return _PATH
if sys.platform.startswith('linux'):
# Make some dangerous assumptions...
for _PATH in [
os.path.join(os.path.expanduser("~"), "lib/freecad/lib"),
"/usr/local/lib/freecad/lib",
"/usr/lib/freecad/lib",
"/opt/freecad/lib/",
"/usr/bin/freecad/lib",
"/usr/lib/freecad-daily/lib",
"/usr/lib/freecad",
"/usr/lib64/freecad/lib",
"$HOME/mambaforge/envs/freecad/lib/"
]:
if os.path.exists(_PATH):
return _PATH
elif sys.platform.startswith('win'):
# Try all the usual suspects
for _PATH in [
"c:/Program Files/FreeCAD0.12/bin",
"c:/Program Files/FreeCAD0.13/bin",
"c:/Program Files/FreeCAD0.14/bin",
"c:/Program Files/FreeCAD0.15/bin",
"c:/Program Files/FreeCAD0.16/bin",
"c:/Program Files/FreeCAD0.17/bin",
"c:/Program Files (x86)/FreeCAD0.12/bin",
"c:/Program Files (x86)/FreeCAD0.13/bin",
"c:/Program Files (x86)/FreeCAD0.14/bin",
"c:/Program Files (x86)/FreeCAD0.15/bin",
"c:/Program Files (x86)/FreeCAD0.16/bin",
"c:/Program Files (x86)/FreeCAD0.17/bin",
"c:/apps/FreeCAD0.12/bin",
"c:/apps/FreeCAD0.13/bin",
"c:/apps/FreeCAD0.14/bin",
"c:/apps/FreeCAD0.15/bin",
"c:/apps/FreeCAD0.16/bin",
"c:/apps/FreeCAD0.17/bin",
"c:/Program Files/FreeCAD 0.12/bin",
"c:/Program Files/FreeCAD 0.13/bin",
"c:/Program Files/FreeCAD 0.14/bin",
"c:/Program Files/FreeCAD 0.15/bin",
"c:/Program Files/FreeCAD 0.16/bin",
"c:/Program Files/FreeCAD 0.17/bin",
"c:/Program Files (x86)/FreeCAD 0.12/bin",
"c:/Program Files (x86)/FreeCAD 0.13/bin",
"c:/Program Files (x86)/FreeCAD 0.14/bin",
"c:/Program Files (x86)/FreeCAD 0.15/bin",
"c:/Program Files (x86)/FreeCAD 0.16/bin",
"c:/Program Files (x86)/FreeCAD 0.17/bin",
"c:/apps/FreeCAD 0.12/bin",
"c:/apps/FreeCAD 0.13/bin",
"c:/apps/FreeCAD 0.14/bin",
"c:/apps/FreeCAD 0.15/bin",
"c:/apps/FreeCAD 0.16/bin",
"c:/apps/FreeCAD 0.17/bin",
]:
if os.path.exists(_PATH):
return _PATH
elif sys.platform.startswith('darwin'):
# Assume we're dealing with a Mac
for _PATH in [
"/Applications/FreeCAD.app/Contents/lib",
"/Applications/FreeCAD.app/Contents/Resources/lib",
os.path.join(os.path.expanduser("~"),
"Library/Application Support/FreeCAD/lib"),
]:
if os.path.exists(_PATH):
return _PATH
raise ImportError('Unable to determine freecad library path')
def import_part_static(fc_part_path):
"""
Imports without parameter handling by extracting the brep file from the FCStd file.
Does NOT require FreeCAD to be installed.
Parameters:
fc_part_path - Path to the FCStd file to be imported.
Returns:
A CadQuery Workplane object or None if the import was unsuccessful.
"""
res = None
# Make sure that the caller gave a valid file path
if not os.path.isfile(fc_part_path):
print("Please specify a valid path.")
return None
# A temporary directory is required to extract the zipped files to
with tempfile.TemporaryDirectory() as temp_dir:
# Extract the contents of the file
with zipfile.ZipFile(fc_part_path, 'r') as zip_ref:
zip_ref.extractall(temp_dir)
# Open the file with CadQuery
res = cq.Workplane(cq.Shape.importBrep(os.path.join(temp_dir, "PartShape.brp")))
return res
def import_part_parametric(fc_part_path, parameters=None):
"""
Uses FreeCAD to import a part and update it with altered parameters.
Requires FreeCAD to be installed and importable in Python.
Parameters:
fc_part_path - Path to the FCStd file to be imported.
parameters - Model parameters that should be used to modify the part.
Returns:
A CadQuery Workplane object or None if the import was unsuccessful.
"""
# If the caller did not specify any parameters, might as well call the static importer
if parameters == None:
return import_part_static(fc_part_path)
try:
# Attempt to include the FreeCAD installation path
path = _fc_path()
sys.path.insert(0, path)
# It should be possible to import FreeCAD now
import FreeCAD
except Exception as err:
print("FreeCAD must be installed, and it must be possible to import it in Python.")
return None
# Open the part file in FreeCAD and get the spreadsheet so we can update it
# doc = FreeCAD.open(fc_part_path)
doc = App.openDocument(fc_part_path)
# Get a reference to the the spreadsheet
sheet = doc.getObject("Spreadsheet")
# part = doc.getObject("body_base_shelf")
# Update each matching item in the spreadsheet
for key in parameters.keys():
sheet.set(key, "=" + str(parameters[key]["value"]) + parameters[key]["units"])
sheet.recompute()
# We need to touch each model to have it update
for object in doc.Objects:
object.touch()
# Make sure the 3D object is updated
# doc.recompute()
FreeCAD.ActiveDocument.recompute()
# Create a temporary path to save the file to
# temp_path = tempfile.NamedTemporaryFile(delete=False).name # tempfile.mktemp()
# We use the local directory for now because FreeCAD does not seem to want to open files from the /tmp directory
updated_path = "updated_part.FCStd" # os.path.join(temp_path,"updated_part.FCStd")
# Save the document and then re-open it as a static part
doc.saveAs(updated_path)
FreeCAD.ActiveDocument.saveAs(updated_path)
# Re-import the model statically
res = import_part_static(updated_path)
# res = import_part_static(fc_part_path)
# Close the open document
FreeCAD.closeDocument(doc.Name)
return res
def import_part(fc_part_path, parameters=None):
"""
Wrapper method that chooses whether or not to do a static import based on whether
or not parameters are passed.
Parameters:
fc_part_path - Path to the FCStd file to be imported.
parameters - Model parameters that should be used to modify the part.
Returns:
A CadQuery Workplane object or None if the import was unsuccessful.
"""
res = None
# If there are no parameters specified, we can do a static import
if parameters == None:
res = import_part_static(fc_part_path)
else:
res = import_part_parametric(fc_part_path, parameters)
return res
def get_part_parameters(fc_part_path, name_column_letter, value_column_letter):
"""
Extracts the parameters from the spreadsheet inside the FCStd file.
Does NOT require FreeCAD to be installed.
Parameters:
fc_part_path - Path to the FCStd file to be imported.
name_column_letter - Allows the caller to specify the column of the spreadsheet where the parameter name can be found.
value_column_letter - Allows the caller to specify the column of the spreadsheet where the parameter value can be found.
Returns:
A dictionary of the parameters, their initial values and the units of the values.
"""
# Make sure that the caller gave a valid file path
if not os.path.isfile(fc_part_path):
print("Please specify a valid path.")
return None
# This will keep the collection of the parameters and their current values
parameters = {}
# To split units from values
import re
# So that the XML file can be parsed
import xml.etree.ElementTree as ET
# A temporary directory is required to extract the zipped files to
with tempfile.TemporaryDirectory() as temp_dir:
# Extract the contents of the file
with zipfile.ZipFile(fc_part_path, 'r') as zip_ref:
zip_ref.extractall(temp_dir)
# parse the Document.xml file that holds metadata like the spreadsheet
tree = ET.parse(os.path.join(temp_dir, 'Document.xml'))
root = tree.getroot()
objects = root.find('ObjectData')
for object in objects.iter("Object"):
if object.get('name') == "Spreadsheet":
props = object.find('Properties')
for prop in props.iter("Property"):
if prop.get('name') == "cells":
for cell in prop.find("Cells").iter():
if cell is None or cell.get('content') is None:
continue
# Determine whether we have a parameter name or a parameter value
if "=" not in cell.get('content'):
# Make sure we did not get a description
if cell.get('address')[0] != name_column_letter and cell.get('address')[0] != value_column_letter:
continue
# Start a parameter entry in the dictionary
parameters[cell.get('content')] = {}
elif "=" in cell.get('content'):
# Extract the units
units = "".join(re.findall("[a-zA-Z]+", cell.get('content')))
if units is not None:
parameters[cell.get('alias')]["units"] = units
else:
parameters[cell.get('alias')]["units"] = "N/A"
# Extract the parameter value and store it
value = cell.get('content').replace("=", "").replace(units, "")
parameters[cell.get('alias')]["value"] = value
break
else:
continue
return parameters
def main():
# Used with FreeCAD models here: https://github.com/hoijui/nimble/tree/master/src/mech/freecad
# Universal shelf
# res = import_freecad_part_static("models/base_shelf.FCStd")
# cq.exporters.export(res, "exports/base_shelf.stl")
# Rail/rack leg
# res = import_freecad_part_static("models/master_rail.FCStd")
# cq.exporters.export(res, "exports/master_rail.stl")
# Raspberry Pi shelf
# res = import_freecad_part_static("models/rpi_4b_shelf.FCStd")
# cq.exporters.export(res, "exports/rpi_4b_shelf.stl")
# if res is not None:
# show(res)
# Example of pulling parameters from the FCStd file
# parameters = get_part_parameters("models/base_shelf.FCStd", name_column_letter="A", value_column_letter="B")
# print(parameters)
# Example of altering and importing a parametric FreeCAD part
res = import_part_parametric("models/base_shelf.FCStd", parameters = {"mount_dia": {"value": 4.8, "units": "mm"}})
from cadquery.vis import show
if res is not None:
show(res)
print(res)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment