FreeCAD: rewrite freecad module as freecad publish plugin
This commit is contained in:
parent
8401cc51fa
commit
b80db52ea5
4 changed files with 357 additions and 304 deletions
31
cg/freecad/utils/__init__.py
Normal file
31
cg/freecad/utils/__init__.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# ***** 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.
|
||||||
|
FreeCAD modules for Robosembler project pipeline.
|
||||||
|
'''
|
227
cg/freecad/utils/freecad_exporters.py
Normal file
227
cg/freecad/utils/freecad_exporters.py
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
# ***** 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.
|
||||||
|
FreeCAD's tools for publishing .FCStd scene.
|
||||||
|
'''
|
||||||
|
|
||||||
|
__version__ = '0.3'
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import FreeCAD
|
||||||
|
|
||||||
|
import freecad_tools
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
def export_assembly_trees(doc, clones_dic=None) -> list:
|
||||||
|
''' Read FreeCAD .FCStd hierarchy and store it to assembly JSON config files. '''
|
||||||
|
# determine root locators
|
||||||
|
lcs_root_points = []
|
||||||
|
for obj in doc.Objects:
|
||||||
|
if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
|
||||||
|
if (hasattr(obj, 'Robossembler_DefaultOrigin')
|
||||||
|
and getattr(obj, 'Robossembler_DefaultOrigin')):
|
||||||
|
lcs_root_points.append(obj)
|
||||||
|
if len(lcs_root_points) > 1:
|
||||||
|
root_locators = []
|
||||||
|
for lcs in lcs_root_points:
|
||||||
|
if hasattr(lcs, 'Robossembler_RootLocator'):
|
||||||
|
root_locators.append(getattr(lcs, 'Robossembler_DefaultOrigin'))
|
||||||
|
else:
|
||||||
|
logger.warning('RootLocator attribute not found for %s LCS! '
|
||||||
|
'So %s used as Root Locator!',
|
||||||
|
lcs.Label, lcs.InList[0].Label)
|
||||||
|
root_locators.append(lcs.InList[0].Label)
|
||||||
|
else:
|
||||||
|
root_locators = [
|
||||||
|
root for root in doc.Objects
|
||||||
|
if not root.InList
|
||||||
|
if root .isDerivedFrom('App::Part')]
|
||||||
|
|
||||||
|
config_files = []
|
||||||
|
for root_locator in root_locators:
|
||||||
|
dict_tree = {}
|
||||||
|
freecad_tools.hierarchy_tree(root_locator, dict_tree, clones_dic)
|
||||||
|
|
||||||
|
# write file
|
||||||
|
main_file_dir = os.path.dirname(doc.FileName)
|
||||||
|
assembly_tree_path = os.path.join(main_file_dir, f'{root_locator.Label}.json')
|
||||||
|
with open(assembly_tree_path, 'w', encoding='utf-8') as json_file:
|
||||||
|
json.dump(dict_tree, json_file, ensure_ascii=False, indent=4)
|
||||||
|
logger.info('Assembly tree saved successfully to %s!', assembly_tree_path)
|
||||||
|
config_files.append(assembly_tree_path)
|
||||||
|
logger.info('Saved %s assembly trees!', len(config_files))
|
||||||
|
|
||||||
|
return config_files
|
||||||
|
|
||||||
|
|
||||||
|
def export_parts_database(
|
||||||
|
doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label),
|
||||||
|
clones_dic=None, **tesselation_params):
|
||||||
|
'''
|
||||||
|
Collect parts database and export as JSON config file
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'type': '',
|
||||||
|
'name': '',
|
||||||
|
'attributes': [],
|
||||||
|
'part_path': '',
|
||||||
|
'material_path': ''
|
||||||
|
},
|
||||||
|
{...},
|
||||||
|
]
|
||||||
|
'''
|
||||||
|
# path directory
|
||||||
|
main_file_dir = os.path.dirname(doc.FileName)
|
||||||
|
parts_dir = os.path.join(main_file_dir, 'parts')
|
||||||
|
if os.path.exists(parts_dir):
|
||||||
|
shutil.rmtree(parts_dir)
|
||||||
|
os.makedirs(parts_dir)
|
||||||
|
else:
|
||||||
|
os.makedirs(parts_dir)
|
||||||
|
|
||||||
|
# collect all materials
|
||||||
|
fem_mats = [fem_mat for fem_mat in doc.Objects
|
||||||
|
if fem_mat.isDerivedFrom('App::MaterialObjectPython')]
|
||||||
|
material_paths = freecad_tools.get_material_paths()
|
||||||
|
|
||||||
|
parts_db = []
|
||||||
|
for obj in doc.Objects:
|
||||||
|
|
||||||
|
# skip hidden objects
|
||||||
|
if not obj.Visibility:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# skip non part objects
|
||||||
|
if not obj.isDerivedFrom('Part::Feature'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# skip lcs objects
|
||||||
|
if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# skip non solid part objects
|
||||||
|
forsed_nonsolid = (
|
||||||
|
hasattr(obj, 'Robossembler_NonSolid')
|
||||||
|
and getattr(obj, 'Robossembler_NonSolid'))
|
||||||
|
if not freecad_tools.is_object_solid(obj) or forsed_nonsolid:
|
||||||
|
logger.warning('Part has non solid shape! Please check %s', obj.Label)
|
||||||
|
continue
|
||||||
|
|
||||||
|
db_obj = {'type': 'PART'}
|
||||||
|
|
||||||
|
db_obj['name'] = obj.Label
|
||||||
|
if clones_dic:
|
||||||
|
for k, v in clones_dic.items():
|
||||||
|
if obj.Label in v:
|
||||||
|
db_obj['name'] = k
|
||||||
|
|
||||||
|
robossembler_attrs = [attr for attr in dir(obj) if 'Robossembler' in attr]
|
||||||
|
if robossembler_attrs:
|
||||||
|
db_obj['attributes'] = []
|
||||||
|
for attr in robossembler_attrs:
|
||||||
|
db_obj['attributes'].append({attr: getattr(obj, attr)})
|
||||||
|
|
||||||
|
part_path = os.path.join(parts_dir, db_obj['name'] + '.stl')
|
||||||
|
if os.path.exists(part_path):
|
||||||
|
# this is clone
|
||||||
|
continue
|
||||||
|
mesh_from_shape = freecad_tools.tesselation(
|
||||||
|
obj, doc,
|
||||||
|
# TODO adaptive=(not forsed_nonsolid),
|
||||||
|
adaptive=False,
|
||||||
|
**tesselation_params
|
||||||
|
)
|
||||||
|
# export to stl files
|
||||||
|
#Mesh.export([mesh_from_shape], part_path, tolerance=linear_deflection)
|
||||||
|
mesh_from_shape.Mesh.write(part_path)
|
||||||
|
db_obj['part_path'] = os.path.relpath(part_path, main_file_dir)
|
||||||
|
logger.info('Part %s exported to stl file %s.', obj.Label, part_path)
|
||||||
|
|
||||||
|
# find linked material path
|
||||||
|
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:
|
||||||
|
db_obj['material_path'] = material_path
|
||||||
|
|
||||||
|
# append to database
|
||||||
|
parts_db.append(db_obj)
|
||||||
|
|
||||||
|
logger.info('Passed %s parts without errors', len(parts_db))
|
||||||
|
parts_db_path = os.path.join(main_file_dir, f'{FreeCAD.ActiveDocument.Label}.FCStd.json')
|
||||||
|
with open(parts_db_path, 'w', encoding='utf-8') as json_file:
|
||||||
|
json.dump(parts_db, json_file, ensure_ascii=False, indent=4)
|
||||||
|
logger.info('Parts Database exported successfully to %s!', parts_db_path)
|
||||||
|
|
||||||
|
return parts_db_path
|
||||||
|
|
||||||
|
|
||||||
|
def publish_project_database(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)):
|
||||||
|
'''
|
||||||
|
Publish FCStd document to CG pipeline:
|
||||||
|
- Exetute openned FreeCAD document.
|
||||||
|
- Save FCStd document to database root path.
|
||||||
|
- Export all assembly trees with LCS DefaultOrigin,
|
||||||
|
or single one as JSON hierarchy tree.
|
||||||
|
- Set tesselate solid parts to mesh.
|
||||||
|
- Export all tesselated parts to STL files.
|
||||||
|
- Return document as JSON dictionary.
|
||||||
|
'''
|
||||||
|
tesselation_params = {
|
||||||
|
# Select tesselation method: Standard or FEM.
|
||||||
|
'tesselation_method': 'Standard',
|
||||||
|
# Max linear distance error
|
||||||
|
'linear_deflection': 0.1,
|
||||||
|
# Max angular distance error
|
||||||
|
'angular_deflection': 20.0,
|
||||||
|
# For FEM method only! Finite element size in mm
|
||||||
|
'fem_size': 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO Save FCStd project file
|
||||||
|
#doc.saveAs(u"/<file_dir>/<file_name>")
|
||||||
|
|
||||||
|
clones_dic = freecad_tools.collect_clones(doc)
|
||||||
|
|
||||||
|
export_assembly_trees(doc, clones_dic)
|
||||||
|
export_parts_database(doc, clones_dic, **tesselation_params)
|
||||||
|
|
||||||
|
logger.info('FreeCAD document %s published!', doc.Label)
|
||||||
|
|
||||||
|
return doc.Label
|
|
@ -1,281 +0,0 @@
|
||||||
# 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!')
|
|
|
@ -27,14 +27,15 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
'''
|
'''
|
||||||
DESCRIPTION.
|
DESCRIPTION.
|
||||||
Solid tools for FreeCAD's .FCStd scene.
|
Various FreeCAD's analise, tesselation and parsing tools.
|
||||||
'''
|
'''
|
||||||
__version__ = '0.2'
|
__version__ = '0.5'
|
||||||
|
|
||||||
import difflib
|
import difflib
|
||||||
import glob
|
import glob
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
|
|
||||||
import FreeCAD
|
import FreeCAD
|
||||||
|
@ -55,6 +56,12 @@ def is_object_solid(obj) -> bool:
|
||||||
if not isinstance(obj, FreeCAD.DocumentObject):
|
if not isinstance(obj, FreeCAD.DocumentObject):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not obj.isDerivedFrom('Part::Feature'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
|
||||||
|
return False
|
||||||
|
|
||||||
if not hasattr(obj, 'Shape'):
|
if not hasattr(obj, 'Shape'):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -65,26 +72,27 @@ def collect_clones(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)) -> lis
|
||||||
'''
|
'''
|
||||||
This script find equal cad parts in a FreeCAD .FCStd scene.
|
This script find equal cad parts in a FreeCAD .FCStd scene.
|
||||||
|
|
||||||
:param doc: document, freecad opened scene (or current scene)
|
:param doc: freecad document by name or current document
|
||||||
:returns: list with clones sublists [['c0'], ['c1', 'c2', 'c4'],]
|
:returns: {'basename': ['c1', 'c2', 'c4']}
|
||||||
'''
|
'''
|
||||||
doc_clones = {}
|
doc_clones = {}
|
||||||
for item in doc.Objects:
|
parts = [item for item in doc.Objects if is_object_solid(item) if item.Visibility]
|
||||||
# for solid objects only
|
for part in parts:
|
||||||
if not is_object_solid(item):
|
|
||||||
continue
|
|
||||||
# collect clones lists
|
# collect clones lists
|
||||||
item_clones = []
|
item_clones = []
|
||||||
for other in doc.Objects:
|
for other in parts:
|
||||||
if other == item:
|
if other == part:
|
||||||
continue
|
continue
|
||||||
if other.Shape.isPartner(item.Shape):
|
if (len(other.Shape.Vertexes) == len(part.Shape.Vertexes)
|
||||||
logger.info('%s and %s objects has equal Shapes',
|
and len(other.Shape.Edges) == len(part.Shape.Edges)
|
||||||
item.Label, other.Label)
|
and len(other.Shape.Faces) == len(part.Shape.Faces)):
|
||||||
item_clones.append(other.Label)
|
if round(other.Shape.Volume, 7) == round(part.Shape.Volume, 7):
|
||||||
# if item has clones
|
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:
|
if item_clones:
|
||||||
item_clones.append(item.Label)
|
item_clones.append(part.Label)
|
||||||
# find common basename for all clones
|
# find common basename for all clones
|
||||||
idx = 0
|
idx = 0
|
||||||
common_name = item_clones[0].lower()
|
common_name = item_clones[0].lower()
|
||||||
|
@ -95,7 +103,7 @@ def collect_clones(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)) -> lis
|
||||||
common_name = common_name[match_data.a:match_data.a + match_data.size]
|
common_name = common_name[match_data.a:match_data.a + match_data.size]
|
||||||
idx += 1
|
idx += 1
|
||||||
# if names has or hasn't common patterns
|
# if names has or hasn't common patterns
|
||||||
item_base_name = common_name.strip(' .,_') or item_clones[0]
|
item_base_name = common_name.strip(' .,_-') or item_clones[0]
|
||||||
# sort list names
|
# sort list names
|
||||||
item_clones.sort()
|
item_clones.sort()
|
||||||
# add only unical list of clones
|
# add only unical list of clones
|
||||||
|
@ -106,11 +114,13 @@ def collect_clones(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)) -> lis
|
||||||
|
|
||||||
def tesselation(obj,
|
def tesselation(obj,
|
||||||
doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label),
|
doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label),
|
||||||
linear_deflection=0.1, angular_deflection=20,
|
adaptive=False,
|
||||||
tesselation_method='Standard', fem_size=50.0,
|
linear_deflection=0.1,
|
||||||
adaptive=True) -> list::
|
angular_deflection=20,
|
||||||
|
tesselation_method='Standard',
|
||||||
|
fem_size=50.0) -> list:
|
||||||
'''
|
'''
|
||||||
Perform shape tesselation.
|
Perform part's shape tesselation.
|
||||||
'''
|
'''
|
||||||
shape = obj.Shape.copy()
|
shape = obj.Shape.copy()
|
||||||
shape.Placement = obj.Placement.inverse().multiply(shape.Placement)
|
shape.Placement = obj.Placement.inverse().multiply(shape.Placement)
|
||||||
|
@ -145,9 +155,9 @@ def tesselation(obj,
|
||||||
linear_deflection = linear_deflection / 2
|
linear_deflection = linear_deflection / 2
|
||||||
angular_deflection = angular_deflection / 2
|
angular_deflection = angular_deflection / 2
|
||||||
doc.removeObject('mesh_from_shape')
|
doc.removeObject('mesh_from_shape')
|
||||||
#doc.recompute()
|
doc.recompute()
|
||||||
tesselation(
|
tesselation(
|
||||||
obj, doc, linear_deflection, angular_deflection,
|
obj, doc, adaptive, linear_deflection, angular_deflection,
|
||||||
tesselation_method, fem_size)
|
tesselation_method, fem_size)
|
||||||
|
|
||||||
return mesh_from_shape
|
return mesh_from_shape
|
||||||
|
@ -225,3 +235,69 @@ def get_material_paths() -> list:
|
||||||
if item.endswith('.FCMat')]
|
if item.endswith('.FCMat')]
|
||||||
)
|
)
|
||||||
return material_paths
|
return material_paths
|
||||||
|
|
||||||
|
|
||||||
|
def hierarchy_tree(obj, dict_tree, clones_dic=None) -> dict:
|
||||||
|
'''
|
||||||
|
Collect hierarchy tree as dict dict_tree
|
||||||
|
{
|
||||||
|
'type': '',
|
||||||
|
'name': '',
|
||||||
|
'base_name': '',
|
||||||
|
'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
|
||||||
|
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['loc_xyz'] = list(obj.Placement.Base)
|
||||||
|
dict_tree['rot_xyzw'] = list(obj.Placement.Rotation.Q)
|
||||||
|
# collect attributes
|
||||||
|
robossembler_attrs = [attr for attr in dir(obj) if 'Robossembler' in attr]
|
||||||
|
if robossembler_attrs:
|
||||||
|
dict_tree['attributes'] = []
|
||||||
|
for attr in robossembler_attrs:
|
||||||
|
dict_tree['attributes'].append({attr: getattr(obj, attr)})
|
||||||
|
# collect children
|
||||||
|
if obj.OutList:
|
||||||
|
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
|
Loading…
Add table
Add a link
Reference in a new issue