diff --git a/test/Models.cpp b/test/Models.cpp index 67f8f5b8ae..ebe3743c71 100644 --- a/test/Models.cpp +++ b/test/Models.cpp @@ -710,4 +710,34 @@ TEST_F(ObjImportTest, UseMtlReferencingMaterial) << "OBJ Model loader should have taken the material from the usemtl keyword"; } +TEST_F(ModelTest, LoadMd5v11Model) +{ + auto model = GlobalModelCache().getModel("models/md5/test_v11.md5mesh"); + EXPECT_TRUE(model) << "MD5 v11 model should load successfully"; + + EXPECT_EQ(model->getSurfaceCount(), 1); + EXPECT_EQ(model->getPolyCount(), 2); + EXPECT_EQ(model->getSurface(0).getDefaultMaterial(), "textures/common/caulk"); +} + +TEST_F(ModelTest, LoadMd5v12Model) +{ + auto model = GlobalModelCache().getModel("models/md5/test_v12.md5mesh"); + EXPECT_TRUE(model) << "MD5 v12 model should load successfully"; + + EXPECT_EQ(model->getSurfaceCount(), 1); + EXPECT_EQ(model->getPolyCount(), 2); + EXPECT_EQ(model->getSurface(0).getDefaultMaterial(), "textures/common/caulk"); +} + +TEST_F(ModelTest, ModelKeyReferencesMd5v11Model) +{ + performModelNodeTest(_context.getTestProjectPath(), "models/md5/test_v11.md5mesh", 2); +} + +TEST_F(ModelTest, ModelKeyReferencesMd5v12Model) +{ + performModelNodeTest(_context.getTestProjectPath(), "models/md5/test_v12.md5mesh", 2); +} + } diff --git a/test/SelectionAlgorithm.cpp b/test/SelectionAlgorithm.cpp index 3061753c13..72d632fed6 100644 --- a/test/SelectionAlgorithm.cpp +++ b/test/SelectionAlgorithm.cpp @@ -1,12 +1,18 @@ #include "RadiantTest.h" #include "iselection.h" +#include "iselectiongroup.h" #include "icommandsystem.h" +#include "scene/EntityNode.h" #include "selectionlib.h" +#include "scenelib.h" +#include "algorithm/Entity.h" namespace test { +using SelectionAlgorithmTest = RadiantTest; + TEST_F(RadiantTest, SelectItemsByModel) { loadMap("select_items_by_model.map"); @@ -33,4 +39,46 @@ TEST_F(RadiantTest, SelectItemsByModel) ASSERT_TRUE(GlobalSelectionSystem().getSelectionInfo().totalCount == 0); } +TEST_F(SelectionAlgorithmTest, GroupCycleThroughSelectionGroup) +{ + auto entity1 = algorithm::createEntityByClassName("fixed_size_entity"); + auto entity2 = algorithm::createEntityByClassName("fixed_size_entity"); + auto entity3 = algorithm::createEntityByClassName("fixed_size_entity"); + + GlobalMapModule().getRoot()->addChildNode(entity1); + GlobalMapModule().getRoot()->addChildNode(entity2); + GlobalMapModule().getRoot()->addChildNode(entity3); + + auto& groupMgr = GlobalMapModule().getRoot()->getSelectionGroupManager(); + auto group = groupMgr.createSelectionGroup(); + group->addNode(entity1); + group->addNode(entity2); + group->addNode(entity3); + + Node_setSelected(entity1, true); + Node_setSelected(entity2, true); + Node_setSelected(entity3, true); + + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 3); + + GlobalCommandSystem().executeCommand("GroupCycleForward"); + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1) + << "After cycling forward, only one group member should be selected"; + + auto firstSelected = GlobalSelectionSystem().ultimateSelected(); + GlobalCommandSystem().executeCommand("GroupCycleForward"); + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); + + auto secondSelected = GlobalSelectionSystem().ultimateSelected(); + EXPECT_NE(firstSelected, secondSelected) + << "Cycling forward should select a different group member"; + + GlobalCommandSystem().executeCommand("GroupCycleBackward"); + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); + + auto thirdSelected = GlobalSelectionSystem().ultimateSelected(); + EXPECT_EQ(thirdSelected, firstSelected) + << "Cycling backward should return to the previously selected member"; +} + } diff --git a/test/Transformation.cpp b/test/Transformation.cpp index 9a31cffde0..c33299ceb9 100644 --- a/test/Transformation.cpp +++ b/test/Transformation.cpp @@ -1,8 +1,11 @@ #include "RadiantTest.h" +#include +#include #include "ieclass.h" #include "ilayer.h" #include "iselection.h" +#include "iarray.h" #include "scene/EntityNode.h" #include "itransformable.h" #include "icommandsystem.h" @@ -13,6 +16,7 @@ #include "algorithm/View.h" #include "algorithm/Entity.h" #include "algorithm/Primitives.h" +#include "algorithm/Scene.h" namespace test { @@ -159,7 +163,7 @@ TEST_F(TransformationTest, CloneSelectedPlacesNodeInActiveLayer) { auto& layerManager = GlobalMapModule().getRoot()->getLayerManager(); - // Create a non-default layer and make it active + // Create a layer and make it active auto testLayerId = layerManager.createLayer("TestLayer"); layerManager.setActiveLayer(testLayerId); @@ -171,13 +175,10 @@ TEST_F(TransformationTest, CloneSelectedPlacesNodeInActiveLayer) EXPECT_EQ(entityNode->getLayers(), scene::LayerList{ 0 }); - // Clone the selection GlobalCommandSystem().executeCommand("CloneSelection"); - // The original should still be on layer 0 EXPECT_EQ(entityNode->getLayers(), scene::LayerList{ 0 }); - // The clone should be on the active layer EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); GlobalSelectionSystem().foreachSelected([&](const scene::INodePtr& node) { @@ -190,7 +191,6 @@ TEST_F(TransformationTest, CloneSelectedDefaultLayerStaysOnDefault) { auto& layerManager = GlobalMapModule().getRoot()->getLayerManager(); - // Active layer is already the default EXPECT_EQ(layerManager.getActiveLayer(), 0); auto entityNode = algorithm::createEntityByClassName("fixed_size_entity"); @@ -216,7 +216,6 @@ TEST_F(TransformationTest, CloneSelectedMovesChildrenToActiveLayer) auto testLayerId = layerManager.createLayer("TestLayer"); layerManager.setActiveLayer(testLayerId); - // Create a func_static entity with a child brush on the default layer auto entityNode = algorithm::createEntityByClassName("func_static"); GlobalMapModule().getRoot()->addChildNode(entityNode); auto brushNode = algorithm::createCubicBrush(entityNode); @@ -229,7 +228,6 @@ TEST_F(TransformationTest, CloneSelectedMovesChildrenToActiveLayer) GlobalCommandSystem().executeCommand("CloneSelection"); - // The cloned entity and its child brush should both be on the active layer EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); GlobalSelectionSystem().foreachSelected([&](const scene::INodePtr& node) { @@ -245,4 +243,132 @@ TEST_F(TransformationTest, CloneSelectedMovesChildrenToActiveLayer) }); } +TEST_F(TransformationTest, ArrayCloneLineFixedOffset) +{ + auto eclass = GlobalEntityClassManager().findClass("fixed_size_entity"); + auto entityNode = GlobalEntityModule().createEntity(eclass); + + GlobalMapModule().getRoot()->addChildNode(entityNode); + Node_setSelected(entityNode, true); + + Vector3 originalPosition = entityNode->worldAABB().getOrigin(); + EXPECT_EQ(originalPosition, Vector3(0, 0, 0)); + + // Create 3 copies with a fixed offset of (100, 0, 0), no rotation + int count = 3; + int offsetMethod = static_cast(ui::ArrayOffsetMethod::Fixed); + Vector3 offset(100, 0, 0); + Vector3 rotation(0, 0, 0); + + GlobalCommandSystem().executeCommand("ArrayCloneSelectionLine", + { cmd::Argument(count), cmd::Argument(offsetMethod), + cmd::Argument(offset), cmd::Argument(rotation) }); + + // We should have 4 selected items: original + 3 clones + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 4); + + // Collect positions of all selected nodes + std::vector positions; + GlobalSelectionSystem().foreachSelected([&](const scene::INodePtr& node) + { + positions.push_back(node->worldAABB().getOrigin()); + }); + + ASSERT_EQ(positions.size(), 4); + + // Sort by X to get deterministic order + std::sort(positions.begin(), positions.end(), + [](const Vector3& a, const Vector3& b) { return a.x() < b.x(); }); + + EXPECT_TRUE(math::isNear(positions[0], Vector3(0, 0, 0), 0.1)) + << "Original should remain at origin"; + EXPECT_TRUE(math::isNear(positions[1], Vector3(100, 0, 0), 0.1)) + << "First clone should be at offset 1x"; + EXPECT_TRUE(math::isNear(positions[2], Vector3(200, 0, 0), 0.1)) + << "Second clone should be at offset 2x"; + EXPECT_TRUE(math::isNear(positions[3], Vector3(300, 0, 0), 0.1)) + << "Third clone should be at offset 3x"; +} + +TEST_F(TransformationTest, ArrayCloneLineEndpointOffset) +{ + auto eclass = GlobalEntityClassManager().findClass("fixed_size_entity"); + auto entityNode = GlobalEntityModule().createEntity(eclass); + + GlobalMapModule().getRoot()->addChildNode(entityNode); + Node_setSelected(entityNode, true); + + // Endpoint mode: offset represents total distance, divided evenly among copies + int count = 4; + int offsetMethod = static_cast(ui::ArrayOffsetMethod::Endpoint); + Vector3 totalOffset(400, 0, 0); + Vector3 rotation(0, 0, 0); + + GlobalCommandSystem().executeCommand("ArrayCloneSelectionLine", + { cmd::Argument(count), cmd::Argument(offsetMethod), + cmd::Argument(totalOffset), cmd::Argument(rotation) }); + + // 5 selected: original + 4 clones + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 5); + + std::vector positions; + GlobalSelectionSystem().foreachSelected([&](const scene::INodePtr& node) + { + positions.push_back(node->worldAABB().getOrigin()); + }); + + std::sort(positions.begin(), positions.end(), + [](const Vector3& a, const Vector3& b) { return a.x() < b.x(); }); + + ASSERT_EQ(positions.size(), 5); + + // Each clone should be offset by totalOffset/count = (100,0,0) * i + for (int i = 0; i < 5; ++i) + { + EXPECT_TRUE(math::isNear(positions[i], Vector3(100.0 * i, 0, 0), 0.1)) + << "Clone " << i << " position mismatch"; + } +} + +TEST_F(TransformationTest, ArrayCloneCircle) +{ + auto eclass = GlobalEntityClassManager().findClass("fixed_size_entity"); + auto entityNode = GlobalEntityModule().createEntity(eclass); + + GlobalMapModule().getRoot()->addChildNode(entityNode); + Node_setSelected(entityNode, true); + + int count = 4; + double radius = 200.0; + double startAngle = 0.0; + double endAngle = 360.0; + int rotateToCenter = 0; + + GlobalCommandSystem().executeCommand("ArrayCloneSelectionCircle", + { cmd::Argument(count), cmd::Argument(radius), + cmd::Argument(startAngle), cmd::Argument(endAngle), + cmd::Argument(rotateToCenter) }); + + // 5 selected: original + 4 clones + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 5); + + // Each clone should be a certain distance from origin + int clonesAtExpectedRadius = 0; + GlobalSelectionSystem().foreachSelected([&](const scene::INodePtr& node) + { + auto pos = node->worldAABB().getOrigin(); + double dist = pos.getLength(); + + // The original is at origin, clones within radius + if (dist > 1.0) + { + EXPECT_NEAR(dist, radius, 1.0) + << "Clone should be at the specified radius"; + ++clonesAtExpectedRadius; + } + }); + + EXPECT_EQ(clonesAtExpectedRadius, 4) << "All 4 clones should be at the circle radius"; +} + } diff --git a/test/resources/tdm/models/md5/test_v11.md5mesh b/test/resources/tdm/models/md5/test_v11.md5mesh new file mode 100644 index 0000000000..88e6f2d19f --- /dev/null +++ b/test/resources/tdm/models/md5/test_v11.md5mesh @@ -0,0 +1,35 @@ +MD5Version 11 +commandline "" + +numJoints 1 +numMeshes 1 + +joints { + "origin" -1 ( 0.000000 0.000000 0.000000 ) ( 0.000000 0.000000 0.000000 ) // +} + +mesh { + name "test_quad" + + shader "textures/common/caulk" + + flags { + flag1 + } + + numverts 4 + vert 0 ( 0.000000 0.000000 ) 0 1 ( 1.0 0.0 0.0 1.0 ) + vert 1 ( 1.000000 0.000000 ) 1 1 ( 0.0 1.0 0.0 1.0 ) + vert 2 ( 1.000000 1.000000 ) 2 1 ( 0.0 0.0 1.0 1.0 ) + vert 3 ( 0.000000 1.000000 ) 3 1 ( 1.0 1.0 1.0 1.0 ) + + numtris 2 + tri 0 0 1 2 + tri 1 0 2 3 + + numweights 4 + weight 0 0 1.000000 ( 0.000000 0.000000 0.000000 ) + weight 1 0 1.000000 ( 64.000000 0.000000 0.000000 ) + weight 2 0 1.000000 ( 64.000000 64.000000 0.000000 ) + weight 3 0 1.000000 ( 0.000000 64.000000 0.000000 ) +} diff --git a/test/resources/tdm/models/md5/test_v12.md5mesh b/test/resources/tdm/models/md5/test_v12.md5mesh new file mode 100644 index 0000000000..d0595f0130 --- /dev/null +++ b/test/resources/tdm/models/md5/test_v12.md5mesh @@ -0,0 +1,35 @@ +MD5Version 12 +commandline "" + +numJoints 1 +numMeshes 1 + +joints { + "origin" -1 ( 0.000000 0.000000 0.000000 ) ( 0.000000 0.000000 0.000000 ) // +} + +mesh { + shader "textures/common/caulk" + + numverts 4 + vert 0 ( 0.000000 0.000000 ) 0 1 ( 0.000000 0.000000 1.000000 ) ( 1.000000 0.000000 0.000000 1.000000 ) + vert 1 ( 1.000000 0.000000 ) 1 1 ( 0.000000 0.000000 1.000000 ) ( 1.000000 0.000000 0.000000 1.000000 ) + vert 2 ( 1.000000 1.000000 ) 2 1 ( 0.000000 0.000000 1.000000 ) ( 1.000000 0.000000 0.000000 1.000000 ) + vert 3 ( 0.000000 1.000000 ) 3 1 ( 0.000000 0.000000 1.000000 ) ( 1.000000 0.000000 0.000000 1.000000 ) + + numtris 2 + tri 0 0 1 2 + tri 1 0 2 3 + + numweights 4 + weight 0 0 1.000000 ( 0.000000 0.000000 0.000000 ) + weight 1 0 1.000000 ( 64.000000 0.000000 0.000000 ) + weight 2 0 1.000000 ( 64.000000 64.000000 0.000000 ) + weight 3 0 1.000000 ( 0.000000 64.000000 0.000000 ) + + numvertexcolors 4 + vertexcolor 0 ( 1.0 0.0 0.0 1.0 ) + vertexcolor 1 ( 0.0 1.0 0.0 1.0 ) + vertexcolor 2 ( 0.0 0.0 1.0 1.0 ) + vertexcolor 3 ( 1.0 1.0 1.0 1.0 ) +}