Skip to main content

pedalkernel_layout/
placement.rs

1//! Phase 4: Vertical placement within columns.
2//!
3//! Places components within each column following electrical convention:
4//! - VCC rail at top (5% from top)
5//! - Supply-connected components (plate loads) extend upward
6//! - Signal path on the horizontal center line (50%)
7//! - Ground-connected components (cathode bias) extend downward
8//! - GND rail at bottom (5% from bottom)
9
10use crate::graph::LayoutGraph;
11use crate::groups::{FunctionalGroup, GroupKind};
12use crate::layering::ColumnAssignment;
13use crate::symbols::symbol_for_kind;
14use crate::types::*;
15use pedalkernel::compiler::Component;
16
17/// Vertical position constants (as fraction of total height).
18const VCC_RAIL_Y: f32 = 0.05;
19const SUPPLY_COMPONENT_Y: f32 = 0.20;
20const SIGNAL_PATH_Y: f32 = 0.50;
21const BIAS_COMPONENT_Y: f32 = 0.75;
22const GND_RAIL_Y: f32 = 0.95;
23
24/// For push-pull / phase inverter: split center line.
25const PUSH_Y: f32 = 0.35;
26const PULL_Y: f32 = 0.65;
27
28/// Minimum spacing between components (in layout units).
29const MIN_COMPONENT_SPACING: f32 = 40.0;
30
31/// Place all components and produce the initial layout.
32pub fn place_components(
33    graph: &LayoutGraph,
34    groups: &[FunctionalGroup],
35    columns: &ColumnAssignment,
36    width: f32,
37    height: f32,
38) -> Layout {
39    let mut placed = Vec::new();
40    let mut layout_groups = Vec::new();
41
42    // Compute column x-positions from widths
43    let col_positions = compute_column_positions(&columns.column_widths, width);
44
45    // Place each group
46    for group in groups {
47        let col = columns.group_columns.get(&group.name).copied().unwrap_or(0);
48        let (col_x, col_w) = col_positions[col];
49        let col_center_x = col_x + col_w / 2.0;
50
51        let group_components = place_group(graph, group, col_center_x, col_w, height);
52        let group_bounds = compute_group_bounds(&group_components, col_x, col_w, height);
53
54        layout_groups.push(Group {
55            name: group.name.clone(),
56            label: group.label.clone(),
57            bounds: group_bounds,
58        });
59
60        placed.extend(group_components);
61    }
62
63    // Build signal path order
64    let signal_path_order = compute_signal_path_order(&placed, graph);
65
66    Layout {
67        version: 1,
68        bounds: Bounds { width, height },
69        components: placed,
70        wires: Vec::new(), // Filled in Phase 5
71        groups: layout_groups,
72        supply_rails: vec![
73            SupplyRail {
74                name: "vcc".into(),
75                y: VCC_RAIL_Y * height,
76            },
77            SupplyRail {
78                name: "gnd".into(),
79                y: GND_RAIL_Y * height,
80            },
81        ],
82        signal_path_order,
83    }
84}
85
86// ---------------------------------------------------------------------------
87// Column position computation
88// ---------------------------------------------------------------------------
89
90/// Returns (x_start, width) for each column.
91fn compute_column_positions(widths: &[f32], total_width: f32) -> Vec<(f32, f32)> {
92    let margin = total_width * 0.05; // 5% margin on each side
93    let usable = total_width - 2.0 * margin;
94    let mut positions = Vec::with_capacity(widths.len());
95    let mut x = margin;
96
97    for &w in widths {
98        let col_w = usable * w;
99        positions.push((x, col_w));
100        x += col_w;
101    }
102
103    positions
104}
105
106// ---------------------------------------------------------------------------
107// Group placement
108// ---------------------------------------------------------------------------
109
110fn place_group(
111    graph: &LayoutGraph,
112    group: &FunctionalGroup,
113    center_x: f32,
114    col_width: f32,
115    height: f32,
116) -> Vec<PlacedComponent> {
117    match group.kind {
118        GroupKind::GainStage => place_gain_stage(graph, group, center_x, col_width, height),
119        GroupKind::OpAmpStage => place_opamp_stage(graph, group, center_x, col_width, height),
120        GroupKind::ToneStack => place_tone_stack(graph, group, center_x, col_width, height),
121        GroupKind::PushPullOutput => place_push_pull(graph, group, center_x, col_width, height),
122        GroupKind::PhaseInverter => place_phase_inverter(graph, group, center_x, col_width, height),
123        _ => place_generic(graph, group, center_x, col_width, height),
124    }
125}
126
127fn place_gain_stage(
128    graph: &LayoutGraph,
129    group: &FunctionalGroup,
130    center_x: f32,
131    _col_width: f32,
132    height: f32,
133) -> Vec<PlacedComponent> {
134    let mut placed = Vec::new();
135    let signal_y = SIGNAL_PATH_Y * height;
136    let supply_y = SUPPLY_COMPONENT_Y * height;
137    let bias_y = BIAS_COMPONENT_Y * height;
138
139    let mut supply_offset = 0.0f32;
140    let mut bias_offset = 0.0f32;
141
142    for &member_id in &group.members {
143        let node = &graph.nodes[member_id];
144        let comp = match &node.comp {
145            Some(c) => c,
146            None => continue,
147        };
148
149        let (x, y, orientation) = if Some(member_id) == group.primary_device {
150            // Active device at center of signal path
151            (center_x, signal_y, 0)
152        } else if graph.connects_to_vcc(member_id) {
153            // Supply component above
154            supply_offset += MIN_COMPONENT_SPACING;
155            (
156                center_x,
157                supply_y + supply_offset - MIN_COMPONENT_SPACING,
158                90,
159            )
160        } else if graph.connects_to_gnd(member_id) {
161            // Bias component below
162            bias_offset += MIN_COMPONENT_SPACING;
163            (center_x, bias_y + bias_offset - MIN_COMPONENT_SPACING, 90)
164        } else {
165            // Coupling component on signal path (offset left)
166            (center_x - MIN_COMPONENT_SPACING, signal_y, 0)
167        };
168
169        let monitor_index = graph.monitor_ids.iter().position(|id| *id == node.comp_id);
170
171        placed.push(PlacedComponent {
172            name: node.comp_id.clone(),
173            kind: kind_to_string(comp.kind.as_ref()),
174            x,
175            y,
176            orientation,
177            symbol: symbol_for_kind(comp.kind.as_ref()),
178            group: group.name.clone(),
179            label: value_label(comp.kind.as_ref()),
180            monitor_index,
181        });
182    }
183
184    placed
185}
186
187fn place_opamp_stage(
188    graph: &LayoutGraph,
189    group: &FunctionalGroup,
190    center_x: f32,
191    col_width: f32,
192    height: f32,
193) -> Vec<PlacedComponent> {
194    let mut placed = Vec::new();
195    let signal_y = SIGNAL_PATH_Y * height;
196
197    let mut offset_x = -col_width * 0.3;
198
199    for &member_id in &group.members {
200        let node = &graph.nodes[member_id];
201        let comp = match &node.comp {
202            Some(c) => c,
203            None => continue,
204        };
205
206        let (x, y, orientation) = if Some(member_id) == group.primary_device {
207            // Op-amp at center
208            (center_x, signal_y, 0)
209        } else if comp.kind.is_diode_family() {
210            // Clipping diodes above signal path
211            (center_x, signal_y - MIN_COMPONENT_SPACING, 0)
212        } else {
213            // Feedback components arranged around the op-amp
214            offset_x += MIN_COMPONENT_SPACING;
215            (
216                center_x + offset_x,
217                signal_y - MIN_COMPONENT_SPACING * 0.5,
218                0,
219            )
220        };
221
222        let monitor_index = graph.monitor_ids.iter().position(|id| *id == node.comp_id);
223
224        placed.push(PlacedComponent {
225            name: node.comp_id.clone(),
226            kind: kind_to_string(comp.kind.as_ref()),
227            x,
228            y,
229            orientation,
230            symbol: symbol_for_kind(comp.kind.as_ref()),
231            group: group.name.clone(),
232            label: value_label(comp.kind.as_ref()),
233            monitor_index,
234        });
235    }
236
237    placed
238}
239
240fn place_tone_stack(
241    graph: &LayoutGraph,
242    group: &FunctionalGroup,
243    center_x: f32,
244    col_width: f32,
245    height: f32,
246) -> Vec<PlacedComponent> {
247    let mut placed = Vec::new();
248    let signal_y = SIGNAL_PATH_Y * height;
249
250    let count = group.members.len() as f32;
251    let spacing = ((col_width * 0.7) / count.max(1.0)).max(MIN_COMPONENT_SPACING);
252
253    for (i, &member_id) in group.members.iter().enumerate() {
254        let node = &graph.nodes[member_id];
255        let comp = match &node.comp {
256            Some(c) => c,
257            None => continue,
258        };
259
260        let is_pot = comp.kind.is_pot();
261        let x = center_x + (i as f32 - count / 2.0) * spacing;
262        let y = if is_pot {
263            signal_y
264        } else if graph.connects_to_gnd(member_id) {
265            signal_y + MIN_COMPONENT_SPACING
266        } else {
267            signal_y - MIN_COMPONENT_SPACING * 0.5
268        };
269
270        let monitor_index = graph.monitor_ids.iter().position(|id| *id == node.comp_id);
271
272        placed.push(PlacedComponent {
273            name: node.comp_id.clone(),
274            kind: kind_to_string(comp.kind.as_ref()),
275            x,
276            y,
277            orientation: if is_pot { 0 } else { 90 },
278            symbol: symbol_for_kind(comp.kind.as_ref()),
279            group: group.name.clone(),
280            label: value_label(comp.kind.as_ref()),
281            monitor_index,
282        });
283    }
284
285    placed
286}
287
288fn place_push_pull(
289    graph: &LayoutGraph,
290    group: &FunctionalGroup,
291    center_x: f32,
292    _col_width: f32,
293    height: f32,
294) -> Vec<PlacedComponent> {
295    let mut placed = Vec::new();
296    let push_y = PUSH_Y * height;
297    let pull_y = PULL_Y * height;
298    let signal_y = SIGNAL_PATH_Y * height;
299
300    let mut pentode_count = 0;
301
302    for &member_id in &group.members {
303        let node = &graph.nodes[member_id];
304        let comp = match &node.comp {
305            Some(c) => c,
306            None => continue,
307        };
308
309        let (x, y, orientation) = if comp.kind.layout_class() == "pentode" {
310            pentode_count += 1;
311            if pentode_count % 2 == 1 {
312                (center_x, push_y, 0)
313            } else {
314                (center_x, pull_y, 0)
315            }
316        } else if comp.kind.is_transformer() {
317            (center_x + MIN_COMPONENT_SPACING * 2.0, signal_y, 0)
318        } else {
319            // Bias components
320            let y = if graph.connects_to_vcc(member_id) {
321                SUPPLY_COMPONENT_Y * height
322            } else {
323                BIAS_COMPONENT_Y * height
324            };
325            (center_x, y, 90)
326        };
327
328        let monitor_index = graph.monitor_ids.iter().position(|id| *id == node.comp_id);
329
330        placed.push(PlacedComponent {
331            name: node.comp_id.clone(),
332            kind: kind_to_string(comp.kind.as_ref()),
333            x,
334            y,
335            orientation,
336            symbol: symbol_for_kind(comp.kind.as_ref()),
337            group: group.name.clone(),
338            label: value_label(comp.kind.as_ref()),
339            monitor_index,
340        });
341    }
342
343    placed
344}
345
346fn place_phase_inverter(
347    graph: &LayoutGraph,
348    group: &FunctionalGroup,
349    center_x: f32,
350    _col_width: f32,
351    height: f32,
352) -> Vec<PlacedComponent> {
353    // Phase inverter is similar to push-pull but with triodes
354    place_push_pull(graph, group, center_x, _col_width, height)
355}
356
357fn place_generic(
358    graph: &LayoutGraph,
359    group: &FunctionalGroup,
360    center_x: f32,
361    col_width: f32,
362    height: f32,
363) -> Vec<PlacedComponent> {
364    let mut placed = Vec::new();
365    let signal_y = SIGNAL_PATH_Y * height;
366    let count = group.members.len() as f32;
367    let spacing = ((col_width * 0.6) / count.max(1.0)).max(MIN_COMPONENT_SPACING);
368
369    for (i, &member_id) in group.members.iter().enumerate() {
370        let node = &graph.nodes[member_id];
371        let comp = match &node.comp {
372            Some(c) => c,
373            None => continue,
374        };
375
376        let x = center_x + (i as f32 - count / 2.0) * spacing;
377        let y = if graph.connects_to_vcc(member_id) {
378            SUPPLY_COMPONENT_Y * height
379        } else if graph.connects_to_gnd(member_id) {
380            BIAS_COMPONENT_Y * height
381        } else {
382            signal_y
383        };
384
385        let monitor_index = graph.monitor_ids.iter().position(|id| *id == node.comp_id);
386
387        placed.push(PlacedComponent {
388            name: node.comp_id.clone(),
389            kind: kind_to_string(comp.kind.as_ref()),
390            x,
391            y,
392            orientation: 0,
393            symbol: symbol_for_kind(comp.kind.as_ref()),
394            group: group.name.clone(),
395            label: value_label(comp.kind.as_ref()),
396            monitor_index,
397        });
398    }
399
400    placed
401}
402
403// ---------------------------------------------------------------------------
404// Helpers
405// ---------------------------------------------------------------------------
406
407fn compute_group_bounds(
408    components: &[PlacedComponent],
409    col_x: f32,
410    col_w: f32,
411    height: f32,
412) -> GroupBounds {
413    if components.is_empty() {
414        return GroupBounds {
415            x: col_x,
416            y: 0.0,
417            w: col_w,
418            h: height,
419        };
420    }
421
422    let min_x = components.iter().map(|c| c.x).fold(f32::INFINITY, f32::min);
423    let max_x = components
424        .iter()
425        .map(|c| c.x)
426        .fold(f32::NEG_INFINITY, f32::max);
427    let min_y = components.iter().map(|c| c.y).fold(f32::INFINITY, f32::min);
428    let max_y = components
429        .iter()
430        .map(|c| c.y)
431        .fold(f32::NEG_INFINITY, f32::max);
432
433    let padding = MIN_COMPONENT_SPACING * 0.5;
434    GroupBounds {
435        x: min_x - padding,
436        y: min_y - padding,
437        w: (max_x - min_x) + 2.0 * padding,
438        h: (max_y - min_y) + 2.0 * padding,
439    }
440}
441
442fn compute_signal_path_order(placed: &[PlacedComponent], graph: &LayoutGraph) -> Vec<usize> {
443    // BFS from graph.in_node following non-feedback, non-supply edges
444    // to determine signal-flow ordering.
445    let mut visited = std::collections::BTreeSet::new();
446    let mut queue = std::collections::VecDeque::new();
447    let mut bfs_order: Vec<String> = Vec::new();
448
449    visited.insert(graph.in_node);
450    queue.push_back(graph.in_node);
451
452    while let Some(node_id) = queue.pop_front() {
453        let comp_id = &graph.nodes[node_id].comp_id;
454        if !graph.nodes[node_id].is_anchor {
455            bfs_order.push(comp_id.clone());
456        }
457
458        // Follow outgoing non-feedback, non-supply edges
459        for edge in &graph.edges {
460            let neighbor = if edge.from == node_id {
461                edge.to
462            } else if edge.to == node_id {
463                edge.from
464            } else {
465                continue;
466            };
467
468            if edge.is_feedback || edge.is_supply {
469                continue;
470            }
471            if visited.contains(&neighbor) {
472                continue;
473            }
474            visited.insert(neighbor);
475            queue.push_back(neighbor);
476        }
477    }
478
479    // Map BFS comp_id order → placed component indices
480    let name_to_placed: std::collections::BTreeMap<&str, usize> = placed
481        .iter()
482        .enumerate()
483        .map(|(i, c)| (c.name.as_str(), i))
484        .collect();
485
486    let mut ordered: Vec<usize> = Vec::new();
487    let mut used = std::collections::BTreeSet::new();
488
489    for comp_id in &bfs_order {
490        if let Some(&idx) = name_to_placed.get(comp_id.as_str()) {
491            if used.insert(idx) {
492                ordered.push(idx);
493            }
494        }
495    }
496
497    // Append any unreached components at the end (sorted by x as fallback)
498    let mut remaining: Vec<usize> = (0..placed.len()).filter(|i| !used.contains(i)).collect();
499    remaining.sort_by(|&a, &b| {
500        placed[a]
501            .x
502            .partial_cmp(&placed[b].x)
503            .unwrap_or(std::cmp::Ordering::Equal)
504    });
505    ordered.extend(remaining);
506
507    ordered
508}
509
510fn kind_to_string(kind: &dyn Component) -> String {
511    kind.layout_class().to_string()
512}
513
514fn value_label(kind: &dyn Component) -> Option<String> {
515    kind.display_value()
516}