--- id: 0037 title: Placement direccional de orphans — abanico de 45 grados en una sola direccion status: done priority: high created: 2026-05-04 completed: 2026-05-03 --- ## Contexto Hoy `place_orphans_near_neighbors` (en `main.cpp`) reparte los hijos del mismo anchor en un anillo de 360 grados alrededor del padre. Resultado: con muchos hijos, el viewport se llena en todas las direcciones, pisando nodos preexistentes y dificultando la lectura del grafo. Issue 0035 ya cluster los hijos del mismo anchor (no los desperdiga por anillos crecientes), pero siguen ocupando 360 grados. ## Objetivo Que los nuevos hijos creados por un enricher (orphans con un anchor detectado por `layout_first_placed_neighbor`) se desplieguen en un **abanico de 45 grados** (no 360) en UNA sola direccion saliendo del anchor. Asi el grafo "crece" en una direccion clara por cada ejecucion de enricher, sin pisar lo existente. ## Diseño ### 1. Direccion del abanico — outward por defecto Para cada anchor con orphans: - Calcular el centroide del resto de nodos placed del grafo (excluyendo los orphans propios y el anchor). - La direccion outward es el vector `anchor - centroide`, normalizado. - Si solo hay 1 nodo en el grafo (el propio anchor) o el centroide coincide con el anchor: usar direccion `(1, 0)` (derecha). ### 2. Reparto en abanico de 45 grados `arc_span = π / 4` (45 grados, configurable como constante). Para los N orphans del anchor: - Si `N == 1`: lo plantamos exactamente en la direccion outward al radio base (~80 px). - Si `N <= 8` (capacidad del primer anillo en 45 grados con min_dist): se reparten equiespaciados en el arco a un radio fijo. - Si `N > 8`: se llenan anillos sucesivos del mismo arco con radios crecientes (`80, 140, 200, 280, 400`) y misma capacidad por anillo. Cada slot dentro del arco tiene angulo: ``` angle = out_angle - arc_span/2 + slot * (arc_span / (slot_count - 1)) ``` (ajusta para cuando `slot_count == 1` para no dividir entre cero). ### 3. Colision con nodos preexistentes Despues de calcular `(px, py)` para cada hijo, si colisiona con un nodo no-orphan, incrementar el indice de anillo (mismo angulo, radio mayor) hasta encontrar hueco o agotar anillos. En el ultimo recurso aceptar solape — coherente con la logica actual. ### 4. Fallback sin anchor Los orphans sin anchor (no tienen vecino placed) siguen el camino existente: ring placement alrededor de la camara o parking lot. NO se les aplica el abanico. ## Acceptance criteria - Lanzar `split_words` (≥50 palabras unicas) sobre un nodo `text`: los 10 sueltos del preview + 1 Group caen en un abanico de 45 grados saliendo del nodo `text`, en la direccion alejada del resto del grafo. - Lanzar el mismo enricher otra vez sobre OTRO nodo en otra posicion: el abanico nuevo apunta hacia su propia direccion outward. - Si el grafo solo tiene 1 nodo (sin existing centroide), el abanico sale a la derecha por default. - Tests pytest siguen verdes (no requiere tests nuevos — el cambio es algoritmico y se valida visualmente; opcional anyadir un test C++ standalone que verifique angulos contra una fixture). ## TBD Branch `issue/0037-directional-orphan-placement`, merge `--no-ff` a master. ## Out of scope - Animar la transicion del placement (poof-in suave). Fase 2. - Layout interno del Group cuando se expande — sigue siendo todos los hijos del Group ocultos en colapsado. - Grouping logic (eso es 0035, ya cerrado fase 1).