From 0c50ce89f38df4824eae15bd374a666ac28df1b4 Mon Sep 17 00:00:00 2001
From: D-AIRY <admin@ds-servers.com>
Date: Sun, 15 Dec 2024 00:47:54 +0300
Subject: [PATCH] TerraX support for grouping

---
 proj/terrax/vs2013/terrax.vcxproj         |   6 +
 proj/terrax/vs2013/terrax.vcxproj.filters |  18 ++
 source/terrax/CommandBuildModel.cpp       |   7 +-
 source/terrax/CommandDuplicate.cpp        |  23 +-
 source/terrax/CommandGroup.cpp            | 178 +++++++++++
 source/terrax/CommandGroup.h              |  52 ++++
 source/terrax/CommandPaste.cpp            |  77 ++++-
 source/terrax/CommandPaste.h              |  11 +
 source/terrax/CommandUngroup.cpp          | 101 ++++++
 source/terrax/CommandUngroup.h            |  39 +++
 source/terrax/GroupObject.cpp             | 361 ++++++++++++++++++++++
 source/terrax/GroupObject.h               | 110 +++++++
 source/terrax/ProxyObject.cpp             |  12 +-
 source/terrax/SceneTreeWindow.cpp         |  79 ++++-
 source/terrax/SceneTreeWindow.h           |   2 +
 source/terrax/UndoManager.cpp             |   3 +-
 source/terrax/mainWindow.cpp              | 232 ++++++++++++--
 source/terrax/resource.h                  | Bin 11833 -> 24580 bytes
 source/terrax/resource/toolbar1.bmp       | Bin 838 -> 1080 bytes
 source/terrax/resource/toolbar2.bmp       | Bin 838 -> 1080 bytes
 source/terrax/terrax.cpp                  | 355 ++++++++++++++++-----
 source/terrax/terrax.h                    |  48 ++-
 source/terrax/terrax.rc                   | Bin 48900 -> 49534 bytes
 23 files changed, 1569 insertions(+), 145 deletions(-)
 create mode 100644 source/terrax/CommandGroup.cpp
 create mode 100644 source/terrax/CommandGroup.h
 create mode 100644 source/terrax/CommandUngroup.cpp
 create mode 100644 source/terrax/CommandUngroup.h
 create mode 100644 source/terrax/GroupObject.cpp
 create mode 100644 source/terrax/GroupObject.h

diff --git a/proj/terrax/vs2013/terrax.vcxproj b/proj/terrax/vs2013/terrax.vcxproj
index 94f6063c1..3714e7454 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 f6cb0656d..c7f15f1fc 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 1584fbb7a..f3ff59d42 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 e5d2e5d8a..52493d9e7 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 000000000..d0e2d75f0
--- /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 000000000..d37541a6c
--- /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 130e1b621..f7c59ec80 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 49cf50404..e962111a4 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 000000000..46dc5f0a7
--- /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 000000000..0ce86b27a
--- /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 000000000..5beb78e13
--- /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 000000000..1e51d9fcf
--- /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 ec0616104..20d1e4131 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 e39d400f3..e423352cd 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 29cf96453..aaaa81185 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 d658b5b5c..17b0c63a7 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 1ebdac59a..6996e46fb 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
GIT binary patch
literal 24580
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

literal 11833
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

diff --git a/source/terrax/resource/toolbar1.bmp b/source/terrax/resource/toolbar1.bmp
index aa1f7466d2b9315dfbfdf34a4cb6bf69abf3b6da..dbb1f0b437a3419ade41cafc9834107c00921d22 100644
GIT binary patch
delta 355
zcmX@cwu8gi$=8B~0Sw9*7#K7d7#JED7#R2&7#J8CAQFd|85o4PAsDQ1qH!^Qk-8EX
zm6w+nFff!)Tx!U#UJeFD<>l!GXO6H>ylN<*4v{S?FE1-NbHt&1G8>~2UpYv&ybLB}
z#%KUmcjkygQF$6j(xH5E8c2|VfdMR7US3dM?od8?8lxdU&lv}hK_ENX%L~dU9|I}m
zIpa`X?ob3W7bG$H8${5By}SS<=m2upWHly`ZWjgy28bQyY3Y;0m<$Be*~?4V%gZ4O
z)62^zw}Au;%FDrmAd5kQ+n9{G8Q4Jvfvhc?{DjGf2kzkl28PK(%qBw6Fi`?217QXR
E0KM07M*si-

delta 113
zcmdnNag5E_$=8jU0Sw9*7#K7d7#I>57#R2&7#J8CSis^Jn7|a65MW?n5SVCOJaL=B
z#BT<Z%@~a)S1{^L-o|J!`5U9*WOF9{$z@CileaM$PX5lMKiQ1gU~(C=@#LM%x|6>#
G8vp<}q!~;A

diff --git a/source/terrax/resource/toolbar2.bmp b/source/terrax/resource/toolbar2.bmp
index 710d69c5e27e3c5948ab841bcdd9cbcc3619d9b2..5051572bc72dbbd0c0b1a1af2b48dcd827182f2d 100644
GIT binary patch
delta 360
zcmX@cwu8gi$=8B~0Sw9*7#K7d7#JED7#R2&7#J8CAQFd|85o4PAsDQ1qH!_*2Yv=H
z`v3p`2L^`!6PFtD^Zy5f5C8x3e_&wXpLo?!fFC0J;s5^+9~c-I{!eCOG~xqk;Q#*t
zCS=BF09D8E;XeaNlHvd4G>{<JGKLQjD;WMyp2lbhau>)TkewiU@-apOF{mKO3XskJ
z|NjU1VDcPBEdeAc29WC}t1%h!LCl5N^q*mJD3c*r9mug@SMvY=Ke>&`KmcSIir_XT
nV{QhpH$irPnEZsvhzHF55AqHJ!(<_56Cr58fUN^r$-n>rmZpVF

delta 120
zcmdnNag5E_$=8jU0Sw9*7#K7d7#I>57#R2&7#J8CSis^Jn7|a65MW?n5SVCOJaL=B
z#BT<Z%@~a)S1{^L-o|J!`5U9*WOF8cw*UV@`X~Qk)SkSZ$zbw#CjH4~%m$Oon2jgz
LWY(ShjoAPI_e2}t

diff --git a/source/terrax/terrax.cpp b/source/terrax/terrax.cpp
index 8d4994851..56df65475 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 d5e581939..0f512ce83 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
GIT binary patch
delta 553
zcmZqq$Mmm>c|(F6e+)wjgEK=3LlHv`gEoWv<b~YIlMDFRChN%aOkN@vf+in2c_Ftv
zNPe@9{5^h2cZMQ{e1=kn0tP(BaJn-DG59ltG6YOMC})n#z{xuDQj?3g5++a3XWJ~G
zFTp(dnw{8WC!?~-*X%?lUoffz(`m*PVEUVJ6`1ZYfyj%QLg*!?5OFgzh`M7?I?Nm*
z{{%`GS%CSIEFsD!Sc18e<*Xp|8Y_q~cGeL30+<$?oMHpvf3ukfcHTPMHZbjH*E0E<
z-7+wHnmtI3*kmn-8ZiHWLkXDna|G!TnatwU0CxHurzS9M=G-)S+f=d1mz?V+Z<{JJ
zxxl4nG8glr$$HzlHqUXL!U*R)aK9ok*&&#V(S5R`gfb|CkV4jwOLX!FZwF>2hES-i
z+~g<TKFmrCFx7ICOMG-D&x%x=JTH-NGMk6o<_8HnOi<H#Ht$RRW-{4`O@`Bep@P8x
L1UH|ZVXg)M(L2P3

delta 369
zcmey@#N6_aX+whC<VD;@lP4r9O!ksDo1DPIHu;?#*W^XqdXp8BR5p9bAK>5YW+2Br
z`I?c$<O<^oFwJ682d1Z)K=?|gRbc)G(=sq^Wd@;-m_fvY%t7j;CO<F-iAzn+u_&2*
z%}8qUHz?g;3E^{DLFA`dLHG*R5PFq0M8Aa%ggyqQB_;>i)`0DMWIGQ`PqFI&(+2i!
zVEUB(Brsjz09H525#sDgjv#9!Cd)W2083wRY5>zI&P`zYmvhr(g&9(lC%M#3R+ynM
zS;w_zG8^-<$qH#)n?u~DFv2-gJg!Jg7D(ZnEKtBQd7p>W=AdL9CXnoAg)}D9$xYc(
Qj0TexlZ7|O%vMqZ03+g+zW@LL

-- 
GitLab