diff --git a/cg/freecad/utils/freecad_to_json.py b/cg/freecad/utils/freecad_to_json.py index ec983e4..ba18f1e 100644 --- a/cg/freecad/utils/freecad_to_json.py +++ b/cg/freecad/utils/freecad_to_json.py @@ -16,16 +16,75 @@ DESCRIPTION. - 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 -import logging -import math -from freecad.utils.solid_tools import (is_object_solid, collect_clones) + +from freecad.utils.solid_tools import (is_object_solid, + collect_clones, + tesselation, + config_parser, + get_material_paths) from utils.custom_parser import CustomArgumentParser @@ -45,66 +104,91 @@ def freecad_to_json(**kwargs): clones = collect_clones(doc): # collect all materials - fem_mats = [] - for fem_mat in doc.Objects: - if fem_mat.isDerivedFrom('App::MaterialObjectPython'): - fem_mats.append(fem_mat) + 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: - js_obj = {} - if kwargs['skiphidden']: - if not obj.Visibility: - continue + # 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' - for attr in dir(obj): - if 'Robossembler' not in attr: - continue - js_obj[attr] = getattr(obj, attr) - elif obj.isDerivedFrom('Part::Feature'): js_obj['type'] = 'PART' - # filter for nonsolids - if is_object_solid(obj) or hasattr(obj, kwargs['property_forse_nonsolid']): - # create mesh from shape - shape = obj.Shape - shape = obj.Shape.copy() - shape.Placement = obj.Placement.inverse().multiply(shape.Placement) - meshfromshape = doc.addObject('Mesh::Feature', 'Mesh') - if kwargs['tesselation_method'] == 'Standard': - meshfromshape.Mesh = MeshPart.meshFromShape( - Shape=shape, - LinearDeflection=kwargs['linear_deflection'], - AngularDeflection=math.radians(kwargs['angular_deflection']), - Relative=False) - elif kwargs['tesselation_method'] == 'FEM': - meshfromshape.Mesh = MeshPart.meshFromShape( - Shape=shape, - MaxLength=kwargs['fem_size']) - else: - raise TypeError('Wrong tesselation method! ' - 'Standard and FEM methods are supported only!') + js_obj['loc_xyz'] = list(obj.Placement.Base) + js_obj['rot_xyzw'] = list(obj.Placement.Rotation.Q) - t = meshfromshape.Mesh.Topology - verts = [[v.x, v.y, v.z] for v in t[0]] - faces = t[1] - js_obj['mesh'] = (verts, faces) + for attr in dir(obj): + if 'Robossembler' not in attr: + continue + js_obj['attributes'].append({attr: getattr(obj, attr)}) - # one material for the whole object - for fem_mat in fem_mats: - for ref in fem_mat.References: - if ref[0].Label == obj.Label: - js_obj['material'] = fem_mat.Material - - # skip for other object's types + 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: - continue + 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 - js_obj['fc_location'] = tuple(obj.Placement.Base) - js_obj['fc_rotation'] = obj.Placement.Rotation.Q # construct assembly hierarchy obj_parent = obj.getParentGeoFeatureGroup() @@ -180,6 +264,7 @@ parser.add_argument( default=True, required=False ) +""" parser.add_argument( '--property_forse_nonsolid', type=str, @@ -187,6 +272,7 @@ parser.add_argument( default='Robossembler_NonSolid', required=False ) +""" fc_kwargs = vars(parser.parse_known_args()[0]) diff --git a/cg/freecad/utils/solid_tools.py b/cg/freecad/utils/solid_tools.py index 74cc620..a8d5516 100644 --- a/cg/freecad/utils/solid_tools.py +++ b/cg/freecad/utils/solid_tools.py @@ -31,8 +31,15 @@ Solid tools for FreeCAD's .FCStd scene. ''' __version__ = '0.2' +import difflib +import glob import logging +import math +import xml.dom.minidom + import FreeCAD +import Part +import MeshPart logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -61,12 +68,13 @@ def collect_clones(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)) -> lis :param doc: document, freecad opened scene (or current scene) :returns: list with clones sublists [['c0'], ['c1', 'c2', 'c4'],] ''' - doc_clones = [] + doc_clones = {} for item in doc.Objects: + # for solid objects only if not is_object_solid(item): continue + # collect clones lists item_clones = [] - item_clones.append(item.Label) for other in doc.Objects: if other == item: continue @@ -74,7 +82,146 @@ def collect_clones(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)) -> lis logger.info('%s and %s objects has equal Shapes', item.Label, other.Label) item_clones.append(other.Label) - item_clones.sort() - if item_clones not in doc_clones: - doc_clones.append(item_clones) + # if item has clones + if item_clones: + item_clones.append(item.Label) + # find common basename for all clones + idx = 0 + common_name = item_clones[0].lower() + while idx < len(item_clones): + match_data = difflib.SequenceMatcher( + None, common_name, item_clones[idx].lower() + ).find_longest_match() + common_name = common_name[match_data.a:match_data.a + match_data.size] + idx += 1 + # if names has or hasn't common patterns + item_base_name = common_name.strip(' .,_') or item_clones[0] + # sort list names + item_clones.sort() + # add only unical list of clones + if item_clones not in doc_clones.values(): + doc_clones[item_base_name] = item_clones return doc_clones + + +def tesselation(obj, + doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label), + linear_deflection=0.1, angular_deflection=20, + tesselation_method='Standard', fem_size=50.0, + adaptive=True) -> list:: + ''' + Perform shape tesselation. + ''' + shape = obj.Shape.copy() + shape.Placement = obj.Placement.inverse().multiply(shape.Placement) + mesh_from_shape = doc.addObject('Mesh::Feature', 'mesh_from_shape') + if tesselation_method == 'Standard': + mesh_from_shape.Mesh = MeshPart.meshFromShape( + Shape=shape, + LinearDeflection=linear_deflection, + AngularDeflection=math.radians(angular_deflection), + Relative=False) + elif tesselation_method == 'FEM': + mesh_from_shape.Mesh = MeshPart.meshFromShape( + Shape=shape, + MaxLength=fem_size) + else: + raise TypeError('Wrong tesselation method! ' + 'Standard and FEM methods are supported only!') + + # for solids only + volume = shape.Volume + mesh_shape = Part.Shape() + mesh_shape.makeShapeFromMesh(mesh_from_shape.Mesh.Topology, 0.100000, False) + mesh_volume = mesh_shape.Volume + + if adaptive: + deviation = 100 * (volume - mesh_volume) / volume + if deviation > 0.05: + logger.info( + 'Percentage tesselation deviation for %s object is %s! ' + 'Double increasing mesh detalisation!', + obj.Label, deviation) + linear_deflection = linear_deflection / 2 + angular_deflection = angular_deflection / 2 + doc.removeObject('mesh_from_shape') + #doc.recompute() + tesselation( + obj, doc, linear_deflection, angular_deflection, + tesselation_method, fem_size) + + return mesh_from_shape + + +def config_parser(comfig_path, param_path, param_name, param_type=None) -> str: + ''' + Parse FreeCAD XML confings + ''' + root_node = xml.dom.minidom.parse(comfig_path).childNodes[0] + node_names = ['Root'] + param_path.split('/') + [param_name] + + for node_name in node_names: + for child in root_node.childNodes: + if hasattr(child, 'getAttribute') and child.getAttribute('Name') == node_name: + root_node = child + break + else: + return None + + tagname = root_node.tagName + + if param_type is not None and tagname != param_type: + return None + + if tagname == 'FCText': + if root_node.firstChild: + return root_node.firstChild._get_data() + return '' + + if tagname == 'FCBool': + return bool(root_node.getAttribute('Value')) + + if tagname in ('FCInt', 'FCUInt'): + return int(root_node.getAttribute('Value')) + + if tagname == 'FCFloat': + return float(root_node.getAttribute('Value')) + + return root_node.getAttribute('Value') + + +def get_material_paths() -> list: + ''' + Collect all available material paths + ''' + material_paths = [] + system_dir = os.path.join( + FreeCAD.getResourceDir(), + 'Mod/Material') + material_paths.extend( + [item for item in glob.glob(f'{system_dir}/**', recursive=True) + if item.endswith('.FCMat')] + ) + user_dir = os.path.join( + FreeCAD.getUserAppDataDir(), + 'Mod/Material') + material_paths.extend( + [item for item in glob.glob(f'{user_dir}/**', recursive=True) + if item.endswith('.FCMat')] + ) + custom_dir = [] + if config_parser( + FreeCAD.ConfigGet("UserParameter"), + 'BaseApp/Preferences/Mod/Material/Resources', + 'UseMaterialsFromCustomDir', 'FCBool' + ): + custom_dir = config_parser( + FreeCAD.ConfigGet("UserParameter"), + 'BaseApp/Preferences/Mod/Material/Resources', + 'CustomMaterialsDir', 'FCText' + ) + material_paths.extend( + [item for item in glob.glob(f'{custom_dir}/**', recursive=True) + if item.endswith('.FCMat')] + ) + return material_paths