feat(0035b): renderer oculta hijos de grupos colapsados + dedup aristas
- AppState anade `group_expanded` (unordered_map<string,bool>) en RAM,
default vacio = todos los grupos colapsados al arranque. Sin
persistencia entre sesiones (fase 1).
- `apply_group_filter(GraphData*, db_path, expanded)` consulta
entities (id, group_id, type_ref) de operations.db, marca como
ocultos los nodos cuyo group_id apunta a un grupo no expandido,
compacta `g->nodes` y re-mapea indices de aristas.
- Aristas:
* Cross-edge (un extremo oculto, otro fuera): se redirige el
extremo oculto al nodo del grupo. Sin dedup (issue 0035 dec. 5).
* Internas (ambos extremos en el mismo grupo colapsado): se ocultan.
* Inter-grupo (ambos en grupos colapsados distintos): dedup por
par no ordenado (group_a, group_b) + rel_type, una linea por par.
* Orfanas (group_id apunta a un grupo no presente en grafo): el
nodo se oculta y sus aristas se descartan.
- Centralizado: el filtro corre en `reload_graph()` cuando se le
pasa `group_expanded`, y en `load_input()` tras el load inicial.
Cubre las 4 rutas de carga del app (toolbar reload, mutaciones,
inspector save, primera carga / switch project).
- Idempotente sobre un grafo ya filtrado y robusto frente a BDs sin
columna `group_id` (schema antiguo) — no toca el grafo.
Smoke test manual con 3 BDs sintéticas:
- Grupo + 2 children + edges cruzadas/internas: nodes 5→3, edges
4→3 (internal hidden, cross redirected).
- 2 grupos con 4 cross-edges entre ellos: edges 4→1 (dedup).
- group_id huerfano: nodo oculto + arista descartada.
Build clean en Windows. Tests verdes:
- WSL pytest: 32 passed.
- Windows pytest: 21 passed + 11 skipped.
Refs: issues/0035b-renderer-hides-grouped-children.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,29 @@
|
|||||||
#include "data.h"
|
#include "data.h"
|
||||||
|
|
||||||
|
#include "../../../../cpp/vendor/sqlite3/sqlite3.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace ge {
|
namespace ge {
|
||||||
|
|
||||||
|
// FNV1a-64 — debe coincidir con graph_sources.cpp y entity_ops.cpp para que
|
||||||
|
// los `user_data` calculados en este archivo casen con los del loader.
|
||||||
|
static uint64_t gf_fnv1a64(const char* s) {
|
||||||
|
uint64_t h = 1469598103934665603ULL;
|
||||||
|
for (; s && *s; ++s) {
|
||||||
|
h ^= (uint8_t)*s;
|
||||||
|
h *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
bool load_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* stats) {
|
bool load_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* stats) {
|
||||||
if (!out || !stats) return false;
|
if (!out || !stats) return false;
|
||||||
*stats = graph::GraphLoadStats{};
|
*stats = graph::GraphLoadStats{};
|
||||||
@@ -27,9 +46,248 @@ bool load_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool reload_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* stats) {
|
bool reload_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* stats,
|
||||||
|
const std::unordered_map<std::string, bool>* group_expanded) {
|
||||||
if (out) graph::graph_free(out);
|
if (out) graph::graph_free(out);
|
||||||
return load_graph(args, out, stats);
|
bool ok = load_graph(args, out, stats);
|
||||||
|
if (!ok) return false;
|
||||||
|
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.
|
||||||
|
apply_group_filter(out, args.uri, *group_expanded);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// apply_group_filter (issue 0035b)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Detecta si la columna `group_id` existe en `entities`. Sin la columna, el
|
||||||
|
// filtro no tiene nada que hacer y devuelve sin tocar el grafo. La migracion
|
||||||
|
// (issue 0035a) la anade en BDs nuevas y existentes; pero si por algun motivo
|
||||||
|
// abrimos una BD vieja antes de que migrate corra, no debemos petar.
|
||||||
|
bool has_group_id_column(sqlite3* db) {
|
||||||
|
sqlite3_stmt* st = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(db, "PRAGMA table_info(entities)", -1, &st, nullptr) != SQLITE_OK)
|
||||||
|
return false;
|
||||||
|
bool found = false;
|
||||||
|
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||||
|
const unsigned char* name = sqlite3_column_text(st, 1);
|
||||||
|
if (name && std::strcmp((const char*)name, "group_id") == 0) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detecta el nombre de la columna de tipo en entities (puede ser type_ref o
|
||||||
|
// type segun la version del schema).
|
||||||
|
std::string entity_type_column(sqlite3* db) {
|
||||||
|
sqlite3_stmt* st = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(db, "PRAGMA table_info(entities)", -1, &st, nullptr) != SQLITE_OK)
|
||||||
|
return "type_ref";
|
||||||
|
std::string col = "type_ref";
|
||||||
|
bool seen_type_ref = false;
|
||||||
|
bool seen_type = false;
|
||||||
|
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||||
|
const unsigned char* name = sqlite3_column_text(st, 1);
|
||||||
|
if (!name) continue;
|
||||||
|
if (std::strcmp((const char*)name, "type_ref") == 0) seen_type_ref = true;
|
||||||
|
if (std::strcmp((const char*)name, "type") == 0) seen_type = true;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
if (seen_type_ref) col = "type_ref";
|
||||||
|
else if (seen_type) col = "type";
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anon
|
||||||
|
|
||||||
|
bool apply_group_filter(GraphData* g, const char* db_path,
|
||||||
|
const std::unordered_map<std::string, bool>& group_expanded) {
|
||||||
|
if (!g || !db_path || !*db_path) return false;
|
||||||
|
if (g->node_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; // schema antiguo: nada que filtrar
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string type_col = entity_type_column(db);
|
||||||
|
|
||||||
|
// Lee (id, group_id, type) para todas las entidades. Construimos:
|
||||||
|
// - hash_to_entity_id: user_data (FNV1a64 del id) → string id
|
||||||
|
// - entity_to_group_id: id string → group_id string ("" si NULL)
|
||||||
|
// - is_group_type: id string → true si type == "Group"
|
||||||
|
std::unordered_map<uint64_t, std::string> hash_to_entity_id;
|
||||||
|
std::unordered_map<std::string, std::string> entity_to_group_id;
|
||||||
|
std::unordered_set<std::string> group_entities;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::string q = "SELECT id, group_id, " + type_col + " FROM entities";
|
||||||
|
sqlite3_stmt* st = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(db, q.c_str(), -1, &st, nullptr) != SQLITE_OK) {
|
||||||
|
sqlite3_close(db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||||
|
const unsigned char* id_c = sqlite3_column_text(st, 0);
|
||||||
|
const unsigned char* gid_c = sqlite3_column_text(st, 1);
|
||||||
|
const unsigned char* tp_c = sqlite3_column_text(st, 2);
|
||||||
|
if (!id_c) continue;
|
||||||
|
std::string id_s = (const char*)id_c;
|
||||||
|
std::string gid_s = gid_c ? (const char*)gid_c : "";
|
||||||
|
std::string type_s = tp_c ? (const char*)tp_c : "";
|
||||||
|
hash_to_entity_id[gf_fnv1a64(id_s.c_str())] = id_s;
|
||||||
|
if (!gid_s.empty()) entity_to_group_id[id_s] = gid_s;
|
||||||
|
if (type_s == "Group") group_entities.insert(id_s);
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
}
|
||||||
|
sqlite3_close(db);
|
||||||
|
|
||||||
|
// Helper: devuelve true si el entity_id `eid` es un grupo expandido.
|
||||||
|
auto is_expanded = [&](const std::string& eid) -> bool {
|
||||||
|
auto it = group_expanded.find(eid);
|
||||||
|
return it != group_expanded.end() && it->second;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Para cada nodo del grafo, decidir:
|
||||||
|
// - hidden: se elimina del array final
|
||||||
|
// - effective_node_idx: indice del nodo en el array nuevo, o -1 si oculto
|
||||||
|
// - redirect_to_group_idx: si oculto y su grupo existe en el grafo y NO
|
||||||
|
// esta expandido, las aristas se redirigen al indice del grupo en el
|
||||||
|
// array nuevo. Si el grupo no existe en el grafo (orfano), se deja -1
|
||||||
|
// y las aristas se descartan.
|
||||||
|
// Construimos primero un map hash→old_idx y entity_id→old_idx para
|
||||||
|
// resolver rapido los grupos.
|
||||||
|
std::unordered_map<std::string, int> entity_id_to_old_idx;
|
||||||
|
entity_id_to_old_idx.reserve((size_t)g->node_count);
|
||||||
|
for (int i = 0; i < g->node_count; ++i) {
|
||||||
|
auto it = hash_to_entity_id.find(g->nodes[i].user_data);
|
||||||
|
if (it != hash_to_entity_id.end()) {
|
||||||
|
entity_id_to_old_idx[it->second] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decidir visibility por nodo.
|
||||||
|
std::vector<unsigned char> hidden((size_t)g->node_count, 0);
|
||||||
|
std::vector<int> redirect_old(g->node_count, -1); // old_idx → old_idx del grupo (si oculto)
|
||||||
|
|
||||||
|
for (int i = 0; i < g->node_count; ++i) {
|
||||||
|
auto hit = hash_to_entity_id.find(g->nodes[i].user_data);
|
||||||
|
if (hit == hash_to_entity_id.end()) continue;
|
||||||
|
const std::string& eid = hit->second;
|
||||||
|
auto git = entity_to_group_id.find(eid);
|
||||||
|
if (git == entity_to_group_id.end()) continue;
|
||||||
|
const std::string& parent = git->second;
|
||||||
|
if (is_expanded(parent)) continue; // grupo expandido → visible
|
||||||
|
// Nodo oculto. Buscar el indice del grupo padre en el grafo.
|
||||||
|
auto pit = entity_id_to_old_idx.find(parent);
|
||||||
|
if (pit != entity_id_to_old_idx.end()) {
|
||||||
|
redirect_old[i] = pit->second;
|
||||||
|
hidden[i] = 1;
|
||||||
|
} else {
|
||||||
|
// Grupo padre no presente en el grafo cargado — ocultamos sin
|
||||||
|
// redirigir. Las aristas se descartan.
|
||||||
|
hidden[i] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compactar nodos: nuevo array sin los ocultos. Mantenemos un map old→new.
|
||||||
|
std::vector<int> old_to_new((size_t)g->node_count, -1);
|
||||||
|
int new_count = 0;
|
||||||
|
for (int i = 0; i < g->node_count; ++i) {
|
||||||
|
if (hidden[i]) continue;
|
||||||
|
old_to_new[i] = new_count;
|
||||||
|
if (new_count != i) g->nodes[new_count] = g->nodes[i];
|
||||||
|
++new_count;
|
||||||
|
}
|
||||||
|
int original_node_count = g->node_count;
|
||||||
|
g->node_count = new_count;
|
||||||
|
|
||||||
|
// Helper: dado old_idx, devuelve el new_idx efectivo (redirige al grupo
|
||||||
|
// si oculto). Devuelve -1 si imposible (oculto sin grupo en el grafo).
|
||||||
|
auto effective_new_idx = [&](int old_idx) -> int {
|
||||||
|
if (old_idx < 0 || old_idx >= original_node_count) return -1;
|
||||||
|
if (!hidden[old_idx]) return old_to_new[old_idx];
|
||||||
|
int g_old = redirect_old[old_idx];
|
||||||
|
if (g_old < 0) return -1;
|
||||||
|
return old_to_new[g_old]; // el grupo es siempre visible
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filtrar/redirigir aristas.
|
||||||
|
// Para deduplicacion grupo-a-grupo: clave = (min(grp_old_a, grp_old_b),
|
||||||
|
// max(...), rel_type_id). Asi mantenemos UNA arista por par + tipo.
|
||||||
|
std::unordered_set<uint64_t> seen_grp_pairs;
|
||||||
|
auto pair_key = [](int a, int b, uint16_t rt) -> uint64_t {
|
||||||
|
if (a > b) std::swap(a, b);
|
||||||
|
// a, b en uint32 + rt en 16 bits. Combinamos.
|
||||||
|
uint64_t k = (uint64_t)(uint32_t)a;
|
||||||
|
k = (k << 24) ^ (uint64_t)(uint32_t)b;
|
||||||
|
k = (k << 16) ^ (uint64_t)rt;
|
||||||
|
return k;
|
||||||
|
};
|
||||||
|
|
||||||
|
int new_edge_count = 0;
|
||||||
|
for (int e = 0; e < g->edge_count; ++e) {
|
||||||
|
GraphEdge& edge = g->edges[e];
|
||||||
|
int s_old = (int)edge.source;
|
||||||
|
int t_old = (int)edge.target;
|
||||||
|
if (s_old < 0 || s_old >= original_node_count ||
|
||||||
|
t_old < 0 || t_old >= original_node_count) continue;
|
||||||
|
|
||||||
|
bool s_hidden = hidden[s_old] != 0;
|
||||||
|
bool t_hidden = hidden[t_old] != 0;
|
||||||
|
|
||||||
|
int s_new = effective_new_idx(s_old);
|
||||||
|
int t_new = effective_new_idx(t_old);
|
||||||
|
if (s_new < 0 || t_new < 0) continue; // descarte por orfandad
|
||||||
|
|
||||||
|
// Caso 1: ambos extremos eran hijos de grupo(s) colapsado(s).
|
||||||
|
if (s_hidden && t_hidden) {
|
||||||
|
// Si caen en el MISMO grupo → arista interna, se descarta
|
||||||
|
// (issue 0035 decision 5).
|
||||||
|
int s_grp_old = redirect_old[s_old];
|
||||||
|
int t_grp_old = redirect_old[t_old];
|
||||||
|
if (s_grp_old < 0 || t_grp_old < 0) continue;
|
||||||
|
if (s_grp_old == t_grp_old) continue;
|
||||||
|
// Distintos grupos → dedup por par + rel_type.
|
||||||
|
uint64_t k = pair_key(s_grp_old, t_grp_old, edge.type_id);
|
||||||
|
if (seen_grp_pairs.count(k)) continue;
|
||||||
|
seen_grp_pairs.insert(k);
|
||||||
|
edge.source = (uint32_t)s_new;
|
||||||
|
edge.target = (uint32_t)t_new;
|
||||||
|
}
|
||||||
|
else if (s_hidden || t_hidden) {
|
||||||
|
// Cross-edge: un extremo dentro de grupo colapsado, otro fuera.
|
||||||
|
// Redirigimos el extremo oculto al grupo. Sin dedup (issue 0035
|
||||||
|
// decision 5: una linea por arista cuando es single-cross).
|
||||||
|
edge.source = (uint32_t)s_new;
|
||||||
|
edge.target = (uint32_t)t_new;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Ambos visibles — arista normal.
|
||||||
|
edge.source = (uint32_t)s_new;
|
||||||
|
edge.target = (uint32_t)t_new;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_edge_count != e) g->edges[new_edge_count] = edge;
|
||||||
|
++new_edge_count;
|
||||||
|
}
|
||||||
|
g->edge_count = new_edge_count;
|
||||||
|
g->update_bounds();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ge
|
} // namespace ge
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "viz/graph_sources.h"
|
#include "viz/graph_sources.h"
|
||||||
#include "viz/graph_types.h"
|
#include "viz/graph_types.h"
|
||||||
|
|
||||||
@@ -22,6 +25,27 @@ bool load_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* st
|
|||||||
|
|
||||||
// Reload helper — usa la misma uri que la ultima `load_graph` exitosa.
|
// Reload helper — usa la misma uri que la ultima `load_graph` exitosa.
|
||||||
// Llama a `graph_free(out)` y vuelve a invocar `load_graph(args, out, stats)`.
|
// Llama a `graph_free(out)` y vuelve a invocar `load_graph(args, out, stats)`.
|
||||||
bool reload_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* stats);
|
// Si `group_expanded` no es null, aplica el filtro de grupos (issue 0035b)
|
||||||
|
// tras la carga: oculta hijos cuyo group_id apunta a un grupo no expandido,
|
||||||
|
// reescribe extremos de aristas que quedan dentro de grupos colapsados al
|
||||||
|
// cuadrado del grupo, y deduplica aristas grupo-a-grupo a una linea por par.
|
||||||
|
bool reload_graph(const InputArgs& args, GraphData* out, graph::GraphLoadStats* stats,
|
||||||
|
const std::unordered_map<std::string, bool>* group_expanded = nullptr);
|
||||||
|
|
||||||
|
// Aplica el filtro de grupos in-place sobre `g` consultando `db_path` para
|
||||||
|
// recuperar `group_id` por entidad. `group_expanded` mapea entity_id (string)
|
||||||
|
// del nodo Group → bool (true = expandido). Reglas:
|
||||||
|
// - Nodo con group_id != NULL y grupo padre no expandido → oculto (NF_VISIBLE
|
||||||
|
// limpiado y removido del array, indices de aristas re-mapeados).
|
||||||
|
// - Arista cuyo extremo cae en grupo colapsado → extremo redirigido al
|
||||||
|
// nodo del grupo. Si el otro extremo tambien cae en el MISMO grupo
|
||||||
|
// colapsado → arista interna, se descarta.
|
||||||
|
// - Aristas con AMBOS extremos en grupos colapsados distintos → dedup por
|
||||||
|
// par (group_a, group_b) sin orden, una sola linea por par + relacion.
|
||||||
|
// Idempotente sobre un grafo ya filtrado (no quedan group_id ocultos).
|
||||||
|
// Retorna true si tuvo exito; en caso de error de DB devuelve false sin
|
||||||
|
// alterar el grafo.
|
||||||
|
bool apply_group_filter(GraphData* g, const char* db_path,
|
||||||
|
const std::unordered_map<std::string, bool>& group_expanded);
|
||||||
|
|
||||||
} // namespace ge
|
} // namespace ge
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: 0035b
|
id: 0035b
|
||||||
title: Renderer oculta hijos de grupos colapsados + dedup de aristas grupo-a-grupo
|
title: Renderer oculta hijos de grupos colapsados + dedup de aristas grupo-a-grupo
|
||||||
status: pending
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
created: 2026-05-03
|
created: 2026-05-03
|
||||||
parent: 0035
|
parent: 0035
|
||||||
|
|||||||
@@ -602,9 +602,12 @@ 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;
|
||||||
}
|
}
|
||||||
|
// 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);
|
||||||
std::fprintf(stdout,
|
std::fprintf(stdout,
|
||||||
"[graph_explorer] loaded %d nodes, %d edges, %d types, %d rel_types from %s\n",
|
"[graph_explorer] loaded %d nodes, %d edges, %d types, %d rel_types from %s\n",
|
||||||
stats.nodes_loaded, stats.edges_loaded,
|
g_graph.node_count, g_graph.edge_count,
|
||||||
stats.types_discovered, stats.rel_types_discovered, g_input.uri);
|
stats.types_discovered, stats.rel_types_discovered, g_input.uri);
|
||||||
|
|
||||||
// types.yaml
|
// types.yaml
|
||||||
@@ -1360,7 +1363,7 @@ static void render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
graph::GraphLoadStats stats{};
|
graph::GraphLoadStats stats{};
|
||||||
if (ge::reload_graph(g_input, &g_graph, &stats)) {
|
if (ge::reload_graph(g_input, &g_graph, &stats, &g_app.group_expanded)) {
|
||||||
ge::views_reset_visibility(g_app);
|
ge::views_reset_visibility(g_app);
|
||||||
ge::views_apply_visibility(g_app);
|
ge::views_apply_visibility(g_app);
|
||||||
|
|
||||||
@@ -1519,7 +1522,7 @@ static void render() {
|
|||||||
ge::layout_store_save(g_graph_hash, g_graph);
|
ge::layout_store_save(g_graph_hash, g_graph);
|
||||||
}
|
}
|
||||||
graph::GraphLoadStats stats{};
|
graph::GraphLoadStats stats{};
|
||||||
if (!ge::reload_graph(g_input, &g_graph, &stats)) return;
|
if (!ge::reload_graph(g_input, &g_graph, &stats, &g_app.group_expanded)) return;
|
||||||
ge::entity_index_build(g_input.uri, &g_idx);
|
ge::entity_index_build(g_input.uri, &g_idx);
|
||||||
ge::views_reset_visibility(g_app);
|
ge::views_reset_visibility(g_app);
|
||||||
ge::views_apply_visibility(g_app);
|
ge::views_apply_visibility(g_app);
|
||||||
@@ -1800,7 +1803,7 @@ static void render() {
|
|||||||
// Reload del grafo para que cambios de name/type/etc. se reflejen
|
// Reload del grafo para que cambios de name/type/etc. se reflejen
|
||||||
// en el viewport (label, color del tipo, etc.).
|
// en el viewport (label, color del tipo, etc.).
|
||||||
graph::GraphLoadStats stats{};
|
graph::GraphLoadStats stats{};
|
||||||
if (ge::reload_graph(g_input, &g_graph, &stats)) {
|
if (ge::reload_graph(g_input, &g_graph, &stats, &g_app.group_expanded)) {
|
||||||
ge::entity_index_build(g_input.uri, &g_idx);
|
ge::entity_index_build(g_input.uri, &g_idx);
|
||||||
ge::views_reset_visibility(g_app);
|
ge::views_reset_visibility(g_app);
|
||||||
ge::views_apply_visibility(g_app);
|
ge::views_apply_visibility(g_app);
|
||||||
|
|||||||
@@ -84,6 +84,15 @@ struct AppState {
|
|||||||
// main.cpp lo escribe tras cargar y los handlers lo leen.
|
// main.cpp lo escribe tras cargar y los handlers lo leen.
|
||||||
std::string input_db_path;
|
std::string input_db_path;
|
||||||
|
|
||||||
|
// ---- Grouping (issue 0035b) ---------------------------------------------
|
||||||
|
// Estado de expansion de nodos `Group` en RAM. Default vacio = todos los
|
||||||
|
// grupos colapsados. No persiste entre sesiones (fase 1). El filtro del
|
||||||
|
// loader (apply_group_filter) consulta este map: si una entidad tiene
|
||||||
|
// `group_id != NULL` y el grupo padre no esta en este map con valor true,
|
||||||
|
// la entidad se oculta del grafo. Sin UI todavia para togglear; se setean
|
||||||
|
// valores manualmente desde tests/debug.
|
||||||
|
std::unordered_map<std::string, bool> group_expanded;
|
||||||
|
|
||||||
// Add-node toolbar input.
|
// Add-node toolbar input.
|
||||||
char add_buf[256] = {};
|
char add_buf[256] = {};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user