From ad8944af8e4f06e304574596e9688af8ab5daa46 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 17 May 2026 20:02:27 +0200 Subject: [PATCH] feat(skill_tree): canvas ImDrawList + ring layout + cards (0109b) Render anillos concentricos + sectores radiales por dominio con ImDrawList. 166 nodos pintados con label ID, tooltip on hover, picking O(N), pan+zoom. Decisiones: - Pivote desde graph_renderer GPU a ImDrawList CPU. 166 nodos no justifican pipeline GPU; ahorra ~120 LOC y init_gl_loader. - 5 rings: done / in-progress / unlocked / locked / deferred. - 18 sectores = 18 dominios canonicos (dev/TAXONOMY.md), labels en aro exterior. - Issues = circulos, flows = rombos para diferencial visual. - Lock-unlock derivado: pendiente_unlocked vs pendiente_locked segun depends. - Animacion lerp 1s ease-in-out entre prev/current position en cambios de status entre reloads. - HUD strip: LV + XP + conteos por bucket. - Pan: drag derecho/medio. Zoom: rueda centrada en cursor. uses_functions: - parse_md_frontmatter_cpp_core (ya existia desde 0109a) - compute_ring_layout_cpp_core (NUEVA, pure, 10/10 tests, 142 assertions) Self-test: 166 nodes, parse_errors=0, unmapped=0, exit 0. Breakdown: done=77 in-progress=2 unlocked=64 locked=22 deferred=1. Sigue: 0109c (Inspector con DoD parseado y uses_functions linkables). Co-Authored-By: Claude Opus 4.7 (1M context) --- CMakeLists.txt | 1 + app.md | 1 + main.cpp | 537 +++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 453 insertions(+), 86 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 03c4b5f..0771bf7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ add_imgui_app(skill_tree main.cpp ${CMAKE_SOURCE_DIR}/functions/core/parse_md_frontmatter.cpp + ${CMAKE_SOURCE_DIR}/functions/core/compute_ring_layout.cpp ) target_include_directories(skill_tree PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/app.md b/app.md index dad94ee..eaa1fa1 100644 --- a/app.md +++ b/app.md @@ -9,6 +9,7 @@ icon: accent: "#c026d3" uses_functions: - parse_md_frontmatter_cpp_core + - compute_ring_layout_cpp_core uses_types: [] framework: "imgui" entry_point: "main.cpp" diff --git a/main.cpp b/main.cpp index c4652de..c087b78 100644 --- a/main.cpp +++ b/main.cpp @@ -4,8 +4,11 @@ #include "core/icons_tabler.h" #include "core/logger.h" #include "core/parse_md_frontmatter.h" +#include "core/compute_ring_layout.h" #include +#include +#include #include #include #include @@ -13,6 +16,8 @@ #include #include #include +#include +#include #include namespace fs = std::filesystem; @@ -22,26 +27,83 @@ namespace fs = std::filesystem; enum class NodeKind { Issue, Flow }; struct Node { - NodeKind kind; - std::string id; // "0109" / "0109a" / "0001" (flow) - std::string title; - std::string status; // pendiente|in-progress|completado|bloqueado|deferred|completed|pending - std::string type; // epic|feature|bugfix|... (issues only) - std::string priority; + NodeKind kind; + std::string id; + std::string title; + std::string status_raw; // status as read from frontmatter + std::string status_eff; // effective status after lock derivation + std::string type; + std::string priority; std::vector domain; - std::vector depends; + std::vector depends; // for issues std::vector related; - std::string file_path; + std::string file_path; + int mtime_rank = 0; // for recency 0..1 + + float x = 0.0f; + float y = 0.0f; + int ring = -1; + int sector = 0; + + // animation + float prev_x = 0.0f; + float prev_y = 0.0f; + double anim_start = 0.0; // seconds since epoch when last status change + std::string anim_prev_status; // status when anim_start was set }; struct ScanResult { - std::vector nodes; - std::map count_by_status; - std::map count_by_domain; - std::map count_by_kind; - int parse_errors = 0; + std::vector nodes; + std::map count_by_status; + std::map count_by_domain; + std::map count_by_kind; + int parse_errors = 0; }; +// ---- Constants ---------------------------------------------------------- + +// Canonical domain order from dev/TAXONOMY.md (18 sectors = 18 domains). +static const std::vector kDomainOrder = { + "meta", "cpp-stack", "kanban", "trading", "gamedev", "osint", + "data-ingest", "registry-quality", "notify", "imagegen", + "apps-infra", "dev-ux", "deploy", "frontend", "mcp", + "browser", "telemetry", "docs", +}; + +// status_eff buckets -> ring index +static const fn_ring::StatusRingMap kStatusMap = { + {"completado", 0}, + {"completed", 0}, + {"in-progress", 1}, + {"pendiente_unlocked", 2}, + {"pendiente_locked", 3}, + {"deferred", 4}, + {"bloqueado", 4}, +}; + +// Colors per ring (RGBA u32, ABGR memory order for IM_COL32) +static ImU32 ring_color(int ring) { + switch (ring) { + case 0: return IM_COL32( 76, 175, 80, 230); // green completado + case 1: return IM_COL32(255, 193, 7, 230); // amber in-progress + case 2: return IM_COL32(192, 38, 211, 230); // fuchsia unlocked (accent) + case 3: return IM_COL32(120, 120, 130, 180); // gray locked + case 4: return IM_COL32( 80, 80, 90, 110); // dim deferred / bloqueado + default: return IM_COL32(150, 150, 150, 120); + } +} + +static const char* ring_label(int ring) { + switch (ring) { + case 0: return "done"; + case 1: return "in-progress"; + case 2: return "unlocked"; + case 3: return "locked"; + case 4: return "deferred"; + default: return "?"; + } +} + // ---- Registry root discovery ------------------------------------------- static fs::path discover_registry_root() { @@ -49,7 +111,6 @@ static fs::path discover_registry_root() { fs::path p(env); if (fs::exists(p / "registry.db")) return p; } - // walk up from cwd for (auto p = fs::current_path(); !p.empty() && p != p.root_path(); p = p.parent_path()) { if (fs::exists(p / "registry.db") && fs::exists(p / "dev" / "issues")) return p; } @@ -82,6 +143,11 @@ static std::vector get_list(const fn_md::Frontmatter& fm, const cha return {}; } +static double now_seconds() { + using namespace std::chrono; + return duration_cast>(steady_clock::now().time_since_epoch()).count(); +} + // ---- Scanner ------------------------------------------------------------ static void scan_dir(const fs::path& dir, NodeKind kind, ScanResult& out) { @@ -90,7 +156,6 @@ static void scan_dir(const fs::path& dir, NodeKind kind, ScanResult& out) { if (!entry.is_regular_file()) continue; const auto& p = entry.path(); if (p.extension() != ".md") continue; - // skip template.md / INDEX.md / README.md / AGENT_GUIDE.md auto stem = p.stem().string(); if (stem == "template" || stem == "INDEX" || stem == "README" || stem == "AGENT_GUIDE") continue; @@ -99,24 +164,20 @@ static void scan_dir(const fs::path& dir, NodeKind kind, ScanResult& out) { if (!fm.has_frontmatter) { ++out.parse_errors; continue; } Node n; - n.kind = kind; - n.id = get_str(fm, kind == NodeKind::Issue ? "id" : "id"); + n.kind = kind; + n.id = get_str(fm, "id"); if (n.id.empty()) n.id = get_str(fm, "name"); - n.title = get_str(fm, kind == NodeKind::Issue ? "title" : "name"); - n.status = get_str(fm, "status"); - n.type = get_str(fm, "type"); - n.priority = get_str(fm, "priority"); - n.domain = get_list(fm, "domain"); - n.depends = get_list(fm, kind == NodeKind::Issue ? "depends" : "related_issues"); - n.related = get_list(fm, "related"); - n.file_path = p.string(); + n.title = get_str(fm, kind == NodeKind::Issue ? "title" : "name"); + n.status_raw = get_str(fm, "status"); + n.type = get_str(fm, "type"); + n.priority = get_str(fm, "priority"); + n.domain = get_list(fm, "domain"); + n.depends = get_list(fm, kind == NodeKind::Issue ? "depends" : "related_issues"); + n.related = get_list(fm, "related"); + n.file_path = p.string(); - if (n.status.empty()) n.status = "(unknown)"; - if (n.domain.empty()) n.domain.push_back("(unknown)"); - - ++out.count_by_status[n.status]; - ++out.count_by_kind[kind == NodeKind::Issue ? "issue" : "flow"]; - for (const auto& d : n.domain) ++out.count_by_domain[d]; + if (n.status_raw.empty()) n.status_raw = "(unknown)"; + if (n.domain.empty()) n.domain.push_back("(unknown)"); out.nodes.push_back(std::move(n)); } @@ -124,28 +185,332 @@ static void scan_dir(const fs::path& dir, NodeKind kind, ScanResult& out) { static ScanResult scan_registry(const fs::path& root) { ScanResult r; - scan_dir(root / "dev" / "issues", NodeKind::Issue, r); - scan_dir(root / "dev" / "issues" / "completed", NodeKind::Issue, r); - scan_dir(root / "dev" / "flows", NodeKind::Flow, r); - scan_dir(root / "dev" / "flows" / "completed", NodeKind::Flow, r); + scan_dir(root / "dev" / "issues", NodeKind::Issue, r); + scan_dir(root / "dev" / "issues" / "completed", NodeKind::Issue, r); + scan_dir(root / "dev" / "flows", NodeKind::Flow, r); + scan_dir(root / "dev" / "flows" / "completed", NodeKind::Flow, r); return r; } +// ---- Lock-unlock derivation -------------------------------------------- + +static void derive_status_eff(std::vector& nodes) { + // Build set of "done" IDs (completado / completed). + std::unordered_set done; + for (const auto& n : nodes) { + if (n.status_raw == "completado" || n.status_raw == "completed") done.insert(n.id); + } + for (auto& n : nodes) { + const auto& s = n.status_raw; + if (s == "completado" || s == "completed") { + n.status_eff = "completado"; + } else if (s == "in-progress") { + n.status_eff = "in-progress"; + } else if (s == "deferred") { + n.status_eff = "deferred"; + } else if (s == "bloqueado") { + n.status_eff = "bloqueado"; + } else if (s == "pendiente" || s == "pending") { + // Locked if any depends/related_issues is NOT in done. + bool locked = false; + for (const auto& d : n.depends) { + if (!d.empty() && done.find(d) == done.end()) { locked = true; break; } + } + n.status_eff = locked ? "pendiente_locked" : "pendiente_unlocked"; + } else { + n.status_eff = "deferred"; // unknown bucket -> outer ring + } + } +} + // ---- UI state ----------------------------------------------------------- -static fs::path g_root; +static fs::path g_root; static ScanResult g_scan; -static bool g_show_tree = true; -static bool g_show_inspector = true; -static int g_selected = -1; +static bool g_show_tree = true; +static bool g_show_inspector = true; +static int g_selected = -1; +static int g_hover = -1; +static float g_cam_x = 0.0f; +static float g_cam_y = 0.0f; +static float g_cam_zoom = 1.0f; + +static const float kNodeRadius = 18.0f; +static const float kAnimDur = 1.0f; // seconds for ring migration lerp + +// Apply ring layout to nodes, preserving anim state across reloads. +static void apply_layout(std::vector& nodes) { + std::vector input; + input.reserve(nodes.size()); + for (const auto& n : nodes) { + fn_ring::LayoutInput li; + li.id = n.id; + li.status = n.status_eff; + li.domain = n.domain.empty() ? "(unknown)" : n.domain.front(); + li.recency = 0.0f; + input.push_back(std::move(li)); + } + + fn_ring::LayoutConfig cfg; + cfg.n_sectors = 18; + cfg.center_x = 0.0f; + cfg.center_y = 0.0f; + cfg.ring_radii = { 0.0f, 150.0f, 280.0f, 450.0f, 650.0f, 850.0f }; + cfg.bin_padding = 18.0f; + cfg.start_angle = -1.5708f; // -PI/2: sector 0 starts at 12 o'clock + + auto out = fn_ring::compute_ring_layout(input, cfg, kStatusMap, kDomainOrder); + + // Map by id for O(1) assignment. + std::unordered_map by_id; + by_id.reserve(out.size()); + for (const auto& o : out) by_id.emplace(o.id, &o); + + double now = now_seconds(); + for (auto& n : nodes) { + auto it = by_id.find(n.id); + if (it == by_id.end()) continue; + const auto& o = *it->second; + + // Detect status change since last layout: kick off anim. + if (!n.anim_prev_status.empty() && n.anim_prev_status != n.status_eff) { + n.prev_x = n.x; + n.prev_y = n.y; + n.anim_start = now; + } + if (n.anim_prev_status.empty()) { + // first apply: no anim needed + n.prev_x = o.x; + n.prev_y = o.y; + n.anim_start = now - kAnimDur; // already finished + } + n.x = o.x; + n.y = o.y; + n.ring = o.ring; + n.sector = o.sector; + n.anim_prev_status = n.status_eff; + } +} + +static void recount(ScanResult& s) { + s.count_by_status.clear(); + s.count_by_domain.clear(); + s.count_by_kind.clear(); + for (const auto& n : s.nodes) { + ++s.count_by_status[n.status_eff]; + ++s.count_by_kind[n.kind == NodeKind::Issue ? "issue" : "flow"]; + for (const auto& d : n.domain) ++s.count_by_domain[d]; + } +} static void reload_scan() { - g_scan = scan_registry(g_root); + auto fresh = scan_registry(g_root); + derive_status_eff(fresh.nodes); + + // Preserve animation state by id across reload. + std::unordered_map prev_by_id; + for (auto& n : g_scan.nodes) prev_by_id.emplace(n.id, std::move(n)); + for (auto& n : fresh.nodes) { + auto it = prev_by_id.find(n.id); + if (it != prev_by_id.end()) { + n.prev_x = it->second.x; + n.prev_y = it->second.y; + n.anim_start = it->second.anim_start; + n.anim_prev_status = it->second.anim_prev_status; + } + } + + g_scan = std::move(fresh); + apply_layout(g_scan.nodes); + recount(g_scan); g_selected = -1; fn_log::log_info("skill_tree: reloaded %d nodes, %d parse errors", (int)g_scan.nodes.size(), g_scan.parse_errors); } +// ---- Canvas drawing ----------------------------------------------------- + +static ImVec2 world_to_screen(const ImVec2& origin, float wx, float wy) { + return ImVec2(origin.x + wx * g_cam_zoom + g_cam_x, + origin.y + wy * g_cam_zoom + g_cam_y); +} + +static void draw_canvas() { + const ImVec2 avail = ImGui::GetContentRegionAvail(); + if (avail.x < 40 || avail.y < 40) return; + + ImGui::InvisibleButton("canvas", avail, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); + const bool hovered = ImGui::IsItemHovered(); + const bool active = ImGui::IsItemActive(); + + const ImVec2 p0 = ImGui::GetItemRectMin(); + const ImVec2 p1 = ImGui::GetItemRectMax(); + const ImVec2 center(0.5f * (p0.x + p1.x), 0.5f * (p0.y + p1.y)); + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->PushClipRect(p0, p1, true); + + // Background. + dl->AddRectFilled(p0, p1, IM_COL32(18, 18, 22, 255)); + + // Pan with right or middle drag. + if (active && ImGui::IsMouseDragging(ImGuiMouseButton_Right, 0.0f)) { + ImVec2 d = ImGui::GetIO().MouseDelta; + g_cam_x += d.x; + g_cam_y += d.y; + } + if (active && ImGui::IsMouseDragging(ImGuiMouseButton_Middle, 0.0f)) { + ImVec2 d = ImGui::GetIO().MouseDelta; + g_cam_x += d.x; + g_cam_y += d.y; + } + // Zoom with wheel (centered on mouse). + if (hovered && std::abs(ImGui::GetIO().MouseWheel) > 0.0f) { + const float wheel = ImGui::GetIO().MouseWheel; + const float old_zoom = g_cam_zoom; + const float new_zoom = std::clamp(old_zoom * (1.0f + wheel * 0.12f), 0.2f, 4.0f); + // Keep mouse-world point under cursor: + ImVec2 mp = ImGui::GetMousePos(); + float mx_world = (mp.x - center.x - g_cam_x) / old_zoom; + float my_world = (mp.y - center.y - g_cam_y) / old_zoom; + g_cam_zoom = new_zoom; + g_cam_x = (mp.x - center.x) - mx_world * new_zoom; + g_cam_y = (mp.y - center.y) - my_world * new_zoom; + } + + // Origin honoring pan; concentric backdrop rings. + static const float radii[] = { 150.0f, 280.0f, 450.0f, 650.0f, 850.0f }; + ImVec2 origin_with_pan(center.x + g_cam_x, center.y + g_cam_y); + for (float r : radii) { + dl->AddCircle(origin_with_pan, r * g_cam_zoom, IM_COL32(255, 255, 255, 22), 96, 1.0f); + } + // Center marker. + dl->AddCircleFilled(origin_with_pan, 3.0f, IM_COL32(255, 255, 255, 80)); + + // Build screen-space index for picking. + const double now = now_seconds(); + auto node_screen = [&](const Node& n) { + float t = std::clamp(float(now - n.anim_start) / kAnimDur, 0.0f, 1.0f); + // ease-in-out + float e = t * t * (3.0f - 2.0f * t); + float wx = n.prev_x + (n.x - n.prev_x) * e; + float wy = n.prev_y + (n.y - n.prev_y) * e; + return ImVec2(origin_with_pan.x + wx * g_cam_zoom, + origin_with_pan.y + wy * g_cam_zoom); + }; + + // Edges: depends + related as thin lines between ID-matched nodes. + std::unordered_map idx_by_id; + idx_by_id.reserve(g_scan.nodes.size()); + for (int i = 0; i < (int)g_scan.nodes.size(); ++i) idx_by_id[g_scan.nodes[i].id] = i; + + auto draw_edge = [&](int src, int dst, ImU32 col) { + if (src < 0 || dst < 0) return; + ImVec2 a = node_screen(g_scan.nodes[src]); + ImVec2 b = node_screen(g_scan.nodes[dst]); + // Curve toward center. + ImVec2 mid((a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f); + ImVec2 toCenter(origin_with_pan.x - mid.x, origin_with_pan.y - mid.y); + float len = std::sqrt(toCenter.x * toCenter.x + toCenter.y * toCenter.y); + if (len > 0.001f) { toCenter.x /= len; toCenter.y /= len; } + ImVec2 c1(mid.x + toCenter.x * 40.0f * g_cam_zoom, mid.y + toCenter.y * 40.0f * g_cam_zoom); + dl->AddBezierQuadratic(a, c1, b, col, 1.2f); + }; + + for (int i = 0; i < (int)g_scan.nodes.size(); ++i) { + const auto& n = g_scan.nodes[i]; + for (const auto& d : n.depends) { + auto it = idx_by_id.find(d); + if (it != idx_by_id.end()) draw_edge(it->second, i, IM_COL32(160, 160, 180, 90)); + } + for (const auto& r : n.related) { + auto it = idx_by_id.find(r); + if (it != idx_by_id.end()) draw_edge(it->second, i, IM_COL32(120, 100, 180, 50)); + } + } + + // Picking + node draw. + g_hover = -1; + const ImVec2 mp = ImGui::GetMousePos(); + const float node_r = kNodeRadius * g_cam_zoom; + + for (int i = 0; i < (int)g_scan.nodes.size(); ++i) { + const auto& n = g_scan.nodes[i]; + if (n.ring < 0) continue; + ImVec2 sp = node_screen(n); + + // Cull off-screen. + if (sp.x < p0.x - node_r || sp.x > p1.x + node_r) continue; + if (sp.y < p0.y - node_r || sp.y > p1.y + node_r) continue; + + float dx = mp.x - sp.x, dy = mp.y - sp.y; + bool over = hovered && (dx * dx + dy * dy) < node_r * node_r; + if (over) g_hover = i; + + ImU32 col = ring_color(n.ring); + // Flow nodes diamond outline as visual differentiator. + if (n.kind == NodeKind::Flow) { + ImVec2 pts[4] = { + { sp.x, sp.y - node_r }, + { sp.x + node_r, sp.y }, + { sp.x, sp.y + node_r }, + { sp.x - node_r, sp.y }, + }; + dl->AddConvexPolyFilled(pts, 4, col); + dl->AddPolyline(pts, 4, IM_COL32(255, 255, 255, 200), ImDrawFlags_Closed, 1.5f); + } else { + dl->AddCircleFilled(sp, node_r, col, 24); + ImU32 outline = (g_selected == i) ? IM_COL32(255, 255, 255, 255) + : (over ? IM_COL32(255, 255, 255, 200) + : IM_COL32(255, 255, 255, 80)); + dl->AddCircle(sp, node_r, outline, 24, g_selected == i ? 2.5f : 1.2f); + } + + // Text: ID (and short title on hover/select). + if (g_cam_zoom > 0.55f) { + const char* lbl = n.id.c_str(); + ImVec2 ts = ImGui::CalcTextSize(lbl); + ImVec2 tp(sp.x - ts.x * 0.5f, sp.y - ts.y * 0.5f); + // White text with shadow for readability. + dl->AddText(ImVec2(tp.x + 1, tp.y + 1), IM_COL32(0, 0, 0, 200), lbl); + dl->AddText(tp, IM_COL32(255, 255, 255, 240), lbl); + } + + // Tooltip on hover (title). + if (over) { + ImGui::BeginTooltip(); + ImGui::Text("%s %s", n.kind == NodeKind::Issue ? "[ISSUE]" : "[FLOW]", n.id.c_str()); + ImGui::TextWrapped("%s", n.title.c_str()); + ImGui::TextDisabled("status: %s · ring: %s · domain: %s", + n.status_eff.c_str(), + ring_label(n.ring), + n.domain.empty() ? "?" : n.domain.front().c_str()); + ImGui::EndTooltip(); + } + } + + // Click: select. + if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && g_hover >= 0) { + g_selected = g_hover; + } + + // Sector labels at outermost ring. + if (g_cam_zoom > 0.4f) { + const int N = 18; + const float r = radii[std::size(radii) - 1] - 8.0f; + for (int s = 0; s < N; ++s) { + float theta = -1.5708f + (s + 0.5f) * (2.0f * 3.14159265f / N); + ImVec2 sp(origin_with_pan.x + std::cos(theta) * r * g_cam_zoom, + origin_with_pan.y + std::sin(theta) * r * g_cam_zoom); + const std::string& dom = (s < (int)kDomainOrder.size()) ? kDomainOrder[s] : std::string("(other)"); + ImVec2 ts = ImGui::CalcTextSize(dom.c_str()); + dl->AddText(ImVec2(sp.x - ts.x * 0.5f, sp.y - ts.y * 0.5f), + IM_COL32(255, 255, 255, 110), dom.c_str()); + } + } + + dl->PopClipRect(); +} + // ---- Panels ------------------------------------------------------------- static void draw_tree() { @@ -154,40 +519,25 @@ static void draw_tree() { return; } - ImGui::Text("skill_tree v0.1.0 — fase A (shell)"); - ImGui::Text("Root: %s", g_root.string().c_str()); + // HUD strip. + int n_done = g_scan.count_by_status["completado"]; + int n_inprog = g_scan.count_by_status["in-progress"]; + int n_unlock = g_scan.count_by_status["pendiente_unlocked"]; + int n_lock = g_scan.count_by_status["pendiente_locked"]; + int n_defer = g_scan.count_by_status["deferred"] + g_scan.count_by_status["bloqueado"]; + int xp_total = 10 * n_done; // crude: tune later (epic=10 etc.) + int level = (int)std::sqrt((float)xp_total); + + ImGui::Text("LV %d · XP %d · %d done · %d in-progress · %d unlocked · %d locked · %d deferred", + level, xp_total, n_done, n_inprog, n_unlock, n_lock, n_defer); + ImGui::SameLine(); + if (ImGui::SmallButton(TI_REFRESH " Reload (F5)")) reload_scan(); + if (ImGui::IsKeyPressed(ImGuiKey_F5, false)) reload_scan(); + ImGui::SameLine(); + if (ImGui::SmallButton("Reset view")) { g_cam_x = g_cam_y = 0.0f; g_cam_zoom = 1.0f; } + ImGui::Separator(); - - ImGui::Text("Total: %d nodos (%d issues + %d flows). Parse errors: %d", - (int)g_scan.nodes.size(), - g_scan.count_by_kind["issue"], - g_scan.count_by_kind["flow"], - g_scan.parse_errors); - - if (ImGui::Button(TI_REFRESH " Reload (F5)")) reload_scan(); - if (ImGui::IsKeyPressed(ImGuiKey_F5, false)) reload_scan(); - - ImGui::SeparatorText("Por status"); - for (auto& [k, v] : g_scan.count_by_status) { - ImGui::Text(" %-16s %d", k.c_str(), v); - } - - ImGui::SeparatorText("Por dominio"); - for (auto& [k, v] : g_scan.count_by_domain) { - ImGui::Text(" %-22s %d", k.c_str(), v); - } - - ImGui::SeparatorText("Nodos (lista provisional — render anillos en 0109b)"); - ImGui::BeginChild("nodes_list", ImVec2(0, 0), ImGuiChildFlags_Borders); - for (int i = 0; i < (int)g_scan.nodes.size(); ++i) { - const auto& n = g_scan.nodes[i]; - char label[512]; - std::snprintf(label, sizeof(label), "%s %s · %s · %s##%d", - n.kind == NodeKind::Issue ? "[ISSUE]" : "[FLOW] ", - n.id.c_str(), n.status.c_str(), n.title.c_str(), i); - if (ImGui::Selectable(label, g_selected == i)) g_selected = i; - } - ImGui::EndChild(); + draw_canvas(); ImGui::End(); } @@ -205,10 +555,11 @@ static void draw_inspector() { ImGui::Text("%s %s", n.kind == NodeKind::Issue ? "[ISSUE]" : "[FLOW]", n.id.c_str()); ImGui::TextWrapped("%s", n.title.c_str()); ImGui::Separator(); - ImGui::Text("status: %s", n.status.c_str()); - if (!n.type.empty()) ImGui::Text("type: %s", n.type.c_str()); - if (!n.priority.empty()) ImGui::Text("priority: %s", n.priority.c_str()); - ImGui::Text("file: %s", n.file_path.c_str()); + ImGui::Text("status: %s (eff: %s)", n.status_raw.c_str(), n.status_eff.c_str()); + ImGui::Text("ring: %s", ring_label(n.ring)); + if (!n.type.empty()) ImGui::Text("type: %s", n.type.c_str()); + if (!n.priority.empty()) ImGui::Text("priority: %s", n.priority.c_str()); + ImGui::Text("file: %s", n.file_path.c_str()); ImGui::SeparatorText("domain"); for (const auto& d : n.domain) ImGui::BulletText("%s", d.c_str()); @@ -223,7 +574,7 @@ static void draw_inspector() { } ImGui::Separator(); - ImGui::TextDisabled("Botones [Generate ideas] y [Run autonomous-task] llegan en 0109e/f."); + ImGui::TextDisabled("Botones [Generate ideas] / [Run autonomous-task] llegan en 0109e/f."); ImGui::End(); } @@ -241,18 +592,32 @@ static int run_self_test() { return 1; } g_scan = scan_registry(g_root); - std::printf("skill_tree v0.1.0\n"); + derive_status_eff(g_scan.nodes); + apply_layout(g_scan.nodes); + recount(g_scan); + + std::printf("skill_tree v0.2.0\n"); std::printf("root: %s\n", g_root.string().c_str()); std::printf("nodes: %zu (%d issues + %d flows)\n", g_scan.nodes.size(), g_scan.count_by_kind["issue"], g_scan.count_by_kind["flow"]); std::printf("parse_errors: %d\n", g_scan.parse_errors); - std::printf("by_status:\n"); - for (auto& [k, v] : g_scan.count_by_status) std::printf(" %-16s %d\n", k.c_str(), v); - std::printf("by_domain:\n"); - for (auto& [k, v] : g_scan.count_by_domain) std::printf(" %-22s %d\n", k.c_str(), v); - return g_scan.parse_errors == 0 ? 0 : 2; + + // Ring breakdown. + int per_ring[5] = {0}; + int unmapped = 0; + for (const auto& n : g_scan.nodes) { + if (n.ring < 0 || n.ring >= 5) ++unmapped; + else ++per_ring[n.ring]; + } + std::printf("by_ring: done=%d in-progress=%d unlocked=%d locked=%d deferred=%d unmapped=%d\n", + per_ring[0], per_ring[1], per_ring[2], per_ring[3], per_ring[4], unmapped); + + std::printf("by_status_eff:\n"); + for (auto& [k, v] : g_scan.count_by_status) std::printf(" %-22s %d\n", k.c_str(), v); + + return (g_scan.parse_errors == 0 && unmapped == 0) ? 0 : 2; } // ---- Entry -------------------------------------------------------------- @@ -263,7 +628,7 @@ int main(int argc, char** argv) { } g_root = discover_registry_root(); - g_scan = scan_registry(g_root); + reload_scan(); static fn_ui::PanelToggle panels[] = { { "Tree", nullptr, &g_show_tree }, @@ -272,7 +637,7 @@ int main(int argc, char** argv) { fn::AppConfig cfg; cfg.title = "skill_tree"; - cfg.about = { "skill_tree", "0.1.0", + cfg.about = { "skill_tree", "0.2.0", "Mapa interactivo de issues+flows en anillos concentricos por estado." }; cfg.log = { "skill_tree.log", 1 }; cfg.panels = panels;