# ***** 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. 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) 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 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: document, freecad opened scene (or current scene) :returns: list with clones sublists [['c0'], ['c1', 'c2', 'c4'],] ''' doc_clones = {} for item in doc.Objects: # for solid objects only if not is_object_solid(item): continue # collect clones lists item_clones = [] for other in doc.Objects: if other == item: continue if other.Shape.isPartner(item.Shape): logger.info('%s and %s objects has equal Shapes', item.Label, other.Label) item_clones.append(other.Label) # 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