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:
@@ -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(),
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user