feat(types): Type Editor panel — CRUD de tipos en vivo (issue 0007)

- views_type_editor: panel "Types" con tabs Entities/Relations.
  Entities: name, color picker, shape combo, icon (ti-* + cp preview),
  principal_field combo, tabla de Fields (string/int/float/bool/date/url/enum)
  con required y enum values CSV; up/down/X por fila.
  Relations: name, color, style.
  Footer Save / Reload from disk + indicador dirty + error inline.
- views_type_editor_delete_modal: confirm con conteo de entidades en uso.
- types_registry: shape_name() + shape: emit en types_save_yaml para
  round-trip estable de la cosmetica editada en UI.
- main.cpp: panel "Types" en g_panels; init types_draft tras load_input;
  want_types_save -> save + apply_types_yaml + rebuild atlas + bind +
  refresh inspector caches; want_types_reload simetrico; conteo de
  uso desde operations.db cuando se abre el modal de delete.
This commit is contained in:
2026-05-01 00:42:30 +02:00
parent 69f1afcf9e
commit f80348d604
5 changed files with 549 additions and 1 deletions
+94
View File
@@ -26,6 +26,8 @@
#include "entity_ops.h"
#include "project_manager.h"
#include "../../../../cpp/vendor/sqlite3/sqlite3.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -206,6 +208,10 @@ static bool load_input() {
g_app.parsed_types = std::move(pt);
}
}
// Inicializar el draft del Type Editor con copia de parsed_types (0007).
g_app.types_draft = g_app.parsed_types;
g_app.types_dirty = false;
g_app.types_save_error.clear();
// Restablecer viewport state (preserva camara user-visible)
g_viewport.selection.clear();
@@ -444,6 +450,7 @@ static fn_ui::PanelToggle g_panels[] = {
{"Inspector", nullptr, &g_app.panel_inspector},
{"Stats", nullptr, &g_app.panel_stats},
{"Note", nullptr, &g_app.panel_note},
{"Types", nullptr, &g_app.panel_type_editor},
};
static void render() {
@@ -573,6 +580,86 @@ static void render() {
load_input();
}
// ---- Type Editor (issue 0007) ----
if (g_app.want_types_save) {
g_app.want_types_save = false;
g_app.types_save_error.clear();
if (g_types_path.empty()) {
g_app.types_save_error =
"No hay types.yaml asignado (abre un proyecto o usa --types).";
} else {
std::string err;
if (!ge::types_save_yaml(g_types_path.c_str(),
g_app.types_draft, &err)) {
g_app.types_save_error = "Save failed: " + err;
} else {
std::fprintf(stdout,
"[graph_explorer] types.yaml saved -> %s\n",
g_types_path.c_str());
g_app.parsed_types = g_app.types_draft;
std::vector<uint16_t> cps =
ge::apply_types_yaml(g_graph, g_app.parsed_types);
if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; }
g_atlas = ge::build_icon_atlas(cps);
g_atlas_bound = false;
g_gpu_dirty = true;
g_app.types_dirty = false;
ge::views_inspector_refresh_caches(g_app);
}
}
}
if (g_app.want_types_reload) {
g_app.want_types_reload = false;
g_app.types_save_error.clear();
if (g_types_path.empty()) {
// Sin types.yaml en disco: descarta el draft a parsed_types actual.
g_app.types_draft = g_app.parsed_types;
g_app.types_dirty = false;
} else {
ge::ParsedTypes pt;
std::string err;
if (!ge::types_load_yaml(g_types_path.c_str(), &pt, &err)) {
g_app.types_save_error = "Reload failed: " + err;
} else {
std::vector<uint16_t> cps = ge::apply_types_yaml(g_graph, pt);
if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; }
g_atlas = ge::build_icon_atlas(cps);
g_atlas_bound = false;
g_gpu_dirty = true;
g_app.parsed_types = pt;
g_app.types_draft = std::move(pt);
g_app.types_dirty = false;
ge::views_inspector_refresh_caches(g_app);
}
}
}
// Conteo de uso para el modal de borrado (entidades activas en BD).
if (g_app.show_te_delete_modal && g_app.te_delete_use_count == 0
&& !g_app.input_db_path.empty()) {
const char* tname = nullptr;
if (g_app.te_pending_delete_e >= 0
&& g_app.te_pending_delete_e < (int)g_app.types_draft.entities.size()) {
tname = g_app.types_draft.entities[g_app.te_pending_delete_e].name.c_str();
}
if (tname && *tname) {
sqlite3* db = nullptr;
if (sqlite3_open_v2(g_app.input_db_path.c_str(), &db,
SQLITE_OPEN_READONLY, nullptr) == SQLITE_OK) {
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db,
"SELECT COUNT(*) FROM entities WHERE type_ref = ?",
-1, &st, nullptr) == SQLITE_OK) {
sqlite3_bind_text(st, 1, tname, -1, SQLITE_TRANSIENT);
if (sqlite3_step(st) == SQLITE_ROW) {
g_app.te_delete_use_count = sqlite3_column_int(st, 0);
}
sqlite3_finalize(st);
}
sqlite3_close(db);
}
}
}
// ---- Mutaciones (add/delete/duplicate/change_type) ----
auto reload_after_mutation = [&]() {
graph::GraphLoadStats stats{};
@@ -845,6 +932,13 @@ static void render() {
ImGui::SetNextWindowSize(ImVec2(700.0f, 480.0f), ImGuiCond_FirstUseEver);
ge::views_note(g_app);
// Type Editor (issue 0007) — flotante, dockeable.
ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + W * 0.20f, top + 40.0f),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(720.0f, 500.0f), ImGuiCond_FirstUseEver);
ge::views_type_editor(g_app);
ge::views_type_editor_delete_modal(g_app);
g_first_render = false;
}