[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:
Peter Johnson
2020-09-12 10:55:46 -07:00
parent 727940d847
commit 2a5ca77454
151 changed files with 10386 additions and 4565 deletions

View 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);
}

View 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();
}

View 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());
}

View 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");
}
}
}
}

View 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();
}