Skip to content

Commit 29e610b

Browse files
committed
Almost there
1 parent 8b7461d commit 29e610b

2 files changed

Lines changed: 155 additions & 22 deletions

File tree

lib/minigun/hud/flow_diagram.rb

Lines changed: 142 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'set'
4+
35
module Minigun
46
module HUD
57
# Renders pipeline DAG as animated ASCII flow diagram with boxes and connections
@@ -275,29 +277,65 @@ def render_connections(terminal, layout, stages, dag, x_offset, y_offset)
275277

276278
edges = bridged_edges.uniq
277279

278-
# Group edges by source
280+
# Group edges by source and target to detect fan-out and fan-in
279281
edges_by_source = edges.group_by { |e| e[:from] }
282+
edges_by_target = edges.group_by { |e| e[:to] }
283+
284+
# Track which edges have been rendered
285+
rendered_edges = Set.new
280286

281-
# Render each source's connections
287+
# First pass: Render fan-out connections (one source to multiple targets)
282288
edges_by_source.each do |from_name, from_edges|
289+
next if from_edges.size <= 1 # Skip single connections for now
290+
283291
from_pos = layout[from_name]
284292
next unless from_pos
285293

286294
stage_data = stage_map[from_name]
287295
next unless stage_data
288296

289-
# Get target positions
290297
target_names = from_edges.map { |e| e[:to] }
291298
target_positions = target_names.map { |name| layout[name] }.compact
292-
293299
next if target_positions.empty?
294300

295-
# Draw fan-out connection or single connection
296-
if target_positions.size > 1
297-
render_fanout_connection(terminal, from_pos, target_positions, stage_data, x_offset, y_offset)
298-
else
299-
render_connection_line(terminal, from_pos, target_positions.first, stage_data, x_offset, y_offset)
300-
end
301+
render_fanout_connection(terminal, from_pos, target_positions, stage_data, x_offset, y_offset)
302+
from_edges.each { |e| rendered_edges.add(e) }
303+
end
304+
305+
# Second pass: Render fan-in connections (multiple sources to one target)
306+
edges_by_target.each do |to_name, to_edges|
307+
next if to_edges.size <= 1 # Skip single connections for now
308+
309+
to_pos = layout[to_name]
310+
next unless to_pos
311+
312+
source_names = to_edges.map { |e| e[:from] }
313+
source_positions = source_names.zip(to_edges).map do |name, edge|
314+
next if rendered_edges.include?(edge)
315+
layout[name]
316+
end.compact
317+
next if source_positions.empty?
318+
319+
# Get stage data from first source for color
320+
first_source = to_edges.first[:from]
321+
stage_data = stage_map[first_source] || {}
322+
323+
render_fanin_connection(terminal, source_positions, to_pos, stage_data, x_offset, y_offset)
324+
to_edges.each { |e| rendered_edges.add(e) }
325+
end
326+
327+
# Third pass: Render remaining single connections
328+
edges.each do |edge|
329+
next if rendered_edges.include?(edge)
330+
331+
from_pos = layout[edge[:from]]
332+
to_pos = layout[edge[:to]]
333+
next unless from_pos && to_pos
334+
335+
stage_data = stage_map[edge[:from]]
336+
next unless stage_data
337+
338+
render_connection_line(terminal, from_pos, to_pos, stage_data, x_offset, y_offset)
301339
end
302340
end
303341

@@ -322,10 +360,12 @@ def render_fanout_connection(terminal, from_pos, target_positions, stage_data, x
322360
leftmost_x = target_xs.first
323361
rightmost_x = target_xs.last
324362

363+
# Check if there's a target directly below the source
364+
has_center_target = target_xs.include?(from_x)
365+
325366
# Draw horizontal spine with junctions
326-
# Pattern: ┌───────────────┼───────────────┐
327-
# │ │ │
328-
# Where ┌ = left corner, ┼ = source (4-way junction), ┐ = right corner
367+
# Pattern with center target: ┌───────────────┼───────────────┐
368+
# Pattern without center: ┌───────────────┴───────────────┐
329369
(leftmost_x..rightmost_x).each do |x|
330370
next if x < 0 || x >= @width
331371

@@ -337,8 +377,8 @@ def render_fanout_connection(terminal, from_pos, target_positions, stage_data, x
337377
# Right corner
338378
"┐"
339379
elsif x == from_x
340-
# Source position uses ┼ (4-way junction)
341-
"┼"
380+
# Source position: ┼ if target below, ┴ if not
381+
has_center_target ? "┼" : "┴"
342382
else
343383
# Regular horizontal line (spine)
344384
if active
@@ -373,6 +413,93 @@ def render_fanout_connection(terminal, from_pos, target_positions, stage_data, x
373413
end
374414
end
375415

416+
# Draw a fan-in connection (multiple sources to one target)
417+
def render_fanin_connection(terminal, source_positions, to_pos, stage_data, x_offset, y_offset)
418+
to_x = to_pos[:x] + to_pos[:width] / 2
419+
to_y = to_pos[:y]
420+
421+
# Check if connection is active
422+
active = stage_data[:throughput] && stage_data[:throughput] > 0
423+
color = active ? Theme.primary : Theme.muted
424+
425+
# Calculate merge point (where horizontal lines converge)
426+
# Place it 1 line above the target
427+
merge_y = to_y - 1
428+
429+
# Get source X positions (sorted)
430+
source_data = source_positions.map do |pos|
431+
{
432+
x: pos[:x] + pos[:width] / 2,
433+
y: pos[:y] + pos[:height]
434+
}
435+
end.sort_by { |s| s[:x] }
436+
437+
# Draw vertical lines from each source down to merge level
438+
# Then turn inward with corners
439+
source_data.each do |source|
440+
# Vertical line from source to turn point
441+
(source[:y]...merge_y).each do |y|
442+
next if y < 0 || y >= @height
443+
444+
char = if active
445+
offset = (@animation_frame / 4) % Theme::FLOW_CHARS.length
446+
phase = (y - source[:y] + offset) % Theme::FLOW_CHARS.length
447+
Theme::FLOW_CHARS[phase]
448+
else
449+
"│"
450+
end
451+
452+
terminal.write_at(x_offset + source[:x], y_offset + y, char, color: color)
453+
end
454+
455+
# Corner at turn point
456+
if source[:x] < to_x
457+
# Left source: turn right with └
458+
terminal.write_at(x_offset + source[:x], y_offset + merge_y, "└", color: color)
459+
460+
# Horizontal line from corner to center (or near target)
461+
((source[:x] + 1)...to_x).each do |x|
462+
next if x < 0 || x >= @width
463+
464+
char = if active
465+
offset = (@animation_frame / 4) % 4
466+
["─", "╌", "┄", "┈"][offset]
467+
else
468+
"─"
469+
end
470+
471+
terminal.write_at(x_offset + x, y_offset + merge_y, char, color: color)
472+
end
473+
elsif source[:x] > to_x
474+
# Right source: turn left with ┘
475+
terminal.write_at(x_offset + source[:x], y_offset + merge_y, "┘", color: color)
476+
477+
# Horizontal line from corner to center (or near target)
478+
((to_x + 1)...source[:x]).each do |x|
479+
next if x < 0 || x >= @width
480+
481+
char = if active
482+
offset = (@animation_frame / 4) % 4
483+
["─", "╌", "┄", "┈"][offset]
484+
else
485+
"─"
486+
end
487+
488+
terminal.write_at(x_offset + x, y_offset + merge_y, char, color: color)
489+
end
490+
else
491+
# Source directly above target - just draw vertical line
492+
# (already drawn above)
493+
end
494+
end
495+
496+
# Draw final vertical line from merge point to target
497+
# (Only if not already covered by a center source)
498+
unless source_data.any? { |s| s[:x] == to_x }
499+
terminal.write_at(x_offset + to_x, y_offset + merge_y, "│", color: color)
500+
end
501+
end
502+
376503
# Draw animated connection line between two boxes
377504
def render_connection_line(terminal, from_pos, to_pos, stage_data, x_offset, y_offset)
378505
# Connection from bottom center of from_box to top center of to_box

spec/hud/diagrams/flow_diagram_rendering_spec.rb

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,17 @@ def normalize_output(buffer)
7979
│ ▶ generate │
8080
└────────────┘
8181
82+
8283
┌────────────┐
8384
│ ◀ double │
8485
└────────────┘
8586
87+
8688
┌────────────┐
8789
│ ◀ add_ten │
8890
└────────────┘
8991
92+
9093
┌────────────┐
9194
│ ◀ collect │
9295
└────────────┘
@@ -131,11 +134,13 @@ def normalize_output(buffer)
131134
┌────────────┐
132135
│ ▶ source │
133136
└────────────┘
137+
134138
┌───────┴───────┐
135139
┌────────────┐ ┌────────────┐
136140
│ ◀ path_b │ │ ◀ path_a │
137141
└────────────┘ └────────────┘
138-
└────┐ ┌─────┘
142+
│ │
143+
└────┐ ┌────┘
139144
┌────────────┐
140145
│ ◀ merge │
141146
└────────────┘
@@ -182,7 +187,6 @@ def normalize_output(buffer)
182187
└────────────┘
183188
184189
┌───────────────┼───────────────┐
185-
│ │ │
186190
┌────────────┐ ┌────────────┐ ┌────────────┐
187191
│ ◀ push │ │ ◀ sms │ │ ◀ email │
188192
└────────────┘ └────────────┘ └────────────┘
@@ -235,11 +239,13 @@ def normalize_output(buffer)
235239
┌────────────┐ ┌────────────┐ ┌────────────┐
236240
│ ◀ slow │ │ ◀ process │ │ ◀ fast │
237241
└────────────┘ └────────────┘ └────────────┘
238-
└───────────┐ │ ┌───────────┘
239-
┌────────────┐
240-
│ ◀ process2 │
241-
└────────────┘
242-
242+
│ │ │
243+
│ │ │
244+
│ ┌────────────┐ │
245+
│ │ ◀ process2 │ │
246+
│ └────────────┘ │
247+
│ │ │
248+
└────────────┐ │ ┌────────────┘
243249
┌────────────┐
244250
│ ◀ final │
245251
└────────────┘

0 commit comments

Comments
 (0)