use crate::brush::Brush;
use crate::color::{Color, ColorSpace};
use crate::geometry::{Affine, Point};
use crate::path::{FillRule, Path};
use crate::pick::PickId;
use crate::plot::scale::Scale;
use crate::plot::theme::{LineDefaults, ShapeDefaults, ThemeColor};
use crate::plot::value::LinetypeStep;
use crate::primitives::offset_polygon;
use crate::primitives::{
clip_polyline, clip_polyline_with_attrs, polyline, polyline_ribbon_full, round_corners,
CornerRounding, EndClip, PolylineOptions, RibbonOptions,
};
use crate::scene::SceneBuilder;
use crate::shape::ShapeRegistry;
use crate::stroke::{Cap, Join};
use std::collections::HashMap;
use std::sync::Arc;
use super::resolve::{
auto_endpoint_clip_pt, draw_stroke_with_linetype, emit_endpoint_marker,
endpoint_marker_outline_px, endpoint_outward, override_alpha, pt_to_px,
resolve_bool_channel_or, resolve_cap_channel, resolve_color_channel,
resolve_color_channel_or_theme, resolve_join_channel, resolve_linetype_channel,
resolve_number_channel, resolve_number_channel_or, resolve_str_channel_or, ChannelBind,
};
use super::{Channel, GeomContext};
const RIBBON_MITER_LIMIT: f64 = 4.0;
#[derive(Clone, Copy)]
pub(crate) struct OutlineDefaults<'a> {
pub stroke: Option<&'a ThemeColor>,
pub linewidth_pt: f64,
pub cap: Cap,
pub join: Join,
}
impl<'a> From<&'a LineDefaults> for OutlineDefaults<'a> {
fn from(d: &'a LineDefaults) -> Self {
OutlineDefaults {
stroke: d.stroke.as_ref(),
linewidth_pt: d.linewidth_pt,
cap: d.cap,
join: d.join,
}
}
}
impl<'a> From<&'a ShapeDefaults> for OutlineDefaults<'a> {
fn from(d: &'a ShapeDefaults) -> Self {
OutlineDefaults {
stroke: d.stroke.as_ref(),
linewidth_pt: d.linewidth_pt,
cap: d.cap,
join: d.join,
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct OutlineChannels<'a> {
pub stroke: Option<&'a Channel>,
pub stroke_opacity: Option<&'a Channel>,
pub linewidth: Option<&'a Channel>,
pub linetype: Option<&'a Channel>,
pub dash_offset: Option<&'a Channel>,
pub cap: Option<&'a Channel>,
pub join: Option<&'a Channel>,
pub clip_start: Option<&'a Channel>,
pub clip_end: Option<&'a Channel>,
pub start_marker: Option<&'a Channel>,
pub end_marker: Option<&'a Channel>,
pub start_marker_size: Option<&'a Channel>,
pub end_marker_size: Option<&'a Channel>,
pub start_marker_fill: Option<&'a Channel>,
pub end_marker_fill: Option<&'a Channel>,
pub start_marker_invert: Option<&'a Channel>,
pub end_marker_invert: Option<&'a Channel>,
}
impl<'a> OutlineChannels<'a> {
pub(crate) fn from_map(channels: &'a HashMap<String, Channel>, suffix: &str) -> Self {
let g = |base: &str| channels.get(&format!("{base}{suffix}"));
OutlineChannels {
stroke: g("stroke"),
stroke_opacity: g("stroke_opacity"),
linewidth: g("linewidth"),
linetype: g("linetype"),
dash_offset: g("dash_offset"),
cap: g("cap"),
join: g("join"),
clip_start: g("clip_start_radius"),
clip_end: g("clip_end_radius"),
start_marker: g("start_marker"),
end_marker: g("end_marker"),
start_marker_size: g("start_marker_size"),
end_marker_size: g("end_marker_size"),
start_marker_fill: g("start_marker_fill"),
end_marker_fill: g("end_marker_fill"),
start_marker_invert: g("start_marker_invert"),
end_marker_invert: g("end_marker_invert"),
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct OutlineScales<'a> {
pub stroke: Option<&'a Scale>,
pub stroke_opacity: Option<&'a Scale>,
pub linewidth: Option<&'a Scale>,
pub linetype: Option<&'a Scale>,
pub dash_offset: Option<&'a Scale>,
pub cap: Option<&'a Scale>,
pub join: Option<&'a Scale>,
pub clip_start: Option<&'a Scale>,
pub clip_end: Option<&'a Scale>,
pub start_marker: Option<&'a Scale>,
pub end_marker: Option<&'a Scale>,
pub start_marker_size: Option<&'a Scale>,
pub end_marker_size: Option<&'a Scale>,
pub start_marker_fill: Option<&'a Scale>,
pub end_marker_fill: Option<&'a Scale>,
pub start_marker_invert: Option<&'a Scale>,
pub end_marker_invert: Option<&'a Scale>,
}
impl<'a> OutlineScales<'a> {
pub(crate) fn from_ctx(ctx: &'a GeomContext<'_>, suffix: &str) -> Self {
let g = |base: &str| ctx.scale_for(&format!("{base}{suffix}"));
OutlineScales {
stroke: g("stroke"),
stroke_opacity: g("stroke_opacity"),
linewidth: g("linewidth"),
linetype: g("linetype"),
dash_offset: g("dash_offset"),
cap: g("cap"),
join: g("join"),
clip_start: g("clip_start_radius"),
clip_end: g("clip_end_radius"),
start_marker: g("start_marker"),
end_marker: g("end_marker"),
start_marker_size: g("start_marker_size"),
end_marker_size: g("end_marker_size"),
start_marker_fill: g("start_marker_fill"),
end_marker_fill: g("end_marker_fill"),
start_marker_invert: g("start_marker_invert"),
end_marker_invert: g("end_marker_invert"),
}
}
}
pub(crate) fn resolve_outline_spec(
ctx: &GeomContext<'_>,
defaults: OutlineDefaults<'_>,
ch: &OutlineChannels<'_>,
sc: &OutlineScales<'_>,
marker_fill: ChannelBind<'_>,
i0: usize,
pick: PickId,
) -> Option<OutlineSpec> {
let stroke_color = override_alpha(
resolve_color_channel_or_theme(
ch.stroke,
sc.stroke,
i0,
defaults.stroke,
&ctx.theme.palette,
),
resolve_number_channel(ch.stroke_opacity, sc.stroke_opacity, i0),
)?;
let linewidth_pt =
resolve_number_channel_or(ch.linewidth, sc.linewidth, i0, defaults.linewidth_pt);
let dash_pattern_pt = resolve_linetype_channel(ch.linetype, sc.linetype, i0);
let dash_offset_pt = resolve_number_channel_or(ch.dash_offset, sc.dash_offset, i0, 0.0);
let cap = resolve_cap_channel(ch.cap, sc.cap, i0, defaults.cap);
let join = resolve_join_channel(ch.join, sc.join, i0, defaults.join);
let user_clip_start_pt = resolve_number_channel_or(ch.clip_start, sc.clip_start, i0, 0.0);
let user_clip_end_pt = resolve_number_channel_or(ch.clip_end, sc.clip_end, i0, 0.0);
let default_marker_size_pt = 3.0 * linewidth_pt;
let start_marker_name = resolve_str_channel_or(ch.start_marker, sc.start_marker, i0, "");
let end_marker_name = resolve_str_channel_or(ch.end_marker, sc.end_marker, i0, "");
let start_marker_size_pt = resolve_number_channel_or(
ch.start_marker_size,
sc.start_marker_size,
i0,
default_marker_size_pt,
);
let end_marker_size_pt = resolve_number_channel_or(
ch.end_marker_size,
sc.end_marker_size,
i0,
default_marker_size_pt,
);
let start_marker_fill = resolve_color_channel(ch.start_marker_fill, sc.start_marker_fill, i0);
let end_marker_fill = resolve_color_channel(ch.end_marker_fill, sc.end_marker_fill, i0);
let start_marker_invert =
resolve_bool_channel_or(ch.start_marker_invert, sc.start_marker_invert, i0, false);
let end_marker_invert =
resolve_bool_channel_or(ch.end_marker_invert, sc.end_marker_invert, i0, false);
Some(OutlineSpec {
stroke_color,
linewidth_pt,
dash_pattern_pt,
dash_offset_pt,
cap,
join,
marker_fill: resolve_color_channel(marker_fill.ch, marker_fill.scale, i0)
.unwrap_or(stroke_color),
user_clip_start_pt,
user_clip_end_pt,
start_marker: EndpointMarker {
name: start_marker_name,
size_pt: start_marker_size_pt,
fill: start_marker_fill,
invert: start_marker_invert,
},
end_marker: EndpointMarker {
name: end_marker_name,
size_pt: end_marker_size_pt,
fill: end_marker_fill,
invert: end_marker_invert,
},
pick,
xform: Affine::IDENTITY,
corner_rounding: None,
})
}
#[derive(Clone)]
pub(crate) struct OutlineSpec {
pub stroke_color: Color,
pub linewidth_pt: f64,
pub dash_pattern_pt: Arc<[LinetypeStep]>,
pub dash_offset_pt: f64,
pub cap: Cap,
pub join: Join,
pub marker_fill: Color,
pub user_clip_start_pt: f64,
pub user_clip_end_pt: f64,
pub start_marker: EndpointMarker,
pub end_marker: EndpointMarker,
pub pick: PickId,
pub xform: Affine,
pub corner_rounding: Option<CornerRounding>,
}
#[derive(Clone)]
pub(crate) struct EndpointMarker {
pub name: String,
pub size_pt: f64,
pub fill: Option<Color>,
pub invert: bool,
}
impl Default for EndpointMarker {
fn default() -> Self {
EndpointMarker {
name: String::new(),
size_pt: 0.0,
fill: None,
invert: false,
}
}
}
pub(crate) struct PolygonSpec {
pub fill_color: Option<Color>,
pub stroke_color: Option<Color>,
pub linewidth_pt: f64,
pub dash_pattern_pt: Arc<[LinetypeStep]>,
pub dash_offset_pt: f64,
pub cap: Cap,
pub join: Join,
pub corner_rounding: Option<CornerRounding>,
pub marker_fill: Color,
pub xform: Affine,
pub pick: PickId,
}
pub(crate) fn draw_polygon_fill_and_stroke(
scene: &mut dyn SceneBuilder,
ctx: &GeomContext<'_>,
rings: &[Vec<Point>],
spec: &PolygonSpec,
) {
let mut path = Path::new();
let mut any = false;
for ring in rings {
if ring.len() < 3 {
continue;
}
if let Some(rounding) = spec.corner_rounding {
let sub = round_corners(ring, true, rounding);
for el in sub.iter() {
path.push(el);
}
} else {
path.move_to(ring[0]);
for q in &ring[1..] {
path.line_to(*q);
}
path.close_path();
}
any = true;
}
if !any {
return;
}
if let Some(fc) = spec.fill_color {
scene.fill(
FillRule::EvenOdd,
spec.xform,
&Brush::Solid(fc),
None,
&path,
spec.pick,
);
}
if let Some(sc) = spec.stroke_color {
let linewidth_px = pt_to_px(spec.linewidth_pt, ctx.dpi);
if !linewidth_px.is_finite() || linewidth_px <= 0.0 {
return;
}
draw_stroke_with_linetype(
scene,
&path,
true,
sc,
spec.marker_fill,
linewidth_px,
spec.linewidth_pt,
spec.cap,
spec.join,
&spec.dash_pattern_pt,
spec.dash_offset_pt,
spec.xform,
spec.pick,
ctx.shapes,
ctx.theme.geom.marker_outline_pt,
ctx.dpi,
);
}
}
pub(crate) fn expand_polygons(
rings: Vec<Vec<Point>>,
ring_owners: Option<&[usize]>,
expand_px: f64,
miter_limit: f64,
) -> Vec<Vec<Point>> {
if expand_px == 0.0 || !expand_px.is_finite() {
return rings;
}
if let Some(owners) = ring_owners {
debug_assert_eq!(rings.len(), owners.len());
let mut out = Vec::new();
let mut start = 0;
while start < rings.len() {
let owner = owners[start];
let mut end = start + 1;
while end < rings.len() && owners[end] == owner {
end += 1;
}
let refs: Vec<&[Point]> = rings[start..end].iter().map(|r| r.as_slice()).collect();
out.extend(offset_polygon(&refs, expand_px, miter_limit));
start = end;
}
out
} else {
let refs: Vec<&[Point]> = rings.iter().map(|r| r.as_slice()).collect();
offset_polygon(&refs, expand_px, miter_limit)
}
}
pub(crate) fn draw_curve_outline(
scene: &mut dyn SceneBuilder,
shapes: &ShapeRegistry,
dpi: f64,
marker_outline_pt: f64,
points: &[Point],
spec: &OutlineSpec,
) {
if points.len() < 2 {
return;
}
let linewidth_px = pt_to_px(spec.linewidth_pt, dpi);
if !linewidth_px.is_finite() || linewidth_px <= 0.0 {
return;
}
let auto_clip_start_pt = auto_endpoint_clip_pt(
&spec.start_marker.name,
spec.start_marker.size_pt,
spec.start_marker.invert,
shapes,
);
let auto_clip_end_pt = auto_endpoint_clip_pt(
&spec.end_marker.name,
spec.end_marker.size_pt,
spec.end_marker.invert,
shapes,
);
let clip_start_pt = spec.user_clip_start_pt + auto_clip_start_pt;
let clip_end_pt = spec.user_clip_end_pt + auto_clip_end_pt;
let clipped: Vec<Point> = if clip_start_pt > 0.0 || clip_end_pt > 0.0 {
let start = (clip_start_pt > 0.0).then(|| EndClip::Circle {
center: points[0],
radius: pt_to_px(clip_start_pt, dpi),
});
let end = (clip_end_pt > 0.0).then(|| EndClip::Circle {
center: *points.last().unwrap(),
radius: pt_to_px(clip_end_pt, dpi),
});
clip_polyline(points, start, end)
} else {
points.to_vec()
};
if clipped.len() < 2 {
return;
}
let path = if let Some(rounding) = spec.corner_rounding {
round_corners(&clipped, false, rounding)
} else {
polyline(&clipped, PolylineOptions::default())
};
let marker_outline_px = endpoint_marker_outline_px(linewidth_px, dpi);
let xform = spec.xform;
if !spec.start_marker.name.is_empty() {
let size_px = pt_to_px(spec.start_marker.size_pt, dpi);
let fill = spec.start_marker.fill.unwrap_or(spec.marker_fill);
let outward = endpoint_outward(&clipped, points, true, clip_start_pt > 0.0);
emit_endpoint_marker(
scene,
clipped[0],
outward,
spec.start_marker.invert,
&spec.start_marker.name,
size_px,
fill,
spec.stroke_color,
marker_outline_px,
xform,
shapes,
spec.pick,
);
}
draw_stroke_with_linetype(
scene,
&path,
false,
spec.stroke_color,
spec.marker_fill,
linewidth_px,
spec.linewidth_pt,
spec.cap,
spec.join,
&spec.dash_pattern_pt,
spec.dash_offset_pt,
xform,
spec.pick,
shapes,
marker_outline_pt,
dpi,
);
if !spec.end_marker.name.is_empty() {
let size_px = pt_to_px(spec.end_marker.size_pt, dpi);
let fill = spec.end_marker.fill.unwrap_or(spec.marker_fill);
let outward = endpoint_outward(&clipped, points, false, clip_end_pt > 0.0);
let placement = *clipped.last().unwrap();
emit_endpoint_marker(
scene,
placement,
outward,
spec.end_marker.invert,
&spec.end_marker.name,
size_px,
fill,
spec.stroke_color,
marker_outline_px,
xform,
shapes,
spec.pick,
);
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_ribbon_mode_curve(
scene: &mut dyn SceneBuilder,
shapes: &ShapeRegistry,
dpi: f64,
points: &[Point],
half_widths: &[f64],
colors: &[Color],
space: ColorSpace,
spec: &OutlineSpec,
marker_xform: Affine,
) {
if points.len() < 2 {
return;
}
let auto_clip_start_pt = auto_endpoint_clip_pt(
&spec.start_marker.name,
spec.start_marker.size_pt,
spec.start_marker.invert,
shapes,
);
let auto_clip_end_pt = auto_endpoint_clip_pt(
&spec.end_marker.name,
spec.end_marker.size_pt,
spec.end_marker.invert,
shapes,
);
let clip_start_pt = spec.user_clip_start_pt + auto_clip_start_pt;
let clip_end_pt = spec.user_clip_end_pt + auto_clip_end_pt;
let (clipped, clipped_widths, clipped_colors) = if clip_start_pt > 0.0 || clip_end_pt > 0.0 {
let start = (clip_start_pt > 0.0).then(|| EndClip::Circle {
center: points[0],
radius: pt_to_px(clip_start_pt, dpi),
});
let end = (clip_end_pt > 0.0).then(|| EndClip::Circle {
center: *points.last().unwrap(),
radius: pt_to_px(clip_end_pt, dpi),
});
clip_polyline_with_attrs(points, half_widths, colors, start, end, space)
} else {
(points.to_vec(), half_widths.to_vec(), colors.to_vec())
};
if clipped.len() < 2 {
return;
}
let marker_outline_px = endpoint_marker_outline_px(pt_to_px(spec.linewidth_pt, dpi), dpi);
if !spec.start_marker.name.is_empty() {
let size_px = pt_to_px(spec.start_marker.size_pt, dpi);
let fill = spec.start_marker.fill.unwrap_or(spec.marker_fill);
let outward = endpoint_outward(&clipped, points, true, clip_start_pt > 0.0);
emit_endpoint_marker(
scene,
clipped[0],
outward,
spec.start_marker.invert,
&spec.start_marker.name,
size_px,
fill,
spec.stroke_color,
marker_outline_px,
marker_xform,
shapes,
spec.pick,
);
}
let opts = RibbonOptions {
half_width: 0.0, cap: spec.cap,
join: spec.join,
miter_limit: RIBBON_MITER_LIMIT,
};
let mesh = polyline_ribbon_full(
&clipped,
Some(&clipped_colors),
Some(&clipped_widths),
&opts,
);
scene.draw_mesh(&mesh, spec.xform, spec.pick);
if !spec.end_marker.name.is_empty() {
let size_px = pt_to_px(spec.end_marker.size_pt, dpi);
let fill = spec.end_marker.fill.unwrap_or(spec.marker_fill);
let outward = endpoint_outward(&clipped, points, false, clip_end_pt > 0.0);
let placement = *clipped.last().unwrap();
emit_endpoint_marker(
scene,
placement,
outward,
spec.end_marker.invert,
&spec.end_marker.name,
size_px,
fill,
spec.stroke_color,
marker_outline_px,
marker_xform,
shapes,
spec.pick,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Rect;
use crate::plot::geom::DirectScaleResolver;
use crate::plot::value::Value;
fn shapes() -> ShapeRegistry {
ShapeRegistry::with_builtins()
}
fn channels(entries: Vec<(&str, Channel)>) -> HashMap<String, Channel> {
entries
.into_iter()
.map(|(name, ch)| (name.to_string(), ch))
.collect()
}
fn spec_for(
ctx: &GeomContext<'_>,
defaults: &ShapeDefaults,
map: &HashMap<String, Channel>,
) -> Option<OutlineSpec> {
let ch = OutlineChannels::from_map(map, "");
let sc = OutlineScales::from_ctx(ctx, "");
resolve_outline_spec(
ctx,
defaults.into(),
&ch,
&sc,
ChannelBind::default(),
0,
PickId::Skip,
)
}
#[test]
fn no_stroke_channel_and_no_theme_stroke_means_no_outline() {
let shapes = shapes();
let resolver = DirectScaleResolver::new();
let ctx = GeomContext::new(Rect::new(0.0, 0.0, 100.0, 100.0), 96.0, &shapes, &resolver);
let defaults = ShapeDefaults::default();
assert!(defaults.stroke.is_none());
assert!(spec_for(&ctx, &defaults, &channels(vec![])).is_none());
}
#[test]
fn an_unbound_stroke_channel_falls_back_to_the_theme_default() {
let shapes = shapes();
let resolver = DirectScaleResolver::new();
let ctx = GeomContext::new(Rect::new(0.0, 0.0, 100.0, 100.0), 96.0, &shapes, &resolver);
let defaults = ShapeDefaults {
stroke: Some(ThemeColor::Ink),
..ShapeDefaults::default()
};
let spec = spec_for(&ctx, &defaults, &channels(vec![])).expect("theme stroke draws");
assert_eq!(spec.stroke_color, ctx.theme.palette.ink);
}
#[test]
fn a_bound_stroke_channel_wins_over_the_theme_default() {
let shapes = shapes();
let resolver = DirectScaleResolver::new();
let ctx = GeomContext::new(Rect::new(0.0, 0.0, 100.0, 100.0), 96.0, &shapes, &resolver);
let red = crate::color::rgb(1.0, 0.0, 0.0);
let defaults = ShapeDefaults {
stroke: Some(ThemeColor::Ink),
..ShapeDefaults::default()
};
let map = channels(vec![("stroke", Channel::Constant(Value::Color(red)))]);
let spec = spec_for(&ctx, &defaults, &map).expect("channel stroke draws");
assert_eq!(spec.stroke_color, red);
}
#[test]
fn stroke_opacity_overrides_the_stroke_colors_own_alpha() {
let shapes = shapes();
let resolver = DirectScaleResolver::new();
let ctx = GeomContext::new(Rect::new(0.0, 0.0, 100.0, 100.0), 96.0, &shapes, &resolver);
let opaque = crate::color::rgba(0.2, 0.4, 0.6, 1.0);
let map = channels(vec![
("stroke", Channel::Constant(Value::Color(opaque))),
("stroke_opacity", Channel::Constant(Value::Number(0.25))),
]);
let spec = spec_for(&ctx, &ShapeDefaults::default(), &map).expect("stroke draws");
let [r, g, b, a] = spec.stroke_color.components;
assert!((r - 0.2).abs() < 1e-6 && (g - 0.4).abs() < 1e-6 && (b - 0.6).abs() < 1e-6);
assert!((a - 0.25).abs() < 1e-6, "{a}");
}
#[test]
fn unbound_style_channels_take_the_supplied_shape_defaults() {
let shapes = shapes();
let resolver = DirectScaleResolver::new();
let ctx = GeomContext::new(Rect::new(0.0, 0.0, 100.0, 100.0), 96.0, &shapes, &resolver);
let defaults = ShapeDefaults {
stroke: Some(ThemeColor::Ink),
linewidth_pt: 3.0,
cap: Cap::Round,
join: Join::Bevel,
..ShapeDefaults::default()
};
let spec = spec_for(&ctx, &defaults, &channels(vec![])).expect("theme stroke draws");
assert_eq!(spec.linewidth_pt, 3.0);
assert!(matches!(spec.cap, Cap::Round));
assert!(matches!(spec.join, Join::Bevel));
assert_eq!(spec.start_marker.size_pt, 9.0);
assert_eq!(spec.end_marker.size_pt, 9.0);
assert_eq!(spec.marker_fill, ctx.theme.palette.ink);
assert!(spec.start_marker.name.is_empty());
assert!(spec.dash_pattern_pt.is_empty(), "unset linetype is solid");
}
#[test]
fn a_suffix_addresses_the_second_curves_channels() {
let shapes = shapes();
let resolver = DirectScaleResolver::new();
let ctx = GeomContext::new(Rect::new(0.0, 0.0, 100.0, 100.0), 96.0, &shapes, &resolver);
let red = crate::color::rgb(1.0, 0.0, 0.0);
let blue = crate::color::rgb(0.0, 0.0, 1.0);
let map = channels(vec![
("stroke", Channel::Constant(Value::Color(red))),
("stroke2", Channel::Constant(Value::Color(blue))),
("linewidth2", Channel::Constant(Value::Number(5.0))),
]);
let defaults = ShapeDefaults::default();
let curve_b = resolve_outline_spec(
&ctx,
(&defaults).into(),
&OutlineChannels::from_map(&map, "2"),
&OutlineScales::from_ctx(&ctx, "2"),
ChannelBind::default(),
0,
PickId::Skip,
)
.expect("curve B has its own stroke");
assert_eq!(curve_b.stroke_color, blue);
assert_eq!(curve_b.linewidth_pt, 5.0);
let curve_a = spec_for(&ctx, &defaults, &map).expect("curve A stroke draws");
assert_eq!(curve_a.stroke_color, red);
assert_eq!(curve_a.linewidth_pt, defaults.linewidth_pt);
}
}