From c27d8e7ffc0046156f896d0272206ce8b923176c Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 4 May 2026 14:21:01 +0200 Subject: [PATCH] feat(0035e): Group hereda iconografia de hijos homogeneos apply_group_inherited_visuals(GraphData*, db_path) recorre los nodos Group del grafo y, para cada uno, consulta los type_ref distintos de sus hijos (entities con group_id apuntando al Group). Si todos comparten un solo tipo, reasigna el type_id del Group al type_id de ese tipo y fija shape_override = SHAPE_SQUARE para preservar el cuadrado distintivo. Heterogeneo o sin hijos: el Group conserva su visual generico (slate + ti-stack-2). Se invoca desde main.cpp y reload_graph antes de apply_group_filter para que la reasignacion sobreviva al compactado del array. --- data.cpp | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ data.h | 7 +++ main.cpp | 2 + 3 files changed, 145 insertions(+) diff --git a/data.cpp b/data.cpp index efc6c4d..9e384df 100644 --- a/data.cpp +++ b/data.cpp @@ -51,6 +51,12 @@ bool reload_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* if (out) graph::graph_free(out); bool ok = load_graph(args, out, stats); if (!ok) return false; + if (args.uri && *args.uri) { + // Issue 0035e: heredar iconografia/color del tipo mayoritario de + // los hijos en cada Group homogeneo. Antes del filtro, asi el + // type_id reasignado se preserva en el array compactado. + apply_group_inherited_visuals(out, args.uri); + } if (group_expanded && args.uri && *args.uri) { // Best-effort: si falla la consulta de group_id, dejamos el grafo // sin filtrar — el caller ya tiene un grafo valido. @@ -290,4 +296,134 @@ bool apply_group_filter(GraphData* g, const char* db_path, return true; } +// ---------------------------------------------------------------------------- +// apply_group_inherited_visuals (issue 0035e) +// ---------------------------------------------------------------------------- +// +// Para cada nodo Group del grafo, consulta los `type_ref` distintos de sus +// hijos (entities con group_id apuntando al grupo). Si todos comparten un +// solo tipo (homogeneo), reasigna el `type_id` del nodo Group al type_id de +// ese tipo y fija `shape_override = SHAPE_SQUARE` para preservar la forma +// distintiva de contenedor. Asi el cuadrado adopta color e icono del tipo +// hijo. Si la familia es heterogenea o el tipo hijo no esta presente en +// graph.types[], el nodo conserva su visual generico (Group / slate). +// +// Idempotente: si la heredancia ya se aplico, vuelve a aplicar lo mismo. +// No-op si la BD no tiene group_id, o si no hay nodos Group. +bool apply_group_inherited_visuals(GraphData* g, const char* db_path) { + if (!g || !db_path || !*db_path) return false; + if (g->node_count <= 0 || g->type_count <= 0) return true; + + sqlite3* db = nullptr; + if (sqlite3_open(db_path, &db) != SQLITE_OK) { + if (db) sqlite3_close(db); + return false; + } + if (!has_group_id_column(db)) { + sqlite3_close(db); + return true; + } + + std::string type_col = entity_type_column(db); + + // Localizar el type_id del tipo "Group" en graph.types[]. + int group_type_id = -1; + for (int i = 0; i < g->type_count; ++i) { + const char* nm = g->types[i].name; + if (nm && (std::strcmp(nm, "Group") == 0 || + std::strcmp(nm, "group") == 0)) { + group_type_id = i; + break; + } + } + if (group_type_id < 0) { sqlite3_close(db); return true; } + + // user_data (FNV1a64 del id) → entity_id string, para resolver cada + // nodo Group del grafo a su id real en operations.db. + // Solo nos interesan los Group nodes — filtramos por type_id. + std::vector> group_nodes; // (node_idx, entity_id) + { + // Cargamos un map id→user_data inverso unico via consulta directa + // a operations.db (id texto → user_data). Mas barato: iterar el + // grafo + invertir hash via consulta. + // Construimos hash→id desde la BD (igual que apply_group_filter). + std::unordered_map hash_to_id; + std::string q = "SELECT id FROM entities WHERE " + type_col + " = 'Group'"; + sqlite3_stmt* st = nullptr; + if (sqlite3_prepare_v2(db, q.c_str(), -1, &st, nullptr) == SQLITE_OK) { + while (sqlite3_step(st) == SQLITE_ROW) { + const unsigned char* idc = sqlite3_column_text(st, 0); + if (!idc) continue; + std::string ids = (const char*)idc; + hash_to_id[gf_fnv1a64(ids.c_str())] = ids; + } + sqlite3_finalize(st); + } + for (int i = 0; i < g->node_count; ++i) { + // Solo nodos cuyo type_id resuelve a Group. Si la inheritance ya + // se aplico en una pasada previa, el type_id ya no es Group y + // el nodo se omite — idempotencia natural pero significa que + // si el set de hijos cambia, hace falta recargar el grafo. + if (g->nodes[i].type_id != (uint16_t)group_type_id) continue; + auto it = hash_to_id.find(g->nodes[i].user_data); + if (it != hash_to_id.end()) group_nodes.emplace_back(i, it->second); + } + } + + if (group_nodes.empty()) { sqlite3_close(db); return true; } + + // Para cada Group, contar type_refs distintos de sus hijos. + // Solo consideramos hijos con group_id == group.id Y type_ref != 'Group' + // (un Group hijo de otro Group seria meta-anidacion, fuera de scope). + std::string child_q = + "SELECT DISTINCT " + type_col + " FROM entities " + "WHERE group_id = ? AND " + type_col + " != 'Group'"; + sqlite3_stmt* cst = nullptr; + if (sqlite3_prepare_v2(db, child_q.c_str(), -1, &cst, nullptr) != SQLITE_OK) { + sqlite3_close(db); + return false; + } + + auto find_type_id_by_name = [&](const std::string& nm) -> int { + for (int i = 0; i < g->type_count; ++i) { + const char* tn = g->types[i].name; + if (!tn) continue; + // case-insensitive match + if (nm.size() != std::strlen(tn)) continue; + bool eq = true; + for (size_t k = 0; k < nm.size(); ++k) { + if (std::tolower((unsigned char)nm[k]) != + std::tolower((unsigned char)tn[k])) { eq = false; break; } + } + if (eq) return i; + } + return -1; + }; + + for (auto& [node_idx, eid] : group_nodes) { + sqlite3_reset(cst); + sqlite3_clear_bindings(cst); + sqlite3_bind_text(cst, 1, eid.c_str(), -1, SQLITE_TRANSIENT); + std::string single_type; + bool homogeneous = true; + int distinct_count = 0; + while (sqlite3_step(cst) == SQLITE_ROW) { + const unsigned char* tc = sqlite3_column_text(cst, 0); + std::string t = tc ? (const char*)tc : ""; + if (t.empty()) continue; + ++distinct_count; + if (distinct_count == 1) single_type = t; + else if (t != single_type) { homogeneous = false; break; } + } + if (!homogeneous || distinct_count != 1) continue; + int new_type_id = find_type_id_by_name(single_type); + if (new_type_id < 0 || new_type_id == group_type_id) continue; + g->nodes[node_idx].type_id = (uint16_t)new_type_id; + g->nodes[node_idx].shape_override = SHAPE_SQUARE; // mantener cuadrado + } + sqlite3_finalize(cst); + sqlite3_close(db); + return true; +} + } // namespace ge diff --git a/data.h b/data.h index 6d6e096..a8b9cdb 100644 --- a/data.h +++ b/data.h @@ -48,4 +48,11 @@ bool reload_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* bool apply_group_filter(GraphData* g, const char* db_path, const std::unordered_map& group_expanded); +// Issue 0035e: para cada nodo Group, si todos sus hijos comparten un +// unico `type_ref`, reasigna el `type_id` del Group al type_id de ese +// tipo y fija `shape_override = SHAPE_SQUARE` para preservar la forma +// cuadrada. Si la familia es heterogenea, el nodo conserva su visual +// generico de Group. No-op si la BD no tiene group_id o no hay Groups. +bool apply_group_inherited_visuals(GraphData* g, const char* db_path); + } // namespace ge diff --git a/main.cpp b/main.cpp index 5d52226..4bfb82a 100644 --- a/main.cpp +++ b/main.cpp @@ -666,6 +666,8 @@ static bool load_input(bool first_load) { std::fprintf(stderr, "[graph_explorer] load failed: %s\n", stats.error_msg); return false; } + // Issue 0035e: iconografia heredada de hijos homogeneos (antes del filtro). + ge::apply_group_inherited_visuals(&g_graph, g_input.uri); // Filtro de grupos colapsados (issue 0035b). Se aplica tras la carga // bruta — el loader sigue siendo agnostico al concepto de grupo. ge::apply_group_filter(&g_graph, g_input.uri, g_app.group_expanded);