1use crate::circuit::Circuit;
47use crate::visualization::VisualizationError;
48use crate::visualization::circuit::ir::{
49 VisualCircuit, VisualControlFlowKind, VisualOpStyle, VisualOperation,
50 flatten_control_flow_visual, reverse_visual_lanes,
51};
52use crate::visualization::circuit::layout::{VisualBuildOptions, build_visual_circuit};
53use crate::visualization::circuit::params::ParameterFormatOptions;
54use crate::visualization::circuit::style::{GateStyle, StyleBook};
55use crate::visualization::svg::render_svg_to_file;
56use std::collections::HashMap;
57
58#[derive(Debug, Clone, Copy)]
59struct FigurePalette {
60 wire_color: &'static str,
61 connector_color: &'static str,
62 text_color: &'static str,
63 gate_edge_color: &'static str,
64 gate_fill_color: Option<&'static str>,
65 barrier_color: &'static str,
66 wire_linewidth: f64,
67 connector_linewidth: f64,
68 gate_linewidth: f64,
69 gate_fontsize: u8,
70}
71
72const MIN_ROW_LOGICAL_WIDTH: f64 = 2.0;
74const WIRE_PITCH: f64 = 2.0;
76const CANVAS_MIN_X: f64 = -1.4;
78const CANVAS_RIGHT_PADDING: f64 = 0.3;
80const CANVAS_Y_PADDING: f64 = 1.0;
82const LOGICAL_UNIT_TO_PX: f64 = 80.0;
84const LABEL_INNER_PADDING_PX: f64 = 10.0;
86const LABEL_MIN_INNER_PX: f64 = 4.0;
88const PARAM_FONT_SCALE: f64 = 0.78;
90const LABEL_LINE_GAP_SCALE: f64 = 0.22;
92const SUBSCRIPT_FONT_SCALE: f64 = 0.70;
94const LABEL_FIT_MIN_SCALE: f64 = 0.72;
96const FOLD_TARGET_SLACK: f64 = 1.12;
98const CANVAS_BACKGROUND_COLOR: &str = "#ffffff";
100const DEFAULT_FIGURE_DPI: u32 = 160;
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum FigureDrawStyle {
106 Cqlib,
108}
109
110#[derive(Debug, Clone)]
115pub struct FigureDrawerOptions {
116 pub show_params: bool,
118 pub decompose_circuit_gates: bool,
120 pub parameter_format: ParameterFormatOptions,
122 pub width_per_column: f64,
124 pub height_per_qubit: f64,
126 pub dpi: u32,
131 pub gate_width: f64,
133 pub gate_height: f64,
135 pub moment_spacing: f64,
137 pub connect_height: f64,
139 pub fold: i32,
141 pub style: FigureDrawStyle,
143 pub gate_styles: HashMap<String, GateStyle>,
145 pub initial_state: bool,
147 pub reverse_bits: bool,
149}
150
151impl FigureDrawerOptions {
152 fn png_scale(&self) -> Result<f64, VisualizationError> {
157 if self.dpi == 0 {
158 return Err(VisualizationError::InvalidInput(
159 "figure dpi must be greater than zero".to_string(),
160 ));
161 }
162 Ok(self.dpi as f64 / DEFAULT_FIGURE_DPI as f64)
163 }
164}
165
166impl Default for FigureDrawerOptions {
167 fn default() -> Self {
168 Self {
169 show_params: true,
170 decompose_circuit_gates: false,
171 parameter_format: ParameterFormatOptions::default(),
172 width_per_column: 1.2,
173 height_per_qubit: 0.9,
174 dpi: DEFAULT_FIGURE_DPI,
175 gate_width: 1.1,
176 gate_height: 1.5,
177 moment_spacing: 0.3,
178 connect_height: 2.0,
179 fold: 18,
180 style: FigureDrawStyle::Cqlib,
181 gate_styles: HashMap::new(),
182 initial_state: false,
183 reverse_bits: false,
184 }
185 }
186}
187
188fn figure_palette(style: FigureDrawStyle) -> FigurePalette {
189 match style {
190 FigureDrawStyle::Cqlib => FigurePalette {
191 wire_color: "black",
192 connector_color: "black",
193 text_color: "black",
194 gate_edge_color: "black",
195 gate_fill_color: Some("white"),
196 barrier_color: "gray",
197 wire_linewidth: 1.1,
198 connector_linewidth: 1.0,
199 gate_linewidth: 1.1,
200 gate_fontsize: 9,
201 },
202 }
203}
204
205pub fn circuit_to_figure(
233 circuit: &Circuit,
234 options: &FigureDrawerOptions,
235) -> Result<String, VisualizationError> {
236 let visual_options = VisualBuildOptions {
237 decompose_circuit_gates: options.decompose_circuit_gates,
238 parameter_format: options.parameter_format,
239 ..VisualBuildOptions::default()
240 };
241 let visual = build_visual_circuit(circuit, &visual_options)?;
242 Ok(draw_figure_svg_from_visual(&visual, options))
243}
244
245pub fn render_figure_to_file(
269 circuit: &Circuit,
270 output_path: &str,
271 options: &FigureDrawerOptions,
272) -> Result<(), VisualizationError> {
273 let visual_options = VisualBuildOptions {
274 decompose_circuit_gates: options.decompose_circuit_gates,
275 parameter_format: options.parameter_format,
276 ..VisualBuildOptions::default()
277 };
278 let visual = build_visual_circuit(circuit, &visual_options)?;
279 let svg = draw_figure_svg_from_visual(&visual, options);
280 render_svg_to_file(&svg, output_path, options.png_scale()?)
281}
282
283fn draw_figure_svg_from_visual(visual: &VisualCircuit, options: &FigureDrawerOptions) -> String {
284 let mut visual_data = flatten_control_flow_visual(visual);
285 if options.reverse_bits {
286 visual_data = reverse_visual_lanes(visual_data);
287 }
288
289 let num_qubits = visual_data.num_qubits();
290 let num_columns = visual_data.num_columns.max(1);
291 let palette = figure_palette(options.style);
292 let style_book = StyleBook::new("default", &options.gate_styles);
293 let sx = LOGICAL_UNIT_TO_PX * options.width_per_column;
294 let sy = LOGICAL_UNIT_TO_PX * options.height_per_qubit;
295 let global_text_fs = style_book
296 .get("default")
297 .font_size
298 .unwrap_or(palette.gate_fontsize as f64)
299 .clamp(8.0, 48.0);
300
301 let mut cols_ops: Vec<Vec<&_>> = vec![Vec::new(); num_columns];
302 for op in &visual_data.operations {
303 if op.column < num_columns {
304 cols_ops[op.column].push(op);
305 }
306 }
307 let mut col_widths = vec![options.gate_width; num_columns];
310 for col in 0..num_columns {
311 for op in &cols_ops[col] {
312 let gate_style = style_book.get(op_style_key(op));
313 let gate_text_fs = gate_font_size(gate_style, global_text_fs);
314 col_widths[col] = col_widths[col].max(gate_label_required_width(
315 op,
316 options.show_params,
317 gate_text_fs,
318 sx,
319 options.gate_width,
320 ));
321 }
322 }
323
324 let row_columns = split_columns_by_fold(
325 &col_widths,
326 options.fold,
327 options.moment_spacing,
328 options.gate_width,
329 );
330 let mut row_widths = Vec::with_capacity(row_columns.len());
331 for row in &row_columns {
332 if row.is_empty() {
333 row_widths.push(MIN_ROW_LOGICAL_WIDTH);
334 continue;
335 }
336 let mut width = options.moment_spacing;
337 for (i, col) in row.iter().enumerate() {
338 width += col_widths[*col];
339 if i + 1 < row.len() {
340 width += options.moment_spacing;
341 }
342 }
343 width += options.moment_spacing;
344 row_widths.push(width.max(MIN_ROW_LOGICAL_WIDTH));
345 }
346 let x_max = row_widths
347 .iter()
348 .fold(0.0f64, |acc, w| acc.max(*w))
349 .max(MIN_ROW_LOGICAL_WIDTH);
350 let qubits_height = if num_qubits == 0 {
351 WIRE_PITCH
352 } else {
353 (num_qubits as f64 - 1.0) * WIRE_PITCH
354 };
355 let total_height = if row_columns.is_empty() {
356 WIRE_PITCH
357 } else {
358 (row_columns.len() as f64 - 1.0) * (qubits_height + options.connect_height) + qubits_height
359 };
360
361 let min_x = CANVAS_MIN_X;
362 let max_x = x_max + CANVAS_RIGHT_PADDING;
363 let min_y = -CANVAS_Y_PADDING;
364 let max_y = total_height + CANVAS_Y_PADDING;
365 let canvas_w = ((max_x - min_x) * sx).max(1.0);
366 let canvas_h = ((max_y - min_y) * sy).max(1.0);
367 let px = |x: f64| (x - min_x) * sx;
368 let py = |y: f64| (y - min_y) * sy;
369
370 let mut elements = Vec::new();
371 elements.push(format!(
372 "<rect x=\"0\" y=\"0\" width=\"{:.3}\" height=\"{:.3}\" fill=\"{}\"/>",
373 canvas_w, canvas_h, CANVAS_BACKGROUND_COLOR
374 ));
375
376 let wire_color = style_book
377 .get("default")
378 .line_color
379 .as_deref()
380 .unwrap_or(palette.wire_color);
381 let default_text_color = style_book
382 .get("default")
383 .text_color
384 .as_deref()
385 .unwrap_or(palette.text_color);
386 for (row_idx, row_cols) in row_columns.iter().enumerate() {
387 let row_x_max = x_max;
389 let y_base = row_idx as f64 * (qubits_height + options.connect_height);
390
391 for (lane, qubit) in visual_data.qubits.iter().enumerate() {
392 let y = lane_to_y(lane, y_base);
393 let q_label = if options.initial_state {
394 format!("q{} |0>", qubit.id())
395 } else {
396 format!("q{}", qubit.id())
397 };
398 elements.push(svg_line(
399 px(0.0),
400 py(y),
401 px(row_x_max),
402 py(y),
403 wire_color,
404 palette.wire_linewidth,
405 None,
406 ));
407 elements.push(svg_text(
408 px(-0.08),
409 py(y),
410 &q_label,
411 global_text_fs,
412 default_text_color,
413 "end",
414 ));
415 }
416 if row_idx > 0 && num_qubits > 0 {
417 elements.push(svg_line(
418 px(0.0),
419 py(y_base),
420 px(0.0),
421 py(y_base + qubits_height),
422 wire_color,
423 palette.wire_linewidth * 1.3,
424 None,
425 ));
426 }
427 if row_idx + 1 < row_columns.len() && num_qubits > 0 {
428 elements.push(svg_line(
429 px(row_x_max),
430 py(y_base),
431 px(row_x_max),
432 py(y_base + qubits_height),
433 wire_color,
434 palette.wire_linewidth * 1.3,
435 None,
436 ));
437 }
438
439 let mut x = options.moment_spacing;
440 for (i, col) in row_cols.iter().enumerate() {
441 let col_w = col_widths[*col];
442 x += col_w / 2.0;
443 let x_center = x;
444
445 for op in &cols_ops[*col] {
446 let op_w = if uses_measured_label_width(op, options.show_params) {
449 col_w
450 } else {
451 options.gate_width
452 };
453 let label = compose_label(&op.label, &op.params, options.show_params);
454 let gate_style = style_book.get(op_style_key(op));
455 let gate_text_fs = gate_font_size(gate_style, global_text_fs);
456 let min_lane = op.covered_lanes.iter().copied().min();
457 let max_lane = op.covered_lanes.iter().copied().max();
458 let connector_color = gate_style
459 .line_color
460 .as_deref()
461 .unwrap_or(palette.connector_color);
462 let connector_lw = gate_style.line_width.unwrap_or(palette.connector_linewidth);
463
464 match &op.style {
465 VisualOpStyle::Gate => {
466 if op.label == "FSIM" && op.lanes.len() >= 2 {
467 if let (Some(min_l), Some(max_l)) = (
468 op.lanes.iter().copied().min(),
469 op.lanes.iter().copied().max(),
470 ) && max_l > min_l
471 {
472 let y0 = lane_to_y(min_l, y_base);
473 let y1 = lane_to_y(max_l, y_base);
474 elements.push(svg_line(
475 px(x_center),
476 py(y0.min(y1)),
477 px(x_center),
478 py(y0.max(y1)),
479 connector_color,
480 connector_lw,
481 None,
482 ));
483 }
484 let r = (options.gate_width * 0.35 * sx.min(sy)).clamp(14.0, 28.0);
485 let circle_face = normalized_fill_color(gate_style, &palette)
486 .unwrap_or_else(|| "white".to_string());
487 let circle_edge = normalized_edge_color(gate_style, &palette)
488 .unwrap_or_else(|| connector_color.to_string());
489 let text_color = gate_style
490 .text_color
491 .as_deref()
492 .unwrap_or(palette.text_color);
493 let fsim_font = (r * 0.62).clamp(7.0, (gate_text_fs * 0.9).max(7.0));
495 for lane in &op.lanes {
496 let y = lane_to_y(*lane, y_base);
497 elements.push(svg_circle(
498 px(x_center),
499 py(y),
500 r,
501 Some(&circle_face),
502 Some(&circle_edge),
503 connector_lw,
504 ));
505 elements.push(svg_text(
506 px(x_center),
507 py(y),
508 "FSIM",
509 fsim_font,
510 text_color,
511 "middle",
512 ));
513 }
514 continue;
515 }
516
517 if op.lanes.len() > 1 {
518 let gate_box_w = op_w;
519 let show_markers = show_span_lane_markers(op);
520 let marker_gutter = if show_markers {
521 (gate_box_w * 0.28).clamp(0.24, 0.42)
522 } else {
523 0.0
524 };
525 let start_lane = op.lanes.iter().copied().min().unwrap_or(0);
526 let end_lane = op.lanes.iter().copied().max().unwrap_or(start_lane);
527 let y0 = lane_to_y(start_lane, y_base);
528 let y1 = lane_to_y(end_lane, y_base);
529 let y_min = y0.min(y1) - options.gate_height / 2.0;
530 let box_h = (y0.max(y1) - y0.min(y1) + options.gate_height)
531 .max(options.gate_height);
532 elements.extend(draw_span_box_svg(
533 x_center,
535 y_min,
536 box_h,
537 &label,
538 &palette,
539 gate_style,
540 gate_text_fs,
541 gate_box_w,
542 sx,
543 sy,
544 &px,
545 &py,
546 ));
547 if show_markers {
548 let marker_x = x_center - gate_box_w / 2.0 + marker_gutter * 0.2;
550 let marker_font_size = gate_text_fs;
551 for (idx, lane) in op.lanes.iter().enumerate() {
552 let y = lane_to_y(*lane, y_base);
553 elements.push(svg_text(
554 px(marker_x),
555 py(y),
556 &idx.to_string(),
557 marker_font_size,
558 gate_style
559 .text_color
560 .as_deref()
561 .unwrap_or(palette.text_color),
562 "start",
563 ));
564 }
565 }
566 } else {
567 let anchor = op.lanes.iter().copied().min().unwrap_or(0);
568 let y = lane_to_y(anchor, y_base);
569 elements.extend(draw_box_svg(
570 x_center,
571 y,
572 &label,
573 &palette,
574 gate_style,
575 gate_text_fs,
576 op_w,
577 options.gate_height,
578 sx,
579 sy,
580 &px,
581 &py,
582 ));
583 }
584 }
585 VisualOpStyle::Controlled { num_controls } => {
586 if let (Some(min_l), Some(max_l)) = (
587 op.lanes.iter().copied().min(),
588 op.lanes.iter().copied().max(),
589 ) && max_l > min_l
590 {
591 let y0 = lane_to_y(min_l, y_base);
592 let y1 = lane_to_y(max_l, y_base);
593 elements.push(svg_line(
594 px(x_center),
595 py(y0.min(y1)),
596 px(x_center),
597 py(y0.max(y1)),
598 connector_color,
599 connector_lw,
600 None,
601 ));
602 }
603 for lane in op.lanes.iter().take(*num_controls) {
604 let y = lane_to_y(*lane, y_base);
605 elements.push(svg_circle(
606 px(x_center),
607 py(y),
608 (0.07 * sx.min(sy)).max(4.0),
609 Some(connector_color),
610 Some(connector_color),
611 connector_lw,
612 ));
613 }
614 for lane in op.lanes.iter().skip(*num_controls) {
615 let y = lane_to_y(*lane, y_base);
616 elements.extend(draw_box_svg(
617 x_center,
618 y,
619 &label,
620 &palette,
621 gate_style,
622 gate_text_fs,
623 op_w,
624 options.gate_height,
625 sx,
626 sy,
627 &px,
628 &py,
629 ));
630 }
631 }
632 VisualOpStyle::Cz => {
633 if let (Some(min_l), Some(max_l)) = (
634 op.lanes.iter().copied().min(),
635 op.lanes.iter().copied().max(),
636 ) && max_l > min_l
637 {
638 let y0 = lane_to_y(min_l, y_base);
639 let y1 = lane_to_y(max_l, y_base);
640 elements.push(svg_line(
641 px(x_center),
642 py(y0.min(y1)),
643 px(x_center),
644 py(y0.max(y1)),
645 connector_color,
646 connector_lw,
647 None,
648 ));
649 }
650 for lane in &op.lanes {
651 let y = lane_to_y(*lane, y_base);
652 elements.push(svg_circle(
653 px(x_center),
654 py(y),
655 (0.07 * sx.min(sy)).max(4.0),
656 Some(connector_color),
657 Some(connector_color),
658 connector_lw,
659 ));
660 }
661 }
662 VisualOpStyle::Swap => {
663 let swap_lw = connector_lw * 1.8;
664 if let (Some(min_l), Some(max_l)) = (
665 op.lanes.iter().copied().min(),
666 op.lanes.iter().copied().max(),
667 ) && max_l > min_l
668 {
669 let y0 = lane_to_y(min_l, y_base);
670 let y1 = lane_to_y(max_l, y_base);
671 elements.push(svg_line(
672 px(x_center),
673 py(y0.min(y1)),
674 px(x_center),
675 py(y0.max(y1)),
676 connector_color,
677 swap_lw,
678 None,
679 ));
680 }
681 for lane in &op.lanes {
682 let y = lane_to_y(*lane, y_base);
683 elements.push(svg_line(
684 px(x_center - 0.15),
685 py(y - 0.15),
686 px(x_center + 0.15),
687 py(y + 0.15),
688 connector_color,
689 swap_lw,
690 None,
691 ));
692 elements.push(svg_line(
693 px(x_center - 0.15),
694 py(y + 0.15),
695 px(x_center + 0.15),
696 py(y - 0.15),
697 connector_color,
698 swap_lw,
699 None,
700 ));
701 }
702 }
703 VisualOpStyle::Barrier => {
704 let barrier_lw =
705 gate_style.line_width.unwrap_or(palette.gate_linewidth) * 1.8;
706 let (start_lane, end_lane) =
707 if let (Some(min_l), Some(max_l)) = (min_lane, max_lane) {
708 (min_l, max_l)
709 } else if num_qubits > 0 {
710 (0, num_qubits - 1)
711 } else {
712 (0, 0)
713 };
714 let y0 = lane_to_y(start_lane, y_base);
715 let y1 = lane_to_y(end_lane, y_base);
716 elements.push(svg_line(
717 px(x_center),
718 py(y0.min(y1) - options.gate_height / 2.0),
719 px(x_center),
720 py(y0.max(y1) + options.gate_height / 2.0),
721 gate_style
722 .line_color
723 .as_deref()
724 .unwrap_or(palette.barrier_color),
725 barrier_lw,
726 Some("6,4"),
727 ));
728 }
729 VisualOpStyle::Measure => {
730 if op.lanes.is_empty() {
731 elements.extend(draw_measure_svg(
732 x_center,
733 lane_to_y(0, y_base),
734 &palette,
735 gate_style,
736 gate_text_fs,
737 op_w,
738 options.gate_height,
739 sx,
740 sy,
741 &px,
742 &py,
743 ));
744 } else {
745 for lane in &op.lanes {
746 elements.extend(draw_measure_svg(
747 x_center,
748 lane_to_y(*lane, y_base),
749 &palette,
750 gate_style,
751 gate_text_fs,
752 op_w,
753 options.gate_height,
754 sx,
755 sy,
756 &px,
757 &py,
758 ));
759 }
760 }
761 }
762 VisualOpStyle::Reset | VisualOpStyle::Delay => {
763 if op.lanes.is_empty() {
764 elements.extend(draw_box_svg(
765 x_center,
766 lane_to_y(0, y_base),
767 &label,
768 &palette,
769 gate_style,
770 gate_text_fs,
771 op_w,
772 options.gate_height,
773 sx,
774 sy,
775 &px,
776 &py,
777 ));
778 } else {
779 for lane in &op.lanes {
780 elements.extend(draw_box_svg(
781 x_center,
782 lane_to_y(*lane, y_base),
783 &label,
784 &palette,
785 gate_style,
786 gate_text_fs,
787 op_w,
788 options.gate_height,
789 sx,
790 sy,
791 &px,
792 &py,
793 ));
794 }
795 }
796 }
797 VisualOpStyle::ControlFlow { kind } => {
798 let start_lane = min_lane
799 .or_else(|| op.lanes.iter().copied().min())
800 .unwrap_or(0);
801 let end_lane = max_lane
802 .or_else(|| op.lanes.iter().copied().max())
803 .unwrap_or(start_lane);
804 let y0 = lane_to_y(start_lane, y_base);
805 let y1 = lane_to_y(end_lane, y_base);
806 let y_min = y0.min(y1) - options.gate_height / 2.0;
807 let box_h = (y0.max(y1) - y0.min(y1) + options.gate_height)
808 .max(options.gate_height);
809 elements.extend(draw_flow_box_svg(
810 x_center,
811 y_min,
812 box_h,
813 &label,
814 &palette,
815 gate_style,
816 gate_text_fs,
817 op_w,
818 sx,
819 sy,
820 &px,
821 &py,
822 ));
823 if matches!(
824 kind,
825 VisualControlFlowKind::IfElseBlock {
826 has_false_branch: true,
827 ..
828 }
829 ) {
830 elements.push(svg_text(
831 px(x_center + op_w / 2.0 + 0.08),
832 py(y_min + box_h - 0.12),
833 "else",
834 gate_text_fs,
835 gate_style
836 .text_color
837 .as_deref()
838 .unwrap_or(palette.text_color),
839 "start",
840 ));
841 }
842 }
843 }
844 }
845
846 x += col_w / 2.0;
847 if i + 1 < row_cols.len() {
848 x += options.moment_spacing;
849 }
850 }
851 }
852
853 let mut out = String::new();
854 out.push_str(&format!(
855 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{:.0}\" height=\"{:.0}\" viewBox=\"0 0 {:.3} {:.3}\">",
856 canvas_w, canvas_h, canvas_w, canvas_h
857 ));
858 for e in elements {
859 out.push_str(&e);
860 }
861 out.push_str("</svg>");
862 out
863}
864
865pub fn draw_figure_from_visual(
895 visual: &VisualCircuit,
896 options: &FigureDrawerOptions,
897 _output_path: Option<&str>,
898) -> String {
899 draw_figure_svg_from_visual(visual, options)
900}
901
902fn svg_line(
903 x1: f64,
904 y1: f64,
905 x2: f64,
906 y2: f64,
907 stroke: &str,
908 stroke_width: f64,
909 dash: Option<&str>,
910) -> String {
911 let dash_attr = dash
912 .map(|d| format!(" stroke-dasharray=\"{}\"", d))
913 .unwrap_or_default();
914 format!(
915 "<line x1=\"{:.3}\" y1=\"{:.3}\" x2=\"{:.3}\" y2=\"{:.3}\" stroke=\"{}\" stroke-width=\"{:.3}\"{} />",
916 x1, y1, x2, y2, stroke, stroke_width, dash_attr
917 )
918}
919
920#[allow(clippy::too_many_arguments)]
921fn svg_rect(
922 x: f64,
923 y: f64,
924 w: f64,
925 h: f64,
926 fill: &str,
927 stroke: &str,
928 lw: f64,
929 dash: Option<&str>,
930) -> String {
931 let dash_attr = dash
932 .map(|d| format!(" stroke-dasharray=\"{}\"", d))
933 .unwrap_or_default();
934 format!(
935 "<rect x=\"{:.3}\" y=\"{:.3}\" width=\"{:.3}\" height=\"{:.3}\" fill=\"{}\" stroke=\"{}\" stroke-width=\"{:.3}\"{} />",
936 x, y, w, h, fill, stroke, lw, dash_attr
937 )
938}
939
940fn svg_circle(
941 cx: f64,
942 cy: f64,
943 r: f64,
944 fill: Option<&str>,
945 stroke: Option<&str>,
946 lw: f64,
947) -> String {
948 format!(
949 "<circle cx=\"{:.3}\" cy=\"{:.3}\" r=\"{:.3}\" fill=\"{}\" stroke=\"{}\" stroke-width=\"{:.3}\" />",
950 cx,
951 cy,
952 r,
953 fill.unwrap_or("none"),
954 stroke.unwrap_or("none"),
955 lw
956 )
957}
958
959fn svg_text(x: f64, y: f64, text: &str, font_size: f64, color: &str, anchor: &str) -> String {
960 svg_text_with_content(x, y, font_size, color, anchor, &escape_xml(text))
961}
962
963fn svg_formula_text(
965 x: f64,
966 y: f64,
967 text: &str,
968 font_size: f64,
969 color: &str,
970 anchor: &str,
971) -> String {
972 svg_text_with_content(x, y, font_size, color, anchor, &format_svg_subscripts(text))
973}
974
975fn svg_text_with_content(
976 x: f64,
977 y: f64,
978 font_size: f64,
979 color: &str,
980 anchor: &str,
981 content: &str,
982) -> String {
983 format!(
984 "<text x=\"{:.3}\" y=\"{:.3}\" fill=\"{}\" font-size=\"{:.3}\" font-family=\"DejaVu Sans, Arial, sans-serif\" text-anchor=\"{}\" dominant-baseline=\"middle\">{}</text>",
985 x, y, color, font_size, anchor, content
986 )
987}
988
989#[allow(clippy::too_many_arguments)]
990fn draw_gate_label_svg(
991 x_px: f64,
992 y_px: f64,
993 label: &str,
994 text_color: &str,
995 box_w_px: f64,
996 box_h_px: f64,
997 base_name_fs: f64,
998 allow_shrink: bool,
999) -> Vec<String> {
1000 let mut parts = label.splitn(2, '\n');
1001 let name = parts.next().unwrap_or_default();
1002 let param = parts.next().filter(|s| !s.is_empty());
1003 let avail_w = (box_w_px - 2.0 * LABEL_INNER_PADDING_PX).max(LABEL_MIN_INNER_PX);
1004 let avail_h = (box_h_px - LABEL_INNER_PADDING_PX).max(LABEL_MIN_INNER_PX);
1005 let nfs = base_name_fs.max(1.0);
1006 let mut pfs = (nfs * PARAM_FONT_SCALE).max(1.0);
1007
1008 if allow_shrink && param.is_some() {
1011 let gap = nfs * LABEL_LINE_GAP_SCALE;
1012 let need_h = nfs + gap + pfs;
1013 if need_h > avail_h {
1014 let height_room = (avail_h - nfs - gap).max(1.0);
1015 pfs *= height_room / pfs.max(1.0);
1016 }
1017 }
1018
1019 let name_fs = fitted_font_size(svg_text_width_px(name, nfs), nfs, avail_w, allow_shrink);
1020 let param_fs = param
1021 .map(|p| fitted_font_size(svg_formula_width_px(p, pfs), pfs, avail_w, allow_shrink))
1022 .unwrap_or(pfs);
1023
1024 let mut out = Vec::new();
1025 if let Some(p) = param {
1026 let gap = name_fs * LABEL_LINE_GAP_SCALE;
1027 let total_h = name_fs + gap + param_fs;
1028 let top = y_px - total_h / 2.0;
1029 let name_y = top + name_fs / 2.0;
1030 let param_y = name_y + (name_fs / 2.0 + gap + param_fs / 2.0);
1031 out.push(svg_text(x_px, name_y, name, name_fs, text_color, "middle"));
1032 out.push(svg_formula_text(
1033 x_px, param_y, p, param_fs, text_color, "middle",
1034 ));
1035 } else {
1036 out.push(svg_text(x_px, y_px, name, name_fs, text_color, "middle"));
1037 }
1038 out
1039}
1040
1041#[allow(clippy::too_many_arguments)]
1042fn draw_box_svg(
1043 x: f64,
1044 y: f64,
1045 label: &str,
1046 palette: &FigurePalette,
1047 style: &GateStyle,
1048 base_font_size: f64,
1049 width: f64,
1050 height: f64,
1051 sx: f64,
1052 sy: f64,
1053 px: &impl Fn(f64) -> f64,
1054 py: &impl Fn(f64) -> f64,
1055) -> Vec<String> {
1056 draw_labeled_rect_svg(
1057 x,
1058 y,
1059 width,
1060 height,
1061 label,
1062 palette,
1063 style,
1064 base_font_size,
1065 sx,
1066 sy,
1067 px,
1068 py,
1069 None,
1070 None,
1071 true,
1072 )
1073}
1074
1075#[allow(clippy::too_many_arguments)]
1076fn draw_span_box_svg(
1077 x: f64,
1078 y_min: f64,
1079 height: f64,
1080 label: &str,
1081 palette: &FigurePalette,
1082 style: &GateStyle,
1083 base_font_size: f64,
1084 width: f64,
1085 sx: f64,
1086 sy: f64,
1087 px: &impl Fn(f64) -> f64,
1088 py: &impl Fn(f64) -> f64,
1089) -> Vec<String> {
1090 draw_labeled_rect_svg(
1091 x,
1092 y_min + height / 2.0,
1093 width,
1094 height,
1095 label,
1096 palette,
1097 style,
1098 base_font_size,
1099 sx,
1100 sy,
1101 px,
1102 py,
1103 None,
1104 None,
1105 true,
1106 )
1107}
1108
1109#[allow(clippy::too_many_arguments)]
1110fn draw_flow_box_svg(
1111 x: f64,
1112 y_min: f64,
1113 height: f64,
1114 label: &str,
1115 palette: &FigurePalette,
1116 style: &GateStyle,
1117 base_font_size: f64,
1118 width: f64,
1119 sx: f64,
1120 sy: f64,
1121 px: &impl Fn(f64) -> f64,
1122 py: &impl Fn(f64) -> f64,
1123) -> Vec<String> {
1124 draw_labeled_rect_svg(
1125 x,
1126 y_min + height / 2.0,
1127 width,
1128 height,
1129 label,
1130 palette,
1131 style,
1132 base_font_size,
1133 sx,
1134 sy,
1135 px,
1136 py,
1137 Some("6,4"),
1138 Some(palette.gate_edge_color),
1139 true,
1140 )
1141}
1142
1143#[allow(clippy::too_many_arguments)]
1144fn draw_labeled_rect_svg(
1145 x: f64,
1146 y: f64,
1147 width: f64,
1148 height: f64,
1149 label: &str,
1150 palette: &FigurePalette,
1151 style: &GateStyle,
1152 base_font_size: f64,
1153 sx: f64,
1154 sy: f64,
1155 px: &impl Fn(f64) -> f64,
1156 py: &impl Fn(f64) -> f64,
1157 dash: Option<&str>,
1158 edge_fallback: Option<&str>,
1159 allow_shrink: bool,
1160) -> Vec<String> {
1161 let fill = normalized_fill_color(style, palette).unwrap_or_else(|| "none".to_string());
1162 let edge = normalized_edge_color(style, palette)
1164 .or_else(|| edge_fallback.map(str::to_string))
1165 .unwrap_or_else(|| fill.clone());
1166 let lw = style_line_width(style, palette);
1167 let text_color = str_color(style.text_color.as_deref(), palette.text_color);
1168 let rx = px(x - width / 2.0);
1169 let ry = py(y - height / 2.0);
1170 let rw = width * sx;
1171 let rh = height * sy;
1172 let mut out = vec![svg_rect(rx, ry, rw, rh, &fill, &edge, lw, dash)];
1173 out.extend(draw_gate_label_svg(
1174 px(x),
1175 py(y),
1176 label,
1177 &text_color,
1178 rw,
1179 rh,
1180 base_font_size,
1181 allow_shrink,
1182 ));
1183 out
1184}
1185
1186#[allow(clippy::too_many_arguments)]
1187fn draw_measure_svg(
1188 x: f64,
1189 y: f64,
1190 palette: &FigurePalette,
1191 style: &GateStyle,
1192 base_font_size: f64,
1193 width: f64,
1194 height: f64,
1195 sx: f64,
1196 sy: f64,
1197 px: &impl Fn(f64) -> f64,
1198 py: &impl Fn(f64) -> f64,
1199) -> Vec<String> {
1200 let mut out = draw_box_svg(
1201 x,
1202 y,
1203 "",
1204 palette,
1205 style,
1206 base_font_size,
1207 width,
1208 height,
1209 sx,
1210 sy,
1211 px,
1212 py,
1213 );
1214 let lw = style_line_width(style, palette);
1215 let line_color = str_color(style.line_color.as_deref(), palette.text_color);
1216
1217 let cx = px(x);
1219 let cy = py(y + 0.18 * height);
1220 let rx = 0.35 * width * sx;
1221 let ry = 0.35 * height * sy;
1222 let arc_lw = (lw * 3.0).max(3.0);
1223 let arc_sx = cx - rx;
1224 let arc_ex = cx + rx;
1225 out.push(format!(
1226 "<path d=\"M {:.3} {:.3} A {:.3} {:.3} 0 0 1 {:.3} {:.3}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{:.3}\" stroke-linecap=\"round\" />",
1227 arc_sx, cy, rx, ry, arc_ex, cy, line_color, arc_lw
1228 ));
1229
1230 let dot_r = (lw * 3.0).max(3.0);
1232 out.push(svg_circle(cx, cy, dot_r, Some(&line_color), None, 0.0));
1233
1234 let arrow_len_x = rx * 0.9;
1236 let arrow_len_y = ry * 0.9;
1237 let ax1 = cx + arrow_len_x;
1238 let ay1 = cy - arrow_len_y;
1239 out.push(svg_line(cx, cy, ax1, ay1, &line_color, lw, None));
1240
1241 let head = (lw * 3.5).max(6.0);
1243 let angle: f64 = std::f64::consts::FRAC_PI_4; let perp = angle + std::f64::consts::FRAC_PI_2;
1245 let p1x = ax1 - head * angle.cos() + (head * 0.4) * perp.cos();
1246 let p1y = ay1 + head * angle.sin() - (head * 0.4) * perp.sin();
1247 let p2x = ax1 - head * angle.cos() - (head * 0.4) * perp.cos();
1248 let p2y = ay1 + head * angle.sin() + (head * 0.4) * perp.sin();
1249 out.push(format!(
1250 "<polygon points=\"{:.3},{:.3} {:.3},{:.3} {:.3},{:.3}\" fill=\"{}\"/>",
1251 ax1, ay1, p1x, p1y, p2x, p2y, line_color
1252 ));
1253
1254 let label_fs = (base_font_size * 0.85).max(6.0);
1256 let label_y = py(y - 0.30 * height);
1257 let label_x0 = cx - rx * 0.75;
1258 let label_x1 = cx + rx * 0.75;
1259 out.push(svg_text(
1260 label_x0,
1261 label_y,
1262 "0",
1263 label_fs,
1264 &line_color,
1265 "middle",
1266 ));
1267 out.push(svg_text(
1268 label_x1,
1269 label_y,
1270 "1",
1271 label_fs,
1272 &line_color,
1273 "middle",
1274 ));
1275
1276 out
1277}
1278
1279fn escape_xml(value: &str) -> String {
1280 value
1281 .replace('&', "&")
1282 .replace('<', "<")
1283 .replace('>', ">")
1284 .replace('\"', """)
1285}
1286
1287fn format_svg_subscripts(value: &str) -> String {
1292 let mut out = String::with_capacity(value.len());
1293 let bytes = value.as_bytes();
1294 let mut i = 0usize;
1295
1296 while i < bytes.len() {
1297 if bytes[i] == b'_' && bytes.get(i + 1).is_some_and(is_subscript_suffix_byte) {
1298 let start = i + 1;
1299 let mut end = start;
1300 while bytes.get(end).is_some_and(is_subscript_suffix_byte) {
1301 end += 1;
1302 }
1303 out.push_str("<tspan baseline-shift=\"sub\" font-size=\"70%\">");
1304 out.push_str(&escape_xml(&value[start..end]));
1305 out.push_str("</tspan>");
1306 i = end;
1307 continue;
1308 }
1309
1310 let ch = value[i..].chars().next().unwrap_or_default();
1311 let mut buf = [0; 4];
1312 out.push_str(&escape_xml(ch.encode_utf8(&mut buf)));
1313 i += ch.len_utf8();
1314 }
1315
1316 out
1317}
1318
1319fn fitted_font_size(text_width_px: f64, font_size: f64, avail_w: f64, allow_shrink: bool) -> f64 {
1321 if !allow_shrink || text_width_px <= avail_w || text_width_px <= 0.0 {
1322 return font_size;
1323 }
1324 let scale = (avail_w / text_width_px).clamp(LABEL_FIT_MIN_SCALE, 1.0);
1325 font_size * scale
1326}
1327
1328fn svg_formula_width_px(value: &str, font_size: f64) -> f64 {
1330 let bytes = value.as_bytes();
1331 let mut i = 0usize;
1332 let mut width = 0.0_f64;
1333
1334 while i < bytes.len() {
1335 if bytes[i] == b'_' && bytes.get(i + 1).is_some_and(is_subscript_suffix_byte) {
1336 i += 1;
1337 while bytes.get(i).is_some_and(is_subscript_suffix_byte) {
1338 let ch = value[i..].chars().next().unwrap_or_default();
1339 width += svg_char_width_em(ch) * font_size * SUBSCRIPT_FONT_SCALE;
1340 i += ch.len_utf8();
1341 }
1342 continue;
1343 }
1344
1345 let ch = value[i..].chars().next().unwrap_or_default();
1346 width += svg_char_width_em(ch) * font_size;
1347 i += ch.len_utf8();
1348 }
1349
1350 width
1351}
1352
1353fn svg_text_width_px(value: &str, font_size: f64) -> f64 {
1355 value
1356 .chars()
1357 .map(|ch| svg_char_width_em(ch) * font_size)
1358 .sum()
1359}
1360
1361fn svg_char_width_em(ch: char) -> f64 {
1363 match ch {
1364 ' ' => 0.33,
1365 'i' | 'j' | 'l' | 'I' | '|' | '!' | '.' | ',' | ':' | ';' | '\'' => 0.28,
1366 '(' | ')' | '[' | ']' | '{' | '}' => 0.35,
1367 '*' | '+' | '-' | '/' | '=' => 0.55,
1368 '0'..='9' => 0.58,
1369 'A'..='Z' => match ch {
1370 'M' | 'W' => 0.90,
1371 _ => 0.68,
1372 },
1373 'a'..='z' => match ch {
1374 'm' | 'w' => 0.82,
1375 'f' | 'r' | 't' => 0.42,
1376 _ => 0.58,
1377 },
1378 '_' => 0.52,
1379 '<' | '>' => 0.62,
1380 'π' | 'θ' | 'φ' | 'λ' | 'γ' | 'β' => 0.62,
1381 _ if ch.is_ascii() => 0.62,
1382 _ => 0.82,
1383 }
1384}
1385
1386fn label_text_width_px(label: &str, base_font_size: f64) -> f64 {
1388 let mut parts = label.splitn(2, '\n');
1389 let name = parts.next().unwrap_or_default();
1390 let param = parts.next().filter(|s| !s.is_empty());
1391 let name_w = svg_text_width_px(name, base_font_size);
1392 let param_w = param
1393 .map(|p| svg_formula_width_px(p, base_font_size * PARAM_FONT_SCALE))
1394 .unwrap_or(0.0);
1395 name_w.max(param_w)
1396}
1397
1398fn is_subscript_suffix_byte(byte: &u8) -> bool {
1400 byte.is_ascii_alphanumeric()
1401}
1402
1403fn compose_label(label: &str, params: &[String], show_params: bool) -> String {
1405 if show_params && !params.is_empty() {
1406 format!("{label}\n{}", params.join(","))
1407 } else {
1408 label.to_string()
1409 }
1410}
1411
1412fn show_span_lane_markers(op: &VisualOperation) -> bool {
1413 matches!(op.label.as_str(), "RXX" | "RYY" | "RZX" | "RZZ" | "UNITARY")
1414}
1415
1416fn is_module_span_gate(op: &VisualOperation) -> bool {
1417 matches!(op.style, VisualOpStyle::Gate) && op.span_box
1418}
1419
1420fn is_control_flow_box(op: &VisualOperation) -> bool {
1421 matches!(op.style, VisualOpStyle::ControlFlow { .. })
1422}
1423
1424fn is_parameterized_box_gate(op: &VisualOperation, show_params: bool) -> bool {
1425 show_params
1426 && !op.params.is_empty()
1427 && op.label != "FSIM"
1428 && matches!(
1429 op.style,
1430 VisualOpStyle::Gate | VisualOpStyle::Controlled { .. }
1431 )
1432}
1433
1434fn uses_measured_label_width(op: &VisualOperation, show_params: bool) -> bool {
1435 is_parameterized_box_gate(op, show_params) || is_module_span_gate(op) || is_control_flow_box(op)
1436}
1437
1438fn gate_font_size(style: &GateStyle, fallback_font_size: f64) -> f64 {
1439 style
1440 .font_size
1441 .unwrap_or(fallback_font_size)
1442 .clamp(1.0, 96.0)
1443}
1444
1445fn gate_label_required_width(
1447 op: &VisualOperation,
1448 show_params: bool,
1449 base_font_size: f64,
1450 sx: f64,
1451 gate_width: f64,
1452) -> f64 {
1453 if !uses_measured_label_width(op, show_params) {
1454 return gate_width;
1455 }
1456
1457 let text_width_px = if is_control_flow_box(op) {
1458 svg_text_width_px(&op.label, base_font_size)
1459 } else {
1460 let label = compose_label(&op.label, &op.params, show_params);
1461 label_text_width_px(&label, base_font_size)
1462 };
1463 if text_width_px <= 0.0 || sx <= 0.0 {
1464 return gate_width;
1465 }
1466
1467 let required_px = text_width_px + 2.0 * LABEL_INNER_PADDING_PX;
1468 gate_width.max(required_px / sx)
1469}
1470
1471fn split_columns_by_fold(
1475 col_widths: &[f64],
1476 fold: i32,
1477 moment_spacing: f64,
1478 gate_width: f64,
1479) -> Vec<Vec<usize>> {
1480 if col_widths.is_empty() {
1481 return vec![Vec::new()];
1482 }
1483 if fold < 0 {
1484 return vec![(0..col_widths.len()).collect()];
1485 }
1486 let target_cols = usize::try_from(fold)
1487 .ok()
1488 .filter(|value| *value > 0)
1489 .unwrap_or(col_widths.len());
1490
1491 let avg_col_width =
1494 col_widths.iter().copied().sum::<f64>() / (col_widths.len() as f64).max(1.0);
1495 let effective_col_width = avg_col_width.max(gate_width);
1496 let target_width = (2.0 * moment_spacing
1497 + target_cols as f64 * effective_col_width
1498 + target_cols.saturating_sub(1) as f64 * moment_spacing)
1499 * FOLD_TARGET_SLACK;
1500
1501 let mut rows = Vec::new();
1502 let mut start = 0usize;
1503 while start < col_widths.len() {
1504 let mut row = vec![start];
1505 let mut width = 2.0 * moment_spacing + col_widths[start];
1506 let mut next = start + 1;
1507 while next < col_widths.len() {
1508 let candidate = width + moment_spacing + col_widths[next];
1509 if candidate <= target_width {
1510 row.push(next);
1511 width = candidate;
1512 next += 1;
1513 } else {
1514 break;
1515 }
1516 }
1517 rows.push(row);
1518 start = next;
1519 }
1520 rows
1521}
1522
1523fn lane_to_y(lane: usize, y_base: f64) -> f64 {
1524 y_base + lane as f64 * WIRE_PITCH
1525}
1526
1527fn str_color(candidate: Option<&str>, fallback: &str) -> String {
1528 candidate.unwrap_or(fallback).to_string()
1529}
1530
1531fn normalize_style_color(candidate: Option<&str>) -> Option<String> {
1532 match candidate {
1533 Some(value)
1534 if value.eq_ignore_ascii_case("none") || value.eq_ignore_ascii_case("transparent") =>
1535 {
1536 None
1537 }
1538 Some(value) => Some(value.to_string()),
1539 None => None,
1540 }
1541}
1542
1543fn normalized_fill_color(style: &GateStyle, palette: &FigurePalette) -> Option<String> {
1544 normalize_style_color(style.background_color.as_deref())
1545 .or_else(|| normalize_style_color(palette.gate_fill_color))
1546}
1547
1548fn normalized_edge_color(style: &GateStyle, palette: &FigurePalette) -> Option<String> {
1549 normalize_style_color(style.border_color.as_deref())
1550 .or_else(|| normalize_style_color(Some(palette.gate_edge_color)))
1551}
1552
1553fn style_line_width(style: &GateStyle, palette: &FigurePalette) -> f64 {
1554 style.line_width.unwrap_or(palette.gate_linewidth)
1555}
1556
1557fn op_style_key(op: &VisualOperation) -> &str {
1565 match &op.style {
1566 VisualOpStyle::Measure => "M",
1567 VisualOpStyle::Reset => "R",
1568 VisualOpStyle::Delay => "D",
1569 VisualOpStyle::Barrier => "B",
1570 VisualOpStyle::Cz => "CZ",
1571 VisualOpStyle::Swap => "SWAP",
1572 VisualOpStyle::ControlFlow { kind } => control_flow_style_key(kind),
1573 VisualOpStyle::Gate if op.span_box => "MODULE",
1574 _ => op.label.as_str(),
1575 }
1576}
1577
1578fn control_flow_style_key(kind: &VisualControlFlowKind) -> &'static str {
1579 match kind {
1580 VisualControlFlowKind::IfElseBlock { .. } | VisualControlFlowKind::IfStart => "IF",
1581 VisualControlFlowKind::ElseStart => "ELSE",
1582 VisualControlFlowKind::WhileBlock { .. } | VisualControlFlowKind::WhileStart => "WHILE",
1583 VisualControlFlowKind::ForBlock { .. } | VisualControlFlowKind::ForStart => "FOR",
1584 VisualControlFlowKind::SwitchBlock { .. } | VisualControlFlowKind::SwitchStart => "SWITCH",
1585 VisualControlFlowKind::CaseStart => "CASE",
1586 VisualControlFlowKind::DefaultStart => "DEFAULT",
1587 VisualControlFlowKind::Break => "BREAK",
1588 VisualControlFlowKind::Continue => "CONTINUE",
1589 VisualControlFlowKind::End => "END",
1590 }
1591}