use crate::brush::Brush;
use crate::geometry::Affine;
use crate::path::FillRule;
#[cfg(test)]
use crate::plot::value::Value;
use crate::scene::{Glyph, GlyphRun, SceneBuilder};
use crate::shape::{Shape, ShapeKind, ShapeStyle};
use crate::stroke::Stroke;
#[cfg(test)]
use std::sync::Arc;
use super::resolve::{
band_width_at, override_alpha, pt_to_px, resolve_angle_channel, resolve_color_channel_or_theme,
resolve_number_channel, resolve_number_channel_or, resolve_pick_id, resolve_position,
resolve_str_channel_or, smallest_nonzero,
};
use super::state::{finalize_state, require_x_and_siblings, GeomState, KeysStrategy};
use super::{BuildableGeom, Channel, ExpectedOutput, Geom, GeomBuilder, GeomContext};
pub(crate) const GLYPH_BBOX_REFERENCE: f64 = 1.6;
const CHANNELS: &[(&str, ExpectedOutput)] = &[
("x", ExpectedOutput::Numbers),
("y", ExpectedOutput::Numbers),
("x_offset", ExpectedOutput::Numbers),
("y_offset", ExpectedOutput::Numbers),
("x_band", ExpectedOutput::Numbers),
("y_band", ExpectedOutput::Numbers),
("fill", ExpectedOutput::Colors),
("stroke", ExpectedOutput::Colors),
("fill_opacity", ExpectedOutput::Numbers),
("stroke_opacity", ExpectedOutput::Numbers),
("size", ExpectedOutput::Numbers),
("size_band", ExpectedOutput::Numbers),
("linewidth", ExpectedOutput::Numbers),
("shape", ExpectedOutput::Strings),
("angle", ExpectedOutput::Numbers),
("pick_id", ExpectedOutput::Numbers),
];
pub struct PointGeom {
pub(crate) state: GeomState,
}
crate::impl_geom_inherents!(PointGeom);
impl BuildableGeom for PointGeom {
fn build_from(builder: GeomBuilder<Self>) -> Self {
let (keys_opt, channels) = builder.into_parts();
let n = require_x_and_siblings(&channels, &["y"], "PointGeom");
let state = finalize_state(
keys_opt,
channels,
n,
KeysStrategy::PerRow,
CHANNELS,
"PointGeom",
);
PointGeom { state }
}
}
impl Geom for PointGeom {
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 kind(&self) -> Option<&'static str> {
Some("point")
}
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 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 x_offset_scale = ctx.scale_for("x_offset");
let y_offset_scale = ctx.scale_for("y_offset");
let x_band_scale = ctx.scale_for("x_band");
let y_band_scale = ctx.scale_for("y_band");
let size_scale = ctx.scale_for("size");
let size_band_scale = ctx.scale_for("size_band");
let shape_scale = ctx.scale_for("shape");
let linewidth_scale = ctx.scale_for("linewidth");
let angle_scale = ctx.scale_for("angle");
let pick_id_scale = ctx.scale_for("pick_id");
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 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 x_offset_ch = channels.get("x_offset");
let y_offset_ch = channels.get("y_offset");
let x_band_ch = channels.get("x_band");
let y_band_ch = channels.get("y_band");
let size_ch = channels.get("size");
let size_band_ch = channels.get("size_band");
let linewidth_ch = channels.get("linewidth");
let shape_ch = channels.get("shape");
let angle_ch = channels.get("angle");
let pick_id_ch = channels.get("pick_id");
for i in 0..n {
let x_raw = x_col.get(i);
let y_raw = y_col.get(i);
let x_band = resolve_number_channel_or(x_band_ch, x_band_scale, i, 0.0);
let y_band = resolve_number_channel_or(y_band_ch, y_band_scale, i, 0.0);
let px_frac = resolve_position(x_raw.clone(), x_scale, x_band);
let py_frac = resolve_position(y_raw.clone(), y_scale, y_band);
if !px_frac.is_finite() || !py_frac.is_finite() {
continue;
}
let (px0, py0) = ctx
.projection
.project_to_panel_px(panel, &[px_frac, py_frac]);
let mut px = px0;
let mut py = py0;
if let Some(off) = resolve_number_channel(x_offset_ch, x_offset_scale, i) {
px += pt_to_px(off, ctx.dpi);
}
if let Some(off) = resolve_number_channel(y_offset_ch, y_offset_scale, i) {
py -= pt_to_px(off, ctx.dpi);
}
let fill_color = override_alpha(
resolve_color_channel_or_theme(
fill_ch,
fill_scale,
i,
ctx.theme.geom.point.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.point.stroke.as_ref(),
&ctx.theme.palette,
),
resolve_number_channel(stroke_opacity_ch, stroke_opacity_scale, i),
);
let size_pt =
resolve_number_channel_or(size_ch, size_scale, i, ctx.theme.geom.point.size_pt);
let shape_name =
resolve_str_channel_or(shape_ch, shape_scale, i, &ctx.theme.geom.point.shape);
let size_band = resolve_number_channel_or(size_band_ch, size_band_scale, i, 0.0);
let x_band_px = band_width_at(x_scale, &x_raw) * panel_w;
let y_band_px = band_width_at(y_scale, &y_raw) * panel_h;
let band_px = smallest_nonzero(x_band_px, y_band_px);
let size_px = pt_to_px(size_pt, ctx.dpi) + size_band * band_px;
if !size_px.is_finite() || size_px <= 0.0 {
continue;
}
let shape: &Shape = match ctx.shapes.get(&shape_name) {
Some(s) => s,
None => continue,
};
let angle = resolve_angle_channel(angle_ch, angle_scale, i);
let xform = if angle == 0.0 {
Affine::translate((px, py)) * Affine::scale(size_px)
} else {
Affine::translate((px, py)) * Affine::rotate(-angle) * Affine::scale(size_px)
};
let pick = resolve_pick_id(pick_id_ch, pick_id_scale, i);
let stroke_width_pt = resolve_number_channel_or(
linewidth_ch,
linewidth_scale,
i,
ctx.theme.geom.point.stroke_width_pt,
);
let stroke_width_local = pt_to_px(stroke_width_pt, ctx.dpi) / size_px;
match shape.kind() {
ShapeKind::Paths { paths, style } => {
for sub in paths {
match style {
ShapeStyle::Fill => {
if let Some(fc) = fill_color {
scene.fill(
FillRule::NonZero,
xform,
&Brush::Solid(fc),
None,
sub,
pick,
);
}
if let Some(sc) = stroke_color {
let st = Stroke::new(stroke_width_local);
scene.stroke(&st, xform, &Brush::Solid(sc), None, sub, pick);
}
}
ShapeStyle::Stroke => {
if let Some(sc) = stroke_color {
let st = Stroke::new(stroke_width_local);
scene.stroke(&st, xform, &Brush::Solid(sc), None, sub, pick);
}
}
}
}
}
ShapeKind::Glyph {
font,
glyph_id,
em_bbox,
em_origin,
} => {
let Some(fc) = fill_color else { continue };
let h = em_bbox.height();
if h <= 0.0 || !h.is_finite() {
continue;
}
let bbox_norm = GLYPH_BBOX_REFERENCE / h;
let effective_font_size_px = size_px * bbox_norm;
let centring_px =
(em_origin.to_vec2() - em_bbox.center().to_vec2()) * effective_font_size_px;
let glyphs = [Glyph {
id: glyph_id,
x: 0.0,
y: 0.0,
}];
let brush = Brush::Solid(fc);
let run = GlyphRun {
font,
font_size: effective_font_size_px as f32,
transform: Affine::translate((px + centring_px.x, py + centring_px.y)),
glyph_transform: None,
brush: &brush,
brush_alpha: 1.0,
hint: false,
glyphs: &glyphs,
style: None,
};
scene.draw_glyphs(&run, pick);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
use crate::geometry::Rect;
use crate::plot::geom::{DirectScaleResolver, Keys, Raw};
use crate::plot::value::Date;
use crate::scene::recording::{Op, RecordingScene};
fn registry() -> crate::shape::ShapeRegistry {
crate::shape::ShapeRegistry::with_builtins()
}
fn ctx<'a>(
panel: Rect,
shapes: &'a crate::shape::ShapeRegistry,
scales: &'a DirectScaleResolver<'a>,
) -> GeomContext<'a> {
GeomContext::new(panel, 96.0, shapes, scales)
}
#[test]
fn builder_synthesises_positional_keys() {
let g = PointGeom::builder()
.set("x", vec![0.0_f64, 1.0, 2.0])
.set("y", vec![0.0_f64, 1.0, 4.0])
.build();
assert_eq!(g.len(), 3);
assert!(!g.has_explicit_keys());
match &g.state.keys {
Keys::Positional(n) => assert_eq!(*n, 3),
Keys::Explicit(_) => panic!("expected positional keys"),
}
}
#[test]
fn builder_uses_explicit_keys() {
let g = PointGeom::builder()
.keys(vec!["a", "b", "c"])
.set("x", vec![0.0_f64, 1.0, 2.0])
.set("y", vec![0.0_f64, 1.0, 2.0])
.build();
assert!(g.has_explicit_keys());
assert_eq!(g.len(), 3);
}
#[test]
#[should_panic(expected = "missing required channel")]
fn builder_missing_x_panics() {
PointGeom::builder()
.set("y", vec![1.0_f64, 2.0, 3.0])
.build();
}
#[test]
#[should_panic(expected = "missing required channel")]
fn builder_missing_y_panics() {
PointGeom::builder()
.set("x", vec![1.0_f64, 2.0, 3.0])
.build();
}
#[test]
#[should_panic(expected = "unknown channel \"colour\"")]
fn builder_unknown_channel_panics() {
PointGeom::builder()
.set("x", vec![1.0_f64, 2.0])
.set("y", vec![1.0_f64, 2.0])
.set("colour", vec!["a", "b"])
.build();
}
#[test]
#[should_panic(expected = "unknown channels \"alpha\", \"width\"")]
fn builder_lists_every_unknown_channel_sorted() {
PointGeom::builder()
.set("x", vec![1.0_f64, 2.0])
.set("y", vec![1.0_f64, 2.0])
.set("width", 2.0)
.set("alpha", 0.5)
.build();
}
#[test]
#[should_panic(expected = "must be data, not constant")]
fn builder_x_constant_panics() {
PointGeom::builder()
.set("x", 5.0)
.set("y", vec![1.0_f64])
.build();
}
#[test]
fn builder_x_string_column_ok() {
let g = PointGeom::builder()
.set("x", vec!["a", "b", "c"])
.set("y", vec![1.0_f64, 2.0, 3.0])
.build();
assert_eq!(g.len(), 3);
}
#[test]
fn builder_temporal_x_ok() {
let g = PointGeom::builder()
.set(
"x",
vec![Date::from_ymd(2024, 1, 1), Date::from_ymd(2024, 1, 2)],
)
.set("y", vec![0.0_f64, 1.0])
.build();
assert_eq!(g.len(), 2);
}
#[test]
#[should_panic(expected = "does not match")]
fn builder_mismatched_lengths_panic() {
PointGeom::builder()
.set("x", vec![0.0_f64, 1.0, 2.0])
.set("y", vec![0.0_f64, 1.0])
.build();
}
#[test]
fn builder_leaves_size_and_shape_unset_for_theme_fallback() {
let g = PointGeom::builder()
.set("x", vec![0.0_f64])
.set("y", vec![0.0_f64])
.build();
assert!(
!g.state.channels.contains_key("size"),
"expected no size channel (theme provides default at draw)"
);
assert!(
!g.state.channels.contains_key("shape"),
"expected no shape channel (theme provides default at draw)"
);
}
fn no_scales<'a>() -> DirectScaleResolver<'a> {
DirectScaleResolver::new()
}
fn red_solid() -> Color {
Color::new([1.0, 0.0, 0.0, 1.0])
}
#[test]
fn draw_emits_one_op_per_row() {
let mut g = PointGeom::builder()
.set("x", vec![0.0_f64, 0.5, 1.0])
.set("y", vec![0.0_f64, 1.0, 0.0])
.set("fill", red_solid())
.build();
g.rebuild_diff_against_previous();
let shapes = registry();
let scales = no_scales();
let c = ctx(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales);
let mut scene = RecordingScene::default();
g.draw(&mut scene, &c);
let fills = scene
.ops
.iter()
.filter(|op| matches!(op, Op::Fill { .. }))
.count();
assert_eq!(fills, 3);
}
#[test]
fn shape_channel_maps_through_its_scale() {
let shape_scale = crate::plot::scale::ordinal(["a", "b"])
.range_strings([Arc::from("circle"), Arc::from("square")]);
let mut g = PointGeom::builder()
.set("x", vec![0.0_f64, 1.0])
.set("y", vec![0.0_f64, 1.0])
.set("fill", red_solid())
.set("shape", vec!["a", "b"])
.build();
g.rebuild_diff_against_previous();
let shapes = registry();
let scales = DirectScaleResolver::new().with("shape", &shape_scale);
let c = ctx(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales);
let mut scene = RecordingScene::default();
g.draw(&mut scene, &c);
let fills = scene
.ops
.iter()
.filter(|op| matches!(op, Op::Fill { .. }))
.count();
assert_eq!(fills, 2, "unmapped shape names drop the row entirely");
}
fn synthetic_glyph_shape() -> crate::shape::Shape {
let blob = crate::brush::Blob::new(std::sync::Arc::new(Vec::<u8>::new()));
let font = crate::scene::Font::new(blob, 0);
let em_bbox = crate::geometry::Rect::new(0.0, 0.0, 0.6, 1.0);
let em_origin = crate::geometry::Point::new(0.05, 0.8);
let anchor = crate::geometry::Point::new(-0.5, 0.0);
crate::shape::Shape::glyph(font, 1, em_bbox, em_origin, anchor)
}
#[test]
fn glyph_shape_emits_draw_glyphs() {
let mut shapes = registry();
shapes.insert("synthetic-glyph", synthetic_glyph_shape());
let mut g = PointGeom::builder()
.set("x", vec![0.0_f64, 0.5, 1.0])
.set("y", vec![0.0_f64, 1.0, 0.0])
.set("fill", red_solid())
.set("shape", "synthetic-glyph")
.set("size", 14.0_f64)
.build();
g.rebuild_diff_against_previous();
let scales = no_scales();
let c = ctx(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales);
let mut scene = RecordingScene::default();
g.draw(&mut scene, &c);
let glyph_ops: Vec<_> = scene
.ops
.iter()
.filter(|op| matches!(op, Op::DrawGlyphs(_)))
.collect();
assert_eq!(glyph_ops.len(), 3, "one DrawGlyphs op per row");
let fills = scene
.ops
.iter()
.filter(|op| matches!(op, Op::Fill { .. }))
.count();
let strokes = scene
.ops
.iter()
.filter(|op| matches!(op, Op::Stroke { .. }))
.count();
assert_eq!(fills, 0);
assert_eq!(strokes, 0);
}
#[test]
fn glyph_shape_with_no_fill_emits_nothing() {
let mut shapes = registry();
shapes.insert("synthetic-glyph", synthetic_glyph_shape());
let mut g = PointGeom::builder()
.set("x", vec![0.0_f64])
.set("y", vec![0.0_f64])
.set("stroke", red_solid())
.set("shape", "synthetic-glyph")
.build();
g.rebuild_diff_against_previous();
let scales = no_scales();
let c = ctx(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales);
let mut scene = RecordingScene::default();
g.draw(&mut scene, &c);
assert!(
scene.ops.is_empty(),
"glyph shape with stroke-only should emit nothing, got {:?}",
scene.ops
);
}
#[test]
fn mixed_path_and_glyph_shapes_both_render() {
let mut shapes = registry();
shapes.insert("synthetic-glyph", synthetic_glyph_shape());
let mut g = PointGeom::builder()
.set("x", vec![0.0_f64, 0.25, 0.5, 0.75])
.set("y", vec![0.5_f64, 0.5, 0.5, 0.5])
.set("fill", red_solid())
.set(
"shape",
vec!["circle", "circle", "synthetic-glyph", "synthetic-glyph"],
)
.build();
g.rebuild_diff_against_previous();
let scales = no_scales();
let c = ctx(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales);
let mut scene = RecordingScene::default();
g.draw(&mut scene, &c);
let fills = scene
.ops
.iter()
.filter(|op| matches!(op, Op::Fill { .. }))
.count();
let glyphs = scene
.ops
.iter()
.filter(|op| matches!(op, Op::DrawGlyphs(_)))
.count();
assert_eq!(fills, 2, "two vector-shape fills");
assert_eq!(glyphs, 2, "two glyph draws");
}
#[test]
fn draw_skips_non_finite_rows() {
let mut g = PointGeom::builder()
.set("x", vec![0.0_f64, f64::NAN, 1.0])
.set("y", vec![0.0_f64, 1.0, 0.0])
.set("fill", red_solid())
.build();
g.rebuild_diff_against_previous();
let shapes = registry();
let scales = no_scales();
let c = ctx(Rect::new(0.0, 0.0, 100.0, 100.0), &shapes, &scales);
let mut scene = RecordingScene::default();
g.draw(&mut scene, &c);
let fills = scene
.ops
.iter()
.filter(|op| matches!(op, Op::Fill { .. }))
.count();
assert_eq!(fills, 2);
}
#[test]
fn declared_channels_alphabetical() {
let g = PointGeom::builder()
.set("x", vec![0.0_f64])
.set("y", vec![0.0_f64])
.set("fill", red_solid())
.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]
#[should_panic(expected = "does not match")]
fn builder_y_length_mismatch_panics() {
PointGeom::builder()
.set("x", vec![1.0_f64, 2.0, 3.0])
.set("y", vec![1.0_f64, 2.0])
.build();
}
#[test]
#[should_panic(expected = "does not match row count")]
fn builder_color_length_mismatch_panics() {
PointGeom::builder()
.set("x", vec![1.0_f64, 2.0, 3.0])
.set("y", vec![1.0_f64, 2.0, 3.0])
.set("fill", vec!["a", "b"])
.build();
}
#[test]
#[should_panic(expected = "does not match row count")]
fn builder_keys_length_mismatch_panics() {
PointGeom::builder()
.keys(vec!["a", "b"])
.set("x", vec![1.0_f64, 2.0, 3.0])
.set("y", vec![1.0_f64, 2.0, 3.0])
.build();
}
#[test]
fn declared_channels_sorted_and_classified() {
use std::collections::HashMap;
let g = PointGeom::builder()
.set("x", vec![0.0_f64, 1.0])
.set("y", vec![0.0_f64, 1.0])
.set("fill", Color::new([1.0, 0.0, 0.0, 1.0]))
.set("shape", "circle")
.set("size", 5.0)
.build();
let decls = g.declared_channels();
let names: Vec<_> = decls.iter().map(|d| d.name).collect();
assert_eq!(names, vec!["fill", "shape", "size", "x", "y"]);
let by_name: HashMap<_, _> = decls.iter().map(|d| (d.name, d)).collect();
assert!(by_name["x"].data_bound);
assert!(by_name["y"].data_bound);
assert!(!by_name["fill"].data_bound);
assert_eq!(by_name["x"].expected_output, ExpectedOutput::Numbers);
assert_eq!(by_name["fill"].expected_output, ExpectedOutput::Colors);
assert_eq!(by_name["shape"].expected_output, ExpectedOutput::Strings);
}
#[test]
fn declared_opacity_channels_are_numeric() {
use std::collections::HashMap;
let g = PointGeom::builder()
.set("x", vec![0.0_f64, 1.0])
.set("y", vec![0.0_f64, 1.0])
.set("fill_opacity", 0.3)
.set("stroke_opacity", vec![0.2_f64, 0.8])
.build();
let decls = g.declared_channels();
let by_name: HashMap<_, _> = decls.iter().map(|d| (d.name, d)).collect();
assert_eq!(
by_name["fill_opacity"].expected_output,
ExpectedOutput::Numbers
);
assert_eq!(
by_name["stroke_opacity"].expected_output,
ExpectedOutput::Numbers
);
assert!(!by_name["fill_opacity"].data_bound);
assert!(by_name["stroke_opacity"].data_bound);
}
#[test]
fn diff_positional_path_after_mutation() {
let mut g = PointGeom::builder()
.set("x", vec![0.0_f64, 1.0, 2.0])
.set("y", vec![0.0_f64, 1.0, 2.0])
.build();
g.rebuild_diff_against_previous();
assert_eq!(g.state.enter, vec![0, 1, 2]);
assert!(g.state.update.is_empty());
assert!(g.state.exit.is_empty());
g.set("y", vec![10.0_f64, 20.0, 30.0]);
g.rebuild_diff_against_previous();
assert!(g.state.enter.is_empty());
assert_eq!(g.state.update, vec![(0, 0), (1, 1), (2, 2)]);
assert!(g.state.exit.is_empty());
}
#[test]
fn update_closure_atomic_multi_channel() {
let mut g = PointGeom::builder()
.set("x", vec![0.0_f64, 1.0, 2.0])
.set("y", vec![10.0_f64, 20.0, 30.0])
.build();
g.update(|b| {
b.set("x", vec![100.0_f64, 200.0, 300.0, 400.0]);
b.set("y", vec![1.0_f64, 2.0, 3.0, 4.0]);
});
assert_eq!(g.len(), 4);
g.rebuild_diff_against_previous();
assert_eq!(g.state.update, vec![(0, 0), (1, 1), (2, 2)]);
assert_eq!(g.state.enter, vec![3]);
assert!(g.state.exit.is_empty());
}
#[test]
fn update_closure_can_change_keys() {
let mut g = PointGeom::builder()
.keys(vec!["a", "b", "c"])
.set("x", vec![0.0_f64, 1.0, 2.0])
.set("y", vec![0.0_f64, 1.0, 2.0])
.build();
g.rebuild_diff_against_previous();
g.update(|b| {
b.keys(vec!["c", "a", "d"]);
b.set("x", vec![20.0_f64, 0.0, 99.0]);
b.set("y", vec![20.0_f64, 0.0, 99.0]);
});
g.rebuild_diff_against_previous();
assert_eq!(g.state.update, vec![(2, 0), (0, 1)]);
assert_eq!(g.state.enter, vec![2]);
assert_eq!(g.state.exit.len(), 1);
assert_eq!(g.state.exit[0].as_str(), Some("b"));
}
#[test]
#[should_panic(expected = "must be data, not constant")]
fn update_closure_validation_panics_on_invalid_state() {
let mut g = PointGeom::builder()
.set("x", vec![0.0_f64, 1.0])
.set("y", vec![0.0_f64, 1.0])
.build();
g.update(|b| {
b.set("x", 5.0);
});
}
#[test]
fn diff_columns_path_with_reordered_keys() {
let mut g = PointGeom::builder()
.keys(vec!["a", "b", "c"])
.set("x", vec![0.0_f64, 1.0, 2.0])
.set("y", vec![0.0_f64, 1.0, 2.0])
.build();
g.rebuild_diff_against_previous();
assert_eq!(g.state.enter, vec![0, 1, 2]);
g.state.keys = Keys::Explicit(vec!["c", "a", "b"].into());
g.state.dirty = true;
g.rebuild_diff_against_previous();
assert!(g.state.enter.is_empty());
assert!(g.state.exit.is_empty());
assert_eq!(g.state.update, vec![(2, 0), (0, 1), (1, 2)]);
}
fn count_ops(ops: &[Op]) -> (usize, usize) {
let mut fills = 0;
let mut strokes = 0;
for op in ops {
match op {
Op::Fill { .. } => fills += 1,
Op::Stroke { .. } => strokes += 1,
_ => {}
}
}
(fills, strokes)
}
#[test]
fn draw_fills_circle_when_fill_bound() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (fills, strokes) = count_ops(&scene.ops);
assert!(fills >= 1);
assert_eq!(strokes, 0);
}
#[test]
fn draw_strokes_circle_when_stroke_bound() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("stroke", Color::new([0.0, 0.0, 0.0, 1.0]))
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (fills, strokes) = count_ops(&scene.ops);
assert_eq!(fills, 0);
assert!(strokes >= 1);
}
#[test]
fn draw_both_fill_and_stroke() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("stroke", Color::new([0.0, 0.0, 0.0, 1.0]))
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (fills, strokes) = count_ops(&scene.ops);
assert!(fills >= 1);
assert!(strokes >= 1);
}
#[test]
fn draw_fill_opacity_overrides_alpha() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("fill_opacity", 0.25)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let alphas: Vec<f32> = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Fill {
brush: crate::brush::Brush::Solid(c),
..
} => Some(c.components[3]),
_ => None,
})
.collect();
assert!(!alphas.is_empty());
for a in &alphas {
assert!((*a as f64 - 0.25).abs() < 1e-6);
}
}
#[test]
fn draw_stroke_opacity_overrides_alpha() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("stroke", Color::new([0.0, 0.0, 0.0, 1.0]))
.set("stroke_opacity", 0.5)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let alphas: Vec<f32> = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Stroke {
brush: crate::brush::Brush::Solid(c),
..
} => Some(c.components[3]),
_ => None,
})
.collect();
assert!(!alphas.is_empty());
for a in &alphas {
assert!((*a as f64 - 0.5).abs() < 1e-6);
}
}
#[test]
fn draw_opacity_unset_preserves_color_alpha() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", Color::new([1.0, 0.0, 0.0, 0.7]))
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
for op in &scene.ops {
if let Op::Fill {
brush: crate::brush::Brush::Solid(c),
..
} = op
{
assert!((c.components[3] as f64 - 0.7).abs() < 1e-6);
}
}
}
#[test]
fn draw_per_row_opacity_data_column() {
let g = PointGeom::builder()
.set("x", vec![0.25_f64, 0.75])
.set("y", vec![0.5_f64, 0.5])
.set("fill", Color::new([0.0, 0.0, 1.0, 1.0]))
.set("fill_opacity", vec![0.2_f64, 0.8])
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let alphas: Vec<f32> = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Fill {
brush: crate::brush::Brush::Solid(c),
..
} => Some(c.components[3]),
_ => None,
})
.collect();
assert_eq!(alphas.len(), 2);
assert!((alphas[0] as f64 - 0.2).abs() < 1e-6);
assert!((alphas[1] as f64 - 0.8).abs() < 1e-6);
}
fn first_fill_translation(scene: &RecordingScene) -> Option<(f64, f64)> {
for op in &scene.ops {
if let Op::Fill { transform, .. } = op {
let v = transform.translation();
return Some((v.x, v.y));
}
}
None
}
#[test]
fn draw_x_offset_shifts_right() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("x_offset", 9.0)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (px, py) = first_fill_translation(&scene).expect("fill op");
assert!((px - 62.0).abs() < 1e-6, "px = {px}");
assert!((py - 50.0).abs() < 1e-6, "py = {py}");
}
#[test]
fn draw_y_offset_positive_is_up() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("y_offset", 9.0)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (px, py) = first_fill_translation(&scene).expect("fill op");
assert!((px - 50.0).abs() < 1e-6);
assert!((py - 38.0).abs() < 1e-6, "py = {py}");
}
#[test]
fn draw_x_band_offset_on_discrete_scale() {
use crate::plot::scale;
let x_scale = scale::discrete(
["a", "b", "c", "d"]
.into_iter()
.map(|s| crate::plot::value::Value::String(Arc::from(s))),
);
let resolver = DirectScaleResolver::new().with("x", &x_scale);
let g = PointGeom::builder()
.set("x", vec!["b"])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("x_band", 0.5)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (px, _py) = first_fill_translation(&scene).expect("fill op");
assert!((px - 50.0).abs() < 1e-6, "px = {px}");
}
#[test]
fn draw_on_reversed_binned_scale_mirrors_the_mark() {
use crate::plot::scale;
use crate::scales::Direction;
let x_scale = scale::binned(0.0..=30.0, vec![0.0, 10.0, 20.0, 30.0])
.with_direction(Direction::Reversed);
let resolver = DirectScaleResolver::new().with("x", &x_scale);
let g = PointGeom::builder()
.set("x", vec![5.0_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (px, _py) = first_fill_translation(&scene).expect("fill op");
assert!((px - 100.0 / 6.0 * 5.0).abs() < 1e-6, "px = {px}");
}
#[test]
fn draw_x_band_no_op_on_continuous_scale() {
use crate::plot::scale;
let x_scale = scale::continuous(0.0..=10.0);
let resolver = DirectScaleResolver::new().with("x", &x_scale);
let g = PointGeom::builder()
.set("x", vec![5.0_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("x_band", 0.5)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (px, _py) = first_fill_translation(&scene).expect("fill op");
assert!((px - 50.0).abs() < 1e-6);
}
#[test]
fn draw_per_row_offset_jitter() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64, 0.5, 0.5])
.set("y", vec![0.5_f64, 0.5, 0.5])
.set("fill", red_solid())
.set("x_offset", vec![-9.0_f64, 0.0, 9.0])
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let xs: Vec<f64> = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Fill { transform, .. } => Some(transform.translation().x),
_ => None,
})
.collect();
assert_eq!(xs.len(), 3);
assert!((xs[0] - 38.0).abs() < 1e-6);
assert!((xs[1] - 50.0).abs() < 1e-6);
assert!((xs[2] - 62.0).abs() < 1e-6);
}
#[test]
fn raw_position_bypasses_scale() {
use crate::plot::scale;
let x_scale = scale::continuous(0.0..=100.0);
let resolver = DirectScaleResolver::new().with("x", &x_scale);
let g = PointGeom::builder()
.set("x", Raw(vec![0.25_f64, 0.5, 0.75]))
.set("y", vec![0.5_f64, 0.5, 0.5])
.set("fill", red_solid())
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let xs: Vec<f64> = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Fill { transform, .. } => Some(transform.translation().x),
_ => None,
})
.collect();
assert!((xs[0] - 25.0).abs() < 1e-6, "xs[0] = {}", xs[0]);
assert!((xs[1] - 50.0).abs() < 1e-6, "xs[1] = {}", xs[1]);
assert!((xs[2] - 75.0).abs() < 1e-6, "xs[2] = {}", xs[2]);
}
#[test]
fn raw_color_bypasses_scale() {
use crate::plot::scale;
use crate::plot::value::Value;
let fill_scale = scale::ordinal(["a", "b"].iter().map(|s| Value::String(Arc::from(*s))))
.range_colors([
Color::new([0.0, 0.0, 1.0, 1.0]),
Color::new([0.0, 1.0, 0.0, 1.0]),
]);
let resolver = DirectScaleResolver::new().with("fill", &fill_scale);
let literal = Color::new([1.0, 0.0, 0.0, 1.0]);
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", Raw(literal))
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let fill_color = scene.ops.iter().find_map(|op| match op {
Op::Fill {
brush: crate::brush::Brush::Solid(c),
..
} => Some(*c),
_ => None,
});
let c = fill_color.expect("fill op");
assert!((c.components[0] - 1.0).abs() < 1e-6);
assert!((c.components[1] - 0.0).abs() < 1e-6);
assert!((c.components[2] - 0.0).abs() < 1e-6);
}
#[test]
fn raw_position_outside_panel_clips() {
let g = PointGeom::builder()
.set("x", Raw(vec![-0.5_f64, 1.5]))
.set("y", vec![0.5_f64, 0.5])
.set("fill", red_solid())
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let xs: Vec<f64> = scene
.ops
.iter()
.filter_map(|op| match op {
Op::Fill { transform, .. } => Some(transform.translation().x),
_ => None,
})
.collect();
assert_eq!(xs.len(), 2);
assert!((xs[0] - -50.0).abs() < 1e-6);
assert!((xs[1] - 150.0).abs() < 1e-6);
}
#[test]
fn raw_constant_size_bypasses_size_scale() {
use crate::plot::scale;
let size_scale = scale::continuous(0.0..=10.0).range_numbers([2.0, 10.0]);
let resolver = DirectScaleResolver::new().with("size", &size_scale);
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("size", Raw(20.0_f64))
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let scale_factor = scene.ops.iter().find_map(|op| match op {
Op::Fill { transform, .. } => Some(transform.as_coeffs()[0]),
_ => None,
});
let s = scale_factor.expect("fill");
assert!((s - 20.0 * 96.0 / 72.0).abs() < 1e-6, "size = {s}");
}
#[test]
fn raw_length_validated_at_build() {
let r = std::panic::catch_unwind(|| {
PointGeom::builder()
.set("x", vec![0.0_f64, 1.0])
.set("y", Raw(vec![0.5_f64, 0.5, 0.5])) .build()
});
assert!(r.is_err());
}
#[test]
fn raw_data_x_is_required_position_data() {
let g = PointGeom::builder()
.set("x", Raw(vec![0.25_f64, 0.75]))
.set("y", Raw(vec![0.5_f64, 0.5]))
.build();
assert_eq!(g.len(), 2);
}
#[test]
#[should_panic(expected = "must be a non-negative integer")]
fn raw_pick_id_validated_at_build() {
PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("pick_id", Raw(0x100_0000_i64))
.build();
}
#[test]
fn raw_pick_id_passes_through_per_row() {
let g = PointGeom::builder()
.set("x", vec![0.2_f64, 0.5, 0.8])
.set("y", vec![0.5_f64, 0.5, 0.5])
.set("fill", red_solid())
.set("pick_id", Raw(vec![5_i64, 6, 7]))
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
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![5, 6, 7]);
}
#[test]
fn draw_neither_fill_nor_stroke_emits_nothing() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (fills, strokes) = count_ops(&scene.ops);
assert_eq!(fills, 0);
assert_eq!(strokes, 0);
}
#[test]
fn draw_vectorised_n_rows() {
let g = PointGeom::builder()
.set("x", vec![0.1_f64, 0.3, 0.5, 0.7, 0.9])
.set("y", vec![0.5_f64; 5])
.set("fill", Color::new([0.0, 0.0, 1.0, 1.0]))
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (fills, _) = count_ops(&scene.ops);
assert!(fills >= 5);
}
#[test]
fn draw_routes_x_through_scale() {
use crate::plot::scale;
let x_scale = scale::continuous(0.0..=100.0);
let resolver = DirectScaleResolver::new().with("x", &x_scale);
let g = PointGeom::builder()
.set("x", vec![50.0_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (fills, _) = count_ops(&scene.ops);
assert!(fills >= 1);
}
#[test]
fn draw_skips_unknown_shape() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("shape", "definitely-not-a-shape")
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let (fills, strokes) = count_ops(&scene.ops);
assert_eq!(fills, 0);
assert_eq!(strokes, 0);
}
fn first_fill_scale(scene: &RecordingScene) -> Option<f64> {
for op in &scene.ops {
if let Op::Fill { transform, .. } = op {
let m = transform.as_coeffs();
return Some(m[0]);
}
}
None
}
#[test]
fn draw_size_band_on_discrete_x_sizes_to_band() {
use crate::plot::scale;
let x_scale = scale::discrete(
["a", "b", "c", "d"]
.into_iter()
.map(|s| Value::String(Arc::from(s))),
);
let resolver = DirectScaleResolver::new().with("x", &x_scale);
let g = PointGeom::builder()
.set("x", vec!["b"])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("size", 0.0_f64)
.set("size_band", 1.0_f64)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let s = first_fill_scale(&scene).expect("fill");
assert!((s - 25.0).abs() < 1e-6, "diameter = {s}");
}
#[test]
fn draw_size_band_no_op_on_continuous_axes() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("size", 6.0_f64)
.set("size_band", 1.0_f64)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let s = first_fill_scale(&scene).expect("fill");
assert!((s - 8.0).abs() < 1e-6, "diameter = {s}");
}
#[test]
fn draw_size_band_picks_smallest_when_both_axes_discrete() {
use crate::plot::scale;
let x_scale = scale::discrete(
["a", "b", "c", "d"]
.into_iter()
.map(|s| Value::String(Arc::from(s))),
);
let y_scale = scale::discrete(["p", "q"].into_iter().map(|s| Value::String(Arc::from(s))));
let resolver = DirectScaleResolver::new()
.with("x", &x_scale)
.with("y", &y_scale);
let g = PointGeom::builder()
.set("x", vec!["b"])
.set("y", vec!["q"])
.set("fill", red_solid())
.set("size", 0.0_f64)
.set("size_band", 1.0_f64)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let s = first_fill_scale(&scene).expect("fill");
assert!((s - 25.0).abs() < 1e-6, "diameter = {s}");
}
#[test]
fn draw_size_band_additive_with_size_pt() {
use crate::plot::scale;
let x_scale = scale::discrete(
["a", "b", "c", "d"]
.into_iter()
.map(|s| Value::String(Arc::from(s))),
);
let resolver = DirectScaleResolver::new().with("x", &x_scale);
let g = PointGeom::builder()
.set("x", vec!["b"])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("size", 6.0_f64)
.set("size_band", 0.5_f64)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let s = first_fill_scale(&scene).expect("fill");
assert!((s - 20.5).abs() < 1e-6, "diameter = {s}");
}
#[test]
fn angle_zero_produces_unrotated_recording() {
let g_no_angle = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("size", 10.0_f64)
.build();
let g_zero = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("size", 10.0_f64)
.set("angle", 0.0_f64)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut s1 = RecordingScene::default();
let mut s2 = RecordingScene::default();
g_no_angle.draw(&mut s1, &ctx(panel, &shapes, &resolver));
g_zero.draw(&mut s2, &ctx(panel, &shapes, &resolver));
let t1 = first_fill_translation(&s1).unwrap();
let t2 = first_fill_translation(&s2).unwrap();
assert!((t1.0 - t2.0).abs() < 1e-9 && (t1.1 - t2.1).abs() < 1e-9);
}
#[test]
fn angle_rotates_glyph_about_centre_math_ccw() {
use std::f64::consts::FRAC_PI_2;
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.set("size", 10.0_f64)
.set("shape", "triangle-up")
.set("angle", FRAC_PI_2)
.build();
let panel = Rect::new(0.0, 0.0, 100.0, 100.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
let xform = scene.ops.iter().find_map(|op| match op {
Op::Fill { transform, .. } => Some(*transform),
_ => None,
});
let xform = xform.expect("fill op");
let apex_path = crate::geometry::Point::new(0.0, -0.92);
let apex_world = xform * apex_path;
let size_px = 10.0 * 96.0 / 72.0;
let expected_x = 50.0 - 0.92 * size_px;
let expected_y = 50.0;
assert!(
(apex_world.x - expected_x).abs() < 0.5,
"apex.x = {}, expected {}",
apex_world.x,
expected_x
);
assert!(
(apex_world.y - expected_y).abs() < 0.5,
"apex.y = {}, expected {}",
apex_world.y,
expected_y
);
}
#[test]
fn draw_silent_on_degenerate_panel() {
let g = PointGeom::builder()
.set("x", vec![0.5_f64])
.set("y", vec![0.5_f64])
.set("fill", red_solid())
.build();
let panel = Rect::new(0.0, 0.0, 0.0, 0.0);
let shapes = registry();
let resolver = no_scales();
let mut scene = RecordingScene::default();
g.draw(&mut scene, &ctx(panel, &shapes, &resolver));
assert!(scene.ops.is_empty());
}
}