# ***** 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 . # # ***** 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 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].Label) 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=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label), 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 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"//") 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) logger.info('FreeCAD document %s published!', doc.Label) return doc.Label