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.
This commit is contained in:
2026-05-04 14:21:01 +02:00
parent 52495af779
commit c27d8e7ffc
3 changed files with 145 additions and 0 deletions
+136
View File
@@ -51,6 +51,12 @@ bool reload_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats*
if (out) graph::graph_free(out); if (out) graph::graph_free(out);
bool ok = load_graph(args, out, stats); bool ok = load_graph(args, out, stats);
if (!ok) return false; 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) { if (group_expanded && args.uri && *args.uri) {
// Best-effort: si falla la consulta de group_id, dejamos el grafo // Best-effort: si falla la consulta de group_id, dejamos el grafo
// sin filtrar — el caller ya tiene un grafo valido. // 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; 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<std::pair<int, std::string>> 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<uint64_t, std::string> 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 } // namespace ge
+7
View File
@@ -48,4 +48,11 @@ bool reload_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats*
bool apply_group_filter(GraphData* g, const char* db_path, bool apply_group_filter(GraphData* g, const char* db_path,
const std::unordered_map<std::string, bool>& group_expanded); const std::unordered_map<std::string, bool>& 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 } // namespace ge
+2
View File
@@ -666,6 +666,8 @@ static bool load_input(bool first_load) {
std::fprintf(stderr, "[graph_explorer] load failed: %s\n", stats.error_msg); std::fprintf(stderr, "[graph_explorer] load failed: %s\n", stats.error_msg);
return false; 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 // Filtro de grupos colapsados (issue 0035b). Se aplica tras la carga
// bruta — el loader sigue siendo agnostico al concepto de grupo. // bruta — el loader sigue siendo agnostico al concepto de grupo.
ge::apply_group_filter(&g_graph, g_input.uri, g_app.group_expanded); ge::apply_group_filter(&g_graph, g_input.uri, g_app.group_expanded);