mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
[sim] Add plotting to simulation GUI
This commit is contained in:
894
simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp
Normal file
894
simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp
Normal file
@@ -0,0 +1,894 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* 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 "PlotGui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include <hal/simulation/MockHooks.h>
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <implot.h>
|
||||
#include <wpi/Signal.h>
|
||||
#include <wpi/SmallVector.h>
|
||||
#include <wpi/timestamp.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
|
||||
#include "GuiDataSource.h"
|
||||
#include "HALSimGui.h"
|
||||
#include "IniSaverInfo.h"
|
||||
#include "IniSaverVector.h"
|
||||
|
||||
using namespace halsimgui;
|
||||
|
||||
namespace {
|
||||
struct PlotSeriesRef {
|
||||
size_t plotIndex;
|
||||
size_t seriesIndex;
|
||||
};
|
||||
|
||||
class PlotSeries : public NameInfo, public OpenInfo {
|
||||
public:
|
||||
explicit PlotSeries(wpi::StringRef id);
|
||||
explicit PlotSeries(GuiDataSource* source, int yAxis = 0);
|
||||
|
||||
const std::string& GetId() const { return m_id; }
|
||||
|
||||
void CheckSource();
|
||||
|
||||
void SetSource(GuiDataSource* source);
|
||||
GuiDataSource* GetSource() const { return m_source; }
|
||||
|
||||
bool ReadIni(wpi::StringRef name, wpi::StringRef value);
|
||||
void WriteIni(ImGuiTextBuffer* out);
|
||||
|
||||
bool EmitPlot(double now, size_t i, size_t plotIndex);
|
||||
bool EmitSettings(size_t i, size_t plotIndex);
|
||||
bool EmitSettingsDetail(size_t i);
|
||||
void EmitDragDropPayload(size_t i, size_t plotIndex);
|
||||
|
||||
void GetLabel(char* buf, size_t size) 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);
|
||||
|
||||
// source linkage
|
||||
GuiDataSource* m_source = nullptr;
|
||||
wpi::sig::ScopedConnection m_sourceCreatedConn;
|
||||
wpi::sig::ScopedConnection m_newValueConn;
|
||||
std::string m_id;
|
||||
|
||||
// user settings
|
||||
int m_yAxis = 0;
|
||||
ImVec4 m_color = IMPLOT_AUTO_COL;
|
||||
int m_marker = 0;
|
||||
|
||||
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 NameInfo, public OpenInfo {
|
||||
public:
|
||||
Plot();
|
||||
bool ReadIni(wpi::StringRef name, wpi::StringRef value);
|
||||
void WriteIni(ImGuiTextBuffer* out);
|
||||
|
||||
void GetLabel(char* buf, size_t size, int index) const;
|
||||
void GetName(char* buf, size_t size, int index) const;
|
||||
|
||||
void DragDropTarget(size_t i, bool inPlot);
|
||||
void EmitPlot(double now, size_t i);
|
||||
void EmitSettings(size_t i);
|
||||
|
||||
std::vector<std::unique_ptr<PlotSeries>> m_series;
|
||||
|
||||
private:
|
||||
void EmitSettingsLimits(int axis);
|
||||
|
||||
bool m_visible = 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
|
||||
};
|
||||
} // namespace
|
||||
|
||||
static IniSaverVector<Plot> gPlots{"Plot"};
|
||||
|
||||
PlotSeries::PlotSeries(wpi::StringRef id) : m_id(id) {
|
||||
if (GuiDataSource* source = GuiDataSource::Find(id)) {
|
||||
SetSource(source);
|
||||
return;
|
||||
}
|
||||
CheckSource();
|
||||
}
|
||||
|
||||
PlotSeries::PlotSeries(GuiDataSource* source, int yAxis) : m_yAxis(yAxis) {
|
||||
SetSource(source);
|
||||
}
|
||||
|
||||
void PlotSeries::CheckSource() {
|
||||
if (!m_newValueConn.connected() && !m_sourceCreatedConn.connected()) {
|
||||
m_source = nullptr;
|
||||
m_sourceCreatedConn = GuiDataSource::sourceCreated.connect_connection(
|
||||
[this](const char* id, GuiDataSource* source) {
|
||||
if (m_id == id) {
|
||||
SetSource(source);
|
||||
m_sourceCreatedConn.disconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PlotSeries::SetSource(GuiDataSource* 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) { AppendValue(value); });
|
||||
}
|
||||
|
||||
void PlotSeries::AppendValue(double value) {
|
||||
double time = 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 (NameInfo::ReadIni(name, value)) return true;
|
||||
if (OpenInfo::ReadIni(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 == "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) {
|
||||
NameInfo::WriteIni(out);
|
||||
OpenInfo::WriteIni(out);
|
||||
out->appendf(
|
||||
"yAxis=%d\ncolor=%u\nmarker=%d\ndigital=%d\n"
|
||||
"digitalBitHeight=%d\ndigitalBitGap=%d\n",
|
||||
m_yAxis, static_cast<ImU32>(ImColor(m_color)), m_marker, m_digital,
|
||||
m_digitalBitHeight, m_digitalBitGap);
|
||||
}
|
||||
|
||||
void PlotSeries::GetLabel(char* buf, size_t size) const {
|
||||
const char* name = GetName();
|
||||
if (name[0] == '\0' && m_newValueConn.connected()) name = m_source->GetName();
|
||||
if (name[0] == '\0') name = m_id.c_str();
|
||||
std::snprintf(buf, size, "%s###%s", name, m_id.c_str());
|
||||
}
|
||||
|
||||
bool PlotSeries::EmitPlot(double now, size_t i, size_t plotIndex) {
|
||||
CheckSource();
|
||||
|
||||
char label[128];
|
||||
GetLabel(label, sizeof(label));
|
||||
|
||||
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::PushStyleColor(ImPlotCol_Line, m_color);
|
||||
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);
|
||||
}
|
||||
ImPlot::PopStyleColor();
|
||||
|
||||
// DND source for PlotSeries
|
||||
if (ImPlot::BeginLegendDragDropSource(label)) {
|
||||
EmitDragDropPayload(i, plotIndex);
|
||||
ImPlot::EndLegendDragDropSource();
|
||||
}
|
||||
|
||||
// Plot-specific variant of IniSaverInfo::PopupEditName() that also
|
||||
// allows editing of other settings
|
||||
bool rv = false;
|
||||
if (ImPlot::BeginLegendPopup(label)) {
|
||||
if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
|
||||
ImGui::Text("Edit name:");
|
||||
if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
rv = EmitSettingsDetail(i);
|
||||
ImPlot::EndLegendPopup();
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
void PlotSeries::EmitDragDropPayload(size_t i, size_t plotIndex) {
|
||||
PlotSeriesRef ref = {plotIndex, i};
|
||||
ImGui::SetDragDropPayload("PlotSeries", &ref, sizeof(ref));
|
||||
const char* name = GetName();
|
||||
if (name[0] == '\0' && m_newValueConn.connected()) name = m_source->GetName();
|
||||
if (name[0] == '\0') name = m_id.c_str();
|
||||
ImGui::TextUnformatted(name);
|
||||
}
|
||||
|
||||
static void MovePlotSeries(size_t fromPlotIndex, size_t fromSeriesIndex,
|
||||
size_t toPlotIndex, size_t toSeriesIndex,
|
||||
int yAxis = -1) {
|
||||
if (fromPlotIndex == toPlotIndex) {
|
||||
// need to handle this specially as the index of the old location changes
|
||||
if (fromSeriesIndex != toSeriesIndex) {
|
||||
auto& plotSeries = gPlots[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 = gPlots[fromPlotIndex];
|
||||
auto& toPlot = gPlots[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);
|
||||
}
|
||||
}
|
||||
|
||||
bool PlotSeries::EmitSettings(size_t i, size_t plotIndex) {
|
||||
char label[128];
|
||||
GetLabel(label, sizeof(label));
|
||||
|
||||
bool open = ImGui::CollapsingHeader(
|
||||
label, IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0);
|
||||
|
||||
// DND source for PlotSeries
|
||||
if (ImGui::BeginDragDropSource()) {
|
||||
EmitDragDropPayload(i, plotIndex);
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// If another PlotSeries is dropped, move it before this series
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload =
|
||||
ImGui::AcceptDragDropPayload("PlotSeries")) {
|
||||
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
||||
MovePlotSeries(ref->plotIndex, ref->seriesIndex, plotIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
SetOpen(open);
|
||||
PopupEditName(i);
|
||||
if (!open) return false;
|
||||
|
||||
return EmitSettingsDetail(i);
|
||||
}
|
||||
|
||||
bool PlotSeries::EmitSettingsDetail(size_t i) {
|
||||
if (ImGui::Button("Delete")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Line color
|
||||
{
|
||||
ImGui::ColorEdit3("Color", &m_color.x, ImGuiColorEditFlags_NoInputs);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Default")) m_color = ImPlot::GetColormapColor(i);
|
||||
}
|
||||
|
||||
// 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]));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Plot::Plot() {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
m_axisRange[i] = PlotRange{};
|
||||
}
|
||||
}
|
||||
|
||||
bool Plot::ReadIni(wpi::StringRef name, wpi::StringRef value) {
|
||||
if (NameInfo::ReadIni(name, value)) return true;
|
||||
if (OpenInfo::ReadIni(name, value)) return true;
|
||||
if (name == "visible") {
|
||||
int num;
|
||||
if (value.getAsInteger(10, num)) return true;
|
||||
m_visible = 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) {
|
||||
NameInfo::WriteIni(out);
|
||||
OpenInfo::WriteIni(out);
|
||||
out->appendf(
|
||||
"visible=%d\nlockPrevX=%d\nlegend=%d\nyaxis2=%d\nyaxis3=%d\n"
|
||||
"viewTime=%d\nheight=%d\n",
|
||||
m_visible ? 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::GetLabel(char* buf, size_t size, int index) const {
|
||||
const char* name = NameInfo::GetName();
|
||||
if (name[0] != '\0') {
|
||||
std::snprintf(buf, size, "%s##Plot%d", name, index);
|
||||
} else {
|
||||
std::snprintf(buf, size, "Plot %d##Plot%d", index, index);
|
||||
}
|
||||
}
|
||||
|
||||
void Plot::GetName(char* buf, size_t size, int index) const {
|
||||
const char* name = NameInfo::GetName();
|
||||
if (name[0] != '\0') {
|
||||
std::snprintf(buf, size, "%s", name);
|
||||
} else {
|
||||
std::snprintf(buf, size, "Plot %d", index);
|
||||
}
|
||||
}
|
||||
|
||||
void Plot::DragDropTarget(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<GuiDataSource**>(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);
|
||||
MovePlotSeries(ref->plotIndex, ref->seriesIndex, i, m_series.size(), yAxis);
|
||||
} else if (const ImGuiPayload* payload =
|
||||
ImGui::AcceptDragDropPayload("Plot")) {
|
||||
auto fromPlotIndex = *static_cast<const size_t*>(payload->Data);
|
||||
if (i != fromPlotIndex) {
|
||||
auto val = std::move(gPlots[fromPlotIndex]);
|
||||
gPlots.insert(gPlots.begin() + i, std::move(val));
|
||||
gPlots.erase(gPlots.begin() + fromPlotIndex +
|
||||
(fromPlotIndex > i ? 1 : 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Plot::EmitPlot(double now, size_t i) {
|
||||
if (!m_visible) return;
|
||||
|
||||
bool lockX = (i != 0 && m_lockPrevX);
|
||||
|
||||
if (!lockX && ImGui::Button(m_paused ? "Resume" : "Pause"))
|
||||
m_paused = !m_paused;
|
||||
|
||||
char label[128];
|
||||
GetLabel(label, sizeof(label), i);
|
||||
|
||||
if (lockX) {
|
||||
ImPlot::SetNextPlotLimitsX(gPlots[i - 1].m_xaxisRange.Min,
|
||||
gPlots[i - 1].m_xaxisRange.Max,
|
||||
ImGuiCond_Always);
|
||||
} else {
|
||||
// also force-pause plots if overall timing is paused
|
||||
ImPlot::SetNextPlotLimitsX(now - m_viewTime, now,
|
||||
(m_paused || HALSIM_IsTimingPaused())
|
||||
? 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) {
|
||||
if (m_series[j]->EmitPlot(now, j, i)) {
|
||||
m_series.erase(m_series.begin() + j);
|
||||
}
|
||||
}
|
||||
DragDropTarget(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();
|
||||
}
|
||||
|
||||
// Delete button (X in circle), based on ImGui::CloseButton()
|
||||
static bool DeleteButton(ImGuiID id, const ImVec2& pos) {
|
||||
ImGuiContext& g = *GImGui;
|
||||
ImGuiWindow* window = g.CurrentWindow;
|
||||
|
||||
// We intentionally allow interaction when clipped so that a mechanical
|
||||
// Alt,Right,Validate sequence close a window. (this isn't the regular
|
||||
// behavior of buttons, but it doesn't affect the user much because navigation
|
||||
// tends to keep items visible).
|
||||
const ImRect bb(
|
||||
pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f);
|
||||
bool is_clipped = !ImGui::ItemAdd(bb, id);
|
||||
|
||||
bool hovered, held;
|
||||
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held);
|
||||
if (is_clipped) return pressed;
|
||||
|
||||
// Render
|
||||
ImU32 col =
|
||||
ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
|
||||
ImVec2 center = bb.GetCenter();
|
||||
if (hovered)
|
||||
window->DrawList->AddCircleFilled(
|
||||
center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), col, 12);
|
||||
|
||||
ImU32 cross_col = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
window->DrawList->AddCircle(center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f),
|
||||
cross_col, 12);
|
||||
float cross_extent = g.FontSize * 0.5f * 0.5f - 1.0f;
|
||||
center -= ImVec2(0.5f, 0.5f);
|
||||
window->DrawList->AddLine(center + ImVec2(+cross_extent, +cross_extent),
|
||||
center + ImVec2(-cross_extent, -cross_extent),
|
||||
cross_col, 1.0f);
|
||||
window->DrawList->AddLine(center + ImVec2(+cross_extent, -cross_extent),
|
||||
center + ImVec2(-cross_extent, +cross_extent),
|
||||
cross_col, 1.0f);
|
||||
|
||||
return pressed;
|
||||
}
|
||||
|
||||
void Plot::EmitSettings(size_t i) {
|
||||
char label[128];
|
||||
GetLabel(label, sizeof(label), i);
|
||||
|
||||
bool open = ImGui::CollapsingHeader(
|
||||
label, ImGuiTreeNodeFlags_AllowItemOverlap |
|
||||
ImGuiTreeNodeFlags_ClipLabelForTrailingButton |
|
||||
(IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0));
|
||||
|
||||
{
|
||||
// Create a small overlapping delete button
|
||||
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
||||
ImGuiContext& g = *GImGui;
|
||||
ImGuiItemHoveredDataBackup last_item_backup;
|
||||
ImGuiID id = window->GetID(label);
|
||||
float button_size = g.FontSize;
|
||||
float button_x = ImMax(window->DC.LastItemRect.Min.x,
|
||||
window->DC.LastItemRect.Max.x -
|
||||
g.Style.FramePadding.x * 2.0f - button_size);
|
||||
float button_y = window->DC.LastItemRect.Min.y;
|
||||
if (DeleteButton(window->GetID(reinterpret_cast<void*>(
|
||||
static_cast<intptr_t>(id) + 1)),
|
||||
ImVec2(button_x, button_y))) {
|
||||
gPlots.erase(gPlots.begin() + i);
|
||||
return;
|
||||
}
|
||||
last_item_backup.Restore();
|
||||
}
|
||||
|
||||
// DND source for Plot
|
||||
if (ImGui::BeginDragDropSource()) {
|
||||
ImGui::SetDragDropPayload("Plot", &i, sizeof(i));
|
||||
char name[64];
|
||||
GetName(name, sizeof(name), i);
|
||||
ImGui::TextUnformatted(name);
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
DragDropTarget(i, false);
|
||||
SetOpen(open);
|
||||
PopupEditName(i);
|
||||
if (!open) return;
|
||||
ImGui::PushID(i);
|
||||
#if 0
|
||||
if (ImGui::Button("Move Up") && i > 0) {
|
||||
std::swap(gPlots[i - 1], gPlots[i]);
|
||||
ImGui::PopID();
|
||||
return;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Move Down") && i < (gPlots.size() - 1)) {
|
||||
std::swap(gPlots[i], gPlots[i + 1]);
|
||||
ImGui::PopID();
|
||||
return;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Delete")) {
|
||||
gPlots.erase(gPlots.begin() + i);
|
||||
ImGui::PopID();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
ImGui::Checkbox("Visible", &m_visible);
|
||||
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;
|
||||
}
|
||||
|
||||
ImGui::Indent();
|
||||
for (size_t j = 0; j < m_series.size(); ++j) {
|
||||
ImGui::PushID(j);
|
||||
if (m_series[j]->EmitSettings(j, i)) {
|
||||
m_series.erase(m_series.begin() + j);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::Unindent();
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
static void DisplayPlot() {
|
||||
if (gPlots.empty()) {
|
||||
ImGui::Text("No Plots");
|
||||
return;
|
||||
}
|
||||
double now = wpi::Now() * 1.0e-6;
|
||||
for (size_t i = 0; i < gPlots.size(); ++i) {
|
||||
ImGui::PushID(i);
|
||||
gPlots[i].EmitPlot(now, i);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::Text("(Right double click for more settings)");
|
||||
}
|
||||
|
||||
static void DisplayPlotSettings() {
|
||||
if (ImGui::Button("Add new plot")) {
|
||||
gPlots.emplace_back();
|
||||
}
|
||||
for (size_t i = 0; i < gPlots.size(); ++i) {
|
||||
gPlots[i].EmitSettings(i);
|
||||
}
|
||||
}
|
||||
|
||||
static void* PlotSeries_ReadOpen(ImGuiContext* ctx,
|
||||
ImGuiSettingsHandler* handler,
|
||||
const char* name) {
|
||||
wpi::StringRef plotIndexStr, id;
|
||||
std::tie(plotIndexStr, id) = wpi::StringRef{name}.split(',');
|
||||
unsigned int plotIndex;
|
||||
if (plotIndexStr.getAsInteger(10, plotIndex)) return nullptr;
|
||||
if (plotIndex >= gPlots.size()) gPlots.resize(plotIndex + 1);
|
||||
auto& plot = gPlots[plotIndex];
|
||||
auto it = std::find_if(
|
||||
plot.m_series.begin(), plot.m_series.end(),
|
||||
[&](const auto& elem) { return elem && elem->GetId() == id; });
|
||||
if (it != plot.m_series.end()) return it->get();
|
||||
return plot.m_series.emplace_back(std::make_unique<PlotSeries>(id)).get();
|
||||
}
|
||||
|
||||
static void PlotSeries_ReadLine(ImGuiContext* ctx,
|
||||
ImGuiSettingsHandler* handler, void* entry,
|
||||
const char* lineStr) {
|
||||
auto element = static_cast<PlotSeries*>(entry);
|
||||
wpi::StringRef line{lineStr};
|
||||
auto [name, value] = line.split('=');
|
||||
name = name.trim();
|
||||
value = value.trim();
|
||||
element->ReadIni(name, value);
|
||||
}
|
||||
|
||||
static void PlotSeries_WriteAll(ImGuiContext* ctx,
|
||||
ImGuiSettingsHandler* handler,
|
||||
ImGuiTextBuffer* out_buf) {
|
||||
for (size_t i = 0; i < gPlots.size(); ++i) {
|
||||
for (const auto& series : gPlots[i].m_series) {
|
||||
out_buf->appendf("[PlotSeries][%d,%s]\n", static_cast<int>(i),
|
||||
series->GetId().c_str());
|
||||
series->WriteIni(out_buf);
|
||||
out_buf->append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlotGui::Initialize() {
|
||||
gPlots.Initialize();
|
||||
|
||||
// hook ini handler for PlotSeries to save settings
|
||||
ImGuiSettingsHandler iniHandler;
|
||||
iniHandler.TypeName = "PlotSeries";
|
||||
iniHandler.TypeHash = ImHashStr("PlotSeries");
|
||||
iniHandler.ReadOpenFn = PlotSeries_ReadOpen;
|
||||
iniHandler.ReadLineFn = PlotSeries_ReadLine;
|
||||
iniHandler.WriteAllFn = PlotSeries_WriteAll;
|
||||
ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
|
||||
|
||||
// HALSimGui::AddExecute([] { ImPlot::ShowDemoWindow(); });
|
||||
HALSimGui::AddWindow("Plot", DisplayPlot);
|
||||
HALSimGui::SetDefaultWindowPos("Plot", 600, 75);
|
||||
HALSimGui::SetDefaultWindowSize("Plot", 300, 200);
|
||||
|
||||
HALSimGui::AddWindow("Plot Settings", DisplayPlotSettings);
|
||||
HALSimGui::SetDefaultWindowPos("Plot Settings", 902, 75);
|
||||
HALSimGui::SetDefaultWindowSize("Plot Settings", 120, 200);
|
||||
}
|
||||
Reference in New Issue
Block a user