1use 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
17const 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
24const PUSH_Y: f32 = 0.35;
26const PULL_Y: f32 = 0.65;
27
28const MIN_COMPONENT_SPACING: f32 = 40.0;
30
31pub 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 let col_positions = compute_column_positions(&columns.column_widths, width);
44
45 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 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(), 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
86fn compute_column_positions(widths: &[f32], total_width: f32) -> Vec<(f32, f32)> {
92 let margin = total_width * 0.05; 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
106fn 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 (center_x, signal_y, 0)
152 } else if graph.connects_to_vcc(member_id) {
153 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_offset += MIN_COMPONENT_SPACING;
163 (center_x, bias_y + bias_offset - MIN_COMPONENT_SPACING, 90)
164 } else {
165 (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 (center_x, signal_y, 0)
209 } else if comp.kind.is_diode_family() {
210 (center_x, signal_y - MIN_COMPONENT_SPACING, 0)
212 } else {
213 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 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 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
403fn 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 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 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 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 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}