281 lines
8.4 KiB
Python
281 lines
8.4 KiB
Python
# coding: utf-8
|
|
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@yandex.com>
|
|
#
|
|
# This program 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.
|
|
'''
|
|
DESCRIPTION.
|
|
- Reads a FreeCAD .FCStd file.
|
|
- Set tesselation parts to mesh.
|
|
- 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'
|
|
|
|
import collections
|
|
import logging
|
|
import math
|
|
import json
|
|
import shutil
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import FreeCAD
|
|
import Part
|
|
import Mesh
|
|
import MeshPart
|
|
|
|
|
|
from freecad.utils.solid_tools import (is_object_solid,
|
|
collect_clones,
|
|
tesselation,
|
|
config_parser,
|
|
get_material_paths)
|
|
|
|
from utils.custom_parser import CustomArgumentParser
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def freecad_to_json(**kwargs):
|
|
''' Reads a FreeCAD .FCStd file and return json assembly. '''
|
|
|
|
scene = {}
|
|
js_objs = {}
|
|
|
|
doc = FreeCAD.open(kwargs['fcstd_path'])
|
|
docname = doc.Name
|
|
|
|
# collect equal cad objects
|
|
clones = collect_clones(doc):
|
|
|
|
# collect all materials
|
|
fem_mats = [fem_mat for fem_mat in doc.Objects
|
|
if fem_mat.isDerivedFrom('App::MaterialObjectPython')
|
|
]
|
|
material_paths = get_material_paths
|
|
|
|
for obj in doc.Objects:
|
|
|
|
# skip hidden objects
|
|
if not obj.Visibility:
|
|
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'):
|
|
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):
|
|
if 'Robossembler' not in attr:
|
|
continue
|
|
js_obj['attributes'].append({attr: getattr(obj, attr)})
|
|
|
|
base_names = [k for k, v in mdct.items() if name in v]
|
|
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
|
|
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
|
|
|
|
|
|
# construct assembly hierarchy
|
|
obj_parent = obj.getParentGeoFeatureGroup()
|
|
obj_child_name = None
|
|
parents = {}
|
|
deep_index = 0
|
|
while obj_parent:
|
|
parent = {}
|
|
parent['fc_location'] = tuple(obj_parent.Placement.Base)
|
|
parent['fc_rotation'] = obj_parent.Placement.Rotation.Q
|
|
obj_child_name = obj_parent.Label
|
|
obj_parent = obj_parent.getParentGeoFeatureGroup()
|
|
if obj_parent:
|
|
parent['parent'] = obj_parent.Label
|
|
else:
|
|
parent['parent'] = None
|
|
parents[obj_child_name] = parent
|
|
parent['deep_index'] = deep_index
|
|
deep_index += 1
|
|
js_obj['hierarchy'] = parents
|
|
|
|
js_objs[obj.Label] = js_obj
|
|
|
|
FreeCAD.closeDocument(docname)
|
|
|
|
scene[kwargs['fcstd_path']] = js_objs
|
|
|
|
logger.info('Passed %s objects without errors', len(js_objs))
|
|
|
|
print(json.dumps(scene))
|
|
|
|
|
|
# to run script via FreeCADCmd
|
|
parser = CustomArgumentParser()
|
|
parser.add_argument(
|
|
'--fcstd_path',
|
|
type=str,
|
|
help='Path to source FreeCAD scene',
|
|
required=True
|
|
)
|
|
parser.add_argument(
|
|
'--tesselation_method',
|
|
type=str,
|
|
help='Select tesselation method: Standard or FEM.',
|
|
default='Standard',
|
|
required=False
|
|
)
|
|
parser.add_argument(
|
|
'--linear_deflection',
|
|
type=float,
|
|
help='Max linear distance error',
|
|
default=0.1,
|
|
required=False
|
|
)
|
|
parser.add_argument(
|
|
'--angular_deflection',
|
|
type=float,
|
|
help='Max angular distance error',
|
|
default=20.0,
|
|
required=False
|
|
)
|
|
parser.add_argument(
|
|
'--fem_size',
|
|
type=float,
|
|
help='For FEM method only! Finite element size in mm',
|
|
default=50.0,
|
|
required=False
|
|
)
|
|
parser.add_argument(
|
|
'--skiphidden',
|
|
type=bool,
|
|
help='Skip processing for hidden FreeCAD objects',
|
|
default=True,
|
|
required=False
|
|
)
|
|
"""
|
|
parser.add_argument(
|
|
'--property_forse_nonsolid',
|
|
type=str,
|
|
help='FreeCAD property to enable processing for nonsolid objects',
|
|
default='Robossembler_NonSolid',
|
|
required=False
|
|
)
|
|
"""
|
|
|
|
fc_kwargs = vars(parser.parse_known_args()[0])
|
|
|
|
freecad_to_json(**fc_kwargs)
|
|
|
|
logger.info('FreeCAD scene passed!')
|