Files
allwpilib/glass/src/lib/native/cpp/other/Field2D.cpp
Peter Johnson 0587b7043a [glass] Use JSON files for storage instead of imgui ini
Storage is now nested.

Separate "roots" can be configured which save to separate files.
In particular, this is used to save wpigui and ImGui window position
to a -window.json file.

ImGui's ini (for window position) is mapped to JSON.

You can optionally specify a directory to load from on the command line.
If one isn't provided, it uses the global system directory.
Any changes made are automatically saved here.

Workspace | Open: select directory, the current layout is replaced with that
workspace, and future auto-saves also switch to that location. The main
window size/location is not changed, only the contents.

Workspace | Save As: select directory, the current layout is saved there,
and future auto-saves also switch to that location.

Workspace | Reset: window locations are preserved, but all other settings
are reset to default (including e.g. removing plot windows). This will also
end up clearing the current save file. as with load, the main window
size/location is not changed.

Workspace | Save As Global: "save as" to the global system location

Notably, the main window size/location is only loaded at startup, but is
auto-saved as part of the current workspace.
2021-11-27 00:12:13 -08:00

1225 lines
36 KiB
C++

// 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.
#include "glass/other/Field2D.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <memory>
#include <string_view>
#include <utility>
#include <fmt/format.h>
#include <frc/geometry/Pose2d.h>
#include <frc/geometry/Rotation2d.h>
#include <frc/geometry/Translation2d.h>
#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_stdlib.h>
#include <portable-file-dialogs.h>
#include <units/angle.h>
#include <units/length.h>
#include <wpi/SmallString.h>
#include <wpi/StringExtras.h>
#include <wpi/StringMap.h>
#include <wpi/fs.h>
#include <wpi/json.h>
#include <wpi/raw_istream.h>
#include <wpigui.h>
#include "glass/Context.h"
#include "glass/Storage.h"
#include "glass/support/ColorSetting.h"
#include "glass/support/EnumSetting.h"
using namespace glass;
namespace gui = wpi::gui;
namespace {
enum DisplayUnits { kDisplayMeters = 0, kDisplayFeet, kDisplayInches };
// Per-frame field data (not persistent)
struct FieldFrameData {
frc::Translation2d GetPosFromScreen(const ImVec2& cursor) const {
return {
units::meter_t{(std::clamp(cursor.x, min.x, max.x) - min.x) / scale},
units::meter_t{(max.y - std::clamp(cursor.y, min.y, max.y)) / scale}};
}
ImVec2 GetScreenFromPos(const frc::Translation2d& pos) const {
return {min.x + scale * pos.X().to<float>(),
max.y - scale * pos.Y().to<float>()};
}
// in screen coordinates
ImVec2 imageMin;
ImVec2 imageMax;
ImVec2 min;
ImVec2 max;
float scale; // scaling from meters to screen units
};
// Pose drag target info
struct SelectedTargetInfo {
FieldObjectModel* objModel = nullptr;
std::string name;
size_t index;
units::radian_t rot;
ImVec2 poseCenter; // center of the pose (screen coordinates)
ImVec2 center; // center of the target (screen coordinates)
float radius; // target radius
float dist; // distance from center to mouse
int corner; // corner (1 = center)
};
// Pose drag state
struct PoseDragState {
SelectedTargetInfo target;
ImVec2 initialOffset;
units::radian_t initialAngle = 0_rad;
};
// Popup edit state
class PopupState {
public:
void Open(SelectedTargetInfo* target, const frc::Translation2d& pos);
void Close();
SelectedTargetInfo* GetTarget() { return &m_target; }
FieldObjectModel* GetInsertModel() { return m_insertModel; }
wpi::span<const frc::Pose2d> GetInsertPoses() const { return m_insertPoses; }
void Display(Field2DModel* model, const FieldFrameData& ffd);
private:
void DisplayTarget(Field2DModel* model, const FieldFrameData& ffd);
void DisplayInsert(Field2DModel* model);
SelectedTargetInfo m_target;
// for insert
FieldObjectModel* m_insertModel;
std::vector<frc::Pose2d> m_insertPoses;
std::string m_insertName;
int m_insertIndex;
};
struct DisplayOptions {
explicit DisplayOptions(const gui::Texture& texture) : texture{texture} {}
enum Style { kBoxImage = 0, kLine, kLineClosed, kTrack };
static constexpr Style kDefaultStyle = kBoxImage;
static constexpr float kDefaultWeight = 4.0f;
static constexpr float kDefaultColorFloat[] = {255, 0, 0, 255};
static constexpr ImU32 kDefaultColor = IM_COL32(255, 0, 0, 255);
static constexpr auto kDefaultWidth = 0.6858_m;
static constexpr auto kDefaultLength = 0.8204_m;
static constexpr bool kDefaultArrows = true;
static constexpr int kDefaultArrowSize = 50;
static constexpr float kDefaultArrowWeight = 4.0f;
static constexpr float kDefaultArrowColorFloat[] = {0, 255, 0, 255};
static constexpr ImU32 kDefaultArrowColor = IM_COL32(0, 255, 0, 255);
static constexpr bool kDefaultSelectable = true;
Style style = kDefaultStyle;
float weight = kDefaultWeight;
int color = kDefaultColor;
units::meter_t width = kDefaultWidth;
units::meter_t length = kDefaultLength;
bool arrows = kDefaultArrows;
int arrowSize = kDefaultArrowSize;
float arrowWeight = kDefaultArrowWeight;
int arrowColor = kDefaultArrowColor;
bool selectable = kDefaultSelectable;
const gui::Texture& texture;
};
// Per-frame pose data (not persistent)
class PoseFrameData {
public:
explicit PoseFrameData(const frc::Pose2d& pose, FieldObjectModel& model,
size_t index, const FieldFrameData& ffd,
const DisplayOptions& displayOptions);
void SetPosition(const frc::Translation2d& pos);
void SetRotation(units::radian_t rot);
const frc::Rotation2d& GetRotation() const { return m_pose.Rotation(); }
const frc::Pose2d& GetPose() const { return m_pose; }
float GetHitRadius() const { return m_hitRadius; }
void UpdateFrameData();
std::pair<int, float> IsHovered(const ImVec2& cursor) const;
SelectedTargetInfo GetDragTarget(int corner, float dist) const;
void HandleDrag(const ImVec2& cursor);
void Draw(ImDrawList* drawList, std::vector<ImVec2>* center,
std::vector<ImVec2>* left, std::vector<ImVec2>* right) const;
// in window coordinates
ImVec2 m_center;
ImVec2 m_corners[6]; // 5 and 6 are used for track width
ImVec2 m_arrow[3];
private:
FieldObjectModel& m_model;
size_t m_index;
const FieldFrameData& m_ffd;
const DisplayOptions& m_displayOptions;
// scaled width/2 and length/2, in screen units
float m_width2;
float m_length2;
float m_hitRadius;
frc::Pose2d m_pose;
};
class ObjectInfo {
public:
explicit ObjectInfo(Storage& storage);
DisplayOptions GetDisplayOptions() const;
void DisplaySettings();
void DrawLine(ImDrawList* drawList, wpi::span<const ImVec2> points) const;
void LoadImage();
const gui::Texture& GetTexture() const { return m_texture; }
private:
void Reset();
bool LoadImageImpl(const std::string& fn);
std::unique_ptr<pfd::open_file> m_fileOpener;
// in meters
float& m_width;
float& m_length;
EnumSetting m_style; // DisplayOptions::Style
float& m_weight;
ColorSetting m_color;
bool& m_arrows;
int& m_arrowSize;
float& m_arrowWeight;
ColorSetting m_arrowColor;
bool& m_selectable;
std::string& m_filename;
gui::Texture m_texture;
};
class FieldInfo {
public:
static constexpr auto kDefaultWidth = 15.98_m;
static constexpr auto kDefaultHeight = 8.21_m;
explicit FieldInfo(Storage& storage);
void DisplaySettings();
void LoadImage();
FieldFrameData GetFrameData(ImVec2 min, ImVec2 max) const;
void Draw(ImDrawList* drawList, const FieldFrameData& frameData) const;
wpi::StringMap<std::unique_ptr<ObjectInfo>> m_objects;
private:
void Reset();
bool LoadImageImpl(const std::string& fn);
void LoadJson(std::string_view jsonfile);
std::unique_ptr<pfd::open_file> m_fileOpener;
std::string& m_filename;
gui::Texture m_texture;
// in meters
float& m_width;
float& m_height;
// in image pixels
int m_imageWidth;
int m_imageHeight;
int& m_top;
int& m_left;
int& m_bottom;
int& m_right;
};
} // namespace
static PoseDragState gDragState;
static PopupState gPopupState;
static DisplayUnits gDisplayUnits = kDisplayMeters;
static double ConvertDisplayLength(units::meter_t v) {
switch (gDisplayUnits) {
case kDisplayFeet:
return v.convert<units::feet>().value();
case kDisplayInches:
return v.convert<units::inches>().value();
case kDisplayMeters:
default:
return v.value();
}
}
static double ConvertDisplayAngle(units::degree_t v) {
return v.value();
}
static bool InputLength(const char* label, units::meter_t* v, double step = 0.0,
double step_fast = 0.0, const char* format = "%.6f",
ImGuiInputTextFlags flags = 0) {
double dv = ConvertDisplayLength(*v);
if (ImGui::InputDouble(label, &dv, step, step_fast, format, flags)) {
switch (gDisplayUnits) {
case kDisplayFeet:
*v = units::foot_t{dv};
break;
case kDisplayInches:
*v = units::inch_t{dv};
break;
case kDisplayMeters:
default:
*v = units::meter_t{dv};
break;
}
return true;
}
return false;
}
static bool InputFloatLength(const char* label, float* v, double step = 0.0,
double step_fast = 0.0,
const char* format = "%.3f",
ImGuiInputTextFlags flags = 0) {
units::meter_t uv{*v};
if (InputLength(label, &uv, step, step_fast, format, flags)) {
*v = uv.to<float>();
return true;
}
return false;
}
static bool InputAngle(const char* label, units::degree_t* v, double step = 0.0,
double step_fast = 0.0, const char* format = "%.6f",
ImGuiInputTextFlags flags = 0) {
double dv = ConvertDisplayAngle(*v);
if (ImGui::InputDouble(label, &dv, step, step_fast, format, flags)) {
*v = units::degree_t{dv};
return true;
}
return false;
}
static bool InputPose(frc::Pose2d* pose) {
auto x = pose->X();
auto y = pose->Y();
auto rot = pose->Rotation().Degrees();
bool changed;
changed = InputLength("x", &x);
changed = InputLength("y", &y) || changed;
changed = InputAngle("rot", &rot) || changed;
if (changed) {
*pose = frc::Pose2d{x, y, rot};
}
return changed;
}
FieldInfo::FieldInfo(Storage& storage)
: m_filename{storage.GetString("image")},
m_width{storage.GetFloat("width", kDefaultWidth.to<float>())},
m_height{storage.GetFloat("height", kDefaultHeight.to<float>())},
m_top{storage.GetInt("top", 0)},
m_left{storage.GetInt("left", 0)},
m_bottom{storage.GetInt("bottom", -1)},
m_right{storage.GetInt("right", -1)} {}
void FieldInfo::DisplaySettings() {
if (ImGui::Button("Choose image...")) {
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")) {
Reset();
}
InputFloatLength("Field Width", &m_width);
InputFloatLength("Field Height", &m_height);
// ImGui::InputInt("Field Top", &m_top);
// ImGui::InputInt("Field Left", &m_left);
// ImGui::InputInt("Field Right", &m_right);
// ImGui::InputInt("Field Bottom", &m_bottom);
}
void FieldInfo::Reset() {
m_texture = gui::Texture{};
m_filename.clear();
m_imageWidth = 0;
m_imageHeight = 0;
m_top = 0;
m_left = 0;
m_bottom = -1;
m_right = -1;
}
void FieldInfo::LoadImage() {
if (m_fileOpener && m_fileOpener->ready(0)) {
auto result = m_fileOpener->result();
if (!result.empty()) {
if (wpi::ends_with(result[0], ".json")) {
LoadJson(result[0]);
} else {
LoadImageImpl(result[0].c_str());
m_top = 0;
m_left = 0;
m_bottom = -1;
m_right = -1;
}
}
m_fileOpener.reset();
}
if (!m_texture && !m_filename.empty()) {
if (!LoadImageImpl(m_filename)) {
m_filename.clear();
}
}
}
void FieldInfo::LoadJson(std::string_view jsonfile) {
std::error_code ec;
wpi::raw_fd_istream f(jsonfile, ec);
if (ec) {
std::fputs("GUI: could not open field JSON file\n", stderr);
return;
}
// parse file
wpi::json j;
try {
j = wpi::json::parse(f);
} catch (const wpi::json::parse_error& e) {
fmt::print(stderr, "GUI: JSON: could not parse: {}\n", e.what());
}
// top level must be an object
if (!j.is_object()) {
std::fputs("GUI: JSON: does not contain a top object\n", stderr);
return;
}
// image filename
std::string image;
try {
image = j.at("field-image").get<std::string>();
} catch (const wpi::json::exception& e) {
fmt::print(stderr, "GUI: JSON: could not read field-image: {}\n", e.what());
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) {
fmt::print(stderr, "GUI: JSON: could not read field-corners: {}\n",
e.what());
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) {
fmt::print(stderr, "GUI: JSON: could not read field-size: {}\n", e.what());
return;
}
// units for size
std::string unit;
try {
unit = j.at("field-unit").get<std::string>();
} catch (const wpi::json::exception& e) {
fmt::print(stderr, "GUI: JSON: could not read field-unit: {}\n", e.what());
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
auto pathname = fs::path{jsonfile}.replace_filename(image).string();
// load field image
if (!LoadImageImpl(pathname.c_str())) {
return;
}
// save to field info
m_filename = pathname;
m_top = top;
m_left = left;
m_bottom = bottom;
m_right = right;
m_width = width;
m_height = height;
}
bool FieldInfo::LoadImageImpl(const std::string& fn) {
fmt::print("GUI: loading field image '{}'\n", fn);
auto texture = gui::Texture::CreateFromFile(fn.c_str());
if (!texture) {
std::puts("GUI: could not read field image");
return false;
}
m_texture = std::move(texture);
m_imageWidth = m_texture.GetWidth();
m_imageHeight = m_texture.GetHeight();
m_filename = 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_bottom > 0 && m_right > 0) {
min.x += m_left * (max.x - min.x) / m_imageWidth;
min.y += m_top * (max.y - min.y) / m_imageHeight;
max.x -= (m_imageWidth - m_right) * (max.x - min.x) / m_imageWidth;
max.y -= (m_imageHeight - m_bottom) * (max.y - min.y) / m_imageHeight;
}
// draw the field "active area" as a yellow boundary box
gui::MaxFit(&min, &max, m_width, m_height);
ffd.min = min;
ffd.max = max;
ffd.scale = (max.x - min.x) / m_width;
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));
}
ObjectInfo::ObjectInfo(Storage& storage)
: m_width{storage.GetFloat("width",
DisplayOptions::kDefaultWidth.to<float>())},
m_length{storage.GetFloat("length",
DisplayOptions::kDefaultLength.to<float>())},
m_style{storage.GetString("style"),
DisplayOptions::kDefaultStyle,
{"Box/Image", "Line", "Line (Closed)", "Track"}},
m_weight{storage.GetFloat("weight", DisplayOptions::kDefaultWeight)},
m_color{
storage.GetFloatArray("color", DisplayOptions::kDefaultColorFloat)},
m_arrows{storage.GetBool("arrows", DisplayOptions::kDefaultArrows)},
m_arrowSize{
storage.GetInt("arrowSize", DisplayOptions::kDefaultArrowSize)},
m_arrowWeight{
storage.GetFloat("arrowWeight", DisplayOptions::kDefaultArrowWeight)},
m_arrowColor{storage.GetFloatArray(
"arrowColor", DisplayOptions::kDefaultArrowColorFloat)},
m_selectable{
storage.GetBool("selectable", DisplayOptions::kDefaultSelectable)},
m_filename{storage.GetString("image")} {}
DisplayOptions ObjectInfo::GetDisplayOptions() const {
DisplayOptions rv{m_texture};
rv.style = static_cast<DisplayOptions::Style>(m_style.GetValue());
rv.weight = m_weight;
rv.color = ImGui::ColorConvertFloat4ToU32(m_color.GetColor());
rv.width = units::meter_t{m_width};
rv.length = units::meter_t{m_length};
rv.arrows = m_arrows;
rv.arrowSize = m_arrowSize;
rv.arrowWeight = m_arrowWeight;
rv.arrowColor = ImGui::ColorConvertFloat4ToU32(m_arrowColor.GetColor());
rv.selectable = m_selectable;
return rv;
}
void ObjectInfo::DisplaySettings() {
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
m_style.Combo("Style");
switch (m_style.GetValue()) {
case DisplayOptions::kBoxImage:
if (ImGui::Button("Choose image...")) {
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")) {
Reset();
}
InputFloatLength("Width", &m_width);
InputFloatLength("Length", &m_length);
break;
case DisplayOptions::kTrack:
InputFloatLength("Width", &m_width);
break;
default:
break;
}
ImGui::InputFloat("Line Weight", &m_weight);
m_color.ColorEdit3("Line Color", ImGuiColorEditFlags_NoInputs);
ImGui::Checkbox("Arrows", &m_arrows);
if (m_arrows) {
ImGui::SliderInt("Arrow Size", &m_arrowSize, 0, 100, "%d%%",
ImGuiSliderFlags_AlwaysClamp);
ImGui::InputFloat("Arrow Weight", &m_arrowWeight);
m_arrowColor.ColorEdit3("Arrow Color", ImGuiColorEditFlags_NoInputs);
}
ImGui::Checkbox("Selectable", &m_selectable);
}
void ObjectInfo::DrawLine(ImDrawList* drawList,
wpi::span<const ImVec2> points) const {
if (points.empty()) {
return;
}
if (points.size() == 1) {
drawList->AddCircleFilled(points.front(), m_weight, m_weight);
return;
}
ImU32 color = ImGui::ColorConvertFloat4ToU32(m_color.GetColor());
// PolyLine doesn't handle acute angles well; workaround from
// https://github.com/ocornut/imgui/issues/3366
size_t i = 0;
while (i + 1 < points.size()) {
int nlin = 2;
while (i + nlin < points.size()) {
auto [x0, y0] = points[i + nlin - 2];
auto [x1, y1] = points[i + nlin - 1];
auto [x2, y2] = points[i + nlin];
auto s0x = x1 - x0, s0y = y1 - y0;
auto s1x = x2 - x1, s1y = y2 - y1;
auto dotprod = s1x * s0x + s1y * s0y;
if (dotprod < 0) {
break;
}
++nlin;
}
drawList->AddPolyline(&points[i], nlin, color, false, m_weight);
i += nlin - 1;
}
if (points.size() > 2 && m_style.GetValue() == DisplayOptions::kLineClosed) {
drawList->AddLine(points.back(), points.front(), color, m_weight);
}
}
void ObjectInfo::Reset() {
m_texture = gui::Texture{};
m_filename.clear();
}
void ObjectInfo::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_filename.empty()) {
if (!LoadImageImpl(m_filename)) {
m_filename.clear();
}
}
}
bool ObjectInfo::LoadImageImpl(const std::string& fn) {
fmt::print("GUI: loading object image '{}'\n", fn);
auto texture = gui::Texture::CreateFromFile(fn.c_str());
if (!texture) {
std::fputs("GUI: could not read object image\n", stderr);
return false;
}
m_texture = std::move(texture);
m_filename = fn;
return true;
}
PoseFrameData::PoseFrameData(const frc::Pose2d& pose, FieldObjectModel& model,
size_t index, const FieldFrameData& ffd,
const DisplayOptions& displayOptions)
: m_model{model},
m_index{index},
m_ffd{ffd},
m_displayOptions{displayOptions},
m_width2(ffd.scale * displayOptions.width / 2),
m_length2(ffd.scale * displayOptions.length / 2),
m_hitRadius((std::min)(m_width2, m_length2) / 2),
m_pose{pose} {
UpdateFrameData();
}
void PoseFrameData::SetPosition(const frc::Translation2d& pos) {
m_pose = frc::Pose2d{pos, m_pose.Rotation()};
m_model.SetPose(m_index, m_pose);
}
void PoseFrameData::SetRotation(units::radian_t rot) {
m_pose = frc::Pose2d{m_pose.Translation(), rot};
m_model.SetPose(m_index, m_pose);
}
void PoseFrameData::UpdateFrameData() {
// (0,0) origin is bottom left
ImVec2 center = m_ffd.GetScreenFromPos(m_pose.Translation());
// build rotated points around center
float length2 = m_length2;
float width2 = m_width2;
auto& rot = GetRotation();
float cos_a = rot.Cos();
float sin_a = -rot.Sin();
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_corners[4] = center + ImRotate(ImVec2(0, -width2), cos_a, sin_a);
m_corners[5] = center + ImRotate(ImVec2(0, width2), cos_a, sin_a);
float arrowScale = m_displayOptions.arrowSize / 100.0f;
m_arrow[0] =
center + ImRotate(ImVec2(-length2 * arrowScale, -width2 * arrowScale),
cos_a, sin_a);
m_arrow[1] = center + ImRotate(ImVec2(length2 * arrowScale, 0), cos_a, sin_a);
m_arrow[2] =
center + ImRotate(ImVec2(-length2 * arrowScale, width2 * arrowScale),
cos_a, sin_a);
m_center = center;
}
std::pair<int, float> PoseFrameData::IsHovered(const ImVec2& cursor) const {
float hitRadiusSquared = m_hitRadius * m_hitRadius;
float dist;
// it's within the hit radius of the center?
dist = gui::GetDistSquared(cursor, m_center);
if (dist < hitRadiusSquared) {
return {1, dist};
}
if (m_displayOptions.style == DisplayOptions::kBoxImage) {
dist = gui::GetDistSquared(cursor, m_corners[0]);
if (dist < hitRadiusSquared) {
return {2, dist};
}
dist = gui::GetDistSquared(cursor, m_corners[1]);
if (dist < hitRadiusSquared) {
return {3, dist};
}
dist = gui::GetDistSquared(cursor, m_corners[2]);
if (dist < hitRadiusSquared) {
return {4, dist};
}
dist = gui::GetDistSquared(cursor, m_corners[3]);
if (dist < hitRadiusSquared) {
return {5, dist};
}
} else if (m_displayOptions.style == DisplayOptions::kTrack) {
dist = gui::GetDistSquared(cursor, m_corners[4]);
if (dist < hitRadiusSquared) {
return {6, dist};
}
dist = gui::GetDistSquared(cursor, m_corners[5]);
if (dist < hitRadiusSquared) {
return {7, dist};
}
}
return {0, 0.0};
}
SelectedTargetInfo PoseFrameData::GetDragTarget(int corner, float dist) const {
SelectedTargetInfo info;
info.objModel = &m_model;
info.rot = GetRotation().Radians();
info.poseCenter = m_center;
if (corner == 1) {
info.center = m_center;
} else {
info.center = m_corners[corner - 2];
}
info.radius = m_hitRadius;
info.dist = dist;
info.corner = corner;
return info;
}
void PoseFrameData::HandleDrag(const ImVec2& cursor) {
if (gDragState.target.corner == 1) {
SetPosition(m_ffd.GetPosFromScreen(cursor - gDragState.initialOffset));
UpdateFrameData();
gDragState.target.center = m_center;
gDragState.target.poseCenter = m_center;
} else {
ImVec2 off = cursor - m_center;
SetRotation(gDragState.initialAngle -
units::radian_t{std::atan2(off.y, off.x)});
gDragState.target.center = m_corners[gDragState.target.corner - 2];
gDragState.target.rot = GetRotation().Radians();
}
}
void PoseFrameData::Draw(ImDrawList* drawList, std::vector<ImVec2>* center,
std::vector<ImVec2>* left,
std::vector<ImVec2>* right) const {
switch (m_displayOptions.style) {
case DisplayOptions::kBoxImage:
if (m_displayOptions.texture) {
drawList->AddImageQuad(m_displayOptions.texture, m_corners[0],
m_corners[1], m_corners[2], m_corners[3]);
return;
}
drawList->AddQuad(m_corners[0], m_corners[1], m_corners[2], m_corners[3],
m_displayOptions.color, m_displayOptions.weight);
break;
case DisplayOptions::kLine:
case DisplayOptions::kLineClosed:
center->emplace_back(m_center);
break;
case DisplayOptions::kTrack:
center->emplace_back(m_center);
left->emplace_back(m_corners[4]);
right->emplace_back(m_corners[5]);
break;
}
if (m_displayOptions.arrows) {
drawList->AddTriangle(m_arrow[0], m_arrow[1], m_arrow[2],
m_displayOptions.arrowColor,
m_displayOptions.arrowWeight);
}
}
void glass::DisplayField2DSettings(Field2DModel* model) {
auto& storage = GetStorage();
auto field = storage.GetData<FieldInfo>();
if (!field) {
storage.SetData(std::make_shared<FieldInfo>(storage));
field = storage.GetData<FieldInfo>();
}
EnumSetting displayUnits{GetStorage().GetString("units"),
kDisplayMeters,
{"meters", "feet", "inches"}};
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
displayUnits.Combo("Units");
gDisplayUnits = static_cast<DisplayUnits>(displayUnits.GetValue());
ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
if (ImGui::CollapsingHeader("Field")) {
ImGui::PushID("Field");
field->DisplaySettings();
ImGui::PopID();
}
model->ForEachFieldObject([&](auto& objModel, auto name) {
if (!objModel.Exists()) {
return;
}
PushID(name);
auto& objRef = field->m_objects[name];
if (!objRef) {
objRef = std::make_unique<ObjectInfo>(GetStorage());
}
auto obj = objRef.get();
wpi::SmallString<64> nameBuf{name};
if (ImGui::CollapsingHeader(nameBuf.c_str())) {
obj->DisplaySettings();
}
PopID();
});
ImGui::PopItemWidth();
}
namespace {
class FieldDisplay {
public:
void Display(FieldInfo* field, Field2DModel* model,
const ImVec2& contentSize);
private:
void DisplayObject(FieldObjectModel& model, std::string_view name);
FieldInfo* m_field;
ImVec2 m_mousePos;
ImDrawList* m_drawList;
// only allow initiation of dragging when invisible button is hovered;
// this prevents the window resize handles from simultaneously activating
// the drag functionality
bool m_isHovered;
FieldFrameData m_ffd;
// drag targets
std::vector<SelectedTargetInfo> m_targets;
// splitter so lines are put behind arrows
ImDrawListSplitter m_drawSplit;
// lines; static so buffer gets reused
std::vector<ImVec2> m_centerLine, m_leftLine, m_rightLine;
};
} // namespace
void FieldDisplay::Display(FieldInfo* field, Field2DModel* model,
const ImVec2& contentSize) {
// screen coords
ImVec2 cursorPos = ImGui::GetWindowPos() + ImGui::GetCursorPos();
// for dragging to work, there needs to be a button (otherwise the window is
// dragged)
ImGui::InvisibleButton("field", contentSize);
m_field = field;
m_mousePos = ImGui::GetIO().MousePos;
m_drawList = ImGui::GetWindowDrawList();
m_isHovered = ImGui::IsItemHovered();
// field
field->LoadImage();
m_ffd = field->GetFrameData(cursorPos, cursorPos + contentSize);
field->Draw(m_drawList, m_ffd);
// stop dragging if mouse button not down
bool isDown = ImGui::IsMouseDown(0);
if (!isDown) {
gDragState.target.objModel = nullptr;
}
// clear popup target if popup closed
bool isPopupOpen = ImGui::IsPopupOpen("edit");
if (!isPopupOpen) {
gPopupState.Close();
}
// field objects
m_targets.resize(0);
model->ForEachFieldObject([this](auto& objModel, auto name) {
if (objModel.Exists()) {
DisplayObject(objModel, name);
}
});
SelectedTargetInfo* target = nullptr;
if (gDragState.target.objModel) {
target = &gDragState.target;
} else if (gPopupState.GetTarget()->objModel) {
target = gPopupState.GetTarget();
} else if (!m_targets.empty()) {
// Find the "best" drag target of the available options. Prefer
// center to non-center, and then pick the closest hit.
std::sort(m_targets.begin(), m_targets.end(),
[](const auto& a, const auto& b) {
return a.corner == 0 || a.dist < b.dist;
});
target = &m_targets.front();
}
if (target) {
// draw the target circle; also draw a smaller circle on the pose center
m_drawList->AddCircle(target->center, target->radius,
IM_COL32(0, 255, 0, 255));
if (target->corner != 1) {
m_drawList->AddCircle(target->poseCenter, target->radius / 2.0,
IM_COL32(0, 255, 0, 255));
}
}
// right-click popup for editing
if (m_isHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
gPopupState.Open(target, m_ffd.GetPosFromScreen(m_mousePos));
ImGui::OpenPopup("edit");
}
if (ImGui::BeginPopup("edit")) {
gPopupState.Display(model, m_ffd);
ImGui::EndPopup();
} else if (target) {
if (m_isHovered && ImGui::IsMouseClicked(0)) {
// initialize drag state
gDragState.target = *target;
gDragState.initialOffset = m_mousePos - target->poseCenter;
if (target->corner != 1) {
gDragState.initialAngle =
units::radian_t{std::atan2(gDragState.initialOffset.y,
gDragState.initialOffset.x)} +
target->rot;
}
}
// show tooltip and highlight
auto pos = m_ffd.GetPosFromScreen(target->poseCenter);
ImGui::SetTooltip(
"%s[%d]\nx: %0.3f y: %0.3f rot: %0.3f", target->name.c_str(),
static_cast<int>(target->index), ConvertDisplayLength(pos.X()),
ConvertDisplayLength(pos.Y()), ConvertDisplayAngle(target->rot));
}
}
void FieldDisplay::DisplayObject(FieldObjectModel& model,
std::string_view name) {
PushID(name);
auto& objRef = m_field->m_objects[name];
if (!objRef) {
objRef = std::make_unique<ObjectInfo>(GetStorage());
}
auto obj = objRef.get();
obj->LoadImage();
auto displayOptions = obj->GetDisplayOptions();
m_centerLine.resize(0);
m_leftLine.resize(0);
m_rightLine.resize(0);
m_drawSplit.Split(m_drawList, 2);
m_drawSplit.SetCurrentChannel(m_drawList, 1);
auto poses = gPopupState.GetInsertModel() == &model
? gPopupState.GetInsertPoses()
: model.GetPoses();
size_t i = 0;
for (auto&& pose : poses) {
PoseFrameData pfd{pose, model, i, m_ffd, displayOptions};
// check for potential drag targets
if (displayOptions.selectable && m_isHovered &&
!gDragState.target.objModel) {
auto [corner, dist] = pfd.IsHovered(m_mousePos);
if (corner > 0) {
m_targets.emplace_back(pfd.GetDragTarget(corner, dist));
m_targets.back().name = name;
m_targets.back().index = i;
}
}
// handle active dragging of this object
if (gDragState.target.objModel == &model && gDragState.target.index == i) {
pfd.HandleDrag(m_mousePos);
}
// draw
pfd.Draw(m_drawList, &m_centerLine, &m_leftLine, &m_rightLine);
++i;
}
m_drawSplit.SetCurrentChannel(m_drawList, 0);
obj->DrawLine(m_drawList, m_centerLine);
obj->DrawLine(m_drawList, m_leftLine);
obj->DrawLine(m_drawList, m_rightLine);
m_drawSplit.Merge(m_drawList);
PopID();
}
void PopupState::Open(SelectedTargetInfo* target,
const frc::Translation2d& pos) {
if (target) {
m_target = *target;
} else {
m_target.objModel = nullptr;
m_insertModel = nullptr;
m_insertPoses.resize(0);
m_insertPoses.emplace_back(pos, 0_deg);
m_insertName.clear();
m_insertIndex = 0;
}
}
void PopupState::Close() {
m_target.objModel = nullptr;
m_insertModel = nullptr;
m_insertPoses.resize(0);
}
void PopupState::Display(Field2DModel* model, const FieldFrameData& ffd) {
if (m_target.objModel) {
DisplayTarget(model, ffd);
} else {
DisplayInsert(model);
}
}
void PopupState::DisplayTarget(Field2DModel* model, const FieldFrameData& ffd) {
ImGui::Text("%s[%d]", m_target.name.c_str(),
static_cast<int>(m_target.index));
frc::Pose2d pose{ffd.GetPosFromScreen(m_target.poseCenter), m_target.rot};
if (InputPose(&pose)) {
m_target.poseCenter = ffd.GetScreenFromPos(pose.Translation());
m_target.rot = pose.Rotation().Radians();
m_target.objModel->SetPose(m_target.index, pose);
}
if (ImGui::Button("Delete Pose")) {
auto posesRef = m_target.objModel->GetPoses();
std::vector<frc::Pose2d> poses{posesRef.begin(), posesRef.end()};
if (m_target.index < poses.size()) {
poses.erase(poses.begin() + m_target.index);
m_target.objModel->SetPoses(poses);
}
ImGui::CloseCurrentPopup();
}
if (ImGui::Button("Delete Object (ALL Poses)")) {
model->RemoveFieldObject(m_target.name);
ImGui::CloseCurrentPopup();
}
}
void PopupState::DisplayInsert(Field2DModel* model) {
ImGui::TextUnformatted("Insert New Pose");
InputPose(&m_insertPoses[m_insertIndex]);
const char* insertName = m_insertModel ? m_insertName.c_str() : "<new>";
if (ImGui::BeginCombo("Object", insertName)) {
bool selected = !m_insertModel;
if (ImGui::Selectable("<new>", selected)) {
m_insertModel = nullptr;
auto pose = m_insertPoses[m_insertIndex];
m_insertPoses.resize(0);
m_insertPoses.emplace_back(std::move(pose));
m_insertName.clear();
m_insertIndex = 0;
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
model->ForEachFieldObject([&](auto& objModel, auto name) {
bool selected = m_insertModel == &objModel;
if (ImGui::Selectable(name.data(), selected)) {
m_insertModel = &objModel;
auto pose = m_insertPoses[m_insertIndex];
auto posesRef = objModel.GetPoses();
m_insertPoses.assign(posesRef.begin(), posesRef.end());
m_insertPoses.emplace_back(std::move(pose));
m_insertName = name;
m_insertIndex = m_insertPoses.size() - 1;
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
});
ImGui::EndCombo();
}
if (m_insertModel) {
int oldIndex = m_insertIndex;
if (ImGui::InputInt("Pos", &m_insertIndex, 1, 5)) {
if (m_insertIndex < 0) {
m_insertIndex = 0;
}
size_t size = m_insertPoses.size();
if (static_cast<size_t>(m_insertIndex) >= size) {
m_insertIndex = size - 1;
}
if (m_insertIndex < oldIndex) {
auto begin = m_insertPoses.begin();
std::rotate(begin + m_insertIndex, begin + oldIndex,
begin + oldIndex + 1);
} else if (m_insertIndex > oldIndex) {
auto rbegin = m_insertPoses.rbegin();
std::rotate(rbegin + (size - m_insertIndex), rbegin + (size - oldIndex),
rbegin + (size - oldIndex - 1));
}
}
} else {
ImGui::InputText("Name", &m_insertName);
}
if (ImGui::Button("Apply")) {
if (m_insertModel) {
m_insertModel->SetPoses(m_insertPoses);
} else if (!m_insertName.empty()) {
model->AddFieldObject(m_insertName)->SetPoses(m_insertPoses);
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
}
}
void glass::DisplayField2D(Field2DModel* model, const ImVec2& contentSize) {
auto& storage = GetStorage();
auto field = storage.GetData<FieldInfo>();
if (!field) {
storage.SetData(std::make_shared<FieldInfo>(storage));
field = storage.GetData<FieldInfo>();
}
if (contentSize.x <= 0 || contentSize.y <= 0) {
return;
}
static FieldDisplay display;
display.Display(field, model, contentSize);
}
void Field2DView::Display() {
if (ImGui::BeginPopupContextItem()) {
DisplayField2DSettings(m_model);
ImGui::EndPopup();
}
DisplayField2D(m_model, ImGui::GetWindowContentRegionMax() -
ImGui::GetWindowContentRegionMin());
}