FreeCAD: uber updating function in freecad_to_json WIP
This commit is contained in:
parent
cc538b719f
commit
8401cc51fa
2 changed files with 288 additions and 55 deletions
|
@ -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'
|
||||||
|
elif obj.isDerivedFrom('Part::Feature'):
|
||||||
|
js_obj['type'] = 'PART'
|
||||||
|
|
||||||
|
js_obj['loc_xyz'] = list(obj.Placement.Base)
|
||||||
|
js_obj['rot_xyzw'] = list(obj.Placement.Rotation.Q)
|
||||||
|
|
||||||
for attr in dir(obj):
|
for attr in dir(obj):
|
||||||
if 'Robossembler' not in attr:
|
if 'Robossembler' not in attr:
|
||||||
continue
|
continue
|
||||||
js_obj[attr] = getattr(obj, attr)
|
js_obj['attributes'].append({attr: getattr(obj, attr)})
|
||||||
|
|
||||||
elif obj.isDerivedFrom('Part::Feature'):
|
base_names = [k for k, v in mdct.items() if name in v]
|
||||||
js_obj['type'] = 'PART'
|
if base_names:
|
||||||
|
js_obj['clones'] = {base_names[0]: clones[base_names[0]]}
|
||||||
|
else:
|
||||||
|
js_obj['clones'] = {}
|
||||||
|
|
||||||
|
if obj.isDerivedFrom('Part::Feature'):
|
||||||
# filter for nonsolids
|
# filter for nonsolids
|
||||||
|
forsed_nonsolid = (hasattr(obj, 'Robossembler_NonSolid')
|
||||||
if is_object_solid(obj) or hasattr(obj, kwargs['property_forse_nonsolid']):
|
and getattr(obj, 'Robossembler_NonSolid'))
|
||||||
|
if is_object_solid(obj) or forsed_nonsolid:
|
||||||
# create mesh from shape
|
# create mesh from shape
|
||||||
shape = obj.Shape
|
mesh_from_shape = tesselation(
|
||||||
shape = obj.Shape.copy()
|
doc, obj,
|
||||||
shape.Placement = obj.Placement.inverse().multiply(shape.Placement)
|
kwargs['linear_deflection'], kwargs['angular_deflection'],
|
||||||
meshfromshape = doc.addObject('Mesh::Feature', 'Mesh')
|
kwargs['tesselation_method'], kwargs['fem_size'],
|
||||||
if kwargs['tesselation_method'] == 'Standard':
|
adaptive=not forsed_nonsolid
|
||||||
meshfromshape.Mesh = MeshPart.meshFromShape(
|
)
|
||||||
Shape=shape,
|
# export to stl files
|
||||||
LinearDeflection=kwargs['linear_deflection'],
|
main_file_path = doc.FileName
|
||||||
AngularDeflection=math.radians(kwargs['angular_deflection']),
|
main_file_dir = os.path.dirname(main_file_path)
|
||||||
Relative=False)
|
mesh_dir = os.path.join(main_file_dir, 'mesh')
|
||||||
elif kwargs['tesselation_method'] == 'FEM':
|
if os.path.exists(mesh_dir):
|
||||||
meshfromshape.Mesh = MeshPart.meshFromShape(
|
shutil.rmtree(mesh_dir)
|
||||||
Shape=shape,
|
os.makedirs(mesh_dir)
|
||||||
MaxLength=kwargs['fem_size'])
|
|
||||||
else:
|
else:
|
||||||
raise TypeError('Wrong tesselation method! '
|
os.makedirs(mesh_dir)
|
||||||
'Standard and FEM methods are supported only!')
|
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)
|
||||||
|
|
||||||
t = meshfromshape.Mesh.Topology
|
# find linked material config
|
||||||
verts = [[v.x, v.y, v.z] for v in t[0]]
|
fem_mat_name = [fem_mat.Material['CardName']
|
||||||
faces = t[1]
|
for fem_mat in fem_mats
|
||||||
js_obj['mesh'] = (verts, faces)
|
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
|
||||||
|
|
||||||
# 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
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
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])
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
# 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()
|
item_clones.sort()
|
||||||
if item_clones not in doc_clones:
|
# add only unical list of clones
|
||||||
doc_clones.append(item_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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue