From 2c16d8352ba53ae798c11f028f92f8e7971ef212 Mon Sep 17 00:00:00 2001 From: Adam Kewley Date: Thu, 16 Nov 2023 14:50:36 +0100 Subject: [PATCH] Refactor ModelGraph helper functions into seperate ModelGraphHelpers hpp/cpp files (#802) --- src/OpenSimCreator/CMakeLists.txt | 4 +- .../ModelGraph/ModelGraphHelpers.cpp | 329 +++++++++++++++++ .../ModelGraph/ModelGraphHelpers.hpp | 114 ++++++ .../UI/Tabs/MeshImporterTab.cpp | 349 +----------------- 4 files changed, 447 insertions(+), 349 deletions(-) create mode 100644 src/OpenSimCreator/ModelGraph/ModelGraphHelpers.cpp create mode 100644 src/OpenSimCreator/ModelGraph/ModelGraphHelpers.hpp diff --git a/src/OpenSimCreator/CMakeLists.txt b/src/OpenSimCreator/CMakeLists.txt index 6e43d4d70..aa0008c6a 100644 --- a/src/OpenSimCreator/CMakeLists.txt +++ b/src/OpenSimCreator/CMakeLists.txt @@ -68,6 +68,8 @@ add_library(OpenSimCreator STATIC ModelGraph/MeshEl.cpp ModelGraph/MeshEl.hpp ModelGraph/ModelGraph.hpp + ModelGraph/ModelGraphHelpers.cpp + ModelGraph/ModelGraphHelpers.hpp ModelGraph/ModelGraphIDs.cpp ModelGraph/ModelGraphIDs.hpp ModelGraph/ModelGraphStrings.hpp @@ -252,7 +254,7 @@ add_library(OpenSimCreator STATIC Utils/TPS3D.hpp Utils/UndoableModelActions.cpp Utils/UndoableModelActions.hpp -) + "ModelGraph/ModelGraphHelpers.cpp") # OpenSimCreatorConfig.hpp # diff --git a/src/OpenSimCreator/ModelGraph/ModelGraphHelpers.cpp b/src/OpenSimCreator/ModelGraph/ModelGraphHelpers.cpp new file mode 100644 index 000000000..4d9c66415 --- /dev/null +++ b/src/OpenSimCreator/ModelGraph/ModelGraphHelpers.cpp @@ -0,0 +1,329 @@ +#include "ModelGraphHelpers.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +void osc::SelectOnly(ModelGraph& mg, SceneEl const& e) +{ + mg.deSelectAll(); + mg.select(e); +} + +bool osc::HasSelection(ModelGraph const& mg) +{ + return !mg.getSelected().empty(); +} + +void osc::DeleteSelected(ModelGraph& mg) +{ + // copy deletion set to ensure iterator can't be invalidated by deletion + std::unordered_set selected = mg.getSelected(); + + for (UID id : selected) + { + mg.deleteElByID(id); + } + + mg.deSelectAll(); +} + +osc::CStringView osc::getLabel(ModelGraph const& mg, UID id) +{ + return mg.getElByID(id).getLabel(); +} + +osc::Transform osc::GetTransform(ModelGraph const& mg, UID id) +{ + return mg.getElByID(id).getXForm(mg); +} + +osc::Vec3 osc::GetPosition(ModelGraph const& mg, UID id) +{ + return mg.getElByID(id).getPos(mg); +} + +// returns `true` if `body` participates in any joint in the model graph +bool osc::IsAChildAttachmentInAnyJoint(ModelGraph const& mg, SceneEl const& el) +{ + auto const iterable = mg.iter(); + return std::any_of(iterable.begin(), iterable.end(), [id = el.getID()](JointEl const& j) + { + return j.getChildID() == id; + }); +} + +// returns `true` if a Joint is complete b.s. +bool osc::IsGarbageJoint(ModelGraph const& modelGraph, JointEl const& jointEl) +{ + if (jointEl.getChildID() == ModelGraphIDs::Ground()) + { + return true; // ground cannot be a child in a joint + } + + if (jointEl.getParentID() == jointEl.getChildID()) + { + return true; // is directly attached to itself + } + + if (jointEl.getParentID() != ModelGraphIDs::Ground() && !modelGraph.containsEl(jointEl.getParentID())) + { + return true; // has a parent ID that's invalid for this model graph + } + + if (!modelGraph.containsEl(jointEl.getChildID())) + { + return true; // has a child ID that's invalid for this model graph + } + + return false; +} + +// returns `true` if `joint` is indirectly or directly attached to ground via its parent +bool osc::IsJointAttachedToGround( + ModelGraph const& modelGraph, + JointEl const& joint, + std::unordered_set& previousVisits) +{ + OSC_ASSERT_ALWAYS(!IsGarbageJoint(modelGraph, joint)); + + if (joint.getParentID() == ModelGraphIDs::Ground()) + { + return true; // it's directly attached to ground + } + + auto const* const parent = modelGraph.tryGetElByID(joint.getParentID()); + if (!parent) + { + return false; // joint's parent is garbage + } + + // else: recurse to parent + return IsBodyAttachedToGround(modelGraph, *parent, previousVisits); +} + +// returns `true` if `body` is attached to ground +bool osc::IsBodyAttachedToGround( + ModelGraph const& modelGraph, + BodyEl const& body, + std::unordered_set& previouslyVisitedJoints) +{ + bool childInAtLeastOneJoint = false; + + for (JointEl const& jointEl : modelGraph.iter()) + { + OSC_ASSERT(!IsGarbageJoint(modelGraph, jointEl)); + + if (jointEl.getChildID() == body.getID()) + { + childInAtLeastOneJoint = true; + + bool const alreadyVisited = !previouslyVisitedJoints.emplace(jointEl.getID()).second; + if (alreadyVisited) + { + continue; // skip this joint: was previously visited + } + + if (IsJointAttachedToGround(modelGraph, jointEl, previouslyVisitedJoints)) + { + return true; // recurse + } + } + } + + return !childInAtLeastOneJoint; +} + +bool osc::GetModelGraphIssues( + ModelGraph const& modelGraph, + std::vector& issuesOut) +{ + issuesOut.clear(); + + for (JointEl const& joint : modelGraph.iter()) + { + if (IsGarbageJoint(modelGraph, joint)) + { + std::stringstream ss; + ss << joint.getLabel() << ": joint is garbage (this is an implementation error)"; + throw std::runtime_error{std::move(ss).str()}; + } + } + + for (BodyEl const& body : modelGraph.iter()) + { + std::unordered_set previouslyVisitedJoints; + if (!IsBodyAttachedToGround(modelGraph, body, previouslyVisitedJoints)) + { + std::stringstream ss; + ss << body.getLabel() << ": body is not attached to ground: it is connected by a joint that, itself, does not connect to ground"; + issuesOut.push_back(std::move(ss).str()); + } + } + + return !issuesOut.empty(); +} + +// returns a string representing the subheader of a scene element +std::string osc::GetContextMenuSubHeaderText( + ModelGraph const& mg, + SceneEl const& e) +{ + std::stringstream ss; + std::visit(Overload + { + [&ss](GroundEl const&) + { + ss << "(scene origin)"; + }, + [&ss, &mg](MeshEl const& m) + { + ss << '(' << m.getClass().getName() << ", " << m.getPath().filename().string() << ", attached to " << getLabel(mg, m.getParentID()) << ')'; + }, + [&ss](BodyEl const& b) + { + ss << '(' << b.getClass().getName() << ')'; + }, + [&ss, &mg](JointEl const& j) + { + ss << '(' << j.getSpecificTypeName() << ", " << getLabel(mg, j.getChildID()) << " --> " << getLabel(mg, j.getParentID()) << ')'; + }, + [&ss, &mg](StationEl const& s) + { + ss << '(' << s.getClass().getName() << ", attached to " << getLabel(mg, s.getParentID()) << ')'; + }, + [&ss, &mg](EdgeEl const& e) + { + ss << '(' << e.getClass().getName() << ", " << getLabel(mg, e.getFirstAttachmentID()) << " --> " << getLabel(mg, e.getSecondAttachmentID()) << ')'; + } + }, e.toVariant()); + return std::move(ss).str(); +} + +// returns true if the given element (ID) is in the "selection group" of +bool osc::IsInSelectionGroupOf( + ModelGraph const& mg, + UID parent, + UID id) +{ + if (id == ModelGraphIDs::Empty() || parent == ModelGraphIDs::Empty()) + { + return false; + } + + if (id == parent) + { + return true; + } + + BodyEl const* bodyEl = nullptr; + + if (auto const* be = mg.tryGetElByID(parent)) + { + bodyEl = be; + } + else if (auto const* me = mg.tryGetElByID(parent)) + { + bodyEl = mg.tryGetElByID(me->getParentID()); + } + + if (!bodyEl) + { + return false; // parent isn't attached to any body (or isn't a body) + } + + if (auto const* be = mg.tryGetElByID(id)) + { + return be->getID() == bodyEl->getID(); + } + else if (auto const* me = mg.tryGetElByID(id)) + { + return me->getParentID() == bodyEl->getID(); + } + else + { + return false; + } +} + +void osc::SelectAnythingGroupedWith(ModelGraph& mg, UID el) +{ + ForEachIDInSelectionGroup(mg, el, [&mg](UID other) + { + mg.select(other); + }); +} + +osc::UID osc::GetStationAttachmentParent(ModelGraph const& mg, SceneEl const& el) +{ + return std::visit(Overload + { + [](GroundEl const&) { return ModelGraphIDs::Ground(); }, + [&mg](MeshEl const& meshEl) { return mg.containsEl(meshEl.getParentID()) ? meshEl.getParentID() : ModelGraphIDs::Ground(); }, + [](BodyEl const& bodyEl) { return bodyEl.getID(); }, + [](JointEl const&) { return ModelGraphIDs::Ground(); }, + [](StationEl const&) { return ModelGraphIDs::Ground(); }, + [](EdgeEl const&) { return ModelGraphIDs::Ground(); }, + }, el.toVariant()); +} + +void osc::PointAxisTowards( + ModelGraph& mg, + UID id, + int axis, + UID other) +{ + Vec3 const choicePos = GetPosition(mg, other); + Transform const sourceXform = Transform{.position = GetPosition(mg, id)}; + + mg.updElByID(id).setXform(mg, PointAxisTowards(sourceXform, axis, choicePos)); +} + +// returns recommended rim intensity for an element in the model graph +osc::SceneDecorationFlags osc::computeFlags( + ModelGraph const& mg, + UID id, + UID hoverID) +{ + if (id == ModelGraphIDs::Empty()) + { + return SceneDecorationFlags::None; + } + else if (mg.isSelected(id)) + { + return SceneDecorationFlags::IsSelected; + } + else if (id == hoverID) + { + return SceneDecorationFlags::IsHovered | SceneDecorationFlags::IsChildOfHovered; + } + else if (IsInSelectionGroupOf(mg, hoverID, id)) + { + return SceneDecorationFlags::IsChildOfHovered; + } + else + { + return SceneDecorationFlags::None; + } +} diff --git a/src/OpenSimCreator/ModelGraph/ModelGraphHelpers.hpp b/src/OpenSimCreator/ModelGraph/ModelGraphHelpers.hpp new file mode 100644 index 000000000..57cf5e217 --- /dev/null +++ b/src/OpenSimCreator/ModelGraph/ModelGraphHelpers.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace osc { class BodyEl; } +namespace osc { class JointEl; } + +namespace osc +{ + void SelectOnly(ModelGraph&, SceneEl const&); + bool HasSelection(ModelGraph const&); + void DeleteSelected(ModelGraph&); + CStringView getLabel(ModelGraph const&, UID); + Transform GetTransform(ModelGraph const&, UID); + Vec3 GetPosition(ModelGraph const&, UID); + + // returns `true` if `body` participates in any joint in the model graph + bool IsAChildAttachmentInAnyJoint(ModelGraph const&, SceneEl const&); + + // returns `true` if a Joint is complete b.s. + bool IsGarbageJoint(ModelGraph const&, JointEl const&); + + // returns `true` if a body is indirectly or directly attached to ground + bool IsBodyAttachedToGround( + ModelGraph const&, + BodyEl const&, + std::unordered_set& previouslyVisitedJoints + ); + + // returns `true` if `joint` is indirectly or directly attached to ground via its parent + bool IsJointAttachedToGround( + ModelGraph const&, + JointEl const&, + std::unordered_set& + ); + + // returns `true` if `body` is attached to ground + bool IsBodyAttachedToGround( + ModelGraph const&, + BodyEl const&, + std::unordered_set& + ); + + // returns `true` if `modelGraph` contains issues + bool GetModelGraphIssues( + ModelGraph const&, + std::vector& + ); + + // returns a string representing the subheader of a scene element + std::string GetContextMenuSubHeaderText( + ModelGraph const& mg, + SceneEl const& + ); + + // returns true if the given element (ID) is in the "selection group" of + bool IsInSelectionGroupOf( + ModelGraph const&, + UID parent, + UID id + ); + + template + void ForEachIDInSelectionGroup( + ModelGraph const& mg, + UID parent, + Consumer f) + requires Invocable + { + for (SceneEl const& e : mg.iter()) + { + UID const id = e.getID(); + + if (IsInSelectionGroupOf(mg, parent, id)) + { + f(id); + } + } + } + + void SelectAnythingGroupedWith(ModelGraph&, UID); + + // returns the ID of the thing the station should attach to when trying to + // attach to something in the scene + UID GetStationAttachmentParent(ModelGraph const&, SceneEl const&); + + // points an axis of a given element towards some other element in the model graph + void PointAxisTowards( + ModelGraph&, + UID, + int axis, + UID + ); + + // returns recommended rim intensity for an element in the model graph + SceneDecorationFlags computeFlags( + ModelGraph const&, + UID id, + UID hoverID = ModelGraphIDs::Empty() + ); +} diff --git a/src/OpenSimCreator/UI/Tabs/MeshImporterTab.cpp b/src/OpenSimCreator/UI/Tabs/MeshImporterTab.cpp index 0b8bcca58..58161e45a 100644 --- a/src/OpenSimCreator/UI/Tabs/MeshImporterTab.cpp +++ b/src/OpenSimCreator/UI/Tabs/MeshImporterTab.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -201,354 +202,6 @@ using osc::JointEl; using osc::StationEl; using osc::EdgeEl; -// modelgraph support -// -// scene elements are collected into a single, potentially interconnected, model graph -// datastructure. This datastructure is what ultimately maps into an "OpenSim::Model". -// -// Main design considerations: -// -// - Must have somewhat fast associative lookup semantics, because the UI needs to -// traverse the graph in a value-based (rather than pointer-based) way -// -// - Must have value semantics, so that other code such as the undo/redo buffer can -// copy an entire ModelGraph somewhere else in memory without having to worry about -// aliased mutations -namespace osc -{ - void SelectOnly(ModelGraph& mg, SceneEl const& e) - { - mg.deSelectAll(); - mg.select(e); - } - - bool HasSelection(ModelGraph const& mg) - { - return !mg.getSelected().empty(); - } - - void DeleteSelected(ModelGraph& mg) - { - // copy deletion set to ensure iterator can't be invalidated by deletion - std::unordered_set selected = mg.getSelected(); - - for (UID id : selected) - { - mg.deleteElByID(id); - } - - mg.deSelectAll(); - } - - CStringView getLabel(ModelGraph const& mg, UID id) - { - return mg.getElByID(id).getLabel(); - } - - Transform GetTransform(ModelGraph const& mg, UID id) - { - return mg.getElByID(id).getXForm(mg); - } - - Vec3 GetPosition(ModelGraph const& mg, UID id) - { - return mg.getElByID(id).getPos(mg); - } - - // returns `true` if `body` participates in any joint in the model graph - bool IsAChildAttachmentInAnyJoint(ModelGraph const& mg, SceneEl const& el) - { - auto const iterable = mg.iter(); - return std::any_of(iterable.begin(), iterable.end(), [id = el.getID()](JointEl const& j) - { - return j.getChildID() == id; - }); - } - - // returns `true` if a Joint is complete b.s. - bool IsGarbageJoint(ModelGraph const& modelGraph, JointEl const& jointEl) - { - if (jointEl.getChildID() == ModelGraphIDs::Ground()) - { - return true; // ground cannot be a child in a joint - } - - if (jointEl.getParentID() == jointEl.getChildID()) - { - return true; // is directly attached to itself - } - - if (jointEl.getParentID() != ModelGraphIDs::Ground() && !modelGraph.containsEl(jointEl.getParentID())) - { - return true; // has a parent ID that's invalid for this model graph - } - - if (!modelGraph.containsEl(jointEl.getChildID())) - { - return true; // has a child ID that's invalid for this model graph - } - - return false; - } - - // returns `true` if a body is indirectly or directly attached to ground - bool IsBodyAttachedToGround( - ModelGraph const& modelGraph, - BodyEl const& body, - std::unordered_set& previouslyVisitedJoints - ); - - // returns `true` if `joint` is indirectly or directly attached to ground via its parent - bool IsJointAttachedToGround( - ModelGraph const& modelGraph, - JointEl const& joint, - std::unordered_set& previousVisits) - { - OSC_ASSERT_ALWAYS(!IsGarbageJoint(modelGraph, joint)); - - if (joint.getParentID() == ModelGraphIDs::Ground()) - { - return true; // it's directly attached to ground - } - - auto const* const parent = modelGraph.tryGetElByID(joint.getParentID()); - if (!parent) - { - return false; // joint's parent is garbage - } - - // else: recurse to parent - return IsBodyAttachedToGround(modelGraph, *parent, previousVisits); - } - - // returns `true` if `body` is attached to ground - bool IsBodyAttachedToGround( - ModelGraph const& modelGraph, - BodyEl const& body, - std::unordered_set& previouslyVisitedJoints) - { - bool childInAtLeastOneJoint = false; - - for (JointEl const& jointEl : modelGraph.iter()) - { - OSC_ASSERT(!IsGarbageJoint(modelGraph, jointEl)); - - if (jointEl.getChildID() == body.getID()) - { - childInAtLeastOneJoint = true; - - bool const alreadyVisited = !previouslyVisitedJoints.emplace(jointEl.getID()).second; - if (alreadyVisited) - { - continue; // skip this joint: was previously visited - } - - if (IsJointAttachedToGround(modelGraph, jointEl, previouslyVisitedJoints)) - { - return true; // recurse - } - } - } - - return !childInAtLeastOneJoint; - } - - // returns `true` if `modelGraph` contains issues - bool GetModelGraphIssues( - ModelGraph const& modelGraph, - std::vector& issuesOut) - { - issuesOut.clear(); - - for (JointEl const& joint : modelGraph.iter()) - { - if (IsGarbageJoint(modelGraph, joint)) - { - std::stringstream ss; - ss << joint.getLabel() << ": joint is garbage (this is an implementation error)"; - throw std::runtime_error{std::move(ss).str()}; - } - } - - for (BodyEl const& body : modelGraph.iter()) - { - std::unordered_set previouslyVisitedJoints; - if (!IsBodyAttachedToGround(modelGraph, body, previouslyVisitedJoints)) - { - std::stringstream ss; - ss << body.getLabel() << ": body is not attached to ground: it is connected by a joint that, itself, does not connect to ground"; - issuesOut.push_back(std::move(ss).str()); - } - } - - return !issuesOut.empty(); - } - - // returns a string representing the subheader of a scene element - std::string GetContextMenuSubHeaderText( - ModelGraph const& mg, - SceneEl const& e) - { - std::stringstream ss; - std::visit(Overload - { - [&ss](GroundEl const&) - { - ss << "(scene origin)"; - }, - [&ss, &mg](MeshEl const& m) - { - ss << '(' << m.getClass().getName() << ", " << m.getPath().filename().string() << ", attached to " << getLabel(mg, m.getParentID()) << ')'; - }, - [&ss](BodyEl const& b) - { - ss << '(' << b.getClass().getName() << ')'; - }, - [&ss, &mg](JointEl const& j) - { - ss << '(' << j.getSpecificTypeName() << ", " << getLabel(mg, j.getChildID()) << " --> " << getLabel(mg, j.getParentID()) << ')'; - }, - [&ss, &mg](StationEl const& s) - { - ss << '(' << s.getClass().getName() << ", attached to " << getLabel(mg, s.getParentID()) << ')'; - }, - [&ss, &mg](EdgeEl const& e) - { - ss << '(' << e.getClass().getName() << ", " << getLabel(mg, e.getFirstAttachmentID()) << " --> " << getLabel(mg, e.getSecondAttachmentID()) << ')'; - } - }, e.toVariant()); - return std::move(ss).str(); - } - - // returns true if the given element (ID) is in the "selection group" of - bool IsInSelectionGroupOf( - ModelGraph const& mg, - UID parent, - UID id) - { - if (id == ModelGraphIDs::Empty() || parent == ModelGraphIDs::Empty()) - { - return false; - } - - if (id == parent) - { - return true; - } - - BodyEl const* bodyEl = nullptr; - - if (auto const* be = mg.tryGetElByID(parent)) - { - bodyEl = be; - } - else if (auto const* me = mg.tryGetElByID(parent)) - { - bodyEl = mg.tryGetElByID(me->getParentID()); - } - - if (!bodyEl) - { - return false; // parent isn't attached to any body (or isn't a body) - } - - if (auto const* be = mg.tryGetElByID(id)) - { - return be->getID() == bodyEl->getID(); - } - else if (auto const* me = mg.tryGetElByID(id)) - { - return me->getParentID() == bodyEl->getID(); - } - else - { - return false; - } - } - - template - void ForEachIDInSelectionGroup( - ModelGraph const& mg, - UID parent, - Consumer f) - requires Invocable - { - for (SceneEl const& e : mg.iter()) - { - UID const id = e.getID(); - - if (IsInSelectionGroupOf(mg, parent, id)) - { - f(id); - } - } - } - - void SelectAnythingGroupedWith(ModelGraph& mg, UID el) - { - ForEachIDInSelectionGroup(mg, el, [&mg](UID other) - { - mg.select(other); - }); - } - - // returns the ID of the thing the station should attach to when trying to - // attach to something in the scene - UID GetStationAttachmentParent(ModelGraph const& mg, SceneEl const& el) - { - return std::visit(Overload - { - [](GroundEl const&) { return ModelGraphIDs::Ground(); }, - [&mg](MeshEl const& meshEl) { return mg.containsEl(meshEl.getParentID()) ? meshEl.getParentID() : ModelGraphIDs::Ground(); }, - [](BodyEl const& bodyEl) { return bodyEl.getID(); }, - [](JointEl const&) { return ModelGraphIDs::Ground(); }, - [](StationEl const&) { return ModelGraphIDs::Ground(); }, - [](EdgeEl const&) { return ModelGraphIDs::Ground(); }, - }, el.toVariant()); - } - - // points an axis of a given element towards some other element in the model graph - void PointAxisTowards( - ModelGraph& mg, - UID id, - int axis, - UID other) - { - Vec3 const choicePos = GetPosition(mg, other); - Transform const sourceXform = Transform{.position = GetPosition(mg, id)}; - - mg.updElByID(id).setXform(mg, PointAxisTowards(sourceXform, axis, choicePos)); - } - - // returns recommended rim intensity for an element in the model graph - SceneDecorationFlags computeFlags( - ModelGraph const& mg, - UID id, - UID hoverID = ModelGraphIDs::Empty()) - { - if (id == ModelGraphIDs::Empty()) - { - return SceneDecorationFlags::None; - } - else if (mg.isSelected(id)) - { - return SceneDecorationFlags::IsSelected; - } - else if (id == hoverID) - { - return SceneDecorationFlags::IsHovered | SceneDecorationFlags::IsChildOfHovered; - } - else if (IsInSelectionGroupOf(mg, hoverID, id)) - { - return SceneDecorationFlags::IsChildOfHovered; - } - else - { - return SceneDecorationFlags::None; - } - } -} - // undoable action support // // functions that mutate the undoable datastructure and commit changes at the