diff --git a/cg/blender/import_fcstd/LICENSE b/cg/blender/import_cad/LICENSE similarity index 100% rename from cg/blender/import_fcstd/LICENSE rename to cg/blender/import_cad/LICENSE diff --git a/cg/blender/import_fcstd/README.md b/cg/blender/import_cad/README.md similarity index 66% rename from cg/blender/import_fcstd/README.md rename to cg/blender/import_cad/README.md index 9a6d195..eb5b5b6 100644 --- a/cg/blender/import_fcstd/README.md +++ b/cg/blender/import_cad/README.md @@ -1,9 +1,8 @@ -## import-fcstd -### Импорт .FCStd сцены FreeCAD в Blender. +## import_cad +### Импорт CAD сцены в Blender. -Представлен в качестве модуля `import_cad_objects` c подмодулями. -По умолчанию, импортирует из FreeCAD cцены все видимые solid объекты в Blender сцену. -Задает имена mesh на основе solid объектов. +Восстанавливает из JSON словаря Bledner сцену сохраняя иерархию и свойства объектов. +Задает имена mesh объектов на основе solid объектов. Импортирует локальные координаты и задает их mesh объектам. Импортирует FEM материалы (если они есть) и задает их mesh объектам. Может работать как в качестве модуля, так и внутри blender сцены. diff --git a/cg/blender/import_fcstd/__init__.py b/cg/blender/import_cad/__init__.py similarity index 100% rename from cg/blender/import_fcstd/__init__.py rename to cg/blender/import_cad/__init__.py diff --git a/cg/blender/import_cad/build_blender_scene.py b/cg/blender/import_cad/build_blender_scene.py new file mode 100644 index 0000000..9359056 --- /dev/null +++ b/cg/blender/import_cad/build_blender_scene.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Original code by (C) 2019 yorikvanhavre +# 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. +- Build Blender scene from JSON data. +- Setup hierarchy. +- Setup materials. +- Setup LCS points. +- Apply Bledner scene transforms. +''' +__version__ = '0.1' + +import logging +import random +import bpy +from blender.utils.object_transforms import apply_transforms +from blender.import_cad.import_hierarchy import (fc_placement, + hierarchy) +from blender.import_cad.import_materials import (assign_materials, + assign_black) + + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +# COLLECTIONS NAMIG CONVENTION +part_col_name = 'Part' +lcs_col_name = 'LCS' +hierarchy_col_name = 'Hierarchy' +lowpoly_col_name = 'Lowpoly Parts' +# LCS POINT'S SUFFIXES CONVENTION +inlet = '_in' +outlet = '_out' +root = '_root' +# CG ASSETS SUFFIXES CONVENTION +hightpoly = '_hp' +lowpoly = '_lp' +render = '_render' + +scene_scale = 0.001 +blackbody_mat_name = 'Robossembler_Black_Body' + + +def json_to_blend(js_data): + ''' Reads JSON data and creates Blender scene ''' + + part_collection = bpy.data.collections.new(part_col_name) + bpy.context.scene.collection.children.link(part_collection) + + lcs_collection = bpy.data.collections.new(lcs_col_name) + bpy.context.scene.collection.children.link(lcs_collection) + + hierarchy_collection = bpy.data.collections.new(hierarchy_col_name) + bpy.context.scene.collection.children.link(hierarchy_collection) + + fc_file = list(js_data.keys())[0] + + bobjs = [] + bobjs_for_render = [] + + for js_obj in js_data[fc_file]: + bobj = None + + if js_data[fc_file][js_obj]['type'] == 'LCS': + bobj = bpy.data.objects.new(js_obj, None) + bobj.empty_display_type = 'ARROWS' + bobj.empty_display_size = round(random.uniform(0.05, 0.15), 3) + bobj.show_in_front = True + lcs_collection.objects.link(bobj) + + elif js_data[fc_file][js_obj]['type'] == 'PART': + if js_data[fc_file][js_obj].get('mesh'): + verts = js_data[fc_file][js_obj]['mesh'][0] + edges = [] + faces = js_data[fc_file][js_obj]['mesh'][1] + + # create blender object data + bmesh = bpy.data.meshes.new(name=js_obj) + bmesh.from_pydata(verts, edges, faces) + bmesh.update() + bobj = bpy.data.objects.new(js_obj, bmesh) + part_collection.objects.link(bobj) + else: + logger.info('%s has not mesh data!', js_obj) + + if bobj: + fc_placement(bobj, + js_data[fc_file][js_obj]['fc_location'], + js_data[fc_file][js_obj]['fc_rotation'], + scene_scale) + if bobj.type == 'MESH': + bobj.scale = (scene_scale, scene_scale, scene_scale) + apply_transforms(bobj, scale=True) + + # construct assembly hierarchy + hierarchy_objs = hierarchy(bobj, + js_data[fc_file][js_obj]['hierarchy'], + scene_scale) + for hierarchy_obj in hierarchy_objs: + hierarchy_collection.objects.link(hierarchy_obj) + + # one material for the whole object + if bobj.type == 'MESH': + if js_data[fc_file][js_obj].get('material'): + fem_mat = js_data[fc_file][js_obj]['material'] + assign_materials(bobj, fem_mat) + bobjs_for_render.append(bobj) + else: + assign_black(bobj) + + bobjs.append(bobj) + + # losted root lcs inlet workaround + lcs_objects = lcs_collection.objects + if lcs_objects: + root_lcs = [lcs for lcs in lcs_objects if lcs.name.endswith(root)] + if root_lcs: + root_lcs = root_lcs[0] + root_inlet_name = '{}{}'.format(root_lcs.name.split(root)[0], inlet) + if not bpy.data.objects.get(root_inlet_name): + root_inlet = bpy.data.objects.new(root_inlet_name, None) + root_inlet.empty_display_type = 'ARROWS' + root_inlet.empty_display_size = 0.1 + root_inlet.show_in_front = True + root_inlet.location = root_lcs.location + root_inlet.rotation_euler = root_lcs.rotation_euler + root_inlet.parent = root_lcs.parent + lcs_collection.objects.link(root_inlet) + else: + logger.info('Lost root LCS object!') + + # TODO + # update do not dork + logger.info('Imported %s objects without errors', len(bobjs)) + return bobjs_for_render diff --git a/cg/blender/import_fcstd/import_coordinate_point.py b/cg/blender/import_cad/import_coordinate_point.py similarity index 99% rename from cg/blender/import_fcstd/import_coordinate_point.py rename to cg/blender/import_cad/import_coordinate_point.py index 463180a..8d51693 100644 --- a/cg/blender/import_fcstd/import_coordinate_point.py +++ b/cg/blender/import_cad/import_coordinate_point.py @@ -14,6 +14,9 @@ ''' DESCRIPTION. Import from json exported FreeCAD's asm4 coordinates as Blender's empty object. + +DEPRECATED + ''' __version__ = '0.2' diff --git a/cg/blender/import_cad/import_hierarchy.py b/cg/blender/import_cad/import_hierarchy.py new file mode 100644 index 0000000..11985a9 --- /dev/null +++ b/cg/blender/import_cad/import_hierarchy.py @@ -0,0 +1,66 @@ +# -*- 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. +Collecting all parents and reconstruct this hierarhy in bledner. +''' +__version__ = '0.3' +import logging +import bpy +from mathutils import Vector + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def fc_placement(bobj, fc_location, fc_rotation, scene_scale): + ''' Prepare FreeCAD's Placement and Quaternion for Blender ''' + bobj.location = Vector(fc_location) * scene_scale + m = bobj.rotation_mode + bobj.rotation_mode = 'QUATERNION' + # FreeCAD Quaternion is XYZW while Blender is WXYZ + fc_rotation.insert(0, fc_rotation.pop(3)) + bobj.rotation_quaternion = (fc_rotation) + bobj.rotation_mode = m + return bobj + + +def hierarchy(bobj, hierarchy, scene_scale): + ''' Blender object, dict, Blender World Scale factor. ''' + hierarchy_objs = [] + for parent_name in hierarchy.keys(): + if bpy.data.objects.get(parent_name): + empty = bpy.data.objects[parent_name] + else: + empty = bpy.data.objects.new(parent_name, None) + empty.empty_display_type = 'CUBE' + empty.empty_display_size = 0.01 + fc_placement(empty, + hierarchy[parent_name]['fc_location'], + hierarchy[parent_name]['fc_rotation'], + scene_scale) + empty.select_set(False) + hierarchy_objs.append(empty) + + if hierarchy[parent_name]['deep_index'] == 0: + bobj.parent = empty + + logger.debug('Add parent %s to object %s', bobj.parent.name, bobj.name) + + for parent_name in hierarchy.keys(): + parent_parenta_name = hierarchy[parent_name]['parent'] + if parent_parenta_name: + bpy.data.objects[parent_name].parent = bpy.data.objects[ + parent_parenta_name] + + return hierarchy_objs diff --git a/cg/blender/import_fcstd/import_materials.py b/cg/blender/import_cad/import_materials.py similarity index 67% rename from cg/blender/import_fcstd/import_materials.py rename to cg/blender/import_cad/import_materials.py index 08d6c48..dc957ad 100644 --- a/cg/blender/import_fcstd/import_materials.py +++ b/cg/blender/import_cad/import_materials.py @@ -10,20 +10,21 @@ # 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. -__version__ = '0.2' +__version__ = '0.3' import logging -import sys import bpy from bpy_extras.node_shader_utils import PrincipledBSDFWrapper -from utils.shininess_to_roughness import shiny_to_rough +from blender.utils.shininess_to_roughness import shiny_to_rough logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) +blackbody_mat_name = 'Robossembler_Black_Body' + def assign_materials(bobj, fem_mat): ''' Build Blender shader from FreeCAD's FEM material ''' - fem_mat_name = fem_mat.Material['Name'] + fem_mat_name = fem_mat['Name'] if fem_mat_name in bpy.data.materials: # prepare for reimport @@ -32,37 +33,35 @@ def assign_materials(bobj, fem_mat): else: bobj.material_slots[0].material = bpy.data.materials[fem_mat_name] else: - if 'DiffuseColor' in fem_mat.Material.keys(): - d_col_str = fem_mat.Material['DiffuseColor'] - d_col4 = tuple( - map(float, d_col_str[1:-1].split(', '))) + if 'DiffuseColor' in fem_mat.keys(): + d_col_str = fem_mat['DiffuseColor'] + d_col4 = tuple(map(float, d_col_str[1:-1].split(', '))) d_col = d_col4[:-1] else: d_col = (0.5, 0.5, 0.5) - if 'Father' in fem_mat.Material.keys(): - if fem_mat.Material['Father'] == 'Metal': + if 'Father' in fem_mat.keys(): + if fem_mat['Father'] == 'Metal': me = 1 else: me = 0 else: me = 0 - if 'Shininess' in fem_mat.Material.keys(): - shiny = float(fem_mat.Material['Shininess']) + if 'Shininess' in fem_mat.keys(): + shiny = float(fem_mat['Shininess']) if shiny == 0: rg = 0.5 else: rg = shiny_to_rough(shiny) else: rg = 0.5 - if 'EmissiveColor' in fem_mat.Material.keys(): - e_col_str = fem_mat.Material['EmissiveColor'] - e_col4 = tuple( - map(float, e_col_str[1:-1].split(', '))) + if 'EmissiveColor' in fem_mat.keys(): + e_col_str = fem_mat['EmissiveColor'] + e_col4 = tuple(map(float, e_col_str[1:-1].split(', '))) e_col = e_col4[:-1] else: e_col = (0.0, 0.0, 0.0) - if 'Transparency' in fem_mat.Material.keys(): - tr_str = fem_mat.Material['Transparency'] + if 'Transparency' in fem_mat.keys(): + tr_str = fem_mat['Transparency'] alpha = 1.0 - float(tr_str) else: alpha = 1.0 @@ -86,11 +85,11 @@ def assign_materials(bobj, fem_mat): def assign_black(bobj): - ''' Set absolute black Blender shader ''' - fem_mat_name = 'black_mat' + ''' Set absolute black body shader ''' + fem_mat_name = blackbody_mat_name if fem_mat_name in bpy.data.materials: - # prepare for reimport + # prepare for reimport TODO if len(bobj.material_slots) < 1: bobj.data.materials.append(bpy.data.materials[fem_mat_name]) else: @@ -99,14 +98,10 @@ def assign_black(bobj): bmat = bpy.data.materials.new(name=fem_mat_name) bmat.use_nodes = True bmat.diffuse_color = (0, 0, 0, 1) - bmat.node_tree.nodes.remove(bmat.node_tree.nodes['Principled BSDF']) - emission = bmat.node_tree.nodes.new(type='ShaderNodeEmission') - emission.location = 0, 300 - emission.inputs['Color'].default_value = (0, 0, 0, 1) - emission.inputs['Strength'].default_value = 0 - bmat.node_tree.links.new( - emission.outputs['Emission'], - bmat.node_tree.nodes['Material Output'].inputs['Surface']) + principled = bmat.node_tree.nodes['Principled BSDF'] + principled.inputs['Base Color'].default_value = (0, 0, 0, 1) + principled.inputs['Specular'].default_value = 0.0 + principled.inputs['Roughness'].default_value = 1.0 # prepare for reimport if len(bobj.material_slots) < 1: bobj.data.materials.append(bmat) diff --git a/cg/blender/import_fcstd/restruct_hierarchy_by_lcs.py b/cg/blender/import_cad/restruct_hierarchy_by_lcs.py similarity index 96% rename from cg/blender/import_fcstd/restruct_hierarchy_by_lcs.py rename to cg/blender/import_cad/restruct_hierarchy_by_lcs.py index d641c75..eb5e819 100644 --- a/cg/blender/import_fcstd/restruct_hierarchy_by_lcs.py +++ b/cg/blender/import_cad/restruct_hierarchy_by_lcs.py @@ -22,9 +22,9 @@ import math import bpy from mathutils import Matrix -from utils.object_relations import (parenting, - unparenting) -from utils.object_transforms import round_transforms +from blender.utils.object_relations import (parenting, + unparenting) +from blender.utils.object_transforms import round_transforms logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) diff --git a/cg/blender/import_fcstd/import_cad_objects.py b/cg/blender/import_fcstd/import_cad_objects.py deleted file mode 100644 index 49845ff..0000000 --- a/cg/blender/import_fcstd/import_cad_objects.py +++ /dev/null @@ -1,217 +0,0 @@ -# -*- coding: utf-8 -*- -# Original code by (C) 2019 yorikvanhavre -# 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. -Main module: -- Reads a FreeCAD .FCStd file. -- Set tesselation parts to mesh. -- Inport meshes in Blender scene. -- Setup hierarchy. -- Setup materials. -- Setup LCS points. -- Apply FreeCAD to Bledner scene transforms. -''' -__version__ = '0.3' -import time -import FreeCAD -import Part, Mesh, MeshPart -import logging -import math -import xml -import sys -import xml.sax -import zipfile -import os -import random -import bpy -from import_fcstd.import_hierarchy import (hierarchy, - placement) -from import_fcstd.import_materials import (assign_materials, - assign_black) -from import_fcstd.is_object_solid import is_object_solid -from utils.object_transforms import apply_transforms -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -# COLLECTIONS NAMIG CONVENTION -parts_col_name = 'Import Parts' -lcs_col_name = 'Import LCS' -hierarchy_col_name = 'Import Hierarchy' -lowpoly_col_name = 'Lowpoly Parts' -# LCS POINT'S SUFFIXES CONVENTION -inlet = '_in' -outlet = '_out' -root = '_root' -# CG ASSETS SUFFIXES CONVENTION -hightpoly = '_hp' -lowpoly = '_lp' -render = '_render' - - -def obj_importer(filename, - tesselation_method='Standard', - linear_deflection=0.1, - angular_deflection=30.0, - fem_size=5.0, - update=False, - scene_placement=True, - skiphidden=True, - scale=0.001, - select=True, - nonsolid_property='Robossembler_NonSolid'): - - ''' Reads a FreeCAD .FCStd file and creates Blender objects ''' - - doc = FreeCAD.open(filename) - docname = doc.Name - - if not update: - parts_collection = bpy.data.collections.new(parts_col_name) - bpy.context.scene.collection.children.link(parts_collection) - - lcs_collection = bpy.data.collections.new(lcs_col_name) - bpy.context.scene.collection.children.link(lcs_collection) - - hierarchy_collection = bpy.data.collections.new(hierarchy_col_name) - bpy.context.scene.collection.children.link(hierarchy_collection) - - # collect all materials - fem_mats = [] - for fem_mat in doc.Objects: - if fem_mat.isDerivedFrom('App::MaterialObjectPython'): - fem_mats.append(fem_mat) - bobjs = [] - bobjs_for_render = [] - for obj in doc.Objects: - bobj = None - if skiphidden: - if not obj.Visibility: - continue - - if obj.isDerivedFrom('PartDesign::CoordinateSystem'): - if update: - # locate existing object - for o in bpy.data.objects: - if o.name == obj.Label: - bobj = o - logger.debug('Replacing existing %s', obj.Label) - else: - bobj = bpy.data.objects.new(obj.Label, None) - bobj.empty_display_type = 'ARROWS' - bobj.empty_display_size = round(random.uniform(0.05, 0.15), 3) - bobj.show_in_front = True - lcs_collection.objects.link(bobj) - - elif obj.isDerivedFrom('Part::Feature'): - # filter for nonsolids - if is_object_solid(obj) or hasattr(obj, nonsolid_property): - verts = [] - edges = [] - faces = [] - # create mesh from shape - shape = obj.Shape - if scene_placement: - shape = obj.Shape.copy() - shape.Placement = obj.Placement.inverse().multiply(shape.Placement) - meshfromshape = doc.addObject('Mesh::Feature','Mesh') - if tesselation_method == 'Standard': - meshfromshape.Mesh = MeshPart.meshFromShape( - Shape=shape, - LinearDeflection=linear_deflection, - AngularDeflection=math.radians(angular_deflection), - Relative=False) - elif tesselation_method == 'FEM': - meshfromshape.Mesh = MeshPart.meshFromShape( - Shape=shape, - MaxLength=fem_size) - else: - raise TypeError('Wrong tesselation method! ' - 'Standard and FEM methods are supported only!') - break - t = meshfromshape.Mesh.Topology - verts = [[v.x,v.y,v.z] for v in t[0]] - faces = t[1] - - if verts and faces: - # create or update object with mesh and material data - bmesh = bpy.data.meshes.new(name=obj.Label) - bmesh.from_pydata(verts, edges, faces) - bmesh.update() - if update: - # locate existing object - for o in bpy.data.objects: - if o.name == obj.Label: - bobj = o - bobj.data = bmesh - logger.debug('Replacing existing %s', obj.Label) - else: - bobj = bpy.data.objects.new(obj.Label, bmesh) - parts_collection.objects.link(bobj) - - # skip for other object's types - else: - continue - - if bobj and scene_placement and not update: - placement(bobj, obj, scale) - if bobj.type == 'MESH': - bobj.scale = (scale, scale, scale) - apply_transforms(bobj, scale=True) - - # construct assembly hierarchy - hierarchy_objs = hierarchy(bobj, obj, scale) - for hierarchy_obj in hierarchy_objs: - hierarchy_collection.objects.link(hierarchy_obj) - - # one material for the whole object - if bobj.type == 'MESH': - for fem_mat in fem_mats: - for ref in fem_mat.References: - if ref[0].Label == bobj.name: - assign_materials(bobj, fem_mat) - bobjs_for_render.append(bobj) - continue - # looks like this is hidden internal object - if not bobj.material_slots: - assign_black(bobj) - - # optional select object after importing - if bobj and select: - bpy.context.view_layer.objects.active = bobj - bobj.select_set(True) - - bobjs.append(bobj) - - # losted root lcs inlet workaround - lcs_objects = lcs_collection.objects - if lcs_objects: - root_lcs = [lcs for lcs in lcs_objects if lcs.name.endswith(root)][0] - root_inlet_name = ('{}{}'.format(root_lcs.name.split(root)[0], inlet)) - if not bpy.data.objects.get(root_inlet_name): - root_inlet = bpy.data.objects.new(root_inlet_name, None) - root_inlet.empty_display_type = 'ARROWS' - root_inlet.empty_display_size = 0.1 - root_inlet.show_in_front = True - root_inlet.location = root_lcs.location - root_inlet.rotation_euler = root_lcs.rotation_euler - root_inlet.parent = root_lcs.parent - lcs_collection.objects.link(root_inlet) - - FreeCAD.closeDocument(docname) - - # TODO - # update do not dork - logger.info('Imported %s objects without errors', len(bobjs)) - return bobjs_for_render diff --git a/cg/blender/import_fcstd/import_hierarchy.py b/cg/blender/import_fcstd/import_hierarchy.py deleted file mode 100644 index 1879773..0000000 --- a/cg/blender/import_fcstd/import_hierarchy.py +++ /dev/null @@ -1,60 +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. -Collecting all parents and reconstruct this hierarhy in bledner. -''' -__version__ = '0.2' -import logging -import bpy - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -def placement(bobj, obj, scale): - ''' blender object, freecad object, scale factor ''' - bobj.location = obj.Placement.Base.multiply(scale) - m = bobj.rotation_mode - bobj.rotation_mode = 'QUATERNION' - if obj.Placement.Rotation.Angle: - # FreeCAD Quaternion is XYZW while Blender is WXYZ - q = (obj.Placement.Rotation.Q[3],)+obj.Placement.Rotation.Q[:3] - bobj.rotation_quaternion = (q) - bobj.rotation_mode = m - return bobj - - -def hierarchy(bobj, obj, scale): - ''' blender object, freecad object, scale factor ''' - obj_parent = obj.getParentGeoFeatureGroup() - obj_child_name = None - parents = [] - while obj_parent: - if bpy.data.objects.get(obj_parent.Label): - empty = bpy.data.objects[obj_parent.Label] - else: - empty = bpy.data.objects.new(obj_parent.Label, None) - empty.empty_display_type = 'CUBE' - empty.empty_display_size = 0.01 - placement(empty, obj_parent, scale) - parents.append(empty) - if not bobj.parent: - bobj.parent = empty - else: - bpy.data.objects[obj_child_name].parent = empty - obj_child_name = obj_parent.Label - obj_parent = obj_parent.getParentGeoFeatureGroup() - empty.select_set(False) - logger.debug('Add parent %s to object %s', empty.name, bobj.name) - return parents diff --git a/cg/blender/remesh/lowpoly_setup.py b/cg/blender/remesh/lowpoly_setup.py index df9e5b5..f3951f7 100644 --- a/cg/blender/remesh/lowpoly_setup.py +++ b/cg/blender/remesh/lowpoly_setup.py @@ -21,9 +21,9 @@ import sys import bpy import math -from utils.generative_modifiers import shell_remesher -from utils.object_converter import convert_mesh_to_mesh -from utils.object_relations import parenting +from blender.utils.generative_modifiers import shell_remesher +from blender.utils.object_converter import convert_mesh_to_mesh +from blender.utils.object_relations import parenting logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) diff --git a/cg/blender/utils/object_transforms.py b/cg/blender/utils/object_transforms.py index 0b85c10..35c9610 100644 --- a/cg/blender/utils/object_transforms.py +++ b/cg/blender/utils/object_transforms.py @@ -29,20 +29,23 @@ def apply_transforms(obj, location=False, rotation=False, scale=False): return rotation.to_matrix().to_4x4() def get_sca_matrix(scale): - scale_martix = Matrix() + scene_scale_martix = Matrix() for i in range(3): - scale_martix[i][i] = scale[i] - return scale_martix + scene_scale_martix[i][i] = scale[i] + return scene_scale_martix if location and rotation and scale: loc, rot, sca = obj.matrix_world.decompose() - mesh_martix = get_loc_matrix(loc) @ get_rot_matrix(rot) @ get_sca_matrix(sca) + mesh_martix = get_loc_matrix(loc) @ get_rot_matrix( + rot) @ get_sca_matrix(sca) obj.data.transform(mesh_martix) apply_matrix = get_loc_matrix(Vector.Fill(3, 0)) @ get_rot_matrix(Quaternion()) @ get_sca_matrix(Vector.Fill(3, 1)) obj.matrix_world = apply_matrix else: if location: - raise Exception('Location only applies with all transformations (rotate and scale) together!') + raise Exception( + 'Location only applies with all transformations (rotate and scale) together!' + ) if rotation: loc, rot, sca = obj.matrix_world.decompose() mesh_martix = get_rot_matrix(rot) diff --git a/cg/blender/utils/sdf_mesh_selector.py b/cg/blender/utils/sdf_mesh_selector.py index 89b59bf..03f3aa9 100644 --- a/cg/blender/utils/sdf_mesh_selector.py +++ b/cg/blender/utils/sdf_mesh_selector.py @@ -4,6 +4,9 @@ DESCRIPTION. Subassembly models setup from SDFormat file. Script selected subassembly models. Support Blender compiled as a Python Module only! + +DEPRECATED + """ __version__ = "0.1" diff --git a/cg/blender/utils/shininess_to_roughness.py b/cg/blender/utils/shininess_to_roughness.py index e284886..aa72ac2 100644 --- a/cg/blender/utils/shininess_to_roughness.py +++ b/cg/blender/utils/shininess_to_roughness.py @@ -20,8 +20,6 @@ def shiny_to_rough(shininess): ''' convert shiny to roughness ''' a, b = -1.0, 2.0 c = (shininess / 100.0) - 1.0 - D = math.pow(b,2) - (4 * a * c) + D = math.pow(b, 2) - (4 * a * c) roughness = (-b + math.sqrt(D)) / (2 * a) - if roughness < 0: - roughness = 0 - return roughness + return max(roughness, 0) diff --git a/cg/freecad/README.md b/cg/freecad/README.md new file mode 100644 index 0000000..b7a264b --- /dev/null +++ b/cg/freecad/README.md @@ -0,0 +1 @@ +## Модули проекта Robossembler для FreeCAD diff --git a/cg/freecad/__init__.py b/cg/freecad/__init__.py new file mode 100644 index 0000000..35743ac --- /dev/null +++ b/cg/freecad/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +DESCRIPTION. +FreeCAD modules for Robosembler project pipeline. +""" diff --git a/cg/freecad/utils/README.md b/cg/freecad/utils/README.md new file mode 100644 index 0000000..94c5590 --- /dev/null +++ b/cg/freecad/utils/README.md @@ -0,0 +1,3 @@ +### export_freecad_scene - Экспорт .FCStd сцены FreeCAD в JSON словарь. + +По умолчанию, импортирует из FreeCAD cцены все видимые solid объекты. diff --git a/cg/freecad/utils/export_freecad_scene.py b/cg/freecad/utils/export_freecad_scene.py new file mode 100644 index 0000000..8f41a31 --- /dev/null +++ b/cg/freecad/utils/export_freecad_scene.py @@ -0,0 +1,145 @@ +# -*- 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. +''' +__version__ = '0.1' +import json +import FreeCAD +import Part +import Mesh +import MeshPart +import logging +import math +import sys +from freecad.utils.is_object_solid import is_object_solid + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def freecad_to_json(filename, + tesselation_method='Standard', + linear_deflection=0.1, + angular_deflection=30.0, + fem_size=50.0, + skiphidden=True, + nonsolid_property='Robossembler_NonSolid'): + ''' Reads a FreeCAD .FCStd file and return json assembly. ''' + + scene = {} + js_objs = {} + + doc = FreeCAD.open(filename) + docname = doc.Name + + # collect all materials + fem_mats = [] + for fem_mat in doc.Objects: + if fem_mat.isDerivedFrom('App::MaterialObjectPython'): + fem_mats.append(fem_mat) + + for obj in doc.Objects: + js_obj = {} + + if skiphidden: + if not obj.Visibility: + continue + + if obj.isDerivedFrom('PartDesign::CoordinateSystem'): + js_obj['type'] = 'LCS' + + elif obj.isDerivedFrom('Part::Feature'): + js_obj['type'] = 'PART' + # filter for nonsolids + + if is_object_solid(obj) or hasattr(obj, nonsolid_property): + # create mesh from shape + shape = obj.Shape + shape = obj.Shape.copy() + shape.Placement = obj.Placement.inverse().multiply(shape.Placement) + meshfromshape = doc.addObject('Mesh::Feature', 'Mesh') + if tesselation_method == 'Standard': + meshfromshape.Mesh = MeshPart.meshFromShape( + Shape=shape, + LinearDeflection=linear_deflection, + AngularDeflection=math.radians(angular_deflection), + Relative=False) + elif tesselation_method == 'FEM': + meshfromshape.Mesh = MeshPart.meshFromShape( + Shape=shape, + MaxLength=fem_size) + else: + raise TypeError('Wrong tesselation method! ' + 'Standard and FEM methods are supported only!') + break + t = meshfromshape.Mesh.Topology + verts = [[v.x, v.y, v.z] for v in t[0]] + faces = t[1] + js_obj['mesh'] = (verts, faces) + + # 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 + 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[filename] = js_objs + + logger.info('Stored %s objects without errors', len(js_objs)) + + print(json.dumps(scene)) + + +args = sys.argv[2:] +for arg in args[2:5]: + args[args.index(arg)] = float(arg) +for arg in args[5:6]: + args[args.index(arg)] = bool(arg) +#for num, item in enumerate(args): +# print(num, type(item)) +freecad_to_json(*args) diff --git a/cg/freecad/utils/freecad_cmd.py b/cg/freecad/utils/freecad_cmd.py new file mode 100644 index 0000000..c8d2bae --- /dev/null +++ b/cg/freecad/utils/freecad_cmd.py @@ -0,0 +1,56 @@ +# coding: utf-8 +#!/usr/bin/env python +# 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. + +import subprocess +import json + + +def freecad_proc(*args, **kwargs): + command = [ + args[0], + args[1], + + # general property + kwargs['filename'], + kwargs['tesselation_method'], + kwargs['linear_deflection'], + kwargs['angular_deflection'], + kwargs['fem_size'], + kwargs['skiphidden'], + kwargs['nonsolid_property'], + ] + + proc = subprocess.run(command, + check=True, + stdout=subprocess.PIPE, + encoding='utf-8') + + return json.loads(proc.stdout.split('FreeCAD ')[0]) + + +kwargs = {} +kwargs['filename'] = '/' +kwargs['tesselation_method'] = 'Standard' +kwargs['linear_deflection'] = '0.1' +kwargs['angular_deflection'] = '30.0' +kwargs['fem_size'] = '10.0' +kwargs['skiphidden'] = 'True' +kwargs['nonsolid_property'] = 'Robossembler_NonSolid' + +js_data = freecad_proc( + 'freecadcmd', + '//cg/freecad/utils/export_freecad_scene.py', + **kwargs) + +print(js_data) diff --git a/cg/blender/import_fcstd/is_object_solid.py b/cg/freecad/utils/is_object_solid.py similarity index 100% rename from cg/blender/import_fcstd/is_object_solid.py rename to cg/freecad/utils/is_object_solid.py diff --git a/cg/pipeline/README.md b/cg/pipeline/README.md index bcfb0f2..9485cbc 100644 --- a/cg/pipeline/README.md +++ b/cg/pipeline/README.md @@ -1,18 +1,15 @@ ### freecad_to_asset.py -Пакетное производство 3д ассетов из объектов сцены Freecad +Пакетное производство 3д ассетов из объектов CAD сцены. Поддерживается работа поверх Blender в качестве модуля! Сценарий производит: -- импорт solid объектов в Blender сцену +- экспорт CAD сцены +- восстановление Blender сцены - имена solid объектов в mesh объекты - тесселяцию solid объектов с заданным уровнем - ретопологию mesh объектов с заданным уровнем - обработку mesh объектов для использования в качестве ассетов - импорт FEM материалов и назначение их для mesh объектов - экспорт mesh объектов в требуемые форматы - -### blender_opts.py - -Различные операции в Bledner diff --git a/cg/pipeline/freecad_to_asset.py b/cg/pipeline/freecad_to_asset.py index 96829be..81f3ba2 100644 --- a/cg/pipeline/freecad_to_asset.py +++ b/cg/pipeline/freecad_to_asset.py @@ -4,20 +4,22 @@ DESCRIPTION. Convert and setup FreeCAD solid objects to 3d assets mesh files. Support Blender compiled as a Python Module only! ''' -__version__ = '0.4' +__version__ = '0.5' import logging import os import sys -sys.path.append('../blender/') -from import_fcstd.import_cad_objects import obj_importer -from import_fcstd.restruct_hierarchy_by_lcs import restruct_hierarchy -from import_fcstd.import_coordinate_point import lcs_json_importer -from utils.remove_collections import remove_collections -from utils.cleanup_orphan_data import cleanup_orphan_data -from utils.sdf_mesh_selector import sdf_mesh_selector -from remesh.highpoly_setup import setup_meshes -from remesh.lowpoly_setup import parts_to_shells +sys.path.append('../') +from freecad.utils.export_freecad_scene import freecad_to_json + +from blender.import_cad.build_blender_scene import json_to_blend +from blender.import_cad.restruct_hierarchy_by_lcs import restruct_hierarchy +from blender.import_cad.import_coordinate_point import lcs_json_importer +from blender.utils.remove_collections import remove_collections +from blender.utils.cleanup_orphan_data import cleanup_orphan_data +from blender.utils.sdf_mesh_selector import sdf_mesh_selector +from blender.remesh.highpoly_setup import setup_meshes +from blender.remesh.lowpoly_setup import parts_to_shells from export.dae import export_dae from export.collision import export_col_stl @@ -75,12 +77,9 @@ def freecad_asset_pipeline(fcstd_path, remove_collections() cleanup_orphan_data() - # import objects - objs_for_render = obj_importer(fcstd_path, - tesselation_method, - linear_deflection, - angular_deflection, - fem_size) + ## convert FreeCAD scene to Blender scene + # TODO + objs_for_render = json_to_blend(freecad_to_json(**kwargs)) # restructuring hierarchy by lcs points lcs_objects = restruct_hierarchy()