From c836d396b3ac03bda9846c8a0b3b598e2e51ed1a Mon Sep 17 00:00:00 2001 From: codereader Date: Sun, 19 Sep 2021 06:25:04 +0200 Subject: [PATCH] #5746: Add unit test covering the drag-manipulation of a face --- include/itexturetoolmodel.h | 12 ++ radiantcore/selection/textool/FaceNode.h | 8 +- radiantcore/selection/textool/NodeBase.h | 2 +- test/TextureTool.cpp | 163 +++++++++++++++++++++++ test/algorithm/Primitives.h | 18 ++- 5 files changed, 200 insertions(+), 3 deletions(-) diff --git a/include/itexturetoolmodel.h b/include/itexturetoolmodel.h index bf008da871..c2ad2ddf22 100644 --- a/include/itexturetoolmodel.h +++ b/include/itexturetoolmodel.h @@ -57,6 +57,18 @@ class INode : virtual void render() = 0; }; +// Node representing a single brush face +class IFaceNode : + public virtual INode +{ +public: + virtual ~IFaceNode() {} + + using Ptr = std::shared_ptr; + + virtual IFace& getFace() = 0; +}; + /** * The scene graph of all texture tool items. From all the selected * items in the regular SceneGraph the texture-editable elements diff --git a/radiantcore/selection/textool/FaceNode.h b/radiantcore/selection/textool/FaceNode.h index 31ece69dd2..8229fe4f9c 100644 --- a/radiantcore/selection/textool/FaceNode.h +++ b/radiantcore/selection/textool/FaceNode.h @@ -8,7 +8,8 @@ namespace textool { class FaceNode : - public NodeBase + public NodeBase, + public IFaceNode { private: IFace& _face; @@ -19,6 +20,11 @@ class FaceNode : _face(face) {} + IFace& getFace() override + { + return _face; + } + void beginTransformation() override { _face.undoSave(); diff --git a/radiantcore/selection/textool/NodeBase.h b/radiantcore/selection/textool/NodeBase.h index af833f827c..3ed61afa59 100644 --- a/radiantcore/selection/textool/NodeBase.h +++ b/radiantcore/selection/textool/NodeBase.h @@ -7,7 +7,7 @@ namespace textool { class NodeBase : - public INode + public virtual INode { private: selection::BasicSelectable _selectable; diff --git a/test/TextureTool.cpp b/test/TextureTool.cpp index 25aab12203..0f2c2f708a 100644 --- a/test/TextureTool.cpp +++ b/test/TextureTool.cpp @@ -217,6 +217,18 @@ inline AABB getTextureSpaceBounds(const IPatch& patch) return bounds; } +inline AABB getTextureSpaceBounds(const IFace& face) +{ + AABB bounds; + + for (const auto& vertex : face.getWinding()) + { + bounds.includePoint({ vertex.texcoord.x(), vertex.texcoord.y(), 0 }); + } + + return bounds; +} + constexpr int TEXTOOL_WIDTH = 500; constexpr int TEXTOOL_HEIGHT = 400; @@ -235,6 +247,25 @@ inline scene::INodePtr setupPatchNodeForTextureTool() return patchNode; } +inline textool::IFaceNode::Ptr findTexToolFaceWithNormal(const Vector3& normal) +{ + textool::IFaceNode::Ptr result; + + GlobalTextureToolSceneGraph().foreachNode([&](const textool::INode::Ptr& node) + { + auto faceNode = std::dynamic_pointer_cast(node); + + if (faceNode && math::isNear(faceNode->getFace().getPlane3().normal(), normal, 0.01)) + { + result = faceNode; + } + + return result == nullptr; + }); + + return result; +} + // Default manipulator mode should be "Drag" TEST_F(TextureToolTest, DefaultManipulatorMode) { @@ -301,6 +332,52 @@ TEST_F(TextureToolTest, TestSelectPatchByPoint) EXPECT_EQ(selectedNodes.size(), 1) << "Only one patch should be selected"; } +TEST_F(TextureToolTest, TestSelectFaceByPoint) +{ + auto worldspawn = GlobalMapModule().findOrInsertWorldspawn(); + auto brush = algorithm::createCubicBrush(worldspawn, Vector3(0, 256, 256), "textures/numbers/1"); + scene::addNodeToContainer(brush, worldspawn); + + // Put all faces into the tex tool scene + Node_setSelected(brush, true); + + auto faceUp = algorithm::findBrushFaceWithNormal(Node_getIBrush(brush), Vector3(0, 0, 1)); + + // Check the face + auto textoolFace = findTexToolFaceWithNormal(faceUp->getPlane3().normal()); + EXPECT_FALSE(textoolFace->isSelected()) << "Face should be unselected at start"; + + // Get the texture space bounds of this face + // Construct a view that includes the patch UV bounds + auto bounds = getTextureSpaceBounds(*faceUp); + bounds.extents *= 1.2f; + + render::TextureToolView view; + view.constructFromTextureSpaceBounds(bounds, TEXTOOL_WIDTH, TEXTOOL_HEIGHT); + + // Check the device coords of the face centroid + auto centroid = algorithm::getFaceCentroid(faceUp); + auto centroidTransformed = view.GetViewProjection().transformPoint(Vector3(centroid.x(), centroid.y(), 0)); + Vector2 devicePoint(centroidTransformed.x(), centroidTransformed.y()); + + // Use the device point we calculated for this vertex and use it to construct a selection test + ConstructSelectionTest(view, selection::Rectangle::ConstructFromPoint(devicePoint, Vector2(0.02f, 0.02f))); + + SelectionVolume test(view); + GlobalTextureToolSelectionSystem().selectPoint(test, SelectionSystem::eToggle); + + // Check if the node was selected + std::vector selectedNodes; + GlobalTextureToolSelectionSystem().foreachSelectedNode([&](const textool::INode::Ptr& node) + { + selectedNodes.push_back(node); + return true; + }); + + EXPECT_EQ(selectedNodes.size(), 1) << "Only one item should be selected"; + EXPECT_EQ(selectedNodes.front(), textoolFace) << "The face should be selected"; +} + TEST_F(TextureToolTest, TestSelectPatchByArea) { auto patchNode = setupPatchNodeForTextureTool(); @@ -332,4 +409,90 @@ TEST_F(TextureToolTest, TestSelectPatchByArea) EXPECT_EQ(selectedNodes.size(), 1) << "Only one patch should be selected"; } +inline std::vector getTexcoords(const IFace* face) +{ + std::vector uvs; + + for (const auto& vertex : face->getWinding()) + { + uvs.push_back(vertex.texcoord); + } + + return uvs; +} + +TEST_F(TextureToolTest, DragManipulateFace) +{ + auto worldspawn = GlobalMapModule().findOrInsertWorldspawn(); + auto brush = algorithm::createCubicBrush(worldspawn, Vector3(0, 256, 256), "textures/numbers/1"); + + // Put all faces into the tex tool scene + Node_setSelected(brush, true); + + auto faceUp = algorithm::findBrushFaceWithNormal(Node_getIBrush(brush), Vector3(0, 0, 1)); + auto faceDown = algorithm::findBrushFaceWithNormal(Node_getIBrush(brush), Vector3(0, 0, -1)); + + // Remember the texcoords of this face + auto oldFaceUpUvs = getTexcoords(faceUp); + auto oldFaceDownUvs = getTexcoords(faceDown); + + // Select the face + auto textoolFace = findTexToolFaceWithNormal(faceUp->getPlane3().normal()); + textoolFace->setSelected(true); + + // Get the texture space bounds of this face + // Construct a view that includes the patch UV bounds + auto bounds = getTextureSpaceBounds(*faceUp); + bounds.extents *= 1.2f; + + render::TextureToolView view; + view.constructFromTextureSpaceBounds(bounds, TEXTOOL_WIDTH, TEXTOOL_HEIGHT); + + // Check the device coords of the face centroid + auto centroid = algorithm::getFaceCentroid(faceUp); + auto centroidTransformed = view.GetViewProjection().transformPoint(Vector3(centroid.x(), centroid.y(), 0)); + Vector2 devicePoint(centroidTransformed.x(), centroidTransformed.y()); + + GlobalTextureToolSelectionSystem().onManipulationStart(); + + // Simulate a transformation by click-and-drag + auto manipulator = GlobalTextureToolSelectionSystem().getActiveManipulator(); + EXPECT_EQ(manipulator->getType(), selection::IManipulator::Drag) << "Wrong manipulator"; + + render::View scissored(view); + ConstructSelectionTest(scissored, selection::Rectangle::ConstructFromPoint(devicePoint, Vector2(0.05, 0.05))); + + auto manipComponent = manipulator->getActiveComponent(); + auto pivot2World = GlobalTextureToolSelectionSystem().getPivot2World(); + manipComponent->beginTransformation(pivot2World, scissored, devicePoint); + + // Move the device point a bit to the lower right + auto secondDevicePoint = devicePoint + (Vector2(1, -1) - devicePoint) / 2; + + render::View scissored2(view); + ConstructSelectionTest(scissored2, selection::Rectangle::ConstructFromPoint(secondDevicePoint, Vector2(0.05, 0.05))); + + manipComponent->transform(pivot2World, scissored2, secondDevicePoint, selection::IManipulator::Component::Constraint::Unconstrained); + + GlobalTextureToolSelectionSystem().onManipulationFinished(); + + // All the texcoords should have been moved to the lower right (U increased, V increased) + auto oldUv = oldFaceUpUvs.begin(); + for (const auto& vertex : faceUp->getWinding()) + { + EXPECT_LT(oldUv->x(), vertex.texcoord.x()); + EXPECT_LT(oldUv->y(), vertex.texcoord.y()); + ++oldUv; + } + + // The texcoords of the other face should not have been changed + oldUv = oldFaceDownUvs.begin(); + for (const auto& vertex : faceDown->getWinding()) + { + EXPECT_EQ(oldUv->x(), vertex.texcoord.x()); + EXPECT_EQ(oldUv->y(), vertex.texcoord.y()); + ++oldUv; + } +} + } diff --git a/test/algorithm/Primitives.h b/test/algorithm/Primitives.h index 3cbb09bdf3..5df0918a12 100644 --- a/test/algorithm/Primitives.h +++ b/test/algorithm/Primitives.h @@ -62,7 +62,7 @@ inline scene::INodePtr createCuboidBrush(const scene::INodePtr& parent, inline IFace* findBrushFaceWithNormal(IBrush* brush, const Vector3& normal) { - for (auto i = 0; brush->getNumFaces(); ++i) + for (auto i = 0; i < brush->getNumFaces(); ++i) { auto& face = brush->getFace(i); @@ -90,6 +90,22 @@ inline bool faceHasVertex(const IFace* face, const std::functiongetWinding().empty()) return { 0, 0 }; + + Vector2 centroid = face->getWinding()[0].texcoord; + + for (std::size_t i = 1; i < face->getWinding().size(); ++i) + { + centroid += face->getWinding()[i].texcoord; + } + + centroid /= static_cast(face->getWinding().size()); + + return centroid; +} + inline scene::INodePtr createPatchFromBounds(const scene::INodePtr& parent, const AABB& bounds = AABB(Vector3(0, 0, 0), Vector3(64, 256, 128)), const std::string& material = "_default")