From b798454f35e395163eede96d76b4ffc126f8e63c Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 1 May 2026 14:18:26 +0200 Subject: [PATCH] feat(table-node): edge CONTAINS_ROW al promover + tabla cuadrada real Tres ajustes derivados de feedback en uso: 1. tableview_promote_row recibe ahora `table_entity_id` y, si no es nulo, inserta una relacion 'CONTAINS_ROW' (id estable, INSERT OR IGNORE) entre la tabla origen y la entidad promovida. El viewport pinta la arista de pertenencia automaticamente sin codigo extra. 2. apply_types_yaml fija default_size = 32 px (world) para tipos Table junto al SHAPE_SQUARE ya existente. La GPU pinta el cuadrado real; antes era invisible bajo el overlay rectangular. 3. views_table_overlay adelgaza al rol que le toca: solo dibuja un contador discreto " rows" debajo del cuadrado (texto pequeno con bg semitransparente). El cuadrado en si lo pinta el GPU. Defensiva: views_table_windows_sync marca page_dirty=true en TODAS las windows live tras cada sync para que el flag promoted se refresque inmediatamente despues de promote/demote/import. --- main.cpp | 1 + tableview.cpp | 25 ++++++++++++++++++ tableview.h | 5 ++++ types_registry.cpp | 9 +++++-- views.cpp | 63 +++++++++++++++++++++++----------------------- 5 files changed, 70 insertions(+), 33 deletions(-) diff --git a/main.cpp b/main.cpp index caec24b..e64c3ca 100644 --- a/main.cpp +++ b/main.cpp @@ -880,6 +880,7 @@ static void render() { g_app.promote_table_id.c_str(), &m)) { char new_id[128] = {}; if (ge::tableview_promote_row(g_input_path.c_str(), + g_app.promote_table_id.c_str(), m.duckdb_path_abs.c_str(), m.table_name.c_str(), g_app.promote_row_id.c_str(), diff --git a/tableview.cpp b/tableview.cpp index 35723f3..6e94203 100644 --- a/tableview.cpp +++ b/tableview.cpp @@ -573,6 +573,7 @@ bool find_existing_promotion(const char* ops_db, const char* duckdb_path, } // namespace bool tableview_promote_row(const char* ops_db, + const char* table_entity_id, const char* duckdb_path, const char* duck_table, const char* row_id, @@ -682,6 +683,30 @@ bool tableview_promote_row(const char* ops_db, sqlite3_bind_text(st, 4, meta.c_str(), -1, SQLITE_TRANSIENT); bool ok = sqlite3_step(st) == SQLITE_DONE; sqlite3_finalize(st); + + // Inserta tambien la relacion CONTAINS_ROW de la tabla a la fila + // promovida — el viewport pintara la arista de pertenencia. + // Idempotente via INSERT OR IGNORE sobre id estable. + if (ok && table_entity_id && *table_entity_id) { + std::string rel_id = "rel_contains_" + sanitize_id_part(table_entity_id) + + "_" + sanitize_id_part(entity_id.c_str()); + const char* rins = + "INSERT OR IGNORE INTO relations(" + " id, name, from_entity, to_entity, status, tags, " + " created_at, updated_at) " + "VALUES (?, 'CONTAINS_ROW', ?, ?, 'implemented', '[]', " + " strftime('%Y-%m-%dT%H:%M:%fZ','now'), " + " strftime('%Y-%m-%dT%H:%M:%fZ','now'))"; + sqlite3_stmt* rst = nullptr; + if (sqlite3_prepare_v2(db, rins, -1, &rst, nullptr) == SQLITE_OK) { + sqlite3_bind_text(rst, 1, rel_id.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(rst, 2, table_entity_id, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(rst, 3, entity_id.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_step(rst); + sqlite3_finalize(rst); + } + } + sqlite3_close(db); if (ok && out_entity_id && out_id_n > 0) { std::snprintf(out_entity_id, out_id_n, "%s", entity_id.c_str()); diff --git a/tableview.h b/tableview.h index 2b3fa03..62dbe86 100644 --- a/tableview.h +++ b/tableview.h @@ -119,7 +119,12 @@ bool tableview_set_columns(const char* ops_db, const char* entity_id, // "prom__", e inserta en ops.entities // con type_ref=row_type, name = valor del label_column (o row_id si vacio), // metadata = { source: {duckdb, table, row_id}, }. +// +// Si table_entity_id no es nulo/vacio, inserta tambien una relacion +// CONTAINS_ROW (idempotente) entre la tabla y la nueva entidad para que el +// viewport pinte la arista de pertenencia. bool tableview_promote_row(const char* ops_db, + const char* table_entity_id, const char* duckdb_path, const char* duck_table, const char* row_id, diff --git a/types_registry.cpp b/types_registry.cpp index 7551b28..4082136 100644 --- a/types_registry.cpp +++ b/types_registry.cpp @@ -571,14 +571,19 @@ std::vector apply_types_yaml(GraphData& graph, const ParsedTypes& type } // Regla de forma: todo nodo es circulo EXCEPTO el tipo "Table" (issue - // 0010 — nodo tabla rectangular contenedor). Sobreescribe lo que diga el + // 0010 — nodo tabla cuadrado contenedor). Sobreescribe lo que diga el // yaml: se aplica en cada reload, por lo que ediciones futuras desde el - // Type Editor no rompen la convencion. + // Type Editor no rompen la convencion. Tambien forzamos un tamano + // notablemente mayor (32 px world) para que la diferencia visual con + // un nodo normal sea evidente. for (int i = 0; i < graph.type_count; ++i) { EntityType& et = graph.types[i]; bool is_table = et.name && (eq_ci(et.name, std::string("Table")) || eq_ci(et.name, std::string("table"))); et.shape = is_table ? SHAPE_SQUARE : SHAPE_CIRCLE; + if (is_table) { + et.default_size = 32.0f; + } } for (int i = 0; i < graph.rel_type_count; ++i) { diff --git a/views.cpp b/views.cpp index 71de634..d45f204 100644 --- a/views.cpp +++ b/views.cpp @@ -1801,17 +1801,21 @@ void views_table_windows_sync(AppState& app, const char* ops_db) { if (live.find(it->first) == live.end()) it = app.table_windows.erase(it); else ++it; } - // Anadir las nuevas o refrescar metadata. + // Anadir las nuevas o refrescar metadata. Tras cualquier sync forzamos + // page_dirty = true para que la siguiente iteracion del render relea + // la pagina contra DuckDB (se evita asi mostrar pages obsoletas tras + // promote/demote/import — donde el flag promoted de cada fila puede + // haber cambiado). for (auto& kv : live) { auto& w = app.table_windows[kv.first]; bool was_present = !w.meta.entity_id.empty(); w.meta = std::move(kv.second); w.open = true; + w.page_dirty = true; if (!was_present) { w.offset = 0; w.page.clear(); w.total_rows = 0; - w.page_dirty = true; } } } @@ -2019,6 +2023,9 @@ void views_table_overlay(AppState& app) { if (!dl) return; ImFont* font = ImGui::GetFont(); + // El cuadrado lo pinta el GPU (apply_types_yaml fija shape=SQUARE + + // size=32 para tipos Table). Aqui solo añadimos un contador discreto + // BAJO el cuadrado: "1000 rows". for (int i = 0; i < g.node_count; ++i) { const GraphNode& n = g.nodes[i]; if (!(n.flags & NF_VISIBLE)) continue; @@ -2026,44 +2033,38 @@ void views_table_overlay(AppState& app) { const EntityType& t = g.types[n.type_id]; if (!t.name || std::strcmp(t.name, "Table") != 0) continue; - const float vx = (n.x - app.viewport->cam_x) * app.viewport->zoom + cx; - const float vy = (n.y - app.viewport->cam_y) * app.viewport->zoom + cy; - if (vx < wmin.x - 200 || vx > wmax.x + 200) continue; + const float zoom = app.viewport->zoom; + const float vx = (n.x - app.viewport->cam_x) * zoom + cx; + const float vy = (n.y - app.viewport->cam_y) * zoom + cy; + if (vx < wmin.x - 100 || vx > wmax.x + 100) continue; if (vy < wmin.y - 100 || vy > wmax.y + 100) continue; int64_t count = -1; auto it = app.table_node_counts.find(n.user_data); if (it != app.table_node_counts.end()) count = it->second; + if (count < 0) continue; - char buf[96]; - if (count >= 0) std::snprintf(buf, sizeof(buf), TI_TABLE " Table %lld", (long long)count); - else std::snprintf(buf, sizeof(buf), TI_TABLE " Table"); + char buf[64]; + std::snprintf(buf, sizeof(buf), "%lld rows", (long long)count); - const float font_size = 13.0f; - ImVec2 ts = font ? font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, buf) - : ImVec2(60.0f, 14.0f); - const float pad_x = 10.0f, pad_y = 6.0f; - const float w = std::max(96.0f, ts.x + pad_x * 2.0f); - const float h = ts.y + pad_y * 2.0f; - ImVec2 a(vx - w * 0.5f, vy - h * 0.5f); - ImVec2 b(vx + w * 0.5f, vy + h * 0.5f); + const float font_size = 12.0f; + if (!font) continue; + ImVec2 ts = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, buf); - // Sombra ligera - dl->AddRectFilled(ImVec2(a.x + 1, a.y + 2), ImVec2(b.x + 1, b.y + 2), - IM_COL32(0, 0, 0, 80), 6.0f); - // Cuerpo - dl->AddRectFilled(a, b, IM_COL32(38, 56, 92, 240), 6.0f); - // Borde - uint32_t border = (n.flags & NF_SELECTED) - ? IM_COL32(180, 200, 255, 255) - : IM_COL32(120, 160, 220, 220); - dl->AddRect(a, b, border, 6.0f, 0, (n.flags & NF_SELECTED) ? 2.0f : 1.5f); + // Posicion: bajo el cuadrado. La mitad del shape en pixeles depende + // del default_size del tipo y del zoom. + const float half_h = (t.default_size * zoom) * 0.5f; + const float gap = 4.0f; + const float tx = vx - ts.x * 0.5f; + const float ty = vy + half_h + gap; - if (font) { - dl->AddText(font, font_size, - ImVec2(vx - ts.x * 0.5f, vy - ts.y * 0.5f), - IM_COL32(230, 240, 255, 255), buf); - } + // Pequeño bg semitransparente para que el texto sea legible sobre + // grafos densos, sin parecer un chip. + dl->AddRectFilled(ImVec2(tx - 4, ty - 1), + ImVec2(tx + ts.x + 4, ty + ts.y + 1), + IM_COL32(20, 25, 35, 180), 3.0f); + dl->AddText(font, font_size, ImVec2(tx, ty), + IM_COL32(200, 220, 240, 230), buf); } }