use crate::core::{Affine2D, Bbox, Path, color::Rgba};
use crate::render::{CapStyle, GraphicsContext, JoinStyle, Renderer};
use crate::artist::Artist;
const DEFAULT_FACECOLOR: Rgba = Rgba::new(0.121_568_63, 0.466_666_67, 0.705_882_35, 1.0);
const WEDGE_ARC_SAMPLES: usize = 60;
#[derive(Debug, Clone, PartialEq)]
pub struct Patch {
path: Path,
facecolor: Option<Rgba>,
edgecolor: Option<Rgba>,
linewidth: f64,
dashes: Option<(f64, Vec<f64>)>,
cap: CapStyle,
join: JoinStyle,
visible: bool,
zorder: f64,
label: Option<String>,
}
impl Patch {
#[must_use]
pub fn new(path: Path) -> Self {
Self {
path,
facecolor: Some(DEFAULT_FACECOLOR),
edgecolor: Some(Rgba::BLACK),
linewidth: 1.0,
dashes: None,
cap: CapStyle::Butt,
join: JoinStyle::Miter,
visible: true,
zorder: 1.0,
label: None,
}
}
#[must_use]
pub fn rectangle(x: f64, y: f64, w: f64, h: f64) -> Self {
let points = [[x, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y]];
Self::new(Path::from_polyline(&points))
}
#[must_use]
pub fn polygon(points: &[[f64; 2]]) -> Self {
let mut verts = points.to_vec();
if let Some(first) = verts
.first()
.copied()
.filter(|first| verts.last() != Some(first))
{
verts.push(first);
}
Self::new(Path::from_polyline(&verts))
}
#[must_use]
pub fn circle(center: [f64; 2], r: f64) -> Self {
let [cx, cy] = center;
let transform = Affine2D::from_scale(r, r).translate(cx, cy);
Self::new(Path::unit_circle().transformed(&transform))
}
#[must_use]
pub fn ellipse(center: [f64; 2], width: f64, height: f64) -> Self {
let [cx, cy] = center;
let transform = Affine2D::from_scale(width / 2.0, height / 2.0).translate(cx, cy);
Self::new(Path::unit_circle().transformed(&transform))
}
#[must_use]
pub fn regular_polygon(
center: [f64; 2],
num_sides: usize,
radius: f64,
rotation_rad: f64,
) -> Self {
let [cx, cy] = center;
let transform = Affine2D::from_rotation(rotation_rad)
.scale(radius, radius)
.translate(cx, cy);
Self::new(Path::unit_regular_polygon(num_sides).transformed(&transform))
}
#[must_use]
pub fn wedge(center: [f64; 2], r: f64, theta1_deg: f64, theta2_deg: f64) -> Self {
let [cx, cy] = center;
let theta1 = theta1_deg.to_radians();
let theta2 = theta2_deg.to_radians();
let mut verts = Vec::with_capacity(WEDGE_ARC_SAMPLES + 2);
verts.push([cx, cy]);
for i in 0..=WEDGE_ARC_SAMPLES {
let t = theta1 + (theta2 - theta1) * (i as f64 / WEDGE_ARC_SAMPLES as f64);
verts.push([cx + r * t.cos(), cy + r * t.sin()]);
}
verts.push([cx, cy]);
Self::new(Path::from_polyline(&verts))
}
#[must_use]
pub fn arc(center: [f64; 2], r: f64, theta1_deg: f64, theta2_deg: f64) -> Self {
let [cx, cy] = center;
let theta1 = theta1_deg.to_radians();
let theta2 = theta2_deg.to_radians();
let verts: Vec<[f64; 2]> = (0..=WEDGE_ARC_SAMPLES)
.map(|i| {
let t = theta1 + (theta2 - theta1) * (i as f64 / WEDGE_ARC_SAMPLES as f64);
[cx + r * t.cos(), cy + r * t.sin()]
})
.collect();
Self::new(Path::from_polyline(&verts)).facecolor(None)
}
#[must_use]
pub fn face(&self) -> Option<Rgba> {
self.facecolor
}
#[must_use]
pub fn facecolor(mut self, facecolor: Option<Rgba>) -> Self {
self.facecolor = facecolor;
self
}
#[must_use]
pub fn edgecolor(mut self, edgecolor: Option<Rgba>) -> Self {
self.edgecolor = edgecolor;
self
}
#[must_use]
pub fn linewidth(mut self, linewidth: f64) -> Self {
self.linewidth = linewidth;
self
}
#[must_use]
pub fn dashes(mut self, dashes: Option<(f64, Vec<f64>)>) -> Self {
self.dashes = dashes;
self
}
#[must_use]
pub fn with_zorder(mut self, zorder: f64) -> Self {
self.zorder = zorder;
self
}
#[must_use]
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.set_label(label);
self
}
pub fn set_label(&mut self, label: impl Into<String>) -> &mut Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
#[must_use]
pub fn edge(&self) -> Option<Rgba> {
self.edgecolor
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub fn draw_path(&self, renderer: &mut dyn Renderer, path: &Path, transform: &Affine2D) {
if !self.visible {
return;
}
let gc = GraphicsContext {
line_width: self.linewidth,
dashes: self.dashes.clone(),
cap: self.cap,
join: self.join,
stroke: self.edgecolor,
..GraphicsContext::new()
};
renderer.draw_path(&gc, path, transform, self.facecolor);
}
}
impl Artist for Patch {
fn draw(&self, renderer: &mut dyn Renderer, transform: &Affine2D) {
self.draw_path(renderer, &self.path, transform);
}
fn zorder(&self) -> f64 {
self.zorder
}
fn visible(&self) -> bool {
self.visible
}
fn data_extents(&self) -> Option<Bbox> {
Some(self.path.get_extents())
}
}
#[cfg(feature = "portable")]
impl Patch {
pub(crate) fn to_portable(
&self,
bank: &mut crate::portable::data::BankWriter,
) -> crate::portable::spec::PatchSpec {
crate::portable::spec::PatchSpec {
path: self.path.to_portable(bank),
facecolor: self.facecolor,
edgecolor: self.edgecolor,
linewidth: self.linewidth,
dashes: self.dashes.clone(),
cap: self.cap,
join: self.join,
visible: self.visible,
zorder: self.zorder,
label: self.label.clone(),
}
}
pub(crate) fn from_portable(
spec: &crate::portable::spec::PatchSpec,
reader: &crate::portable::data::BankReader<'_>,
) -> Result<Patch, crate::portable::PortableError> {
Ok(Patch {
path: Path::from_portable(&spec.path, reader)?,
facecolor: spec.facecolor,
edgecolor: spec.edgecolor,
linewidth: spec.linewidth,
dashes: spec.dashes.clone(),
cap: spec.cap,
join: spec.join,
visible: spec.visible,
zorder: spec.zorder,
label: spec.label.clone(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct MockRenderer {
calls: Vec<(usize, Option<Rgba>)>,
}
impl Renderer for MockRenderer {
fn draw_path(
&mut self,
_gc: &GraphicsContext,
path: &Path,
_transform: &Affine2D,
fill: Option<Rgba>,
) {
self.calls.push((path.vertices().len(), fill));
}
fn canvas_size(&self) -> (f64, f64) {
(100.0, 100.0)
}
}
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-9, "expected {b}, got {a}");
}
#[test]
fn red_rectangle_draws_once_with_fill() {
let patch = Patch::rectangle(0.0, 0.0, 2.0, 2.0).facecolor(Some(Rgba::RED));
let mut r = MockRenderer::default();
patch.draw(&mut r, &Affine2D::identity());
assert_eq!(r.calls, vec![(5, Some(Rgba::RED))]);
}
#[test]
fn rectangle_data_extents() {
let patch = Patch::rectangle(0.0, 0.0, 2.0, 2.0);
let e = patch.data_extents().expect("non-empty");
approx(e.xmin(), 0.0);
approx(e.ymin(), 0.0);
approx(e.xmax(), 2.0);
approx(e.ymax(), 2.0);
}
#[test]
fn rectangle_is_closed() {
let patch = Patch::rectangle(1.0, 2.0, 3.0, 4.0);
let verts = patch.path().vertices();
assert_eq!(verts.len(), 5);
assert_eq!(verts.first(), verts.last());
}
#[test]
fn invisible_patch_draws_nothing() {
let patch = Patch::rectangle(0.0, 0.0, 1.0, 1.0).with_visible(false);
let mut r = MockRenderer::default();
patch.draw(&mut r, &Affine2D::identity());
assert!(r.calls.is_empty());
}
#[test]
fn default_zorder_is_one() {
let patch = Patch::rectangle(0.0, 0.0, 1.0, 1.0);
assert_eq!(patch.zorder(), 1.0);
}
#[test]
fn with_zorder_sets_trait_zorder() {
let patch = Patch::rectangle(0.0, 0.0, 1.0, 1.0).with_zorder(7.0);
assert_eq!(patch.zorder(), 7.0);
}
#[test]
fn polygon_closes_open_input() {
let patch = Patch::polygon(&[[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]]);
let verts = patch.path().vertices();
assert_eq!(verts.len(), 4);
assert_eq!(verts.first(), verts.last());
}
#[test]
fn circle_extents_match_center_and_radius() {
let patch = Patch::circle([50.0, 50.0], 30.0);
let e = patch.data_extents().expect("non-empty");
approx(e.xmin(), 20.0);
approx(e.xmax(), 80.0);
approx(e.ymin(), 20.0);
approx(e.ymax(), 80.0);
}
#[test]
fn ellipse_uses_full_width_and_height() {
let patch = Patch::ellipse([0.0, 0.0], 4.0, 2.0);
let e = patch.data_extents().expect("non-empty");
approx(e.xmin(), -2.0);
approx(e.xmax(), 2.0);
approx(e.ymin(), -1.0);
approx(e.ymax(), 1.0);
}
#[test]
fn regular_polygon_square_has_four_distinct_corners() {
let patch = Patch::regular_polygon([0.0, 0.0], 4, 1.0, 0.0);
let verts = patch.path().vertices();
assert_eq!(verts.len(), 5);
let corners = &verts[..4];
for i in 0..corners.len() {
for j in (i + 1)..corners.len() {
let d = (corners[i][0] - corners[j][0]).hypot(corners[i][1] - corners[j][1]);
assert!(d > 1e-6, "corners {i} and {j} coincide");
}
}
}
#[test]
fn wedge_stays_within_bounding_quadrant() {
let patch = Patch::wedge([0.0, 0.0], 10.0, 0.0, 90.0);
let e = patch.data_extents().expect("non-empty");
approx(e.xmin(), 0.0);
approx(e.ymin(), 0.0);
approx(e.xmax(), 10.0);
approx(e.ymax(), 10.0);
for &[x, y] in patch.path().vertices() {
assert!(
x.hypot(y) <= 10.0 + 1e-9,
"vertex ({x}, {y}) outside radius"
);
}
}
#[test]
fn arc_vertices_lie_on_the_radius_and_span_the_sweep() {
let patch = Patch::arc([2.0, -1.0], 3.0, 45.0, 180.0);
let verts = patch.path().vertices();
for &[x, y] in verts {
approx((x - 2.0).hypot(y + 1.0), 3.0);
}
let first = verts.first().copied().unwrap();
let last = verts.last().copied().unwrap();
approx(first[0], 2.0 + 3.0 * 45f64.to_radians().cos());
approx(first[1], -1.0 + 3.0 * 45f64.to_radians().sin());
approx(last[0], 2.0 - 3.0); approx(last[1], -1.0);
}
#[test]
fn arc_defaults_to_unfilled_stroke_only() {
let patch = Patch::arc([0.0, 0.0], 1.0, 0.0, 90.0);
let mut r = MockRenderer::default();
patch.draw(&mut r, &Affine2D::identity());
assert_eq!(r.calls.len(), 1);
assert_eq!(r.calls[0].1, None);
}
}