FreeCAD: uber updating function in freecad_to_json WIP

This commit is contained in:
brothermechanic 2024-03-26 22:00:41 +03:00
parent cc538b719f
commit 8401cc51fa
No known key found for this signature in database
GPG key ID: 9C59EF9503ACD106
2 changed files with 288 additions and 55 deletions

View file

@ -16,16 +16,75 @@ DESCRIPTION.
- Set tesselation parts to mesh. - Set tesselation parts to mesh.
- Return scene as JSON dictionary. - Return scene as JSON dictionary.
''' '''
{
"<file_name>.FCStd": {
"name": "<locator_name>",
"type": "LOCATOR",
"loc_xyz": ["x", "y", "z"],
"rot_xyzw": ["x", "y", "z", "w"],
"attributes": [],
"clones": {},
"mesh_path": "",
"material": "",
"children": [
{
"name": "<part_name>",
"type": "PART",
"loc_xyz": ["x", "y", "z"],
"rot_xyzw": ["x", "y", "z", "w"],
"attributes": [
"Robossembler_NonSolid": True
],
"clones": {
"<part_basename>": [
"<part_name>",
]
},
"mesh_path": "/path/to/robossembler/database/mesh/<file_name>.<part_basename>.stl",
"material": "/path/to/robossembler/materials/Robossembler_ABS-Dark-Rough.FCMat",
"children": []
},
{
"name": "<lcs_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' __version__ = '0.1'
import collections
import logging
import math
import json import json
import shutil
import xml.etree.ElementTree as ET
import FreeCAD import FreeCAD
import Part import Part
import Mesh import Mesh
import MeshPart 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 from utils.custom_parser import CustomArgumentParser
@ -45,66 +104,91 @@ def freecad_to_json(**kwargs):
clones = collect_clones(doc): clones = collect_clones(doc):
# collect all materials # collect all materials
fem_mats = [] fem_mats = [fem_mat for fem_mat in doc.Objects
for fem_mat in doc.Objects: if fem_mat.isDerivedFrom('App::MaterialObjectPython')
if fem_mat.isDerivedFrom('App::MaterialObjectPython'): ]
fem_mats.append(fem_mat) material_paths = get_material_paths
for obj in doc.Objects: for obj in doc.Objects:
js_obj = {}
if kwargs['skiphidden']: # skip hidden objects
if not obj.Visibility: if not obj.Visibility:
continue 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'): if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
js_obj['type'] = 'LCS' 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'): elif obj.isDerivedFrom('Part::Feature'):
js_obj['type'] = 'PART' js_obj['type'] = 'PART'
# filter for nonsolids
if is_object_solid(obj) or hasattr(obj, kwargs['property_forse_nonsolid']): js_obj['loc_xyz'] = list(obj.Placement.Base)
# create mesh from shape js_obj['rot_xyzw'] = list(obj.Placement.Rotation.Q)
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!')
t = meshfromshape.Mesh.Topology for attr in dir(obj):
verts = [[v.x, v.y, v.z] for v in t[0]] if 'Robossembler' not in attr:
faces = t[1] continue
js_obj['mesh'] = (verts, faces) js_obj['attributes'].append({attr: getattr(obj, attr)})
# one material for the whole object base_names = [k for k, v in mdct.items() if name in v]
for fem_mat in fem_mats: if base_names:
for ref in fem_mat.References: js_obj['clones'] = {base_names[0]: clones[base_names[0]]}
if ref[0].Label == obj.Label:
js_obj['material'] = fem_mat.Material
# skip for other object's types
else: 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 # construct assembly hierarchy
obj_parent = obj.getParentGeoFeatureGroup() obj_parent = obj.getParentGeoFeatureGroup()
@ -180,6 +264,7 @@ parser.add_argument(
default=True, default=True,
required=False required=False
) )
"""
parser.add_argument( parser.add_argument(
'--property_forse_nonsolid', '--property_forse_nonsolid',
type=str, type=str,
@ -187,6 +272,7 @@ parser.add_argument(
default='Robossembler_NonSolid', default='Robossembler_NonSolid',
required=False required=False
) )
"""
fc_kwargs = vars(parser.parse_known_args()[0]) fc_kwargs = vars(parser.parse_known_args()[0])

View file

@ -31,8 +31,15 @@ Solid tools for FreeCAD's .FCStd scene.
''' '''
__version__ = '0.2' __version__ = '0.2'
import difflib
import glob
import logging import logging
import math
import xml.dom.minidom
import FreeCAD import FreeCAD
import Part
import MeshPart
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) 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) :param doc: document, freecad opened scene (or current scene)
:returns: list with clones sublists [['c0'], ['c1', 'c2', 'c4'],] :returns: list with clones sublists [['c0'], ['c1', 'c2', 'c4'],]
''' '''
doc_clones = [] doc_clones = {}
for item in doc.Objects: for item in doc.Objects:
# for solid objects only
if not is_object_solid(item): if not is_object_solid(item):
continue continue
# collect clones lists
item_clones = [] item_clones = []
item_clones.append(item.Label)
for other in doc.Objects: for other in doc.Objects:
if other == item: if other == item:
continue continue
@ -74,7 +82,146 @@ def collect_clones(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)) -> lis
logger.info('%s and %s objects has equal Shapes', logger.info('%s and %s objects has equal Shapes',
item.Label, other.Label) item.Label, other.Label)
item_clones.append(other.Label) item_clones.append(other.Label)
item_clones.sort() # if item has clones
if item_clones not in doc_clones: if item_clones:
doc_clones.append(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 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