framework/cg/freecad/utils/freecad_exporters.py

227 lines
7.7 KiB
Python

# ***** 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 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
lcs_root_points = []
for obj in doc.Objects:
if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
if (hasattr(obj, 'Robossembler_DefaultOrigin')
and getattr(obj, 'Robossembler_DefaultOrigin')):
lcs_root_points.append(obj)
if len(lcs_root_points) > 1:
root_locators = []
for lcs in lcs_root_points:
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].Label)
else:
root_locators = [
root for root in doc.Objects
if not root.InList
if root .isDerivedFrom('App::Part')]
config_files = []
for root_locator in root_locators:
dict_tree = {}
freecad_tools.hierarchy_tree(root_locator, dict_tree, clones_dic)
# write file
main_file_dir = os.path.dirname(doc.FileName)
assembly_tree_path = os.path.join(main_file_dir, f'{root_locator.Label}.json')
with open(assembly_tree_path, 'w', encoding='utf-8') as json_file:
json.dump(dict_tree, json_file, ensure_ascii=False, indent=4)
logger.info('Assembly tree saved successfully to %s!', assembly_tree_path)
config_files.append(assembly_tree_path)
logger.info('Saved %s assembly trees!', len(config_files))
return config_files
def export_parts_database(
doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label),
clones_dic=None, **tesselation_params):
'''
Collect parts database and export as JSON config file
[
{
'type': '',
'name': '',
'attributes': [],
'part_path': '',
'material_path': ''
},
{...},
]
'''
# path directory
main_file_dir = os.path.dirname(doc.FileName)
parts_dir = os.path.join(main_file_dir, 'parts')
if os.path.exists(parts_dir):
shutil.rmtree(parts_dir)
os.makedirs(parts_dir)
else:
os.makedirs(parts_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 = {'type': 'PART'}
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(parts_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, main_file_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 material_path in material_paths:
if fem_mat_name[0] in material_path:
db_obj['material_path'] = material_path
# append to database
parts_db.append(db_obj)
logger.info('Passed %s parts without errors', len(parts_db))
parts_db_path = os.path.join(main_file_dir, f'{FreeCAD.ActiveDocument.Label}.FCStd.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 publish_project_database(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)):
'''
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)
export_assembly_trees(doc, clones_dic)
export_parts_database(doc, clones_dic, **tesselation_params)
logger.info('FreeCAD document %s published!', doc.Label)
return doc.Label