use crate::brush::Brush;
use crate::geometry::{Affine, Point, Vec2};
use crate::path::FillRule;
use crate::primitives::ellipse as ellipse_path;
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 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),
("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 EllipseGeom {
pub(crate) state: GeomState,
}
crate::impl_geom_inherents!(EllipseGeom);
impl BuildableGeom for EllipseGeom {
fn build_from(builder: GeomBuilder<Self>) -> Self {
let (keys_opt, channels) = builder.into_parts();
let n = require_x_and_siblings(&channels, &["y", "x2", "y2"], "EllipseGeom");
let state = finalize_state(
keys_opt,
channels,
n,
KeysStrategy::PerRow,
CHANNELS,
"EllipseGeom",
);
EllipseGeom { state }
}
}
impl Geom for EllipseGeom {
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 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 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, 0.0);
let x2_band = resolve_number_channel_or(x2_band_ch, x2_band_scale, i, 0.0);
let y_band = resolve_number_channel_or(y_band_ch, y_band_scale, i, 0.0);
let y2_band = resolve_number_channel_or(y2_band_ch, y2_band_scale, i, 0.0);
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 (cx0, cy0) = ctx.projection.project_to_panel_px(panel, &[x_frac, y_frac]);
let (x2_px0, y2_px0) = ctx
.projection
.project_to_panel_px(panel, &[x2_frac, y2_frac]);
let mut cx = cx0;
let mut x2_px = x2_px0;
let mut cy = cy0;
let mut y2_px = y2_px0;
if let Some(off) = resolve_number_channel(x_offset_ch, x_offset_scale, i) {
cx += pt_to_px(off, ctx.dpi);
}
if let Some(off) = resolve_number_channel(x2_offset_ch, x2_offset_scale, i) {
x2_px += pt_to_px(off, ctx.dpi);
}
if let Some(off) = resolve_number_channel(y_offset_ch, y_offset_scale, i) {
cy -= pt_to_px(off, ctx.dpi);
}
if let Some(off) = resolve_number_channel(y2_offset_ch, y2_offset_scale, i) {
y2_px -= pt_to_px(off, ctx.dpi);
}
let expand_px = pt_to_px(
resolve_number_channel_or(expand_ch, expand_scale, i, 0.0),
ctx.dpi,
);
let rx = (x2_px - cx).abs() + expand_px;
let ry = (y2_px - cy).abs() + expand_px;
if !rx.is_finite() || !ry.is_finite() || rx <= 0.0 || ry <= 0.0 {
continue;
}
let fill_color = override_alpha(
resolve_color_channel_or_theme(
fill_ch,
fill_scale,
i,
ctx.theme.geom.ellipse.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.ellipse.stroke.as_ref(),
&ctx.theme.palette,
),
resolve_number_channel(stroke_opacity_ch, stroke_opacity_scale, i),
);
if fill_color.is_none() && stroke_color.is_none() {
continue;
}
let path = ellipse_path(Point::new(cx, cy), Vec2::new(rx, ry));
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, Point::new(cx, cy))
};
if let Some(fc) = fill_color {
scene.fill(
FillRule::NonZero,
xform,
&Brush::Solid(fc),
None,
&path,
pick,
);
}
if let Some(sc) = stroke_color {
let linewidth_pt = resolve_number_channel_or(
linewidth_ch,
linewidth_scale,
i,
ctx.theme.geom.ellipse.linewidth_pt,
);
let linewidth_px = pt_to_px(linewidth_pt, ctx.dpi);
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.ellipse.cap);
let join =
resolve_join_channel(join_ch, join_scale, i, ctx.theme.geom.ellipse.join);
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;
use crate::geometry::Shape as _;
use crate::plot::geom::{linetype, DirectScaleResolver};
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: Rect,
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])
}
fn first_fill_bbox(scene: &RecordingScene) -> Option<Rect> {
for op in &scene.ops {
if let Op::Fill { path, .. } = op {
let bb = path.bounding_box();
return Some(Rect::new(bb.x0, bb.y0, bb.x1, bb.y1));
}
}
None
}
#[test]
fn builder_requires_all_four_positions() {
let r = std::panic::catch_unwind(|| {
EllipseGeom::builder()
.set("x", vec![0.0_f64])
.set("y", vec![0.0_f64])
.set("x2", vec![1.0_f64])
.build()
});
assert!(r.is_err());
}
#[test]
#[should_panic(expected = "must be data, not constant")]
fn builder_x_constant_panics() {
EllipseGeom::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_length_mismatch_panics() {
EllipseGeom::builder()
.set("x", vec![0.0_f64, 1.0])
.set("y", vec![0.0_f64, 1.0])
.set("x2", vec![1.0_f64])
.set("y2", vec![1.0_f64])
.build();
}
#[test]
fn ellipse_centre_and_radii_from_far_edge() {
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_f64])
.set("fill", red())
.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 = first_fill_bbox(&scene).expect("fill");
assert!((bb.x0 - 30.0).abs() < 1e-6, "x0 = {}", bb.x0);
assert!((bb.x1 - 70.0).abs() < 1e-6, "x1 = {}", bb.x1);
assert!((bb.y0 - 40.0).abs() < 1e-6, "y0 = {}", bb.y0);
assert!((bb.y1 - 60.0).abs() < 1e-6, "y1 = {}", bb.y1);
}
#[test]
fn ellipse_inverted_far_edge_still_works() {
let g = EllipseGeom::builder()
.set("x", vec![0.7_f64])
.set("y", vec![0.6_f64])
.set("x2", vec![0.5_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(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let bb = first_fill_bbox(&scene).expect("fill");
assert!((bb.x0 - 50.0).abs() < 1e-6, "x0 = {}", bb.x0);
assert!((bb.x1 - 90.0).abs() < 1e-6, "x1 = {}", bb.x1);
}
#[test]
fn band_fill_on_discrete_x() {
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 = EllipseGeom::builder()
.set("x", vec!["B"])
.set("x2", vec!["B"])
.set("y", vec![0.5_f64])
.set("y2", vec![0.7_f64])
.set("x2_band", vec![0.5_f64])
.set("fill", red())
.build();
let shapes = shapes();
let mut scene = RecordingScene::default();
g.draw(
&mut scene,
&ctx(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &resolver),
);
let bb = first_fill_bbox(&scene).expect("fill");
assert!((bb.x0 - 50.0).abs() < 1e-6, "x0 = {}", bb.x0);
assert!((bb.x1 - 100.0).abs() < 1e-6, "x1 = {}", bb.x1);
}
#[test]
fn pt_offset_on_far_edge_only_grows_radius() {
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.5_f64])
.set("y2", vec![0.5_f64])
.set("x2_offset", vec![9.0_f64])
.set("y2_offset", vec![9.0_f64])
.set("fill", red())
.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 = first_fill_bbox(&scene).expect("fill");
assert!((bb.x0 - 38.0).abs() < 1e-6);
assert!((bb.x1 - 62.0).abs() < 1e-6);
assert!((bb.y1 - bb.y0 - 24.0).abs() < 1e-6);
}
#[test]
fn zero_radius_skips_row() {
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.5_f64])
.set("y2", vec![0.5_f64])
.set("fill", red())
.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),
);
assert!(scene.ops.is_empty());
}
#[test]
fn nonfinite_position_skips_row() {
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64, f64::NAN])
.set("y", vec![0.5_f64, 0.5])
.set("x2", vec![0.7_f64, 0.7])
.set("y2", vec![0.6_f64, 0.6])
.set("fill", red())
.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 fills = scene
.ops
.iter()
.filter(|op| matches!(op, Op::Fill { .. }))
.count();
assert_eq!(fills, 1);
}
#[test]
fn no_fill_no_stroke_emits_nothing() {
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_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),
);
assert!(scene.ops.is_empty());
}
#[test]
fn stroke_uses_linewidth_pt_to_px() {
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_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(Rect::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 = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_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(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales),
);
let dashed = scene.ops.iter().any(|op| match op {
Op::Stroke { stroke, .. } => !stroke.dash_pattern.is_empty(),
_ => false,
});
assert!(dashed);
}
#[test]
fn declared_channels_alphabetical() {
let g = EllipseGeom::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);
assert!(!names.contains(&"corner_radius"));
}
#[test]
fn pick_id_channel_passes_through_per_row() {
let g = EllipseGeom::builder()
.set("x", vec![0.3_f64, 0.5, 0.7])
.set("y", vec![0.5_f64, 0.5, 0.5])
.set("x2", vec![0.35_f64, 0.55, 0.75])
.set("y2", vec![0.6_f64, 0.6, 0.6])
.set("fill", red())
.set("pick_id", vec![10_i64, 20, 30])
.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 picks: Vec<u32> = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Fill {
pick_id: crate::pick::PickId::Id(n),
..
} => Some(*n),
_ => None,
})
.collect();
assert_eq!(picks, vec![10, 20, 30]);
}
#[test]
fn pick_id_unset_emits_skip() {
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_f64])
.set("fill", red())
.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),
);
for op in &scene.ops {
if let Op::Fill { pick_id, .. } = op {
assert!(matches!(pick_id, crate::pick::PickId::Skip));
}
}
}
#[test]
fn pick_id_zero_maps_to_block() {
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_f64])
.set("fill", red())
.set("pick_id", 0_i64)
.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 has_block = scene.ops.iter().any(|op| {
matches!(
op,
Op::Fill {
pick_id: crate::pick::PickId::Block,
..
}
)
});
assert!(has_block);
}
#[test]
#[should_panic(expected = "must be a non-negative integer")]
fn pick_id_above_24_bit_panics_at_build() {
EllipseGeom::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("pick_id", 0x100_0000_i64)
.build();
}
#[test]
fn angle_zero_produces_identity_xform() {
let g_no_angle = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_f64])
.set("fill", red())
.build();
let g_zero = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_f64])
.set("fill", red())
.set("angle", 0.0_f64)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut s1 = RecordingScene::default();
let mut s2 = RecordingScene::default();
g_no_angle.draw(&mut s1, &ctx(panel, &shapes, &scales));
g_zero.draw(&mut s2, &ctx(panel, &shapes, &scales));
let coeffs = |scene: &RecordingScene| {
scene.ops.iter().find_map(|op| match op {
Op::Fill { transform, .. } => Some(transform.as_coeffs()),
_ => None,
})
};
assert_eq!(coeffs(&s1), coeffs(&s2));
}
#[test]
fn angle_uses_centre_as_pivot() {
use std::f64::consts::FRAC_PI_2;
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_f64])
.set("fill", red())
.set("angle", FRAC_PI_2)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = shapes();
let scales = DirectScaleResolver::new();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &scales));
let xform = scene
.ops
.iter()
.find_map(|op| match op {
Op::Fill { transform, .. } => Some(*transform),
_ => None,
})
.expect("fill op");
let pivot_world = xform * crate::geometry::Point::new(50.0, 50.0);
assert!(
(pivot_world.x - 50.0).abs() < 1e-6,
"pivot.x = {}",
pivot_world.x
);
assert!(
(pivot_world.y - 50.0).abs() < 1e-6,
"pivot.y = {}",
pivot_world.y
);
}
#[test]
fn expand_grows_ellipse_radii() {
let g = EllipseGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("x2", vec![0.7_f64])
.set("y2", vec![0.6_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 = first_fill_bbox(&scene).expect("fill");
let expand_px = 3.0 * 96.0 / 72.0;
let expected_w = 2.0 * (20.0 + expand_px);
assert!(
(bb.x1 - bb.x0 - expected_w).abs() < 0.5,
"width = {}",
bb.x1 - bb.x0
);
}
}