fix(skill_tree): anti-overlap + flows mas visibles
- kNodeRadius 18 -> 10 (mas cards caben sin pisarse)
- ring_radii expandidos { 0, 200, 380, 600, 900, 1200 } (mas banda por anillo)
- bin_padding 18 -> 28 (margen entre anillos)
- Initial zoom = auto-fit (Min(canvas) * 0.92 / 2400) en lugar de zoom=1
- Boton 'Fit view' reemplaza 'Reset view'
- Flows ahora 1.55x mayores que issues + outline cyan permanente para
destacar entre la marea de 160 circulos
- Render en 2 pasadas: issues primero (background), flows on top
- Labels de ID siempre visibles en flows (no condicionado al zoom)
- Ring bands con tint sutil por status para legibilidad
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -235,8 +235,12 @@ 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
|
||||
static const float kNodeRadius = 10.0f;
|
||||
static const float kFlowRadiusMul = 1.55f;
|
||||
static const float kAnimDur = 1.0f; // seconds for ring migration lerp
|
||||
static const float kWorldExtent = 1200.0f * 2.0f; // diameter to fit
|
||||
static const std::vector<float> kRingRadii = { 0.0f, 200.0f, 380.0f, 600.0f, 900.0f, 1200.0f };
|
||||
static bool g_fit_pending = true; // auto-fit on first frame
|
||||
|
||||
// Apply ring layout to nodes, preserving anim state across reloads.
|
||||
static void apply_layout(std::vector<Node>& nodes) {
|
||||
@@ -255,8 +259,8 @@ static void apply_layout(std::vector<Node>& nodes) {
|
||||
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.ring_radii = kRingRadii;
|
||||
cfg.bin_padding = 28.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);
|
||||
@@ -378,10 +382,35 @@ static void draw_canvas() {
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Auto-fit on first frame (after avail is known): scale so outer ring fits with margin.
|
||||
if (g_fit_pending) {
|
||||
float min_dim = std::min(avail.x, avail.y);
|
||||
g_cam_zoom = std::clamp((min_dim * 0.92f) / kWorldExtent, 0.05f, 4.0f);
|
||||
g_cam_x = 0.0f;
|
||||
g_cam_y = 0.0f;
|
||||
g_fit_pending = false;
|
||||
}
|
||||
// Ring band fills with subtle tint by status.
|
||||
static const ImU32 ring_band_tint[5] = {
|
||||
IM_COL32( 30, 60, 30, 40), // done
|
||||
IM_COL32( 60, 50, 20, 40), // in-progress
|
||||
IM_COL32( 60, 20, 60, 40), // unlocked
|
||||
IM_COL32( 32, 32, 40, 40), // locked
|
||||
IM_COL32( 20, 20, 24, 40), // deferred
|
||||
};
|
||||
for (int i = 0; i < (int)kRingRadii.size() - 1; ++i) {
|
||||
float ri = kRingRadii[i] * g_cam_zoom;
|
||||
float ro = kRingRadii[i + 1] * g_cam_zoom;
|
||||
// Filled annulus via two circles (approximate); ImDrawList lacks ring fill.
|
||||
// Trick: filled circle outer alpha + cut center with inner. Use AddCircleFilled twice
|
||||
// — outer in tint, inner in bg to subtract. Cheaper: thick line outline + tinted bg.
|
||||
dl->AddCircleFilled(origin_with_pan, ro, ring_band_tint[i], 96);
|
||||
if (ri > 0.5f) dl->AddCircleFilled(origin_with_pan, ri, IM_COL32(18, 18, 22, 255), 96);
|
||||
}
|
||||
// Ring outlines.
|
||||
for (int i = 1; i < (int)kRingRadii.size(); ++i) {
|
||||
dl->AddCircle(origin_with_pan, kRingRadii[i] * g_cam_zoom, IM_COL32(255, 255, 255, 30), 96, 1.0f);
|
||||
}
|
||||
// Center marker.
|
||||
dl->AddCircleFilled(origin_with_pan, 3.0f, IM_COL32(255, 255, 255, 80));
|
||||
@@ -428,51 +457,56 @@ static void draw_canvas() {
|
||||
}
|
||||
}
|
||||
|
||||
// Picking + node draw.
|
||||
// Picking + node draw. Two passes: issues first (background), flows on top.
|
||||
g_hover = -1;
|
||||
const ImVec2 mp = ImGui::GetMousePos();
|
||||
const float node_r = kNodeRadius * g_cam_zoom;
|
||||
const float node_r_issue = kNodeRadius * g_cam_zoom;
|
||||
const float node_r_flow = kNodeRadius * kFlowRadiusMul * g_cam_zoom;
|
||||
|
||||
for (int i = 0; i < (int)g_scan.nodes.size(); ++i) {
|
||||
auto draw_one = [&](int i, bool flow_pass) {
|
||||
const auto& n = g_scan.nodes[i];
|
||||
if (n.ring < 0) continue;
|
||||
if (n.ring < 0) return;
|
||||
bool is_flow = (n.kind == NodeKind::Flow);
|
||||
if (is_flow != flow_pass) return;
|
||||
ImVec2 sp = node_screen(n);
|
||||
const float r = is_flow ? node_r_flow : node_r_issue;
|
||||
|
||||
// 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;
|
||||
if (sp.x < p0.x - r || sp.x > p1.x + r) return;
|
||||
if (sp.y < p0.y - r || sp.y > p1.y + r) return;
|
||||
|
||||
float dx = mp.x - sp.x, dy = mp.y - sp.y;
|
||||
bool over = hovered && (dx * dx + dy * dy) < node_r * node_r;
|
||||
bool over = hovered && (dx * dx + dy * dy) < r * r;
|
||||
if (over) g_hover = i;
|
||||
|
||||
ImU32 col = ring_color(n.ring);
|
||||
// Flow nodes diamond outline as visual differentiator.
|
||||
if (n.kind == NodeKind::Flow) {
|
||||
if (is_flow) {
|
||||
// Diamond + thick cyan outline so flows pop out of the issue sea.
|
||||
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 },
|
||||
{ sp.x, sp.y - r },
|
||||
{ sp.x + r, sp.y },
|
||||
{ sp.x, sp.y + r },
|
||||
{ sp.x - 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);
|
||||
: IM_COL32(34, 211, 238, 255); // cyan-400 always
|
||||
dl->AddPolyline(pts, 4, outline, ImDrawFlags_Closed,
|
||||
(g_selected == i) ? 3.0f : 2.0f);
|
||||
} else {
|
||||
dl->AddCircleFilled(sp, r, col, 20);
|
||||
ImU32 outline = (g_selected == i) ? IM_COL32(255, 255, 255, 255)
|
||||
: (over ? IM_COL32(255, 255, 255, 220)
|
||||
: IM_COL32(255, 255, 255, 90));
|
||||
dl->AddCircle(sp, r, outline, 20, g_selected == i ? 2.5f : 1.0f);
|
||||
}
|
||||
|
||||
// Text: ID (and short title on hover/select).
|
||||
if (g_cam_zoom > 0.55f) {
|
||||
// Text only when zoomed in or this is a flow (always show flow labels).
|
||||
if ((g_cam_zoom > 0.65f) || is_flow) {
|
||||
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);
|
||||
dl->AddText(ImVec2(tp.x + 1, tp.y + 1), IM_COL32(0, 0, 0, 220), lbl);
|
||||
dl->AddText(tp, IM_COL32(255, 255, 255, 245), lbl);
|
||||
}
|
||||
|
||||
// Tooltip on hover (title).
|
||||
@@ -486,7 +520,11 @@ static void draw_canvas() {
|
||||
n.domain.empty() ? "?" : n.domain.front().c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Pass 1: issues. Pass 2: flows on top.
|
||||
for (int i = 0; i < (int)g_scan.nodes.size(); ++i) draw_one(i, false);
|
||||
for (int i = 0; i < (int)g_scan.nodes.size(); ++i) draw_one(i, true);
|
||||
|
||||
// Click: select.
|
||||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && g_hover >= 0) {
|
||||
@@ -496,7 +534,7 @@ static void draw_canvas() {
|
||||
// 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;
|
||||
const float r = kRingRadii.back() - 12.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,
|
||||
@@ -534,7 +572,7 @@ static void draw_tree() {
|
||||
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; }
|
||||
if (ImGui::SmallButton("Fit view")) { g_fit_pending = true; }
|
||||
|
||||
ImGui::Separator();
|
||||
draw_canvas();
|
||||
|
||||
Reference in New Issue
Block a user