use std::fmt::Write as _;
use ratex_svg::{SvgOptions, render_to_svg};
use super::*;
const PIXELS_PER_CENTIMETER: f32 = POINTS_PER_CENTIMETER;
const STROKE_WIDTH: f32 = 1.0;
const CHARACTER_ADVANCE: f32 = 0.62;
const TEXT_PADDING: f32 = 4.0;
const BORDER: f32 = 6.0;
const FONT_FAMILY: &str = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
pub(super) fn render_svg(circuit: &Circuit) -> String {
let (scheduled, _) = schedule(circuit);
let last_column = scheduled
.iter()
.map(|operation| operation.column)
.max()
.unwrap_or(0);
let columns = column_positions(circuit, &scheduled);
let end_x = columns[last_column + 1];
let mut canvas = Canvas {
rotated: circuit.layout.orientation == Orientation::Vertical,
..Canvas::default()
};
for (group_index, group) in circuit.groups.iter().enumerate() {
draw_group(
&mut canvas,
circuit,
&scheduled,
group_index,
group,
&columns,
);
}
for (wire_index, wire) in circuit.wires.iter().enumerate() {
draw_wire(
&mut canvas,
circuit,
&scheduled,
wire_index,
wire,
&columns,
end_x,
);
}
for operation in &scheduled {
draw_operation(&mut canvas, circuit, operation, &columns);
}
assemble(circuit, canvas)
}
fn assemble(circuit: &Circuit, canvas: Canvas) -> String {
let bounds = canvas.bounds.unwrap_or(Bounds {
left: 0.0,
top: 0.0,
right: 1.0,
bottom: 1.0,
});
let vertical = circuit.layout.orientation == Orientation::Vertical;
let (left, top, width, height) = if vertical {
(
-bounds.bottom,
bounds.left,
bounds.bottom - bounds.top,
bounds.right - bounds.left,
)
} else {
(
bounds.left,
bounds.top,
bounds.right - bounds.left,
bounds.bottom - bounds.top,
)
};
let (left, top) = (left - BORDER, top - BORDER);
let (width, height) = (width + 2.0 * BORDER, height + 2.0 * BORDER);
let scale = circuit.layout.scale;
let mut output = String::new();
emit!(
output,
"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" \
width=\"{:.3}\" height=\"{:.3}\" viewBox=\"{left:.3} {top:.3} {width:.3} {height:.3}\" \
font-family=\"{FONT_FAMILY}\" font-size=\"{FONT_SIZE}\">",
width * scale,
height * scale
);
emit!(output, " <title>{}</title>", escape_text(&circuit.name));
emit!(
output,
" <rect x=\"{left:.3}\" y=\"{top:.3}\" width=\"{width:.3}\" height=\"{height:.3}\" fill=\"{}\"/>",
svg_color(&circuit.layout.background)
);
if vertical {
output.push_str(" <g transform=\"rotate(90)\">\n");
} else {
output.push_str(" <g>\n");
}
output.push_str(&canvas.body);
output.push_str(" </g>\n");
output.push_str("</svg>\n");
output
}
#[derive(Clone, Copy)]
struct Bounds {
left: f32,
top: f32,
right: f32,
bottom: f32,
}
#[derive(Default)]
struct Canvas {
body: String,
bounds: Option<Bounds>,
rotated: bool,
}
impl Canvas {
fn point(&mut self, x: f32, y: f32) -> (f32, f32) {
let point = (x * PIXELS_PER_CENTIMETER, -y * PIXELS_PER_CENTIMETER);
self.cover(point.0, point.1);
point
}
fn cover(&mut self, x: f32, y: f32) {
self.bounds = Some(match self.bounds {
None => Bounds {
left: x,
top: y,
right: x,
bottom: y,
},
Some(bounds) => Bounds {
left: bounds.left.min(x),
top: bounds.top.min(y),
right: bounds.right.max(x),
bottom: bounds.bottom.max(y),
},
});
}
fn line(&mut self, start_x: f32, start_y: f32, end_x: f32, end_y: f32, attributes: &str) {
let (x1, y1) = self.point(start_x, start_y);
let (x2, y2) = self.point(end_x, end_y);
emit!(
self.body,
" <line x1=\"{x1:.3}\" y1=\"{y1:.3}\" x2=\"{x2:.3}\" y2=\"{y2:.3}\"{attributes}/>"
);
}
fn rectangle(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32, attributes: &str) {
let (left, top) = self.point(x1.min(x2), y1.max(y2));
let (right, bottom) = self.point(x1.max(x2), y1.min(y2));
let corner = if radius > 0.0 {
format!(" rx=\"{radius:.3}\"")
} else {
String::new()
};
emit!(
self.body,
" <rect x=\"{left:.3}\" y=\"{top:.3}\" width=\"{:.3}\" height=\"{:.3}\"{corner}{attributes}/>",
right - left,
bottom - top
);
}
fn circle(&mut self, x: f32, y: f32, radius: f32, attributes: &str) {
let (cx, cy) = self.point(x, y);
self.cover(cx - radius, cy - radius);
self.cover(cx + radius, cy + radius);
emit!(
self.body,
" <circle cx=\"{cx:.3}\" cy=\"{cy:.3}\" r=\"{radius:.3}\"{attributes}/>"
);
}
fn ellipse(&mut self, x: f32, y: f32, radius_x: f32, radius_y: f32, attributes: &str) {
let (cx, cy) = self.point(x, y);
self.cover(cx - radius_x, cy - radius_y);
self.cover(cx + radius_x, cy + radius_y);
emit!(
self.body,
" <ellipse cx=\"{cx:.3}\" cy=\"{cy:.3}\" rx=\"{radius_x:.3}\" ry=\"{radius_y:.3}\"{attributes}/>"
);
}
fn path(&mut self, data: &str, attributes: &str) {
emit!(self.body, " <path d=\"{data}\"{attributes}/>");
}
#[allow(clippy::too_many_arguments)]
fn text(&mut self, x: f32, y: f32, value: &str, anchor: Anchor, style: &Style, boxed: bool) {
if !label_has_math(value) {
self.text_sized(x, y, &label_text(value), anchor, style, boxed, FONT_SIZE);
return;
}
let (anchor_x, anchor_y) = self.point(x, y);
let dimensions = label_dimensions(value);
let width = dimensions.width * PIXELS_PER_CENTIMETER;
let height = dimensions.height * PIXELS_PER_CENTIMETER;
let (offset_x, baseline_offset) = anchor.offset(width, FONT_SIZE);
let (center_x, center_y) = (
anchor_x + offset_x,
anchor_y + baseline_offset - FONT_SIZE * 0.35,
);
if self.rotated {
let (cx, cy) = (
anchor_x + center_y - anchor_y,
anchor_y - (center_x - anchor_x),
);
self.cover(cx - height / 2.0, cy - width / 2.0);
self.cover(cx + height / 2.0, cy + width / 2.0);
} else {
self.cover(center_x - width / 2.0, center_y - height / 2.0);
self.cover(center_x + width / 2.0, center_y + height / 2.0);
}
let mut fragment = String::new();
if boxed && let Some(fill) = &style.fill {
let mut attributes = format!(" fill=\"{}\"", svg_color(fill));
push_opacity(&mut attributes, style);
emit!(
fragment,
" <rect x=\"{:.3}\" y=\"{:.3}\" width=\"{:.3}\" height=\"{:.3}\"{attributes}/>",
center_x - width / 2.0 - TEXT_PADDING / 2.0,
center_y - height / 2.0 - TEXT_PADDING / 2.0,
width + TEXT_PADDING,
height + TEXT_PADDING
);
}
let mut cursor = center_x - width / 2.0;
for span in label_spans(value) {
let text = match span {
LabelSpan::Text(text) => label_text(text),
LabelSpan::Math(math) => {
if let Some(rendered) = render_math(math, style) {
emit!(
fragment,
" <g transform=\"translate({cursor:.3} {:.3})\">{}</g>",
center_y - rendered.height / 2.0,
rendered.body
);
cursor += rendered.width;
continue;
}
math.into()
}
};
let span_width = text_width_at(&text, FONT_SIZE);
let mut attributes = format!(
" text-anchor=\"middle\" fill=\"{}\"",
svg_color(style.stroke.as_deref().unwrap_or("black"))
);
push_opacity(&mut attributes, style);
emit!(
fragment,
" <text x=\"{:.3}\" y=\"{:.3}\"{attributes}>{}</text>",
cursor + span_width / 2.0,
center_y + FONT_SIZE * 0.35,
escape_text(&text)
);
cursor += span_width;
}
if let Some(link) = &style.link {
fragment = format!(
" <a xlink:href=\"{0}\" href=\"{0}\">\n{fragment} </a>\n",
escape_text(link)
);
}
if self.rotated {
emit!(
self.body,
" <g transform=\"rotate(-90 {anchor_x:.3} {anchor_y:.3})\">\n{fragment} </g>"
);
} else {
self.body.push_str(&fragment);
}
}
#[allow(clippy::too_many_arguments)]
fn text_sized(
&mut self,
x: f32,
y: f32,
value: &str,
anchor: Anchor,
style: &Style,
boxed: bool,
font_size: f32,
) {
let (anchor_x, anchor_y) = self.point(x, y);
let width = text_width_at(value, font_size);
let (offset_x, offset_y) = anchor.offset(width, font_size);
let (px, py) = (anchor_x + offset_x, anchor_y + offset_y);
let turn = if self.rotated {
format!(" transform=\"rotate(-90 {anchor_x:.3} {anchor_y:.3})\"")
} else {
String::new()
};
if self.rotated {
let (cx, cy) = (anchor_x + offset_y, anchor_y - offset_x);
self.cover(cx - font_size * 0.8, cy - width / 2.0);
self.cover(cx + font_size * 0.8, cy + width / 2.0);
} else {
self.cover(px - width / 2.0, py - font_size * 0.8);
self.cover(px + width / 2.0, py + font_size * 0.8);
}
if boxed && let Some(fill) = &style.fill {
let mut attributes = format!(" fill=\"{}\"", svg_color(fill));
push_opacity(&mut attributes, style);
emit!(
self.body,
" <rect x=\"{:.3}\" y=\"{:.3}\" width=\"{:.3}\" height=\"{:.3}\"{attributes}{turn}/>",
px - width / 2.0 - TEXT_PADDING / 2.0,
py - font_size * 0.72,
width + TEXT_PADDING,
font_size * 1.1
);
}
let mut attributes = format!(
" text-anchor=\"middle\" fill=\"{}\"",
svg_color(style.stroke.as_deref().unwrap_or("black"))
);
if font_size != FONT_SIZE {
emit!(attributes, " font-size=\"{font_size:.3}\"");
}
push_opacity(&mut attributes, style);
let body = escape_text(value);
let body = match &style.link {
Some(link) => format!(
"<a xlink:href=\"{0}\" href=\"{0}\">{body}</a>",
escape_text(link)
),
None => body,
};
emit!(
self.body,
" <text x=\"{px:.3}\" y=\"{py:.3}\"{attributes}{turn}>{body}</text>"
);
}
}
struct RenderedMath {
body: String,
width: f32,
height: f32,
}
fn render_math(math: &str, style: &Style) -> Option<RenderedMath> {
let mut color = Color::parse(style.stroke.as_deref().unwrap_or("black"))?;
color.a *= style.opacity.unwrap_or(1.0);
let display = math_display_list(math, color)?;
let width = display.width as f32 * FONT_SIZE;
let height = display.total_height() as f32 * FONT_SIZE;
let svg = render_to_svg(
&display,
&SvgOptions {
font_size: FONT_SIZE.into(),
padding: 0.0,
stroke_width: STROKE_WIDTH.into(),
embed_glyphs: true,
font_dir: String::new(),
},
);
let body = svg.split_once('>')?.1.strip_suffix("</svg>")?.to_owned();
Some(RenderedMath {
body,
width,
height,
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Anchor {
Center,
East,
West,
South,
North,
SouthWest,
}
impl Anchor {
fn offset(self, width: f32, font_size: f32) -> (f32, f32) {
let middle = font_size * 0.35;
match self {
Anchor::Center => (0.0, middle),
Anchor::East => (-width / 2.0 - TEXT_PADDING, middle),
Anchor::West => (width / 2.0 + TEXT_PADDING, middle),
Anchor::South => (0.0, -TEXT_PADDING * 0.5),
Anchor::North => (0.0, font_size * 0.85),
Anchor::SouthWest => (width / 2.0, -TEXT_PADDING * 0.5),
}
}
}
fn text_width_at(value: &str, font_size: f32) -> f32 {
value.chars().count() as f32 * CHARACTER_ADVANCE * font_size
}
fn draw_group(
canvas: &mut Canvas,
circuit: &Circuit,
scheduled: &[Scheduled<'_>],
group_index: usize,
group: &Group,
columns: &[f32],
) {
let (first_column, last_column, first_row, last_row) =
group_bounds(group, scheduled, circuit.wires.len());
let left = columns[first_column - 1] - 0.52;
let right = columns[last_column - 1] + 0.52;
let top = -(first_row as f32) * circuit.layout.wire_gap + 0.48;
let bottom = -(last_row as f32) * circuit.layout.wire_gap - 0.48;
let stroke = if group.style.shape == Some(Shape::None) {
"none"
} else {
group.style.stroke.as_deref().unwrap_or("black")
};
let mut attributes = format!(
" fill=\"{}\" stroke=\"{}\" stroke-width=\"{STROKE_WIDTH}\"",
svg_color(group.style.fill.as_deref().unwrap_or("none")),
svg_color(stroke)
);
push_dash(&mut attributes, &group.style);
push_opacity(&mut attributes, &group.style);
let radius = if matches!(group.style.shape, Some(Shape::Circle | Shape::Ellipse)) {
5.0
} else {
0.0
};
canvas.rectangle(left, top, right, bottom, radius, &attributes);
canvas.text(
left,
top + group_index as f32 * 0.24,
&group.label,
Anchor::SouthWest,
&Style::default(),
false,
);
}
fn draw_wire(
canvas: &mut Canvas,
circuit: &Circuit,
scheduled: &[Scheduled<'_>],
wire_index: usize,
wire: &Wire,
columns: &[f32],
end_x: f32,
) {
let initial_kind = initial_wire_kind(circuit, scheduled, wire_index);
let mut kind = initial_kind;
let mut row = initial_row(circuit, wire_index);
let mut start_x = 0.0;
for operation in scheduled {
let x = columns[operation.column];
if let OperationKind::Permute { wires } = operation.kind
&& wires.contains(&wire_index)
{
let half_width = operation
.style
.width
.map_or(0.45, |width| width / (2.0 * POINTS_PER_CENTIMETER))
.min(circuit.layout.column_gap * 0.45);
let next_row = operation.permuted_row(wire_index);
let source_y = -(row as f32) * circuit.layout.wire_gap;
let destination_y = -(next_row as f32) * circuit.layout.wire_gap;
draw_wire_run(canvas, kind, start_x, x - half_width, source_y, &wire.style);
draw_permutation_curve(
canvas,
kind,
x - half_width,
x,
x + half_width,
source_y,
destination_y,
&merged_line_style(&wire.style, operation.style),
circuit.layout.corner_radius,
);
row = next_row;
start_x = x + half_width;
}
if let Some((transition_x, next_kind)) =
wire_transition(circuit, operation, wire_index, columns)
{
let y = -(row as f32) * circuit.layout.wire_gap;
draw_wire_run(canvas, kind, start_x, transition_x, y, &wire.style);
kind = next_kind;
start_x = transition_x;
}
}
let y = -(row as f32) * circuit.layout.wire_gap;
draw_wire_run(canvas, kind, start_x, end_x, y, &wire.style);
if initial_kind != WireKind::Hidden || wire.ellipsis {
let input = wire.input.as_deref().unwrap_or(&wire.name);
let input_y = -(initial_row(circuit, wire_index) as f32) * circuit.layout.wire_gap;
canvas.text(0.0, input_y, input, Anchor::East, &Style::default(), false);
}
if (kind != WireKind::Hidden || wire.ellipsis)
&& let Some(label) = &wire.output
{
canvas.text(end_x, y, label, Anchor::West, &Style::default(), false);
}
}
fn draw_wire_run(
canvas: &mut Canvas,
kind: WireKind,
start_x: f32,
end_x: f32,
y: f32,
style: &Style,
) {
if end_x <= start_x {
return;
}
let attributes = line_attributes(style);
match kind {
WireKind::Quantum => canvas.line(start_x, y, end_x, y, &attributes),
WireKind::Classical => {
for offset in [-0.035, 0.035] {
canvas.line(start_x, y + offset, end_x, y + offset, &attributes);
}
}
WireKind::Hidden => {}
}
}
#[allow(clippy::too_many_arguments)]
fn draw_permutation_curve(
canvas: &mut Canvas,
kind: WireKind,
start_x: f32,
control_x: f32,
end_x: f32,
source_y: f32,
destination_y: f32,
style: &Style,
corner_radius: f32,
) {
let offsets: &[f32] = match kind {
WireKind::Quantum => &[0.0],
WireKind::Classical => &[-0.035, 0.035],
WireKind::Hidden => return,
};
let attributes = line_attributes(style);
for offset in offsets {
if corner_radius == 0.0 {
canvas.line(
start_x,
source_y + offset,
end_x,
destination_y + offset,
&attributes,
);
continue;
}
let bend = (corner_radius / 4.0).min(1.0);
let first_control = start_x + (control_x - start_x) * bend;
let second_control = end_x - (end_x - control_x) * bend;
let start = canvas.point(start_x, source_y + offset);
let first = canvas.point(first_control, source_y + offset);
let second = canvas.point(second_control, destination_y + offset);
let end = canvas.point(end_x, destination_y + offset);
let data = format!(
"M {:.3} {:.3} C {:.3} {:.3} {:.3} {:.3} {:.3} {:.3}",
start.0, start.1, first.0, first.1, second.0, second.1, end.0, end.1
);
canvas.path(&data, &attributes);
}
}
fn draw_operation(
canvas: &mut Canvas,
circuit: &Circuit,
operation: &Scheduled<'_>,
columns: &[f32],
) {
let layout = &circuit.layout;
let wire_gap = layout.wire_gap;
let x = columns[operation.column];
match operation.kind {
OperationKind::Gate {
label,
targets,
controls,
} => {
let targets = targets
.iter()
.map(|wire| operation.positions[*wire])
.collect::<Vec<_>>();
let controls = controls
.iter()
.map(|control| Control {
wire: operation.positions[control.wire],
..*control
})
.collect::<Vec<_>>();
draw_gate(
canvas,
x,
layout,
label,
&targets,
&controls,
operation.style,
);
}
OperationKind::Measure {
targets,
label,
shape,
} => {
for target in targets {
draw_measurement(
canvas,
x,
layout,
operation.positions[*target],
label.as_deref(),
*shape,
operation.style,
);
}
}
OperationKind::Swap { left, right } => {
let left_y = -(operation.positions[*left] as f32) * wire_gap;
let right_y = -(operation.positions[*right] as f32) * wire_gap;
let attributes = line_attributes(operation.style);
canvas.line(x, left_y, x, right_y, &attributes);
draw_cross(canvas, x, left_y, operation.style);
draw_cross(canvas, x, right_y, operation.style);
}
OperationKind::Barrier { .. } => {
let top = -(operation.first as f32) * wire_gap + 0.42;
let bottom = -(operation.last as f32) * wire_gap - 0.42;
let mut barrier_style = operation.style.clone();
barrier_style.dashed = true;
canvas.line(x, top, x, bottom, &line_attributes(&barrier_style));
}
OperationKind::WireChange { wires, kind, label } => {
if let Some(label) = label {
for wire in wires {
draw_value_transition(
canvas,
x,
-(operation.positions[*wire] as f32) * wire_gap,
label,
*kind,
operation.style,
&layout.background,
);
}
}
}
OperationKind::Endpoint {
wires,
start,
label,
} => {
let attributes = line_attributes(operation.style);
for wire in expanded_wires(wires, circuit.wires.len()) {
let y = -(operation.positions[wire] as f32) * wire_gap;
canvas.line(x, y - 0.13, x, y + 0.13, &attributes);
if let Some(label) = label {
let anchor = if *start { Anchor::East } else { Anchor::West };
canvas.text(x, y, label, anchor, operation.style, true);
}
}
}
OperationKind::Label {
wires,
label,
brace,
} => {
let mut rows = expanded_wires(wires, circuit.wires.len())
.iter()
.map(|wire| operation.positions[*wire])
.collect::<Vec<_>>();
rows.sort_unstable();
let first = *rows.first().expect("circuit has a wire");
let last = *rows.last().expect("circuit has a wire");
if let Some(side) = brace {
draw_brace(
canvas,
x,
wire_gap,
first,
last,
label,
*side,
operation.style,
&layout.background,
);
} else {
let y = -((first + last) as f32) * wire_gap / 2.0;
let masked = masked_style(operation.style, &layout.background);
canvas.text(x, y, label, Anchor::Center, &masked, true);
}
}
OperationKind::Bundle { wire, label } => {
let y = -(operation.positions[*wire] as f32) * wire_gap;
canvas.line(
x - 0.10,
y - 0.15,
x + 0.10,
y + 0.15,
&line_attributes(operation.style),
);
canvas.text(
x + 0.08,
y + 0.08,
label,
Anchor::SouthWest,
&Style::default(),
false,
);
}
OperationKind::Permute { .. } => {}
OperationKind::Phantom { wires } => {
if operation.style.width.is_some() || operation.style.height.is_some() {
let half_width = operation.style.width.unwrap_or(0.0) / 2.0;
let half_height = operation.style.height.unwrap_or(0.0) / 2.0;
for wire in expanded_wires(wires, circuit.wires.len()) {
let y = -(operation.positions[wire] as f32) * wire_gap;
let (px, py) = canvas.point(x, y);
canvas.cover(px - half_width, py - half_height);
canvas.cover(px + half_width, py + half_height);
}
}
}
OperationKind::Touch { .. } => {
if has_line_style(operation.style) {
let top = -(operation.first as f32) * wire_gap + 0.35;
let bottom = -(operation.last as f32) * wire_gap - 0.35;
canvas.line(x, top, x, bottom, &line_attributes(operation.style));
}
}
OperationKind::WireLabels { wires, labels } => {
let wires = selected_wires(wires, circuit.wires.len());
for (index, wire) in wires.iter().enumerate() {
let label = if labels.len() == 1 {
&labels[0]
} else {
&labels[index]
};
let y = -(operation.positions[*wire] as f32) * wire_gap;
let masked = masked_style(operation.style, &layout.background);
canvas.text(x, y, label, Anchor::Center, &masked, true);
}
}
OperationKind::Brace { wires, label, side } => {
let mut rows = selected_wires(wires, circuit.wires.len())
.iter()
.map(|wire| operation.positions[*wire])
.collect::<Vec<_>>();
rows.sort_unstable();
draw_brace(
canvas,
x,
wire_gap,
*rows.first().expect("circuit has a wire"),
*rows.last().expect("circuit has a wire"),
label,
*side,
operation.style,
&layout.background,
);
}
OperationKind::Note { wires, text, side } => {
let mut rows = selected_wires(wires, circuit.wires.len())
.iter()
.map(|wire| operation.positions[*wire])
.collect::<Vec<_>>();
rows.sort_unstable();
let above = note_is_above(circuit, *side);
let row = if above {
*rows.first().expect("circuit has a wire") as f32
} else {
*rows.last().expect("circuit has a wire") as f32
};
let y = -row * wire_gap + if above { 0.42 } else { -0.42 };
let anchor = match (circuit.layout.orientation == Orientation::Vertical, above) {
(true, true) => Anchor::West,
(true, false) => Anchor::East,
(false, true) => Anchor::South,
(false, false) => Anchor::North,
};
canvas.text(x, y, text, anchor, &Style::default(), false);
}
OperationKind::Cut { label, .. } => {
let top = -(operation.first as f32) * wire_gap + 0.42;
let bottom = -(operation.last as f32) * wire_gap - 0.42;
let mut cut_style = operation.style.clone();
cut_style.dashed = true;
canvas.line(x, top, x, bottom, &line_attributes(&cut_style));
if let Some(label) = label {
canvas.text(x, top, label, Anchor::South, &Style::default(), false);
}
}
}
}
fn draw_gate(
canvas: &mut Canvas,
x: f32,
layout: &Layout,
label: &str,
targets: &[usize],
controls: &[Control],
style: &Style,
) {
let wire_gap = layout.wire_gap;
let gate_size = layout.gate_size;
if !controls.is_empty() {
let (first, last) = occupied_bounds(targets, controls);
canvas.line(
x,
-(first as f32) * wire_gap,
x,
-(last as f32) * wire_gap,
&line_attributes(style),
);
for control in controls {
let y = -(control.wire as f32) * wire_gap;
if let Some(parity) = control.parity {
draw_parity_control(
canvas,
x,
y,
gate_size,
parity,
style,
layout.orientation == Orientation::Vertical,
);
} else {
canvas.circle(x, y, 2.2, &marker_attributes(style, control.positive));
}
}
}
if targets.len() > 1 {
let first = *targets.iter().min().expect("gate has a target");
let last = *targets.iter().max().expect("gate has a target");
let midpoint = -((first + last) as f32) * wire_gap / 2.0;
let span = (last - first) as f32 * wire_gap * PIXELS_PER_CENTIMETER + gate_size;
let height = span.max(style.height.unwrap_or(0.0));
let mut node_style = style.clone();
node_style.height = None;
draw_node(canvas, x, midpoint, label, gate_size, height, &node_style);
return;
}
let y = -(targets[0] as f32) * wire_gap;
if !controls.is_empty() && label == "X" && style.link.is_none() {
canvas.circle(x, y, 4.0, &marker_attributes(style, false));
let attributes = line_attributes(style);
canvas.line(x - 0.14, y, x + 0.14, y, &attributes);
canvas.line(x, y - 0.14, x, y + 0.14, &attributes);
} else if !controls.is_empty() && label == "Z" && style.link.is_none() {
canvas.circle(x, y, 2.2, &marker_attributes(style, true));
} else {
draw_node(canvas, x, y, label, gate_size, gate_size, style);
}
}
fn draw_parity_control(
canvas: &mut Canvas,
x: f32,
y: f32,
gate_size: f32,
parity: ParityBasis,
style: &Style,
vertical: bool,
) {
let width = style.width.unwrap_or(gate_size * 0.75).max(15.0);
let height = style.height.unwrap_or(gate_size * 0.9).max(18.0);
let (draw_width, draw_height) = if vertical {
(height, width)
} else {
(width, height)
};
canvas.rectangle(
x - draw_width / (2.0 * PIXELS_PER_CENTIMETER),
y + draw_height / (2.0 * PIXELS_PER_CENTIMETER),
x + draw_width / (2.0 * PIXELS_PER_CENTIMETER),
y - draw_height / (2.0 * PIXELS_PER_CENTIMETER),
0.0,
&node_attributes(style),
);
let mut label_style = style.clone();
label_style.fill = None;
label_style.link = None;
let (axis_x, axis_y, suffix_x, suffix_y) = if vertical {
(x - 0.10, y, x + 0.11, y)
} else {
(x, y + 0.10, x, y - 0.11)
};
canvas.text_sized(
axis_x,
axis_y,
parity.label(),
Anchor::Center,
&label_style,
false,
8.0,
);
label_style.stroke = Some("red".into());
canvas.text_sized(
suffix_x,
suffix_y,
"par",
Anchor::Center,
&label_style,
false,
6.0,
);
}
fn draw_node(
canvas: &mut Canvas,
x: f32,
y: f32,
label: &str,
minimum_width: f32,
minimum_height: f32,
style: &Style,
) {
let label_size = label_dimensions(label);
let width = style
.width
.unwrap_or(minimum_width)
.max(label_size.width * PIXELS_PER_CENTIMETER + 2.0 * TEXT_PADDING);
let height = style
.height
.unwrap_or(minimum_height)
.max(label_size.height * PIXELS_PER_CENTIMETER + 2.0 * TEXT_PADDING);
if style.shape != Some(Shape::None) {
let attributes = node_attributes(style);
match style.shape {
Some(Shape::Circle) => {
let radius = width.max(height) / 2.0;
canvas.circle(x, y, radius, &attributes);
}
Some(Shape::Ellipse) => {
canvas.ellipse(x, y, width / 2.0, height / 2.0, &attributes);
}
Some(Shape::Box | Shape::None) | None => {
let half_width = width / (2.0 * PIXELS_PER_CENTIMETER);
let half_height = height / (2.0 * PIXELS_PER_CENTIMETER);
canvas.rectangle(
x - half_width,
y - half_height,
x + half_width,
y + half_height,
0.0,
&attributes,
);
}
}
}
let mut label_style = style.clone();
label_style.fill = None;
canvas.text(x, y, label, Anchor::Center, &label_style, false);
}
fn draw_measurement(
canvas: &mut Canvas,
x: f32,
layout: &Layout,
target: usize,
label: Option<&str>,
shape: MeasurementShape,
style: &Style,
) {
let y = -(target as f32) * layout.wire_gap;
let label_size = label.map(label_dimensions);
let height = (style.height.unwrap_or(layout.gate_size) / PIXELS_PER_CENTIMETER)
.max(label_size.map_or(0.0, |size| size.height + 2.0 * LABEL_PADDING));
let nose = height / 2.0;
let width = (style.width.unwrap_or(layout.gate_size) / PIXELS_PER_CENTIMETER)
.max(label_size.map_or(0.0, |size| size.width + 2.0 * LABEL_PADDING + nose));
let (left, right) = (x - width / 2.0, x + width / 2.0);
let (top, bottom) = (y + height / 2.0, y - height / 2.0);
if let Some(label) = label {
let outline = marker_attributes(style, false);
match shape {
MeasurementShape::D => {
let arc_x = right - height / 2.0;
let start = canvas.point(left, bottom);
let arc_start = canvas.point(arc_x, bottom);
let arc_end = canvas.point(arc_x, top);
let corner = canvas.point(left, top);
let radius = height / 2.0 * PIXELS_PER_CENTIMETER;
let data = format!(
"M {:.3} {:.3} L {:.3} {:.3} A {radius:.3} {radius:.3} 0 0 0 {:.3} {:.3} L {:.3} {:.3} Z",
start.0,
start.1,
arc_start.0,
arc_start.1,
arc_end.0,
arc_end.1,
corner.0,
corner.1
);
canvas.path(&data, &outline);
}
MeasurementShape::Tag => {
let point = (height / 2.0).min(width / 3.0);
let corners = [
(left, y),
(left + point, top),
(right, top),
(right, bottom),
(left + point, bottom),
]
.map(|(px, py)| canvas.point(px, py));
let mut data = format!("M {:.3} {:.3}", corners[0].0, corners[0].1);
for corner in &corners[1..] {
let _ = write!(data, " L {:.3} {:.3}", corner.0, corner.1);
}
data.push_str(" Z");
canvas.path(&data, &outline);
}
}
let mut label_style = style.clone();
label_style.fill = None;
let text_x = match shape {
MeasurementShape::D => x - nose / 2.0,
MeasurementShape::Tag => x + nose / 2.0,
};
canvas.text(text_x, y, label, Anchor::Center, &label_style, false);
return;
}
canvas.rectangle(
left,
bottom,
right,
top,
0.0,
&marker_attributes(style, false),
);
let base = y - 0.10;
let arc_start = canvas.point(x - 0.22, base);
let arc_end = canvas.point(x + 0.22, base);
let arc_radius = 0.22 * PIXELS_PER_CENTIMETER;
let arc_attributes = line_attributes(style);
canvas.path(
&format!(
"M {:.3} {:.3} A {arc_radius:.3} {arc_radius:.3} 0 0 1 {:.3} {:.3}",
arc_start.0, arc_start.1, arc_end.0, arc_end.1
),
&arc_attributes,
);
draw_needle(canvas, x, base, x + 0.17, y + 0.12, style);
}
fn draw_needle(canvas: &mut Canvas, x1: f32, y1: f32, x2: f32, y2: f32, style: &Style) {
const BARB: f32 = 0.08;
const COS: f32 = 0.906;
const SIN: f32 = 0.423;
let attributes = line_attributes(style);
canvas.line(x1, y1, x2, y2, &attributes);
let (dx, dy) = (x1 - x2, y1 - y2);
let length = dx.hypot(dy);
if length == 0.0 {
return;
}
let (dx, dy) = (dx / length * BARB, dy / length * BARB);
for sign in [1.0, -1.0_f32] {
let sin = SIN * sign;
canvas.line(
x2,
y2,
x2 + dx * COS - dy * sin,
y2 + dx * sin + dy * COS,
&attributes,
);
}
}
fn draw_value_transition(
canvas: &mut Canvas,
x: f32,
y: f32,
label: &str,
kind: WireKind,
style: &Style,
background: &str,
) {
let label_size = label_has_math(label).then(|| label_dimensions(label));
let width = style
.width
.map_or(0.48, |width| width / PIXELS_PER_CENTIMETER)
.max(label_size.map_or(0.0, |size| size.width + 2.0 * LABEL_PADDING));
let height = style
.height
.map_or(0.34, |height| height / PIXELS_PER_CENTIMETER)
.max(label_size.map_or(0.0, |size| size.height + 2.0 * LABEL_PADDING));
let (left, right) = (x - width / 2.0, x + width / 2.0);
let (top, bottom) = (y + height / 2.0, y - height / 2.0);
let mut fill_attributes = format!(
" fill=\"{}\" stroke=\"none\"",
svg_color(style.fill.as_deref().unwrap_or(background))
);
push_opacity(&mut fill_attributes, style);
canvas.rectangle(left, bottom, right, top, 0.0, &fill_attributes);
let edge = if kind == WireKind::Hidden {
left
} else {
right
};
canvas.line(edge, bottom, edge, top, &line_attributes(style));
let mut label_style = style.clone();
label_style.fill = None;
label_style.shape = None;
canvas.text(x, y, label, Anchor::Center, &label_style, false);
}
#[allow(clippy::too_many_arguments)]
fn draw_brace(
canvas: &mut Canvas,
x: f32,
wire_gap: f32,
first: usize,
last: usize,
label: &str,
side: BraceSide,
style: &Style,
background: &str,
) {
let top = -(first as f32) * wire_gap + 0.34;
let bottom = -(last as f32) * wire_gap - 0.34;
let reach = (label_width(label) / 2.0 + LABEL_PADDING).max(0.22);
for (offset, mirror) in match side {
BraceSide::Left => [Some((-reach, false)), None],
BraceSide::Right => [Some((reach, true)), None],
BraceSide::Both => [Some((-reach, false)), Some((reach, true))],
}
.into_iter()
.flatten()
{
let mut attributes = format!(
" fill=\"none\" stroke=\"{}\" stroke-width=\"{STROKE_WIDTH}\"",
svg_color(style.stroke.as_deref().unwrap_or("black"))
);
push_opacity(&mut attributes, style);
draw_curly_brace(canvas, x + offset, top, bottom, mirror, &attributes);
}
let label_style = masked_style(style, background);
canvas.text(
x,
(top + bottom) / 2.0,
label,
Anchor::Center,
&label_style,
true,
);
}
fn draw_curly_brace(
canvas: &mut Canvas,
x: f32,
top: f32,
bottom: f32,
mirror: bool,
attributes: &str,
) {
const AMPLITUDE: f32 = 4.0;
let reach = if mirror { -AMPLITUDE } else { AMPLITUDE } / 2.0;
let (spine_x, top_y) = canvas.point(x, top);
let (_, bottom_y) = canvas.point(x, bottom);
canvas.cover(spine_x - 2.0 * reach, top_y);
canvas.cover(spine_x - 2.0 * reach, bottom_y);
let middle_y = (top_y + bottom_y) / 2.0;
let data = format!(
"M {spine_x:.3} {top_y:.3} \
Q {:.3} {top_y:.3} {:.3} {:.3} \
T {:.3} {middle_y:.3} \
Q {:.3} {middle_y:.3} {:.3} {:.3} \
T {spine_x:.3} {bottom_y:.3}",
spine_x - reach,
spine_x - reach,
(top_y + middle_y) / 2.0,
spine_x - 2.0 * reach,
spine_x - reach,
spine_x - reach,
(middle_y + bottom_y) / 2.0
);
canvas.path(&data, attributes);
}
fn draw_cross(canvas: &mut Canvas, x: f32, y: f32, style: &Style) {
let attributes = line_attributes(style);
canvas.line(x - 0.11, y - 0.11, x + 0.11, y + 0.11, &attributes);
canvas.line(x - 0.11, y + 0.11, x + 0.11, y - 0.11, &attributes);
}
fn line_attributes(style: &Style) -> String {
let mut attributes = format!(
" fill=\"none\" stroke=\"{}\" stroke-width=\"{STROKE_WIDTH}\"",
svg_color(style.stroke.as_deref().unwrap_or("black"))
);
push_dash(&mut attributes, style);
push_opacity(&mut attributes, style);
attributes
}
fn marker_attributes(style: &Style, filled: bool) -> String {
let stroke = style.stroke.as_deref().unwrap_or("black");
let fill = style
.fill
.as_deref()
.unwrap_or(if filled { stroke } else { "white" });
let mut attributes = format!(
" fill=\"{}\" stroke=\"{}\" stroke-width=\"{STROKE_WIDTH}\"",
svg_color(fill),
svg_color(stroke)
);
push_dash(&mut attributes, style);
push_opacity(&mut attributes, style);
attributes
}
fn node_attributes(style: &Style) -> String {
let mut attributes = format!(
" fill=\"{}\" stroke=\"{}\" stroke-width=\"{STROKE_WIDTH}\"",
svg_color(style.fill.as_deref().unwrap_or("white")),
svg_color(style.stroke.as_deref().unwrap_or("black"))
);
push_dash(&mut attributes, style);
push_opacity(&mut attributes, style);
attributes
}
fn push_dash(attributes: &mut String, style: &Style) {
if style.dashed {
attributes.push_str(" stroke-dasharray=\"3 2\"");
}
}
fn push_opacity(attributes: &mut String, style: &Style) {
if let Some(opacity) = style.opacity {
let _ = write!(attributes, " opacity=\"{opacity:.3}\"");
}
}
fn svg_color(color: &str) -> &str {
match color {
"black" => "#000000",
"white" => "#FFFFFF",
"gray" => "#808080",
"red" => "#FF0000",
"green" => "#00FF00",
"blue" => "#0000FF",
"teal" => "#008080",
"purple" => "#BF0040",
"orange" => "#FF8000",
"yellow" => "#FFFF00",
"olive" => "#808000",
"lime" => "#BFFF00",
other => other,
}
}
fn escape_text(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for character in value.chars() {
match character {
'&' => escaped.push_str("&"),
'<' => escaped.push_str("<"),
'>' => escaped.push_str(">"),
'"' => escaped.push_str("""),
'\'' => escaped.push_str("'"),
'\n' | '\r' => escaped.push(' '),
_ => escaped.push(character),
}
}
escaped
}