framework/freecad_workbench/utils/freecad_exporters.py
2024-04-14 18:54:47 +00:00

259 lines
No EOL
8.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ***** BEGIN GPL LICENSE BLOCK *****
#
# Copyright (C) 2021-2024 Robossembler LLC
#
# Created by Ilia Kurochkin (brothermechanic)
# contact: brothermechanic@yandex.com
#
# This file is part of Robossembler Framework
# project repo: https://gitlab.com/robossembler/framework
#
# Robossembler Framework is free software;
# you can redistribute it and/or modify
# it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <https://www.gnu.org/licenses/>.
#
# ***** END GPL LICENSE BLOCK *****
#
# coding: utf-8
'''
DESCRIPTION.
FreeCAD's tools for publishing .FCStd scene.
'''
__version__ = '0.3'
import logging
import json
import os
import shutil
import FreeCAD
import utils.freecad_tools as freecad_tools
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
def export_assembly_trees(doc, clones_dic=None) -> list:
''' Read FreeCAD .FCStd hierarchy and store it to assembly JSON config files. '''
# determine root locators
default_origins = []
for obj in doc.Objects:
if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
if (hasattr(obj, 'Robossembler_DefaultOrigin')
and getattr(obj, 'Robossembler_DefaultOrigin')):
default_origins.append(obj)
if len(default_origins) > 1:
root_locators = []
for lcs in default_origins:
if hasattr(lcs, 'Robossembler_RootLocator'):
root_locators.append(getattr(lcs, 'Robossembler_DefaultOrigin'))
else:
logger.warning('RootLocator attribute not found for %s LCS! '
'So %s used as Root Locator!',
lcs.Label, lcs.InList[0].Label)
root_locators.append(lcs.InList[0])
else:
root_locators = [
root for root in doc.Objects
if not root.InList
if root.isDerivedFrom('App::Part')]
trees = []
for root_locator in root_locators:
dict_tree = {}
freecad_tools.hierarchy_tree(root_locator, dict_tree, clones_dic)
trees.append(dict_tree)
# write file
project_dir = os.path.dirname(doc.FileName)
trees_path = os.path.join(project_dir, 'trees.json')
with open(trees_path, 'w', encoding='utf-8') as json_file:
json.dump(trees, json_file, ensure_ascii=False, indent=4)
logger.info('Assembly tree saved successfully to %s!', trees_path)
logger.info('Saved %s assembly trees!', len(trees))
return trees_path
def export_parts_database(
doc,
clones_dic=None, **tesselation_params):
'''
Collect parts database and export as JSON config file
[
{
'name': '',
'part_path': '',
'material_path': ''
},
{...},
]
'''
# path directory
project_dir = os.path.dirname(doc.FileName)
materials_dir = os.path.join(project_dir, 'parts', 'materials')
if os.path.exists(materials_dir):
shutil.rmtree(materials_dir)
os.makedirs(materials_dir)
else:
os.makedirs(materials_dir)
objects_dir = os.path.join(project_dir, 'parts', 'objects')
if os.path.exists(objects_dir):
shutil.rmtree(objects_dir)
os.makedirs(objects_dir)
else:
os.makedirs(objects_dir)
# collect all materials
fem_mats = [fem_mat for fem_mat in doc.Objects
if fem_mat.isDerivedFrom('App::MaterialObjectPython')]
material_paths = freecad_tools.get_material_paths()
parts_db = []
for obj in doc.Objects:
# skip hidden objects
if not obj.Visibility:
continue
# skip non part objects
if not obj.isDerivedFrom('Part::Feature'):
continue
# skip lcs objects
if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
continue
# skip non solid part objects
forsed_nonsolid = (
hasattr(obj, 'Robossembler_NonSolid')
and getattr(obj, 'Robossembler_NonSolid'))
if not freecad_tools.is_object_solid(obj) or forsed_nonsolid:
logger.warning('Part has non solid shape! Please check %s', obj.Label)
continue
db_obj = {
'name': '',
'part_path': '',
'material_path': ''
}
db_obj['name'] = obj.Label
if clones_dic:
for k, v in clones_dic.items():
if obj.Label in v:
db_obj['name'] = k
robossembler_attrs = [attr for attr in dir(obj) if 'Robossembler' in attr]
if robossembler_attrs:
db_obj['attributes'] = []
for attr in robossembler_attrs:
db_obj['attributes'].append({attr: getattr(obj, attr)})
part_path = os.path.join(objects_dir, db_obj['name'] + '.stl')
if os.path.exists(part_path):
# this is clone
continue
mesh_from_shape = freecad_tools.tesselation(
obj, doc,
# TODO adaptive=(not forsed_nonsolid),
adaptive=False,
**tesselation_params
)
# export to stl files
#Mesh.export([mesh_from_shape], part_path, tolerance=linear_deflection)
mesh_from_shape.Mesh.write(part_path)
db_obj['part_path'] = os.path.relpath(part_path, project_dir)
logger.info('Part %s exported to stl file %s.', obj.Label, part_path)
# find linked material path
fem_mat_name = [fem_mat.Material['CardName']
for fem_mat in fem_mats
for ref in fem_mat.References
if ref[0].Label == obj.Label]
if fem_mat_name:
for source_path in material_paths:
if fem_mat_name[0] not in source_path:
continue
material_path = os.path.join(
materials_dir, os.path.basename(source_path))
if not os.path.exists(material_path):
shutil.copy2(source_path, material_path)
db_obj['material_path'] = os.path.relpath(material_path, project_dir)
# append to database
parts_db.append(db_obj)
logger.info('Passed %s parts without errors', len(parts_db))
parts_db_path = os.path.join(project_dir, 'parts.json')
with open(parts_db_path, 'w', encoding='utf-8') as json_file:
json.dump(parts_db, json_file, ensure_ascii=False, indent=4)
logger.info('Parts Database exported successfully to %s!', parts_db_path)
return parts_db_path
def save_FCStd_project(doc):
if doc is not None:
# path directory
project_dir = os.path.dirname(doc.FileName)
if project_dir:
project_dir, project_filename = os.path.split(project_path)
new_path = os.path.join(project_dir, project_filename)
FreeCAD.saveDocument(doc, new_path)
print("Проект сохранен в", new_path)
else:
print("Проект не был сохранен, поэтому невозможно сохранить с тем же именем.")
else:
print("Документ не найден.")
def publish_project_database(doc):
'''
Publish FCStd document to CG pipeline:
- Exetute openned FreeCAD document.
- Save FCStd document to database root path.
- Export all assembly trees with LCS DefaultOrigin,
or single one as JSON hierarchy tree.
- Set tesselate solid parts to mesh.
- Export all tesselated parts to STL files.
- Return document as JSON dictionary.
'''
tesselation_params = {
# Select tesselation method: Standard or FEM.
'tesselation_method': 'Standard',
# Max linear distance error
'linear_deflection': 0.1,
# Max angular distance error
'angular_deflection': 20.0,
# For FEM method only! Finite element size in mm
'fem_size': 10.0
}
# TODO Save FCStd project file
#doc.saveAs(u"/<file_dir>/<file_name>")
clones_dic = freecad_tools.collect_clones(doc)
tree_files = export_assembly_trees(doc, clones_dic)
parts_data = export_parts_database(doc, clones_dic, **tesselation_params)
save_FCStd_project(doc)
logger.info('FreeCAD document %s published!', doc.Label)
return doc.Label
def cmd_publish_project_database(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)):
publish_project_database(doc)