From 32cdd2bf09f59426d38daf9ea56da6d48507d535 Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Sat, 18 May 2024 13:48:31 +0200 Subject: [PATCH 1/3] :angel:Script: new script 'example_ImGui_nodeHighlight.as' Showcases existing and new API to get node info. EXISTING: ``` * actor.getNodeCount() * actor.getWheelNodeCount() * actor.getNodePosition(int nodenum) ``` NEW: ``` * actor.isNodeWheelRim(int nodenum) * actor.isNodeWheelTire(int nodenum) ``` --- doc/angelscript/Script2Game/BeamClass.h | 18 +++- .../scripts/example_ImGui_nodeHighlight.as | 86 +++++++++++++++++++ source/main/physics/Actor.cpp | 24 ++++++ source/main/physics/Actor.h | 2 + .../scripting/bindings/ActorAngelscript.cpp | 2 + 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 resources/scripts/example_ImGui_nodeHighlight.as diff --git a/doc/angelscript/Script2Game/BeamClass.h b/doc/angelscript/Script2Game/BeamClass.h index a57889e497..034ee7534d 100644 --- a/doc/angelscript/Script2Game/BeamClass.h +++ b/doc/angelscript/Script2Game/BeamClass.h @@ -71,10 +71,24 @@ class BeamClass /** * Returns the position of the node - * @param the nuber of the node + * @param the number of the node (counts from 0, see `getNodeCount()`) * @return vector3 of the world position for the node */ - vector3 getNodePosition(int nodeNumber); + vector3 getNodePosition(int nodeNumber); + + /** + * Is node marked as wheel rim? Note some wheel models use only tire nodes. See https://docs.rigsofrods.org/vehicle-creation/fileformat-truck/#wheels + * @param the number of the node (counts from 0, see `getNodeCount()`) + * @return True if the node is a wheel rim. + */ + vector3 isNodeWheelRim(int nodeNumber); + + /** + * Is node marked as wheel tire? Note some wheel models use only tire nodes. See https://docs.rigsofrods.org/vehicle-creation/fileformat-truck/#wheels + * @param the number of the node (counts from 0, see `getNodeCount()`) + * @return True if this is a tire node. + */ + vector3 isNodeWheelTire(int nodeNumber); /** * Gets the total amount of nodes of the wheels of the truck. diff --git a/resources/scripts/example_ImGui_nodeHighlight.as b/resources/scripts/example_ImGui_nodeHighlight.as new file mode 100644 index 0000000000..4904548fcf --- /dev/null +++ b/resources/scripts/example_ImGui_nodeHighlight.as @@ -0,0 +1,86 @@ +// NODE HIGHLIGHT DEMO +// =================================================== + +//#region GAME CALLBACKS: + +// `frameStep()` runs every frame; `dt` is delta time in seconds. +void frameStep(float dt) +{ + ImGui::TextDisabled("::: NODE HIGHLIGHT DEMO:::"); + ImGui::Separator(); + BeamClass@ actor = game.getCurrentTruck(); + if (@actor == null) + { + ImGui::Text("you are on foot"); + } + else + { + ImGui::Text("Driving '"+actor.getTruckName()+"' with total "+actor.getNodeCount()+" nodes ("+actor.getWheelNodeCount()+" wheel nodes)"); + //DOC: bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power) + ImGui::SliderFloat("Node radius", cfgNodeRadius, 0.5f, 10.f); + ImGui::SliderInt("Node num segments", cfgNodeNumSegments, 3, 20); + //DOC: bool ColorEdit3(const string&in, color&inout) + ImGui::ColorEdit3("Node color", cfgNodeColor); + ImGui::Checkbox("Show wheel nodes", cfgShowWheelNodes); + if (cfgShowWheelNodes) + { + ImGui::ColorEdit3("Rim node color", cfgRimNodeColor); + ImGui::ColorEdit3("Tire node color", cfgTireNodeColor); + } + drawNodeHighlights(actor); + } +} + +//#endregion + +color cfgNodeColor = color(0.9, 0.8, 0.5, 1); +color cfgRimNodeColor = color(0.4, 0.8, 0.5, 1); +color cfgTireNodeColor = color(0.5, 0.8, 0.8, 1); +float cfgNodeRadius = 3.f; +int cfgNodeNumSegments = 5; +bool cfgShowWheelNodes = true; + +// #region CUSTOM FUNCTIONS: + +void drawNodeHighlights(BeamClass@ actor) +{ + + ImDrawList@ drawlist = getDummyFullscreenWindow("nodeHighlights"); + for (int i = 0; i < actor.getNodeCount(); i++) + { + vector3 posWorld = actor.getNodePosition(i); + color col = cfgNodeColor; + bool wheelNode = false; + if (actor.isNodeWheelRim(i)) {col=cfgRimNodeColor; wheelNode=true;} + if (actor.isNodeWheelTire(i)) {col=cfgTireNodeColor; wheelNode=true;} + if (!wheelNode || cfgShowWheelNodes) + { + vector2 pos = vector2(0,0); + bool inFrontOfCamera = game.getScreenPosFromWorldPos(posWorld, /*out:*/ pos); + if (inFrontOfCamera) + { + //DOC: void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) + drawlist.AddCircleFilled(vector2(pos.x, pos.y), cfgNodeRadius, col, cfgNodeNumSegments); + } + } + } +} + +// shamelessly copypasted from 'road_editor.as', line 787 +ImDrawList@ getDummyFullscreenWindow(const string&in name) +{ + // Dummy fullscreen window to draw to + int window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar| ImGuiWindowFlags_NoInputs + | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus; + ImGui::SetNextWindowPos(vector2(0,0)); + ImGui::SetNextWindowSize(game.getDisplaySize()); + ImGui::PushStyleColor(/*ImGuiCol_WindowBg*/2, color(0.f,0.f,0.f,0.f)); // Fully transparent background! + ImGui::Begin(name, /*open:*/true, window_flags); + ImDrawList@ drawlist = ImGui::GetWindowDrawList(); + ImGui::End(); + ImGui::PopStyleColor(1); // WindowBg + + return drawlist; +} + +// #endregion diff --git a/source/main/physics/Actor.cpp b/source/main/physics/Actor.cpp index d57c20ad6e..b48f6f3923 100644 --- a/source/main/physics/Actor.cpp +++ b/source/main/physics/Actor.cpp @@ -4509,6 +4509,30 @@ Vector3 Actor::getNodePosition(int nodeNumber) } } +bool Actor::isNodeWheelRim(int nodeNumber) +{ + if (nodeNumber >= 0 && nodeNumber < ar_num_nodes) + { + return ar_nodes[nodeNumber].nd_rim_node; + } + else + { + return false; + } +} + +bool Actor::isNodeWheelTire(int nodeNumber) +{ + if (nodeNumber >= 0 && nodeNumber < ar_num_nodes) + { + return ar_nodes[nodeNumber].nd_tyre_node; + } + else + { + return false; + } +} + void Actor::WriteDiagnosticDump(std::string const& fileName) { // Purpose: to diff against output from https://github.com/only-a-ptr/rigs-of-rods/tree/retro-0407 diff --git a/source/main/physics/Actor.h b/source/main/physics/Actor.h index 7d24883de9..a02dcedebd 100644 --- a/source/main/physics/Actor.h +++ b/source/main/physics/Actor.h @@ -85,6 +85,8 @@ class Actor : public RefCountingObject float getTotalMass(bool withLocked=true); int getNodeCount() { return ar_num_nodes; } Ogre::Vector3 getNodePosition(int nodeNumber); //!< Returns world position of node + bool isNodeWheelRim(int nodeNumber); //!< Is node marked as wheel rim? Note some wheel models use only tire nodes. See https://docs.rigsofrods.org/vehicle-creation/fileformat-truck/#wheels + bool isNodeWheelTire(int nodeNumber); //!< Is node marked as wheel tire? Note some wheel models use only tire nodes. See https://docs.rigsofrods.org/vehicle-creation/fileformat-truck/#wheels int getWheelNodeCount() const; float getWheelSpeed() const { return ar_wheel_speed; } void reset(bool keep_position = false); //!< call this one to reset a truck from any context diff --git a/source/main/scripting/bindings/ActorAngelscript.cpp b/source/main/scripting/bindings/ActorAngelscript.cpp index a7d7bee67a..a45fa4764a 100644 --- a/source/main/scripting/bindings/ActorAngelscript.cpp +++ b/source/main/scripting/bindings/ActorAngelscript.cpp @@ -95,6 +95,8 @@ void RoR::RegisterActor(asIScriptEngine *engine) result = engine->RegisterObjectMethod("BeamClass", "float getTotalMass(bool)", asMETHOD(Actor,getTotalMass), asCALL_THISCALL); ROR_ASSERT(result>=0); result = engine->RegisterObjectMethod("BeamClass", "int getNodeCount()", asMETHOD(Actor,getNodeCount), asCALL_THISCALL); ROR_ASSERT(result>=0); result = engine->RegisterObjectMethod("BeamClass", "vector3 getNodePosition(int)", asMETHOD(Actor,getNodePosition), asCALL_THISCALL); ROR_ASSERT(result>=0); + result = engine->RegisterObjectMethod("BeamClass", "bool isNodeWheelRim(int)", asMETHOD(Actor,isNodeWheelRim), asCALL_THISCALL); ROR_ASSERT(result>=0); + result = engine->RegisterObjectMethod("BeamClass", "bool isNodeWheelTire(int)", asMETHOD(Actor,isNodeWheelTire), asCALL_THISCALL); ROR_ASSERT(result>=0); result = engine->RegisterObjectMethod("BeamClass", "int getWheelNodeCount()", asMETHOD(Actor,getWheelNodeCount), asCALL_THISCALL); ROR_ASSERT(result>=0); result = engine->RegisterObjectMethod("BeamClass", "float getWheelSpeed()", asMETHOD(Actor,getWheelSpeed), asCALL_THISCALL); ROR_ASSERT(result>=0); result = engine->RegisterObjectMethod("BeamClass", "void reset(bool)", asMETHOD(Actor,reset), asCALL_THISCALL); ROR_ASSERT(result>=0); From 438b87d44b690102035c0136533daa06abd3d99f Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Thu, 30 May 2024 13:48:50 +0200 Subject: [PATCH 2/3] Doc: added missing script func `game.getMouseScreenPosition()` --- doc/angelscript/Script2Game/GameScriptClass.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/angelscript/Script2Game/GameScriptClass.h b/doc/angelscript/Script2Game/GameScriptClass.h index f837bdd89d..dcd6436b8d 100644 --- a/doc/angelscript/Script2Game/GameScriptClass.h +++ b/doc/angelscript/Script2Game/GameScriptClass.h @@ -235,6 +235,11 @@ class GameScriptClass * Gets screen size in pixels. */ vector2 getDisplaySize(); + + /** + * Gets mouse position in pixels. + */ + Ogre::Vector2 getMouseScreenPosition(); /// @} From 082b63dcb73630171e2658044426447c195222d7 Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Thu, 30 May 2024 13:49:25 +0200 Subject: [PATCH 3/3] :scroll: example_ImGui_nodeHighlight.as: added node selection. --- .../scripts/example_ImGui_nodeHighlight.as | 216 ++++++++++++++++-- 1 file changed, 201 insertions(+), 15 deletions(-) diff --git a/resources/scripts/example_ImGui_nodeHighlight.as b/resources/scripts/example_ImGui_nodeHighlight.as index 4904548fcf..a93d283485 100644 --- a/resources/scripts/example_ImGui_nodeHighlight.as +++ b/resources/scripts/example_ImGui_nodeHighlight.as @@ -1,8 +1,22 @@ -// NODE HIGHLIGHT DEMO -// =================================================== +/// \title NODE HIGHLIGHT DEMO +/// \brief How to draw nodes and detect/highligt mouse interaction +/// +/// File structure (#region-s) +/// * GAME CALLBACKS - funcs invoked by game +/// * CONFIG - vars and UI funcs +/// * NODE INTERACTION - vars and drawing/UI funcs +/// * HELPERS - funcs +/// =================================================== //#region GAME CALLBACKS: +// `main()` runs once when script is loaded. +void main() +{ + game.registerForEvent(SE_TRUCK_ENTER); + prepareNodeSelectionForNewActor(game.getCurrentTruck()); +} + // `frameStep()` runs every frame; `dt` is delta time in seconds. void frameStep(float dt) { @@ -16,35 +30,144 @@ void frameStep(float dt) else { ImGui::Text("Driving '"+actor.getTruckName()+"' with total "+actor.getNodeCount()+" nodes ("+actor.getWheelNodeCount()+" wheel nodes)"); - //DOC: bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power) - ImGui::SliderFloat("Node radius", cfgNodeRadius, 0.5f, 10.f); - ImGui::SliderInt("Node num segments", cfgNodeNumSegments, 3, 20); - //DOC: bool ColorEdit3(const string&in, color&inout) - ImGui::ColorEdit3("Node color", cfgNodeColor); - ImGui::Checkbox("Show wheel nodes", cfgShowWheelNodes); - if (cfgShowWheelNodes) + + drawNodeHighlights(actor); + + // Left mouse button click = flip the selection state of the + if (mouseHoveredNodeID != -1 && ImGui::IsMouseClicked(0)) { - ImGui::ColorEdit3("Rim node color", cfgRimNodeColor); - ImGui::ColorEdit3("Tire node color", cfgTireNodeColor); + nodeSelectedStates[mouseHoveredNodeID] = !nodeSelectedStates[mouseHoveredNodeID]; } - drawNodeHighlights(actor); + + + drawNodeSelectionUI(); + + if (ImGui::CollapsingHeader("Config")) + { + drawConfigUI(); + } + } +} + +// Handles events registered using `game.registerForEvent()` +void eventCallback(scriptEvents ev, int arg1, int arg2ex, int arg3ex, int arg4ex, string arg5ex, string arg6ex, string arg7ex, string arg8ex) +{ + if (ev == SE_TRUCK_ENTER) + { + prepareNodeSelectionForNewActor(game.getTruckByNum(arg1)); } } //#endregion +// #region CONFIG + color cfgNodeColor = color(0.9, 0.8, 0.5, 1); color cfgRimNodeColor = color(0.4, 0.8, 0.5, 1); color cfgTireNodeColor = color(0.5, 0.8, 0.8, 1); float cfgNodeRadius = 3.f; int cfgNodeNumSegments = 5; bool cfgShowWheelNodes = true; +int cfgNodeHoverMaxCursorDist = 10; // Only confirm the hovered state if the cursor is less than 10 pixels away from it in any direction. +color cfgNodeHoverColor = color(1,0,0,1); +float cfgNodeHoverRadius = 5.f; +float cfgNodeHoverThickness = 2.f; +color cfgSelectedNodeColor = color(0.8f,1.f,0.1f,1.f); +float cfgSelectedNodeSize = 7.f; // drawn as rectangle + +void drawConfigUI() +{ + + ImGui::InputFloat("Node radius", cfgNodeRadius); + ImGui::InputInt("Node num segments", cfgNodeNumSegments); + //DOC: bool ColorEdit3(const string&in, color&inout) + ImGui::ColorEdit3("Node color", cfgNodeColor); + ImGui::Checkbox("Show wheel nodes", cfgShowWheelNodes); + if (cfgShowWheelNodes) + { + ImGui::ColorEdit3("Rim node color", cfgRimNodeColor); + ImGui::ColorEdit3("Tire node color", cfgTireNodeColor); + } + + ImGui::ColorEdit3("Hover color", cfgNodeHoverColor); + ImGui::InputFloat("Hover radius", cfgNodeHoverRadius); + ImGui::InputFloat("Hover thickness", cfgNodeHoverThickness); + ImGui::InputInt("Hover max cursor dist.", cfgNodeHoverMaxCursorDist); + + ImGui::ColorEdit3("Selected node color", cfgSelectedNodeColor); + ImGui::InputFloat("Selected node size", cfgSelectedNodeSize); +} + +// #endregion + +// #region NODE INTERACTION: + +int mouseHoveredNodeID = -1; +array nodeSelectedStates = array(); // 1:1 with nodes, resized on `SE_TRUCK_ENTER` event + +void prepareNodeSelectionForNewActor(BeamClass@ actor) +{ + // should be invoked for each SE_TRUCK_ENTER event, but also in `main()`, in case this script starts when already in vehicle! + // ------------------- + + nodeSelectedStates.resize(0); // purge the selection array + mouseHoveredNodeID = -1; + + if (@actor != null) + { + + nodeSelectedStates.resize(actor.getNodeCount()); // scale the selection array, with all nodes deselected (false) + } +} -// #region CUSTOM FUNCTIONS: +void drawNodeSelectionUI() +{ + int totalSelected = 0; + string strSelected = ""; + string strJoiner = ""; + for (uint i=0; i < nodeSelectedStates.length(); i++) + { + if (nodeSelectedStates[i]) + { + strSelected += (strJoiner + i); + strJoiner = ","; + totalSelected++; + } + } + ImGui::TextDisabled("Selected nodes (total: " + totalSelected + ")"); + ImGui::Text(strSelected); + if (ImGui::Button("Select all")) + { + for (uint i=0; i < nodeSelectedStates.length(); i++) + { + nodeSelectedStates[i] = true; + } + } + ImGui::SameLine(); + if (ImGui::Button("UnSelect all")) + { + for (uint i=0; i < nodeSelectedStates.length(); i++) + { + nodeSelectedStates[i] = false; + } + } + ImGui::SameLine(); + if (ImGui::Button("Flip selection")) + { + for (uint i=0; i < nodeSelectedStates.length(); i++) + { + nodeSelectedStates[i] = !nodeSelectedStates[i]; + } + } +} void drawNodeHighlights(BeamClass@ actor) { + int mouseClosestNodeID = -1; + + int mouseClosestNodeDist = 999999; + ImDrawList@ drawlist = getDummyFullscreenWindow("nodeHighlights"); for (int i = 0; i < actor.getNodeCount(); i++) { @@ -59,13 +182,53 @@ void drawNodeHighlights(BeamClass@ actor) bool inFrontOfCamera = game.getScreenPosFromWorldPos(posWorld, /*out:*/ pos); if (inFrontOfCamera) { - //DOC: void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) - drawlist.AddCircleFilled(vector2(pos.x, pos.y), cfgNodeRadius, col, cfgNodeNumSegments); + // Draw the node + if (nodeSelectedStates[i]) + { + // DOC: void AddRectFilled(const vector2&in p_min, const vector2&in p_max, const color&in col, float rounding = 0.0f, int rounding_corners = 15) + vector2 halfSize(cfgSelectedNodeSize/2.f, cfgSelectedNodeSize/2.f); + drawlist.AddRectFilled(pos-halfSize, pos+halfSize, cfgSelectedNodeColor); + } + else + { + //DOC: void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) + drawlist.AddCircleFilled(pos, cfgNodeRadius, col, cfgNodeNumSegments); + } + + // Accumulate mouse hover info + int nodeDist = getMouseShortestDistance(game.getMouseScreenPosition(), pos); + if (nodeDist < mouseClosestNodeDist) + { + mouseClosestNodeID = i; + mouseClosestNodeDist = nodeDist; + } + + // Draw mouse hover marker + if (i == mouseHoveredNodeID) + { + // DOC: void AddCircle(const vector2&in center, float radius, const color&in col, int num_segments = 12, float thickness = 1.f) + drawlist.AddCircle(pos, cfgNodeHoverRadius, cfgNodeHoverColor, cfgNodeNumSegments, cfgNodeHoverThickness); + } } } } + + // Finalize hover detection + ImGui::Text(" Mouse closest node: " + mouseClosestNodeID + " (dist:" + mouseClosestNodeDist + ")"); + if (mouseClosestNodeDist <= cfgNodeHoverMaxCursorDist) + { + mouseHoveredNodeID = mouseClosestNodeID; + } + else + { + mouseHoveredNodeID = -1; + } } +// #endregion + +// #region HELPERS + // shamelessly copypasted from 'road_editor.as', line 787 ImDrawList@ getDummyFullscreenWindow(const string&in name) { @@ -83,4 +246,27 @@ ImDrawList@ getDummyFullscreenWindow(const string&in name) return drawlist; } +// shamelessly copypasted from 'road_editor.as', line 803 +int getMouseShortestDistance(vector2 mouse, vector2 target) +{ + int dx = iabs(int(mouse.x) - int(target.x)); + int dy = iabs(int(mouse.y) - int(target.y)); + return imax(dx, dy); +} + +int imin(int a, int b) +{ + return (a < b) ? a : b; +} + +int imax(int a, int b) +{ + return (a > b) ? a : b; +} + +int iabs(int a) +{ + return (a < 0) ? -a : a; +} + // #endregion