diff --git a/README.md b/README.md
index 233f5b5..cb12262 100644
--- a/README.md
+++ b/README.md
@@ -21,3 +21,9 @@
- __Предикат стабильной осуществимости__. Верен для последовательности сборки, когда сборка на каждом из этапов приходит к стабильному состоянию.
- __Предикат степеней свободы__. Формируется на основе уже сгенерированных графов/графа сборки. В каких степенях свободы возможно перемещать деталь.
+# Генерация сцен
+
+TODO: составить описание
+
+[пример файла описания сцены](docs/scene_generator)
+
diff --git a/cg/blender/scripts/addons/Robossembler/__init__.py b/cg/blender/scripts/addons/Robossembler/__init__.py
new file mode 100644
index 0000000..2b52d89
--- /dev/null
+++ b/cg/blender/scripts/addons/Robossembler/__init__.py
@@ -0,0 +1,139 @@
+# ***** BEGIN GPL LICENSE BLOCK *****
+#
+# 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see
+# and write to the Free Software Foundation, Inc., 51 Franklin Street,
+# Fifth Floor, Boston, MA 02110-1301, USA..
+#
+# The Original Code is Copyright (C) 2023 by Kurochkin Ilia ###
+# All rights reserved.
+#
+# Contact: brothermechanic@yandex.com ###
+# Information: https://gitlab.com/robossembler ###
+#
+# The Original Code is: all of this file.
+#
+# ***** END GPL LICENSE BLOCK *****
+#
+# -*- coding: utf-8 -*-
+
+import os
+import bpy
+
+from bpy.types import (
+ Panel,
+ Operator,
+ PropertyGroup)
+
+from bpy.props import (
+ StringProperty,
+ EnumProperty,
+ PointerProperty)
+
+from .scene_to_json import export_scene_conf
+
+bl_info = {
+ 'name': 'Robossembler Tools',
+ 'author': 'brothermechanic@gmail.com',
+ 'version': (0, 1),
+ 'blender': (3, 6, 1),
+ 'location': '3D View > Toolbox',
+ 'description': 'Robossembler pipeline tools',
+ 'warning': '',
+ 'wiki_url': '',
+ 'tracker_url': 'https://gitlab.com/robossembler',
+ 'category': 'Robossembler',
+}
+
+
+class addon_Properties(PropertyGroup):
+
+ engine: EnumProperty(
+ name='Physics Engine',
+ description='Selest Target Engine',
+ items=[('BULLET', 'Bullet', ''),
+ ('ODE', 'O D E', ''),
+ ('SIMBODY', 'Simbody', ''),
+ ('OPENSIM', 'OpenSim', '')
+ ]
+ )
+
+ conf_file_path: StringProperty(
+ name='Config File Path',
+ description='Input/output config file path',
+ default='',
+ maxlen=1023,
+ subtype='FILE_PATH'
+ )
+
+
+class RobossemblerPanel(Panel):
+ '''Doc'''
+ bl_label = 'Robossembler Tools'
+ bl_idname = 'ROBOSSEMBLER_PT_PANEL'
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_category = 'Robossembler'
+
+ def draw(self, context):
+ prop = context.scene.robossembler_properties
+
+ layout = self.layout
+ layout.prop(prop, 'conf_file_path')
+ layout.prop(prop, 'engine')
+
+ col = layout.column()
+ col.alert = True
+ col.scale_y = 2.0
+ col.operator('export.scene_config',
+ icon='WORLD_DATA',
+ text='Export Scene Config')
+
+
+class RobossemblerOperator(Operator):
+ '''Tooltip'''
+ bl_idname = 'export.scene_config'
+ bl_label = ''
+ bl_description = 'Export scene config'
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ prop = context.scene.robossembler_properties
+
+ conf_file_path = os.path.realpath(bpy.path.abspath((prop.conf_file_path)))
+ physics_engine = prop.engine
+ export_scene_conf(context, conf_file_path, physics_engine)
+
+ return {'FINISHED'}
+
+
+classes = (
+ RobossemblerPanel,
+ RobossemblerOperator,
+ addon_Properties)
+
+
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+ bpy.types.Scene.robossembler_properties = PointerProperty(type=addon_Properties)
+
+
+def unregister():
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
+ del bpy.types.Scene.robossembler_properties
+
+
+if __name__ == '__main__':
+ register()
diff --git a/cg/blender/scripts/addons/Robossembler/model_md5.py b/cg/blender/scripts/addons/Robossembler/model_md5.py
new file mode 100644
index 0000000..e76d6fd
--- /dev/null
+++ b/cg/blender/scripts/addons/Robossembler/model_md5.py
@@ -0,0 +1,30 @@
+# coding: utf-8
+
+import hashlib
+import os
+
+
+def md5(file_path):
+ ''' Generate md5 hash. '''
+ hash_md5 = hashlib.md5()
+ with open(file_path, "rb") as f:
+ for chunk in iter(lambda: f.read(4096), b""):
+ hash_md5.update(chunk)
+ return hash_md5.hexdigest()
+
+
+def create_file_md5(file_path):
+ ''' Get md5_file_hash of file and store to md5 file. '''
+ md5_file_path = f'{file_path}.md5'
+ md5_file_hash = md5(file_path)
+ # create md5_file
+ if not os.path.isfile(md5_file_path):
+ with open(md5_file_path, 'w', encoding='utf-8') as md5_file:
+ md5_file.write(md5_file_hash)
+ else:
+ with open(md5_file_path, 'r', encoding='utf-8') as md5_file:
+ md5_file_stored = md5_file.read()
+ # check md5_file
+ assert md5_file_hash == md5_file_stored, (
+ "Mosel's md5 don't matched with stored %s.md5 file", file_path)
+ return md5_file_hash
diff --git a/cg/blender/scripts/addons/Robossembler/model_name.py b/cg/blender/scripts/addons/Robossembler/model_name.py
new file mode 100644
index 0000000..4fb0a50
--- /dev/null
+++ b/cg/blender/scripts/addons/Robossembler/model_name.py
@@ -0,0 +1,18 @@
+# coding: utf-8
+
+import logging
+import xml.etree.ElementTree as ET
+
+logger = logging.getLogger(__name__)
+
+
+def get_sdf_urdf_model_name(model_path):
+ mytree = ET.parse(model_path)
+ myroot = mytree.getroot()
+ for elem in myroot.iter():
+ if elem.tag in ('link', 'joint', 'material', 'visual', 'collision'):
+ continue
+ if not elem.attrib.get('name'):
+ continue
+ return elem.attrib.get('name')
+ return logger.warning('Model %s do not have a name!', model_path)
diff --git a/cg/blender/scripts/addons/Robossembler/model_paths.py b/cg/blender/scripts/addons/Robossembler/model_paths.py
new file mode 100644
index 0000000..5f0c0fa
--- /dev/null
+++ b/cg/blender/scripts/addons/Robossembler/model_paths.py
@@ -0,0 +1,12 @@
+# coding: utf-8
+
+import glob
+
+
+def get_urdf_sdf_model_paths(models_dir):
+ ''' Collect models paths. '''
+ model_paths = []
+ for file_name in glob.glob(f'{models_dir}/**', recursive=True):
+ if file_name.endswith('.urdf') or file_name.endswith('.sdf'):
+ model_paths.append(file_name.replace('\\', '/'))
+ return model_paths
diff --git a/cg/blender/scripts/addons/Robossembler/scene_to_json.py b/cg/blender/scripts/addons/Robossembler/scene_to_json.py
new file mode 100644
index 0000000..8bc242d
--- /dev/null
+++ b/cg/blender/scripts/addons/Robossembler/scene_to_json.py
@@ -0,0 +1,189 @@
+# ***** BEGIN GPL LICENSE BLOCK *****
+#
+# 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see
+# and write to the Free Software Foundation, Inc., 51 Franklin Street,
+# Fifth Floor, Boston, MA 02110-1301, USA..
+#
+# The Original Code is Copyright (C) 2023 by Kurochkin Ilia ###
+# All rights reserved.
+#
+# Contact: brothermechanic@yandex.com ###
+# Information: https://gitlab.com/robossembler ###
+#
+# The Original Code is: all of this file.
+#
+# ***** END GPL LICENSE BLOCK *****
+#
+# -*- coding: utf-8 -*-
+'''
+DESCRIPTION.
+Collect all root objects in blender scene and export as json scene configuration.
+'''
+__version__ = '0.1'
+
+import collections
+import json
+import logging
+import math
+import os
+
+from .model_paths import get_urdf_sdf_model_paths
+from .model_name import get_sdf_urdf_model_name
+from .model_md5 import create_file_md5
+
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO)
+
+
+def export_scene_conf(context, scene_config_path, physics_engine):
+ ''' Export scene config to json. '''
+ # open conf file
+ if os.path.isfile(scene_config_path):
+ with open(scene_config_path, 'r', encoding='utf-8') as conf_file:
+ world_dic = json.load(conf_file)
+ else:
+ world_dic = collections.defaultdict(list)
+ models_dir = os.path.join(os.path.dirname(scene_config_path), 'models')
+ model_paths = get_urdf_sdf_model_paths(models_dir)
+ for model_path in model_paths:
+ model_dic = {}
+ # 1
+ model_dic['name'] = get_sdf_urdf_model_name(model_path)
+ # 2
+ model_dic['id'] = create_file_md5(model_path)
+ # 3
+ relative_model_path = model_path.split(
+ os.path.dirname(scene_config_path))[1][1:]
+ model_dic['path'] = relative_model_path
+ world_dic['models'].append(model_dic)
+
+ # collect all non parented objs
+ roots = [obj for obj in context.scene.objects if not obj.parent]
+
+ # collect instances
+ for instance in roots:
+ instance_dic = {}
+ # 1
+ instance_dic['model_name'] = ''
+ if instance.get('model/name'):
+ instance_dic['model_name'] = instance['model/name']
+ # 2
+ instance_dic['model_id'] = ''
+ for item in world_dic['models']:
+ if item['name'] != instance_dic['model_name']:
+ continue
+ instance_dic['model_id'] = item['id']
+ # 2
+ instance_dic['id'] = ''
+ # 3
+ instance_dic['pose'] = {}
+ instance_dic['pose']['x'] = round(instance.location[0], 3)
+ instance_dic['pose']['y'] = round(instance.location[1], 3)
+ instance_dic['pose']['z'] = round(instance.location[2], 3)
+ instance_dic['pose']['roll'] = round(math.degrees(instance.rotation_euler[0]), 3)
+ instance_dic['pose']['pitch'] = round(math.degrees(instance.rotation_euler[1]), 3)
+ instance_dic['pose']['yaw'] = round(math.degrees(instance.rotation_euler[2]), 3)
+ # 4
+ if instance.scale[0] != sum(instance.scale[:]) / 3:
+ logger.warning('Asset instance {} is not uniformly scaled!'.format(
+ instance.name))
+ instance_dic['scale'] = round(instance.scale[0], 3)
+ # 5
+ # if assembly
+ if instance_dic['model_name']:
+ instance_dic['type'] = 'asset'
+ links = instance.children_recursive
+ links.insert(0, instance)
+ instance_dic['link_names'] = []
+ instance_dic['links'] = {}
+ transforms = 0.0
+ for link in links:
+ child = {}
+ if link.type != 'ARMATURE':
+ continue
+ bone = link.pose.bones[0]
+ rotation_mode = bone.rotation_mode
+ bone.rotation_mode = 'XYZ'
+ child['pose'] = {}
+ child['pose']['x'] = round(bone.location[0], 6)
+ child['pose']['y'] = round(bone.location[1], 6)
+ child['pose']['z'] = round(bone.location[2], 6)
+ child['pose']['roll'] = round(math.degrees(bone.rotation_euler[0]), 6)
+ child['pose']['pitch'] = round(math.degrees(bone.rotation_euler[1]), 6)
+ child['pose']['yaw'] = round(math.degrees(bone.rotation_euler[2]), 6)
+ bone.rotation_mode = rotation_mode
+ if bone.scale[0] != sum(bone.scale[:]) / 3:
+ logger.warning(
+ 'Link {} of asset {} is not uniformly scaled!'.format(
+ link.name, asset_dic['name']))
+ child['scale'] = round(bone.scale[0], 3)
+ transforms += round(sum(
+ [child['pose']['x'], child['pose']['y'], child['pose']['z'],
+ child['pose']['roll'], child['pose']['pitch'], child['pose']['yaw']],
+ (child['scale'] - 1.0)),
+ 6)
+ if '.' in link.name:
+ link_name = link.name.rpartition('.')[0]
+ else:
+ link_name = link.name
+
+ instance_dic['link_names'].append(link_name)
+ instance_dic['links'][link_name] = child
+
+ if transforms == 0.0:
+ instance_dic.pop('link_names')
+ instance_dic.pop('links')
+
+ # 6
+ instance_dic['parent'] = 'world'
+ # if light
+ if instance.type == 'LIGHT':
+ instance_dic['type'] = instance.type.lower()
+ if instance.data.type == 'POINT':
+ instance_dic['light_type'] = 'point'
+ elif instance.data.type == 'SUN':
+ instance_dic['light_type'] = 'directional'
+ elif instance.data.type == 'SPOT':
+ instance_dic['light_type'] = 'spot'
+ instance_dic['spot_angle'] = round(
+ math.degrees(instance.data.spot_size), 1)
+ else:
+ logger.warning(
+ 'Unsupported light type {} on instance {}!'.format(
+ instance.data.type, instance.name))
+ instance_dic['intencity'] = instance.data.energy
+ instance_dic['diffuse'] = list(instance.data.color)
+ # if camera
+ if instance.type == 'CAMERA':
+ instance_dic['type'] = instance.type.lower()
+ instance_dic['focal_length'] = instance.data.lens
+ instance_dic['sensor_size'] = [instance.data.sensor_width,
+ instance.data.sensor_height]
+
+ world_dic['instances'].append(instance_dic)
+
+ # select physics engine
+ world_dic['physics'] = {}
+ world_dic['physics']['engine_name'] = physics_engine
+ world_dic['physics']['gravity'] = {}
+ world_dic['physics']['gravity']['x'] = round(context.scene.gravity[0], 3)
+ world_dic['physics']['gravity']['y'] = round(context.scene.gravity[1], 3)
+ world_dic['physics']['gravity']['z'] = round(context.scene.gravity[2], 3)
+
+ # write conf file
+ with open(scene_config_path, 'w', encoding='utf-8') as world_conf_file:
+ json.dump(world_dic, world_conf_file, ensure_ascii=False, indent=4)
+ logger.info('Scene configuration in json file was completed successfully!')
+ return scene_config_path
diff --git a/docs/scene_generator.md b/docs/scene_generator.md
new file mode 100644
index 0000000..bf29982
--- /dev/null
+++ b/docs/scene_generator.md
@@ -0,0 +1,61 @@
+## Пример файла описания сцены из Blender
+
+TODO: рассписать потребность в основных элементах, что и для чего
+
+```json
+{
+ "assets": [
+ {
+ "name": "robo-arm",
+ "id": "629b29d7-fe15-428b-9014-6c3dde045af8",
+ "model_path": "../model.urdf"
+ }
+ ],
+ "instances": [
+ // измненная URDF модель
+ {
+ "id": "0e29084f-1190-45d0-bd59-8f4ce2591gb1",
+ "name": "robo-arm-1",
+ //assetId указывает на родительский ассет
+ "assetId": "629b29d7-fe15-428b-9014-6c3dde045af8",
+ "pose": {
+ "x": 12.0,
+ "y": 1.0,
+ "z": 4.0,
+ "roll": 1.0,
+ "pitch": 4.0,
+ "yaw": 5.0
+ },
+ //если изменено внутренее состояние URDF модели
+ "tags": [
+ ""
+ ]
+ },
+ {
+ "id": "0e27984f-8890-4d90-bd59-8f4ce29920f9",
+ "name": "robo-arm-2",
+ //assetId указывает на родительский ассет
+ "assetId": "629b29d7-fe15-428b-9014-6c3dde045af8",
+ //если дефолтные позиции модели
+ "pose": null,
+ //если не изменено внутренее состояние URDF модели
+ "tags": null
+ },
+ {
+ "type": "LIGHT",
+ "light_type": "SPOT",
+ "power": 10.0,
+ "spot_angle": 45.0,
+ "name": null,
+ "pose": {
+ "x": 0.0,
+ "y": 0.0,
+ "z": 0.0,
+ "roll": 0.0,
+ "pitch": 0.0,
+ "yaw": 0.0
+ }
+ }
+ ]
+}
+```