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 "<N> 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.
This commit is contained in:
2026-05-01 14:18:26 +02:00
parent 6ee79d51a6
commit b798454f35
5 changed files with 70 additions and 33 deletions
+1
View File
@@ -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(),
+25
View File
@@ -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());
+5
View File
@@ -119,7 +119,12 @@ bool tableview_set_columns(const char* ops_db, const char* entity_id,
// "prom_<sanitize(row_type)>_<sanitize(row_id)>", 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}, <columnas> }.
//
// 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,
+7 -2
View File
@@ -571,14 +571,19 @@ std::vector<uint16_t> 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) {
+32 -31
View File
@@ -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);
}
}