diff --git a/cg/freecad/utils/__init__.py b/cg/freecad/utils/__init__.py
new file mode 100644
index 0000000..74a52df
--- /dev/null
+++ b/cg/freecad/utils/__init__.py
@@ -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 .
+#
+# ***** END GPL LICENSE BLOCK *****
+#
+# coding: utf-8
+'''
+DESCRIPTION.
+FreeCAD modules for Robosembler project pipeline.
+'''
diff --git a/cg/freecad/utils/freecad_exporters.py b/cg/freecad/utils/freecad_exporters.py
new file mode 100644
index 0000000..926bd8a
--- /dev/null
+++ b/cg/freecad/utils/freecad_exporters.py
@@ -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 .
+#
+# ***** 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"//")
+
+ 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
diff --git a/cg/freecad/utils/freecad_to_json.py b/cg/freecad/utils/freecad_to_json.py
deleted file mode 100644
index ba18f1e..0000000
--- a/cg/freecad/utils/freecad_to_json.py
+++ /dev/null
@@ -1,281 +0,0 @@
-# coding: utf-8
-# Copyright (C) 2023 Ilia Kurochkin
-#
-# 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.
-'''
-
-{
- ".FCStd": {
- "name": "",
- "type": "LOCATOR",
- "loc_xyz": ["x", "y", "z"],
- "rot_xyzw": ["x", "y", "z", "w"],
- "attributes": [],
- "clones": {},
- "mesh_path": "",
- "material": "",
- "children": [
- {
- "name": "",
- "type": "PART",
- "loc_xyz": ["x", "y", "z"],
- "rot_xyzw": ["x", "y", "z", "w"],
- "attributes": [
- "Robossembler_NonSolid": True
- ],
- "clones": {
- "": [
- "",
- ]
- },
- "mesh_path": "/path/to/robossembler/database/mesh/..stl",
- "material": "/path/to/robossembler/materials/Robossembler_ABS-Dark-Rough.FCMat",
- "children": []
- },
- {
- "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!')
diff --git a/cg/freecad/utils/solid_tools.py b/cg/freecad/utils/freecad_tools.py
similarity index 64%
rename from cg/freecad/utils/solid_tools.py
rename to cg/freecad/utils/freecad_tools.py
index a8d5516..ccdc5c9 100644
--- a/cg/freecad/utils/solid_tools.py
+++ b/cg/freecad/utils/freecad_tools.py
@@ -27,14 +27,15 @@
# coding: utf-8
'''
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 glob
import logging
import math
+import os
import xml.dom.minidom
import FreeCAD
@@ -55,6 +56,12 @@ def is_object_solid(obj) -> bool:
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
@@ -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.
- :param doc: document, freecad opened scene (or current scene)
- :returns: list with clones sublists [['c0'], ['c1', 'c2', 'c4'],]
+ :param doc: freecad document by name or current document
+ :returns: {'basename': ['c1', 'c2', 'c4']}
'''
doc_clones = {}
- for item in doc.Objects:
- # for solid objects only
- if not is_object_solid(item):
- continue
+ 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 doc.Objects:
- if other == item:
+ for other in parts:
+ if other == part:
continue
- if other.Shape.isPartner(item.Shape):
- logger.info('%s and %s objects has equal Shapes',
- item.Label, other.Label)
- item_clones.append(other.Label)
- # if item has clones
+ 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(item.Label)
+ item_clones.append(part.Label)
# find common basename for all clones
idx = 0
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]
idx += 1
# 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
item_clones.sort()
# add only unical list of clones
@@ -106,11 +114,13 @@ def collect_clones(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Label)) -> lis
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::
+ adaptive=False,
+ linear_deflection=0.1,
+ angular_deflection=20,
+ tesselation_method='Standard',
+ fem_size=50.0) -> list:
'''
- Perform shape tesselation.
+ Perform part's shape tesselation.
'''
shape = obj.Shape.copy()
shape.Placement = obj.Placement.inverse().multiply(shape.Placement)
@@ -145,9 +155,9 @@ def tesselation(obj,
linear_deflection = linear_deflection / 2
angular_deflection = angular_deflection / 2
doc.removeObject('mesh_from_shape')
- #doc.recompute()
+ doc.recompute()
tesselation(
- obj, doc, linear_deflection, angular_deflection,
+ obj, doc, adaptive, linear_deflection, angular_deflection,
tesselation_method, fem_size)
return mesh_from_shape
@@ -225,3 +235,69 @@ def get_material_paths() -> list:
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': '',
+ '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