306 lines
9.8 KiB
Python
306 lines
9.8 KiB
Python
# ***** 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 <https://www.gnu.org/licenses/>.
|
|
#
|
|
# ***** 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
|