---
id: "0109b"
title: "skill_tree layout anillos + render canvas ImDrawList con cards"
status: completado
type: feature
domain:
  - meta
  - cpp-stack
scope: app-scoped
priority: media
depends:
  - "0109a"
blocks:
  - "0109c"
related:
  - "0109"
created: 2026-05-17
updated: 2026-05-17
tags:
  - skill-tree
  - cpp
  - imgui
  - layout
  - canvas
---

# 0109b — skill_tree layout anillos + render canvas

Segundo slice del epic 0109. Reemplaza la lista textual del Tree por un canvas interactivo basado en `ImDrawList`. Pivote desde `graph_renderer_cpp_viz` (GPU) → `ImDrawList` (CPU) para mantener simplicidad: 166 nodos no justifican el pipeline GPU.

## Decisiones tomadas durante la implementacion

- **Stack: `ImGui::ImDrawList`**, NO `graph_renderer_cpp_viz`. Razon: 166 nodos cabian de sobra en CPU; `graph_renderer` exige `init_gl_loader=true`, build de `GraphData` con tipos OSINT, shaders, FBO + texture flip-Y. Diferencia ~120 LOC + un monton de rebuilds para cero beneficio observable.
- **Sin fisicas** (el usuario lo pidio explicito). Layout deterministico via `compute_ring_layout_cpp_core`.
- **5 rings**: done (0), in-progress (1), unlocked (2), locked (3), deferred/bloqueado (4).
- **18 sectores radiales** = 18 dominios canonicos (`dev/TAXONOMY.md`). Labels en el aro exterior.
- **Lock derivation**: `pendiente` se subdivide en `pendiente_unlocked` (todos los `depends[]` en done) vs `pendiente_locked` (algun depends sin completar). Set de `done` IDs se computa al cargar y se cruza con cada `depends[]`.
- **Animacion lerp 1s** entre prev y current position cuando un nodo cambia de `status_eff` entre dos `reload_scan()`s. Ease-in-out cuadratica.
- **Cards con texto**: cada nodo muestra su ID en blanco con sombra negra para legibilidad sobre cualquier color de ring.
- **Diferencial visual flows vs issues**: issues = circulos, flows = rombos.
- **Pan**: drag con boton derecho o medio.
- **Zoom**: rueda del raton, centrado en cursor (re-anchora coordenadas mundo bajo el puntero).
- **Picking**: O(N) radius check (166 nodos = trivial; spatial hash innecesario).

## Tareas

1. Crear funcion del registry `compute_ring_layout_cpp_core` (pure, 10 tests Catch2, FNV-1a determinista para sub-jitter angular).
2. Reescribir `main.cpp::draw_tree()` como canvas con `ImGui::InvisibleButton` + `ImDrawList`.
3. Implementar `derive_status_eff()` para lock/unlock.
4. Implementar `apply_layout()` con preservacion de prev_x/prev_y para animacion.
5. Render: aristas curvas Bezier (depends + related) + nodos con outline + label ID + tooltip on hover.
6. HUD strip con LV/XP/contadores.
7. Self-test 0-exit cuando `parse_errors == 0 && unmapped == 0`.
8. Build Linux + deploy Windows.

## DoD

- [x] `compute_ring_layout_cpp_core` indexada (10/10 tests, 142 assertions).
- [x] `apps/skill_tree/main.cpp` usa `parse_md_frontmatter_cpp_core` + `compute_ring_layout_cpp_core`.
- [x] `app.md uses_functions` actualizado con ambas.
- [x] Self-test imprime breakdown por ring: `done=77 in-progress=2 unlocked=64 locked=22 deferred=1 unmapped=0`.
- [x] Linux build OK.
- [x] Windows deploy OK (PID corriendo).
- [x] Tarjeta visible en `app_hub_launcher`.
- [x] `fn doctor cpp-apps` limpio.

## Sigue

0109c: Inspector evolucionado con DoD parseado de la seccion `## DoD` del .md (checkboxes interactivos read-only) + lista de `uses_functions` del registry para esa issue.
