use bounding_box::BoundingBox;
use crate::Rotation2;
use crate::composite::Composite;
use crate::contour::Contour;
use crate::polysegment::Polysegment;
use crate::prelude::ArrowHeadSize;
use crate::segment::{ArcSegment, LineSegment, Segment};
use crate::shape::Shape;
impl Segment {
pub fn draw(&self, style: &Style, context: &cairo::Context) -> Result<(), cairo::Error> {
match self {
Segment::LineSegment(s) => s.draw(style, context),
Segment::ArcSegment(s) => s.draw(style, context),
}
}
}
impl ArcSegment {
pub fn draw(&self, style: &Style, context: &cairo::Context) -> Result<(), cairo::Error> {
let vertex = self.start();
context.move_to(vertex[0], vertex[1]);
let radius = self.radius();
let center = self.center();
let start_angle = self.start_angle();
let stop_angle = self.stop_angle();
if self.is_positive() {
context.arc(center[0], center[1], radius, start_angle, stop_angle);
} else {
context.arc_negative(center[0], center[1], radius, start_angle, stop_angle);
}
stroke_line(style, context)?;
if let Some(txt) = &style.text {
let bb = if txt.anchor == crate::draw::Anchor::Centroid {
let c = self.centroid();
BoundingBox::new(c[0], c[0], c[1], c[1])
} else {
BoundingBox::from(self)
};
txt.draw_with_bounding_box::<true>(context, &bb)?;
}
return Ok(());
}
}
impl LineSegment {
pub fn draw(&self, style: &Style, context: &cairo::Context) -> Result<(), cairo::Error> {
context.move_to(self.start()[0], self.start()[1]);
context.line_to(self.stop()[0], self.stop()[1]);
stroke_line(style, context)?;
if let Some(txt) = &style.text {
let bb = if txt.anchor == crate::draw::Anchor::Centroid {
let c = self.centroid();
BoundingBox::new(c[0], c[0], c[1], c[1])
} else {
BoundingBox::from(self)
};
txt.draw_with_bounding_box::<true>(context, &bb)?;
}
return Ok(());
}
}
#[doc = ""]
#[cfg_attr(feature = "doc-images", doc = "![Grid][example_grid]")]
#[cfg_attr(
feature = "doc-images",
embed_doc_image::embed_doc_image("example_grid", "docs/img/example_grid.svg")
)]
#[cfg_attr(
not(feature = "doc-images"),
doc = "**Doc images not enabled**. Compile docs with `cargo doc --features 'doc-images'` and Rust version >= 1.54."
)]
pub fn grid(
bounding_box: &BoundingBox,
spacing: f64,
offset: [f64; 2],
style: &Style,
context: &cairo::Context,
) -> Result<(), cairo::Error> {
fn draw_line(
x1: f64,
y1: f64,
x2: f64,
y2: f64,
style: &Style,
context: &cairo::Context,
) -> Result<(), cairo::Error> {
context.move_to(x1, y1);
context.line_to(x2, y2);
stroke_line(style, context)
}
if spacing <= 0.0 {
return Ok(());
}
let o_x = offset[0].rem_euclid(spacing);
let o_y = offset[1].rem_euclid(spacing);
let xmin = bounding_box.xmin() - spacing;
let xmax = bounding_box.xmax() + spacing;
let ymin = bounding_box.ymin() - spacing;
let ymax = bounding_box.ymax() + spacing;
let mut x = (xmin / spacing).ceil() * spacing + o_x;
while x <= xmax {
draw_line(x, ymin, x, ymax, style, context)?;
x += spacing;
}
let mut y = (ymin / spacing).ceil() * spacing + o_y;
while y <= ymax {
draw_line(xmin, y, xmax, y, style, context)?;
y += spacing;
}
return Ok(());
}
#[doc = ""]
#[cfg_attr(feature = "doc-images", doc = "![Coordinate system][example_cs]")]
#[cfg_attr(
feature = "doc-images",
embed_doc_image::embed_doc_image("example_cs", "docs/img/example_cs.svg")
)]
#[cfg_attr(
not(feature = "doc-images"),
doc = "**Doc images not enabled**. Compile docs with `cargo doc --features 'doc-images'` and Rust version >= 1.54."
)]
pub fn coordinate_system(
origin: [f64; 2],
x: f64,
y: f64,
arrow_head_size: ArrowHeadSize,
style: &Style,
context: &cairo::Context,
) -> Result<(), cairo::Error> {
let arrow_height = arrow_head_size.height();
let side_length = arrow_head_size.side_length();
let h = if x > 0.0 { arrow_height } else { -arrow_height };
context.move_to(origin[0], origin[1]);
context.line_to(origin[0] + x - h, origin[1]);
context.line_to(origin[0] + x - h, origin[1] + 0.5 * side_length);
context.line_to(origin[0] + x, origin[1]);
context.line_to(origin[0] + x - h, origin[1] - 0.5 * side_length);
context.line_to(origin[0] + x - h, origin[1]);
context.close_path();
let fc = &style.background_color;
context.set_source_rgba(fc.r.into(), fc.g.into(), fc.b.into(), fc.a.into());
context.fill_preserve()?;
stroke_line(style, context)?;
let h = if y > 0.0 { arrow_height } else { -arrow_height };
context.move_to(origin[0], origin[1]);
context.line_to(origin[0], origin[1] + y - h);
context.line_to(origin[0] + 0.5 * side_length, origin[1] + y - h);
context.line_to(origin[0], origin[1] + y);
context.line_to(origin[0] - 0.5 * side_length, origin[1] + y - h);
context.line_to(origin[0], origin[1] + y - h);
context.close_path();
let fc = &style.background_color;
context.set_source_rgba(fc.r.into(), fc.g.into(), fc.b.into(), fc.a.into());
context.fill_preserve()?;
stroke_line(style, context)?;
return Ok(());
}
fn stroke_line(style: &Style, context: &cairo::Context) -> Result<(), cairo::Error> {
match &style.line_style {
LineStyle::None => {}
LineStyle::Dotted => {
context.set_dash([1.0, 3.0].as_slice(), 0.0);
}
LineStyle::Dashed { pattern, offset } => {
context.set_dash(pattern.as_slice(), *offset);
}
LineStyle::Solid => {
context.set_dash([].as_slice(), 0.0);
}
};
if let LineStyle::None = style.line_style {
} else {
let lc = &style.line_color;
context.set_line_width(style.line_width);
context.set_source_rgba(lc.r.into(), lc.g.into(), lc.b.into(), lc.a.into());
context.set_line_cap(style.line_cap);
context.save()?;
context.identity_matrix();
context.stroke()?;
context.restore()?;
}
return Ok(());
}
impl Polysegment {
pub fn draw(&self, style: &Style, context: &cairo::Context) -> Result<(), cairo::Error> {
return self.draw_stroking::<false, false>(style, context);
}
fn draw_stroking<const CLOSE: bool, const FILL: bool>(
&self,
style: &Style,
context: &cairo::Context,
) -> Result<(), cairo::Error> {
self.draw_without_stroking::<CLOSE, FILL>(&style, context)?;
if let LineStyle::None = style.line_style {
} else {
let lc = &style.line_color;
context.set_line_width(style.line_width);
context.set_source_rgba(lc.r.into(), lc.g.into(), lc.b.into(), lc.a.into());
context.set_line_cap(style.line_cap);
context.set_line_join(style.line_join);
context.save()?;
context.identity_matrix();
context.stroke()?;
context.restore()?;
}
if let Some(txt) = &style.text {
let bb = if txt.anchor == crate::draw::Anchor::Centroid {
let c = self.centroid();
BoundingBox::new(c[0], c[0], c[1], c[1])
} else {
BoundingBox::from(self)
};
txt.draw_with_bounding_box::<true>(context, &bb)?;
}
return Ok(());
}
fn draw_without_stroking<const CLOSE: bool, const FILL: bool>(
&self,
style: &Style,
context: &cairo::Context,
) -> Result<(), cairo::Error> {
if let Some(first_segment) = self.front() {
match &style.line_style {
LineStyle::None => {}
LineStyle::Dotted => {
context.set_dash([1.0, 3.0].as_slice(), 0.0);
}
LineStyle::Dashed { pattern, offset } => {
context.set_dash(pattern.as_slice(), *offset);
}
LineStyle::Solid => {
context.set_dash([].as_slice(), 0.0);
}
};
let vertex = first_segment.start();
context.move_to(vertex[0], vertex[1]);
for segment in self.segments() {
match segment {
Segment::LineSegment(ls) => context.line_to(ls.stop()[0], ls.stop()[1]),
Segment::ArcSegment(arc) => {
let radius = arc.radius();
let center = arc.center();
let start_angle = arc.start_angle();
let stop_angle = arc.stop_angle();
if arc.is_positive() {
context.arc(center[0], center[1], radius, start_angle, stop_angle);
} else {
context.arc_negative(
center[0],
center[1],
radius,
start_angle,
stop_angle,
);
}
}
}
}
if CLOSE {
context.close_path();
if FILL {
let fc = &style.background_color;
context.set_source_rgba(fc.r.into(), fc.g.into(), fc.b.into(), fc.a.into());
context.fill_preserve()?;
}
}
}
return Ok(());
}
}
impl Contour {
pub fn draw(&self, style: &Style, context: &cairo::Context) -> Result<(), cairo::Error> {
return self
.polysegment()
.draw_stroking::<true, true>(style, context);
}
}
impl Shape {
pub fn draw(&self, style: &Style, context: &cairo::Context) -> Result<(), cairo::Error> {
let draw_contour = if let LineStyle::None = style.line_style {
false
} else {
true
};
let fill_contour = style.background_color.a != 0.0;
if draw_contour || fill_contour {
self.contour()
.polysegment()
.draw_without_stroking::<true, false>(style, context)?;
for hole in self.holes() {
context.new_sub_path();
hole.polysegment()
.draw_without_stroking::<true, false>(style, context)?;
}
if fill_contour {
context.set_fill_rule(cairo::FillRule::EvenOdd);
let fc = &style.background_color;
context.set_source_rgba(fc.r.into(), fc.g.into(), fc.b.into(), fc.a.into());
context.fill_preserve()?;
}
if draw_contour {
let lc = &style.line_color;
context.set_line_width(style.line_width);
context.set_source_rgba(lc.r.into(), lc.g.into(), lc.b.into(), lc.a.into());
context.set_line_cap(style.line_cap);
context.set_line_join(style.line_join);
context.save()?;
context.identity_matrix();
context.stroke()?;
context.restore()?;
}
}
if let Some(txt) = &style.text {
let bb = if txt.anchor == crate::draw::Anchor::Centroid {
let c = self.centroid();
BoundingBox::new(c[0], c[0], c[1], c[1])
} else {
BoundingBox::from(self)
};
txt.draw_with_bounding_box::<true>(context, &bb)?;
}
return Ok(());
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
return Self { r, g, b, a };
}
pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: a as f32 / 255.0,
}
}
}
#[derive(Debug, Clone)]
pub struct Style {
pub line_color: Color,
pub background_color: Color,
pub line_width: f64,
pub line_style: LineStyle,
pub line_cap: cairo::LineCap,
pub line_join: cairo::LineJoin,
pub text: Option<Box<Text>>,
}
impl Style {
pub fn new(
line_color: Color,
background_color: Color,
line_width: f64,
line_style: LineStyle,
line_cap: cairo::LineCap,
line_join: cairo::LineJoin,
text: Option<Box<Text>>,
) -> Self {
return Style {
line_color,
background_color,
line_width,
line_style,
line_cap,
line_join,
text,
};
}
}
impl Default for Style {
fn default() -> Self {
let black = Color::new(0.0, 0.0, 0.0, 1.0);
let white = Color::new(1.0, 1.0, 1.0, 1.0);
return Style {
line_color: black,
background_color: white,
line_width: 0.5,
line_style: LineStyle::Solid,
line_cap: cairo::LineCap::Round,
line_join: cairo::LineJoin::Miter,
text: None,
};
}
}
#[doc = ""]
#[cfg_attr(feature = "doc-images", doc = "![Line styles][line_styles]")]
#[cfg_attr(
feature = "doc-images",
embed_doc_image::embed_doc_image("line_styles", "docs/img/line_styles.svg")
)]
#[cfg_attr(
not(feature = "doc-images"),
doc = "**Doc images not enabled**. Compile docs with
`cargo doc --features 'doc-images'` and Rust version >= 1.54."
)]
#[derive(Debug, Clone)]
pub enum LineStyle {
None,
Dotted,
Dashed {
pattern: Vec<f64>,
offset: f64,
},
Solid,
}
impl LineStyle {
pub fn default_dashed() -> Self {
return LineStyle::Dashed {
pattern: vec![4.0, 4.0],
offset: 0.0,
};
}
}
#[doc = ""]
#[cfg_attr(
feature = "doc-images",
doc = "![Unscaled][anchor_offset_scale_1] ![Scaled][anchor_offset_scale_2]"
)]
#[cfg_attr(
feature = "doc-images",
embed_doc_image::embed_doc_image(
"anchor_offset_scale_1",
"docs/img/anchor_offset_scale_1.svg"
)
)]
#[cfg_attr(
feature = "doc-images",
embed_doc_image::embed_doc_image(
"anchor_offset_scale_2",
"docs/img/anchor_offset_scale_2.svg"
)
)]
#[cfg_attr(
not(feature = "doc-images"),
doc = "**Doc images not enabled**. Compile docs with
`cargo doc --features 'doc-images'` and Rust version >= 1.54."
)]
#[derive(Debug, Clone)]
pub struct Text {
pub text: String,
pub anchor: Anchor,
pub fixed_anchor_offset: [f64; 2],
pub scaled_anchor_offset: [f64; 2],
pub color: Color,
pub font_size: f64,
pub angle: f64,
}
impl Text {
pub fn new(
text: String,
anchor: Anchor,
fixed_anchor_offset: [f64; 2],
scaled_anchor_offset: [f64; 2],
color: Color,
font_size: f64,
angle: f64,
) -> Self {
return Text {
text,
anchor,
fixed_anchor_offset,
scaled_anchor_offset,
color,
font_size,
angle,
};
}
pub fn draw(&self, context: &cairo::Context) -> Result<(), cairo::Error> {
let bb = BoundingBox::new(0.0, 0.0, 0.0, 0.0);
return self.draw_with_bounding_box::<false>(context, &bb);
}
pub(crate) fn draw_with_bounding_box<const INSIDE_BB: bool>(
&self,
context: &cairo::Context,
bounding_box: &BoundingBox,
) -> Result<(), cairo::Error> {
let bb = bounding_box;
let xmean = (bb.xmin() + bb.xmax()) / 2.0;
let ymean = (bb.ymin() + bb.ymax()) / 2.0;
context.set_source_rgba(
self.color.r.into(),
self.color.g.into(),
self.color.b.into(),
self.color.a.into(),
);
let font_size = self.font_size / context.matrix().xx(); context.set_font_size(font_size);
let extents = context.text_extents(&self.text)?;
let x: f64;
let y: f64;
let anchor = if INSIDE_BB {
self.anchor
} else {
self.anchor.opposite()
};
match anchor {
Anchor::Center | Anchor::Centroid => {
x = xmean - (extents.width() * 0.5 + extents.x_bearing());
y = ymean - (extents.height() * 0.5 + extents.y_bearing());
}
Anchor::Top => {
x = xmean - (extents.width() * 0.5 + extents.x_bearing());
y = bb.ymin() - extents.y_bearing();
}
Anchor::TopRight => {
x = bb.xmax() - (extents.width() + extents.x_bearing());
y = bb.ymin() - extents.y_bearing();
}
Anchor::Right => {
x = bb.xmax() - (extents.width() + extents.x_bearing());
y = ymean - (extents.height() * 0.5 + extents.y_bearing());
}
Anchor::BottomRight => {
x = bb.xmax() - (extents.width() + extents.x_bearing());
y = bb.ymax();
}
Anchor::Bottom => {
x = xmean - (extents.width() * 0.5 + extents.x_bearing());
y = bb.ymax();
}
Anchor::BottomLeft => {
x = bb.xmin();
y = bb.ymax();
}
Anchor::Left => {
x = bb.xmin();
y = ymean - (extents.height() * 0.5 + extents.y_bearing());
}
Anchor::TopLeft => {
x = bb.xmin();
y = bb.ymin() - extents.y_bearing();
}
}
let offset_x =
self.fixed_anchor_offset[0] / context.matrix().xx() + self.scaled_anchor_offset[0];
let offset_y =
self.fixed_anchor_offset[1] / context.matrix().yy() + self.scaled_anchor_offset[1];
if self.angle == 0.0 {
context.new_path();
context.move_to(x + offset_x, y + offset_y);
context.show_text(&self.text)?;
} else {
let x0 = context.matrix().x0() / context.matrix().xx();
let y0 = context.matrix().y0() / context.matrix().yy();
let text_box_ll = [x + offset_x + x0, y + offset_y + y0];
let center_text_box = [
text_box_ll[0] + 0.5 * extents.width(),
text_box_ll[1] - 0.5 * extents.height(),
];
let delta_ll_to_center = [-0.5 * extents.width(), 0.5 * extents.height()];
let center_text_box_rot = Rotation2::new(-self.angle) * center_text_box;
context.save()?;
context.translate(-x0, -y0); context.rotate(self.angle);
context.move_to(
center_text_box_rot[0] + delta_ll_to_center[0],
center_text_box_rot[1] + delta_ll_to_center[1],
);
context.show_text(&self.text)?;
context.restore()?;
}
return Ok(());
}
}
#[doc = ""]
#[cfg_attr(
feature = "doc-images",
doc = "![Text placement options][text_placement]"
)]
#[cfg_attr(
feature = "doc-images",
embed_doc_image::embed_doc_image("text_placement", "docs/img/text_placement.svg")
)]
#[cfg_attr(
not(feature = "doc-images"),
doc = "**Doc images not enabled**. Compile docs with
`cargo doc --features 'doc-images'` and Rust version >= 1.54."
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Anchor {
Centroid,
Center,
TopLeft,
Top,
TopRight,
Right,
BottomRight,
Bottom,
BottomLeft,
Left,
}
impl Anchor {
pub fn opposite(&self) -> Self {
match self {
Anchor::Centroid => Anchor::Centroid,
Anchor::Center => Anchor::Center,
Anchor::TopLeft => Anchor::BottomRight,
Anchor::Top => Anchor::Bottom,
Anchor::TopRight => Anchor::BottomLeft,
Anchor::Right => Anchor::Left,
Anchor::BottomRight => Anchor::TopLeft,
Anchor::Bottom => Anchor::Top,
Anchor::BottomLeft => Anchor::TopRight,
Anchor::Left => Anchor::Right,
}
}
}