diff --git a/fieldImages/CMakeLists.txt b/fieldImages/CMakeLists.txt index 90140f47a1..b79742efa5 100644 --- a/fieldImages/CMakeLists.txt +++ b/fieldImages/CMakeLists.txt @@ -25,7 +25,7 @@ endif() GENERATE_RESOURCES(src/main/native/resources/edu/wpi/first/fields generated/main/cpp FIELDS fields field_images_resources_src) -add_library(fieldImages ${field_images_resources_src}) +add_library(fieldImages src/main/native/cpp/fields.cpp ${field_images_resources_src}) set_target_properties(fieldImages PROPERTIES DEBUG_POSTFIX "d") set_property(TARGET fieldImages PROPERTY FOLDER "libraries") diff --git a/fieldImages/build.gradle b/fieldImages/build.gradle index 123e89d4db..2f18d7279d 100644 --- a/fieldImages/build.gradle +++ b/fieldImages/build.gradle @@ -52,7 +52,7 @@ if (!project.hasProperty('onlylinuxathena')) { sources { cpp { source { - srcDirs "$buildDir/generated/main/cpp" + srcDirs 'src/main/native/cpp', "$buildDir/generated/main/cpp" include '**/*.cpp' } exportedHeaders { diff --git a/fieldImages/src/main/native/cpp/fields.cpp b/fieldImages/src/main/native/cpp/fields.cpp new file mode 100644 index 0000000000..4f79e6bf77 --- /dev/null +++ b/fieldImages/src/main/native/cpp/fields.cpp @@ -0,0 +1,48 @@ +// 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 "fields/fields.h" + +#include "fields/2018-powerup.h" +#include "fields/2019-deepspace.h" +#include "fields/2020-infiniterecharge.h" +#include "fields/2021-barrel.h" +#include "fields/2021-bounce.h" +#include "fields/2021-galacticsearcha.h" +#include "fields/2021-galacticsearchb.h" +#include "fields/2021-infiniterecharge.h" +#include "fields/2021-slalom.h" +#include "fields/2022-rapidreact.h" +#include "fields/2023-chargedup.h" + +using namespace fields; + +static const Field kFields[] = { + {"2023 Charged Up", GetResource_2023_chargedup_json, + GetResource_2023_field_png}, + {"2022 Rapid React", GetResource_2022_rapidreact_json, + GetResource_2022_field_png}, + {"2021 Barrel Racing Path", GetResource_2021_barrelracingpath_json, + GetResource_2021_barrel_png}, + {"2021 Bounce Path", GetResource_2021_bouncepath_json, + GetResource_2021_bounce_png}, + {"2021 Galactic Search A", GetResource_2021_galacticsearcha_json, + GetResource_2021_galacticsearcha_png}, + {"2021 Galactic Search B", GetResource_2021_galacticsearchb_json, + GetResource_2021_galacticsearchb_png}, + {"2021 Infinite Recharge", GetResource_2021_infiniterecharge_json, + GetResource_2021_field_png}, + {"2021 Slalom Path", GetResource_2021_slalompath_json, + GetResource_2021_slalom_png}, + {"2020 Infinite Recharge", GetResource_2020_infiniterecharge_json, + GetResource_2020_field_png}, + {"2019 Destination: Deep Space", GetResource_2019_deepspace_json, + GetResource_2019_field_jpg}, + {"2018 Power Up", GetResource_2018_powerup_json, + GetResource_2018_field_jpg}, +}; + +std::span fields::GetFields() { + return kFields; +} diff --git a/fieldImages/src/main/native/include/fields/fields.h b/fieldImages/src/main/native/include/fields/fields.h new file mode 100644 index 0000000000..ea6b23e6f6 --- /dev/null +++ b/fieldImages/src/main/native/include/fields/fields.h @@ -0,0 +1,20 @@ +// 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. + +#pragma once + +#include +#include + +namespace fields { + +struct Field { + const char* name; + std::string_view (*getJson)(); + std::string_view (*getImage)(); +}; + +std::span GetFields(); + +} // namespace fields diff --git a/glass/.styleguide b/glass/.styleguide index c2a291a78d..974eaa954b 100644 --- a/glass/.styleguide +++ b/glass/.styleguide @@ -22,6 +22,7 @@ includeOtherLibs { ^GLFW ^cscore ^fmt/ + ^fields/ ^frc/ ^imgui ^networktables/ diff --git a/glass/CMakeLists.txt b/glass/CMakeLists.txt index 41a7819343..a252c2e834 100644 --- a/glass/CMakeLists.txt +++ b/glass/CMakeLists.txt @@ -16,7 +16,7 @@ set_property(TARGET libglass PROPERTY POSITION_INDEPENDENT_CODE ON) set_property(TARGET libglass PROPERTY FOLDER "libraries") wpilib_target_warnings(libglass) -target_link_libraries(libglass PUBLIC wpigui wpimath wpiutil) +target_link_libraries(libglass PUBLIC wpigui wpimath wpiutil fieldImages) target_include_directories(libglass PUBLIC $ diff --git a/glass/build.gradle b/glass/build.gradle index 47b943c45e..abe7d7a810 100644 --- a/glass/build.gradle +++ b/glass/build.gradle @@ -113,6 +113,7 @@ if (!project.hasProperty('onlylinuxathena')) { lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' lib project: ':wpigui', library: 'wpigui', linkage: 'static' + lib project: ':fieldImages', library: 'fieldImages', linkage: 'shared' nativeUtils.useRequiredLibrary(it, 'imgui') } appendDebugPathToBinaries(binaries) @@ -144,6 +145,7 @@ if (!project.hasProperty('onlylinuxathena')) { lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' lib project: ':wpimath', library: 'wpimath', linkage: 'shared' lib project: ':wpigui', library: 'wpigui', linkage: 'static' + lib project: ':fieldImages', library: 'fieldImages', linkage: 'shared' nativeUtils.useRequiredLibrary(it, 'imgui') } appendDebugPathToBinaries(binaries) @@ -183,6 +185,7 @@ if (!project.hasProperty('onlylinuxathena')) { lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' lib project: ':wpimath', library: 'wpimath', linkage: 'static' lib project: ':wpigui', library: 'wpigui', linkage: 'static' + lib project: ':fieldImages', library: 'fieldImages', linkage: 'static' nativeUtils.useRequiredLibrary(it, 'opencv_static') nativeUtils.useRequiredLibrary(it, 'imgui') if (it.targetPlatform.operatingSystem.isWindows()) { diff --git a/glass/src/lib/native/cpp/other/Field2D.cpp b/glass/src/lib/native/cpp/other/Field2D.cpp index 66e90b18f1..67946da6fd 100644 --- a/glass/src/lib/native/cpp/other/Field2D.cpp +++ b/glass/src/lib/native/cpp/other/Field2D.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -237,10 +238,12 @@ class FieldInfo { private: void Reset(); bool LoadImageImpl(const std::string& fn); - void LoadJson(std::string_view jsonfile); + bool LoadJson(wpi::raw_istream& is, std::string_view filename); + void LoadJsonFile(std::string_view jsonfile); std::unique_ptr m_fileOpener; + std::string& m_builtin; std::string& m_filename; gui::Texture m_texture; @@ -340,7 +343,8 @@ static bool InputPose(frc::Pose2d* pose) { } FieldInfo::FieldInfo(Storage& storage) - : m_filename{storage.GetString("image")}, + : m_builtin{storage.GetString("builtin")}, + m_filename{storage.GetString("image")}, m_width{storage.GetFloat("width", kDefaultWidth.to())}, m_height{storage.GetFloat("height", kDefaultHeight.to())}, m_top{storage.GetInt("top", 0)}, @@ -349,7 +353,25 @@ FieldInfo::FieldInfo(Storage& storage) m_right{storage.GetInt("right", -1)} {} void FieldInfo::DisplaySettings() { - if (ImGui::Button("Choose image...")) { + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10); + if (ImGui::BeginCombo("Image", + m_builtin.empty() ? "Custom" : m_builtin.c_str())) { + if (ImGui::Selectable("Custom", m_builtin.empty())) { + Reset(); + } + for (auto&& field : fields::GetFields()) { + bool selected = field.name == m_builtin; + if (ImGui::Selectable(field.name, selected)) { + Reset(); + m_builtin = field.name; + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + if (m_builtin.empty() && ImGui::Button("Load image...")) { m_fileOpener = std::make_unique( "Choose field image", "", std::vector{"Image File", @@ -370,6 +392,7 @@ void FieldInfo::DisplaySettings() { void FieldInfo::Reset() { m_texture = gui::Texture{}; + m_builtin.clear(); m_filename.clear(); m_imageWidth = 0; m_imageHeight = 0; @@ -384,7 +407,7 @@ void FieldInfo::LoadImage() { auto result = m_fileOpener->result(); if (!result.empty()) { if (wpi::ends_with(result[0], ".json")) { - LoadJson(result[0]); + LoadJsonFile(result[0]); } else { LoadImageImpl(result[0].c_str()); m_top = 0; @@ -395,33 +418,47 @@ void FieldInfo::LoadImage() { } m_fileOpener.reset(); } - if (!m_texture && !m_filename.empty()) { - if (!LoadImageImpl(m_filename)) { - m_filename.clear(); + if (!m_texture) { + if (!m_builtin.empty()) { + for (auto&& field : fields::GetFields()) { + if (field.name == m_builtin) { + auto jsonstr = field.getJson(); + wpi::raw_mem_istream is{jsonstr.data(), jsonstr.size()}; + auto imagedata = field.getImage(); + auto texture = gui::Texture::CreateFromImage( + reinterpret_cast(imagedata.data()), + imagedata.size()); + if (texture && LoadJson(is, {})) { + m_texture = std::move(texture); + m_imageWidth = m_texture.GetWidth(); + m_imageHeight = m_texture.GetHeight(); + } else { + m_builtin.clear(); + } + } + } + } else if (!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; - } - +bool FieldInfo::LoadJson(wpi::raw_istream& is, std::string_view filename) { // parse file wpi::json j; try { - j = wpi::json::parse(f); + j = wpi::json::parse(is); } catch (const wpi::json::parse_error& e) { fmt::print(stderr, "GUI: JSON: could not parse: {}\n", e.what()); + return false; } // top level must be an object if (!j.is_object()) { std::fputs("GUI: JSON: does not contain a top object\n", stderr); - return; + return false; } // image filename @@ -430,7 +467,7 @@ void FieldInfo::LoadJson(std::string_view jsonfile) { image = j.at("field-image").get(); } catch (const wpi::json::exception& e) { fmt::print(stderr, "GUI: JSON: could not read field-image: {}\n", e.what()); - return; + return false; } // corners @@ -443,7 +480,7 @@ void FieldInfo::LoadJson(std::string_view jsonfile) { } catch (const wpi::json::exception& e) { fmt::print(stderr, "GUI: JSON: could not read field-corners: {}\n", e.what()); - return; + return false; } // size @@ -454,7 +491,7 @@ void FieldInfo::LoadJson(std::string_view jsonfile) { height = j.at("field-size").at(1).get(); } catch (const wpi::json::exception& e) { fmt::print(stderr, "GUI: JSON: could not read field-size: {}\n", e.what()); - return; + return false; } // units for size @@ -463,7 +500,7 @@ void FieldInfo::LoadJson(std::string_view jsonfile) { unit = j.at("field-unit").get(); } catch (const wpi::json::exception& e) { fmt::print(stderr, "GUI: JSON: could not read field-unit: {}\n", e.what()); - return; + return false; } // convert size units to meters @@ -472,22 +509,35 @@ void FieldInfo::LoadJson(std::string_view jsonfile) { height = units::convert(height); } - // the image filename is relative to the json file - auto pathname = fs::path{jsonfile}.replace_filename(image).string(); + if (!filename.empty()) { + // the image filename is relative to the json file + auto pathname = fs::path{filename}.replace_filename(image).string(); - // load field image - if (!LoadImageImpl(pathname.c_str())) { - return; + // load field image + if (!LoadImageImpl(pathname.c_str())) { + return false; + } + m_filename = pathname; } // 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; + return true; +} + +void FieldInfo::LoadJsonFile(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; + } + LoadJson(f, jsonfile); } bool FieldInfo::LoadImageImpl(const std::string& fn) { diff --git a/outlineviewer/build.gradle b/outlineviewer/build.gradle index bb4a4cad01..8523c6d4cf 100644 --- a/outlineviewer/build.gradle +++ b/outlineviewer/build.gradle @@ -105,6 +105,7 @@ if (!project.hasProperty('onlylinuxathena')) { lib project: ':wpinet', library: 'wpinet', linkage: 'static' lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' lib project: ':wpigui', library: 'wpigui', linkage: 'static' + lib project: ':fieldImages', library: 'fieldImages', linkage: 'static' nativeUtils.useRequiredLibrary(it, 'imgui') if (it.targetPlatform.operatingSystem.isWindows()) { it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib' diff --git a/simulation/halsim_gui/build.gradle b/simulation/halsim_gui/build.gradle index a8608e38e5..d124d75d25 100644 --- a/simulation/halsim_gui/build.gradle +++ b/simulation/halsim_gui/build.gradle @@ -31,6 +31,7 @@ if (!project.hasProperty('onlylinuxathena')) { project(':ntcore').addNtcoreDependency(it, 'shared') lib project: ':wpinet', library: 'wpinet', linkage: 'shared' lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + lib project: ':fieldImages', library: 'fieldImages', linkage: 'static' nativeUtils.useRequiredLibrary(it, 'imgui') if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio) { it.buildable = false