feat: add reflection
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Serialization/Reflect.h>
|
||||
|
||||
Component::Component(GameObject& pParent, const std::string& name)
|
||||
: Object(name),
|
||||
@@ -50,6 +51,28 @@ void Component::FixedUpdate(float fixedDt) {
|
||||
}
|
||||
|
||||
void Component::ImGuiInspector() {
|
||||
InspectorDrawer drawer(m_ParentGameObjectPtr != nullptr ? m_ParentGameObjectPtr->GetScene() : nullptr);
|
||||
Reflect(drawer);
|
||||
if (drawer.Changed()) {
|
||||
OnFieldsEdited();
|
||||
}
|
||||
}
|
||||
|
||||
// Reflect also loads, so it can't be const, but the writer only reads the fields
|
||||
nlohmann::json Component::Serialize() const {
|
||||
JsonWriter writer;
|
||||
const_cast<Component*>(this)->Reflect(writer);
|
||||
return std::move(writer.Result());
|
||||
}
|
||||
|
||||
void Component::Deserialize(const nlohmann::json& data) {
|
||||
JsonReader reader(data);
|
||||
Reflect(reader);
|
||||
}
|
||||
|
||||
void Component::ResolveReferences(const ObjectMap& objects) {
|
||||
ReferenceResolver resolver(*this, objects);
|
||||
Reflect(resolver);
|
||||
}
|
||||
|
||||
void Component::ImGuiRender() {
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
#include <destrum/Serialization/Reflect.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] ImGuiDataType ToImGui(reflect_detail::Scalar type)
|
||||
{
|
||||
switch (type) {
|
||||
case reflect_detail::Scalar::S8: return ImGuiDataType_S8;
|
||||
case reflect_detail::Scalar::U8: return ImGuiDataType_U8;
|
||||
case reflect_detail::Scalar::S16: return ImGuiDataType_S16;
|
||||
case reflect_detail::Scalar::U16: return ImGuiDataType_U16;
|
||||
case reflect_detail::Scalar::S32: return ImGuiDataType_S32;
|
||||
case reflect_detail::Scalar::U32: return ImGuiDataType_U32;
|
||||
case reflect_detail::Scalar::S64: return ImGuiDataType_S64;
|
||||
case reflect_detail::Scalar::U64: return ImGuiDataType_U64;
|
||||
case reflect_detail::Scalar::Float: return ImGuiDataType_Float;
|
||||
case reflect_detail::Scalar::Double: return ImGuiDataType_Double;
|
||||
}
|
||||
return ImGuiDataType_Float;
|
||||
}
|
||||
|
||||
[[nodiscard]] ImGuiSliderFlags ClampFlags(const FieldOptions& options)
|
||||
{
|
||||
return options.min || options.max ? ImGuiSliderFlags_AlwaysClamp : ImGuiSliderFlags_None;
|
||||
}
|
||||
|
||||
template <int N>
|
||||
bool DrawFloats(const char* label, float* value, const FieldOptions& options)
|
||||
{
|
||||
if constexpr (N == 3) {
|
||||
if (options.color) {
|
||||
return ImGui::ColorEdit3(label, value);
|
||||
}
|
||||
} else if constexpr (N == 4) {
|
||||
if (options.color) {
|
||||
return ImGui::ColorEdit4(label, value);
|
||||
}
|
||||
}
|
||||
const float min = options.min ? static_cast<float>(*options.min) : 0.0f;
|
||||
const float max = options.max ? static_cast<float>(*options.max) : 0.0f;
|
||||
return reflect_detail::DrawScalar(label, reflect_detail::Scalar::Float, value, N,
|
||||
options.min ? &min : nullptr, options.max ? &max : nullptr, options);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string ObjectLabel(const ObjectRef& ref, const Scene& scene)
|
||||
{
|
||||
const GameObject* object = ref.Get(scene);
|
||||
return object != nullptr ? object->GetName() : "Missing object " + std::to_string(ref.GetId());
|
||||
}
|
||||
}
|
||||
|
||||
namespace reflect_detail {
|
||||
bool DrawScalar(const char* label, Scalar type, void* value, int components, const void* min, const void* max, const FieldOptions& options)
|
||||
{
|
||||
return ImGui::DragScalarN(label, ToImGui(type), value, components, options.speed, min, max, options.format, ClampFlags(options));
|
||||
}
|
||||
|
||||
bool DrawEnum(const char* label, std::int64_t& value, const FieldOptions& options)
|
||||
{
|
||||
if (options.enumNames.empty()) {
|
||||
return ImGui::InputScalar(label, ImGuiDataType_S64, &value);
|
||||
}
|
||||
|
||||
const auto count = static_cast<std::int64_t>(options.enumNames.size());
|
||||
const char* preview = value >= 0 && value < count ? options.enumNames[static_cast<std::size_t>(value)] : "?";
|
||||
bool changed = false;
|
||||
if (ImGui::BeginCombo(label, preview)) {
|
||||
for (std::int64_t i = 0; i < count; ++i) {
|
||||
if (ImGui::Selectable(options.enumNames[static_cast<std::size_t>(i)], i == value)) {
|
||||
value = i;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void BeginList(const char* label)
|
||||
{
|
||||
ImGui::SeparatorText(label);
|
||||
}
|
||||
|
||||
bool ListRemoveButton(std::size_t index)
|
||||
{
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
const bool clicked = ImGui::SmallButton("x");
|
||||
ImGui::SameLine();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
void EndListRow()
|
||||
{
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
bool ListAddButton()
|
||||
{
|
||||
return ImGui::SmallButton("+");
|
||||
}
|
||||
|
||||
std::string PrettifyFieldName(std::string_view key)
|
||||
{
|
||||
std::string label;
|
||||
label.reserve(key.size() + 4);
|
||||
for (std::size_t i = 0; i < key.size(); ++i) {
|
||||
const auto c = static_cast<unsigned char>(key[i]);
|
||||
if (c == '_') {
|
||||
label += ' ';
|
||||
continue;
|
||||
}
|
||||
const bool wordStart = i > 0 && std::isupper(c) && !std::isupper(static_cast<unsigned char>(key[i - 1])) && key[i - 1] != '_';
|
||||
if (wordStart) {
|
||||
label += ' ';
|
||||
}
|
||||
label += static_cast<char>(label.empty() || label.back() == ' ' ? std::toupper(c) : c);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
bool DrawField(const char* label, bool& value, const FieldOptions&, const Scene*)
|
||||
{
|
||||
return ImGui::Checkbox(label, &value);
|
||||
}
|
||||
|
||||
bool DrawField(const char* label, std::string& value, const FieldOptions&, const Scene*)
|
||||
{
|
||||
return ImGuiUtils::InputString(label, value);
|
||||
}
|
||||
|
||||
bool DrawField(const char* label, glm::vec2& value, const FieldOptions& options, const Scene*)
|
||||
{
|
||||
return DrawFloats<2>(label, &value.x, options);
|
||||
}
|
||||
|
||||
bool DrawField(const char* label, glm::vec3& value, const FieldOptions& options, const Scene*)
|
||||
{
|
||||
return DrawFloats<3>(label, &value.x, options);
|
||||
}
|
||||
|
||||
bool DrawField(const char* label, glm::vec4& value, const FieldOptions& options, const Scene*)
|
||||
{
|
||||
return DrawFloats<4>(label, &value.x, options);
|
||||
}
|
||||
|
||||
bool DrawField(const char* label, ObjectRef& value, const FieldOptions& options, const Scene* scene)
|
||||
{
|
||||
if (scene == nullptr) {
|
||||
ImGui::TextDisabled("%s: not in a scene", label);
|
||||
return false;
|
||||
}
|
||||
return ImGuiUtils::DrawObjectRefPicker(label, value, *scene, options.filter);
|
||||
}
|
||||
|
||||
bool DrawField(const char* label, std::vector<ObjectRef>& value, const FieldOptions& options, const Scene* scene)
|
||||
{
|
||||
reflect_detail::BeginList(label);
|
||||
if (scene == nullptr) {
|
||||
ImGui::TextDisabled("Not in a scene");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
std::optional<std::size_t> removeIndex;
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
if (reflect_detail::ListRemoveButton(i)) {
|
||||
removeIndex = i;
|
||||
}
|
||||
if (value[i].Get(*scene) != nullptr) {
|
||||
ImGui::TextUnformatted(ObjectLabel(value[i], *scene).c_str());
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", ObjectLabel(value[i], *scene).c_str());
|
||||
}
|
||||
reflect_detail::EndListRow();
|
||||
}
|
||||
if (removeIndex) {
|
||||
value.erase(value.begin() + static_cast<std::ptrdiff_t>(*removeIndex));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const auto canAdd = [&](const GameObject& object) {
|
||||
return !object.IsBeingDestroyed() && std::ranges::find(value, ObjectRef(object)) == value.end() &&
|
||||
(!options.filter || options.filter(object));
|
||||
};
|
||||
|
||||
if (ImGui::BeginCombo("##Add", "Add...")) {
|
||||
for (const auto& object : scene->GetObjects()) {
|
||||
if (object == nullptr || !canAdd(*object)) {
|
||||
continue;
|
||||
}
|
||||
ImGui::PushID(static_cast<int>(object->GetId()));
|
||||
if (ImGui::Selectable(object->GetName().c_str())) {
|
||||
value.emplace_back(*object);
|
||||
changed = true;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::GameObjectPayload)) {
|
||||
const GameObject* dropped = scene->FindGameObject(*static_cast<const ObjectId*>(payload->Data));
|
||||
if (dropped != nullptr && canAdd(*dropped)) {
|
||||
value.emplace_back(*dropped);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void ResolveField(ObjectRef& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped)
|
||||
{
|
||||
const ObjectId fileId = value.GetId();
|
||||
if (!value.Resolve(objects)) {
|
||||
dropped.push_back(fileId);
|
||||
}
|
||||
}
|
||||
|
||||
void ResolveField(std::vector<ObjectRef>& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped)
|
||||
{
|
||||
std::erase_if(value, [&](ObjectRef& ref) {
|
||||
const ObjectId fileId = ref.GetId();
|
||||
if (!ref.Resolve(objects)) {
|
||||
dropped.push_back(fileId);
|
||||
return true;
|
||||
}
|
||||
return !ref.IsSet();
|
||||
});
|
||||
}
|
||||
|
||||
void JsonWriter::Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops)
|
||||
{
|
||||
if (!options.transient) {
|
||||
ops.save(value, m_Result[std::string{key}], options);
|
||||
}
|
||||
}
|
||||
|
||||
void JsonReader::Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops)
|
||||
{
|
||||
if (options.transient || !m_Data.is_object()) {
|
||||
return;
|
||||
}
|
||||
const auto it = m_Data.find(std::string{key});
|
||||
if (it == m_Data.end()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ops.load(value, *it, options);
|
||||
} catch (const std::exception& exception) {
|
||||
throw std::runtime_error("\"" + std::string{key} + "\": " + exception.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ReferenceResolver::Visit(std::string_view key, void* value, const FieldOptions&, const FieldOps& ops)
|
||||
{
|
||||
if (ops.resolve == nullptr) {
|
||||
return;
|
||||
}
|
||||
std::vector<ObjectId> dropped;
|
||||
ops.resolve(value, m_Objects, dropped);
|
||||
const GameObject* owner = m_Owner.GetGameObject();
|
||||
for (const ObjectId id : dropped) {
|
||||
spdlog::warn("{} on {}: {} references object {} which is not in the scene file, dropping it",
|
||||
m_Owner.GetTypeName(), owner != nullptr ? owner->GetName() : "?", key, id);
|
||||
}
|
||||
}
|
||||
|
||||
void InspectorDrawer::Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops)
|
||||
{
|
||||
if (options.hidden) {
|
||||
return;
|
||||
}
|
||||
const std::string label = options.label != nullptr ? options.label : reflect_detail::PrettifyFieldName(key);
|
||||
ImGui::PushID(key.data(), key.data() + key.size());
|
||||
ImGui::BeginDisabled(options.readOnly);
|
||||
m_Changed |= ops.draw(label.c_str(), value, options, m_Scene);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopID();
|
||||
}
|
||||
Reference in New Issue
Block a user