diff --git a/main.cpp b/main.cpp index c087b78..1b20b00 100644 --- a/main.cpp +++ b/main.cpp @@ -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 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& nodes) { @@ -255,8 +259,8 @@ static void apply_layout(std::vector& 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();