# coding: utf-8 # Copyright (C) 2023 Ilia Kurochkin # # This program 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. ''' DESCRIPTION. - Reads a FreeCAD .FCStd file. - Set tesselation parts to mesh. - Return scene as JSON dictionary. ''' { ".FCStd": { "name": "", "type": "LOCATOR", "loc_xyz": ["x", "y", "z"], "rot_xyzw": ["x", "y", "z", "w"], "attributes": [], "clones": {}, "mesh_path": "", "material": "", "children": [ { "name": "", "type": "PART", "loc_xyz": ["x", "y", "z"], "rot_xyzw": ["x", "y", "z", "w"], "attributes": [ "Robossembler_NonSolid": True ], "clones": { "": [ "", ] }, "mesh_path": "/path/to/robossembler/database/mesh/..stl", "material": "/path/to/robossembler/materials/Robossembler_ABS-Dark-Rough.FCMat", "children": [] }, { "name": "", "type": "LCS", "loc_xyz": ["x", "y", "z"], "rot_xyzw": ["x", "y", "z", "w"], "attributes": [ "Robossembler_DefaultOrigin": True, "Robossembler_SocketFlow": "inlet", "Robossembler_SocketType": "planar", "Robossembler_WorldStable": True ], "clones": {}, "mesh_path": "", "material": "", "children": [] } ] } } __version__ = '0.1' import collections import logging import math import json import shutil import xml.etree.ElementTree as ET import FreeCAD import Part import Mesh import MeshPart from freecad.utils.solid_tools import (is_object_solid, collect_clones, tesselation, config_parser, get_material_paths) from utils.custom_parser import CustomArgumentParser logger = logging.getLogger(__name__) def freecad_to_json(**kwargs): ''' Reads a FreeCAD .FCStd file and return json assembly. ''' scene = {} js_objs = {} doc = FreeCAD.open(kwargs['fcstd_path']) docname = doc.Name # collect equal cad objects clones = collect_clones(doc): # collect all materials fem_mats = [fem_mat for fem_mat in doc.Objects if fem_mat.isDerivedFrom('App::MaterialObjectPython') ] material_paths = get_material_paths for obj in doc.Objects: # skip hidden objects if not obj.Visibility: continue # skip non part or lcs objects if not (obj.isDerivedFrom('PartDesign::CoordinateSystem') or obj.isDerivedFrom('PartDesign::CoordinateSystem')): continue js_obj = { 'name': 'NAME', 'type': 'TYPE', 'loc_xyz': ['X', 'Y', 'Z'], 'rot_xyzw': ['X', 'Y', 'Z', 'W'], 'attributes': [], 'clones': {}, 'mesh_path': '', 'material': '', 'children': []} js_obj['name'] = obj.Label if obj.isDerivedFrom('PartDesign::CoordinateSystem'): js_obj['type'] = 'LCS' elif obj.isDerivedFrom('Part::Feature'): js_obj['type'] = 'PART' js_obj['loc_xyz'] = list(obj.Placement.Base) js_obj['rot_xyzw'] = list(obj.Placement.Rotation.Q) for attr in dir(obj): if 'Robossembler' not in attr: continue js_obj['attributes'].append({attr: getattr(obj, attr)}) base_names = [k for k, v in mdct.items() if name in v] if base_names: js_obj['clones'] = {base_names[0]: clones[base_names[0]]} else: js_obj['clones'] = {} if obj.isDerivedFrom('Part::Feature'): # filter for nonsolids forsed_nonsolid = (hasattr(obj, 'Robossembler_NonSolid') and getattr(obj, 'Robossembler_NonSolid')) if is_object_solid(obj) or forsed_nonsolid: # create mesh from shape mesh_from_shape = tesselation( doc, obj, kwargs['linear_deflection'], kwargs['angular_deflection'], kwargs['tesselation_method'], kwargs['fem_size'], adaptive=not forsed_nonsolid ) # export to stl files main_file_path = doc.FileName main_file_dir = os.path.dirname(main_file_path) mesh_dir = os.path.join(main_file_dir, 'mesh') if os.path.exists(mesh_dir): shutil.rmtree(mesh_dir) os.makedirs(mesh_dir) else: os.makedirs(mesh_dir) mesh_path = os.path.join(mesh_dir, obj.Label + '.stl') mesh_from_shape.Mesh.write(mesh_path) js_obj['mesh_path'] = mesh_path logger.info('Part %s exported to stl mesh file %s.', obj.Label, mesh_path) # find linked material config 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: js_obj['material'] = material_path # construct assembly hierarchy obj_parent = obj.getParentGeoFeatureGroup() obj_child_name = None parents = {} deep_index = 0 while obj_parent: parent = {} parent['fc_location'] = tuple(obj_parent.Placement.Base) parent['fc_rotation'] = obj_parent.Placement.Rotation.Q obj_child_name = obj_parent.Label obj_parent = obj_parent.getParentGeoFeatureGroup() if obj_parent: parent['parent'] = obj_parent.Label else: parent['parent'] = None parents[obj_child_name] = parent parent['deep_index'] = deep_index deep_index += 1 js_obj['hierarchy'] = parents js_objs[obj.Label] = js_obj FreeCAD.closeDocument(docname) scene[kwargs['fcstd_path']] = js_objs logger.info('Passed %s objects without errors', len(js_objs)) print(json.dumps(scene)) # to run script via FreeCADCmd parser = CustomArgumentParser() parser.add_argument( '--fcstd_path', type=str, help='Path to source FreeCAD scene', required=True ) parser.add_argument( '--tesselation_method', type=str, help='Select tesselation method: Standard or FEM.', default='Standard', required=False ) parser.add_argument( '--linear_deflection', type=float, help='Max linear distance error', default=0.1, required=False ) parser.add_argument( '--angular_deflection', type=float, help='Max angular distance error', default=20.0, required=False ) parser.add_argument( '--fem_size', type=float, help='For FEM method only! Finite element size in mm', default=50.0, required=False ) parser.add_argument( '--skiphidden', type=bool, help='Skip processing for hidden FreeCAD objects', default=True, required=False ) """ parser.add_argument( '--property_forse_nonsolid', type=str, help='FreeCAD property to enable processing for nonsolid objects', default='Robossembler_NonSolid', required=False ) """ fc_kwargs = vars(parser.parse_known_args()[0]) freecad_to_json(**fc_kwargs) logger.info('FreeCAD scene passed!')