mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-29 02:21:44 +00:00
[glass] Add glass: an application for display of robot data
This reuses many pieces of the current simulation GUI. The common pieces have been refactored into the libglass library. The libglass library is designed to be usable for other standalone data visualization applications (e.g. viewing data logs). The name "glass" comes from "glass cockpit", as the application features several multi-function displays that can be adjusted to display robot information as needed.
This commit is contained in:
161
glass/src/lib/native/cpp/other/DeviceTree.cpp
Normal file
161
glass/src/lib/native/cpp/other/DeviceTree.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/other/DeviceTree.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include "glass/Context.h"
|
||||
#include "glass/ContextInternal.h"
|
||||
#include "glass/DataSource.h"
|
||||
|
||||
using namespace glass;
|
||||
|
||||
void DeviceTreeModel::Update() {
|
||||
for (auto&& display : m_displays) {
|
||||
if (display.first) display.first->Update();
|
||||
}
|
||||
}
|
||||
|
||||
bool DeviceTreeModel::Exists() {
|
||||
for (auto&& display : m_displays) {
|
||||
if (display.first && display.first->Exists()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DeviceTreeModel::Display() {
|
||||
for (auto&& display : m_displays) {
|
||||
if (display.second) display.second(display.first);
|
||||
}
|
||||
}
|
||||
|
||||
void glass::HideDevice(const char* id) { gContext->deviceHidden[id] = true; }
|
||||
|
||||
bool glass::BeginDevice(const char* id, ImGuiTreeNodeFlags flags) {
|
||||
if (gContext->deviceHidden[id]) return false;
|
||||
|
||||
PushID(id);
|
||||
|
||||
// build label
|
||||
std::string* name = GetStorage().GetStringRef("name");
|
||||
char label[128];
|
||||
std::snprintf(label, sizeof(label), "%s###name",
|
||||
name->empty() ? id : name->c_str());
|
||||
|
||||
bool open = CollapsingHeader(label, flags);
|
||||
PopupEditName("name", name);
|
||||
|
||||
if (!open) PopID();
|
||||
return open;
|
||||
}
|
||||
|
||||
void glass::EndDevice() { PopID(); }
|
||||
|
||||
static bool DeviceBooleanImpl(const char* name, bool readonly, bool* value) {
|
||||
if (readonly) {
|
||||
ImGui::LabelText(name, "%s", *value ? "true" : "false");
|
||||
} else {
|
||||
static const char* boolOptions[] = {"false", "true"};
|
||||
int val = *value ? 1 : 0;
|
||||
if (ImGui::Combo(name, &val, boolOptions, 2)) {
|
||||
*value = val;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool DeviceDoubleImpl(const char* name, bool readonly, double* value) {
|
||||
if (readonly) {
|
||||
ImGui::LabelText(name, "%.6f", *value);
|
||||
return false;
|
||||
} else {
|
||||
return ImGui::InputDouble(name, value, 0, 0, "%.6f",
|
||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
}
|
||||
}
|
||||
|
||||
static bool DeviceEnumImpl(const char* name, bool readonly, int* value,
|
||||
const char** options, int32_t numOptions) {
|
||||
if (readonly) {
|
||||
if (*value < 0 || *value >= numOptions)
|
||||
ImGui::LabelText(name, "%d (unknown)", *value);
|
||||
else
|
||||
ImGui::LabelText(name, "%s", options[*value]);
|
||||
return false;
|
||||
} else {
|
||||
return ImGui::Combo(name, value, options, numOptions);
|
||||
}
|
||||
}
|
||||
|
||||
static bool DeviceIntImpl(const char* name, bool readonly, int32_t* value) {
|
||||
if (readonly) {
|
||||
ImGui::LabelText(name, "%" PRId32, *value);
|
||||
return false;
|
||||
} else {
|
||||
return ImGui::InputScalar(name, ImGuiDataType_S32, value, nullptr, nullptr,
|
||||
nullptr, ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
}
|
||||
}
|
||||
|
||||
static bool DeviceLongImpl(const char* name, bool readonly, int64_t* value) {
|
||||
if (readonly) {
|
||||
ImGui::LabelText(name, "%" PRId64, *value);
|
||||
return false;
|
||||
} else {
|
||||
return ImGui::InputScalar(name, ImGuiDataType_S64, value, nullptr, nullptr,
|
||||
nullptr, ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename F, typename... Args>
|
||||
static inline bool DeviceValueImpl(const char* name, bool readonly,
|
||||
const DataSource* source, F&& func,
|
||||
Args... args) {
|
||||
ImGui::SetNextItemWidth(ImGui::GetWindowWidth() * 0.5f);
|
||||
if (!source) {
|
||||
return func(name, readonly, args...);
|
||||
} else {
|
||||
ImGui::PushID(name);
|
||||
bool rv = func("", readonly, args...);
|
||||
ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
|
||||
ImGui::Selectable(name);
|
||||
source->EmitDrag();
|
||||
ImGui::PopID();
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
bool glass::DeviceBoolean(const char* name, bool readonly, bool* value,
|
||||
const DataSource* source) {
|
||||
return DeviceValueImpl(name, readonly, source, DeviceBooleanImpl, value);
|
||||
}
|
||||
|
||||
bool glass::DeviceDouble(const char* name, bool readonly, double* value,
|
||||
const DataSource* source) {
|
||||
return DeviceValueImpl(name, readonly, source, DeviceDoubleImpl, value);
|
||||
}
|
||||
|
||||
bool glass::DeviceEnum(const char* name, bool readonly, int* value,
|
||||
const char** options, int32_t numOptions,
|
||||
const DataSource* source) {
|
||||
return DeviceValueImpl(name, readonly, source, DeviceEnumImpl, value, options,
|
||||
numOptions);
|
||||
}
|
||||
|
||||
bool glass::DeviceInt(const char* name, bool readonly, int32_t* value,
|
||||
const DataSource* source) {
|
||||
return DeviceValueImpl(name, readonly, source, DeviceIntImpl, value);
|
||||
}
|
||||
|
||||
bool glass::DeviceLong(const char* name, bool readonly, int64_t* value,
|
||||
const DataSource* source) {
|
||||
return DeviceValueImpl(name, readonly, source, DeviceLongImpl, value);
|
||||
}
|
||||
140
glass/src/lib/native/cpp/other/FMS.cpp
Normal file
140
glass/src/lib/native/cpp/other/FMS.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/other/FMS.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <wpi/SmallString.h>
|
||||
|
||||
#include "glass/DataSource.h"
|
||||
|
||||
using namespace glass;
|
||||
|
||||
static const char* stations[] = {"Red 1", "Red 2", "Red 3",
|
||||
"Blue 1", "Blue 2", "Blue 3"};
|
||||
|
||||
void glass::DisplayFMS(FMSModel* model, bool* matchTimeEnabled) {
|
||||
if (!model->Exists() || model->IsReadOnly()) return DisplayFMSReadOnly(model);
|
||||
|
||||
// FMS Attached
|
||||
if (auto data = model->GetFmsAttachedData()) {
|
||||
bool val = data->GetValue();
|
||||
if (ImGui::Checkbox("FMS Attached", &val)) model->SetFmsAttached(val);
|
||||
data->EmitDrag();
|
||||
}
|
||||
|
||||
// DS Attached
|
||||
if (auto data = model->GetDsAttachedData()) {
|
||||
bool val = data->GetValue();
|
||||
if (ImGui::Checkbox("DS Attached", &val)) model->SetDsAttached(val);
|
||||
data->EmitDrag();
|
||||
}
|
||||
|
||||
// Alliance Station
|
||||
if (auto data = model->GetAllianceStationIdData()) {
|
||||
int val = data->GetValue();
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
|
||||
if (ImGui::Combo("Alliance Station", &val, stations, 6))
|
||||
model->SetAllianceStationId(val);
|
||||
data->EmitDrag();
|
||||
}
|
||||
|
||||
// Match Time
|
||||
if (auto data = model->GetMatchTimeData()) {
|
||||
if (matchTimeEnabled)
|
||||
ImGui::Checkbox("Match Time Enabled", matchTimeEnabled);
|
||||
|
||||
double val = data->GetValue();
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
|
||||
if (ImGui::InputDouble("Match Time", &val, 0, 0, "%.1f",
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
model->SetMatchTime(val);
|
||||
}
|
||||
data->EmitDrag();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reset")) {
|
||||
model->SetMatchTime(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Game Specific Message
|
||||
// make buffer full 64 width, null terminated, for editability
|
||||
wpi::SmallString<64> gameSpecificMessage;
|
||||
model->GetGameSpecificMessage(gameSpecificMessage);
|
||||
gameSpecificMessage.resize(63);
|
||||
gameSpecificMessage.push_back('\0');
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
|
||||
if (ImGui::InputText("Game Specific", gameSpecificMessage.data(),
|
||||
gameSpecificMessage.size(),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
model->SetGameSpecificMessage(gameSpecificMessage.data());
|
||||
}
|
||||
}
|
||||
|
||||
void glass::DisplayFMSReadOnly(FMSModel* model) {
|
||||
bool exists = model->Exists();
|
||||
if (!exists) ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
|
||||
|
||||
if (auto data = model->GetEStopData()) {
|
||||
ImGui::Selectable("E-Stopped: ");
|
||||
data->EmitDrag();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
|
||||
}
|
||||
if (auto data = model->GetEnabledData()) {
|
||||
ImGui::Selectable("Robot Enabled: ");
|
||||
data->EmitDrag();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
|
||||
}
|
||||
if (auto data = model->GetTestData()) {
|
||||
ImGui::Selectable("Test Mode: ");
|
||||
data->EmitDrag();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
|
||||
}
|
||||
if (auto data = model->GetAutonomousData()) {
|
||||
ImGui::Selectable("Autonomous Mode: ");
|
||||
data->EmitDrag();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
|
||||
}
|
||||
if (auto data = model->GetFmsAttachedData()) {
|
||||
ImGui::Selectable("FMS Attached: ");
|
||||
data->EmitDrag();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
|
||||
}
|
||||
if (auto data = model->GetDsAttachedData()) {
|
||||
ImGui::Selectable("DS Attached: ");
|
||||
data->EmitDrag();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
|
||||
}
|
||||
if (auto data = model->GetAllianceStationIdData()) {
|
||||
ImGui::Selectable("Alliance Station: ");
|
||||
data->EmitDrag();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(exists ? stations[static_cast<int>(data->GetValue())]
|
||||
: "?");
|
||||
}
|
||||
if (auto data = model->GetMatchTimeData()) {
|
||||
ImGui::Selectable("Match Time: ");
|
||||
data->EmitDrag();
|
||||
ImGui::SameLine();
|
||||
if (exists)
|
||||
ImGui::Text("%.1f", data->GetValue());
|
||||
else
|
||||
ImGui::TextUnformatted("?");
|
||||
}
|
||||
|
||||
wpi::SmallString<64> gameSpecificMessage;
|
||||
model->GetGameSpecificMessage(gameSpecificMessage);
|
||||
ImGui::Text("Game Specific: %s", exists ? gameSpecificMessage.c_str() : "?");
|
||||
|
||||
if (!exists) ImGui::PopStyleColor();
|
||||
}
|
||||
617
glass/src/lib/native/cpp/other/Field2D.cpp
Normal file
617
glass/src/lib/native/cpp/other/Field2D.cpp
Normal file
@@ -0,0 +1,617 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/other/Field2D.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <portable-file-dialogs.h>
|
||||
#include <units/angle.h>
|
||||
#include <units/length.h>
|
||||
#include <wpi/Path.h>
|
||||
#include <wpi/SmallString.h>
|
||||
#include <wpi/StringMap.h>
|
||||
#include <wpi/json.h>
|
||||
#include <wpi/raw_istream.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
#include <wpigui.h>
|
||||
|
||||
#include "glass/Context.h"
|
||||
#include "glass/DataSource.h"
|
||||
|
||||
using namespace glass;
|
||||
|
||||
namespace gui = wpi::gui;
|
||||
|
||||
namespace {
|
||||
|
||||
// Per-frame field data (not persistent)
|
||||
struct FieldFrameData {
|
||||
// in screen coordinates
|
||||
ImVec2 imageMin;
|
||||
ImVec2 imageMax;
|
||||
ImVec2 min;
|
||||
ImVec2 max;
|
||||
|
||||
float scale; // scaling from field units to screen units
|
||||
};
|
||||
|
||||
// Object drag state
|
||||
struct ObjectDragState {
|
||||
int object = 0;
|
||||
int corner = 0;
|
||||
ImVec2 initialOffset;
|
||||
double initialAngle = 0;
|
||||
};
|
||||
|
||||
// Per-frame object data (not persistent)
|
||||
class ObjectFrameData {
|
||||
public:
|
||||
explicit ObjectFrameData(FieldObjectModel& model, const FieldFrameData& ffd,
|
||||
float width, float length);
|
||||
void SetPosition(double x, double y);
|
||||
// set and get rotation in radians
|
||||
void SetRotation(double rot);
|
||||
double GetRotation() const {
|
||||
return units::convert<units::degrees, units::radians>(m_rot);
|
||||
}
|
||||
void UpdateFrameData();
|
||||
int IsHovered(const ImVec2& cursor) const;
|
||||
bool HandleDrag(const ImVec2& cursor, int hitCorner, ObjectDragState* drag);
|
||||
void Draw(ImDrawList* drawList, const gui::Texture& texture,
|
||||
int hitCorner) const;
|
||||
|
||||
// in window coordinates
|
||||
ImVec2 m_center;
|
||||
ImVec2 m_corners[4];
|
||||
ImVec2 m_arrow[3];
|
||||
|
||||
private:
|
||||
FieldObjectModel& m_model;
|
||||
const FieldFrameData& m_ffd;
|
||||
|
||||
// scaled width/2 and length/2, in screen units
|
||||
float m_width2;
|
||||
float m_length2;
|
||||
|
||||
float m_hitRadius;
|
||||
|
||||
double m_x = 0;
|
||||
double m_y = 0;
|
||||
double m_rot = 0;
|
||||
};
|
||||
|
||||
class ObjectGroupInfo {
|
||||
public:
|
||||
static constexpr float kDefaultWidth = 0.6858f;
|
||||
static constexpr float kDefaultLength = 0.8204f;
|
||||
|
||||
ObjectGroupInfo();
|
||||
|
||||
std::unique_ptr<pfd::open_file> m_fileOpener;
|
||||
float* m_pWidth;
|
||||
float* m_pLength;
|
||||
ObjectDragState m_dragState;
|
||||
|
||||
void Reset();
|
||||
void LoadImage();
|
||||
const gui::Texture& GetTexture() const { return m_texture; }
|
||||
|
||||
private:
|
||||
bool LoadImageImpl(const char* fn);
|
||||
|
||||
std::string* m_pFilename;
|
||||
gui::Texture m_texture;
|
||||
};
|
||||
|
||||
class FieldInfo {
|
||||
public:
|
||||
static constexpr float kDefaultWidth = 15.98f;
|
||||
static constexpr float kDefaultHeight = 8.21f;
|
||||
|
||||
FieldInfo();
|
||||
|
||||
std::unique_ptr<pfd::open_file> m_fileOpener;
|
||||
float* m_pWidth;
|
||||
float* m_pHeight;
|
||||
|
||||
void Reset();
|
||||
void LoadImage();
|
||||
void LoadJson(const wpi::Twine& jsonfile);
|
||||
FieldFrameData GetFrameData(ImVec2 min, ImVec2 max) const;
|
||||
void Draw(ImDrawList* drawList, const FieldFrameData& frameData) const;
|
||||
|
||||
wpi::StringMap<std::unique_ptr<ObjectGroupInfo>> m_objectGroups;
|
||||
|
||||
private:
|
||||
bool LoadImageImpl(const char* fn);
|
||||
|
||||
std::string* m_pFilename;
|
||||
gui::Texture m_texture;
|
||||
int m_imageWidth;
|
||||
int m_imageHeight;
|
||||
int* m_pTop;
|
||||
int* m_pLeft;
|
||||
int* m_pBottom;
|
||||
int* m_pRight;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
FieldInfo::FieldInfo() {
|
||||
auto& storage = GetStorage();
|
||||
m_pFilename = storage.GetStringRef("image");
|
||||
m_pTop = storage.GetIntRef("top", 0);
|
||||
m_pLeft = storage.GetIntRef("left", 0);
|
||||
m_pBottom = storage.GetIntRef("bottom", -1);
|
||||
m_pRight = storage.GetIntRef("right", -1);
|
||||
m_pWidth = storage.GetFloatRef("width", kDefaultWidth);
|
||||
m_pHeight = storage.GetFloatRef("height", kDefaultHeight);
|
||||
}
|
||||
|
||||
void FieldInfo::Reset() {
|
||||
m_texture = gui::Texture{};
|
||||
m_pFilename->clear();
|
||||
m_imageWidth = 0;
|
||||
m_imageHeight = 0;
|
||||
*m_pTop = 0;
|
||||
*m_pLeft = 0;
|
||||
*m_pBottom = -1;
|
||||
*m_pRight = -1;
|
||||
}
|
||||
|
||||
void FieldInfo::LoadImage() {
|
||||
if (m_fileOpener && m_fileOpener->ready(0)) {
|
||||
auto result = m_fileOpener->result();
|
||||
if (!result.empty()) {
|
||||
if (wpi::StringRef(result[0]).endswith(".json")) {
|
||||
LoadJson(result[0]);
|
||||
} else {
|
||||
LoadImageImpl(result[0].c_str());
|
||||
*m_pTop = 0;
|
||||
*m_pLeft = 0;
|
||||
*m_pBottom = -1;
|
||||
*m_pRight = -1;
|
||||
}
|
||||
}
|
||||
m_fileOpener.reset();
|
||||
}
|
||||
if (!m_texture && !m_pFilename->empty()) {
|
||||
if (!LoadImageImpl(m_pFilename->c_str())) m_pFilename->clear();
|
||||
}
|
||||
}
|
||||
|
||||
void FieldInfo::LoadJson(const wpi::Twine& jsonfile) {
|
||||
std::error_code ec;
|
||||
wpi::raw_fd_istream f(jsonfile, ec);
|
||||
if (ec) {
|
||||
wpi::errs() << "GUI: could not open field JSON file\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// parse file
|
||||
wpi::json j;
|
||||
try {
|
||||
j = wpi::json::parse(f);
|
||||
} catch (const wpi::json::parse_error& e) {
|
||||
wpi::errs() << "GUI: JSON: could not parse: " << e.what() << '\n';
|
||||
}
|
||||
|
||||
// top level must be an object
|
||||
if (!j.is_object()) {
|
||||
wpi::errs() << "GUI: JSON: does not contain a top object\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// image filename
|
||||
std::string image;
|
||||
try {
|
||||
image = j.at("field-image").get<std::string>();
|
||||
} catch (const wpi::json::exception& e) {
|
||||
wpi::errs() << "GUI: JSON: could not read field-image: " << e.what()
|
||||
<< '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// corners
|
||||
int top, left, bottom, right;
|
||||
try {
|
||||
top = j.at("field-corners").at("top-left").at(1).get<int>();
|
||||
left = j.at("field-corners").at("top-left").at(0).get<int>();
|
||||
bottom = j.at("field-corners").at("bottom-right").at(1).get<int>();
|
||||
right = j.at("field-corners").at("bottom-right").at(0).get<int>();
|
||||
} catch (const wpi::json::exception& e) {
|
||||
wpi::errs() << "GUI: JSON: could not read field-corners: " << e.what()
|
||||
<< '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// size
|
||||
float width;
|
||||
float height;
|
||||
try {
|
||||
width = j.at("field-size").at(0).get<float>();
|
||||
height = j.at("field-size").at(1).get<float>();
|
||||
} catch (const wpi::json::exception& e) {
|
||||
wpi::errs() << "GUI: JSON: could not read field-size: " << e.what() << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// units for size
|
||||
std::string unit;
|
||||
try {
|
||||
unit = j.at("field-unit").get<std::string>();
|
||||
} catch (const wpi::json::exception& e) {
|
||||
wpi::errs() << "GUI: JSON: could not read field-unit: " << e.what() << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// convert size units to meters
|
||||
if (unit == "foot" || unit == "feet") {
|
||||
width = units::convert<units::feet, units::meters>(width);
|
||||
height = units::convert<units::feet, units::meters>(height);
|
||||
}
|
||||
|
||||
// the image filename is relative to the json file
|
||||
wpi::SmallString<128> pathname;
|
||||
jsonfile.toVector(pathname);
|
||||
wpi::sys::path::remove_filename(pathname);
|
||||
wpi::sys::path::append(pathname, image);
|
||||
|
||||
// load field image
|
||||
if (!LoadImageImpl(pathname.c_str())) return;
|
||||
|
||||
// save to field info
|
||||
*m_pFilename = pathname.str();
|
||||
*m_pTop = top;
|
||||
*m_pLeft = left;
|
||||
*m_pBottom = bottom;
|
||||
*m_pRight = right;
|
||||
*m_pWidth = width;
|
||||
*m_pHeight = height;
|
||||
}
|
||||
|
||||
bool FieldInfo::LoadImageImpl(const char* fn) {
|
||||
wpi::outs() << "GUI: loading field image '" << fn << "'\n";
|
||||
auto texture = gui::Texture::CreateFromFile(fn);
|
||||
if (!texture) {
|
||||
wpi::errs() << "GUI: could not read field image\n";
|
||||
return false;
|
||||
}
|
||||
m_texture = std::move(texture);
|
||||
m_imageWidth = m_texture.GetWidth();
|
||||
m_imageHeight = m_texture.GetHeight();
|
||||
*m_pFilename = fn;
|
||||
return true;
|
||||
}
|
||||
|
||||
FieldFrameData FieldInfo::GetFrameData(ImVec2 min, ImVec2 max) const {
|
||||
// fit the image into the window
|
||||
if (m_texture && m_imageHeight != 0 && m_imageWidth != 0)
|
||||
gui::MaxFit(&min, &max, m_imageWidth, m_imageHeight);
|
||||
|
||||
FieldFrameData ffd;
|
||||
ffd.imageMin = min;
|
||||
ffd.imageMax = max;
|
||||
|
||||
// size down the box by the image corners (if any)
|
||||
if (*m_pBottom > 0 && *m_pRight > 0) {
|
||||
min.x += *m_pLeft * (max.x - min.x) / m_imageWidth;
|
||||
min.y += *m_pTop * (max.y - min.y) / m_imageHeight;
|
||||
max.x -= (m_imageWidth - *m_pRight) * (max.x - min.x) / m_imageWidth;
|
||||
max.y -= (m_imageHeight - *m_pBottom) * (max.y - min.y) / m_imageHeight;
|
||||
}
|
||||
|
||||
// draw the field "active area" as a yellow boundary box
|
||||
gui::MaxFit(&min, &max, *m_pWidth, *m_pHeight);
|
||||
|
||||
ffd.min = min;
|
||||
ffd.max = max;
|
||||
ffd.scale = (max.x - min.x) / *m_pWidth;
|
||||
return ffd;
|
||||
}
|
||||
|
||||
void FieldInfo::Draw(ImDrawList* drawList, const FieldFrameData& ffd) const {
|
||||
if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) {
|
||||
drawList->AddImage(m_texture, ffd.imageMin, ffd.imageMax);
|
||||
}
|
||||
|
||||
// draw the field "active area" as a yellow boundary box
|
||||
drawList->AddRect(ffd.min, ffd.max, IM_COL32(255, 255, 0, 255));
|
||||
}
|
||||
|
||||
ObjectGroupInfo::ObjectGroupInfo() {
|
||||
auto& storage = GetStorage();
|
||||
m_pFilename = storage.GetStringRef("image");
|
||||
m_pWidth = storage.GetFloatRef("width", kDefaultWidth);
|
||||
m_pLength = storage.GetFloatRef("length", kDefaultLength);
|
||||
}
|
||||
|
||||
void ObjectGroupInfo::Reset() {
|
||||
m_texture = gui::Texture{};
|
||||
m_pFilename->clear();
|
||||
}
|
||||
|
||||
void ObjectGroupInfo::LoadImage() {
|
||||
if (m_fileOpener && m_fileOpener->ready(0)) {
|
||||
auto result = m_fileOpener->result();
|
||||
if (!result.empty()) LoadImageImpl(result[0].c_str());
|
||||
m_fileOpener.reset();
|
||||
}
|
||||
if (!m_texture && !m_pFilename->empty()) {
|
||||
if (!LoadImageImpl(m_pFilename->c_str())) m_pFilename->clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool ObjectGroupInfo::LoadImageImpl(const char* fn) {
|
||||
wpi::outs() << "GUI: loading object image '" << fn << "'\n";
|
||||
auto texture = gui::Texture::CreateFromFile(fn);
|
||||
if (!texture) {
|
||||
wpi::errs() << "GUI: could not read object image\n";
|
||||
return false;
|
||||
}
|
||||
m_texture = std::move(texture);
|
||||
*m_pFilename = fn;
|
||||
return true;
|
||||
}
|
||||
|
||||
ObjectFrameData::ObjectFrameData(FieldObjectModel& model,
|
||||
const FieldFrameData& ffd, float width,
|
||||
float length)
|
||||
: m_model{model},
|
||||
m_ffd{ffd},
|
||||
m_width2(ffd.scale * width / 2),
|
||||
m_length2(ffd.scale * length / 2),
|
||||
m_hitRadius((std::min)(m_width2, m_length2) / 2) {
|
||||
if (auto xData = model.GetXData()) m_x = xData->GetValue();
|
||||
if (auto yData = model.GetYData()) m_y = yData->GetValue();
|
||||
if (auto rotationData = model.GetRotationData())
|
||||
m_rot = rotationData->GetValue();
|
||||
UpdateFrameData();
|
||||
}
|
||||
|
||||
void ObjectFrameData::SetPosition(double x, double y) {
|
||||
m_x = x;
|
||||
m_y = y;
|
||||
m_model.SetPosition(x, y);
|
||||
}
|
||||
|
||||
void ObjectFrameData::SetRotation(double rot) {
|
||||
double rotDegrees = units::convert<units::radians, units::degrees>(rot);
|
||||
// force to -180 to +180 range
|
||||
rotDegrees = rotDegrees + std::ceil((-rotDegrees - 180) / 360) * 360;
|
||||
m_rot = rotDegrees;
|
||||
m_model.SetRotation(rotDegrees);
|
||||
}
|
||||
|
||||
void ObjectFrameData::UpdateFrameData() {
|
||||
// (0,0) origin is bottom left
|
||||
ImVec2 center(m_ffd.min.x + m_ffd.scale * m_x,
|
||||
m_ffd.max.y - m_ffd.scale * m_y);
|
||||
|
||||
// build rotated points around center
|
||||
float length2 = m_length2;
|
||||
float width2 = m_width2;
|
||||
double rot = GetRotation();
|
||||
float cos_a = std::cos(-rot);
|
||||
float sin_a = std::sin(-rot);
|
||||
|
||||
m_corners[0] = center + ImRotate(ImVec2(-length2, -width2), cos_a, sin_a);
|
||||
m_corners[1] = center + ImRotate(ImVec2(length2, -width2), cos_a, sin_a);
|
||||
m_corners[2] = center + ImRotate(ImVec2(length2, width2), cos_a, sin_a);
|
||||
m_corners[3] = center + ImRotate(ImVec2(-length2, width2), cos_a, sin_a);
|
||||
m_arrow[0] =
|
||||
center + ImRotate(ImVec2(-length2 / 2, -width2 / 2), cos_a, sin_a);
|
||||
m_arrow[1] = center + ImRotate(ImVec2(length2 / 2, 0), cos_a, sin_a);
|
||||
m_arrow[2] =
|
||||
center + ImRotate(ImVec2(-length2 / 2, width2 / 2), cos_a, sin_a);
|
||||
|
||||
m_center = center;
|
||||
}
|
||||
|
||||
int ObjectFrameData::IsHovered(const ImVec2& cursor) const {
|
||||
// only allow initiation of dragging when invisible button is hovered;
|
||||
// this prevents the window resize handles from simultaneously activating
|
||||
// the drag functionality
|
||||
if (!ImGui::IsItemHovered()) return 0;
|
||||
|
||||
float hitRadiusSquared = m_hitRadius * m_hitRadius;
|
||||
// it's within the hit radius of the center?
|
||||
if (gui::GetDistSquared(cursor, m_center) < hitRadiusSquared)
|
||||
return 1;
|
||||
else if (gui::GetDistSquared(cursor, m_corners[0]) < hitRadiusSquared)
|
||||
return 2;
|
||||
else if (gui::GetDistSquared(cursor, m_corners[1]) < hitRadiusSquared)
|
||||
return 3;
|
||||
else if (gui::GetDistSquared(cursor, m_corners[2]) < hitRadiusSquared)
|
||||
return 4;
|
||||
else if (gui::GetDistSquared(cursor, m_corners[3]) < hitRadiusSquared)
|
||||
return 5;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ObjectFrameData::HandleDrag(const ImVec2& cursor, int hitCorner,
|
||||
ObjectDragState* drag) {
|
||||
bool rv = false;
|
||||
if (hitCorner > 0 && ImGui::IsMouseClicked(0)) {
|
||||
if (hitCorner == 1) {
|
||||
drag->corner = hitCorner;
|
||||
drag->initialOffset = cursor - m_center;
|
||||
} else {
|
||||
drag->corner = hitCorner;
|
||||
ImVec2 off = cursor - m_center;
|
||||
drag->initialAngle = std::atan2(off.y, off.x) + GetRotation();
|
||||
}
|
||||
rv = true;
|
||||
}
|
||||
|
||||
if (drag->corner > 0 && ImGui::IsMouseDown(0)) {
|
||||
if (drag->corner == 1) {
|
||||
ImVec2 newPos = cursor - drag->initialOffset;
|
||||
SetPosition(
|
||||
(std::clamp(newPos.x, m_ffd.min.x, m_ffd.max.x) - m_ffd.min.x) /
|
||||
m_ffd.scale,
|
||||
(m_ffd.max.y - std::clamp(newPos.y, m_ffd.min.y, m_ffd.max.y)) /
|
||||
m_ffd.scale);
|
||||
UpdateFrameData();
|
||||
} else {
|
||||
ImVec2 off = cursor - m_center;
|
||||
SetRotation(drag->initialAngle - std::atan2(off.y, off.x));
|
||||
}
|
||||
} else {
|
||||
drag->corner = 0;
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
void ObjectFrameData::Draw(ImDrawList* drawList, const gui::Texture& texture,
|
||||
int hitCorner) const {
|
||||
if (texture) {
|
||||
drawList->AddImageQuad(texture, m_corners[0], m_corners[1], m_corners[2],
|
||||
m_corners[3]);
|
||||
} else {
|
||||
drawList->AddQuad(m_corners[0], m_corners[1], m_corners[2], m_corners[3],
|
||||
IM_COL32(255, 0, 0, 255), 4.0);
|
||||
drawList->AddTriangle(m_arrow[0], m_arrow[1], m_arrow[2],
|
||||
IM_COL32(0, 255, 0, 255), 4.0);
|
||||
}
|
||||
|
||||
if (hitCorner > 0) {
|
||||
if (hitCorner == 1) {
|
||||
drawList->AddCircle(m_center, m_hitRadius, IM_COL32(0, 255, 0, 255));
|
||||
} else {
|
||||
drawList->AddCircle(m_corners[hitCorner - 2], m_hitRadius,
|
||||
IM_COL32(0, 255, 0, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void glass::DisplayField2DSettings(Field2DModel* model) {
|
||||
auto& storage = GetStorage();
|
||||
auto field = storage.GetData<FieldInfo>();
|
||||
if (!field) {
|
||||
storage.SetData(std::make_shared<FieldInfo>());
|
||||
field = storage.GetData<FieldInfo>();
|
||||
}
|
||||
|
||||
ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
|
||||
if (ImGui::CollapsingHeader("Field")) {
|
||||
ImGui::PushID("Field");
|
||||
if (ImGui::Button("Choose image...")) {
|
||||
field->m_fileOpener = std::make_unique<pfd::open_file>(
|
||||
"Choose field image", "",
|
||||
std::vector<std::string>{"Image File",
|
||||
"*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif "
|
||||
"*.hdr *.pic *.ppm *.pgm",
|
||||
"PathWeaver JSON File", "*.json"});
|
||||
}
|
||||
if (ImGui::Button("Reset image")) {
|
||||
field->Reset();
|
||||
}
|
||||
ImGui::InputFloat("Field Width", field->m_pWidth);
|
||||
ImGui::InputFloat("Field Height", field->m_pHeight);
|
||||
// ImGui::InputInt("Field Top", field->m_pTop);
|
||||
// ImGui::InputInt("Field Left", field->m_pLeft);
|
||||
// ImGui::InputInt("Field Right", field->m_pRight);
|
||||
// ImGui::InputInt("Field Bottom", field->m_pBottom);
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
model->ForEachFieldObjectGroup([&](auto& groupModel, auto name) {
|
||||
if (!groupModel.Exists()) return;
|
||||
PushID(name);
|
||||
auto& objGroupRef = field->m_objectGroups[name];
|
||||
if (!objGroupRef) objGroupRef = std::make_unique<ObjectGroupInfo>();
|
||||
auto objGroup = objGroupRef.get();
|
||||
|
||||
wpi::SmallString<64> nameBuf = name;
|
||||
if (ImGui::CollapsingHeader(nameBuf.c_str())) {
|
||||
if (ImGui::Button("Choose image...")) {
|
||||
objGroup->m_fileOpener = std::make_unique<pfd::open_file>(
|
||||
"Choose object image", "",
|
||||
std::vector<std::string>{
|
||||
"Image File",
|
||||
"*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif "
|
||||
"*.hdr *.pic *.ppm *.pgm"});
|
||||
}
|
||||
if (ImGui::Button("Reset image")) {
|
||||
objGroup->Reset();
|
||||
}
|
||||
ImGui::InputFloat("Width", objGroup->m_pWidth);
|
||||
ImGui::InputFloat("Length", objGroup->m_pLength);
|
||||
}
|
||||
PopID();
|
||||
});
|
||||
ImGui::PopItemWidth();
|
||||
}
|
||||
|
||||
void glass::DisplayField2D(Field2DModel* model, const ImVec2& contentSize) {
|
||||
auto& storage = GetStorage();
|
||||
auto field = storage.GetData<FieldInfo>();
|
||||
if (!field) {
|
||||
storage.SetData(std::make_shared<FieldInfo>());
|
||||
field = storage.GetData<FieldInfo>();
|
||||
}
|
||||
|
||||
ImVec2 windowPos = ImGui::GetWindowPos();
|
||||
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
||||
|
||||
// for dragging to work, there needs to be a button (otherwise the window is
|
||||
// dragged)
|
||||
if (contentSize.x <= 0 || contentSize.y <= 0) return;
|
||||
ImVec2 cursorPos = windowPos + ImGui::GetCursorPos(); // screen coords
|
||||
ImGui::InvisibleButton("field", contentSize);
|
||||
|
||||
// field
|
||||
field->LoadImage();
|
||||
FieldFrameData ffd = field->GetFrameData(cursorPos, cursorPos + contentSize);
|
||||
auto drawList = ImGui::GetWindowDrawList();
|
||||
field->Draw(drawList, ffd);
|
||||
|
||||
model->ForEachFieldObjectGroup([&](auto& groupModel, auto name) {
|
||||
if (!groupModel.Exists()) return;
|
||||
PushID(name);
|
||||
auto& objGroupRef = field->m_objectGroups[name];
|
||||
if (!objGroupRef) objGroupRef = std::make_unique<ObjectGroupInfo>();
|
||||
auto objGroup = objGroupRef.get();
|
||||
objGroup->LoadImage();
|
||||
|
||||
int i = 0;
|
||||
groupModel.ForEachFieldObject([&](auto& objModel) {
|
||||
++i;
|
||||
ObjectFrameData ofd{objModel, ffd, *objGroup->m_pWidth,
|
||||
*objGroup->m_pLength};
|
||||
|
||||
int hitCorner = 0;
|
||||
if (objGroup->m_dragState.object == 0 ||
|
||||
objGroup->m_dragState.object == i) {
|
||||
hitCorner = ofd.IsHovered(mousePos);
|
||||
if (ofd.HandleDrag(mousePos, hitCorner, &objGroup->m_dragState))
|
||||
objGroup->m_dragState.object = i;
|
||||
}
|
||||
|
||||
// draw
|
||||
ofd.Draw(drawList, objGroup->GetTexture(), hitCorner);
|
||||
});
|
||||
PopID();
|
||||
});
|
||||
}
|
||||
|
||||
void Field2DView::Display() {
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
DisplayField2DSettings(m_model);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
DisplayField2D(m_model, ImGui::GetWindowContentRegionMax() -
|
||||
ImGui::GetWindowContentRegionMin());
|
||||
}
|
||||
932
glass/src/lib/native/cpp/other/Plot.cpp
Normal file
932
glass/src/lib/native/cpp/other/Plot.cpp
Normal file
@@ -0,0 +1,932 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/other/Plot.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <imgui_stdlib.h>
|
||||
#include <implot.h>
|
||||
#include <wpigui.h>
|
||||
#include <wpi/Signal.h>
|
||||
#include <wpi/SmallString.h>
|
||||
#include <wpi/SmallVector.h>
|
||||
#include <wpi/timestamp.h>
|
||||
|
||||
#include "glass/Context.h"
|
||||
#include "glass/DataSource.h"
|
||||
#include "glass/support/ExtraGuiWidgets.h"
|
||||
|
||||
using namespace glass;
|
||||
|
||||
namespace {
|
||||
class PlotView;
|
||||
|
||||
struct PlotSeriesRef {
|
||||
PlotView* view;
|
||||
size_t plotIndex;
|
||||
size_t seriesIndex;
|
||||
};
|
||||
|
||||
class PlotSeries {
|
||||
public:
|
||||
explicit PlotSeries(wpi::StringRef id);
|
||||
explicit PlotSeries(DataSource* source, int yAxis = 0);
|
||||
|
||||
const std::string& GetId() const { return m_id; }
|
||||
|
||||
void CheckSource();
|
||||
|
||||
void SetSource(DataSource* source);
|
||||
DataSource* GetSource() const { return m_source; }
|
||||
|
||||
void Clear() { m_size = 0; }
|
||||
|
||||
bool ReadIni(wpi::StringRef name, wpi::StringRef value);
|
||||
void WriteIni(ImGuiTextBuffer* out);
|
||||
|
||||
enum Action { kNone, kMoveUp, kMoveDown, kDelete };
|
||||
Action EmitPlot(PlotView& view, double now, size_t i, size_t plotIndex);
|
||||
void EmitSettings(size_t i);
|
||||
void EmitDragDropPayload(PlotView& view, size_t i, size_t plotIndex);
|
||||
|
||||
const char* GetName() const;
|
||||
|
||||
int GetYAxis() const { return m_yAxis; }
|
||||
void SetYAxis(int yAxis) { m_yAxis = yAxis; }
|
||||
|
||||
private:
|
||||
bool IsDigital() const {
|
||||
return m_digital == kDigital ||
|
||||
(m_digital == kAuto && m_source && m_source->IsDigital());
|
||||
}
|
||||
void AppendValue(double value, uint64_t time);
|
||||
|
||||
// source linkage
|
||||
DataSource* m_source = nullptr;
|
||||
wpi::sig::ScopedConnection m_sourceCreatedConn;
|
||||
wpi::sig::ScopedConnection m_newValueConn;
|
||||
std::string m_id;
|
||||
|
||||
// user settings
|
||||
std::string m_name;
|
||||
int m_yAxis = 0;
|
||||
ImVec4 m_color = IMPLOT_AUTO_COL;
|
||||
int m_marker = 0;
|
||||
float m_weight = IMPLOT_AUTO;
|
||||
|
||||
enum Digital { kAuto, kDigital, kAnalog };
|
||||
int m_digital = 0;
|
||||
int m_digitalBitHeight = 8;
|
||||
int m_digitalBitGap = 4;
|
||||
|
||||
// value storage
|
||||
static constexpr int kMaxSize = 2000;
|
||||
static constexpr double kTimeGap = 0.05;
|
||||
std::atomic<int> m_size = 0;
|
||||
std::atomic<int> m_offset = 0;
|
||||
ImPlotPoint m_data[kMaxSize];
|
||||
};
|
||||
|
||||
class Plot {
|
||||
public:
|
||||
Plot();
|
||||
|
||||
bool ReadIni(wpi::StringRef name, wpi::StringRef value);
|
||||
void WriteIni(ImGuiTextBuffer* out);
|
||||
|
||||
void Clear();
|
||||
|
||||
void DragDropTarget(PlotView& view, size_t i, bool inPlot);
|
||||
void EmitPlot(PlotView& view, double now, bool paused, size_t i);
|
||||
void EmitSettings(size_t i);
|
||||
|
||||
const std::string& GetName() const { return m_name; }
|
||||
|
||||
std::vector<std::unique_ptr<PlotSeries>> m_series;
|
||||
|
||||
private:
|
||||
void EmitSettingsLimits(int axis);
|
||||
|
||||
std::string m_name;
|
||||
bool m_visible = true;
|
||||
bool m_showPause = true;
|
||||
unsigned int m_plotFlags = ImPlotFlags_Default;
|
||||
bool m_lockPrevX = false;
|
||||
bool m_paused = false;
|
||||
float m_viewTime = 10;
|
||||
int m_height = 300;
|
||||
struct PlotRange {
|
||||
double min = 0;
|
||||
double max = 1;
|
||||
bool lockMin = false;
|
||||
bool lockMax = false;
|
||||
bool apply = false;
|
||||
};
|
||||
PlotRange m_axisRange[3];
|
||||
ImPlotRange m_xaxisRange; // read from plot, used for lockPrevX
|
||||
};
|
||||
|
||||
class PlotView : public View {
|
||||
public:
|
||||
explicit PlotView(PlotProvider* provider) : m_provider{provider} {}
|
||||
|
||||
void Clear();
|
||||
|
||||
void Display() override;
|
||||
|
||||
void MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex);
|
||||
|
||||
void MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
|
||||
size_t fromSeriesIndex, size_t toPlotIndex,
|
||||
size_t toSeriesIndex, int yAxis = -1);
|
||||
|
||||
PlotProvider* m_provider;
|
||||
std::vector<std::unique_ptr<Plot>> m_plots;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
PlotSeries::PlotSeries(wpi::StringRef id) : m_id(id) {
|
||||
if (DataSource* source = DataSource::Find(id)) {
|
||||
SetSource(source);
|
||||
return;
|
||||
}
|
||||
CheckSource();
|
||||
}
|
||||
|
||||
PlotSeries::PlotSeries(DataSource* source, int yAxis) : m_yAxis(yAxis) {
|
||||
SetSource(source);
|
||||
}
|
||||
|
||||
void PlotSeries::CheckSource() {
|
||||
if (!m_newValueConn.connected() && !m_sourceCreatedConn.connected()) {
|
||||
m_source = nullptr;
|
||||
m_sourceCreatedConn = DataSource::sourceCreated.connect_connection(
|
||||
[this](const char* id, DataSource* source) {
|
||||
if (m_id == id) {
|
||||
SetSource(source);
|
||||
m_sourceCreatedConn.disconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PlotSeries::SetSource(DataSource* source) {
|
||||
m_source = source;
|
||||
m_id = source->GetId();
|
||||
|
||||
// add initial value
|
||||
m_data[m_size++] = ImPlotPoint{wpi::Now() * 1.0e-6, source->GetValue()};
|
||||
|
||||
m_newValueConn = source->valueChanged.connect_connection(
|
||||
[this](double value, uint64_t time) { AppendValue(value, time); });
|
||||
}
|
||||
|
||||
void PlotSeries::AppendValue(double value, uint64_t timeUs) {
|
||||
double time = (timeUs != 0 ? timeUs : wpi::Now()) * 1.0e-6;
|
||||
if (IsDigital()) {
|
||||
if (m_size < kMaxSize) {
|
||||
m_data[m_size] = ImPlotPoint{time, value};
|
||||
++m_size;
|
||||
} else {
|
||||
m_data[m_offset] = ImPlotPoint{time, value};
|
||||
m_offset = (m_offset + 1) % kMaxSize;
|
||||
}
|
||||
} else {
|
||||
// as an analog graph draws linear lines in between each value,
|
||||
// insert duplicate value if "long" time between updates so it
|
||||
// looks appropriately flat
|
||||
if (m_size < kMaxSize) {
|
||||
if (m_size > 0) {
|
||||
if ((time - m_data[m_size - 1].x) > kTimeGap) {
|
||||
m_data[m_size] = ImPlotPoint{time, m_data[m_size - 1].y};
|
||||
++m_size;
|
||||
}
|
||||
}
|
||||
m_data[m_size] = ImPlotPoint{time, value};
|
||||
++m_size;
|
||||
} else {
|
||||
if (m_offset == 0) {
|
||||
if ((time - m_data[kMaxSize - 1].x) > kTimeGap) {
|
||||
m_data[m_offset] = ImPlotPoint{time, m_data[kMaxSize - 1].y};
|
||||
++m_offset;
|
||||
}
|
||||
} else {
|
||||
if ((time - m_data[m_offset - 1].x) > kTimeGap) {
|
||||
m_data[m_offset] = ImPlotPoint{time, m_data[m_offset - 1].y};
|
||||
m_offset = (m_offset + 1) % kMaxSize;
|
||||
}
|
||||
}
|
||||
m_data[m_offset] = ImPlotPoint{time, value};
|
||||
m_offset = (m_offset + 1) % kMaxSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PlotSeries::ReadIni(wpi::StringRef name, wpi::StringRef value) {
|
||||
if (name == "name") {
|
||||
m_name = value;
|
||||
return true;
|
||||
}
|
||||
if (name == "yAxis") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_yAxis = num;
|
||||
return true;
|
||||
} else if (name == "color") {
|
||||
unsigned int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_color = ImColor(num);
|
||||
return true;
|
||||
} else if (name == "marker") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_marker = num;
|
||||
return true;
|
||||
} else if (name == "weight") {
|
||||
std::sscanf(value.data(), "%f", &m_weight);
|
||||
return true;
|
||||
} else if (name == "digital") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_digital = num;
|
||||
return true;
|
||||
} else if (name == "digitalBitHeight") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_digitalBitHeight = num;
|
||||
return true;
|
||||
} else if (name == "digitalBitGap") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_digitalBitGap = num;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PlotSeries::WriteIni(ImGuiTextBuffer* out) {
|
||||
out->appendf(
|
||||
"name=%s\nyAxis=%d\ncolor=%u\nmarker=%d\nweight=%f\ndigital=%d\n"
|
||||
"digitalBitHeight=%d\ndigitalBitGap=%d\n",
|
||||
m_name.c_str(), m_yAxis, static_cast<ImU32>(ImColor(m_color)), m_marker,
|
||||
m_weight, m_digital, m_digitalBitHeight, m_digitalBitGap);
|
||||
}
|
||||
|
||||
const char* PlotSeries::GetName() const {
|
||||
if (!m_name.empty()) return m_name.c_str();
|
||||
if (m_newValueConn.connected()) {
|
||||
auto sourceName = m_source->GetName();
|
||||
if (sourceName[0] != '\0') return sourceName;
|
||||
}
|
||||
return m_id.c_str();
|
||||
}
|
||||
|
||||
PlotSeries::Action PlotSeries::EmitPlot(PlotView& view, double now, size_t i,
|
||||
size_t plotIndex) {
|
||||
CheckSource();
|
||||
|
||||
char label[128];
|
||||
std::snprintf(label, sizeof(label), "%s###name", GetName());
|
||||
|
||||
int size = m_size;
|
||||
int offset = m_offset;
|
||||
|
||||
// need to have last value at current time, so need to create fake last value
|
||||
// we handle the offset logic ourselves to avoid wrap issues with size + 1
|
||||
struct GetterData {
|
||||
double now;
|
||||
ImPlotPoint* data;
|
||||
int size;
|
||||
int offset;
|
||||
};
|
||||
GetterData getterData = {now, m_data, size, offset};
|
||||
auto getter = [](void* data, int idx) {
|
||||
auto d = static_cast<GetterData*>(data);
|
||||
if (idx == d->size)
|
||||
return ImPlotPoint{
|
||||
d->now, d->data[d->offset == 0 ? d->size - 1 : d->offset - 1].y};
|
||||
if (d->offset + idx < d->size)
|
||||
return d->data[d->offset + idx];
|
||||
else
|
||||
return d->data[d->offset + idx - d->size];
|
||||
};
|
||||
|
||||
if (m_color.w == IMPLOT_AUTO_COL.w) m_color = ImPlot::GetColormapColor(i);
|
||||
ImPlot::SetNextLineStyle(m_color, m_weight);
|
||||
if (IsDigital()) {
|
||||
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight);
|
||||
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap);
|
||||
ImPlot::PlotDigital(label, getter, &getterData, size + 1);
|
||||
ImPlot::PopStyleVar();
|
||||
ImPlot::PopStyleVar();
|
||||
} else {
|
||||
ImPlot::SetPlotYAxis(m_yAxis);
|
||||
ImPlot::SetNextMarkerStyle(m_marker - 1);
|
||||
ImPlot::PlotLine(label, getter, &getterData, size + 1);
|
||||
}
|
||||
|
||||
// DND source for PlotSeries
|
||||
if (ImPlot::BeginLegendDragDropSource(label)) {
|
||||
EmitDragDropPayload(view, i, plotIndex);
|
||||
ImPlot::EndLegendDragDropSource();
|
||||
}
|
||||
|
||||
// Edit settings via popup
|
||||
Action rv = kNone;
|
||||
if (ImPlot::BeginLegendPopup(label)) {
|
||||
if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
|
||||
ImGui::Text("Edit series name:");
|
||||
ImGui::InputText("##editname", &m_name);
|
||||
if (ImGui::Button("Move Up")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
rv = kMoveUp;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Move Down")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
rv = kMoveDown;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Delete")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
rv = kDelete;
|
||||
}
|
||||
EmitSettings(i);
|
||||
ImPlot::EndLegendPopup();
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
void PlotSeries::EmitDragDropPayload(PlotView& view, size_t i,
|
||||
size_t plotIndex) {
|
||||
PlotSeriesRef ref = {&view, plotIndex, i};
|
||||
ImGui::SetDragDropPayload("PlotSeries", &ref, sizeof(ref));
|
||||
ImGui::TextUnformatted(GetName());
|
||||
}
|
||||
|
||||
void PlotSeries::EmitSettings(size_t i) {
|
||||
// Line color
|
||||
{
|
||||
ImGui::ColorEdit3("Color", &m_color.x, ImGuiColorEditFlags_NoInputs);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Default")) m_color = ImPlot::GetColormapColor(i);
|
||||
}
|
||||
|
||||
// Line weight
|
||||
{
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
||||
ImGui::InputFloat("Weight", &m_weight, 0.1f, 1.0f, "%.1f");
|
||||
}
|
||||
|
||||
// Digital
|
||||
{
|
||||
static const char* const options[] = {"Auto", "Digital", "Analog"};
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
||||
ImGui::Combo("Digital", &m_digital, options,
|
||||
sizeof(options) / sizeof(options[0]));
|
||||
}
|
||||
|
||||
if (IsDigital()) {
|
||||
// Bit Height
|
||||
{
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
|
||||
ImGui::InputInt("Bit Height", &m_digitalBitHeight);
|
||||
}
|
||||
|
||||
// Bit Gap
|
||||
{
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
|
||||
ImGui::InputInt("Bit Gap", &m_digitalBitGap);
|
||||
}
|
||||
} else {
|
||||
// Y-axis
|
||||
{
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
|
||||
static const char* const options[] = {"1", "2", "3"};
|
||||
ImGui::Combo("Y-Axis", &m_yAxis, options, 3);
|
||||
}
|
||||
|
||||
// Marker
|
||||
{
|
||||
static const char* const options[] = {
|
||||
"None", "Circle", "Square", "Diamond", "Up", "Down",
|
||||
"Left", "Right", "Cross", "Plus", "Asterisk"};
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
|
||||
ImGui::Combo("Marker", &m_marker, options,
|
||||
sizeof(options) / sizeof(options[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Plot::Plot() {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
m_axisRange[i] = PlotRange{};
|
||||
}
|
||||
}
|
||||
|
||||
bool Plot::ReadIni(wpi::StringRef name, wpi::StringRef value) {
|
||||
if (name == "name") {
|
||||
m_name = value;
|
||||
return true;
|
||||
} else if (name == "visible") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_visible = num != 0;
|
||||
return true;
|
||||
} else if (name == "showPause") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_showPause = num != 0;
|
||||
return true;
|
||||
} else if (name == "lockPrevX") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_lockPrevX = num != 0;
|
||||
return true;
|
||||
} else if (name == "legend") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
if (num == 0)
|
||||
m_plotFlags &= ~ImPlotFlags_Legend;
|
||||
else
|
||||
m_plotFlags |= ImPlotFlags_Legend;
|
||||
return true;
|
||||
} else if (name == "yaxis2") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
if (num == 0)
|
||||
m_plotFlags &= ~ImPlotFlags_YAxis2;
|
||||
else
|
||||
m_plotFlags |= ImPlotFlags_YAxis2;
|
||||
return true;
|
||||
} else if (name == "yaxis3") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
if (num == 0)
|
||||
m_plotFlags &= ~ImPlotFlags_YAxis3;
|
||||
else
|
||||
m_plotFlags |= ImPlotFlags_YAxis3;
|
||||
return true;
|
||||
} else if (name == "viewTime") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_viewTime = num / 1000.0;
|
||||
return true;
|
||||
} else if (name == "height") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_height = num;
|
||||
return true;
|
||||
} else if (name.startswith("y")) {
|
||||
auto [yAxisStr, yName] = name.split('_');
|
||||
int yAxis;
|
||||
if (yAxisStr.substr(1).getAsInteger(10, yAxis)) return false;
|
||||
if (yAxis < 0 || yAxis > 3) return false;
|
||||
if (yName == "min") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_axisRange[yAxis].min = num / 1000.0;
|
||||
return true;
|
||||
} else if (yName == "max") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_axisRange[yAxis].max = num / 1000.0;
|
||||
return true;
|
||||
} else if (yName == "lockMin") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_axisRange[yAxis].lockMin = num != 0;
|
||||
return true;
|
||||
} else if (yName == "lockMax") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_axisRange[yAxis].lockMax = num != 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Plot::WriteIni(ImGuiTextBuffer* out) {
|
||||
out->appendf(
|
||||
"name=%s\nvisible=%d\nshowPause=%d\nlockPrevX=%d\nlegend=%d\n"
|
||||
"yaxis2=%d\nyaxis3=%d\nviewTime=%d\nheight=%d\n",
|
||||
m_name.c_str(), m_visible ? 1 : 0, m_showPause ? 1 : 0,
|
||||
m_lockPrevX ? 1 : 0, (m_plotFlags & ImPlotFlags_Legend) ? 1 : 0,
|
||||
(m_plotFlags & ImPlotFlags_YAxis2) ? 1 : 0,
|
||||
(m_plotFlags & ImPlotFlags_YAxis3) ? 1 : 0,
|
||||
static_cast<int>(m_viewTime * 1000), m_height);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
out->appendf("y%d_min=%d\ny%d_max=%d\ny%d_lockMin=%d\ny%d_lockMax=%d\n", i,
|
||||
static_cast<int>(m_axisRange[i].min * 1000), i,
|
||||
static_cast<int>(m_axisRange[i].max * 1000), i,
|
||||
m_axisRange[i].lockMin ? 1 : 0, i,
|
||||
m_axisRange[i].lockMax ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Plot::Clear() {
|
||||
for (auto&& series : m_series) series->Clear();
|
||||
}
|
||||
|
||||
void Plot::DragDropTarget(PlotView& view, size_t i, bool inPlot) {
|
||||
if (!ImGui::BeginDragDropTarget()) return;
|
||||
// handle dragging onto a specific Y axis
|
||||
int yAxis = -1;
|
||||
if (inPlot) {
|
||||
for (int y = 0; y < 3; ++y) {
|
||||
if (ImPlot::IsPlotYAxisHovered(y)) {
|
||||
yAxis = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const ImGuiPayload* payload =
|
||||
ImGui::AcceptDragDropPayload("DataSource")) {
|
||||
auto source = *static_cast<DataSource**>(payload->Data);
|
||||
// don't add duplicates unless it's onto a different Y axis
|
||||
auto it =
|
||||
std::find_if(m_series.begin(), m_series.end(), [=](const auto& elem) {
|
||||
return elem->GetId() == source->GetId() &&
|
||||
(yAxis == -1 || elem->GetYAxis() == yAxis);
|
||||
});
|
||||
if (it == m_series.end()) {
|
||||
m_series.emplace_back(
|
||||
std::make_unique<PlotSeries>(source, yAxis == -1 ? 0 : yAxis));
|
||||
}
|
||||
} else if (const ImGuiPayload* payload =
|
||||
ImGui::AcceptDragDropPayload("PlotSeries")) {
|
||||
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
||||
view.MovePlotSeries(ref->view, ref->plotIndex, ref->seriesIndex, i,
|
||||
m_series.size(), yAxis);
|
||||
} else if (const ImGuiPayload* payload =
|
||||
ImGui::AcceptDragDropPayload("Plot")) {
|
||||
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
||||
view.MovePlot(ref->view, ref->plotIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
void Plot::EmitPlot(PlotView& view, double now, bool paused, size_t i) {
|
||||
if (!m_visible) return;
|
||||
|
||||
bool lockX = (i != 0 && m_lockPrevX);
|
||||
|
||||
if (!lockX && m_showPause && ImGui::Button(m_paused ? "Resume" : "Pause"))
|
||||
m_paused = !m_paused;
|
||||
|
||||
char label[128];
|
||||
std::snprintf(label, sizeof(label), "%s##plot", m_name.c_str());
|
||||
|
||||
if (lockX) {
|
||||
ImPlot::SetNextPlotLimitsX(view.m_plots[i - 1]->m_xaxisRange.Min,
|
||||
view.m_plots[i - 1]->m_xaxisRange.Max,
|
||||
ImGuiCond_Always);
|
||||
} else {
|
||||
// also force-pause plots if overall timing is paused
|
||||
ImPlot::SetNextPlotLimitsX(
|
||||
now - m_viewTime, now,
|
||||
(paused || m_paused) ? ImGuiCond_Once : ImGuiCond_Always);
|
||||
}
|
||||
|
||||
ImPlotAxisFlags yFlags[3] = {ImPlotAxisFlags_Default,
|
||||
ImPlotAxisFlags_Auxiliary,
|
||||
ImPlotAxisFlags_Auxiliary};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
ImPlot::SetNextPlotLimitsY(
|
||||
m_axisRange[i].min, m_axisRange[i].max,
|
||||
m_axisRange[i].apply ? ImGuiCond_Always : ImGuiCond_Once, i);
|
||||
m_axisRange[i].apply = false;
|
||||
if (m_axisRange[i].lockMin) yFlags[i] |= ImPlotAxisFlags_LockMin;
|
||||
if (m_axisRange[i].lockMax) yFlags[i] |= ImPlotAxisFlags_LockMax;
|
||||
}
|
||||
|
||||
if (ImPlot::BeginPlot(label, nullptr, nullptr, ImVec2(-1, m_height),
|
||||
m_plotFlags, ImPlotAxisFlags_Default, yFlags[0],
|
||||
yFlags[1], yFlags[2])) {
|
||||
for (size_t j = 0; j < m_series.size(); ++j) {
|
||||
ImGui::PushID(j);
|
||||
switch (m_series[j]->EmitPlot(view, now, j, i)) {
|
||||
case PlotSeries::kMoveUp:
|
||||
if (j > 0) std::swap(m_series[j - 1], m_series[j]);
|
||||
break;
|
||||
case PlotSeries::kMoveDown:
|
||||
if (j < (m_series.size() - 1))
|
||||
std::swap(m_series[j], m_series[j + 1]);
|
||||
break;
|
||||
case PlotSeries::kDelete:
|
||||
m_series.erase(m_series.begin() + j);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
DragDropTarget(view, i, true);
|
||||
m_xaxisRange = ImPlot::GetPlotLimits().X;
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
}
|
||||
|
||||
void Plot::EmitSettingsLimits(int axis) {
|
||||
ImGui::Indent();
|
||||
ImGui::PushID(axis);
|
||||
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
|
||||
ImGui::InputDouble("Min", &m_axisRange[axis].min, 0, 0, "%.3f");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
|
||||
ImGui::InputDouble("Max", &m_axisRange[axis].max, 0, 0, "%.3f");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Apply")) m_axisRange[axis].apply = true;
|
||||
|
||||
ImGui::TextUnformatted("Lock Axis");
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Min##minlock", &m_axisRange[axis].lockMin);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Max##maxlock", &m_axisRange[axis].lockMax);
|
||||
|
||||
ImGui::PopID();
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void Plot::EmitSettings(size_t i) {
|
||||
ImGui::Text("Edit plot name:");
|
||||
ImGui::InputText("##editname", &m_name);
|
||||
ImGui::Checkbox("Visible", &m_visible);
|
||||
ImGui::Checkbox("Show Pause Button", &m_showPause);
|
||||
ImGui::CheckboxFlags("Show Legend", &m_plotFlags, ImPlotFlags_Legend);
|
||||
if (i != 0) ImGui::Checkbox("Lock X-axis to previous plot", &m_lockPrevX);
|
||||
ImGui::TextUnformatted("Primary Y-Axis");
|
||||
EmitSettingsLimits(0);
|
||||
ImGui::CheckboxFlags("2nd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis2);
|
||||
if ((m_plotFlags & ImPlotFlags_YAxis2) != 0) EmitSettingsLimits(1);
|
||||
ImGui::CheckboxFlags("3rd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis3);
|
||||
if ((m_plotFlags & ImPlotFlags_YAxis3) != 0) EmitSettingsLimits(2);
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
||||
ImGui::InputFloat("View Time (s)", &m_viewTime, 0.1f, 1.0f, "%.1f");
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
||||
if (ImGui::InputInt("Height", &m_height, 10)) {
|
||||
if (m_height < 0) m_height = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::Clear() {
|
||||
for (auto&& plot : m_plots) plot->Clear();
|
||||
}
|
||||
|
||||
void PlotView::Display() {
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::Button("Add plot"))
|
||||
m_plots.emplace_back(std::make_unique<Plot>());
|
||||
|
||||
for (size_t i = 0; i < m_plots.size(); ++i) {
|
||||
auto& plot = m_plots[i];
|
||||
ImGui::PushID(i);
|
||||
|
||||
char name[64];
|
||||
if (!plot->GetName().empty())
|
||||
std::snprintf(name, sizeof(name), "%s", plot->GetName().c_str());
|
||||
else
|
||||
std::snprintf(name, sizeof(name), "Plot %d", static_cast<int>(i));
|
||||
|
||||
char label[90];
|
||||
std::snprintf(label, sizeof(label), "%s###header%d", name,
|
||||
static_cast<int>(i));
|
||||
|
||||
bool open = ImGui::CollapsingHeader(label);
|
||||
|
||||
// DND source and target for Plot
|
||||
if (ImGui::BeginDragDropSource()) {
|
||||
PlotSeriesRef ref = {this, i, 0};
|
||||
ImGui::SetDragDropPayload("Plot", &ref, sizeof(ref));
|
||||
ImGui::TextUnformatted(name);
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
plot->DragDropTarget(*this, i, false);
|
||||
|
||||
if (open) {
|
||||
if (ImGui::Button("Move Up")) {
|
||||
if (i > 0) std::swap(m_plots[i - 1], plot);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Move Down")) {
|
||||
if (i < (m_plots.size() - 1)) std::swap(plot, m_plots[i + 1]);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Delete")) {
|
||||
m_plots.erase(m_plots.begin() + i);
|
||||
ImGui::PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
plot->EmitSettings(i);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (m_plots.empty()) {
|
||||
if (ImGui::Button("Add plot"))
|
||||
m_plots.emplace_back(std::make_unique<Plot>());
|
||||
|
||||
// Make "add plot" button a DND target for Plot
|
||||
if (!ImGui::BeginDragDropTarget()) return;
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("Plot")) {
|
||||
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
||||
MovePlot(ref->view, ref->plotIndex, 0);
|
||||
}
|
||||
}
|
||||
|
||||
double now = (wpi::Now() - m_provider->GetStartTime()) * 1.0e-6;
|
||||
for (size_t i = 0; i < m_plots.size(); ++i) {
|
||||
ImGui::PushID(i);
|
||||
m_plots[i]->EmitPlot(*this, now, m_provider->IsPaused(), i);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex) {
|
||||
if (fromView == this) {
|
||||
if (fromIndex == toIndex) return;
|
||||
auto val = std::move(m_plots[fromIndex]);
|
||||
m_plots.insert(m_plots.begin() + toIndex, std::move(val));
|
||||
m_plots.erase(m_plots.begin() + fromIndex + (fromIndex > toIndex ? 1 : 0));
|
||||
} else {
|
||||
auto val = std::move(fromView->m_plots[fromIndex]);
|
||||
m_plots.insert(m_plots.begin() + toIndex, std::move(val));
|
||||
fromView->m_plots.erase(fromView->m_plots.begin() + fromIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
|
||||
size_t fromSeriesIndex, size_t toPlotIndex,
|
||||
size_t toSeriesIndex, int yAxis) {
|
||||
if (fromView == this && fromPlotIndex == toPlotIndex) {
|
||||
// need to handle this specially as the index of the old location changes
|
||||
if (fromSeriesIndex != toSeriesIndex) {
|
||||
auto& plotSeries = m_plots[fromPlotIndex]->m_series;
|
||||
auto val = std::move(plotSeries[fromSeriesIndex]);
|
||||
// only set Y-axis if actually set
|
||||
if (yAxis != -1) val->SetYAxis(yAxis);
|
||||
plotSeries.insert(plotSeries.begin() + toSeriesIndex, std::move(val));
|
||||
plotSeries.erase(plotSeries.begin() + fromSeriesIndex +
|
||||
(fromSeriesIndex > toSeriesIndex ? 1 : 0));
|
||||
}
|
||||
} else {
|
||||
auto& fromPlot = *fromView->m_plots[fromPlotIndex];
|
||||
auto& toPlot = *m_plots[toPlotIndex];
|
||||
// always set Y-axis if moving plots
|
||||
fromPlot.m_series[fromSeriesIndex]->SetYAxis(yAxis == -1 ? 0 : yAxis);
|
||||
toPlot.m_series.insert(toPlot.m_series.begin() + toSeriesIndex,
|
||||
std::move(fromPlot.m_series[fromSeriesIndex]));
|
||||
fromPlot.m_series.erase(fromPlot.m_series.begin() + fromSeriesIndex);
|
||||
}
|
||||
}
|
||||
|
||||
PlotProvider::PlotProvider(const wpi::Twine& iniName)
|
||||
: WindowManager{iniName + "Window"},
|
||||
m_plotSaver{iniName, this, false},
|
||||
m_seriesSaver{iniName + "Series", this, true} {}
|
||||
|
||||
PlotProvider::~PlotProvider() {}
|
||||
|
||||
void PlotProvider::GlobalInit() {
|
||||
WindowManager::GlobalInit();
|
||||
wpi::gui::AddInit([this] {
|
||||
m_plotSaver.Initialize();
|
||||
m_seriesSaver.Initialize();
|
||||
});
|
||||
}
|
||||
|
||||
void PlotProvider::ResetTime() {
|
||||
m_startTime = wpi::Now();
|
||||
for (auto&& window : m_windows) {
|
||||
if (auto view = static_cast<PlotView*>(window->GetView())) {
|
||||
view->Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlotProvider::DisplayMenu() {
|
||||
for (size_t i = 0; i < m_windows.size(); ++i) {
|
||||
m_windows[i]->DisplayMenuItem();
|
||||
// provide method to destroy the plot window
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::Selectable("Destroy Plot Window")) {
|
||||
m_windows.erase(m_windows.begin() + i);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::MenuItem("New Plot Window")) {
|
||||
char id[32];
|
||||
std::snprintf(id, sizeof(id), "Plot <%d>",
|
||||
static_cast<int>(m_windows.size()));
|
||||
AddWindow(id, std::make_unique<PlotView>(this));
|
||||
}
|
||||
}
|
||||
|
||||
void PlotProvider::DisplayWindows() {
|
||||
// create views if not already created
|
||||
for (auto&& window : m_windows) {
|
||||
if (!window->HasView()) window->SetView(std::make_unique<PlotView>(this));
|
||||
}
|
||||
WindowManager::DisplayWindows();
|
||||
}
|
||||
|
||||
PlotProvider::IniSaver::IniSaver(const wpi::Twine& typeName,
|
||||
PlotProvider* provider, bool forSeries)
|
||||
: IniSaverBase{typeName}, m_provider{provider}, m_forSeries{forSeries} {}
|
||||
|
||||
void* PlotProvider::IniSaver::IniReadOpen(const char* name) {
|
||||
auto [viewId, plotNumStr] = wpi::StringRef{name}.split('#');
|
||||
wpi::StringRef seriesId;
|
||||
if (m_forSeries) {
|
||||
std::tie(plotNumStr, seriesId) = plotNumStr.split('#');
|
||||
if (seriesId.empty()) return nullptr;
|
||||
}
|
||||
unsigned int plotNum;
|
||||
if (plotNumStr.getAsInteger(10, plotNum)) return nullptr;
|
||||
|
||||
// get or create window
|
||||
auto win = m_provider->GetOrAddWindow(viewId, true);
|
||||
if (!win) return nullptr;
|
||||
|
||||
// get or create view
|
||||
auto view = static_cast<PlotView*>(win->GetView());
|
||||
if (!view) {
|
||||
win->SetView(std::make_unique<PlotView>(m_provider));
|
||||
view = static_cast<PlotView*>(win->GetView());
|
||||
}
|
||||
|
||||
// get or create plot
|
||||
if (view->m_plots.size() <= plotNum) view->m_plots.resize(plotNum + 1);
|
||||
auto& plot = view->m_plots[plotNum];
|
||||
if (!plot) plot = std::make_unique<Plot>();
|
||||
|
||||
// early exit for plot data
|
||||
if (!m_forSeries) return plot.get();
|
||||
|
||||
// get or create series
|
||||
return plot->m_series.emplace_back(std::make_unique<PlotSeries>(seriesId))
|
||||
.get();
|
||||
}
|
||||
|
||||
void PlotProvider::IniSaver::IniReadLine(void* entry, const char* lineStr) {
|
||||
auto [name, value] = wpi::StringRef{lineStr}.split('=');
|
||||
name = name.trim();
|
||||
value = value.trim();
|
||||
if (m_forSeries)
|
||||
static_cast<PlotSeries*>(entry)->ReadIni(name, value);
|
||||
else
|
||||
static_cast<Plot*>(entry)->ReadIni(name, value);
|
||||
}
|
||||
|
||||
void PlotProvider::IniSaver::IniWriteAll(ImGuiTextBuffer* out_buf) {
|
||||
for (auto&& win : m_provider->m_windows) {
|
||||
auto view = static_cast<PlotView*>(win->GetView());
|
||||
auto id = win->GetId();
|
||||
for (size_t i = 0; i < view->m_plots.size(); ++i) {
|
||||
if (m_forSeries) {
|
||||
// Loop over series
|
||||
for (auto&& series : view->m_plots[i]->m_series) {
|
||||
out_buf->appendf("[%s][%s#%d#%s]\n", GetTypeName(), id.data(),
|
||||
static_cast<int>(i), series->GetId().c_str());
|
||||
series->WriteIni(out_buf);
|
||||
out_buf->append("\n");
|
||||
}
|
||||
} else {
|
||||
// Just the plot
|
||||
out_buf->appendf("[%s][%s#%d]\n", GetTypeName(), id.data(),
|
||||
static_cast<int>(i));
|
||||
view->m_plots[i]->WriteIni(out_buf);
|
||||
out_buf->append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
glass/src/lib/native/cpp/other/StringChooser.cpp
Normal file
44
glass/src/lib/native/cpp/other/StringChooser.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "glass/other/StringChooser.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
using namespace glass;
|
||||
|
||||
void glass::DisplayStringChooser(StringChooserModel* model) {
|
||||
auto& defaultValue = model->GetDefault();
|
||||
auto& selected = model->GetSelected();
|
||||
auto& active = model->GetActive();
|
||||
auto& options = model->GetOptions();
|
||||
|
||||
const char* preview =
|
||||
selected.empty() ? defaultValue.c_str() : selected.c_str();
|
||||
|
||||
const char* label;
|
||||
if (active == preview) {
|
||||
label = "GOOD##select";
|
||||
} else {
|
||||
label = "BAD ##select";
|
||||
}
|
||||
|
||||
if (ImGui::BeginCombo(label, preview)) {
|
||||
for (auto&& option : options) {
|
||||
ImGui::PushID(option.c_str());
|
||||
bool isSelected = (option == selected);
|
||||
if (ImGui::Selectable(option.c_str(), isSelected)) {
|
||||
model->SetSelected(option);
|
||||
}
|
||||
if (isSelected) ImGui::SetItemDefaultFocus();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
}
|
||||
Reference in New Issue
Block a user