feat: editable properties
CI / build-and-test (push) Successful in 2m19s

This commit is contained in:
2026-03-09 13:24:39 +13:00
parent cd754fda28
commit 2a74648ffd
3 changed files with 378 additions and 3 deletions
+35
View File
@@ -7,6 +7,9 @@
#include "LogWindow.h" // Include the new LogWindow class
#include <entt.hpp>
#include <string>
#include <unordered_map>
#include <vector>
#include <filesystem>
class Application;
@@ -30,8 +33,37 @@ private:
void RenderEntityNode(entt::entity entity);
bool IsEntityAncestor(entt::entity ancestor, entt::entity entity) const;
const char* GetEntityLabel(entt::entity entity);
struct ScriptEditableFieldMeta {
std::string name;
std::string type;
bool hasRange = false;
float rangeMin = 0.0f;
float rangeMax = 1.0f;
std::vector<std::string> options;
};
struct ScriptEditableCache {
std::string className;
std::vector<ScriptEditableFieldMeta> fields;
std::filesystem::file_time_type lastWriteTime;
bool hasWriteTime = false;
};
enum class ScriptValueKind { Float, Int, Bool, String };
struct ScriptEditableValue {
ScriptValueKind kind = ScriptValueKind::Float;
bool hasValue = false;
float f = 0.0f;
int i = 0;
bool b = false;
std::string s;
};
void SyncTagBuffer(entt::entity entity);
void SyncScriptPathBuffer(entt::entity entity);
void RenderScriptEditableControls(entt::entity entity);
ScriptEditableCache &GetOrParseEditableCache(const std::string &path, const std::string &className);
bool showLogWindow = true;
bool showSceneWindow = true;
@@ -52,4 +84,7 @@ private:
char sceneLoadPath[260] = {0};
char sceneSavePath[260] = {0};
int addComponentIndex = 0;
std::unordered_map<std::string, ScriptEditableCache> scriptEditableCache;
std::unordered_map<entt::entity, std::unordered_map<std::string, ScriptEditableValue>> scriptEditableStaged;
};
+3 -3
View File
@@ -6,11 +6,11 @@ class EntityScript
float baseX;
float baseY;
float baseZ;
float phase;
[editable][range[0,1]]float phase;
// per-instance movement parameters (can be tuned per-entity)
float amplitude = 2.5f;
float speed = 2.5f;
[editable][range[0,10]]float amplitude = 2.5f;
[editable][range[0,10]]float speed = 2.5f;
EntityScript()
{
+340
View File
@@ -5,9 +5,11 @@
#include <iostream>
#include <vector>
#include <unordered_set>
#include <unordered_map>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <sstream>
#include <entt.hpp>
#include "log.h"
#include "Application.h"
@@ -768,6 +770,8 @@ void GuiManager::RenderInspectorWindow()
}
ImGui::PopID();
RenderScriptEditableControls(selectedEntity);
}
// Camera
@@ -912,6 +916,342 @@ void GuiManager::RenderInspectorWindow()
ImGui::End();
}
static std::string TrimString(const std::string &value)
{
size_t start = value.find_first_not_of(" \t\r\n");
if (start == std::string::npos) return std::string();
size_t end = value.find_last_not_of(" \t\r\n");
return value.substr(start, end - start + 1);
}
static bool ExtractTagContent(const std::string &line, const std::string &tag, std::string &out)
{
std::string needle = "[" + tag + "[";
size_t start = line.find(needle);
if (start == std::string::npos) return false;
start += needle.size();
size_t end = line.find("]]", start);
if (end == std::string::npos) return false;
out = line.substr(start, end - start);
return true;
}
static std::vector<std::string> SplitOptions(const std::string &value)
{
std::vector<std::string> parts;
std::stringstream ss(value);
std::string item;
while (std::getline(ss, item, ',')) {
parts.push_back(TrimString(item));
}
return parts;
}
static bool TryParseRange(const std::string &value, float &outMin, float &outMax)
{
size_t comma = value.find(',');
if (comma == std::string::npos) return false;
std::string left = TrimString(value.substr(0, comma));
std::string right = TrimString(value.substr(comma + 1));
try {
outMin = std::stof(left);
outMax = std::stof(right);
return true;
} catch (...) {
return false;
}
}
GuiManager::ScriptEditableCache &GuiManager::GetOrParseEditableCache(const std::string &path, const std::string &className)
{
std::string key = path + "::" + className;
auto &cache = scriptEditableCache[key];
cache.className = className;
bool needsParse = true;
try {
auto writeTime = std::filesystem::last_write_time(path);
if (cache.hasWriteTime && cache.lastWriteTime == writeTime) {
needsParse = false;
} else {
cache.lastWriteTime = writeTime;
cache.hasWriteTime = true;
}
} catch (const std::exception &) {
// If file is missing or time cannot be read, parse anyway (will be empty).
}
if (!needsParse) return cache;
cache.fields.clear();
std::ifstream file(path);
if (!file.is_open()) {
return cache;
}
std::string line;
bool inClass = false;
int braceDepth = 0;
while (std::getline(file, line)) {
std::string trimmed = TrimString(line);
if (!inClass) {
if (trimmed.find("class " + className) != std::string::npos) {
inClass = true;
for (char c : trimmed) {
if (c == '{') braceDepth++;
}
}
continue;
}
for (char c : line) {
if (c == '{') braceDepth++;
if (c == '}') braceDepth--;
}
if (braceDepth <= 0) {
inClass = false;
continue;
}
if (line.find("[editable]") == std::string::npos) continue;
ScriptEditableFieldMeta meta;
meta.hasRange = false;
meta.rangeMin = 0.0f;
meta.rangeMax = 1.0f;
std::string rangeContent;
if (ExtractTagContent(line, "range", rangeContent)) {
float minVal = 0.0f;
float maxVal = 1.0f;
if (TryParseRange(rangeContent, minVal, maxVal)) {
meta.hasRange = true;
meta.rangeMin = minVal;
meta.rangeMax = maxVal;
}
}
std::string optionsContent;
if (ExtractTagContent(line, "options", optionsContent)) {
meta.options = SplitOptions(optionsContent);
}
size_t lastTag = line.find_last_of(']');
if (lastTag == std::string::npos) continue;
std::string declaration = TrimString(line.substr(lastTag + 1));
if (declaration.empty()) continue;
std::stringstream declStream(declaration);
declStream >> meta.type;
std::string nameToken;
declStream >> nameToken;
if (meta.type.empty() || nameToken.empty()) continue;
size_t equalPos = nameToken.find('=');
if (equalPos != std::string::npos) {
nameToken = nameToken.substr(0, equalPos);
}
if (!nameToken.empty() && nameToken.back() == ';') {
nameToken.pop_back();
}
meta.name = TrimString(nameToken);
if (!meta.name.empty()) {
cache.fields.push_back(meta);
}
}
return cache;
}
void GuiManager::RenderScriptEditableControls(entt::entity entity)
{
auto &registry = app->GetRegistry();
auto &script = registry.get<ScriptComponent>(entity);
if (!script.enabled || script.scriptPath.empty() || !script.scriptInstance) return;
auto *obj = reinterpret_cast<asIScriptObject *>(script.scriptInstance);
if (!obj) return;
asITypeInfo *objType = obj->GetObjectType();
if (!objType) return;
asIScriptEngine *engine = obj->GetEngine();
if (!engine) return;
const char *className = objType->GetName();
if (!className) return;
auto &cache = GetOrParseEditableCache(script.scriptPath, className);
if (cache.fields.empty()) return;
ImGui::Separator();
ImGui::Text("Editable");
ImGui::PushID("ScriptEditable");
int floatTypeId = engine->GetTypeIdByDecl("float");
int intTypeId = engine->GetTypeIdByDecl("int");
int boolTypeId = engine->GetTypeIdByDecl("bool");
int stringTypeId = engine->GetTypeIdByDecl("string");
auto &stagedMap = scriptEditableStaged[entity];
for (const auto &field : cache.fields) {
int propertyIndex = -1;
int propertyTypeId = 0;
const int propertyCount = static_cast<int>(obj->GetPropertyCount());
for (int i = 0; i < propertyCount; ++i) {
const char *propName = obj->GetPropertyName(i);
if (propName && field.name == propName) {
propertyIndex = i;
propertyTypeId = obj->GetPropertyTypeId(i);
break;
}
}
if (propertyIndex < 0) continue;
void *propAddr = obj->GetAddressOfProperty(propertyIndex);
if (!propAddr) continue;
ScriptEditableValue &staged = stagedMap[field.name];
if (!staged.hasValue) {
if (propertyTypeId == floatTypeId) {
staged.kind = ScriptValueKind::Float;
staged.f = *reinterpret_cast<float *>(propAddr);
staged.hasValue = true;
} else if (propertyTypeId == intTypeId) {
staged.kind = ScriptValueKind::Int;
staged.i = *reinterpret_cast<int *>(propAddr);
staged.hasValue = true;
} else if (propertyTypeId == boolTypeId) {
staged.kind = ScriptValueKind::Bool;
staged.b = *reinterpret_cast<bool *>(propAddr);
staged.hasValue = true;
} else if (propertyTypeId == stringTypeId) {
staged.kind = ScriptValueKind::String;
staged.s = *reinterpret_cast<std::string *>(propAddr);
staged.hasValue = true;
} else {
continue;
}
}
ImGui::PushID(field.name.c_str());
if (staged.kind == ScriptValueKind::Float) {
if (!field.options.empty()) {
int currentIndex = 0;
float currentValue = staged.f;
int maxIndex = static_cast<int>(field.options.size()) - 1;
if (maxIndex < 0) maxIndex = 0;
currentIndex = static_cast<int>(currentValue);
currentIndex = std::clamp(currentIndex, 0, maxIndex);
if (ImGui::SliderInt(field.name.c_str(), &currentIndex, 0, maxIndex)) {
staged.f = static_cast<float>(currentIndex);
}
} else if (field.hasRange) {
if (ImGui::SliderFloat(field.name.c_str(), &staged.f, field.rangeMin, field.rangeMax)) {
staged.hasValue = true;
}
} else {
if (ImGui::DragFloat(field.name.c_str(), &staged.f, 0.1f)) {
staged.hasValue = true;
}
}
} else if (staged.kind == ScriptValueKind::Int) {
if (!field.options.empty()) {
int currentIndex = staged.i;
int maxIndex = static_cast<int>(field.options.size()) - 1;
if (maxIndex < 0) maxIndex = 0;
currentIndex = std::clamp(currentIndex, 0, maxIndex);
if (ImGui::SliderInt(field.name.c_str(), &currentIndex, 0, maxIndex)) {
staged.i = currentIndex;
}
} else if (field.hasRange) {
if (ImGui::SliderInt(field.name.c_str(), &staged.i, static_cast<int>(field.rangeMin), static_cast<int>(field.rangeMax))) {
staged.hasValue = true;
}
} else {
if (ImGui::DragInt(field.name.c_str(), &staged.i, 1.0f)) {
staged.hasValue = true;
}
}
} else if (staged.kind == ScriptValueKind::Bool) {
if (ImGui::Checkbox(field.name.c_str(), &staged.b)) {
staged.hasValue = true;
}
} else if (staged.kind == ScriptValueKind::String) {
if (!field.options.empty()) {
int currentIndex = 0;
for (size_t i = 0; i < field.options.size(); ++i) {
if (field.options[i] == staged.s) {
currentIndex = static_cast<int>(i);
break;
}
}
if (ImGui::Combo(field.name.c_str(), &currentIndex, [](void *data, int idx, const char **outText) {
auto *opts = reinterpret_cast<std::vector<std::string> *>(data);
if (idx < 0 || idx >= static_cast<int>(opts->size())) return false;
*outText = (*opts)[idx].c_str();
return true;
}, (void *)&field.options, static_cast<int>(field.options.size()))) {
if (currentIndex >= 0 && currentIndex < static_cast<int>(field.options.size())) {
staged.s = field.options[currentIndex];
}
}
} else {
char buffer[256];
std::snprintf(buffer, sizeof(buffer), "%s", staged.s.c_str());
if (ImGui::InputText(field.name.c_str(), buffer, sizeof(buffer))) {
staged.s = buffer;
staged.hasValue = true;
}
}
}
ImGui::PopID();
}
if (ImGui::Button("Apply")) {
for (const auto &field : cache.fields) {
auto it = stagedMap.find(field.name);
if (it == stagedMap.end()) continue;
ScriptEditableValue &staged = it->second;
if (!staged.hasValue) continue;
int propertyIndex = -1;
int propertyTypeId = 0;
const int propertyCount = static_cast<int>(obj->GetPropertyCount());
for (int i = 0; i < propertyCount; ++i) {
const char *propName = obj->GetPropertyName(i);
if (propName && field.name == propName) {
propertyIndex = i;
propertyTypeId = obj->GetPropertyTypeId(i);
break;
}
}
if (propertyIndex < 0) continue;
void *propAddr = obj->GetAddressOfProperty(propertyIndex);
if (!propAddr) continue;
if (propertyTypeId == floatTypeId && staged.kind == ScriptValueKind::Float) {
*reinterpret_cast<float *>(propAddr) = staged.f;
} else if (propertyTypeId == intTypeId && staged.kind == ScriptValueKind::Int) {
*reinterpret_cast<int *>(propAddr) = staged.i;
} else if (propertyTypeId == boolTypeId && staged.kind == ScriptValueKind::Bool) {
*reinterpret_cast<bool *>(propAddr) = staged.b;
} else if (propertyTypeId == stringTypeId && staged.kind == ScriptValueKind::String) {
*reinterpret_cast<std::string *>(propAddr) = staged.s;
}
}
}
ImGui::SameLine();
if (ImGui::Button("Reset")) {
stagedMap.clear();
}
ImGui::PopID();
}
void GuiManager::RenderErrorBanner()
{
if (!app || !app->HasScriptCompilationError())