259 lines
No EOL
8.8 KiB
Python
259 lines
No EOL
8.8 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 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) |