2020-12-26 14:31:24 -08:00
|
|
|
// Copyright (c) FIRST and other WPILib contributors.
|
|
|
|
|
// Open Source Software; you can modify and/or share it under the terms of
|
|
|
|
|
// the WPILib BSD license file in the root directory of this project.
|
2020-08-14 20:02:35 -07:00
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
#include "glass/other/Plot.h"
|
|
|
|
|
|
|
|
|
|
#include <stdint.h>
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <atomic>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <memory>
|
2020-09-12 10:55:46 -07:00
|
|
|
#include <string>
|
2020-08-14 20:02:35 -07:00
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
#define IMGUI_DEFINE_MATH_OPERATORS
|
|
|
|
|
#include <imgui.h>
|
|
|
|
|
#include <imgui_internal.h>
|
2020-09-12 10:55:46 -07:00
|
|
|
#include <imgui_stdlib.h>
|
2020-08-14 20:02:35 -07:00
|
|
|
#include <implot.h>
|
2020-09-12 10:55:46 -07:00
|
|
|
#include <wpigui.h>
|
2020-08-14 20:02:35 -07:00
|
|
|
#include <wpi/Signal.h>
|
2020-09-12 10:55:46 -07:00
|
|
|
#include <wpi/SmallString.h>
|
2020-08-14 20:02:35 -07:00
|
|
|
#include <wpi/SmallVector.h>
|
|
|
|
|
#include <wpi/timestamp.h>
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
#include "glass/Context.h"
|
|
|
|
|
#include "glass/DataSource.h"
|
|
|
|
|
#include "glass/support/ExtraGuiWidgets.h"
|
2020-08-14 20:02:35 -07:00
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
using namespace glass;
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
namespace {
|
2020-09-12 10:55:46 -07:00
|
|
|
class PlotView;
|
|
|
|
|
|
2020-08-14 20:02:35 -07:00
|
|
|
struct PlotSeriesRef {
|
2020-09-12 10:55:46 -07:00
|
|
|
PlotView* view;
|
2020-08-14 20:02:35 -07:00
|
|
|
size_t plotIndex;
|
|
|
|
|
size_t seriesIndex;
|
|
|
|
|
};
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
class PlotSeries {
|
2020-08-14 20:02:35 -07:00
|
|
|
public:
|
|
|
|
|
explicit PlotSeries(wpi::StringRef id);
|
2020-09-12 10:55:46 -07:00
|
|
|
explicit PlotSeries(DataSource* source, int yAxis = 0);
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
const std::string& GetId() const { return m_id; }
|
|
|
|
|
|
|
|
|
|
void CheckSource();
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void SetSource(DataSource* source);
|
|
|
|
|
DataSource* GetSource() const { return m_source; }
|
|
|
|
|
|
2020-08-14 20:02:35 -07:00
|
|
|
bool ReadIni(wpi::StringRef name, wpi::StringRef value);
|
|
|
|
|
void WriteIni(ImGuiTextBuffer* out);
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
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);
|
2020-08-14 20:02:35 -07:00
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
const char* GetName() const;
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
void AppendValue(double value, uint64_t time);
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
// source linkage
|
2020-09-12 10:55:46 -07:00
|
|
|
DataSource* m_source = nullptr;
|
2020-08-14 20:02:35 -07:00
|
|
|
wpi::sig::ScopedConnection m_sourceCreatedConn;
|
|
|
|
|
wpi::sig::ScopedConnection m_newValueConn;
|
|
|
|
|
std::string m_id;
|
|
|
|
|
|
|
|
|
|
// user settings
|
2020-09-12 10:55:46 -07:00
|
|
|
std::string m_name;
|
2020-08-14 20:02:35 -07:00
|
|
|
int m_yAxis = 0;
|
|
|
|
|
ImVec4 m_color = IMPLOT_AUTO_COL;
|
|
|
|
|
int m_marker = 0;
|
2020-09-12 10:55:46 -07:00
|
|
|
float m_weight = IMPLOT_AUTO;
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
};
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
class Plot {
|
2020-08-14 20:02:35 -07:00
|
|
|
public:
|
|
|
|
|
Plot();
|
2020-09-12 10:55:46 -07:00
|
|
|
|
2020-08-14 20:02:35 -07:00
|
|
|
bool ReadIni(wpi::StringRef name, wpi::StringRef value);
|
|
|
|
|
void WriteIni(ImGuiTextBuffer* out);
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void DragDropTarget(PlotView& view, size_t i, bool inPlot);
|
|
|
|
|
void EmitPlot(PlotView& view, double now, bool paused, size_t i);
|
2020-08-14 20:02:35 -07:00
|
|
|
void EmitSettings(size_t i);
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
const std::string& GetName() const { return m_name; }
|
|
|
|
|
|
2020-08-14 20:02:35 -07:00
|
|
|
std::vector<std::unique_ptr<PlotSeries>> m_series;
|
|
|
|
|
|
2021-02-21 16:38:06 -08:00
|
|
|
// Returns base height; does not include actual plot height if auto-sized.
|
|
|
|
|
int GetAutoBaseHeight(bool* isAuto, size_t i);
|
|
|
|
|
|
|
|
|
|
void SetAutoHeight(int height) {
|
|
|
|
|
if (m_autoHeight) {
|
|
|
|
|
m_height = height;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-14 20:02:35 -07:00
|
|
|
private:
|
|
|
|
|
void EmitSettingsLimits(int axis);
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
std::string m_name;
|
2020-08-14 20:02:35 -07:00
|
|
|
bool m_visible = true;
|
2020-09-12 10:55:46 -07:00
|
|
|
bool m_showPause = true;
|
2021-01-08 01:07:48 -08:00
|
|
|
unsigned int m_plotFlags = ImPlotFlags_None;
|
2020-08-14 20:02:35 -07:00
|
|
|
bool m_lockPrevX = false;
|
|
|
|
|
bool m_paused = false;
|
|
|
|
|
float m_viewTime = 10;
|
2021-02-21 16:38:06 -08:00
|
|
|
bool m_autoHeight = true;
|
2020-08-14 20:02:35 -07:00
|
|
|
int m_height = 300;
|
|
|
|
|
struct PlotRange {
|
|
|
|
|
double min = 0;
|
|
|
|
|
double max = 1;
|
|
|
|
|
bool lockMin = false;
|
|
|
|
|
bool lockMax = false;
|
|
|
|
|
bool apply = false;
|
|
|
|
|
};
|
2021-01-08 01:10:20 -08:00
|
|
|
std::string m_axisLabel[3];
|
2020-08-14 20:02:35 -07:00
|
|
|
PlotRange m_axisRange[3];
|
|
|
|
|
ImPlotRange m_xaxisRange; // read from plot, used for lockPrevX
|
|
|
|
|
};
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
class PlotView : public View {
|
|
|
|
|
public:
|
|
|
|
|
explicit PlotView(PlotProvider* provider) : m_provider{provider} {}
|
|
|
|
|
|
|
|
|
|
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
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
PlotSeries::PlotSeries(wpi::StringRef id) : m_id(id) {
|
2020-09-12 10:55:46 -07:00
|
|
|
if (DataSource* source = DataSource::Find(id)) {
|
2020-08-14 20:02:35 -07:00
|
|
|
SetSource(source);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
CheckSource();
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
PlotSeries::PlotSeries(DataSource* source, int yAxis) : m_yAxis(yAxis) {
|
2020-08-14 20:02:35 -07:00
|
|
|
SetSource(source);
|
2021-03-21 11:14:25 -07:00
|
|
|
m_id = source->GetId();
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PlotSeries::CheckSource() {
|
|
|
|
|
if (!m_newValueConn.connected() && !m_sourceCreatedConn.connected()) {
|
|
|
|
|
m_source = nullptr;
|
2020-09-12 10:55:46 -07:00
|
|
|
m_sourceCreatedConn = DataSource::sourceCreated.connect_connection(
|
|
|
|
|
[this](const char* id, DataSource* source) {
|
2020-08-14 20:02:35 -07:00
|
|
|
if (m_id == id) {
|
|
|
|
|
SetSource(source);
|
|
|
|
|
m_sourceCreatedConn.disconnect();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void PlotSeries::SetSource(DataSource* source) {
|
2020-08-14 20:02:35 -07:00
|
|
|
m_source = source;
|
|
|
|
|
|
|
|
|
|
// add initial value
|
|
|
|
|
m_data[m_size++] = ImPlotPoint{wpi::Now() * 1.0e-6, source->GetValue()};
|
|
|
|
|
|
|
|
|
|
m_newValueConn = source->valueChanged.connect_connection(
|
2020-09-12 10:55:46 -07:00
|
|
|
[this](double value, uint64_t time) { AppendValue(value, time); });
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void PlotSeries::AppendValue(double value, uint64_t timeUs) {
|
|
|
|
|
double time = (timeUs != 0 ? timeUs : wpi::Now()) * 1.0e-6;
|
2020-08-14 20:02:35 -07:00
|
|
|
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) {
|
2020-09-12 10:55:46 -07:00
|
|
|
if (name == "name") {
|
|
|
|
|
m_name = value;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
if (name == "yAxis") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_yAxis = num;
|
|
|
|
|
return true;
|
|
|
|
|
} else if (name == "color") {
|
|
|
|
|
unsigned int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_color = ImColor(num);
|
|
|
|
|
return true;
|
|
|
|
|
} else if (name == "marker") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_marker = num;
|
|
|
|
|
return true;
|
2020-09-12 10:55:46 -07:00
|
|
|
} else if (name == "weight") {
|
|
|
|
|
std::sscanf(value.data(), "%f", &m_weight);
|
|
|
|
|
return true;
|
2020-08-14 20:02:35 -07:00
|
|
|
} else if (name == "digital") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_digital = num;
|
|
|
|
|
return true;
|
|
|
|
|
} else if (name == "digitalBitHeight") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_digitalBitHeight = num;
|
|
|
|
|
return true;
|
|
|
|
|
} else if (name == "digitalBitGap") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_digitalBitGap = num;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PlotSeries::WriteIni(ImGuiTextBuffer* out) {
|
|
|
|
|
out->appendf(
|
2020-09-12 10:55:46 -07:00
|
|
|
"name=%s\nyAxis=%d\ncolor=%u\nmarker=%d\nweight=%f\ndigital=%d\n"
|
2020-08-14 20:02:35 -07:00
|
|
|
"digitalBitHeight=%d\ndigitalBitGap=%d\n",
|
2020-09-12 10:55:46 -07:00
|
|
|
m_name.c_str(), m_yAxis, static_cast<ImU32>(ImColor(m_color)), m_marker,
|
|
|
|
|
m_weight, m_digital, m_digitalBitHeight, m_digitalBitGap);
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
const char* PlotSeries::GetName() const {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!m_name.empty()) {
|
|
|
|
|
return m_name.c_str();
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
if (m_newValueConn.connected()) {
|
|
|
|
|
auto sourceName = m_source->GetName();
|
2020-12-28 12:58:06 -08:00
|
|
|
if (sourceName[0] != '\0') {
|
|
|
|
|
return sourceName;
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
}
|
|
|
|
|
return m_id.c_str();
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
PlotSeries::Action PlotSeries::EmitPlot(PlotView& view, double now, size_t i,
|
|
|
|
|
size_t plotIndex) {
|
2020-08-14 20:02:35 -07:00
|
|
|
CheckSource();
|
|
|
|
|
|
|
|
|
|
char label[128];
|
2020-09-12 10:55:46 -07:00
|
|
|
std::snprintf(label, sizeof(label), "%s###name", GetName());
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
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;
|
2020-12-17 07:27:29 -08:00
|
|
|
double zeroTime;
|
2020-08-14 20:02:35 -07:00
|
|
|
ImPlotPoint* data;
|
|
|
|
|
int size;
|
|
|
|
|
int offset;
|
|
|
|
|
};
|
2020-12-17 07:27:29 -08:00
|
|
|
GetterData getterData = {now, GetZeroTime() * 1.0e-6, m_data, size, offset};
|
2020-08-14 20:02:35 -07:00
|
|
|
auto getter = [](void* data, int idx) {
|
|
|
|
|
auto d = static_cast<GetterData*>(data);
|
2020-12-28 12:58:06 -08:00
|
|
|
if (idx == d->size) {
|
2020-08-14 20:02:35 -07:00
|
|
|
return ImPlotPoint{
|
2020-12-17 07:27:29 -08:00
|
|
|
d->now - d->zeroTime,
|
|
|
|
|
d->data[d->offset == 0 ? d->size - 1 : d->offset - 1].y};
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-12-17 07:27:29 -08:00
|
|
|
ImPlotPoint* point;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (d->offset + idx < d->size) {
|
2020-12-17 07:27:29 -08:00
|
|
|
point = &d->data[d->offset + idx];
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2020-12-17 07:27:29 -08:00
|
|
|
point = &d->data[d->offset + idx - d->size];
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-12-17 07:27:29 -08:00
|
|
|
return ImPlotPoint{point->x - d->zeroTime, point->y};
|
2020-08-14 20:02:35 -07:00
|
|
|
};
|
|
|
|
|
|
2020-12-28 12:58:06 -08:00
|
|
|
if (m_color.w == IMPLOT_AUTO_COL.w) {
|
|
|
|
|
m_color = ImPlot::GetColormapColor(i);
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
ImPlot::SetNextLineStyle(m_color, m_weight);
|
2020-08-14 20:02:35 -07:00
|
|
|
if (IsDigital()) {
|
|
|
|
|
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight);
|
|
|
|
|
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap);
|
2021-01-08 01:07:48 -08:00
|
|
|
ImPlot::PlotDigitalG(label, getter, &getterData, size + 1);
|
2020-08-14 20:02:35 -07:00
|
|
|
ImPlot::PopStyleVar();
|
|
|
|
|
ImPlot::PopStyleVar();
|
|
|
|
|
} else {
|
|
|
|
|
ImPlot::SetPlotYAxis(m_yAxis);
|
|
|
|
|
ImPlot::SetNextMarkerStyle(m_marker - 1);
|
2021-01-08 01:07:48 -08:00
|
|
|
ImPlot::PlotLineG(label, getter, &getterData, size + 1);
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DND source for PlotSeries
|
2021-05-10 18:59:14 -07:00
|
|
|
if (ImPlot::BeginDragDropSourceItem(label)) {
|
2020-09-12 10:55:46 -07:00
|
|
|
EmitDragDropPayload(view, i, plotIndex);
|
2021-05-10 18:59:14 -07:00
|
|
|
ImPlot::EndDragDropSource();
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2021-03-20 20:59:31 -07:00
|
|
|
// Show full source name tooltip
|
|
|
|
|
if (!m_name.empty() && ImPlot::IsLegendEntryHovered(label)) {
|
|
|
|
|
ImGui::SetTooltip("%s", m_id.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
// Edit settings via popup
|
|
|
|
|
Action rv = kNone;
|
2020-08-14 20:02:35 -07:00
|
|
|
if (ImPlot::BeginLegendPopup(label)) {
|
2021-03-20 20:59:31 -07:00
|
|
|
ImGui::TextUnformatted(m_id.c_str());
|
2020-12-28 12:58:06 -08:00
|
|
|
if (ImGui::Button("Close")) {
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
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")) {
|
2020-08-14 20:02:35 -07:00
|
|
|
ImGui::CloseCurrentPopup();
|
2020-09-12 10:55:46 -07:00
|
|
|
rv = kDelete;
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
EmitSettings(i);
|
2020-08-14 20:02:35 -07:00
|
|
|
ImPlot::EndLegendPopup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void PlotSeries::EmitDragDropPayload(PlotView& view, size_t i,
|
|
|
|
|
size_t plotIndex) {
|
|
|
|
|
PlotSeriesRef ref = {&view, plotIndex, i};
|
2020-08-14 20:02:35 -07:00
|
|
|
ImGui::SetDragDropPayload("PlotSeries", &ref, sizeof(ref));
|
2020-09-12 10:55:46 -07:00
|
|
|
ImGui::TextUnformatted(GetName());
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void PlotSeries::EmitSettings(size_t i) {
|
2020-08-14 20:02:35 -07:00
|
|
|
// Line color
|
|
|
|
|
{
|
|
|
|
|
ImGui::ColorEdit3("Color", &m_color.x, ImGuiColorEditFlags_NoInputs);
|
|
|
|
|
ImGui::SameLine();
|
2020-12-28 12:58:06 -08:00
|
|
|
if (ImGui::Button("Default")) {
|
|
|
|
|
m_color = ImPlot::GetColormapColor(i);
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
// Line weight
|
|
|
|
|
{
|
|
|
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
|
|
|
|
ImGui::InputFloat("Weight", &m_weight, 0.1f, 1.0f, "%.1f");
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-14 20:02:35 -07:00
|
|
|
// 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) {
|
2020-09-12 10:55:46 -07:00
|
|
|
if (name == "name") {
|
|
|
|
|
m_name = value;
|
|
|
|
|
return true;
|
|
|
|
|
} else if (name == "visible") {
|
2020-08-14 20:02:35 -07:00
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_visible = num != 0;
|
|
|
|
|
return true;
|
2020-09-12 10:55:46 -07:00
|
|
|
} else if (name == "showPause") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
m_showPause = num != 0;
|
|
|
|
|
return true;
|
2020-08-14 20:02:35 -07:00
|
|
|
} else if (name == "lockPrevX") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_lockPrevX = num != 0;
|
|
|
|
|
return true;
|
|
|
|
|
} else if (name == "legend") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (num == 0) {
|
2021-01-08 01:07:48 -08:00
|
|
|
m_plotFlags |= ImPlotFlags_NoLegend;
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2021-01-08 01:07:48 -08:00
|
|
|
m_plotFlags &= ~ImPlotFlags_NoLegend;
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
return true;
|
|
|
|
|
} else if (name == "yaxis2") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (num == 0) {
|
2020-08-14 20:02:35 -07:00
|
|
|
m_plotFlags &= ~ImPlotFlags_YAxis2;
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2020-08-14 20:02:35 -07:00
|
|
|
m_plotFlags |= ImPlotFlags_YAxis2;
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
return true;
|
|
|
|
|
} else if (name == "yaxis3") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (num == 0) {
|
2020-08-14 20:02:35 -07:00
|
|
|
m_plotFlags &= ~ImPlotFlags_YAxis3;
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2020-08-14 20:02:35 -07:00
|
|
|
m_plotFlags |= ImPlotFlags_YAxis3;
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
return true;
|
|
|
|
|
} else if (name == "viewTime") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_viewTime = num / 1000.0;
|
|
|
|
|
return true;
|
2021-02-21 16:38:06 -08:00
|
|
|
} else if (name == "autoHeight") {
|
|
|
|
|
int num;
|
|
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
m_autoHeight = num != 0;
|
|
|
|
|
return true;
|
2020-08-14 20:02:35 -07:00
|
|
|
} else if (name == "height") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_height = num;
|
|
|
|
|
return true;
|
|
|
|
|
} else if (name.startswith("y")) {
|
|
|
|
|
auto [yAxisStr, yName] = name.split('_');
|
|
|
|
|
int yAxis;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (yAxisStr.substr(1).getAsInteger(10, yAxis)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (yAxis < 0 || yAxis > 3) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
if (yName == "min") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_axisRange[yAxis].min = num / 1000.0;
|
|
|
|
|
return true;
|
|
|
|
|
} else if (yName == "max") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_axisRange[yAxis].max = num / 1000.0;
|
|
|
|
|
return true;
|
|
|
|
|
} else if (yName == "lockMin") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_axisRange[yAxis].lockMin = num != 0;
|
|
|
|
|
return true;
|
|
|
|
|
} else if (yName == "lockMax") {
|
|
|
|
|
int num;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (value.getAsInteger(10, num)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
m_axisRange[yAxis].lockMax = num != 0;
|
|
|
|
|
return true;
|
2021-01-08 01:10:20 -08:00
|
|
|
} else if (yName == "label") {
|
|
|
|
|
m_axisLabel[yAxis] = value;
|
|
|
|
|
return true;
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Plot::WriteIni(ImGuiTextBuffer* out) {
|
|
|
|
|
out->appendf(
|
2020-09-12 10:55:46 -07:00
|
|
|
"name=%s\nvisible=%d\nshowPause=%d\nlockPrevX=%d\nlegend=%d\n"
|
2021-02-21 16:38:06 -08:00
|
|
|
"yaxis2=%d\nyaxis3=%d\nviewTime=%d\nautoHeight=%d\nheight=%d\n",
|
2020-09-12 10:55:46 -07:00
|
|
|
m_name.c_str(), m_visible ? 1 : 0, m_showPause ? 1 : 0,
|
2021-01-08 01:07:48 -08:00
|
|
|
m_lockPrevX ? 1 : 0, (m_plotFlags & ImPlotFlags_NoLegend) ? 0 : 1,
|
2020-08-14 20:02:35 -07:00
|
|
|
(m_plotFlags & ImPlotFlags_YAxis2) ? 1 : 0,
|
|
|
|
|
(m_plotFlags & ImPlotFlags_YAxis3) ? 1 : 0,
|
2021-02-21 16:38:06 -08:00
|
|
|
static_cast<int>(m_viewTime * 1000), m_autoHeight ? 1 : 0, m_height);
|
2020-08-14 20:02:35 -07:00
|
|
|
for (int i = 0; i < 3; ++i) {
|
2021-01-08 01:10:20 -08:00
|
|
|
out->appendf(
|
|
|
|
|
"y%d_min=%d\ny%d_max=%d\ny%d_lockMin=%d\ny%d_lockMax=%d\n"
|
|
|
|
|
"y%d_label=%s\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, i,
|
|
|
|
|
m_axisLabel[i].c_str());
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void Plot::DragDropTarget(PlotView& view, size_t i, bool inPlot) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!ImGui::BeginDragDropTarget()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
// 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")) {
|
2020-09-12 10:55:46 -07:00
|
|
|
auto source = *static_cast<DataSource**>(payload->Data);
|
2020-08-14 20:02:35 -07:00
|
|
|
// 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);
|
2020-09-12 10:55:46 -07:00
|
|
|
view.MovePlotSeries(ref->view, ref->plotIndex, ref->seriesIndex, i,
|
|
|
|
|
m_series.size(), yAxis);
|
2020-08-14 20:02:35 -07:00
|
|
|
} else if (const ImGuiPayload* payload =
|
|
|
|
|
ImGui::AcceptDragDropPayload("Plot")) {
|
2020-09-12 10:55:46 -07:00
|
|
|
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
|
|
|
|
view.MovePlot(ref->view, ref->plotIndex, i);
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void Plot::EmitPlot(PlotView& view, double now, bool paused, size_t i) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!m_visible) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
bool lockX = (i != 0 && m_lockPrevX);
|
|
|
|
|
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!lockX && m_showPause && ImGui::Button(m_paused ? "Resume" : "Pause")) {
|
2020-08-14 20:02:35 -07:00
|
|
|
m_paused = !m_paused;
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
char label[128];
|
2020-09-12 10:55:46 -07:00
|
|
|
std::snprintf(label, sizeof(label), "%s##plot", m_name.c_str());
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
if (lockX) {
|
2020-09-12 10:55:46 -07:00
|
|
|
ImPlot::SetNextPlotLimitsX(view.m_plots[i - 1]->m_xaxisRange.Min,
|
|
|
|
|
view.m_plots[i - 1]->m_xaxisRange.Max,
|
2020-08-14 20:02:35 -07:00
|
|
|
ImGuiCond_Always);
|
|
|
|
|
} else {
|
|
|
|
|
// also force-pause plots if overall timing is paused
|
2020-12-17 07:27:29 -08:00
|
|
|
double zeroTime = GetZeroTime() * 1.0e-6;
|
2020-09-12 10:55:46 -07:00
|
|
|
ImPlot::SetNextPlotLimitsX(
|
2020-12-17 07:27:29 -08:00
|
|
|
now - zeroTime - m_viewTime, now - zeroTime,
|
2020-09-12 10:55:46 -07:00
|
|
|
(paused || m_paused) ? ImGuiCond_Once : ImGuiCond_Always);
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2021-01-08 01:07:48 -08:00
|
|
|
ImPlotAxisFlags yFlags[3] = {ImPlotAxisFlags_None,
|
|
|
|
|
ImPlotAxisFlags_NoGridLines,
|
|
|
|
|
ImPlotAxisFlags_NoGridLines};
|
2020-08-14 20:02:35 -07:00
|
|
|
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;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (m_axisRange[i].lockMin) {
|
|
|
|
|
yFlags[i] |= ImPlotAxisFlags_LockMin;
|
|
|
|
|
}
|
|
|
|
|
if (m_axisRange[i].lockMax) {
|
|
|
|
|
yFlags[i] |= ImPlotAxisFlags_LockMax;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2021-01-08 01:10:20 -08:00
|
|
|
if (ImPlot::BeginPlot(
|
|
|
|
|
label, nullptr,
|
|
|
|
|
m_axisLabel[0].empty() ? nullptr : m_axisLabel[0].c_str(),
|
|
|
|
|
ImVec2(-1, m_height), m_plotFlags, ImPlotAxisFlags_None, yFlags[0],
|
|
|
|
|
yFlags[1], yFlags[2],
|
|
|
|
|
m_axisLabel[1].empty() ? nullptr : m_axisLabel[1].c_str(),
|
|
|
|
|
m_axisLabel[2].empty() ? nullptr : m_axisLabel[2].c_str())) {
|
2020-08-14 20:02:35 -07:00
|
|
|
for (size_t j = 0; j < m_series.size(); ++j) {
|
2020-09-12 10:55:46 -07:00
|
|
|
ImGui::PushID(j);
|
|
|
|
|
switch (m_series[j]->EmitPlot(view, now, j, i)) {
|
|
|
|
|
case PlotSeries::kMoveUp:
|
2020-12-28 12:58:06 -08:00
|
|
|
if (j > 0) {
|
|
|
|
|
std::swap(m_series[j - 1], m_series[j]);
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
break;
|
|
|
|
|
case PlotSeries::kMoveDown:
|
2020-12-28 12:58:06 -08:00
|
|
|
if (j < (m_series.size() - 1)) {
|
2020-09-12 10:55:46 -07:00
|
|
|
std::swap(m_series[j], m_series[j + 1]);
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
break;
|
|
|
|
|
case PlotSeries::kDelete:
|
|
|
|
|
m_series.erase(m_series.begin() + j);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
ImGui::PopID();
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
DragDropTarget(view, i, true);
|
2020-08-14 20:02:35 -07:00
|
|
|
m_xaxisRange = ImPlot::GetPlotLimits().X;
|
|
|
|
|
ImPlot::EndPlot();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Plot::EmitSettingsLimits(int axis) {
|
|
|
|
|
ImGui::Indent();
|
|
|
|
|
ImGui::PushID(axis);
|
|
|
|
|
|
2021-01-08 01:10:20 -08:00
|
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10);
|
|
|
|
|
ImGui::InputText("Label", &m_axisLabel[axis]);
|
2020-08-14 20:02:35 -07:00
|
|
|
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();
|
2020-12-28 12:58:06 -08:00
|
|
|
if (ImGui::Button("Apply")) {
|
|
|
|
|
m_axisRange[axis].apply = true;
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
|
|
|
|
|
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) {
|
2020-09-12 10:55:46 -07:00
|
|
|
ImGui::Text("Edit plot name:");
|
|
|
|
|
ImGui::InputText("##editname", &m_name);
|
2020-08-14 20:02:35 -07:00
|
|
|
ImGui::Checkbox("Visible", &m_visible);
|
2020-09-12 10:55:46 -07:00
|
|
|
ImGui::Checkbox("Show Pause Button", &m_showPause);
|
2021-01-08 01:07:48 -08:00
|
|
|
ImGui::CheckboxFlags("Hide Legend", &m_plotFlags, ImPlotFlags_NoLegend);
|
2020-12-28 12:58:06 -08:00
|
|
|
if (i != 0) {
|
|
|
|
|
ImGui::Checkbox("Lock X-axis to previous plot", &m_lockPrevX);
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
ImGui::TextUnformatted("Primary Y-Axis");
|
|
|
|
|
EmitSettingsLimits(0);
|
|
|
|
|
ImGui::CheckboxFlags("2nd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis2);
|
2020-12-28 12:58:06 -08:00
|
|
|
if ((m_plotFlags & ImPlotFlags_YAxis2) != 0) {
|
|
|
|
|
EmitSettingsLimits(1);
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
ImGui::CheckboxFlags("3rd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis3);
|
2020-12-28 12:58:06 -08:00
|
|
|
if ((m_plotFlags & ImPlotFlags_YAxis3) != 0) {
|
|
|
|
|
EmitSettingsLimits(2);
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
|
|
|
|
ImGui::InputFloat("View Time (s)", &m_viewTime, 0.1f, 1.0f, "%.1f");
|
2021-02-21 16:38:06 -08:00
|
|
|
ImGui::Checkbox("Auto Height", &m_autoHeight);
|
|
|
|
|
if (!m_autoHeight) {
|
|
|
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
|
|
|
|
if (ImGui::InputInt("Height", &m_height, 10)) {
|
|
|
|
|
if (m_height < 0) {
|
|
|
|
|
m_height = 0;
|
|
|
|
|
}
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
|
2021-02-21 16:38:06 -08:00
|
|
|
int Plot::GetAutoBaseHeight(bool* isAuto, size_t i) {
|
|
|
|
|
*isAuto = m_autoHeight;
|
|
|
|
|
|
|
|
|
|
if (!m_visible) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int height = m_autoHeight ? 0 : m_height;
|
|
|
|
|
|
|
|
|
|
// Pause button
|
|
|
|
|
if ((i == 0 || !m_lockPrevX) && m_showPause) {
|
|
|
|
|
height += ImGui::GetFrameHeightWithSpacing();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return height;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void PlotView::Display() {
|
|
|
|
|
if (ImGui::BeginPopupContextItem()) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (ImGui::Button("Add plot")) {
|
2020-09-12 10:55:46 -07:00
|
|
|
m_plots.emplace_back(std::make_unique<Plot>());
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
|
|
|
|
|
for (size_t i = 0; i < m_plots.size(); ++i) {
|
|
|
|
|
auto& plot = m_plots[i];
|
|
|
|
|
ImGui::PushID(i);
|
|
|
|
|
|
|
|
|
|
char name[64];
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!plot->GetName().empty()) {
|
2020-09-12 10:55:46 -07:00
|
|
|
std::snprintf(name, sizeof(name), "%s", plot->GetName().c_str());
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2020-09-12 10:55:46 -07:00
|
|
|
std::snprintf(name, sizeof(name), "Plot %d", static_cast<int>(i));
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
|
|
|
|
|
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")) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (i > 0) {
|
|
|
|
|
std::swap(m_plots[i - 1], plot);
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button("Move Down")) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (i < (m_plots.size() - 1)) {
|
|
|
|
|
std::swap(plot, m_plots[i + 1]);
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::Button("Delete")) {
|
|
|
|
|
m_plots.erase(m_plots.begin() + i);
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plot->EmitSettings(i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
|
|
|
|
|
ImGui::EndPopup();
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
if (m_plots.empty()) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (ImGui::Button("Add plot")) {
|
2020-09-12 10:55:46 -07:00
|
|
|
m_plots.emplace_back(std::make_unique<Plot>());
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
// Make "add plot" button a DND target for Plot
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!ImGui::BeginDragDropTarget()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("Plot")) {
|
|
|
|
|
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
|
|
|
|
MovePlot(ref->view, ref->plotIndex, 0);
|
|
|
|
|
}
|
2021-02-21 16:38:06 -08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auto-size plots. This requires two passes: the first pass to get the
|
|
|
|
|
// total height, the second to actually set the height after averaging it
|
|
|
|
|
// across all auto-sized heights.
|
|
|
|
|
int availHeight = ImGui::GetContentRegionAvail().y;
|
|
|
|
|
int numAuto = 0;
|
|
|
|
|
for (size_t i = 0; i < m_plots.size(); ++i) {
|
|
|
|
|
bool isAuto;
|
|
|
|
|
availHeight -= m_plots[i]->GetAutoBaseHeight(&isAuto, i);
|
|
|
|
|
availHeight -= ImGui::GetStyle().ItemSpacing.y;
|
|
|
|
|
if (isAuto) {
|
|
|
|
|
++numAuto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (numAuto > 0) {
|
|
|
|
|
availHeight /= numAuto;
|
|
|
|
|
for (size_t i = 0; i < m_plots.size(); ++i) {
|
|
|
|
|
m_plots[i]->SetAutoHeight(availHeight);
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
|
2020-12-17 07:27:29 -08:00
|
|
|
double now = wpi::Now() * 1.0e-6;
|
2020-09-12 10:55:46 -07:00
|
|
|
for (size_t i = 0; i < m_plots.size(); ++i) {
|
2020-08-14 20:02:35 -07:00
|
|
|
ImGui::PushID(i);
|
2020-09-12 10:55:46 -07:00
|
|
|
m_plots[i]->EmitPlot(*this, now, m_provider->IsPaused(), i);
|
2020-08-14 20:02:35 -07:00
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void PlotView::MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex) {
|
|
|
|
|
if (fromView == this) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (fromIndex == toIndex) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
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
|
2020-12-28 12:58:06 -08:00
|
|
|
if (yAxis != -1) {
|
|
|
|
|
val->SetYAxis(yAxis);
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
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} {}
|
|
|
|
|
|
2020-12-28 00:37:33 -08:00
|
|
|
PlotProvider::~PlotProvider() = default;
|
2020-09-12 10:55:46 -07:00
|
|
|
|
|
|
|
|
void PlotProvider::GlobalInit() {
|
|
|
|
|
WindowManager::GlobalInit();
|
|
|
|
|
wpi::gui::AddInit([this] {
|
|
|
|
|
m_plotSaver.Initialize();
|
|
|
|
|
m_seriesSaver.Initialize();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
|
|
|
|
|
if (ImGui::MenuItem("New Plot Window")) {
|
2021-03-21 18:06:18 -07:00
|
|
|
// this is an inefficient algorithm, but the number of windows is small
|
2020-09-12 10:55:46 -07:00
|
|
|
char id[32];
|
2021-03-21 18:06:18 -07:00
|
|
|
size_t numWindows = m_windows.size();
|
|
|
|
|
for (size_t i = 0; i <= numWindows; ++i) {
|
|
|
|
|
std::snprintf(id, sizeof(id), "Plot <%d>", static_cast<int>(i));
|
|
|
|
|
bool match = false;
|
|
|
|
|
for (size_t j = i; j < numWindows; ++j) {
|
|
|
|
|
if (m_windows[j]->GetId() == id) {
|
|
|
|
|
match = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!match) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-21 10:50:41 -07:00
|
|
|
if (auto win = AddWindow(id, std::make_unique<PlotView>(this))) {
|
|
|
|
|
win->SetDefaultSize(700, 400);
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
void PlotProvider::DisplayWindows() {
|
|
|
|
|
// create views if not already created
|
|
|
|
|
for (auto&& window : m_windows) {
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!window->HasView()) {
|
|
|
|
|
window->SetView(std::make_unique<PlotView>(this));
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
}
|
|
|
|
|
WindowManager::DisplayWindows();
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
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('#');
|
2020-12-28 12:58:06 -08:00
|
|
|
if (seriesId.empty()) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
}
|
|
|
|
|
unsigned int plotNum;
|
2020-12-28 12:58:06 -08:00
|
|
|
if (plotNumStr.getAsInteger(10, plotNum)) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
|
|
|
|
|
// get or create window
|
|
|
|
|
auto win = m_provider->GetOrAddWindow(viewId, true);
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!win) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
|
|
|
|
|
// 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
|
2020-12-28 12:58:06 -08:00
|
|
|
if (view->m_plots.size() <= plotNum) {
|
|
|
|
|
view->m_plots.resize(plotNum + 1);
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
auto& plot = view->m_plots[plotNum];
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!plot) {
|
|
|
|
|
plot = std::make_unique<Plot>();
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
|
|
|
|
|
// early exit for plot data
|
2020-12-28 12:58:06 -08:00
|
|
|
if (!m_forSeries) {
|
|
|
|
|
return plot.get();
|
|
|
|
|
}
|
2020-09-12 10:55:46 -07:00
|
|
|
|
|
|
|
|
// 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('=');
|
2020-08-14 20:02:35 -07:00
|
|
|
name = name.trim();
|
|
|
|
|
value = value.trim();
|
2020-12-28 12:58:06 -08:00
|
|
|
if (m_forSeries) {
|
2020-09-12 10:55:46 -07:00
|
|
|
static_cast<PlotSeries*>(entry)->ReadIni(name, value);
|
2020-12-28 12:58:06 -08:00
|
|
|
} else {
|
2020-09-12 10:55:46 -07:00
|
|
|
static_cast<Plot*>(entry)->ReadIni(name, value);
|
2020-12-28 12:58:06 -08:00
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-12 10:55:46 -07:00
|
|
|
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");
|
|
|
|
|
}
|
2020-08-14 20:02:35 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|