Merge pull request #168 from orange-cpp/feature/hud_declarative

Feature/hud declarative
This commit is contained in:
2026-03-15 20:02:32 +03:00
committed by GitHub
5 changed files with 584 additions and 299 deletions

View File

@@ -68,71 +68,81 @@ namespace imgui_desktop::gui
if (ImGui::CollapsingHeader("Entity", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::SliderFloat("X##ent", &m_entity_x, 100.f, vp->Size.x - 100.f);
ImGui::SliderFloat("Top Y", &m_entity_top_y, 20.f, m_entity_bottom_y - 20.f);
ImGui::SliderFloat("Bottom Y", &m_entity_bottom_y, m_entity_top_y + 20.f, vp->Size.y - 20.f);
ImGui::SliderFloat("X##ent", &m_entity_x, 100.f, vp->Size.x - 100.f);
ImGui::SliderFloat("Top Y", &m_entity_top_y, 20.f, m_entity_bottom_y - 20.f);
ImGui::SliderFloat("Bottom Y", &m_entity_bottom_y, m_entity_top_y + 20.f, vp->Size.y - 20.f);
}
if (ImGui::CollapsingHeader("Box", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Box", &m_show_box); ImGui::SameLine();
ImGui::Checkbox("Cornered", &m_show_cornered_box); ImGui::SameLine();
ImGui::Checkbox("Dashed", &m_show_dashed_box);
ImGui::Checkbox("Box##chk", &m_show_box);
ImGui::SameLine();
ImGui::Checkbox("Cornered", &m_show_cornered_box);
ImGui::SameLine();
ImGui::Checkbox("Dashed", &m_show_dashed_box);
ImGui::ColorEdit4("Color##box", reinterpret_cast<float*>(&m_box_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Fill##box", reinterpret_cast<float*>(&m_box_fill), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thickness", &m_box_thickness, 0.5f, 5.f);
ImGui::SliderFloat("Corner ratio", &m_corner_ratio, 0.05f, 0.5f);
ImGui::ColorEdit4("Fill##box", reinterpret_cast<float*>(&m_box_fill), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thickness", &m_box_thickness, 0.5f, 5.f);
ImGui::SliderFloat("Corner ratio", &m_corner_ratio, 0.05f, 0.5f);
ImGui::Separator();
ImGui::ColorEdit4("Dash color", reinterpret_cast<float*>(&m_dash_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Dash length", &m_dash_len, 2.f, 30.f);
ImGui::SliderFloat("Dash gap", &m_dash_gap, 1.f, 20.f);
ImGui::SliderFloat("Dash thick", &m_dash_thickness, 0.5f, 5.f);
ImGui::ColorEdit4("Dash color", reinterpret_cast<float*>(&m_dash_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Dash length", &m_dash_len, 2.f, 30.f);
ImGui::SliderFloat("Dash gap", &m_dash_gap, 1.f, 20.f);
ImGui::SliderFloat("Dash thick", &m_dash_thickness, 0.5f, 5.f);
}
if (ImGui::CollapsingHeader("Bars", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::ColorEdit4("Color##bar", reinterpret_cast<float*>(&m_bar_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("BG##bar", reinterpret_cast<float*>(&m_bar_bg_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Outline##bar", reinterpret_cast<float*>(&m_bar_outline_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Width##bar", &m_bar_width, 1.f, 20.f);
ImGui::SliderFloat("Value##bar", &m_bar_value, 0.f, 1.f);
ImGui::ColorEdit4("Color##bar", reinterpret_cast<float*>(&m_bar_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("BG##bar", reinterpret_cast<float*>(&m_bar_bg_color), ImGuiColorEditFlags_NoInputs);
ImGui::ColorEdit4("Outline##bar", reinterpret_cast<float*>(&m_bar_outline_color),
ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Width##bar", &m_bar_width, 1.f, 20.f);
ImGui::SliderFloat("Value##bar", &m_bar_value, 0.f, 1.f);
ImGui::SliderFloat("Offset##bar", &m_bar_offset, 1.f, 20.f);
ImGui::Checkbox("Right##bar", &m_show_right_bar); ImGui::SameLine();
ImGui::Checkbox("Left##bar", &m_show_left_bar);
ImGui::Checkbox("Top##bar", &m_show_top_bar); ImGui::SameLine();
ImGui::Checkbox("Bottom##bar", &m_show_bottom_bar);
ImGui::Checkbox("Right dashed##bar", &m_show_right_dashed_bar); ImGui::SameLine();
ImGui::Checkbox("Left dashed##bar", &m_show_left_dashed_bar);
ImGui::Checkbox("Top dashed##bar", &m_show_top_dashed_bar); ImGui::SameLine();
ImGui::Checkbox("Bot dashed##bar", &m_show_bottom_dashed_bar);
ImGui::SliderFloat("Dash len##bar", &m_bar_dash_len, 2.f, 20.f);
ImGui::SliderFloat("Dash gap##bar", &m_bar_dash_gap, 1.f, 15.f);
ImGui::Checkbox("Right##bar", &m_show_right_bar);
ImGui::SameLine();
ImGui::Checkbox("Left##bar", &m_show_left_bar);
ImGui::Checkbox("Top##bar", &m_show_top_bar);
ImGui::SameLine();
ImGui::Checkbox("Bottom##bar", &m_show_bottom_bar);
ImGui::Checkbox("Right dashed##bar", &m_show_right_dashed_bar);
ImGui::SameLine();
ImGui::Checkbox("Left dashed##bar", &m_show_left_dashed_bar);
ImGui::Checkbox("Top dashed##bar", &m_show_top_dashed_bar);
ImGui::SameLine();
ImGui::Checkbox("Bot dashed##bar", &m_show_bottom_dashed_bar);
ImGui::SliderFloat("Dash len##bar", &m_bar_dash_len, 2.f, 20.f);
ImGui::SliderFloat("Dash gap##bar", &m_bar_dash_gap, 1.f, 15.f);
}
if (ImGui::CollapsingHeader("Labels", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Checkbox("Outlined", &m_outlined);
ImGui::SliderFloat("Offset##lbl", &m_label_offset, 0.f, 15.f);
ImGui::Checkbox("Right##lbl", &m_show_right_labels); ImGui::SameLine();
ImGui::Checkbox("Left##lbl", &m_show_left_labels);
ImGui::Checkbox("Top##lbl", &m_show_top_labels); ImGui::SameLine();
ImGui::Checkbox("Bottom##lbl", &m_show_bottom_labels);
ImGui::Checkbox("Ctr top##lbl", &m_show_centered_top); ImGui::SameLine();
ImGui::Checkbox("Ctr bot##lbl", &m_show_centered_bottom);
ImGui::Checkbox("Right##lbl", &m_show_right_labels);
ImGui::SameLine();
ImGui::Checkbox("Left##lbl", &m_show_left_labels);
ImGui::Checkbox("Top##lbl", &m_show_top_labels);
ImGui::SameLine();
ImGui::Checkbox("Bottom##lbl", &m_show_bottom_labels);
ImGui::Checkbox("Ctr top##lbl", &m_show_centered_top);
ImGui::SameLine();
ImGui::Checkbox("Ctr bot##lbl", &m_show_centered_bottom);
}
if (ImGui::CollapsingHeader("Skeleton"))
{
ImGui::Checkbox("Show##skel", &m_show_skeleton);
ImGui::ColorEdit4("Color##skel", reinterpret_cast<float*>(&m_skel_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thick##skel", &m_skel_thickness, 0.5f, 5.f);
ImGui::Checkbox("Show##skel", &m_show_skeleton);
ImGui::ColorEdit4("Color##skel", reinterpret_cast<float*>(&m_skel_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Thick##skel", &m_skel_thickness, 0.5f, 5.f);
}
if (ImGui::CollapsingHeader("Snap Line"))
{
ImGui::Checkbox("Show##snap", &m_show_snap);
ImGui::ColorEdit4("Color##snap", reinterpret_cast<float*>(&m_snap_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Width##snap", &m_snap_width, 0.5f, 5.f);
ImGui::Checkbox("Show##snap", &m_show_snap);
ImGui::ColorEdit4("Color##snap", reinterpret_cast<float*>(&m_snap_color), ImGuiColorEditFlags_NoInputs);
ImGui::SliderFloat("Width##snap", &m_snap_width, 0.5f, 5.f);
}
ImGui::PopItemWidth();
@@ -141,77 +151,64 @@ namespace imgui_desktop::gui
void MainWindow::draw_overlay()
{
using namespace omath::hud::widget;
using omath::hud::when;
const auto* vp = ImGui::GetMainViewport();
const Bar bar{m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_offset};
const DashedBar dbar{m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width,
m_bar_value, m_bar_dash_len, m_bar_dash_gap, m_bar_offset};
omath::hud::EntityOverlay ent(
{m_entity_x, m_entity_top_y}, {m_entity_x, m_entity_bottom_y},
std::make_shared<omath::hud::ImguiHudRenderer>());
omath::hud::EntityOverlay({m_entity_x, m_entity_top_y}, {m_entity_x, m_entity_bottom_y},
std::make_shared<omath::hud::ImguiHudRenderer>())
.contents(
// ── Boxes ────────────────────────────────────────────────────
when(m_show_box, Box{m_box_color, m_box_fill, m_box_thickness}),
when(m_show_cornered_box, CorneredBox{omath::Color::from_rgba(255, 0, 255, 255), m_box_fill,
m_corner_ratio, m_box_thickness}),
when(m_show_dashed_box, DashedBox{m_dash_color, m_dash_len, m_dash_gap, m_dash_thickness}),
draw_boxes(ent);
draw_bars(ent);
draw_labels(ent);
if (m_show_skeleton)
ent.add_skeleton(m_skel_color, m_skel_thickness);
if (m_show_snap)
ent.add_snap_line({vp->Size.x / 2.f, vp->Size.y}, m_snap_color, m_snap_width);
}
void MainWindow::draw_boxes(omath::hud::EntityOverlay& ent) const
{
if (m_show_box)
ent.add_2d_box(m_box_color, m_box_fill, m_box_thickness);
if (m_show_cornered_box)
ent.add_cornered_2d_box(omath::Color::from_rgba(255, 0, 255, 255), m_box_fill, m_corner_ratio, m_box_thickness);
if (m_show_dashed_box)
ent.add_dashed_box(m_dash_color, m_dash_len, m_dash_gap, m_dash_thickness);
}
void MainWindow::draw_bars(omath::hud::EntityOverlay& ent) const
{
if (m_show_right_bar)
ent.add_right_bar(m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_offset);
if (m_show_left_bar)
ent.add_left_bar(m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_offset);
if (m_show_top_bar)
ent.add_top_bar(m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_offset);
if (m_show_bottom_bar)
ent.add_bottom_bar(m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_offset);
if (m_show_right_dashed_bar)
ent.add_right_dashed_bar(m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_dash_len, m_bar_dash_gap, m_bar_offset);
if (m_show_left_dashed_bar)
ent.add_left_dashed_bar(m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_dash_len, m_bar_dash_gap, m_bar_offset);
if (m_show_top_dashed_bar)
ent.add_top_dashed_bar(m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_dash_len, m_bar_dash_gap, m_bar_offset);
if (m_show_bottom_dashed_bar)
ent.add_bottom_dashed_bar(m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_dash_len, m_bar_dash_gap, m_bar_offset);
}
void MainWindow::draw_labels(omath::hud::EntityOverlay& ent) const
{
if (m_show_right_labels)
{
ent.add_right_label({0.f, 1.f, 0.f, 1.f}, m_label_offset, m_outlined, "Health: {}/100", 100);
ent.add_right_label({1.f, 0.f, 0.f, 1.f}, m_label_offset, m_outlined, "Shield: {}/125", 125);
ent.add_right_label({1.f, 0.f, 1.f, 1.f}, m_label_offset, m_outlined, "*LOCKED*");
}
if (m_show_left_labels)
{
ent.add_left_label(omath::Color::from_rgba(255, 128, 0, 255), m_label_offset, m_outlined, "Armor: 75");
ent.add_left_label(omath::Color::from_rgba(0, 200, 255, 255), m_label_offset, m_outlined, "Level: 42");
}
if (m_show_top_labels)
{
ent.add_top_label(omath::Color::from_rgba(255, 255, 0, 255), m_label_offset, m_outlined, "*SCOPED*");
ent.add_top_label(omath::Color::from_rgba(255, 0, 0, 255), m_label_offset, m_outlined, "*BLEEDING*");
}
if (m_show_centered_top)
ent.add_centered_top_label(omath::Color::from_rgba(0, 255, 255, 255), m_label_offset, m_outlined, "*VISIBLE*");
if (m_show_centered_bottom)
ent.add_centered_bottom_label(omath::Color::from_rgba(255, 255, 255, 255), m_label_offset, m_outlined, "PlayerName");
if (m_show_bottom_labels)
ent.add_bottom_label(omath::Color::from_rgba(200, 200, 0, 255), m_label_offset, m_outlined, "42m");
RightSide
{
when(m_show_right_bar, bar),
when(m_show_right_dashed_bar, dbar),
when(m_show_right_labels,
Label{{0.f, 1.f, 0.f, 1.f}, m_label_offset, m_outlined, "Health: 100/100"}),
when(m_show_right_labels,
Label{{1.f, 0.f, 0.f, 1.f}, m_label_offset, m_outlined, "Shield: 125/125"}),
when(m_show_right_labels,
Label{{1.f, 0.f, 1.f, 1.f}, m_label_offset, m_outlined, "*LOCKED*"}),
},
LeftSide
{
when(m_show_left_bar, bar),
when(m_show_left_dashed_bar, dbar),
when(m_show_left_labels, Label{omath::Color::from_rgba(255, 128, 0, 255),
m_label_offset, m_outlined, "Armor: 75"}),
when(m_show_left_labels, Label{omath::Color::from_rgba(0, 200, 255, 255),
m_label_offset, m_outlined, "Level: 42"}),
},
TopSide
{
when(m_show_top_bar, bar),
when(m_show_top_dashed_bar, dbar),
when(m_show_centered_top, Centered{Label{omath::Color::from_rgba(0, 255, 255, 255),
m_label_offset, m_outlined, "*VISIBLE*"}}),
when(m_show_top_labels, Label{omath::Color::from_rgba(255, 255, 0, 255), m_label_offset,
m_outlined, "*SCOPED*"}),
when(m_show_top_labels, Label{omath::Color::from_rgba(255, 0, 0, 255), m_label_offset,
m_outlined, "*BLEEDING*"}),
},
BottomSide
{
when(m_show_bottom_bar, bar),
when(m_show_bottom_dashed_bar, dbar),
when(m_show_centered_bottom, Centered{Label{omath::Color::from_rgba(255, 255, 255, 255),
m_label_offset, m_outlined, "PlayerName"}}),
when(m_show_bottom_labels, Label{omath::Color::from_rgba(200, 200, 0, 255),
m_label_offset, m_outlined, "42m"}),
},
when(m_show_skeleton, Skeleton{m_skel_color, m_skel_thickness}),
when(m_show_snap, SnapLine{{vp->Size.x / 2.f, vp->Size.y}, m_snap_color, m_snap_width}));
}
void MainWindow::present()

View File

@@ -19,9 +19,6 @@ namespace imgui_desktop::gui
private:
void draw_controls();
void draw_overlay();
void draw_boxes(omath::hud::EntityOverlay& ent) const;
void draw_bars(omath::hud::EntityOverlay& ent) const;
void draw_labels(omath::hud::EntityOverlay& ent) const;
void present();
GLFWwindow* m_window = nullptr;
@@ -46,8 +43,8 @@ namespace imgui_desktop::gui
omath::Color m_bar_bg_color{0.f, 0.f, 0.f, 0.5f};
omath::Color m_bar_outline_color{0.f, 0.f, 0.f, 1.f};
float m_bar_width = 4.f, m_bar_value = 0.75f, m_bar_offset = 5.f;
bool m_show_right_bar = true, m_show_left_bar = true;
bool m_show_top_bar = true, m_show_bottom_bar = true;
bool m_show_right_bar = true, m_show_left_bar = true;
bool m_show_top_bar = true, m_show_bottom_bar = true;
bool m_show_right_dashed_bar = false, m_show_left_dashed_bar = false;
bool m_show_top_dashed_bar = false, m_show_bottom_dashed_bar = false;
float m_bar_dash_len = 6.f, m_bar_dash_gap = 4.f;

View File

@@ -3,127 +3,161 @@
//
#pragma once
#include "canvas_box.hpp"
#include "entity_overlay_widgets.hpp"
#include "hud_renderer_interface.hpp"
#include "omath/linear_algebra/vector2.hpp"
#include "omath/utility/color.hpp"
#include <memory>
#include <string_view>
namespace omath::hud
{
class EntityOverlay final
{
public:
EntityOverlay(const Vector2<float>& top, const Vector2<float>& bottom,
const std::shared_ptr<HudRendererInterface>& renderer);
const std::shared_ptr<HudRendererInterface>& renderer);
void add_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
float thickness = 1.f) const;
// ── Boxes ────────────────────────────────────────────────────────
EntityOverlay& add_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
float thickness = 1.f);
void add_cornered_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
float corner_ratio_len = 0.2f, float thickness = 1.f) const;
EntityOverlay& add_cornered_2d_box(const Color& box_color, const Color& fill_color = Color{0.f, 0.f, 0.f, 0.f},
float corner_ratio_len = 0.2f, float thickness = 1.f);
void add_right_bar(const Color& color, const Color& outline_color, const Color& bg_color, float width,
float ratio, float offset = 5.f);
EntityOverlay& add_dashed_box(const Color& color, float dash_len = 8.f, float gap_len = 5.f,
float thickness = 1.f);
void add_left_bar(const Color& color, const Color& outline_color, const Color& bg_color, float width,
float ratio, float offset = 5.f);
// ── Bars ─────────────────────────────────────────────────────────
EntityOverlay& add_right_bar(const Color& color, const Color& outline_color, const Color& bg_color, float width,
float ratio, float offset = 5.f);
EntityOverlay& add_left_bar(const Color& color, const Color& outline_color, const Color& bg_color, float width,
float ratio, float offset = 5.f);
EntityOverlay& add_top_bar(const Color& color, const Color& outline_color, const Color& bg_color, float height,
float ratio, float offset = 5.f);
EntityOverlay& add_bottom_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float offset = 5.f);
EntityOverlay& add_right_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float width, float ratio, float dash_len, float gap_len,
float offset = 5.f);
EntityOverlay& add_left_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float width, float ratio, float dash_len, float gap_len, float offset = 5.f);
EntityOverlay& add_top_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float dash_len, float gap_len, float offset = 5.f);
EntityOverlay& add_bottom_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float dash_len, float gap_len,
float offset = 5.f);
// ── Labels ───────────────────────────────────────────────────────
EntityOverlay& add_right_label(const Color& color, float offset, bool outlined, const std::string_view& text);
EntityOverlay& add_left_label(const Color& color, float offset, bool outlined, const std::string_view& text);
EntityOverlay& add_top_label(const Color& color, float offset, bool outlined, std::string_view text);
EntityOverlay& add_bottom_label(const Color& color, float offset, bool outlined, std::string_view text);
EntityOverlay& add_centered_top_label(const Color& color, float offset, bool outlined,
const std::string_view& text);
EntityOverlay& add_centered_bottom_label(const Color& color, float offset, bool outlined,
const std::string_view& text);
template<typename... Args>
void add_right_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
EntityOverlay& add_right_label(const Color& color, float offset, bool outlined, std::format_string<Args...> fmt,
Args&&... args)
{
const std::string label = std::vformat(fmt.get(), std::make_format_args(args...));
add_right_label(color, offset, outlined, std::string_view{label});
return add_right_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
void add_right_label(const Color& color, float offset, bool outlined, const std::string_view& text);
template<typename... Args>
void add_top_label(const Color& color, const float offset, const bool outlined, std::format_string<Args...> fmt,
Args&&... args)
EntityOverlay& add_left_label(const Color& color, float offset, bool outlined, std::format_string<Args...> fmt,
Args&&... args)
{
const std::string label = std::vformat(fmt.get(), std::make_format_args(args...));
add_top_label(color, offset, outlined, std::string_view{label});
return add_left_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
void add_top_label(const Color& color, float offset, bool outlined, std::string_view text);
void add_top_bar(const Color& color, const Color& outline_color, const Color& bg_color, float height,
float ratio, float offset = 5.f);
void add_snap_line(const Vector2<float>& start_pos, const Color& color, float width);
void add_dashed_box(const Color& color, float dash_len = 8.f, float gap_len = 5.f,
float thickness = 1.f) const;
void add_skeleton(const Color& color, float thickness = 1.f) const;
void add_right_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float width, float ratio, float dash_len, float gap_len, float offset = 5.f);
void add_left_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float width, float ratio, float dash_len, float gap_len, float offset = 5.f);
void add_top_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float dash_len, float gap_len, float offset = 5.f);
void add_bottom_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
float height, float ratio, float dash_len, float gap_len, float offset = 5.f);
void add_bottom_bar(const Color& color, const Color& outline_color, const Color& bg_color, float height,
float ratio, float offset = 5.f);
template<typename... Args>
void add_bottom_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
EntityOverlay& add_top_label(const Color& color, float offset, bool outlined, std::format_string<Args...> fmt,
Args&&... args)
{
const std::string label = std::vformat(fmt.get(), std::make_format_args(args...));
add_bottom_label(color, offset, outlined, std::string_view{label});
return add_top_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
void add_bottom_label(const Color& color, float offset, bool outlined, std::string_view text);
template<typename... Args>
void add_left_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
EntityOverlay& add_bottom_label(const Color& color, float offset, bool outlined,
std::format_string<Args...> fmt, Args&&... args)
{
const std::string label = std::vformat(fmt.get(), std::make_format_args(args...));
add_left_label(color, offset, outlined, std::string_view{label});
return add_bottom_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
void add_left_label(const Color& color, float offset, bool outlined, const std::string_view& text);
template<typename... Args>
void add_centered_bottom_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
EntityOverlay& add_centered_top_label(const Color& color, float offset, bool outlined,
std::format_string<Args...> fmt, Args&&... args)
{
const std::string label = std::vformat(fmt.get(), std::make_format_args(args...));
add_centered_bottom_label(color, offset, outlined, std::string_view{label});
return add_centered_top_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
void add_centered_bottom_label(const Color& color, float offset, bool outlined, const std::string_view& text);
template<typename... Args>
void add_centered_top_label(const Color& color, const float offset, const bool outlined,
std::format_string<Args...> fmt, Args&&... args)
EntityOverlay& add_centered_bottom_label(const Color& color, float offset, bool outlined,
std::format_string<Args...> fmt, Args&&... args)
{
const std::string label = std::vformat(fmt.get(), std::make_format_args(args...));
add_centered_top_label(color, offset, outlined, std::string_view{label});
return add_centered_bottom_label(color, offset, outlined,
std::string_view{std::vformat(fmt.get(), std::make_format_args(args...))});
}
void add_centered_top_label(const Color& color, float offset, bool outlined, const std::string_view& text);
// ── Misc ─────────────────────────────────────────────────────────
EntityOverlay& add_snap_line(const Vector2<float>& start_pos, const Color& color, float width);
EntityOverlay& add_skeleton(const Color& color, float thickness = 1.f);
// ── Declarative interface ─────────────────────────────────────────
/// Pass any combination of widget:: descriptor structs (and std::optional<W>
/// from when()) to render them all in declaration order.
template<typename... Widgets>
EntityOverlay& contents(Widgets&&... widgets)
{
(dispatch(std::forward<Widgets>(widgets)), ...);
return *this;
}
private:
void draw_outlined_text(const Vector2<float>& position, const Color& color,
const std::string_view& text);
void draw_dashed_line(const Vector2<float>& from, const Vector2<float>& to, const Color& color,
float dash_len, float gap_len, float thickness) const;
// optional<W> dispatch — enables when() conditional widgets
template<typename W>
void dispatch(const std::optional<W>& w)
{
if (w)
dispatch(*w);
}
void dispatch(const widget::Box& w);
void dispatch(const widget::CorneredBox& w);
void dispatch(const widget::DashedBox& w);
void dispatch(const widget::RightSide& w);
void dispatch(const widget::LeftSide& w);
void dispatch(const widget::TopSide& w);
void dispatch(const widget::BottomSide& w);
void dispatch(const widget::Skeleton& w);
void dispatch(const widget::SnapLine& w);
void draw_outlined_text(const Vector2<float>& position, const Color& color, const std::string_view& text);
void draw_dashed_line(const Vector2<float>& from, const Vector2<float>& to, const Color& color, float dash_len,
float gap_len, float thickness) const;
void draw_dashed_fill(const Vector2<float>& origin, const Vector2<float>& step_dir,
const Vector2<float>& perp_dir, float full_len, float filled_len,
const Color& fill_color, const Color& split_color,
float dash_len, float gap_len) const;
const Vector2<float>& perp_dir, float full_len, float filled_len, const Color& fill_color,
const Color& split_color, float dash_len, float gap_len) const;
CanvasBox m_canvas;
Vector2<float> m_text_cursor_right;
Vector2<float> m_text_cursor_top;
@@ -131,4 +165,4 @@ namespace omath::hud
Vector2<float> m_text_cursor_left;
std::shared_ptr<HudRendererInterface> m_renderer;
};
} // namespace omath::hud
} // namespace omath::hud

View File

@@ -0,0 +1,142 @@
//
// Created by orange on 15.03.2026.
//
#pragma once
#include "omath/linear_algebra/vector2.hpp"
#include "omath/utility/color.hpp"
#include <initializer_list>
#include <optional>
#include <string_view>
#include <variant>
namespace omath::hud::widget
{
// ── Overloaded helper for std::visit ──────────────────────────────────────
template<typename... Ts>
struct Overloaded : Ts...
{
using Ts::operator()...;
};
template<typename... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;
// ── Standalone widgets ────────────────────────────────────────────────────
struct Box
{
Color color;
Color fill{0.f, 0.f, 0.f, 0.f};
float thickness = 1.f;
};
struct CorneredBox
{
Color color;
Color fill{0.f, 0.f, 0.f, 0.f};
float corner_ratio = 0.2f;
float thickness = 1.f;
};
struct DashedBox
{
Color color;
float dash_len = 8.f;
float gap_len = 5.f;
float thickness = 1.f;
};
struct Skeleton
{
Color color;
float thickness = 1.f;
};
struct SnapLine
{
Vector2<float> start;
Color color;
float width;
};
// ── Side-agnostic widgets (used inside XxxSide containers) ────────────────
/// A filled bar. `size` is width for left/right sides, height for top/bottom.
struct Bar
{
Color color;
Color outline;
Color bg;
float size;
float ratio;
float offset = 5.f;
};
/// A dashed bar. Same field semantics as Bar plus dash parameters.
struct DashedBar
{
Color color;
Color outline;
Color bg;
float size;
float ratio;
float dash_len;
float gap_len;
float offset = 5.f;
};
struct Label
{
Color color;
float offset;
bool outlined;
std::string_view text;
};
/// Wraps a Label to request horizontal centering (only applied in TopSide / BottomSide).
template<typename W>
struct Centered
{
W child;
};
template<typename W>
Centered(W) -> Centered<W>;
// ── Side widget variant ───────────────────────────────────────────────────
struct None {}; ///< No-op placeholder — used by widget::when for disabled elements.
using SideWidget = std::variant<None, Bar, DashedBar, Label, Centered<Label>>;
// ── Side containers ───────────────────────────────────────────────────────
// Storing std::initializer_list<SideWidget> is safe here: the backing array
// is a const SideWidget[] on the caller's stack whose lifetime matches the
// temporary side-container object, which is consumed within the same
// full-expression by EntityOverlay::dispatch. No heap allocation occurs.
struct RightSide { std::initializer_list<SideWidget> children; RightSide(std::initializer_list<SideWidget> c) : children(c) {} };
struct LeftSide { std::initializer_list<SideWidget> children; LeftSide(std::initializer_list<SideWidget> c) : children(c) {} };
struct TopSide { std::initializer_list<SideWidget> children; TopSide(std::initializer_list<SideWidget> c) : children(c) {} };
struct BottomSide { std::initializer_list<SideWidget> children; BottomSide(std::initializer_list<SideWidget> c) : children(c) {} };
} // namespace omath::hud::widget
namespace omath::hud::widget
{
/// Inside XxxSide containers: returns the widget as a SideWidget when condition is true,
/// or None{} otherwise. Preferred over hud::when for types inside the SideWidget variant.
template<typename W>
requires std::constructible_from<SideWidget, W>
SideWidget when(const bool condition, W widget)
{
if (condition) return SideWidget{std::move(widget)};
return None{};
}
} // namespace omath::hud::widget
namespace omath::hud
{
/// Top-level: returns an engaged optional<W> when condition is true, std::nullopt otherwise.
/// Designed for use with EntityOverlay::contents() for top-level widget types.
template<typename W>
std::optional<W> when(const bool condition, W widget)
{
if (condition) return std::move(widget);
return std::nullopt;
}
} // namespace omath::hud

View File

@@ -5,7 +5,7 @@
namespace omath::hud
{
void EntityOverlay::add_2d_box(const Color& box_color, const Color& fill_color, const float thickness) const
EntityOverlay& EntityOverlay::add_2d_box(const Color& box_color, const Color& fill_color, const float thickness)
{
const auto points = m_canvas.as_array();
@@ -13,9 +13,11 @@ namespace omath::hud
if (fill_color.value().w > 0.f)
m_renderer->add_filled_polyline({points.data(), points.size()}, fill_color);
return *this;
}
void EntityOverlay::add_cornered_2d_box(const Color& box_color, const Color& fill_color,
const float corner_ratio_len, const float thickness) const
EntityOverlay& EntityOverlay::add_cornered_2d_box(const Color& box_color, const Color& fill_color,
const float corner_ratio_len, const float thickness)
{
const auto corner_line_length =
std::abs((m_canvas.top_left_corner - m_canvas.top_right_corner).x * corner_ratio_len);
@@ -50,9 +52,11 @@ namespace omath::hud
m_renderer->add_line(m_canvas.bottom_right_corner,
m_canvas.bottom_right_corner - Vector2<float>{corner_line_length, 0.f}, box_color,
thickness);
return *this;
}
void EntityOverlay::add_right_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float offset)
EntityOverlay& EntityOverlay::add_right_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_height = std::abs(m_canvas.top_right_corner.y - m_canvas.bottom_right_corner.y);
@@ -65,9 +69,11 @@ namespace omath::hud
bar_start + Vector2<float>(width, -max_bar_height), outline_color);
m_text_cursor_right.x += offset + width;
return *this;
}
void EntityOverlay::add_left_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float offset)
EntityOverlay& EntityOverlay::add_left_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_height = std::abs(m_canvas.top_left_corner.y - m_canvas.bottom_right_corner.y);
@@ -80,9 +86,11 @@ namespace omath::hud
bar_start + Vector2<float>(width, -max_bar_height), outline_color);
m_text_cursor_left.x -= offset + width;
return *this;
}
void EntityOverlay::add_right_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
EntityOverlay& EntityOverlay::add_right_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
if (outlined)
draw_outlined_text(m_text_cursor_right + Vector2<float>{offset, 0.f}, color, text);
@@ -90,9 +98,11 @@ namespace omath::hud
m_renderer->add_text(m_text_cursor_right + Vector2<float>{offset, 0.f}, color, text.data());
m_text_cursor_right.y += m_renderer->calc_text_size(text.data()).y;
return *this;
}
void EntityOverlay::add_top_label(const Color& color, const float offset, const bool outlined,
const std::string_view text)
EntityOverlay& EntityOverlay::add_top_label(const Color& color, const float offset, const bool outlined,
const std::string_view text)
{
m_text_cursor_top.y -= m_renderer->calc_text_size(text.data()).y;
@@ -100,9 +110,11 @@ namespace omath::hud
draw_outlined_text(m_text_cursor_top + Vector2<float>{0.f, -offset}, color, text);
else
m_renderer->add_text(m_text_cursor_top + Vector2<float>{0.f, -offset}, color, text.data());
return *this;
}
void EntityOverlay::add_top_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float offset)
EntityOverlay& EntityOverlay::add_top_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_width = std::abs(m_canvas.top_left_corner.x - m_canvas.bottom_right_corner.x);
@@ -114,36 +126,38 @@ namespace omath::hud
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, -height), outline_color);
m_text_cursor_top.y -= offset + height;
return *this;
}
void EntityOverlay::add_snap_line(const Vector2<float>& start_pos, const Color& color, const float width)
EntityOverlay& EntityOverlay::add_snap_line(const Vector2<float>& start_pos, const Color& color, const float width)
{
const Vector2<float> line_end =
m_canvas.bottom_left_corner
+ Vector2<float>{m_canvas.bottom_right_corner.x - m_canvas.bottom_left_corner.x, 0.f} / 2;
m_renderer->add_line(start_pos, line_end, color, width);
return *this;
}
void EntityOverlay::draw_dashed_fill(const Vector2<float>& origin, const Vector2<float>& step_dir,
const Vector2<float>& perp_dir, const float full_len,
const float filled_len, const Color& fill_color,
const Color& split_color, const float dash_len,
const Vector2<float>& perp_dir, const float full_len, const float filled_len,
const Color& fill_color, const Color& split_color, const float dash_len,
const float gap_len) const
{
if (full_len <= 0.f)
return;
const float step = dash_len + gap_len;
const float n = std::floor((full_len + gap_len) / step);
const float step = dash_len + gap_len;
const float n = std::floor((full_len + gap_len) / step);
if (n < 1.f)
return;
const float used = n * dash_len + (n - 1.f) * gap_len;
const float used = n * dash_len + (n - 1.f) * gap_len;
const float offset = (full_len - used) / 2.f;
const auto fill_rect = [&](const Vector2<float>& a, const Vector2<float>& b, const Color& c)
{
m_renderer->add_filled_rectangle(
{std::min(a.x, b.x), std::min(a.y, b.y)},
{std::max(a.x, b.x), std::max(a.y, b.y)}, c);
m_renderer->add_filled_rectangle({std::min(a.x, b.x), std::min(a.y, b.y)},
{std::max(a.x, b.x), std::max(a.y, b.y)}, c);
};
// Draw split lines (gaps) across the full bar first
@@ -154,9 +168,9 @@ namespace omath::hud
for (float i = 0.f; i < n; ++i)
{
const float dash_start = offset + i * step;
const float dash_end = dash_start + dash_len;
const float gap_start = dash_end;
const float gap_end = dash_start + step;
const float dash_end = dash_start + dash_len;
const float gap_start = dash_end;
const float gap_end = dash_start + step;
// Fill dash only up to filled_len
if (dash_start < filled_len)
@@ -181,106 +195,115 @@ namespace omath::hud
fill_rect(origin + step_dir * trail_start, origin + step_dir * full_len + perp_dir, split_color);
}
void EntityOverlay::add_right_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float dash_len,
const float gap_len, const float offset)
EntityOverlay& EntityOverlay::add_right_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float width, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float height = std::abs(m_canvas.top_right_corner.y - m_canvas.bottom_right_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_right.x + offset, m_canvas.bottom_right_corner.y};
const float height = std::abs(m_canvas.top_right_corner.y - m_canvas.bottom_right_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_right.x + offset, m_canvas.bottom_right_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{width, -height}, bg_color);
draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio,
color, outline_color, dash_len, gap_len);
m_renderer->add_rectangle(bar_start - Vector2<float>{1.f, 0.f},
bar_start + Vector2<float>{width, -height}, outline_color);
draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start - Vector2<float>{1.f, 0.f}, bar_start + Vector2<float>{width, -height},
outline_color);
m_text_cursor_right.x += offset + width;
return *this;
}
void EntityOverlay::add_left_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float width, float ratio, const float dash_len,
const float gap_len, const float offset)
EntityOverlay& EntityOverlay::add_left_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float width, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float height = std::abs(m_canvas.top_left_corner.y - m_canvas.bottom_left_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_left.x - (offset + width), m_canvas.bottom_left_corner.y};
const float height = std::abs(m_canvas.top_left_corner.y - m_canvas.bottom_left_corner.y);
const auto bar_start = Vector2<float>{m_text_cursor_left.x - (offset + width), m_canvas.bottom_left_corner.y};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{width, -height}, bg_color);
draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio,
color, outline_color, dash_len, gap_len);
m_renderer->add_rectangle(bar_start - Vector2<float>{1.f, 0.f},
bar_start + Vector2<float>{width, -height}, outline_color);
draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start - Vector2<float>{1.f, 0.f}, bar_start + Vector2<float>{width, -height},
outline_color);
m_text_cursor_left.x -= offset + width;
return *this;
}
void EntityOverlay::add_top_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float dash_len,
const float gap_len, const float offset)
EntityOverlay& EntityOverlay::add_top_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float height, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float bar_w = std::abs(m_canvas.top_left_corner.x - m_canvas.top_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.top_left_corner.x, m_text_cursor_top.y - offset};
const float bar_w = std::abs(m_canvas.top_left_corner.x - m_canvas.top_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.top_left_corner.x, m_text_cursor_top.y - offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{bar_w, -height}, bg_color);
draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, -height}, bar_w, bar_w * ratio,
color, outline_color, dash_len, gap_len);
draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, -height}, bar_w, bar_w * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>{bar_w, -height}, outline_color);
m_text_cursor_top.y -= offset + height;
return *this;
}
void EntityOverlay::add_bottom_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float dash_len,
const float gap_len, const float offset)
EntityOverlay& EntityOverlay::add_bottom_dashed_bar(const Color& color, const Color& outline_color,
const Color& bg_color, const float height, float ratio,
const float dash_len, const float gap_len, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const float bar_w = std::abs(m_canvas.bottom_left_corner.x - m_canvas.bottom_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.bottom_left_corner.x, m_text_cursor_bottom.y + offset};
const float bar_w = std::abs(m_canvas.bottom_left_corner.x - m_canvas.bottom_right_corner.x);
const auto bar_start = Vector2<float>{m_canvas.bottom_left_corner.x, m_text_cursor_bottom.y + offset};
m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2<float>{bar_w, height}, bg_color);
draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, height}, bar_w, bar_w * ratio,
color, outline_color, dash_len, gap_len);
draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, height}, bar_w, bar_w * ratio, color, outline_color, dash_len,
gap_len);
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>{bar_w, height}, outline_color);
m_text_cursor_bottom.y += offset + height;
return *this;
}
void EntityOverlay::add_skeleton(const Color& color, const float thickness) const
EntityOverlay& EntityOverlay::add_skeleton(const Color& color, const float thickness)
{
// Maps normalized (rx in [0,1], ry in [0,1]) to canvas screen position
const auto joint = [&](const float rx, const float ry) -> Vector2<float>
{
const auto top = m_canvas.top_left_corner
+ (m_canvas.top_right_corner - m_canvas.top_left_corner) * rx;
const auto bot = m_canvas.bottom_left_corner
+ (m_canvas.bottom_right_corner - m_canvas.bottom_left_corner) * rx;
const auto top = m_canvas.top_left_corner + (m_canvas.top_right_corner - m_canvas.top_left_corner) * rx;
const auto bot =
m_canvas.bottom_left_corner + (m_canvas.bottom_right_corner - m_canvas.bottom_left_corner) * rx;
return top + (bot - top) * ry;
};
using B = std::pair<std::pair<float,float>, std::pair<float,float>>;
using B = std::pair<std::pair<float, float>, std::pair<float, float>>;
static constexpr std::array<B, 15> k_bones{{
// Spine
{{0.50f, 0.13f}, {0.50f, 0.22f}}, // head → neck
{{0.50f, 0.22f}, {0.50f, 0.38f}}, // neck → chest
{{0.50f, 0.38f}, {0.50f, 0.55f}}, // chest → pelvis
// Left arm
{{0.50f, 0.22f}, {0.25f, 0.25f}}, // neck → L shoulder
{{0.25f, 0.25f}, {0.13f, 0.42f}}, // L shoulder → L elbow
{{0.13f, 0.42f}, {0.08f, 0.56f}}, // L elbow → L hand
// Right arm
{{0.50f, 0.22f}, {0.75f, 0.25f}}, // neck → R shoulder
{{0.75f, 0.25f}, {0.87f, 0.42f}}, // R shoulder → R elbow
{{0.87f, 0.42f}, {0.92f, 0.56f}}, // R elbow → R hand
// Left leg
{{0.50f, 0.55f}, {0.36f, 0.58f}}, // pelvis → L hip
{{0.36f, 0.58f}, {0.32f, 0.77f}}, // L hip → L knee
{{0.32f, 0.77f}, {0.27f, 0.97f}}, // L knee → L foot
// Right leg
{{0.50f, 0.55f}, {0.64f, 0.58f}}, // pelvis → R hip
{{0.64f, 0.58f}, {0.68f, 0.77f}}, // R hip → R knee
{{0.68f, 0.77f}, {0.73f, 0.97f}}, // R knee → R foot
// Spine
{{0.50f, 0.13f}, {0.50f, 0.22f}}, // head → neck
{{0.50f, 0.22f}, {0.50f, 0.38f}}, // neck → chest
{{0.50f, 0.38f}, {0.50f, 0.55f}}, // chest → pelvis
// Left arm
{{0.50f, 0.22f}, {0.25f, 0.25f}}, // neck → L shoulder
{{0.25f, 0.25f}, {0.13f, 0.42f}}, // L shoulder → L elbow
{{0.13f, 0.42f}, {0.08f, 0.56f}}, // L elbow → L hand
// Right arm
{{0.50f, 0.22f}, {0.75f, 0.25f}}, // neck → R shoulder
{{0.75f, 0.25f}, {0.87f, 0.42f}}, // R shoulder → R elbow
{{0.87f, 0.42f}, {0.92f, 0.56f}}, // R elbow → R hand
// Left leg
{{0.50f, 0.55f}, {0.36f, 0.58f}}, // pelvis → L hip
{{0.36f, 0.58f}, {0.32f, 0.77f}}, // L hip → L knee
{{0.32f, 0.77f}, {0.27f, 0.97f}}, // L knee → L foot
// Right leg
{{0.50f, 0.55f}, {0.64f, 0.58f}}, // pelvis → R hip
{{0.64f, 0.58f}, {0.68f, 0.77f}}, // R hip → R knee
{{0.68f, 0.77f}, {0.73f, 0.97f}}, // R knee → R foot
}};
for (const auto& [a, b] : k_bones)
m_renderer->add_line(joint(a.first, a.second), joint(b.first, b.second), color, thickness);
return *this;
}
void EntityOverlay::draw_dashed_line(const Vector2<float>& from, const Vector2<float>& to, const Color& color,
@@ -290,31 +313,30 @@ namespace omath::hud
if (total <= 0.f)
return;
const auto dir = (to - from).normalized();
const auto dir = (to - from).normalized();
const float step = dash_len + gap_len;
const float n_dashes = std::floor((total + gap_len) / step);
const float n_dashes = std::floor((total + gap_len) / step);
if (n_dashes < 1.f)
return;
const float used = n_dashes * dash_len + (n_dashes - 1.f) * gap_len;
const float used = n_dashes * dash_len + (n_dashes - 1.f) * gap_len;
const float offset = (total - used) / 2.f;
for (float i = 0.f; i < n_dashes; ++i)
{
const float pos = offset + i * step;
const float pos = offset + i * step;
const auto dash_start = from + dir * pos;
const auto dash_end = from + dir * std::min(pos + dash_len, total);
const auto dash_end = from + dir * std::min(pos + dash_len, total);
m_renderer->add_line(dash_start, dash_end, color, thickness);
}
}
void EntityOverlay::add_dashed_box(const Color& color, const float dash_len, const float gap_len,
const float thickness) const
EntityOverlay& EntityOverlay::add_dashed_box(const Color& color, const float dash_len, const float gap_len,
const float thickness)
{
const float min_edge = std::min(
(m_canvas.top_right_corner - m_canvas.top_left_corner).length(),
(m_canvas.bottom_right_corner - m_canvas.top_right_corner).length());
const float min_edge = std::min((m_canvas.top_right_corner - m_canvas.top_left_corner).length(),
(m_canvas.bottom_right_corner - m_canvas.top_right_corner).length());
const float corner_len = std::min(dash_len, min_edge / 2.f);
const auto draw_edge = [&](const Vector2<float>& from, const Vector2<float>& to)
@@ -326,10 +348,12 @@ namespace omath::hud
m_renderer->add_line(to - dir * corner_len, to, color, thickness);
};
draw_edge(m_canvas.top_left_corner, m_canvas.top_right_corner);
draw_edge(m_canvas.top_right_corner, m_canvas.bottom_right_corner);
draw_edge(m_canvas.bottom_right_corner,m_canvas.bottom_left_corner);
draw_edge(m_canvas.top_left_corner, m_canvas.top_right_corner);
draw_edge(m_canvas.top_right_corner, m_canvas.bottom_right_corner);
draw_edge(m_canvas.bottom_right_corner, m_canvas.bottom_left_corner);
draw_edge(m_canvas.bottom_left_corner, m_canvas.top_left_corner);
return *this;
}
void EntityOverlay::draw_outlined_text(const Vector2<float>& position, const Color& color,
@@ -343,8 +367,8 @@ namespace omath::hud
m_renderer->add_text(position + outline_offset, Color{0.f, 0.f, 0.f, 1.f}, text.data());
m_renderer->add_text(position, color, text.data());
}
void EntityOverlay::add_bottom_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float offset)
EntityOverlay& EntityOverlay::add_bottom_bar(const Color& color, const Color& outline_color, const Color& bg_color,
const float height, float ratio, const float offset)
{
ratio = std::clamp(ratio, 0.f, 1.f);
const auto max_bar_width = std::abs(m_canvas.bottom_right_corner.x - m_canvas.bottom_left_corner.x);
@@ -355,10 +379,12 @@ namespace omath::hud
m_renderer->add_rectangle(bar_start, bar_start + Vector2<float>(max_bar_width, height), outline_color);
m_text_cursor_bottom.y += offset + height;
return *this;
}
void EntityOverlay::add_bottom_label(const Color& color, const float offset, const bool outlined,
const std::string_view text)
EntityOverlay& EntityOverlay::add_bottom_label(const Color& color, const float offset, const bool outlined,
const std::string_view text)
{
const auto text_size = m_renderer->calc_text_size(text);
@@ -368,10 +394,12 @@ namespace omath::hud
m_renderer->add_text(m_text_cursor_bottom + Vector2<float>{0.f, offset}, color, text);
m_text_cursor_bottom.y += text_size.y;
return *this;
}
void EntityOverlay::add_left_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
EntityOverlay& EntityOverlay::add_left_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
const auto text_size = m_renderer->calc_text_size(text);
const auto pos = m_text_cursor_left + Vector2<float>{-(offset + text_size.x), 0.f};
@@ -382,10 +410,12 @@ namespace omath::hud
m_renderer->add_text(pos, color, text);
m_text_cursor_left.y += text_size.y;
return *this;
}
void EntityOverlay::add_centered_bottom_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
EntityOverlay& EntityOverlay::add_centered_bottom_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
const auto text_size = m_renderer->calc_text_size(text);
const auto box_center_x =
@@ -398,10 +428,12 @@ namespace omath::hud
m_renderer->add_text(pos, color, text);
m_text_cursor_bottom.y += text_size.y;
return *this;
}
void EntityOverlay::add_centered_top_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
EntityOverlay& EntityOverlay::add_centered_top_label(const Color& color, const float offset, const bool outlined,
const std::string_view& text)
{
const auto text_size = m_renderer->calc_text_size(text);
const auto box_center_x =
@@ -414,6 +446,8 @@ namespace omath::hud
draw_outlined_text(pos, color, text);
else
m_renderer->add_text(pos, color, text);
return *this;
}
EntityOverlay::EntityOverlay(const Vector2<float>& top, const Vector2<float>& bottom,
@@ -423,4 +457,85 @@ namespace omath::hud
m_text_cursor_left(m_canvas.top_left_corner), m_renderer(renderer)
{
}
// ── widget dispatch ───────────────────────────────────────────────────────
void EntityOverlay::dispatch(const widget::Box& w)
{ add_2d_box(w.color, w.fill, w.thickness); }
void EntityOverlay::dispatch(const widget::CorneredBox& w)
{ add_cornered_2d_box(w.color, w.fill, w.corner_ratio, w.thickness); }
void EntityOverlay::dispatch(const widget::DashedBox& w)
{ add_dashed_box(w.color, w.dash_len, w.gap_len, w.thickness); }
void EntityOverlay::dispatch(const widget::Skeleton& w)
{ add_skeleton(w.color, w.thickness); }
void EntityOverlay::dispatch(const widget::SnapLine& w)
{ add_snap_line(w.start, w.color, w.width); }
// ── Side container dispatch ───────────────────────────────────────────────
void EntityOverlay::dispatch(const widget::RightSide& s)
{
for (const auto& child : s.children)
std::visit(widget::Overloaded{
[](const widget::None&) {},
[this](const widget::Bar& w)
{ add_right_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset); },
[this](const widget::DashedBar& w)
{ add_right_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len, w.offset); },
[this](const widget::Label& w)
{ add_right_label(w.color, w.offset, w.outlined, w.text); },
[this](const widget::Centered<widget::Label>& w)
{ add_right_label(w.child.color, w.child.offset, w.child.outlined, w.child.text); },
}, child);
}
void EntityOverlay::dispatch(const widget::LeftSide& s)
{
for (const auto& child : s.children)
std::visit(widget::Overloaded{
[](const widget::None&) {},
[this](const widget::Bar& w)
{ add_left_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset); },
[this](const widget::DashedBar& w)
{ add_left_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len, w.offset); },
[this](const widget::Label& w)
{ add_left_label(w.color, w.offset, w.outlined, w.text); },
[this](const widget::Centered<widget::Label>& w)
{ add_left_label(w.child.color, w.child.offset, w.child.outlined, w.child.text); },
}, child);
}
void EntityOverlay::dispatch(const widget::TopSide& s)
{
for (const auto& child : s.children)
std::visit(widget::Overloaded{
[](const widget::None&) {},
[this](const widget::Bar& w)
{ add_top_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset); },
[this](const widget::DashedBar& w)
{ add_top_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len, w.offset); },
[this](const widget::Label& w)
{ add_top_label(w.color, w.offset, w.outlined, w.text); },
[this](const widget::Centered<widget::Label>& w)
{ add_centered_top_label(w.child.color, w.child.offset, w.child.outlined, w.child.text); },
}, child);
}
void EntityOverlay::dispatch(const widget::BottomSide& s)
{
for (const auto& child : s.children)
std::visit(widget::Overloaded{
[](const widget::None&) {},
[this](const widget::Bar& w)
{ add_bottom_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.offset); },
[this](const widget::DashedBar& w)
{ add_bottom_dashed_bar(w.color, w.outline, w.bg, w.size, w.ratio, w.dash_len, w.gap_len, w.offset); },
[this](const widget::Label& w)
{ add_bottom_label(w.color, w.offset, w.outlined, w.text); },
[this](const widget::Centered<widget::Label>& w)
{ add_centered_bottom_label(w.child.color, w.child.offset, w.child.outlined, w.child.text); },
}, child);
}
} // namespace omath::hud