---
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).
