diff --git a/proj/terrax/vs2013/terrax.vcxproj b/proj/terrax/vs2013/terrax.vcxproj
index 94f6063c1cb5fba4a5dda7a1af413e43d4dd0bf4..3714e7454c5446f0fc1fa17a452affecb38d0674 100644
--- a/proj/terrax/vs2013/terrax.vcxproj
+++ b/proj/terrax/vs2013/terrax.vcxproj
@@ -214,6 +214,7 @@
     <ClCompile Include="..\..\..\source\terrax\CommandDelete.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CommandDestroyModel.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CommandDuplicate.cpp" />
+    <ClCompile Include="..\..\..\source\terrax\CommandGroup.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CommandModifyModel.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CommandMove.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CommandPaste.cpp" />
@@ -221,6 +222,7 @@
     <ClCompile Include="..\..\..\source\terrax\CommandRotate.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CommandScale.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CommandSelect.cpp" />
+    <ClCompile Include="..\..\..\source\terrax\CommandUngroup.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CurveEditorDialog.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CurveEditorGraphNode.cpp" />
     <ClCompile Include="..\..\..\source\terrax\CurveEditorGraphNodeData.cpp" />
@@ -234,6 +236,7 @@
     <ClCompile Include="..\..\..\source\terrax\GradientPreviewGraphNode.cpp" />
     <ClCompile Include="..\..\..\source\terrax\GradientPreviewGraphNodeData.cpp" />
     <ClCompile Include="..\..\..\source\terrax\Grid.cpp" />
+    <ClCompile Include="..\..\..\source\terrax\GroupObject.cpp" />
     <ClCompile Include="..\..\..\source\terrax\LevelOpenDialog.cpp" />
     <ClCompile Include="..\..\..\source\terrax\mainWindow.cpp" />
     <ClCompile Include="..\..\..\source\terrax\MaterialBrowser.cpp" />
@@ -271,6 +274,7 @@
     <ClInclude Include="..\..\..\source\terrax\CommandDelete.h" />
     <ClInclude Include="..\..\..\source\terrax\CommandDestroyModel.h" />
     <ClInclude Include="..\..\..\source\terrax\CommandDuplicate.h" />
+    <ClInclude Include="..\..\..\source\terrax\CommandGroup.h" />
     <ClInclude Include="..\..\..\source\terrax\CommandModifyModel.h" />
     <ClInclude Include="..\..\..\source\terrax\CommandMove.h" />
     <ClInclude Include="..\..\..\source\terrax\CommandPaste.h" />
@@ -278,6 +282,7 @@
     <ClInclude Include="..\..\..\source\terrax\CommandRotate.h" />
     <ClInclude Include="..\..\..\source\terrax\CommandScale.h" />
     <ClInclude Include="..\..\..\source\terrax\CommandSelect.h" />
+    <ClInclude Include="..\..\..\source\terrax\CommandUngroup.h" />
     <ClInclude Include="..\..\..\source\terrax\CurveEditorDialog.h" />
     <ClInclude Include="..\..\..\source\terrax\CurveEditorGraphNode.h" />
     <ClInclude Include="..\..\..\source\terrax\CurveEditorGraphNodeData.h" />
@@ -290,6 +295,7 @@
     <ClInclude Include="..\..\..\source\terrax\GizmoScale.h" />
     <ClInclude Include="..\..\..\source\terrax\GradientPreviewGraphNode.h" />
     <ClInclude Include="..\..\..\source\terrax\GradientPreviewGraphNodeData.h" />
+    <ClInclude Include="..\..\..\source\terrax\GroupObject.h" />
     <ClInclude Include="..\..\..\source\terrax\ICompoundObject.h" />
     <ClInclude Include="..\..\..\source\terrax\LevelOpenDialog.h" />
     <ClInclude Include="..\..\..\source\terrax\MaterialBrowser.h" />
diff --git a/proj/terrax/vs2013/terrax.vcxproj.filters b/proj/terrax/vs2013/terrax.vcxproj.filters
index f6cb0656ddbd482409af3bae3430120d2995f584..c7f15f1fc8863e186cc481aa6a876468a8a27a9d 100644
--- a/proj/terrax/vs2013/terrax.vcxproj.filters
+++ b/proj/terrax/vs2013/terrax.vcxproj.filters
@@ -207,6 +207,15 @@
     <ClCompile Include="..\..\..\source\terrax\SceneTreeWindow.cpp">
       <Filter>Source Files\windows</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\..\source\terrax\GroupObject.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\..\source\terrax\CommandGroup.cpp">
+      <Filter>Source Files\cmd</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\..\source\terrax\CommandUngroup.cpp">
+      <Filter>Source Files\cmd</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\..\..\source\terrax\terrax.rc">
@@ -400,6 +409,15 @@
     <ClInclude Include="..\..\..\source\terrax\ICompoundObject.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\..\source\terrax\GroupObject.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\terrax\CommandGroup.h">
+      <Filter>Header Files\cmd</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\terrax\CommandUngroup.h">
+      <Filter>Header Files\cmd</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <Image Include="..\..\..\source\terrax\resource\new.bmp">
diff --git a/source/terrax/CommandBuildModel.cpp b/source/terrax/CommandBuildModel.cpp
index 1584fbb7af4534ec00bfad615cbbb8e7f777fb4d..f3ff59d42f8b94b22cca96ca04f36814cf8e700e 100644
--- a/source/terrax/CommandBuildModel.cpp
+++ b/source/terrax/CommandBuildModel.cpp
@@ -39,7 +39,12 @@ bool XMETHODCALLTYPE CCommandBuildModel::exec()
 		{
 			if((*i.first)->isSelected())
 			{
-				m_aObjLocations.push_back({*i.first, *i.second});
+				void *pData = NULL;
+				(*i.first)->getInternalData(&X_IS_COMPOUND_GUID, &pData);
+				if(!pData)
+				{
+					m_aObjLocations.push_back({*i.first, *i.second});
+				}
 			}
 		}
 
diff --git a/source/terrax/CommandDuplicate.cpp b/source/terrax/CommandDuplicate.cpp
index e5d2e5d8a01cd5b7cab33e3ffe769e738f483460..52493d9e7ab3fb43310f1242b128a0bc319dcde9 100644
--- a/source/terrax/CommandDuplicate.cpp
+++ b/source/terrax/CommandDuplicate.cpp
@@ -27,15 +27,15 @@ bool XMETHODCALLTYPE CCommandDuplicate::undo()
 
 void CCommandDuplicate::initialize()
 {
-	for(UINT i = 0, l = g_pLevelObjects.size(); i < l; ++i)
-	{
-		IXEditorObject *pObj = g_pLevelObjects[i];
+	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
 		if(pObj->isSelected())
 		{
 			processObject(pObj);
+			return(XEOR_SKIP_CHILDREN);
 		}
-	}
-
+		return(XEOR_CONTINUE);
+	});
+	
 	for(UINT i = 0, l = g_apProxies.size(); i < l; ++i)
 	{
 		CProxyObject *pObj = g_apProxies[i];
@@ -48,6 +48,19 @@ void CCommandDuplicate::initialize()
 			}
 		}
 	}
+
+	for(UINT i = 0, l = g_apGroups.size(); i < l; ++i)
+	{
+		CGroupObject *pObj = g_apGroups[i];
+		if(pObj->isSelected())
+		{
+			UINT uGroup = m_commandPaste.addGroup(*pObj->getGUID());
+			for(UINT j = 0, jl = pObj->getObjectCount(); j < jl; ++j)
+			{
+				m_commandPaste.addGroupObject(uGroup, *pObj->getObject(j)->getGUID());
+			}
+		}
+	}
 }
 
 void CCommandDuplicate::processObject(IXEditorObject *pObj)
diff --git a/source/terrax/CommandGroup.cpp b/source/terrax/CommandGroup.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d0e2d75f0c7a864f352cd79a4a7b02b7bae19272
--- /dev/null
+++ b/source/terrax/CommandGroup.cpp
@@ -0,0 +1,178 @@
+#include "CommandGroup.h"
+#include <common/aastring.h>
+
+extern AssotiativeArray<AAString, IXEditable*> g_mEditableSystems;
+
+CCommandGroup::CCommandGroup()
+{
+}
+
+CCommandGroup::~CCommandGroup()
+{
+	mem_release(m_pGroup);
+}
+
+bool XMETHODCALLTYPE CCommandGroup::exec()
+{
+	if(!m_isLocationsSaved)
+	{
+		IXEditorObject *pObj;
+		const Map<IXEditorObject*, ICompoundObject*>::Node *pNode;
+		for(auto i = g_mObjectsLocation.begin(); i; ++i)
+		{
+			if((*i.first)->isSelected() && !(*i.second)->isSelected())
+			{
+				m_aObjLocations.push_back({*i.first, *i.second});
+			}
+		}
+
+		bool canHaveCommonParent = true;
+		fora(i, g_pLevelObjects)
+		{
+			if(g_pLevelObjects[i]->isSelected())
+			{
+				canHaveCommonParent = false;
+				break;
+			}
+		}
+
+		if(canHaveCommonParent)
+		{
+			Array<ICompoundObject*> aPath;
+			ICompoundObject *pParent;
+			fora(i, m_aObjLocations)
+			{
+				pParent = m_aObjLocations[i].pLocation;
+				while(pParent)
+				{
+					if(i == 0)
+					{
+						aPath.push_back(pParent);
+					}
+					else
+					{
+						int idx = aPath.indexOf(pParent);
+						if(idx >= 0)
+						{
+							while(idx--)
+							{
+								aPath.erase(0);
+							}
+							break;
+						}
+					}
+					pParent = XGetObjectParent(pParent);
+				}
+
+				if(i != 0 && !pParent)
+				{
+					aPath.clear();
+					break;
+				}
+			}
+
+			if(aPath.size())
+			{
+				fora(i, aPath)
+				{
+					// Groups cannot be inside proxies
+					void *pData = NULL;
+					aPath[i]->getInternalData(&X_IS_PROXY_GUID, &pData);
+					if(!pData)
+					{
+						m_pCommonParent = aPath[i];
+						break;
+					}
+				}
+			}
+		}
+
+		m_isLocationsSaved = true;
+	}
+	
+	if(!m_pGroup)
+	{
+		m_pGroup = new CGroupObject();
+		m_pGroup->setPos(m_vPos);
+	}
+
+	g_pEditor->addObject(m_pGroup);
+
+	if(m_pCommonParent)
+	{
+		m_pCommonParent->addChildObject(m_pGroup);
+	}
+
+	fora(i, m_aObjLocations)
+	{
+		ObjLocation &loc = m_aObjLocations[i];
+		loc.pLocation->removeChildObject(loc.pObj);
+	}
+
+	IXEditorObject *pObject;
+	forar(i, g_pLevelObjects)
+	{
+		pObject = g_pLevelObjects[i];
+		if(pObject->isSelected())
+		{
+			m_pGroup->addChildObject(pObject);
+		}
+	}
+
+	//g_pEditor->addObject(m_pGroup);
+
+	m_pGroup->setSelected(true);
+
+	add_ref(m_pGroup);
+	g_apGroups.push_back(m_pGroup);
+
+	//TODO("Find deepest common parent to place group into");
+
+	return(true);
+}
+bool XMETHODCALLTYPE CCommandGroup::undo()
+{
+	int idx = g_apGroups.indexOf(m_pGroup);
+	assert(idx >= 0);
+	if(idx >= 0)
+	{
+		mem_release(g_apGroups[idx]);
+		g_apGroups.erase(idx);
+	}
+
+	//m_pProxy->setSelected(false);
+
+
+	// destroy proxy
+	//m_pProxy->reset();
+
+	IXEditorObject *pObj;
+	for(int i = (int)m_pGroup->getObjectCount() - 1; i >= 0; --i)
+	{
+		pObj = m_pGroup->getObject(i);
+		m_pGroup->removeChildObject(pObj);
+
+		if(m_aObjLocations.indexOf(pObj, [](const ObjLocation &a, IXEditorObject *pB){
+			return(a.pObj == pB);
+		}) < 0)
+		{
+			g_pEditor->onObjectAdded(pObj);
+		}
+	}
+	// restore object locations
+	fora(i, m_aObjLocations)
+	{
+		ObjLocation &loc = m_aObjLocations[i];
+		loc.pLocation->addChildObject(loc.pObj);
+	}
+
+	m_pGroup->setSelected(false);
+
+	if(m_pCommonParent)
+	{
+		m_pCommonParent->removeChildObject(m_pGroup);
+	}
+	g_pEditor->removeObject(m_pGroup);
+		
+	return(true);
+}
diff --git a/source/terrax/CommandGroup.h b/source/terrax/CommandGroup.h
new file mode 100644
index 0000000000000000000000000000000000000000..d37541a6c2a1ff1d15f4e7c3d464a98c80d7950a
--- /dev/null
+++ b/source/terrax/CommandGroup.h
@@ -0,0 +1,52 @@
+#ifndef _COMMAND_GROUP_H_
+#define _COMMAND_GROUP_H_
+
+#include <xcommon/editor/IXEditorExtension.h>
+#include "terrax.h"
+
+//#include <common/assotiativearray.h>
+//#include <common/string.h>
+//#include <xcommon/editor/IXEditable.h>
+
+//#include "CommandCreate.h"
+#include "GroupObject.h"
+
+class CCommandGroup final: public IXUnknownImplementation<IXEditorCommand>
+{
+public:
+	CCommandGroup();
+	~CCommandGroup();
+
+	bool XMETHODCALLTYPE exec() override;
+	bool XMETHODCALLTYPE undo() override;
+
+	const char* XMETHODCALLTYPE getText() override
+	{
+		return("group");
+	}
+
+	bool XMETHODCALLTYPE isEmpty() override
+	{
+		return(false);
+	}
+
+private:
+	CGroupObject *m_pGroup = NULL;
+
+	struct ObjLocation
+	{
+		IXEditorObject *pObj;
+		ICompoundObject *pLocation;
+	};
+	Array<ObjLocation> m_aObjLocations;
+
+	ICompoundObject *m_pCommonParent = NULL;
+
+	float3_t m_vPos;
+
+	bool m_isLocationsSaved = false;
+	bool m_isCenterFound = false;
+
+};
+
+#endif
diff --git a/source/terrax/CommandPaste.cpp b/source/terrax/CommandPaste.cpp
index 130e1b62170354dd9081a6185b09119bb7504183..f7c59ec80d51c334304af87323bab912382229a3 100644
--- a/source/terrax/CommandPaste.cpp
+++ b/source/terrax/CommandPaste.cpp
@@ -10,10 +10,12 @@ bool XMETHODCALLTYPE CCommandPaste::exec()
 		m_pCommandSelect = new CCommandSelect();
 		
 		XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-			if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+			if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 			{
 				m_pCommandSelect->addDeselected(pObj);
+				return(XEOR_SKIP_CHILDREN);
 			}
+			return(XEOR_CONTINUE);
 		});
 	}
 
@@ -45,6 +47,7 @@ bool XMETHODCALLTYPE CCommandPaste::exec()
 		_proxy_obj &po = m_aProxies[i];
 
 		po.pProxy->setDstObject(m_mapGuids[po.guid]);
+		m_mapGuids[po.guid] = *po.pProxy->getGUID();
 		fora(j, po.aObjects)
 		{
 			IXEditorObject *pObj = XFindObjectByGUID(m_mapGuids[po.aObjects[j]]);
@@ -56,20 +59,51 @@ bool XMETHODCALLTYPE CCommandPaste::exec()
 		
 		po.pProxy->build();
 		g_pEditor->addObject(po.pProxy);
+
 		po.pProxy->setSelected(true);
 		add_ref(po.pProxy);
 		g_apProxies.push_back(po.pProxy);
 	}
 
+	fora(i, m_aGroups)
+	{
+		_group_obj &go = m_aGroups[i];
+
+		go.pGroup = (CGroupObject*)XFindObjectByGUID(m_mapGuids[go.guid]);
+
+		fora(j, go.aObjects)
+		{
+			IXEditorObject *pObj = XFindObjectByGUID(m_mapGuids[go.aObjects[j]]);
+			if(pObj)
+			{
+				go.pGroup->addChildObject(pObj);
+			}
+		}
+
+		go.pGroup->setSelected(true);
+	}
+
 	XUpdatePropWindow();
 	return(m_aObjects.size() != 0);
 }
 bool XMETHODCALLTYPE CCommandPaste::undo()
 {
+	forar(i, m_aGroups)
+	{
+		CGroupObject *pGroup = m_aGroups[i].pGroup;
+		
+		while(pGroup->getObjectCount())
+		{
+			pGroup->removeChildObject(pGroup->getObject(0));
+		}
+	}
+
 	forar(i, m_aProxies)
 	{
 		CProxyObject *pProxy = m_aProxies[i].pProxy;
 
+		m_mapGuids[m_aProxies[i].guid] = *pProxy->getTargetObject()->getGUID();
+
 		int idx = g_apProxies.indexOf(pProxy);
 		assert(idx >= 0);
 		if(idx >= 0)
@@ -87,8 +121,7 @@ bool XMETHODCALLTYPE CCommandPaste::undo()
 
 		pProxy->reset();
 	}
-
-
+	
 	_paste_obj *pObj;
 	forar(i, m_aObjects)
 	{
@@ -120,20 +153,31 @@ CCommandPaste::~CCommandPaste()
 
 UINT CCommandPaste::addObject(const char *szTypeName, const char *szClassName, const float3 &vPos, const float3 &vScale, const SMQuaternion &qRotate, const XGUID &oldGUID)
 {
-	const AssotiativeArray<AAString, IXEditable*>::Node *pNode;
-	if(!g_mEditableSystems.KeyExists(AAString(szTypeName), &pNode))
+	_paste_obj obj;
+	if(!fstrcmp(szTypeName, "TerraX"))
 	{
-		LibReport(REPORT_MSG_LEVEL_ERROR, "Unknown object type %s, skipping!", szTypeName);
-		return(UINT_MAX);
+		if(!fstrcmp(szClassName, "Group"))
+		{
+			obj.pObject = new CGroupObject();
+		}
+	}
+	else
+	{
+		const AssotiativeArray<AAString, IXEditable*>::Node *pNode;
+		if(!g_mEditableSystems.KeyExists(AAString(szTypeName), &pNode))
+		{
+			LibReport(REPORT_MSG_LEVEL_ERROR, "Unknown object type %s, skipping!\n", szTypeName);
+			return(UINT_MAX);
+		}
+		obj.pObject = (*(pNode->Val))->newObject(szClassName);
 	}
 
-	_paste_obj obj;
-	obj.pObject = (*(pNode->Val))->newObject(szClassName);
 	if(!obj.pObject)
 	{
-		LibReport(REPORT_MSG_LEVEL_ERROR, "Cannot create object type %s/%s, skipping!", szTypeName, szClassName);
+		LibReport(REPORT_MSG_LEVEL_ERROR, "Cannot create object type %s/%s, skipping!\n", szTypeName, szClassName);
 		return(UINT_MAX);
 	}
+
 	obj.vPos = vPos;
 	obj.vScale = vScale;
 	obj.qRotate = qRotate;
@@ -164,3 +208,16 @@ void CCommandPaste::addProxyObject(UINT uProxy, const XGUID &guid)
 	
 	m_aProxies[uProxy].aObjects.push_back(guid);
 }
+
+UINT CCommandPaste::addGroup(const XGUID &guid)
+{
+	m_aGroups.push_back({guid, NULL});
+
+	return(m_aGroups.size() - 1);
+}
+void CCommandPaste::addGroupObject(UINT uGroup, const XGUID &guid)
+{
+	assert(uGroup < m_aGroups.size());
+
+	m_aGroups[uGroup].aObjects.push_back(guid);
+}
diff --git a/source/terrax/CommandPaste.h b/source/terrax/CommandPaste.h
index 49cf504045c6378470509936aa248878759066e2..e962111a4113fb21ce6f30c4602ac4b44a7da60a 100644
--- a/source/terrax/CommandPaste.h
+++ b/source/terrax/CommandPaste.h
@@ -33,6 +33,9 @@ public:
 	UINT addProxy(const XGUID &guid);
 	void addProxyObject(UINT uProxy, const XGUID &guid);
 
+	UINT addGroup(const XGUID &guid);
+	void addGroupObject(UINT uGroup, const XGUID &guid);
+
 protected:
 	struct _paste_obj
 	{
@@ -57,6 +60,14 @@ protected:
 	Array<_proxy_obj> m_aProxies;
 
 	Map<XGUID, XGUID> m_mapGuids;
+
+	struct _group_obj
+	{
+		XGUID guid;
+		CGroupObject *pGroup;
+		Array<XGUID> aObjects;
+	};
+	Array<_group_obj> m_aGroups;
 };
 
 #endif
diff --git a/source/terrax/CommandUngroup.cpp b/source/terrax/CommandUngroup.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..46dc5f0a7377f5fbcf8bc7443b0463cb8408c206
--- /dev/null
+++ b/source/terrax/CommandUngroup.cpp
@@ -0,0 +1,101 @@
+#include "CommandUngroup.h"
+
+CCommandUngroup::CCommandUngroup(CGroupObject *pObject):
+	m_pGroup(pObject)
+{
+	add_ref(m_pGroup);
+	
+	IXEditorObject *pObj;
+	for(UINT i = 0, l = pObject->getObjectCount(); i < l; ++i)
+	{
+		pObj = pObject->getObject(i);
+		add_ref(pObj);
+		m_aObjects.push_back(pObj);
+	}
+}
+
+CCommandUngroup::~CCommandUngroup()
+{
+	fora(i, m_aObjects)
+	{
+		mem_release(m_aObjects[i]);
+	}
+	mem_release(m_pGroup);
+}
+
+bool XMETHODCALLTYPE CCommandUngroup::exec()
+{
+	ICompoundObject *pParent = XGetObjectParent(m_pGroup);
+
+	fora(i, m_aObjects)
+	{
+		m_pGroup->removeChildObject(m_aObjects[i]);
+		if(pParent)
+		{
+			pParent->addChildObject(m_aObjects[i]);
+		}
+		else
+		{
+			g_pEditor->onObjectAdded(m_aObjects[i]);
+		}
+	}
+
+	/*
+	int idx = g_apGroups.indexOf(m_pGroup);
+	assert(idx >= 0);
+	if(idx >= 0)
+	{
+		mem_release(g_apGroups[idx]);
+		g_apGroups.erase(idx);
+	}
+
+	//m_pProxy->setSelected(false);
+
+	g_pEditor->removeObject(m_pGroup);
+	*/
+	return(true);
+}
+bool XMETHODCALLTYPE CCommandUngroup::undo()
+{
+	ICompoundObject *pParent;
+	fora(i, m_aObjects)
+	{
+		pParent = XGetObjectParent(m_aObjects[i]);
+		SAFE_CALL(pParent, removeChildObject, m_aObjects[i]);
+		m_pGroup->addChildObject(m_aObjects[i]);
+	}
+
+	/*
+	fora(i, m_aModels)
+	{
+		IXEditorModel *pMdl = m_aModels[i];
+		assert(!g_apLevelModels.KeyExists(*pMdl->getGUID()));
+		g_apLevelModels[*pMdl->getGUID()] = pMdl;
+		add_ref(pMdl);
+		pMdl->restore();
+	}
+	fora(i, aObjModels)
+	{
+		ObjModel &om = aObjModels[i];
+		om.pModel->addObject(om.pObj);
+	}
+	
+	fora(i, m_aModels)
+	{
+		IXEditorModel *pMdl = m_aModels[i];
+		m_pProxy->addSrcModel(*pMdl->getGUID());
+	}
+
+	bool res = m_pProxy->setDstObject(*m_pEntity->getGUID());
+	assert(res);
+	m_pProxy->build();
+
+	g_pEditor->addObject(m_pProxy);
+
+	//m_pProxy->setSelected(true);
+
+	add_ref(m_pProxy);
+	g_apProxies.push_back(m_pProxy);
+	*/
+	return(true);
+}
diff --git a/source/terrax/CommandUngroup.h b/source/terrax/CommandUngroup.h
new file mode 100644
index 0000000000000000000000000000000000000000..0ce86b27ad97699a9c7f75868c4051658bc85d45
--- /dev/null
+++ b/source/terrax/CommandUngroup.h
@@ -0,0 +1,39 @@
+#ifndef _COMMAND_UNGROUP_H_
+#define _COMMAND_UNGROUP_H_
+
+#include <xcommon/editor/IXEditorExtension.h>
+#include "terrax.h"
+
+#include <common/assotiativearray.h>
+#include <common/string.h>
+#include <xcommon/editor/IXEditable.h>
+
+#include "CommandDelete.h"
+#include "ProxyObject.h"
+
+class CCommandUngroup final: public IXUnknownImplementation<IXEditorCommand>
+{
+public:
+	CCommandUngroup(CGroupObject *pObj);
+	~CCommandUngroup();
+
+	bool XMETHODCALLTYPE exec() override;
+	bool XMETHODCALLTYPE undo() override;
+
+	const char* XMETHODCALLTYPE getText() override
+	{
+		return("ungroup");
+	}
+
+	bool XMETHODCALLTYPE isEmpty() override
+	{
+		return(false);
+	}
+
+private:
+	CGroupObject *m_pGroup = NULL;
+	
+	Array<IXEditorObject*> m_aObjects;
+};
+
+#endif
diff --git a/source/terrax/GroupObject.cpp b/source/terrax/GroupObject.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5beb78e13a37a1a825bd2c00fd5406de316446e1
--- /dev/null
+++ b/source/terrax/GroupObject.cpp
@@ -0,0 +1,361 @@
+#include "GroupObject.h"
+
+#include <xcommon/resource/IXResourceManager.h>
+#include <xcommon/IXModelWriter.h>
+#include "terrax.h"
+#include "CommandDelete.h"
+#include <common/aastring.h>
+#include <core/sxcore.h>
+
+extern AssotiativeArray<AAString, IXEditable*> g_mEditableSystems;
+
+CGroupObject::CGroupObject()
+{
+	XCreateGUID(&m_guid);
+}
+
+CGroupObject::CGroupObject(const XGUID &guid)
+{
+	m_guid = guid;
+}
+
+CGroupObject::~CGroupObject()
+{
+	fora(i, m_aObjects)
+	{
+		mem_release(m_aObjects[i].pObj);
+	}
+}
+
+void XMETHODCALLTYPE CGroupObject::setPos(const float3_t &pos)
+{
+	m_vPos = pos;
+
+	fora(i, m_aObjects)
+	{
+		SrcObject &o = m_aObjects[i];
+		o.pObj->setPos((float3)(m_vPos + m_qOrient * o.vOffset));
+	}
+}
+
+void XMETHODCALLTYPE CGroupObject::setSize(const float3_t &vSize)
+{
+	float3 vMin, vMax, vScale;
+	getBound(&vMin, &vMax);
+	vScale = vSize / (vMax - vMin);
+	fora(i, m_aObjects)
+	{
+		SrcObject &o = m_aObjects[i];
+		o.vOffset = m_qOrient.Conjugate() * (float3)((o.pObj->getPos() - m_vPos) * vScale);
+		o.pObj->setPos((float3)(m_vPos + m_qOrient * o.vOffset));
+		o.pObj->getBound(&vMin, &vMax);
+		//printf("%.2f %.2f %.2f : %.2f %.2f %.2f\n", vMin.x, vMin.y, vMin.z, vMax.x, vMax.y, vMax.z);
+		o.pObj->setSize((float3)(vScale * (vMax - vMin)));
+	}
+}
+
+void XMETHODCALLTYPE CGroupObject::setOrient(const SMQuaternion &orient)
+{
+	m_qOrient = orient;
+	//m_pTargetObject->setOrient(orient);
+	fora(i, m_aObjects)
+	{
+		SrcObject &o = m_aObjects[i];
+		o.pObj->setOrient(orient * o.qOffset);
+		o.pObj->setPos((float3)(m_vPos + m_qOrient * o.vOffset));
+	}
+}
+
+float3_t XMETHODCALLTYPE CGroupObject::getPos()
+{
+	float3 vMin, vMax;
+	getBound(&vMin, &vMax);
+
+	m_vPos = (vMax + vMin) * 0.5f;
+
+	fora(i, m_aObjects)
+	{
+		m_aObjects[i].vOffset = m_qOrient.Conjugate() * (m_aObjects[i].pObj->getPos() - m_vPos);
+	}
+
+	return(m_vPos);
+}
+
+SMQuaternion XMETHODCALLTYPE CGroupObject::getOrient()
+{
+	fora(i, m_aObjects)
+	{
+		m_aObjects[i].qOffset = m_aObjects[i].pObj->getOrient() * m_qOrient.Conjugate();
+	}
+
+	return(m_qOrient);
+}
+
+void XMETHODCALLTYPE CGroupObject::getBound(float3 *pvMin, float3 *pvMax)
+{
+	if(!m_aObjects.size())
+	{
+		*pvMin = 0.0f;
+		*pvMax = 0.0f;
+		return;
+	}
+
+	float3 vMin, vMax;
+
+	m_aObjects[0].pObj->getBound(pvMin, pvMax);
+	for(UINT i = 1, l = m_aObjects.size(); i < l; ++i)
+	{
+		m_aObjects[i].pObj->getBound(&vMin, &vMax);
+		*pvMin = SMVectorMin(*pvMin, vMin);
+		*pvMax = SMVectorMax(*pvMax, vMax);
+	}
+}
+
+void XMETHODCALLTYPE CGroupObject::render(bool is3D, bool bRenderSelection, IXGizmoRenderer *pGizmoRenderer)
+{
+	fora(i, m_aObjects)
+	{
+		m_aObjects[i].pObj->render(is3D, bRenderSelection, pGizmoRenderer);
+	}
+}
+
+bool XMETHODCALLTYPE CGroupObject::rayTest(const float3 &vStart, const float3 &vEnd, float3 *pvOut, float3 *pvNormal, ID *pidMtrl, bool bReturnNearestPoint)
+{
+	if(bReturnNearestPoint)
+	{
+		float fBestDist = FLT_MAX;
+		float3 vPoint, vNormal;
+		ID idMtrl = -1;
+		float fDist;
+		bool isFound = false;
+
+		fora(i, m_aObjects)
+		{
+			if(m_aObjects[i].pObj->rayTest(vStart, vEnd, &vPoint, &vNormal, &idMtrl, bReturnNearestPoint))
+			{
+				fDist = SMVector3Length2(vPoint - vStart);
+				if(fDist < fBestDist)
+				{
+					fBestDist = fBestDist;
+					if(pvOut)
+					{
+						*pvOut = vPoint;
+					}
+					if(pvNormal)
+					{
+						*pvNormal = vNormal;
+					}
+					if(pidMtrl)
+					{
+						*pidMtrl = idMtrl;
+					}
+				}
+
+				isFound = true;
+			}
+		}
+
+		return(isFound);
+	}
+	else
+	{
+		fora(i, m_aObjects)
+		{
+			if(m_aObjects[i].pObj->rayTest(vStart, vEnd, pvOut, pvNormal, pidMtrl, bReturnNearestPoint))
+			{
+				return(true);
+			}
+		}
+	}
+
+	return(false);
+}
+
+void XMETHODCALLTYPE CGroupObject::remove()
+{
+	int idx = g_apGroups.indexOf(this);
+	assert(idx >= 0);
+	if(idx >= 0)
+	{
+		mem_release(g_apGroups[idx]);
+		g_apGroups.erase(idx);
+	}
+
+	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
+		g_mObjectsLocation.erase(pObj);
+		mem_release(pObj);
+		return(XEOR_SKIP_CHILDREN);
+	}, this);
+
+	m_isRemoved = true;
+	fora(i, m_aObjects)
+	{
+		m_aObjects[i].pObj->remove();
+	}
+}
+void XMETHODCALLTYPE CGroupObject::preSetup()
+{
+	//m_pTargetObject->preSetup();
+}
+void XMETHODCALLTYPE CGroupObject::postSetup()
+{
+	//m_pTargetObject->preSetup();
+}
+
+void XMETHODCALLTYPE CGroupObject::create()
+{
+	int idx = g_apGroups.indexOf(this);
+	assert(idx < 0);
+	if(idx < 0)
+	{
+		add_ref(this);
+		g_apGroups.push_back(this);
+	}
+
+	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
+		g_mObjectsLocation[pObj] = pParent;
+		add_ref(pObj);
+		return(XEOR_SKIP_CHILDREN);
+	}, this);
+
+
+	m_isRemoved = false;
+	fora(i, m_aObjects)
+	{
+		m_aObjects[i].pObj->create();
+	}
+}
+
+void XMETHODCALLTYPE CGroupObject::setKV(const char *szKey, const char *szValue)
+{
+	if(!fstrcmp(szKey, "guid"))
+	{
+		XGUIDFromString(&m_guid, szValue);
+	}
+	else if(!fstrcmp(szKey, "name"))
+	{
+		m_sName = szValue;
+	}
+}
+const char* XMETHODCALLTYPE CGroupObject::getKV(const char *szKey)
+{
+	if(!fstrcmp(szKey, "guid"))
+	{
+		char tmp[64];
+		XGUIDToSting(m_guid, tmp, sizeof(tmp));
+		m_sGUID = tmp;
+		return(m_sGUID.c_str());
+	}
+	else if(!fstrcmp(szKey, "name"))
+	{
+		return(m_sName.c_str());
+	}
+	return(NULL);
+}
+const X_PROP_FIELD* XMETHODCALLTYPE CGroupObject::getPropertyMeta(UINT uKey)
+{
+	static X_PROP_FIELD s_prop0 = {"guid", "GUID", XPET_TEXT, NULL, "", true};
+	static X_PROP_FIELD s_prop1 = {"name", "Name", XPET_TEXT, NULL, ""};
+	switch(uKey)
+	{
+	case 0:
+		return(&s_prop0);
+	case 1:
+		return(&s_prop1);
+	}
+	return(NULL);
+}
+UINT XMETHODCALLTYPE CGroupObject::getProperyCount()
+{
+	return(2);
+}
+
+const char* XMETHODCALLTYPE CGroupObject::getTypeName()
+{
+	return("TerraX");
+}
+const char* XMETHODCALLTYPE CGroupObject::getClassName()
+{
+	return("Group");
+}
+
+void XMETHODCALLTYPE CGroupObject::setSelected(bool set)
+{
+	m_isSelected = set;
+
+	fora(i, m_aObjects)
+	{
+		m_aObjects[i].pObj->setSelected(set);
+	}
+}
+
+void XMETHODCALLTYPE CGroupObject::setSimulationMode(bool set)
+{
+	fora(i, m_aObjects)
+	{
+		m_aObjects[i].pObj->setSimulationMode(set);
+	}
+}
+
+void CGroupObject::addChildObject(IXEditorObject *pObject)
+{
+	assert(pObject != this);
+
+	ICompoundObject *pOldContainer = XTakeObject(pObject, this);
+	assert(pOldContainer == NULL);
+	//add_ref(pObject);
+	m_aObjects.push_back({pObject, m_qOrient.Conjugate() * (pObject->getPos() - m_vPos), pObject->getOrient() * m_qOrient.Conjugate()});
+	
+	g_pEditor->onObjectAdded(pObject);
+}
+void CGroupObject::removeChildObject(IXEditorObject *pObject)
+{
+	ICompoundObject *pOldContainer = XTakeObject(pObject, NULL);
+	assert(pOldContainer == this);
+
+	int idx = m_aObjects.indexOf(pObject, [](const SrcObject &a, IXEditorObject *b){
+		return(a.pObj == b);
+	});
+	assert(idx >= 0);
+	if(idx >= 0)
+	{
+		m_aObjects.erase(idx);
+
+		g_pEditor->onObjectRemoved(pObject);
+
+		//mem_release(pObject);
+
+		if(!m_aObjects.size())
+		{
+			CCommandDelete *pCmd = new CCommandDelete();
+			pCmd->addObject(this);
+			XAttachCommand(pCmd);
+		}
+	}
+}
+
+UINT CGroupObject::getObjectCount()
+{
+	return(m_aObjects.size());
+}
+IXEditorObject* CGroupObject::getObject(UINT id)
+{
+	assert(id < m_aObjects.size());
+	if(id < m_aObjects.size())
+	{
+		return(m_aObjects[id].pObj);
+	}
+	return(NULL);
+}
+
+void XMETHODCALLTYPE CGroupObject::getInternalData(const XGUID *pGUID, void **ppOut)
+{
+	if(*pGUID == X_IS_GROUP_GUID || *pGUID == X_IS_COMPOUND_GUID)
+	{
+		*ppOut = (void*)1;
+	}
+	else
+	{
+		BaseClass::getInternalData(pGUID, ppOut);
+	}
+}
diff --git a/source/terrax/GroupObject.h b/source/terrax/GroupObject.h
new file mode 100644
index 0000000000000000000000000000000000000000..1e51d9fcf376eb7efdf1b92f6b1b1fc00e3a08f0
--- /dev/null
+++ b/source/terrax/GroupObject.h
@@ -0,0 +1,110 @@
+#ifndef __GROUPOBJECT_H
+#define __GROUPOBJECT_H
+
+#include <xcommon/editor/IXEditorObject.h>
+#include <xcommon/editor/IXEditable.h>
+#include "ICompoundObject.h"
+#include <common/string.h>
+
+
+// {B92F6791-0F82-4A47-BA46-6319149FEFED}
+#define X_IS_GROUP_GUID DEFINE_XGUID(0xb92f6791, 0xf82, 0x4a47, 0xba, 0x46, 0x63, 0x19, 0x14, 0x9f, 0xef, 0xed)
+
+
+//#############################################################################
+
+class CGroupObject final: public IXUnknownImplementation<ICompoundObject>
+{
+	DECLARE_CLASS(CGroupObject, IXUnknownImplementation<ICompoundObject>);
+public:
+	CGroupObject();
+	CGroupObject(const XGUID &guid);
+	~CGroupObject();
+
+	void XMETHODCALLTYPE setPos(const float3_t &pos) override;
+	void XMETHODCALLTYPE setOrient(const SMQuaternion &orient) override;
+	void XMETHODCALLTYPE setSize(const float3_t &vSize) override;
+
+	void XMETHODCALLTYPE getBound(float3 *pvMin, float3 *pvMax) override;
+
+	void XMETHODCALLTYPE render(bool is3D, bool bRenderSelection, IXGizmoRenderer *pGizmoRenderer) override;
+
+	bool XMETHODCALLTYPE rayTest(const float3 &vStart, const float3 &vEnd, float3 *pvOut = NULL, float3 *pvNormal = NULL, ID *pidMtrl = NULL, bool bReturnNearestPoint = false) override;
+
+	void XMETHODCALLTYPE remove() override;
+	void XMETHODCALLTYPE create() override;
+	void XMETHODCALLTYPE preSetup() override;
+	void XMETHODCALLTYPE postSetup() override;
+
+	void XMETHODCALLTYPE setKV(const char *szKey, const char *szValue) override;
+	const char* XMETHODCALLTYPE getKV(const char *szKey) override;
+	const X_PROP_FIELD* XMETHODCALLTYPE getPropertyMeta(UINT uKey) override;
+	UINT XMETHODCALLTYPE getProperyCount() override;
+
+	const char* XMETHODCALLTYPE getTypeName() override;
+	const char* XMETHODCALLTYPE getClassName() override;
+
+	float3_t XMETHODCALLTYPE getPos() override;
+
+	SMQuaternion XMETHODCALLTYPE getOrient() override;
+
+	bool XMETHODCALLTYPE isSelected() override
+	{
+		return(m_isSelected);
+	}
+	void XMETHODCALLTYPE setSelected(bool set) override;
+
+	IXTexture* XMETHODCALLTYPE getIcon() override
+	{
+		return(NULL);
+	}
+
+	void XMETHODCALLTYPE setSimulationMode(bool set) override;
+
+	bool XMETHODCALLTYPE hasVisualModel() override
+	{
+		return(true);
+	}
+
+	const XGUID* XMETHODCALLTYPE getGUID() override
+	{
+		return(&m_guid);
+	}
+
+	void XMETHODCALLTYPE getInternalData(const XGUID *pGUID, void **ppOut) override;
+
+	void addChildObject(IXEditorObject *pObject) override;
+	void removeChildObject(IXEditorObject *pObject) override;
+
+	UINT getObjectCount() override;
+	IXEditorObject* getObject(UINT id) override;
+
+	/*bool isRemoved()
+	{
+		return(m_isRemoved);
+	}
+	*/
+private:
+	XGUID m_guid;
+
+	bool m_isSelected = false;
+
+	bool m_isRemoved = false;
+
+	float3_t m_vPos;
+	SMQuaternion m_qOrient;
+
+	struct SrcObject
+	{
+		IXEditorObject *pObj;
+		float3_t vOffset;
+		SMQuaternion qOffset;
+	};
+
+	Array<SrcObject> m_aObjects;
+
+	String m_sGUID;
+	String m_sName;
+};
+
+#endif
diff --git a/source/terrax/ProxyObject.cpp b/source/terrax/ProxyObject.cpp
index ec0616104b832504b2fc8af3c48fecccfeeb1985..20d1e4131093faeec19e352d7cb0dd2ff5562ad3 100644
--- a/source/terrax/ProxyObject.cpp
+++ b/source/terrax/ProxyObject.cpp
@@ -140,6 +140,7 @@ void XMETHODCALLTYPE CProxyObject::remove()
 	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
 		g_mObjectsLocation.erase(pObj);
 		mem_release(pObj);
+		return(XEOR_SKIP_CHILDREN);
 	}, this);
 
 	m_isRemoved = true;
@@ -171,6 +172,7 @@ void XMETHODCALLTYPE CProxyObject::create()
 	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
 		g_mObjectsLocation[pObj] = pParent;
 		add_ref(pObj);
+		return(XEOR_SKIP_CHILDREN);
 	}, this);
 
 
@@ -235,6 +237,10 @@ bool CProxyObject::setDstObject(const XGUID &guid)
 	if(pObj)
 	{
 		m_pTargetObject = pObj;
+
+		ICompoundObject *pParent = XGetObjectParent(pObj);
+		SAFE_CALL(pParent, removeChildObject, pObj);
+
 		int idx = g_pLevelObjects.indexOf(pObj);
 		assert(idx >= 0);
 		if(idx >= 0)
@@ -253,7 +259,7 @@ bool CProxyObject::setDstObject(const XGUID &guid)
 	char tmp[64], tmp2[64];
 	XGUIDToSting(guid, tmp, sizeof(tmp));
 	XGUIDToSting(m_guid, tmp2, sizeof(tmp2));
-	LibReport(REPORT_MSG_LEVEL_ERROR, "Cannot set the object %s as the proxy %s target object. The щиоусе is not found\n", tmp, tmp2);
+	LibReport(REPORT_MSG_LEVEL_ERROR, "Cannot set the object %s as the proxy %s target object. The object is not found\n", tmp, tmp2);
 	return(false);
 }
 void CProxyObject::addSrcModel(const XGUID &guid)
@@ -453,6 +459,8 @@ void CProxyObject::addChildObject(IXEditorObject *pObject)
 		m_aObjects.push_back({pObject, m_qOrient.Conjugate() * (pObject->getPos() - m_vPos), pObject->getOrient() * m_qOrient.Conjugate()});
 
 		m_aModels[idx].pModel->addObject(pObject);
+
+		g_pEditor->onObjectAdded(pObject);
 	}
 }
 void CProxyObject::removeChildObject(IXEditorObject *pObject)
@@ -478,6 +486,8 @@ void CProxyObject::removeChildObject(IXEditorObject *pObject)
 			m_aModels[idx].pModel->removeObject(pObject);
 		}
 
+		g_pEditor->onObjectRemoved(pObject);
+
 		mem_release(pObject);
 	}
 }
diff --git a/source/terrax/SceneTreeWindow.cpp b/source/terrax/SceneTreeWindow.cpp
index e39d400f37f5dd7e13754f59c3769884f6dcc8a3..e423352cd9a8e8f859e8d1a6a0d01168e734bdb7 100644
--- a/source/terrax/SceneTreeWindow.cpp
+++ b/source/terrax/SceneTreeWindow.cpp
@@ -44,6 +44,9 @@ CSceneTreeWindow::CSceneTreeWindow(CEditor *pEditor, IXCore *pCore):
 	m_pTreeMenu->addItem("Copy\tCtrl+C", "copy");
 	m_pTreeMenu->addItem("Delete\tDel", "delete");
 	m_pTreeMenu->addSeparator();
+	m_pTreeMenu->addItem("Group\tCtrl+G", "group");
+	m_pTreeMenu->addItem("Ungroup\tCtrl+U", "ungroup");
+	m_pTreeMenu->addSeparator();
 	m_pTreeMenu->addItem("To Object\tCtrl+T", "to_object");
 	m_pTreeMenu->addItem("To World\tCtrl+Shift+T", "to_world");
 	m_pTreeMenu->addSeparator();
@@ -75,6 +78,9 @@ CSceneTreeWindow::CSceneTreeWindow(CEditor *pEditor, IXCore *pCore):
 	pAccelTable->addItem({XAF_VIRTKEY, KEY_F2}, "rename");
 	pAccelTable->addItem({XAF_CTRL | XAF_VIRTKEY, KEY_E}, "center_on_selection");
 	pAccelTable->addItem({XAF_CTRL | XAF_SHIFT | XAF_VIRTKEY, KEY_E}, "go_to_selection");
+	pAccelTable->addItem({XAF_CTRL | XAF_VIRTKEY, KEY_G}, "group");
+	pAccelTable->addItem({XAF_CTRL | XAF_VIRTKEY, KEY_U}, "ungroup");
+	pAccelTable->addItem({XAF_CTRL | XAF_SHIFT | XAF_VIRTKEY, KEY_G}, "ungroup");
 	m_pWindow->setAcceleratorTable(pAccelTable);
 	mem_release(pAccelTable);
 
@@ -117,6 +123,12 @@ CSceneTreeWindow::CSceneTreeWindow(CEditor *pEditor, IXCore *pCore):
 	m_pWindow->addCommand("rename", [](void *pCtx){
 		((CSceneTreeWindow*)pCtx)->m_pTree->editSelectedNode();
 	}, this);
+	m_pWindow->addCommand("group", [](void *pCtx){
+		((CSceneTreeWindow*)pCtx)->sendParentCommand(ID_TOOLS_GROUP);
+	}, this);
+	m_pWindow->addCommand("ungroup", [](void *pCtx){
+		((CSceneTreeWindow*)pCtx)->sendParentCommand(ID_TOOLS_UNGROUP);
+	}, this);
 
 	onResize();
 
@@ -369,6 +381,11 @@ static int CompareNodes(const CSceneTreeAdapter::TreeNode &a, const CSceneTreeAd
 	return(cmp);
 }
 
+CSceneTreeAdapter::CSceneTreeAdapter()
+{
+	m_rootNode.isExpanded = true;
+}
+
 void CSceneTreeAdapter::setTree(IUITree *pTree)
 {
 	m_pTree = pTree;
@@ -617,6 +634,10 @@ bool CSceneTreeAdapter::isNodeSelected(UITreeNodeHandle hNode)
 
 bool CSceneTreeAdapter::onNodeExpanded(UITreeNodeHandle hNode, bool isExpanded, bool isRecursive)
 {
+	if(!hNode)
+	{
+		return(true);
+	}
 	TreeNode *pNode = findTreeNode((IXEditorObject*)hNode);
 	assert(pNode);
 	if(pNode)
@@ -665,6 +686,7 @@ bool CSceneTreeAdapter::onNodeSelected(UITreeNodeHandle hNode, bool isSelected,
 					pCmd->addDeselected(pObj);
 				}
 			}
+			return(XEOR_CONTINUE);
 		});
 	}
 
@@ -705,6 +727,7 @@ bool CSceneTreeAdapter::onMultiSelected(UITreeNodeHandle *aNodes, UINT uNodeCoun
 					pCmd->addDeselected(pObj);
 				}
 			}
+			return(XEOR_CONTINUE);
 		});
 	}
 
@@ -734,10 +757,12 @@ void CSceneTreeAdapter::onNodeEdited(UITreeNodeHandle hNode, const char *szNewTe
 {
 	CCommandProperties *pCmd = new CCommandProperties();
 	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-		if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+		if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 		{
 			pCmd->addObject(pObj);
+			return(XEOR_SKIP_CHILDREN);
 		}
+		return(XEOR_CONTINUE);
 	});
 	//pCmd->addObject((IXEditorObject*)hNode);
 	pCmd->setKV("name", szNewText);
@@ -756,19 +781,49 @@ void CSceneTreeAdapter::ensureExpanded(UITreeNodeHandle hNode)
 	}
 }
 
-void CSceneTreeAdapter::onObjectsetChanged()
+static void SaveExpansionState(CSceneTreeAdapter::TreeNode *pNode, Map<IXEditorObject*, bool> *pMap)
+{
+	if(pNode->isExpanded)
+	{
+		(*pMap)[pNode->pObject] = true;
+	}
+	fora(i, pNode->aChildren)
+	{
+		SaveExpansionState(&pNode->aChildren[i], pMap);
+	}
+}
+
+static void RestoreExpansionState(CSceneTreeAdapter::TreeNode *pNode, Map<IXEditorObject*, bool> *pMap, CSceneTreeAdapter *pAdapter)
 {
-	m_rootNode.aChildren.clearFast();
-	m_rootNode.aChildren.reserve(g_pLevelObjects.size());
+	if(pMap->KeyExists(pNode->pObject))
+	{
+		pAdapter->onNodeExpanded((UITreeNodeHandle)pNode->pObject, true, false);
+
+		fora(i, pNode->aChildren)
+		{
+			RestoreExpansionState(&pNode->aChildren[i], pMap, pAdapter);
+		}
+	}
+}
 
+void CSceneTreeAdapter::onObjectsetChanged()
+{
 	if(m_hasFilter)
 	{
+		m_rootNode.aChildren.clearFast();
+		m_rootNode.aChildren.reserve(g_pLevelObjects.size());
 		// load filtered recursive
 		bool hasItems = false;
 		loadFiltered(&m_rootNode, NULL, &hasItems);
 	}
 	else
 	{
+		Map<IXEditorObject*, bool> mapExpansionState;
+		SaveExpansionState(&m_rootNode, &mapExpansionState);
+
+		m_rootNode.aChildren.clearFast();
+		m_rootNode.aChildren.reserve(g_pLevelObjects.size());
+
 		TreeNode tmp;
 
 		fora(i, g_pLevelObjects)
@@ -779,17 +834,16 @@ void CSceneTreeAdapter::onObjectsetChanged()
 
 			tmp.pObject = pObj;
 			m_rootNode.aChildren.push_back(tmp);
-			/*
-			//Func(pObj, isProxy ? true : false, pWhere);
-
 
-			if(isProxy)
+			if(mapExpansionState.KeyExists(pObj))
 			{
-				((CProxyObject*)pObj)->getObjectCount();
-			}*/
+				onNodeExpanded((UITreeNodeHandle)pObj, true, false);
+			}
 		}
 
 		sortChildren(&m_rootNode);
+
+		RestoreExpansionState(&m_rootNode, &mapExpansionState, this);
 	}
 
 	m_pTree->notifyDatasetChanged();
@@ -847,10 +901,13 @@ bool CSceneTreeAdapter::hasSelection()
 {
 	bool hasSelection = false;
 	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-		if(!hasSelection && pObj->isSelected())
+		if(pObj->isSelected())
 		{
 			hasSelection = true;
+
+			return(XEOR_STOP);
 		}
+		return(XEOR_CONTINUE);
 	});
 
 	return(hasSelection);
diff --git a/source/terrax/SceneTreeWindow.h b/source/terrax/SceneTreeWindow.h
index 29cf9645329c4de1b24a0562992b17c573c5a249..aaaa811854359c9bf52037b825b8e46b531877ac 100644
--- a/source/terrax/SceneTreeWindow.h
+++ b/source/terrax/SceneTreeWindow.h
@@ -10,6 +10,8 @@ class ICompoundObject;
 class CSceneTreeAdapter final: public IUITreeAdapter
 {
 public:
+	CSceneTreeAdapter();
+
 	void setTree(IUITree *pTree);
 
 	UINT getColumnCount() override;
diff --git a/source/terrax/UndoManager.cpp b/source/terrax/UndoManager.cpp
index d658b5b5c1773fa23f4568723e98f858644842f8..17b0c63a7a19bbddc6010be16ef6509fc1443f69 100644
--- a/source/terrax/UndoManager.cpp
+++ b/source/terrax/UndoManager.cpp
@@ -84,13 +84,12 @@ bool CUndoManager::execCommand(IXEditorCommand *pCommand, bool bSaveForUndo)
 	++m_isInCommandContext;
 	if(!pCommand->isEmpty() && pCommand->exec())
 	{
-
 		if(aAttachedCommands.size())
 		{
 			// create new command container
 			CCommandContainer *pContainer = new CCommandContainer();
 			pContainer->addCommand(pCommand);
-			fora(i, aAttachedCommands)
+			for(UINT i = 0; i < aAttachedCommands.size(); ++i) // size can change during iteration
 			{
 				aAttachedCommands[i]->exec();
 				pContainer->addCommand(aAttachedCommands[i]);
diff --git a/source/terrax/mainWindow.cpp b/source/terrax/mainWindow.cpp
index 1ebdac59add1623670cf478be2ec56f2f78dc866..6996e46fb3b992688fdb054b67f5426a06bdd360 100644
--- a/source/terrax/mainWindow.cpp
+++ b/source/terrax/mainWindow.cpp
@@ -40,6 +40,8 @@
 #include "CommandBuildModel.h"
 #include "CommandDestroyModel.h"
 #include "CommandModifyModel.h"
+#include "CommandGroup.h"
+#include "CommandUngroup.h"
 
 #include "PropertyWindow.h"
 
@@ -197,10 +199,12 @@ public:
 		
 		m_pPropsCmd = new CCommandProperties();
 		XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-			if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+			if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 			{
 				m_pPropsCmd->addObject(pObj);
+				return(XEOR_SKIP_CHILDREN);
 			}
+			return(XEOR_CONTINUE);
 		});
 		
 		for(UINT i = 0, l = g_pPropWindow->getCustomTabCount(); i < l; ++i)
@@ -542,10 +546,12 @@ static void DeleteSelection()
 {
 	CCommandDelete *pDelCmd = new CCommandDelete();
 	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-		if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+		if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 		{
 			pDelCmd->addObject(pObj);
+			return(XEOR_SKIP_CHILDREN);
 		}
+		return(XEOR_CONTINUE);
 	});
 	XExecCommand(pDelCmd);
 }
@@ -667,6 +673,37 @@ static void ToClipboard(bool isCut = false)
 	pConfig->set("meta", "proxy_count", szSection);
 
 
+	uCount = 0;
+	for(UINT i = 0, l = g_apGroups.size(); i < l; ++i)
+	{
+		CGroupObject *pObj = g_apGroups[i];
+		if(pObj->isSelected())
+		{
+			sprintf(szSection, "group_%u", uCount);
+
+			XGUIDToSting(*pObj->getGUID(), szTmp, sizeof(szTmp));
+			pConfig->set(szSection, "guid", szTmp);
+
+			UINT uObjCount = 0;
+			sprintf(szTmp, "%u", pObj->getObjectCount());
+			pConfig->set(szSection, "o_count", szTmp);
+
+			for(UINT i = 0, l = pObj->getObjectCount(); i < l; ++i)
+			{
+				XGUIDToSting(*pObj->getObject(i)->getGUID(), szTmp, sizeof(szTmp));
+				sprintf(szKey, "o_%u", uObjCount);
+				pConfig->set(szSection, szKey, szTmp);
+				++uObjCount;
+			}
+
+			++uCount;
+		}
+	}
+
+	sprintf(szSection, "%u", uCount);
+	pConfig->set("meta", "group_count", szSection);
+
+
 	sprintf(szSection, "%f %f %f", g_xState.vSelectionBoundMin.x, g_xState.vSelectionBoundMin.y, g_xState.vSelectionBoundMin.z);
 	pConfig->set("meta", "aabb_min", szSection);
 	sprintf(szSection, "%f %f %f", g_xState.vSelectionBoundMax.x, g_xState.vSelectionBoundMax.y, g_xState.vSelectionBoundMax.z);
@@ -896,6 +933,36 @@ guid = {9D7D2E62-24C7-42B7-8D83-8448FC4604F0}
 				}
 			}
 
+			szVal = pConfig->getKey("meta", "group_count");
+			if(szVal)
+			{
+				sscanf(szVal, "%u", &uCount);
+				for(UINT i = 0; i < uCount; ++i)
+				{
+					sprintf(szSection, "group_%u", i);
+					const char *szTmp;
+					XGUID guid;
+					UINT uObjCount;
+					if(
+						(szTmp = pConfig->getKey(szSection, "guid")) && XGUIDFromString(&guid, szTmp)
+						&& (szTmp = pConfig->getKey(szSection, "o_count")) && sscanf(szTmp, "%u", &uObjCount)
+						)
+					{
+						char szKey[64];
+						UINT uGroup = pCmd->addGroup(guid);
+						for(UINT j = 0; j < uObjCount; ++j)
+						{
+							sprintf(szKey, "o_%u", j);
+							if((szTmp = pConfig->getKey(szSection, szKey)) && XGUIDFromString(&guid, szTmp))
+							{
+								pCmd->addGroupObject(uGroup, guid);
+							}
+						}
+					}
+
+				}
+			}
+
 			XExecCommand(pCmd);
 		}
 	}
@@ -1332,6 +1399,21 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 			EnableMenuItem(hMenu, ID_EDIT_COPY, hasSelection ? MF_ENABLED : MF_DISABLED);
 			EnableMenuItem(hMenu, ID_EDIT_DELETE, hasSelection ? MF_ENABLED : MF_DISABLED);
 			EnableMenuItem(hMenu, ID_EDIT_PASTE, GetFileAttributesA(g_szClipboardFile) != ~0 ? MF_ENABLED : MF_DISABLED);
+			EnableMenuItem(hMenu, ID_TOOLS_CONVERTTOENTITY, hasSelection && IsWindowEnabled(g_hButtonToEntityWnd) ? MF_ENABLED : MF_DISABLED);
+			EnableMenuItem(hMenu, ID_TOOLS_CONVERTTOSTATIC, hasSelection ? MF_ENABLED : MF_DISABLED);
+			EnableMenuItem(hMenu, ID_TOOLS_GROUP, hasSelection ? MF_ENABLED : MF_DISABLED);
+
+			bool hasGroupSelected = false;
+			fora(i, g_apGroups)
+			{
+				if(g_apGroups[i]->isSelected())
+				{
+					hasGroupSelected = true;
+					break;
+				}
+			}
+
+			EnableMenuItem(hMenu, ID_TOOLS_UNGROUP, hasGroupSelected ? MF_ENABLED : MF_DISABLED);
 		}
 		XUpdateUndoRedo();
 
@@ -1911,6 +1993,46 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 			}
 			break;
 
+		case ID_TOOLS_GROUP:
+			XExecCommand(new CCommandGroup());
+			break;
+
+		case ID_TOOLS_UNGROUP:
+			//if(!g_xConfig.m_bIgnoreGroups)
+			{
+				CCommandContainer *pContainer = NULL;
+				fora(i, g_apGroups)
+				{
+					CGroupObject *pGroup = g_apGroups[i];
+					if(pGroup->isSelected())
+					{
+						ICompoundObject *pParent = pGroup;
+						bool bSkip = false;
+						while((pParent = XGetObjectParent(pParent)))
+						{
+							if(pParent->isSelected())
+							{
+								bSkip = true;
+								break;
+							}
+						}
+						if(!bSkip)
+						{
+							if(!pContainer)
+							{
+								pContainer = new CCommandContainer();
+							}
+							pContainer->addCommand(new CCommandUngroup(pGroup));
+						}
+					}
+				}
+				if(pContainer)
+				{
+					XExecCommand(pContainer);
+				}
+			}
+			break;
+
 		case ID_HELP_SKYXENGINEWIKI:
 			ShellExecute(0, 0, "https://wiki.skyxengine.com", 0, 0, SW_SHOW);
 			break;
@@ -1924,10 +2046,12 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 			{
 				CCommandSelect *pCmdUnselect = new CCommandSelect();
 				XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-					if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+					if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 					{
 						pCmdUnselect->addDeselected(pObj);
+						return(XEOR_SKIP_CHILDREN);
 					}
+					return(XEOR_CONTINUE);
 				});
 				g_pUndoManager->execCommand(pCmdUnselect);
 			}
@@ -1937,10 +2061,12 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 			{
 				CCommandSelect *pCmdSelect = new CCommandSelect();
 				XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-					if(!pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+					if(!pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 					{
 						pCmdSelect->addSelected(pObj);
+						return(XEOR_SKIP_CHILDREN);
 					}
+					return(XEOR_CONTINUE);
 				});
 				g_pUndoManager->execCommand(pCmdSelect);
 			}
@@ -2069,10 +2195,12 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 			{
 				CCommandRotate *pCmd = new CCommandRotate(GetKeyState(VK_SHIFT) < 0);
 				XEnumerateObjects([pCmd](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-					if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+					if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 					{
 						pCmd->addObject(pObj);
+						return(XEOR_SKIP_CHILDREN);
 					}
+					return(XEOR_CONTINUE);
 				});
 
 				X_2D_VIEW xCurView = g_xConfig.m_x2DView[g_xState.activeWindow];
@@ -2128,6 +2256,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 									hasUnselectedChild = true;
 								}
 							}
+
+							return(XEOR_CONTINUE);
 						}, (ICompoundObject*)pObj);
 						if(hasUnselectedChild)
 						{
@@ -2152,6 +2282,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 										pCmdUnselect->addDeselected(pObj);
 									}
 								}
+								return(XEOR_CONTINUE);
 							}, (ICompoundObject*)pObj);
 							if(pObj->isSelected())
 							{
@@ -2177,6 +2308,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 											pCmdUnselect->addDeselected(pParent);
 										}
 									}
+									return(XEOR_CONTINUE);
 								}, (ICompoundObject*)pObj);
 							}
 						}
@@ -2190,12 +2322,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 									{
 										pCmdUnselect->addDeselected(pObj);
 									}
+									return(XEOR_CONTINUE);
 								}, (ICompoundObject*)pObj);
 								pCmdUnselect->addSelected(pObj);
 							}
 							
 						}
 					}
+					return(XEOR_CONTINUE);
 				});
 				
 				pCmdUnselect->setIGMode(CCommandSelect::IGM_ENABLE);
@@ -2896,6 +3030,7 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 							}
 						}
 					}
+					return(XEOR_CONTINUE);
 				});
 
 				if(bUse)
@@ -3052,7 +3187,8 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 
 				g_aRaytracedItems.clearFast();
 				XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-					if(g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)
+					//if(g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)
+					if(!g_xConfig.m_bIgnoreGroups || !isProxy)
 					{
 						float fDist2 = -1.0f;
 						if(!pObj->hasVisualModel())
@@ -3087,6 +3223,8 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 							g_aRaytracedItems.push_back({fDist2, pObj});
 						}
 					}
+
+					return(XEOR_CONTINUE);
 				});
 				
 				g_aRaytracedItems.quickSort([](const SelectItem &a, const SelectItem &b){
@@ -3123,6 +3261,7 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 							}
 						}
 					}
+					return(XEOR_CONTINUE);
 				});
 				
 				s_aRaytracedItems.quickSort([](const SelectItem2 &a, const SelectItem2 &b){
@@ -3267,10 +3406,12 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 								s_pScaleCmd->setTransformDir(dirs[g_xConfig.m_x2DView[g_xState.activeWindow]][i]);
 								s_pScaleCmd->setStartPos(vStartPos);
 								XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-									if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+									if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 									{
 										s_pScaleCmd->addObject(pObj);
+										return(XEOR_SKIP_CHILDREN);
 									}
+									return(XEOR_CONTINUE);
 								});
 							}
 							else if(g_xState.xformType == X2DXF_ROTATE)
@@ -3280,10 +3421,12 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 								s_pRotateCmd->setStartOrigin((g_xState.vSelectionBoundMax + g_xState.vSelectionBoundMin) * 0.5f * vMask, float3(1.0f) - vMask);
 								s_pRotateCmd->setStartPos(vStartPos);
 								XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-									if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+									if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 									{
 										s_pRotateCmd->addObject(pObj);
+										return(XEOR_SKIP_CHILDREN);
 									}
+									return(XEOR_CONTINUE);
 								});
 							}
 							bHandled = true;
@@ -3308,7 +3451,7 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 					bool wasSel = false;
 
 					XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-						if(!(g_xConfig.m_bIgnoreGroups && isProxy))
+						//if(!(g_xConfig.m_bIgnoreGroups && isProxy))
 						{
 							bool sel = XIsClicked(pObj->getPos());
 
@@ -3355,6 +3498,7 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 								}
 							}
 						}
+						return(XEOR_CONTINUE);
 					});
 
 					if(bUse)
@@ -3386,25 +3530,44 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 					s_pMoveCmd->setStartPos(XSnapToGrid(vStartPos));
 
 					bool bReferenceFound = false;
-
+					IXEditorObject *pReferenceObject = NULL;
 					float3 vBoundMin, vBoundMax;
 					XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
 						if(pObj->isSelected())
 						{
-							if(g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)
-							{
-								s_pMoveCmd->addObject(pObj);
-							}
-							if(!bReferenceFound && g_xConfig.m_bSnapGrid)
+							s_pMoveCmd->addObject(pObj);
+							return(XEOR_SKIP_CHILDREN);
+						}
+						return(XEOR_CONTINUE);
+					});
+
+					if(g_xConfig.m_bSnapGrid)
+					{
+						XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
+							if(pObj->isSelected())
 							{
-								pObj->getBound(&vBoundMin, &vBoundMax);
-								if(XIsMouseInBound(g_xState.activeWindow, vBoundMin, vBoundMax))
+								if((!bReferenceFound || (pReferenceObject == pParent)))
 								{
-									bReferenceFound = true;
+									pObj->getBound(&vBoundMin, &vBoundMax);
+									if(XIsMouseInBound(g_xState.activeWindow, vBoundMin, vBoundMax))
+									{
+										bReferenceFound = true;
+										pReferenceObject = pObj;
+
+										if(!isProxy)
+										{
+											return(XEOR_STOP);
+										}
+									}
+									else
+									{
+										return(XEOR_SKIP_CHILDREN);
+									}
 								}
 							}
-						}
-					});
+							return(XEOR_CONTINUE);
+						});
+					}
 
 					if(!bReferenceFound)
 					{
@@ -3917,11 +4080,13 @@ void XFrameRun(float fDeltaTime)
 			if(g_uSelectedIndex == ~0 && !g_isSelectionCtrl)
 			{
 				XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-					if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+					//if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+					if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups || !pParent))
 					{
 						pObj->setSelected(false);
 						g_pSelectCmd->addDeselected(pObj);
 					}
+					return(XEOR_CONTINUE);
 				});
 			}
 
@@ -4430,7 +4595,7 @@ void XUpdatePropWindow()
 	UINT uSelectedCount = 0;
 
 	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-		if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+		if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 		{
 			++uSelectedCount;
 			if(!szFirstType)
@@ -4490,7 +4655,9 @@ void XUpdatePropWindow()
 					mProps[AAString(pField->szKey)] = {*pField, true, pObj->getKV(pField->szKey)};
 				}
 			}
+			return(XEOR_SKIP_CHILDREN);
 		}
+		return(XEOR_CONTINUE);
 	});
 
 	XCleanupUnreferencedPropGizmos();
@@ -4590,10 +4757,12 @@ void XMETHODCALLTYPE CGizmoMoveCallback::onStart(IXEditorGizmoMove *pGizmo)
 	m_pCmd = new CCommandMove(GetKeyState(VK_SHIFT) < 0);
 	m_pCmd->setStartPos(pGizmo->getPos());
 	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-		if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+		if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 		{
 			m_pCmd->addObject(pObj);
+			return(XEOR_SKIP_CHILDREN);
 		}
+		return(XEOR_CONTINUE);
 	});
 }
 void XMETHODCALLTYPE CGizmoMoveCallback::onEnd(IXEditorGizmoMove *pGizmo)
@@ -4626,10 +4795,12 @@ void XMETHODCALLTYPE CGizmoRotateCallback::onStart(const float3_t &vAxis, IXEdit
 	m_pCmd->setStartOrigin(pGizmo->getPos(), vAxis);
 	m_pCmd->setStartPos(pGizmo->getPos() + vStartOffset);
 	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-		if(pObj->isSelected() && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent))
+		if(pObj->isSelected()/* && (g_xConfig.m_bIgnoreGroups ? !isProxy : !pParent)*/)
 		{
 			m_pCmd->addObject(pObj);
+			return(XEOR_SKIP_CHILDREN);
 		}
+		return(XEOR_CONTINUE);
 	});
 
 	pGizmo->setOrient(SMQuaternion());
@@ -4649,6 +4820,11 @@ void CheckToolbarButton(int iCmd, BOOL isChecked)
 	SendMessage(g_hToolbarWnd, TB_CHECKBUTTON, iCmd, MAKELPARAM(isChecked, 0));
 }
 
+void EnableToolbarButton(int iCmd, BOOL isChecked)
+{
+	SendMessage(g_hToolbarWnd, TB_ENABLEBUTTON, iCmd, MAKELPARAM(isChecked, 0));
+}
+
 void CheckXformButton(X_2DXFORM_TYPE type, bool isChecked)
 {
 	int iCmd = 0;
@@ -4672,14 +4848,14 @@ HWND CreateToolbar(HWND hWndParent)
 {
 	// Declare and initialize local constants.
 	const int ImageListID = 0;
-	const int numButtons = 4;
+	const int numButtons = 8;
 	const int bitmapSize = 16;
 
 	const DWORD buttonStyles = BTNS_AUTOSIZE;
 
 	// Create the toolbar.
 	HWND hWndToolbar = CreateWindowEx(0, TOOLBARCLASSNAME, NULL,
-		WS_CHILD | TBSTYLE_WRAPABLE | TBSTYLE_LIST | TBSTYLE_FLAT | TBSTYLE_TOOLTIPS, 0, 0, 0, 0,
+		WS_CHILD | /*TBSTYLE_WRAPABLE | */TBSTYLE_LIST | TBSTYLE_FLAT | TBSTYLE_TOOLTIPS, 0, 0, 0, 0,
 		hWndParent, NULL, hInst, NULL);
 
 	//SetWindowLong(hWndToolbar, GWL_EXSTYLE, GetWindowLong(hWndToolbar, GWL_EXSTYLE) | TBSTYLE_EX_MIXEDBUTTONS);
@@ -4732,6 +4908,8 @@ HWND CreateToolbar(HWND hWndParent)
 		{MAKELONG(1, ImageListID), ID_XFORM_TRANSLATE, TBSTATE_ENABLED, buttonStyles, {0}, 0, (INT_PTR)"Move [W]"},
 		{MAKELONG(2, ImageListID), ID_XFORM_ROTATE, TBSTATE_ENABLED, buttonStyles, {0}, 0, (INT_PTR)"Rotate [R]"},
 		{0, 0, TBSTATE_ENABLED, BTNS_SEP, 0L, 0},
+		{MAKELONG(6, ImageListID), ID_TOOLS_GROUP, TBSTATE_ENABLED, buttonStyles, {0}, 0, (INT_PTR)"Group selected [Ctrl+G]"},
+		{MAKELONG(7, ImageListID), ID_TOOLS_UNGROUP, TBSTATE_ENABLED, buttonStyles, {0}, 0, (INT_PTR)"Ungroup selected [Ctrl+U]"},
 		{MAKELONG(5, ImageListID), ID_IGNORE_GROUPS, TBSTATE_ENABLED, buttonStyles, {0}, 0, (INT_PTR)"Toggle group ignore [Ctrl+W]"},
 		{0, 0, TBSTATE_ENABLED, BTNS_SEP, 0L, 0},
 		{MAKELONG(3, ImageListID), ID_LEVEL_RUN, TBSTATE_ENABLED, buttonStyles, {0}, 0, (INT_PTR)"Run [F5]"}
@@ -4746,7 +4924,7 @@ HWND CreateToolbar(HWND hWndParent)
 	SendMessage(hWndToolbar, TB_SETEXTENDEDSTYLE, 0, (LPARAM)TBSTYLE_EX_MIXEDBUTTONS);
 	ShowWindow(hWndToolbar, TRUE);
 
-	return hWndToolbar;
+	return(hWndToolbar);
 }
 
 void XSetXformType(X_2DXFORM_TYPE type)
diff --git a/source/terrax/resource.h b/source/terrax/resource.h
index d5ba81e5ad59d168a6b76c7d8032749b41f9af8c..bdbd2937659c1c2521c71074b5341ca191f4b3a6 100644
Binary files a/source/terrax/resource.h and b/source/terrax/resource.h differ
diff --git a/source/terrax/resource/toolbar1.bmp b/source/terrax/resource/toolbar1.bmp
index aa1f7466d2b9315dfbfdf34a4cb6bf69abf3b6da..dbb1f0b437a3419ade41cafc9834107c00921d22 100644
Binary files a/source/terrax/resource/toolbar1.bmp and b/source/terrax/resource/toolbar1.bmp differ
diff --git a/source/terrax/resource/toolbar2.bmp b/source/terrax/resource/toolbar2.bmp
index 710d69c5e27e3c5948ab841bcdd9cbcc3619d9b2..5051572bc72dbbd0c0b1a1af2b48dcd827182f2d 100644
Binary files a/source/terrax/resource/toolbar2.bmp and b/source/terrax/resource/toolbar2.bmp differ
diff --git a/source/terrax/terrax.cpp b/source/terrax/terrax.cpp
index 8d4994851eec23ad2b8ec8059b42a42c70b969eb..56df654753d9174e57e199ded9859da02a34a061 100644
--- a/source/terrax/terrax.cpp
+++ b/source/terrax/terrax.cpp
@@ -75,6 +75,7 @@ Map<AAString, IXEditable*> g_mEditableSystems;
 Map<XGUID, IXEditorModel*> g_apLevelModels;
 Map<IXEditorObject*, ICompoundObject*> g_mObjectsLocation;
 Array<CProxyObject*> g_apProxies;
+Array<CGroupObject*> g_apGroups;
 //SGeom_GetCountModels()
 Array<IXEditorImporter*> g_pEditorImporters;
 
@@ -1005,6 +1006,12 @@ int main(int argc, char **argv)
 				mem_release(g_apProxies[i]);
 			}
 			g_apProxies.clear();
+
+			fora(i, g_apGroups)
+			{
+				mem_release(g_apGroups[i]);
+			}
+			g_apGroups.clear();
 			
 			for(Map<XGUID, IXEditorModel*>::Iterator i = g_apLevelModels.begin(); i; ++i)
 			{
@@ -1075,72 +1082,118 @@ int main(int argc, char **argv)
 				pCfg->save();
 				mem_release(pCfg);
 
-				// save proxies
-				sprintf(szPathLevel, "levels/%s/editor/proxies.json", pData->szLevelName);
-				
+
+				// save groups
+				sprintf(szPathLevel, "levels/%s/editor/groups.json", pData->szLevelName);
+
 				IFile *pFile = pFS->openFile(szPathLevel, FILE_MODE_WRITE);
 				if(!pFile)
 				{
 					LibReport(REPORT_MSG_LEVEL_ERROR, "Unable to save data '%s'\n", szPathLevel);
-					return;
 				}
-
-				pFile->writeText("[\n");
-
-				sprintf(szPathLevel, "levels/%s/models", pData->szLevelName);
-				pFS->deleteDirectory(szPathLevel);
-				//IFileIterator *pIter = pFS->getFileList(szPathLevel, "dse");
-				//const char *szFile;
-				//while((szFile = pIter->next()))
-				//{
-				//	//printf("%s\n", szFile);
-				//	pFS->deleteFile(szFile);
-				//}
-
-				bool isFirst = true;
-				char tmp[64];
-				fora(i, g_apProxies)
+				else
 				{
-					CProxyObject *pProxy = g_apProxies[i];
-					if(pProxy->isRemoved())
+					pFile->writeText("[\n");
+
+					bool isFirst = true;
+					char tmp[64];
+					fora(i, g_apGroups)
 					{
-						continue;
-					}
-					pFile->writeText("\t%s{\n", isFirst ? "" : ",");
-					isFirst = false;
+						CGroupObject *pGroup = g_apGroups[i];
 
-					XGUIDToSting(*pProxy->getGUID(), tmp, sizeof(tmp));
-					pFile->writeText("\t\t\"guid\": \"%s\"\n", tmp);
-					XGUIDToSting(*pProxy->getTargetObject()->getGUID(), tmp, sizeof(tmp));
-					pFile->writeText("\t\t,\"t\": \"%s\"\n", tmp);
-					pFile->writeText("\t\t,\"s\": [\n", tmp);
+						pFile->writeText("\t%s{\n", isFirst ? "" : ",");
+						isFirst = false;
 
-					// TODO don't save empty models!
+						XGUIDToSting(*pGroup->getGUID(), tmp, sizeof(tmp));
+						pFile->writeText("\t\t\"guid\": \"%s\"\n", tmp);
+						pFile->writeText("\t\t,\"name\": \"%s\"\n", pGroup->getKV("name"));
 
-					for(UINT j = 0, jl = pProxy->getModelCount(); j < jl; ++j)
-					{
-						XGUIDToSting(*pProxy->getModel(j)->getGUID(), tmp, sizeof(tmp));
-						pFile->writeText("\t\t\t%s\"%s\"\n", j == 0 ? "" : ",", tmp);
-					}
+						pFile->writeText("\t\t,\"o\": [\n", tmp);
+
+						for(UINT j = 0, jl = pGroup->getObjectCount(); j < jl; ++j)
+						{
+							XGUIDToSting(*pGroup->getObject(j)->getGUID(), tmp, sizeof(tmp));
+							pFile->writeText("\t\t\t%s\"%s\"\n", j == 0 ? "" : ",", tmp);
+						}
 
-					pFile->writeText("\t\t]\n\t\t,\"o\": [\n", tmp);
+						pFile->writeText("\t\t]\n", tmp);
 
-					for(UINT j = 0, jl = pProxy->getObjectCount(); j < jl; ++j)
-					{
-						XGUIDToSting(*pProxy->getObject(j)->getGUID(), tmp, sizeof(tmp));
-						pFile->writeText("\t\t\t%s\"%s\"\n", j == 0 ? "" : ",", tmp);
+						pFile->writeText("\t}\n");
 					}
 
-					pFile->writeText("\t\t]\n", tmp);
+					pFile->writeText("]\n");
+
+					mem_release(pFile);
+				}
 
-					pFile->writeText("\t}\n");
 
-					pProxy->saveModel();
+				// save proxies
+				sprintf(szPathLevel, "levels/%s/editor/proxies.json", pData->szLevelName);
+				
+				pFile = pFS->openFile(szPathLevel, FILE_MODE_WRITE);
+				if(!pFile)
+				{
+					LibReport(REPORT_MSG_LEVEL_ERROR, "Unable to save data '%s'\n", szPathLevel);
 				}
+				else
+				{
+					pFile->writeText("[\n");
+
+					sprintf(szPathLevel, "levels/%s/models", pData->szLevelName);
+					pFS->deleteDirectory(szPathLevel);
+					//IFileIterator *pIter = pFS->getFileList(szPathLevel, "dse");
+					//const char *szFile;
+					//while((szFile = pIter->next()))
+					//{
+					//	//printf("%s\n", szFile);
+					//	pFS->deleteFile(szFile);
+					//}
+
+					bool isFirst = true;
+					char tmp[64];
+					fora(i, g_apProxies)
+					{
+						CProxyObject *pProxy = g_apProxies[i];
+						if(pProxy->isRemoved())
+						{
+							continue;
+						}
+						pFile->writeText("\t%s{\n", isFirst ? "" : ",");
+						isFirst = false;
 
-				pFile->writeText("\n]\n");
+						XGUIDToSting(*pProxy->getGUID(), tmp, sizeof(tmp));
+						pFile->writeText("\t\t\"guid\": \"%s\"\n", tmp);
+						XGUIDToSting(*pProxy->getTargetObject()->getGUID(), tmp, sizeof(tmp));
+						pFile->writeText("\t\t,\"t\": \"%s\"\n", tmp);
+						pFile->writeText("\t\t,\"s\": [\n", tmp);
 
-				mem_release(pFile);
+						// TODO don't save empty models!
+
+						for(UINT j = 0, jl = pProxy->getModelCount(); j < jl; ++j)
+						{
+							XGUIDToSting(*pProxy->getModel(j)->getGUID(), tmp, sizeof(tmp));
+							pFile->writeText("\t\t\t%s\"%s\"\n", j == 0 ? "" : ",", tmp);
+						}
+
+						pFile->writeText("\t\t]\n\t\t,\"o\": [\n", tmp);
+
+						for(UINT j = 0, jl = pProxy->getObjectCount(); j < jl; ++j)
+						{
+							XGUIDToSting(*pProxy->getObject(j)->getGUID(), tmp, sizeof(tmp));
+							pFile->writeText("\t\t\t%s\"%s\"\n", j == 0 ? "" : ",", tmp);
+						}
+
+						pFile->writeText("\t\t]\n", tmp);
+
+						pFile->writeText("\t}\n");
+
+						pProxy->saveModel();
+					}
+
+					pFile->writeText("]\n");
+
+					mem_release(pFile);
+				}
 			}
 			break;
 
@@ -1259,7 +1312,120 @@ int main(int argc, char **argv)
 
 				if(!isLoaded)
 				{
-					LibReport(REPORT_MSG_LEVEL_ERROR, "Unable to load '%s'\n", szFile);
+					LibReport(REPORT_MSG_LEVEL_WARNING, "Unable to load '%s'\n", szFile);
+				}
+			}
+
+			sprintf(szFile, "levels/%s/editor/groups.json", pData->szLevelName);
+
+			pFile = Core_GetIXCore()->getFileSystem()->openFile(szFile, FILE_MODE_READ);
+			if(pFile)
+			{
+				size_t sizeFile = pFile->getSize();
+				char *szJSON = new char[sizeFile + 1];
+				pFile->readBin(szJSON, sizeFile);
+				szJSON[sizeFile] = 0;
+
+				bool isLoaded = false;
+
+				IXJSON *pJSON = (IXJSON*)Core_GetIXCore()->getPluginManager()->getInterface(IXJSON_GUID);
+				IXJSONItem *pRoot;
+				if(pJSON->parse(szJSON, &pRoot))
+				{
+					IXJSONArray *pArr = pRoot->asArray();
+					if(pArr)
+					{
+						Array<CGroupObject*> aGroups(pArr->size());
+						const char *szGUID = NULL;
+						const char *szName = NULL;
+						XGUID guid;
+
+						for(UINT j = 0, jl = pArr->size(); j < jl; ++j)
+						{
+							aGroups[j] = NULL;
+
+							IXJSONObject *pGroupObj = pArr->at(j)->asObject();
+							if(pGroupObj)
+							{
+								IXJSONItem *pGuidItem = pGroupObj->getItem("guid");
+								if(!pGuidItem || !(szGUID = pGuidItem->getString()) || !XGUIDFromString(&guid, szGUID))
+								{
+									LibReport(REPORT_MSG_LEVEL_ERROR, "Invalid group '%u' in '%s'. Invalid GUID\n", j, szFile);
+									continue;
+								}
+
+								IXJSONItem *pNameItem = pGroupObj->getItem("name");
+								if(!pNameItem || !(szName = pNameItem->getString()))
+								{
+									LibReport(REPORT_MSG_LEVEL_ERROR, "Invalid group '%u' in '%s'. Invalid name\n", j, szFile);
+									continue;
+								}
+
+								IXJSONItem *pOItem = pGroupObj->getItem("o");
+								IXJSONArray *pOArr = NULL;
+								if(!pOItem || !(pOArr = pOItem->asArray()))
+								{
+									LibReport(REPORT_MSG_LEVEL_ERROR, "Invalid group '%u' in '%s'. Missing 'o' key\n", j, szFile);
+									continue;
+								}
+
+								if(!pOArr->size())
+								{
+									LibReport(REPORT_MSG_LEVEL_ERROR, "Invalid group '%u' in '%s'. No child objects\n", j, szFile);
+									continue;
+								}
+
+								CGroupObject *pGroup = new CGroupObject(guid);
+								pGroup->setKV("name", szName);
+								
+								g_apGroups.push_back(pGroup);
+
+								add_ref(pGroup);
+								g_pLevelObjects.push_back(pGroup);
+
+								aGroups[j] = pGroup;
+							}
+						}
+
+						for(UINT j = 0, jl = pArr->size(); j < jl; ++j)
+						{
+							CGroupObject *pGroup = aGroups[j];
+							if(pGroup)
+							{								
+								IXJSONArray *pOArr = pArr->at(j)->asObject()->getItem("o")->asArray();
+
+								for(UINT k = 0, kl = pOArr->size(); k < kl; ++k)
+								{
+									szGUID = pOArr->at(k)->getString();
+									if(!szGUID || !XGUIDFromString(&guid, szGUID))
+									{
+										LibReport(REPORT_MSG_LEVEL_ERROR, "Invalid object '%u' guid in group '%u' in '%s'. '%s'\n", k, j, szFile, szGUID ? szGUID : "");
+										continue;
+									}
+
+									IXEditorObject *pObj = XFindObjectByGUID(guid);
+									if(!pObj)
+									{
+										LibReport(REPORT_MSG_LEVEL_ERROR, "Invalid object '%u' in group '%u' in '%s'. '%s'. No object with GUID found.\n", k, j, szFile, szGUID ? szGUID : "");
+										continue;
+									}
+									pGroup->addChildObject(pObj);
+
+									isLoaded = true;
+								}
+							}
+						}
+					}
+
+					mem_release(pRoot);
+				}
+
+				mem_delete_a(szJSON);
+				mem_release(pFile);
+
+				if(!isLoaded)
+				{
+					LibReport(REPORT_MSG_LEVEL_WARNING, "Unable to load '%s'\n", szFile);
 				}
 			}
 
@@ -1577,6 +1743,7 @@ void XRender3D()
 			{
 				pObj->render(true, true, g_pSelectionRenderer);
 			}
+			return(XEOR_CONTINUE);
 		});
 
 		g_pSelectionRenderer->render(false, false);
@@ -1590,6 +1757,7 @@ void XRender3D()
 			{
 				pObj->render(true, false, g_pUnselectedRenderer);
 			}
+			return(XEOR_CONTINUE);
 		});
 
 		g_pUnselectedRenderer->render(false, false);
@@ -1679,25 +1847,25 @@ void XRender3D()
 		{
 			UINT uHandlerCount = 0;
 			pvData = NULL;
-			for(UINT i = 0, l = g_pLevelObjects.size(); i < l; ++i)
-			{
-				float3_t vPos = g_pLevelObjects[i]->getPos();
-				//@TODO: Add visibility check
+
+			XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
+				float3_t vPos = pObj->getPos();
+				TODO("Add visibility check");
 				/*if(fViewportBorders.x > vPos.x || fViewportBorders.z < vPos.x || fViewportBorders.y < vPos.z) // not visible
 				{
-				continue;
+					continue;
 				}*/
-				//if(isSelected != g_pLevelObjects[i]->isSelected() || (!isSelected && (g_pLevelObjects[i]->hasVisualModel() || g_pLevelObjects[i]->getIcon())))
-				if(g_pLevelObjects[i]->hasVisualModel() || isSelected != g_pLevelObjects[i]->isSelected() || (!isSelected && g_pLevelObjects[i]->getIcon()))
+
+				if(pObj->hasVisualModel() || isSelected != pObj->isSelected() || (!isSelected && pObj->getIcon()))
 				{
-					continue;
+					return(XEOR_CONTINUE);
 				}
 				if(!pvData && !g_xRenderStates.pHandlerInstanceVB->lock((void**)&pvData, GXBL_WRITE))
 				{
-					break;
+					return(XEOR_STOP);
 				}
 				float3 vMin, vMax;
-				g_pLevelObjects[i]->getBound(&vMin, &vMax);
+				pObj->getBound(&vMin, &vMax);
 
 				pvData[uHandlerCount++] = {(float3)((vMax + vMin) * 0.5f), (float3)(vMax - vMin)};
 				if(uHandlerCount == X_MAX_HANDLERS_PER_DIP)
@@ -1707,7 +1875,10 @@ void XRender3D()
 					pvData = NULL;
 					uHandlerCount = 0;
 				}
-			}
+
+				return(XEOR_CONTINUE);
+			});
+
 			if(pvData)
 			{
 				g_xRenderStates.pHandlerInstanceVB->unlock();
@@ -1774,6 +1945,7 @@ void XRender3D()
 					icon.vPos = pObj->getPos();
 					aIcons.push_back(icon);
 				}
+				return(XEOR_CONTINUE);
 			});
 			aIcons.quickSort([](const Icon &a, const Icon &b){
 				return(a.pTexture < b.pTexture);
@@ -1882,6 +2054,7 @@ void XRender2D(IXCamera *pCamera, X_2D_VIEW view, float fScale, bool preScene, b
 						{
 							pObj->render(false, true, g_pSelectionRenderer);
 						}
+						return(XEOR_CONTINUE);
 					});
 
 					g_isRenderedSelection3D = false;
@@ -1900,6 +2073,7 @@ void XRender2D(IXCamera *pCamera, X_2D_VIEW view, float fScale, bool preScene, b
 				{
 					pObj->render(false, false, g_pUnselectedRenderer);
 				}
+				return(XEOR_CONTINUE);
 			});
 
 			g_isRenderedUnselected3D = false;
@@ -1950,16 +2124,16 @@ void XRender2D(IXCamera *pCamera, X_2D_VIEW view, float fScale, bool preScene, b
 					}*/
 					if(isSelected != pObj->isSelected())
 					{
-						return;
+						return(XEOR_CONTINUE);
 					}
 					if(isProxy)
 					{
-						return;
+						return(XEOR_CONTINUE);
 					}
 
 					if(!pvData && !g_xRenderStates.pHandlerInstanceVB->lock((void**)&pvData, GXBL_WRITE))
 					{
-						return;
+						return(XEOR_CONTINUE);
 					}
 
 					pvData[uHandlerCount++] = vPos;
@@ -1970,6 +2144,7 @@ void XRender2D(IXCamera *pCamera, X_2D_VIEW view, float fScale, bool preScene, b
 						pvData = NULL;
 						uHandlerCount = 0;
 					}
+					return(XEOR_CONTINUE);
 				});
 
 				if(pvData)
@@ -2018,25 +2193,24 @@ void XRender2D(IXCamera *pCamera, X_2D_VIEW view, float fScale, bool preScene, b
 			{
 				UINT uHandlerCount = 0;
 				pvData = NULL;
-				for(UINT i = 0, l = g_pLevelObjects.size(); i < l; ++i)
-				{
-					float3_t vPos = g_pLevelObjects[i]->getPos();
-					//@TODO: Add visibility check
+				XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
+					float3_t vPos = pObj->getPos();
+					TODO("Add visibility check");
 					/*if(fViewportBorders.x > vPos.x || fViewportBorders.z < vPos.x || fViewportBorders.y < vPos.z) // not visible
 					{
 					continue;
 					}*/
-					//if(isSelected != g_pLevelObjects[i]->isSelected() || (!isSelected && (g_pLevelObjects[i]->hasVisualModel() || g_pLevelObjects[i]->getIcon())))
-					if(g_pLevelObjects[i]->hasVisualModel() || isSelected != g_pLevelObjects[i]->isSelected())
+
+					if(pObj->hasVisualModel() || isSelected != pObj->isSelected())
 					{
-						continue;
+						return(XEOR_CONTINUE);
 					}
 					if(!pvData && !g_xRenderStates.pHandlerInstanceVB->lock((void**)&pvData, GXBL_WRITE))
 					{
-						break;
+						return(XEOR_STOP);
 					}
 					float3 vMin, vMax;
-					g_pLevelObjects[i]->getBound(&vMin, &vMax);
+					pObj->getBound(&vMin, &vMax);
 
 					pvData[uHandlerCount++] = {(float3)((vMax + vMin) * 0.5f), (float3)(vMax - vMin)};
 					if(uHandlerCount == X_MAX_HANDLERS_PER_DIP)
@@ -2046,7 +2220,10 @@ void XRender2D(IXCamera *pCamera, X_2D_VIEW view, float fScale, bool preScene, b
 						pvData = NULL;
 						uHandlerCount = 0;
 					}
-				}
+
+					return(XEOR_CONTINUE);
+				});
+				
 				if(pvData)
 				{
 					g_xRenderStates.pHandlerInstanceVB->unlock();
@@ -2436,7 +2613,7 @@ void XUpdateSelectionBound()
 	float3 vMin, vMax;
 
 	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
-		if(pObj->isSelected() && !(g_xConfig.m_bIgnoreGroups && isProxy))
+		if(pObj->isSelected()/* && !(g_xConfig.m_bIgnoreGroups && isProxy)*/)
 		{
 			pObj->getBound(&vMin, &vMax);
 			if(!g_xState.bHasSelection)
@@ -2450,8 +2627,25 @@ void XUpdateSelectionBound()
 				g_xState.vSelectionBoundMax = (float3)SMVectorMax(g_xState.vSelectionBoundMax, vMax);
 				g_xState.vSelectionBoundMin = (float3)SMVectorMin(g_xState.vSelectionBoundMin, vMin);
 			}
+			return(XEOR_SKIP_CHILDREN);
 		}
+		return(XEOR_CONTINUE);
 	});
+	
+	bool hasGroupSelected = false;
+	if(g_xState.bHasSelection)
+	{
+		fora(i, g_apGroups)
+		{
+			if(g_apGroups[i]->isSelected())
+			{
+				hasGroupSelected = true;
+				break;
+			}
+		}
+	}
+	EnableToolbarButton(ID_TOOLS_GROUP, g_xState.bHasSelection);
+	EnableToolbarButton(ID_TOOLS_UNGROUP, hasGroupSelected);
 }
 
 bool XRayCast(X_WINDOW_POS wnd)
@@ -2487,6 +2681,7 @@ bool XRayCast(X_WINDOW_POS wnd)
 		{
 			res = true;
 		}
+		return(XEOR_CONTINUE);
 	});
 	return(false);
 }
@@ -2746,15 +2941,17 @@ ICompoundObject* XGetObjectParent(IXEditorObject *pObject)
 
 IXEditorObject* XFindObjectByGUID(const XGUID &guid)
 {
-	fora(i, g_pLevelObjects)
-	{
-		if(*g_pLevelObjects[i]->getGUID() == guid)
+	IXEditorObject *pFoundObj = NULL;
+	XEnumerateObjects([&](IXEditorObject *pObj, bool isProxy, ICompoundObject *pParent){
+		if(*pObj->getGUID() == guid)
 		{
-			return(g_pLevelObjects[i]);
+			pFoundObj = pObj;
+			return(XEOR_STOP);
 		}
-	}
+		return(XEOR_CONTINUE);
+	});
 
-	return(NULL);
+	return(pFoundObj);
 }
 
 void BeginMaterialEdit(const char *szMaterialName)
diff --git a/source/terrax/terrax.h b/source/terrax/terrax.h
index d5e581939a63c8620e99eadb05ebb0c181df7e47..0f512ce832315f45f4e01dfa3faa42037ee384f1 100644
--- a/source/terrax/terrax.h
+++ b/source/terrax/terrax.h
@@ -36,6 +36,7 @@
 #include "MaterialEditor.h"
 #include "Editor.h"
 #include "ProxyObject.h"
+#include "GroupObject.h"
 
 enum X_VIEWPORT_LAYOUT
 {
@@ -216,9 +217,17 @@ extern Array<IXEditorObject*> g_pLevelObjects;
 extern Map<XGUID, IXEditorModel*> g_apLevelModels;
 extern Map<IXEditorObject*, ICompoundObject*> g_mObjectsLocation;
 extern Array<CProxyObject*> g_apProxies;
+extern Array<CGroupObject*> g_apGroups;
+
+enum XENUMERATE_OBJECTS_RESULT
+{
+	XEOR_CONTINUE,
+	XEOR_SKIP_CHILDREN,
+	XEOR_STOP
+};
 
 template<typename T, class L>
-void XEnumerateObjects(const T &Func, L *pWhere)
+XENUMERATE_OBJECTS_RESULT XEnumerateObjects(const T &Func, L *pWhere)
 {
 	void *isProxy;
 	if(pWhere)
@@ -228,10 +237,19 @@ void XEnumerateObjects(const T &Func, L *pWhere)
 			IXEditorObject *pObj = pWhere->getObject(i);
 			isProxy = NULL;
 			pObj->getInternalData(&X_IS_COMPOUND_GUID, &isProxy);
-			Func(pObj, isProxy ? true : false, pWhere);
-			if(isProxy)
+			XENUMERATE_OBJECTS_RESULT res = Func(pObj, isProxy ? true : false, pWhere);
+			if(res == XEOR_STOP)
+			{
+				return(XEOR_STOP);
+			}
+
+			if(isProxy && res != XEOR_SKIP_CHILDREN)
 			{
-				XEnumerateObjects(Func, (ICompoundObject*)pObj);
+				res = XEnumerateObjects(Func, (ICompoundObject*)pObj);
+				if(res == XEOR_STOP)
+				{
+					return(XEOR_STOP);
+				}
 			}
 		}
 	}
@@ -242,19 +260,30 @@ void XEnumerateObjects(const T &Func, L *pWhere)
 			IXEditorObject *pObj = g_pLevelObjects[i];
 			isProxy = NULL;
 			pObj->getInternalData(&X_IS_COMPOUND_GUID, &isProxy);
-			Func(pObj, isProxy ? true : false, pWhere);
-			if(isProxy)
+			XENUMERATE_OBJECTS_RESULT res = Func(pObj, isProxy ? true : false, pWhere);
+			if(res == XEOR_STOP)
 			{
-				XEnumerateObjects(Func, (ICompoundObject*)pObj);
+				return(XEOR_STOP);
+			}
+
+			if(isProxy && res != XEOR_SKIP_CHILDREN)
+			{
+				res = XEnumerateObjects(Func, (ICompoundObject*)pObj);
+				if(res == XEOR_STOP)
+				{
+					return(XEOR_STOP);
+				}
 			}
 		}
 	}
+
+	return(XEOR_CONTINUE);
 }
 
 template<typename T>
-void XEnumerateObjects(const T &Func)
+XENUMERATE_OBJECTS_RESULT XEnumerateObjects(const T &Func)
 {
-	XEnumerateObjects(Func, (ICompoundObject*)NULL);
+	return(XEnumerateObjects(Func, (ICompoundObject*)NULL));
 }
 
 void XDrawBorder(GXCOLOR color, const float3_t &vA, const float3_t &vB, const float3_t &vC, const float3_t &vD, float fViewportScale = 0.01f);
@@ -292,6 +321,7 @@ IXEditorObject* XFindObjectByGUID(const XGUID &guid);
 ICompoundObject* XGetObjectParent(IXEditorObject *pObject);
 
 void CheckToolbarButton(int iCmd, BOOL isChecked);
+void EnableToolbarButton(int iCmd, BOOL isChecked);
 
 
 int DivDpi(int iUnscaled, UINT uCurrentDpi);
diff --git a/source/terrax/terrax.rc b/source/terrax/terrax.rc
index d9c1bca1a02a3743b3ee50789a73c7297ad200ab..906103b7f896e6a69aee0def76275d68924939ea 100644
Binary files a/source/terrax/terrax.rc and b/source/terrax/terrax.rc differ