Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions test/Models.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
48 changes: 48 additions & 0 deletions test/SelectionAlgorithm.cpp
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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";
}

}
140 changes: 133 additions & 7 deletions test/Transformation.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#include "RadiantTest.h"

#include <algorithm>
#include <vector>
#include "ieclass.h"
#include "ilayer.h"
#include "iselection.h"
#include "iarray.h"
#include "scene/EntityNode.h"
#include "itransformable.h"
#include "icommandsystem.h"
Expand All @@ -13,6 +16,7 @@
#include "algorithm/View.h"
#include "algorithm/Entity.h"
#include "algorithm/Primitives.h"
#include "algorithm/Scene.h"

namespace test
{
Expand Down Expand Up @@ -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);

Expand All @@ -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)
{
Expand All @@ -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");
Expand All @@ -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);
Expand All @@ -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)
{
Expand All @@ -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<int>(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<Vector3> 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<int>(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<Vector3> 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";
}

}
35 changes: 35 additions & 0 deletions test/resources/tdm/models/md5/test_v11.md5mesh
Original file line number Diff line number Diff line change
@@ -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 )
}
35 changes: 35 additions & 0 deletions test/resources/tdm/models/md5/test_v12.md5mesh
Original file line number Diff line number Diff line change
@@ -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 )
}