# ***** 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. Various FreeCAD's analise, tesselation and parsing tools. ''' __version__ = '0.5' import difflib import glob import logging import math import os import xml.dom.minidom import FreeCAD import Part import MeshPart logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) def is_object_solid(obj) -> bool: ''' Simple FreeCAD's object test for manifold mawater-tight surface. :param obj: part, FreeCAD Part::Feature object :returns: boolean of obj solid state ''' if not isinstance(obj, FreeCAD.DocumentObject): return False if not obj.isDerivedFrom('Part::Feature'): return False if obj.isDerivedFrom('PartDesign::CoordinateSystem'): return False if not hasattr(obj, 'Shape'): return False return obj.Shape.isClosed() def collect_clones(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)) -> list: ''' This script find equal cad parts in a FreeCAD .FCStd scene. :param doc: freecad document by name or current document :returns: {'basename': ['c1', 'c2', 'c4']} ''' doc_clones = {} parts = [item for item in doc.Objects if is_object_solid(item) if item.Visibility] for part in parts: # collect clones lists item_clones = [] for other in parts: if other == part: continue if (len(other.Shape.Vertexes) == len(part.Shape.Vertexes) and len(other.Shape.Edges) == len(part.Shape.Edges) and len(other.Shape.Faces) == len(part.Shape.Faces)): if round(other.Shape.Volume, 7) == round(part.Shape.Volume, 7): logger.info('Objects has equal Shapes: %s and %s ', part.Label, other.Label) item_clones.append(other.Label) # if part has clones if item_clones: item_clones.append(part.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), adaptive=False, linear_deflection=0.1, angular_deflection=20, tesselation_method='Standard', fem_size=50.0) -> list: ''' Perform part's 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, adaptive, 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 def hierarchy_tree(obj, dict_tree, clones_dic=None) -> dict: ''' Collect hierarchy tree as dict dict_tree { 'type': '', 'name': '', 'base_name': '', 'pose': [{'loc_xyz': []}, {'rot_xyzw': []}], 'attributes': [], 'children': [ { 'type': '', 'name': '', 'base_name': '', 'loc_xyz': [], 'rot_xyzw': [], 'attributes': [], 'children': [...] }, {...}, ] } ''' # collect type if obj.isDerivedFrom('Part::Feature'): if obj.isDerivedFrom('PartDesign::CoordinateSystem'): dict_tree['type'] = 'LCS' else: dict_tree['type'] = 'PART' elif obj.isDerivedFrom('App::Part'): dict_tree['type'] = 'LOCATOR' else: return False # collect name dict_tree['name'] = obj.Label # collect base_name dict_tree['base_name'] = '' if clones_dic: if obj.isDerivedFrom('Part::Feature'): for k, v in clones_dic.items(): if obj.Label in v: dict_tree['base_name'] = k # collect transforms dict_tree['pose'] = [ {'loc_xyz': list(obj.Placement.Base)}, {'rot_xyzw': list(obj.Placement.Rotation.Q)} ] # collect attributes dict_tree['attributes'] = [] robossembler_attrs = [attr for attr in dir(obj) if 'Robossembler' in attr] if robossembler_attrs: for attr in robossembler_attrs: dict_tree['attributes'].append({attr: getattr(obj, attr)}) # collect children for LOCATOR only if obj.OutList and obj.isDerivedFrom('App::Part'): dict_tree['children'] = [] for child in obj.OutList: # skip hidden objects if not child.Visibility: continue # skip helper objects if (child.isDerivedFrom('Part::Feature') or child.isDerivedFrom('App::Part')): dict_tree['children'].append({}) hierarchy_tree(child, dict_tree['children'][-1], clones_dic) return True