use crate::brush::Brush;
use crate::geometry::{Affine, Point, Rect};
use crate::path::FillRule;
use crate::primitives::{polygon as polygon_path, rect as rect_path, rounded_rect, PolygonOptions};
use crate::scene::SceneBuilder;
use super::resolve::{
draw_stroke_with_linetype, override_alpha, pt_to_px, resolve_angle_channel,
resolve_cap_channel, resolve_color_channel_or_theme, resolve_join_channel,
resolve_linetype_channel, resolve_number_channel, resolve_number_channel_or, resolve_pick_id,
resolve_position,
};
use super::state::{finalize_state, require_x_and_siblings, GeomState, KeysStrategy};
use super::{BuildableGeom, Channel, ExpectedOutput, Geom, GeomBuilder, GeomContext};
const DEFAULT_X_BAND: f64 = -0.5;
const DEFAULT_X2_BAND: f64 = 0.5;
const DEFAULT_Y_BAND: f64 = 0.0;
const DEFAULT_Y2_BAND: f64 = 0.0;
const CHANNELS: &[(&str, ExpectedOutput)] = &[
("x", ExpectedOutput::Numbers),
("y", ExpectedOutput::Numbers),
("x2", ExpectedOutput::Numbers),
("y2", ExpectedOutput::Numbers),
("x_offset", ExpectedOutput::Numbers),
("y_offset", ExpectedOutput::Numbers),
("x2_offset", ExpectedOutput::Numbers),
("y2_offset", ExpectedOutput::Numbers),
("x_band", ExpectedOutput::Numbers),
("y_band", ExpectedOutput::Numbers),
("x2_band", ExpectedOutput::Numbers),
("y2_band", ExpectedOutput::Numbers),
("corner_radius", ExpectedOutput::Numbers),
("fill", ExpectedOutput::Colors),
("stroke", ExpectedOutput::Colors),
("fill_opacity", ExpectedOutput::Numbers),
("stroke_opacity", ExpectedOutput::Numbers),
("linewidth", ExpectedOutput::Numbers),
("linetype", ExpectedOutput::Linetypes),
("dash_offset", ExpectedOutput::Numbers),
("cap", ExpectedOutput::Strings),
("join", ExpectedOutput::Strings),
("expand", ExpectedOutput::Numbers),
("angle", ExpectedOutput::Numbers),
("pick_id", ExpectedOutput::Numbers),
];
pub struct RectGeom {
pub(crate) state: GeomState,
}
crate::impl_geom_inherents!(RectGeom);
impl BuildableGeom for RectGeom {
fn build_from(builder: GeomBuilder<Self>) -> Self {
let (keys_opt, channels) = builder.into_parts();
let n = require_x_and_siblings(&channels, &["y", "x2", "y2"], "RectGeom");
let state = finalize_state(
keys_opt,
channels,
n,
KeysStrategy::PerRow,
CHANNELS,
"RectGeom",
);
RectGeom { state }
}
}
impl Geom for RectGeom {
fn state(&self) -> &GeomState {
&self.state
}
fn state_mut(&mut self) -> &mut GeomState {
&mut self.state
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn draw(&self, scene: &mut dyn SceneBuilder, ctx: &GeomContext<'_>) {
let panel = ctx.panel_rect;
let panel_w = panel.x1 - panel.x0;
let panel_h = panel.y1 - panel.y0;
if panel_w <= 0.0 || panel_h <= 0.0 {
return;
}
let n = self.len();
if n == 0 {
return;
}
let x_scale_bound = ctx.scale_for("x");
let y_scale_bound = ctx.scale_for("y");
let x2_scale_bound = ctx.scale_for("x2").or(x_scale_bound);
let y2_scale_bound = ctx.scale_for("y2").or(y_scale_bound);
let x_offset_scale = ctx.scale_for("x_offset");
let y_offset_scale = ctx.scale_for("y_offset");
let x2_offset_scale = ctx.scale_for("x2_offset");
let y2_offset_scale = ctx.scale_for("y2_offset");
let x_band_scale = ctx.scale_for("x_band");
let y_band_scale = ctx.scale_for("y_band");
let x2_band_scale = ctx.scale_for("x2_band");
let y2_band_scale = ctx.scale_for("y2_band");
let corner_radius_scale = ctx.scale_for("corner_radius");
let fill_scale = ctx.scale_for("fill");
let stroke_scale = ctx.scale_for("stroke");
let fill_opacity_scale = ctx.scale_for("fill_opacity");
let stroke_opacity_scale = ctx.scale_for("stroke_opacity");
let linewidth_scale = ctx.scale_for("linewidth");
let linetype_scale = ctx.scale_for("linetype");
let dash_offset_scale = ctx.scale_for("dash_offset");
let cap_scale = ctx.scale_for("cap");
let join_scale = ctx.scale_for("join");
let pick_id_scale = ctx.scale_for("pick_id");
let expand_scale = ctx.scale_for("expand");
let angle_scale = ctx.scale_for("angle");
let channels = &self.state.channels;
let (x_col, x_scale) = match channels.get("x") {
Some(Channel::Data(c)) => (c, x_scale_bound),
Some(Channel::RawData(c)) => (c, None),
_ => return,
};
let (y_col, y_scale) = match channels.get("y") {
Some(Channel::Data(c)) => (c, y_scale_bound),
Some(Channel::RawData(c)) => (c, None),
_ => return,
};
let (x2_col, x2_scale) = match channels.get("x2") {
Some(Channel::Data(c)) => (c, x2_scale_bound),
Some(Channel::RawData(c)) => (c, None),
_ => return,
};
let (y2_col, y2_scale) = match channels.get("y2") {
Some(Channel::Data(c)) => (c, y2_scale_bound),
Some(Channel::RawData(c)) => (c, None),
_ => return,
};
let x_offset_ch = channels.get("x_offset");
let y_offset_ch = channels.get("y_offset");
let x2_offset_ch = channels.get("x2_offset");
let y2_offset_ch = channels.get("y2_offset");
let x_band_ch = channels.get("x_band");
let y_band_ch = channels.get("y_band");
let x2_band_ch = channels.get("x2_band");
let y2_band_ch = channels.get("y2_band");
let corner_radius_ch = channels.get("corner_radius");
let fill_ch = channels.get("fill");
let stroke_ch = channels.get("stroke");
let fill_opacity_ch = channels.get("fill_opacity");
let stroke_opacity_ch = channels.get("stroke_opacity");
let linewidth_ch = channels.get("linewidth");
let linetype_ch = channels.get("linetype");
let dash_offset_ch = channels.get("dash_offset");
let cap_ch = channels.get("cap");
let join_ch = channels.get("join");
let pick_id_ch = channels.get("pick_id");
let expand_ch = channels.get("expand");
let angle_ch = channels.get("angle");
for i in 0..n {
let x_band = resolve_number_channel_or(x_band_ch, x_band_scale, i, DEFAULT_X_BAND);
let x2_band = resolve_number_channel_or(x2_band_ch, x2_band_scale, i, DEFAULT_X2_BAND);
let y_band = resolve_number_channel_or(y_band_ch, y_band_scale, i, DEFAULT_Y_BAND);
let y2_band = resolve_number_channel_or(y2_band_ch, y2_band_scale, i, DEFAULT_Y2_BAND);
let x_frac = resolve_position(x_col.get(i), x_scale, x_band);
let x2_frac = resolve_position(x2_col.get(i), x2_scale, x2_band);
let y_frac = resolve_position(y_col.get(i), y_scale, y_band);
let y2_frac = resolve_position(y2_col.get(i), y2_scale, y2_band);
if !x_frac.is_finite()
|| !x2_frac.is_finite()
|| !y_frac.is_finite()
|| !y2_frac.is_finite()
{
continue;
}
let x_off_px = resolve_number_channel(x_offset_ch, x_offset_scale, i)
.map(|o| pt_to_px(o, ctx.dpi))
.unwrap_or(0.0);
let x2_off_px = resolve_number_channel(x2_offset_ch, x2_offset_scale, i)
.map(|o| pt_to_px(o, ctx.dpi))
.unwrap_or(0.0);
let y_off_px = resolve_number_channel(y_offset_ch, y_offset_scale, i)
.map(|o| pt_to_px(o, ctx.dpi))
.unwrap_or(0.0);
let y2_off_px = resolve_number_channel(y2_offset_ch, y2_offset_scale, i)
.map(|o| pt_to_px(o, ctx.dpi))
.unwrap_or(0.0);
let (px0, py0) = ctx.projection.project_to_panel_px(panel, &[x_frac, y_frac]);
let (px20, py20) = ctx
.projection
.project_to_panel_px(panel, &[x2_frac, y2_frac]);
let px = px0 + x_off_px;
let py = py0 - y_off_px;
let px2 = px20 + x2_off_px;
let py2 = py20 - y2_off_px;
let expand_px = pt_to_px(
resolve_number_channel_or(expand_ch, expand_scale, i, 0.0),
ctx.dpi,
);
let x0 = px.min(px2) - expand_px;
let y0 = py.min(py2) - expand_px;
let x1 = px.max(px2) + expand_px;
let y1 = py.max(py2) + expand_px;
let r = Rect::new(x0, y0, x1, y1);
let is_linear = ctx.projection.is_linear();
if !r.is_finite() || (is_linear && (r.width() <= 0.0 || r.height() <= 0.0)) {
continue;
}
let fill_color = override_alpha(
resolve_color_channel_or_theme(
fill_ch,
fill_scale,
i,
ctx.theme.geom.rect.fill.as_ref(),
&ctx.theme.palette,
),
resolve_number_channel(fill_opacity_ch, fill_opacity_scale, i),
);
let stroke_color = override_alpha(
resolve_color_channel_or_theme(
stroke_ch,
stroke_scale,
i,
ctx.theme.geom.rect.stroke.as_ref(),
&ctx.theme.palette,
),
resolve_number_channel(stroke_opacity_ch, stroke_opacity_scale, i),
);
let linewidth_pt = resolve_number_channel_or(
linewidth_ch,
linewidth_scale,
i,
ctx.theme.geom.rect.linewidth_pt,
);
let linewidth_px = pt_to_px(linewidth_pt, ctx.dpi);
let corner_radius_pt =
resolve_number_channel_or(corner_radius_ch, corner_radius_scale, i, 0.0);
let corner_radius_px = pt_to_px(corner_radius_pt, ctx.dpi).max(0.0);
let dash_pattern_pt = resolve_linetype_channel(linetype_ch, linetype_scale, i);
let dash_offset_pt =
resolve_number_channel_or(dash_offset_ch, dash_offset_scale, i, 0.0);
let cap = resolve_cap_channel(cap_ch, cap_scale, i, ctx.theme.geom.rect.cap);
let join = resolve_join_channel(join_ch, join_scale, i, ctx.theme.geom.rect.join);
let (path, rotation_centre) = if is_linear {
let p = if corner_radius_px > 0.0 {
rounded_rect(r, corner_radius_px)
} else {
rect_path(r)
};
let centre = Point::new(0.5 * (x0 + x1), 0.5 * (y0 + y1));
(p, centre)
} else {
let corners: [([f64; 2], f64, f64); 4] = [
([x_frac, y_frac], x_off_px, y_off_px),
([x2_frac, y_frac], x2_off_px, y_off_px),
([x2_frac, y2_frac], x2_off_px, y2_off_px),
([x_frac, y2_frac], x_off_px, y2_off_px),
];
let mut pts: Vec<Point> = Vec::with_capacity(16);
let mut interior: Vec<(f64, f64)> = Vec::new();
for k in 0..4 {
let (curr, x_off, y_off) = corners[k];
let next_channels = corners[(k + 1) % 4].0;
let (cpx, cpy) = ctx.projection.project_to_panel_px(panel, &curr);
pts.push(Point::new(cpx + x_off, cpy - y_off));
interior.clear();
ctx.projection
.interpolate_segment(panel, &curr, &next_channels, &mut interior);
for (ipx, ipy) in &interior {
pts.push(Point::new(*ipx, *ipy));
}
}
let n_pts = pts.len() as f64;
let cx = pts.iter().map(|p| p.x).sum::<f64>() / n_pts;
let cy = pts.iter().map(|p| p.y).sum::<f64>() / n_pts;
let p = polygon_path(&[&pts], PolygonOptions::default());
(p, Point::new(cx, cy))
};
let pick = resolve_pick_id(pick_id_ch, pick_id_scale, i);
let angle = resolve_angle_channel(angle_ch, angle_scale, i);
let xform = if angle == 0.0 {
Affine::IDENTITY
} else {
Affine::rotate_about(-angle, rotation_centre)
};
if let Some(fc) = fill_color {
scene.fill(
FillRule::NonZero,
xform,
&Brush::Solid(fc),
None,
&path,
pick,
);
}
if let Some(sc) = stroke_color {
draw_stroke_with_linetype(
scene,
&path,
true,
sc,
sc,
linewidth_px,
linewidth_pt,
cap,
join,
&dash_pattern_pt,
dash_offset_pt,
xform,
pick,
ctx.shapes,
ctx.theme.geom.marker_outline_pt,
ctx.dpi,
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use crate::color::Color;
use crate::geometry::Rect as GeomRect;
use crate::geometry::Shape as _;
use crate::plot::geom::{linetype, DirectScaleResolver, Raw};
use crate::plot::scale;
use crate::plot::value::Value;
use crate::scene::recording::{Op, RecordingScene};
fn shapes() -> crate::shape::ShapeRegistry {
crate::shape::ShapeRegistry::with_builtins()
}
fn ctx<'a>(
panel: GeomRect,
registry: &'a crate::shape::ShapeRegistry,
scales: &'a DirectScaleResolver<'a>,
) -> GeomContext<'a> {
GeomContext::new(panel, 96.0, registry, scales)
}
fn red() -> Color {
Color::new([1.0, 0.0, 0.0, 1.0])
}
#[test]
fn builder_requires_all_four_positions() {
let r = std::panic::catch_unwind(|| {
RectGeom::builder()
.set("x", vec![0.0_f64, 1.0])
.set("y", vec![0.0_f64, 1.0])
.set("x2", vec![0.5_f64, 1.5])
.build()
});
assert!(r.is_err());
}
#[test]
#[should_panic(expected = "must be data, not constant")]
fn builder_x_constant_panics() {
RectGeom::builder()
.set("x", 0.0_f64)
.set("y", vec![0.0_f64])
.set("x2", vec![1.0_f64])
.set("y2", vec![1.0_f64])
.build();
}
#[test]
#[should_panic(expected = "does not match")]
fn builder_x2_length_mismatch_panics() {
RectGeom::builder()
.set("x", vec![0.0_f64, 1.0, 2.0])
.set("y", vec![0.0_f64, 1.0, 2.0])
.set("x2", vec![0.5_f64, 1.5]) .set("y2", vec![1.0_f64, 2.0, 3.0])
.build();
}
fn first_fill_rect(scene: &RecordingScene) -> Option<GeomRect> {
for op in &scene.ops {
if let Op::Fill { path, .. } = op {
let bbox = path.bounding_box();
return Some(GeomRect::new(bbox.x0, bbox.y0, bbox.x1, bbox.y1));
}
}
None
}
#[test]
fn fill_only_at_continuous_corners() {
let g = RectGeom::builder()
.set("x", vec![0.1_f64])
.set("y", vec![0.1_f64])
.set("x2", vec![0.4_f64])
.set("y2", vec![0.4_f64])
.set("fill", red())
.build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let r = first_fill_rect(&scene).expect("fill");
assert!((r.width() - 30.0).abs() < 1e-6, "width = {}", r.width());
assert!((r.height() - 30.0).abs() < 1e-6, "height = {}", r.height());
}
#[test]
fn band_fill_default_on_discrete_x_scale() {
let x = scale::discrete(["A", "B"].into_iter().map(|s| Value::String(Arc::from(s))));
let resolver = DirectScaleResolver::new().with("x", &x).with("x2", &x);
let g = RectGeom::builder()
.set("x", vec!["B"])
.set("x2", vec!["B"])
.set("y", vec![0.2_f64])
.set("y2", vec![0.8_f64])
.set("fill", red())
.build();
let shapes = shapes();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &resolver),
);
let r = first_fill_rect(&scene).expect("fill");
assert!((r.width() - 50.0).abs() < 1e-6, "width = {}", r.width());
assert!((r.x0 - 50.0).abs() < 1e-6, "x0 = {}", r.x0);
assert!((r.x1 - 100.0).abs() < 1e-6, "x1 = {}", r.x1);
}
#[test]
fn dodge_within_band_via_per_row_bands() {
let x = scale::discrete(["A", "B"].into_iter().map(|s| Value::String(Arc::from(s))));
let resolver = DirectScaleResolver::new().with("x", &x).with("x2", &x);
let g = RectGeom::builder()
.set("x", vec!["B", "B"])
.set("x2", vec!["B", "B"])
.set("y", vec![0.2_f64, 0.2])
.set("y2", vec![0.8_f64, 0.8])
.set("x_band", vec![-0.5_f64, 0.0])
.set("x2_band", vec![0.0_f64, 0.5])
.set("fill", red())
.build();
let shapes = shapes();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &resolver),
);
let widths: Vec<f64> = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Fill { path, .. } => {
let bb = path.bounding_box();
Some(bb.x1 - bb.x0)
}
_ => None,
})
.collect();
assert_eq!(widths.len(), 2);
for w in &widths {
assert!((w - 25.0).abs() < 1e-6, "width = {w}");
}
}
#[test]
fn continuous_scale_band_defaults_have_no_effect() {
let x = scale::continuous(0.0..=10.0);
let resolver = DirectScaleResolver::new().with("x", &x).with("x2", &x);
let g = RectGeom::builder()
.set("x", vec![2.0_f64])
.set("y", vec![0.0_f64])
.set("x2", vec![8.0_f64])
.set("y2", vec![1.0_f64])
.set("fill", red())
.build();
let shapes = shapes();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &resolver),
);
let r = first_fill_rect(&scene).expect("fill");
assert!((r.x0 - 20.0).abs() < 1e-6);
assert!((r.x1 - 80.0).abs() < 1e-6);
}
#[test]
fn diagonal_corner_order_doesnt_matter() {
let g = RectGeom::builder()
.set("x", vec![0.4_f64])
.set("x2", vec![0.1_f64])
.set("y", vec![0.4_f64])
.set("y2", vec![0.1_f64])
.set("fill", red())
.build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let r = first_fill_rect(&scene).expect("fill");
assert!((r.width() - 30.0).abs() < 1e-6);
assert!((r.height() - 30.0).abs() < 1e-6);
}
#[test]
fn rect_with_corner_radius_uses_rounded_path() {
let g = RectGeom::builder()
.set("x", vec![0.1_f64])
.set("y", vec![0.1_f64])
.set("x2", vec![0.5_f64])
.set("y2", vec![0.5_f64])
.set("fill", red())
.set("corner_radius", 4.0_f64)
.build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let curves = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Fill { path, .. } => Some(path.elements().iter().any(|el| {
matches!(
el,
crate::path::PathEl::CurveTo(_, _, _) | crate::path::PathEl::QuadTo(_, _)
)
})),
_ => None,
})
.next();
assert_eq!(curves, Some(true), "rounded rect should contain curves");
}
#[test]
fn no_fill_no_stroke_emits_nothing() {
let g = RectGeom::builder()
.set("x", vec![0.1_f64])
.set("y", vec![0.1_f64])
.set("x2", vec![0.5_f64])
.set("y2", vec![0.5_f64])
.build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
assert!(scene.ops.is_empty());
}
#[test]
fn stroke_uses_linewidth_pt_to_px() {
let g = RectGeom::builder()
.set("x", vec![0.1_f64])
.set("y", vec![0.1_f64])
.set("x2", vec![0.5_f64])
.set("y2", vec![0.5_f64])
.set("stroke", red())
.set("linewidth", 2.0_f64) .build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let widths: Vec<f64> = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Stroke { stroke, .. } => Some(stroke.width),
_ => None,
})
.collect();
assert_eq!(widths.len(), 1);
assert!((widths[0] - 2.0 * 96.0 / 72.0).abs() < 1e-9);
}
#[test]
fn linetype_dashes_stroke() {
let g = RectGeom::builder()
.set("x", vec![0.1_f64])
.set("y", vec![0.1_f64])
.set("x2", vec![0.5_f64])
.set("y2", vec![0.5_f64])
.set("stroke", red())
.set("linetype", Value::Linetype(linetype::dashed()))
.build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let dashed = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Stroke { stroke, .. } => Some(!stroke.dash_pattern.is_empty()),
_ => None,
})
.any(|b| b);
assert!(dashed);
}
#[test]
fn x_offset_translates_left_edge_in_pt() {
let g = RectGeom::builder()
.set("x", vec![0.2_f64])
.set("y", vec![0.0_f64])
.set("x2", vec![0.8_f64])
.set("y2", vec![1.0_f64])
.set("fill", red())
.set("x_offset", 9.0_f64) .build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let r = first_fill_rect(&scene).expect("fill");
assert!((r.x0 - 32.0).abs() < 1e-6, "x0 = {}", r.x0);
assert!((r.x1 - 80.0).abs() < 1e-6, "x1 = {}", r.x1);
}
#[test]
fn nonfinite_position_skips_row() {
let g = RectGeom::builder()
.set("x", vec![0.1_f64, f64::NAN])
.set("y", vec![0.1_f64, 0.1])
.set("x2", vec![0.5_f64, 0.5])
.set("y2", vec![0.5_f64, 0.5])
.set("fill", red())
.build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(GeomRect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let fills = scene
.ops
.iter()
.filter(|op| matches!(op, Op::Fill { .. }))
.count();
assert_eq!(fills, 1);
}
#[test]
fn declared_channels_alphabetical() {
let g = RectGeom::builder()
.set("x", vec![0.0_f64])
.set("y", vec![0.0_f64])
.set("x2", vec![1.0_f64])
.set("y2", vec![1.0_f64])
.set("fill", red())
.build();
let names: Vec<&str> = g.declared_channels().iter().map(|d| d.name).collect();
let mut sorted = names.clone();
sorted.sort();
assert_eq!(names, sorted);
}
#[test]
fn expand_grows_rect_on_all_sides() {
use crate::geometry::Shape as _;
let g = RectGeom::builder()
.set("x", Raw(vec![0.2_f64]))
.set("y", Raw(vec![0.2_f64]))
.set("x2", Raw(vec![0.8_f64]))
.set("y2", Raw(vec![0.8_f64]))
.set("fill", red())
.set("expand", 3.0_f64)
.build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let bb = scene
.ops
.iter()
.find_map(|op| match op {
Op::Fill { path, .. } => Some(path.bounding_box()),
_ => None,
})
.expect("fill");
let expand_px = 3.0 * 96.0 / 72.0;
let expected = 60.0 + 2.0 * expand_px;
assert!((bb.width() - expected).abs() < 0.5);
assert!((bb.height() - expected).abs() < 0.5);
}
#[test]
fn negative_expand_shrinks_rect() {
use crate::geometry::Shape as _;
let g = RectGeom::builder()
.set("x", Raw(vec![0.2_f64]))
.set("y", Raw(vec![0.2_f64]))
.set("x2", Raw(vec![0.8_f64]))
.set("y2", Raw(vec![0.8_f64]))
.set("fill", red())
.set("expand", -3.0_f64)
.build();
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let bb = scene
.ops
.iter()
.find_map(|op| match op {
Op::Fill { path, .. } => Some(path.bounding_box()),
_ => None,
})
.expect("fill");
let expand_px = 3.0 * 96.0 / 72.0;
let expected = 60.0 - 2.0 * expand_px;
assert!((bb.width() - expected).abs() < 0.5);
}
}