#include "RadiantTest.h" #include "imap.h" #include "ibrush.h" #include "entitylib.h" #include "algorithm/Scene.h" namespace test { using CsgTest = RadiantTest; TEST_F(CsgTest, CSGMergeTwoRegularWorldspawnBrushes) { loadMap("csg_merge.map"); // Locate the first worldspawn brush auto worldspawn = GlobalMapModule().getWorldspawn(); // Try to merge the two brushes with the "1" and "2" materials auto firstBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "1"); auto secondBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "2"); ASSERT_TRUE(Node_getIBrush(firstBrush)->getNumFaces() == 5); ASSERT_TRUE(Node_getIBrush(secondBrush)->getNumFaces() == 5); // Select the brushes and merge them GlobalSelectionSystem().setSelectedAll(false); Node_setSelected(firstBrush, true); Node_setSelected(secondBrush, true); // CSG merge GlobalCommandSystem().executeCommand("CSGMerge"); // The two brushes should be gone, replaced by a new one ASSERT_TRUE(firstBrush->getParent() == nullptr); ASSERT_TRUE(secondBrush->getParent() == nullptr); // The merged brush will carry both materials auto brushWithMaterial1 = algorithm::findFirstBrushWithMaterial(worldspawn, "1"); auto brushWithMaterial2 = algorithm::findFirstBrushWithMaterial(worldspawn, "2"); ASSERT_TRUE(brushWithMaterial1 == brushWithMaterial2); ASSERT_TRUE(Node_getIBrush(brushWithMaterial1)->getNumFaces() == 6); } TEST_F(CsgTest, CSGMergeFourRegularWorldspawnBrushes) { loadMap("csg_merge.map"); // Locate the first worldspawn brush auto worldspawn = GlobalMapModule().getWorldspawn(); // Try to merge the two brushes with the "1" and "2" materials std::vector brushes = { algorithm::findFirstBrushWithMaterial(worldspawn, "1"), algorithm::findFirstBrushWithMaterial(worldspawn, "2"), algorithm::findFirstBrushWithMaterial(worldspawn, "3"), algorithm::findFirstBrushWithMaterial(worldspawn, "4") }; // Check the correct setup for (const auto& brush : brushes) { ASSERT_TRUE(Node_getIBrush(brush)->getNumFaces() == 5); } // Select the brushes and merge them GlobalSelectionSystem().setSelectedAll(false); for (const auto& brush : brushes) { Node_setSelected(brush, true); } // CSG merge GlobalCommandSystem().executeCommand("CSGMerge"); // All brushes should be gone, replaced by a new one for (const auto& brush : brushes) { ASSERT_TRUE(brush->getParent() == nullptr); } // The combined brush should be a 6-sided cuboid auto brushWithMaterial1 = algorithm::findFirstBrushWithMaterial(worldspawn, "1"); ASSERT_TRUE(Node_getIBrush(brushWithMaterial1)->getNumFaces() == 6); } TEST_F(CsgTest, CSGMergeTwoFuncStaticBrushes) { loadMap("csg_merge.map"); // Locate the func_static in the map EntityNodeFindByClassnameWalker walker("func_static"); GlobalSceneGraph().root()->traverse(walker); auto entity = walker.getEntityNode(); // Try to merge the two brushes with the "1" and "2" materials auto firstBrush = algorithm::findFirstBrushWithMaterial(entity, "1"); auto secondBrush = algorithm::findFirstBrushWithMaterial(entity, "2"); ASSERT_TRUE(Node_getIBrush(firstBrush)->getNumFaces() == 5); ASSERT_TRUE(Node_getIBrush(secondBrush)->getNumFaces() == 5); // Select the brushes and merge them GlobalSelectionSystem().setSelectedAll(false); Node_setSelected(firstBrush, true); Node_setSelected(secondBrush, true); // CSG merge GlobalCommandSystem().executeCommand("CSGMerge"); // The two brushes should be gone, replaced by a new one ASSERT_TRUE(firstBrush->getParent() == nullptr); ASSERT_TRUE(secondBrush->getParent() == nullptr); // The merged brush will carry both materials auto brushWithMaterial1 = algorithm::findFirstBrushWithMaterial(entity, "1"); auto brushWithMaterial2 = algorithm::findFirstBrushWithMaterial(entity, "2"); ASSERT_TRUE(brushWithMaterial1 == brushWithMaterial2); ASSERT_TRUE(Node_getIBrush(brushWithMaterial1)->getNumFaces() == 6); // They should still be children of the same entity ASSERT_TRUE(brushWithMaterial1->getParent() == entity); ASSERT_TRUE(brushWithMaterial2->getParent() == entity); } // #5344: Check that selecting a couple of brushes will only merge those // which share the same parent entity TEST_F(CsgTest, CSGMergeBrushesOfMixedEntitySelection) { loadMap("csg_merge.map"); // Locate the func_static in the map EntityNodeFindByClassnameWalker walker("func_static"); GlobalSceneGraph().root()->traverse(walker); auto entity = walker.getEntityNode(); auto worldspawn = GlobalMapModule().getWorldspawn(); // Select the mergeable brushes of both entities carrying the "1" and "2" materials std::vector brushes = { algorithm::findFirstBrushWithMaterial(entity, "1"), algorithm::findFirstBrushWithMaterial(entity, "2"), algorithm::findFirstBrushWithMaterial(worldspawn, "1"), algorithm::findFirstBrushWithMaterial(worldspawn, "2") }; // Check the correct setup for (const auto& brush : brushes) { ASSERT_TRUE(Node_getIBrush(brush)->getNumFaces() == 5); } // Select the brushes and merge them GlobalSelectionSystem().setSelectedAll(false); for (const auto& brush : brushes) { Node_setSelected(brush, true); } // CSG merge GlobalCommandSystem().executeCommand("CSGMerge"); // All brushes should be gone, replaced by TWO new ones for (const auto& brush : brushes) { ASSERT_TRUE(brush->getParent() == nullptr); } // The merged brush will carry both materials auto funcBrush1 = algorithm::findFirstBrushWithMaterial(entity, "1"); auto funcBrush2 = algorithm::findFirstBrushWithMaterial(entity, "2"); ASSERT_TRUE(funcBrush1); ASSERT_TRUE(funcBrush1 == funcBrush2); ASSERT_TRUE(Node_getIBrush(funcBrush1)->getNumFaces() == 6); // Same for the worldspawn entity auto worldBrush1 = algorithm::findFirstBrushWithMaterial(worldspawn, "1"); auto worldBrush2 = algorithm::findFirstBrushWithMaterial(worldspawn, "2"); ASSERT_TRUE(worldBrush1); ASSERT_TRUE(worldBrush1 == worldBrush2); ASSERT_TRUE(Node_getIBrush(worldBrush1)->getNumFaces() == 6); } // Issue #5336: Crash when using CSG Merge on brushes that are part of worldspawn and a func_static TEST_F(CsgTest, CSGMergeWithFuncStatic) { loadMap("csg_merge_with_func_static.map"); // Locate the first worldspawn brush auto firstBrush = algorithm::getNthChild(GlobalMapModule().getWorldspawn(), 0); ASSERT_TRUE(firstBrush); // Locate the func_static in the map EntityNodeFindByClassnameWalker walker("func_static"); GlobalSceneGraph().root()->traverse(walker); auto entityNode = walker.getEntityNode(); ASSERT_TRUE(entityNode); ASSERT_TRUE(entityNode->hasChildNodes()); // Select both of them, the order is important Node_setSelected(firstBrush, true); Node_setSelected(entityNode, true); // CSG merge GlobalCommandSystem().executeCommand("CSGMerge"); // No merge should have happened since the brushes // are not part of the same entity // So assume the scene didn't change ASSERT_TRUE(algorithm::getNthChild(GlobalMapModule().getWorldspawn(), 0) == firstBrush); EntityNodeFindByClassnameWalker walker2("func_static"); GlobalSceneGraph().root()->traverse(walker2); ASSERT_TRUE(walker.getEntityNode()); ASSERT_TRUE(walker.getEntityNode()->hasChildNodes()); } TEST_F(CsgTest, CSGIntersectTwoOverlappingBrushes) { loadMap("csg_intersect.map"); auto worldspawn = GlobalMapModule().getWorldspawn(); // Find the two overlapping brushes with materials "1" and "2" auto firstBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "1"); auto secondBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "2"); ASSERT_TRUE(firstBrush != nullptr); ASSERT_TRUE(secondBrush != nullptr); ASSERT_TRUE(Node_getIBrush(firstBrush)->getNumFaces() == 6); ASSERT_TRUE(Node_getIBrush(secondBrush)->getNumFaces() == 6); // Select the brushes and intersect them GlobalSelectionSystem().setSelectedAll(false); Node_setSelected(firstBrush, true); Node_setSelected(secondBrush, true); // CSG intersect GlobalCommandSystem().executeCommand("CSGIntersect"); // The two brushes should be gone, replaced by a new one ASSERT_TRUE(firstBrush->getParent() == nullptr); ASSERT_TRUE(secondBrush->getParent() == nullptr); // The intersection should have created a new brush // It should have materials from the first brush (since we started with that) auto resultBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "1"); ASSERT_TRUE(resultBrush != nullptr); // The result should be a valid 6-sided brush (the intersection of two cubes is a cube) ASSERT_TRUE(Node_getIBrush(resultBrush)->getNumFaces() == 6); } TEST_F(CsgTest, CSGIntersectNonOverlappingBrushes) { loadMap("csg_intersect.map"); auto worldspawn = GlobalMapModule().getWorldspawn(); // Find brush "1" and the non-overlapping brush "3" auto firstBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "1"); auto nonOverlappingBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "3"); ASSERT_TRUE(firstBrush != nullptr); ASSERT_TRUE(nonOverlappingBrush != nullptr); // Select the brushes GlobalSelectionSystem().setSelectedAll(false); Node_setSelected(firstBrush, true); Node_setSelected(nonOverlappingBrush, true); // CSG intersect - should fail silently because brushes don't overlap GlobalCommandSystem().executeCommand("CSGIntersect"); // The original brushes should still exist (operation failed, no changes) ASSERT_TRUE(firstBrush->getParent() != nullptr); ASSERT_TRUE(nonOverlappingBrush->getParent() != nullptr); } TEST_F(CsgTest, CSGIntersectContainedBrush) { loadMap("csg_intersect.map"); auto worldspawn = GlobalMapModule().getWorldspawn(); // Find brush "1" (large) and brush "4" (small, contained within "1") auto largeBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "1"); auto smallBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "4"); ASSERT_TRUE(largeBrush != nullptr); ASSERT_TRUE(smallBrush != nullptr); ASSERT_TRUE(Node_getIBrush(largeBrush)->getNumFaces() == 6); ASSERT_TRUE(Node_getIBrush(smallBrush)->getNumFaces() == 6); // Select the brushes GlobalSelectionSystem().setSelectedAll(false); Node_setSelected(largeBrush, true); Node_setSelected(smallBrush, true); // CSG intersect GlobalCommandSystem().executeCommand("CSGIntersect"); // The two brushes should be gone ASSERT_TRUE(largeBrush->getParent() == nullptr); ASSERT_TRUE(smallBrush->getParent() == nullptr); // The result should be a brush equal in size to the small brush // The result will have material "4" since those faces define the intersection volume auto resultBrush = algorithm::findFirstBrushWithMaterial(worldspawn, "4"); ASSERT_TRUE(resultBrush != nullptr); ASSERT_TRUE(Node_getIBrush(resultBrush)->getNumFaces() == 6); } TEST_F(CsgTest, CSGIntersectRequiresTwoBrushes) { loadMap("csg_intersect.map"); auto worldspawn = GlobalMapModule().getWorldspawn(); // Find just one brush auto brush = algorithm::findFirstBrushWithMaterial(worldspawn, "1"); ASSERT_TRUE(brush != nullptr); // Select only one brush GlobalSelectionSystem().setSelectedAll(false); Node_setSelected(brush, true); // CSG intersect - should fail silently because we need at least 2 brushes GlobalCommandSystem().executeCommand("CSGIntersect"); // The original brush should still exist (operation failed, no changes) ASSERT_TRUE(brush->getParent() != nullptr); } }