use tiny_skia::{FillRule, FilterQuality, Paint, PathBuilder, Pixmap, SpreadMode, Transform};
use super::graphics::RenderState;
#[derive(Debug, Default)]
pub(crate) struct PathAccumulator {
builder: PathBuilder,
has_content: bool,
last_x: f32,
last_y: f32,
subpath_start_x: f32,
subpath_start_y: f32,
}
impl PathAccumulator {
pub fn new() -> Self {
Self {
builder: PathBuilder::new(),
has_content: false,
last_x: 0.0,
last_y: 0.0,
subpath_start_x: 0.0,
subpath_start_y: 0.0,
}
}
pub fn move_to(&mut self, x: f32, y: f32) {
self.builder.move_to(x, y);
self.last_x = x;
self.last_y = y;
self.subpath_start_x = x;
self.subpath_start_y = y;
self.has_content = true;
}
pub fn line_to(&mut self, x: f32, y: f32) {
self.builder.line_to(x, y);
self.last_x = x;
self.last_y = y;
self.has_content = true;
}
pub fn cubic_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
self.builder.cubic_to(x1, y1, x2, y2, x3, y3);
self.last_x = x3;
self.last_y = y3;
self.has_content = true;
}
pub fn cubic_to_v(&mut self, x2: f32, y2: f32, x3: f32, y3: f32) {
self.builder
.cubic_to(self.last_x, self.last_y, x2, y2, x3, y3);
self.last_x = x3;
self.last_y = y3;
self.has_content = true;
}
pub fn cubic_to_y(&mut self, x1: f32, y1: f32, x3: f32, y3: f32) {
self.builder.cubic_to(x1, y1, x3, y3, x3, y3);
self.last_x = x3;
self.last_y = y3;
self.has_content = true;
}
pub fn close(&mut self) {
self.builder.close();
self.last_x = self.subpath_start_x;
self.last_y = self.subpath_start_y;
}
pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
self.builder.move_to(x, y);
self.builder.line_to(x + w, y);
self.builder.line_to(x + w, y + h);
self.builder.line_to(x, y + h);
self.builder.close();
self.last_x = x;
self.last_y = y;
self.subpath_start_x = x;
self.subpath_start_y = y;
self.has_content = true;
}
pub fn move_to_ops(&mut self, ops: &[&crate::core::objects::Object]) {
if let (Some(x), Some(y)) = (super::colors::op_f32(ops, 0), super::colors::op_f32(ops, 1)) {
self.move_to(x, y);
}
}
pub fn line_to_ops(&mut self, ops: &[&crate::core::objects::Object]) {
if let (Some(x), Some(y)) = (super::colors::op_f32(ops, 0), super::colors::op_f32(ops, 1)) {
self.line_to(x, y);
}
}
pub fn cubic_to_ops(&mut self, ops: &[&crate::core::objects::Object]) {
if ops.len() >= 6 {
if let (Some(x1), Some(y1), Some(x2), Some(y2), Some(x3), Some(y3)) = (
super::colors::op_f32(ops, 0),
super::colors::op_f32(ops, 1),
super::colors::op_f32(ops, 2),
super::colors::op_f32(ops, 3),
super::colors::op_f32(ops, 4),
super::colors::op_f32(ops, 5),
) {
self.cubic_to(x1, y1, x2, y2, x3, y3);
}
}
}
pub fn cubic_to_v_ops(&mut self, ops: &[&crate::core::objects::Object]) {
if ops.len() >= 4 {
if let (Some(x2), Some(y2), Some(x3), Some(y3)) = (
super::colors::op_f32(ops, 0),
super::colors::op_f32(ops, 1),
super::colors::op_f32(ops, 2),
super::colors::op_f32(ops, 3),
) {
self.cubic_to_v(x2, y2, x3, y3);
}
}
}
pub fn cubic_to_y_ops(&mut self, ops: &[&crate::core::objects::Object]) {
if ops.len() >= 4 {
if let (Some(x1), Some(y1), Some(x3), Some(y3)) = (
super::colors::op_f32(ops, 0),
super::colors::op_f32(ops, 1),
super::colors::op_f32(ops, 2),
super::colors::op_f32(ops, 3),
) {
self.cubic_to_y(x1, y1, x3, y3);
}
}
}
pub fn rect_ops(&mut self, ops: &[&crate::core::objects::Object]) {
if ops.len() >= 4 {
if let (Some(x), Some(y), Some(w), Some(h)) = (
super::colors::op_f32(ops, 0),
super::colors::op_f32(ops, 1),
super::colors::op_f32(ops, 2),
super::colors::op_f32(ops, 3),
) {
self.rect(x, y, w, h);
}
}
}
pub fn has_content(&self) -> bool {
self.has_content
}
pub fn finish(self) -> Option<tiny_skia::Path> {
self.builder.finish()
}
pub fn reset(&mut self) {
self.builder = PathBuilder::new();
self.has_content = false;
self.last_x = 0.0;
self.last_y = 0.0;
self.subpath_start_x = 0.0;
self.subpath_start_y = 0.0;
}
}
fn pattern_paint(
pat_data: &super::graphics::PatternData,
blend_mode: tiny_skia::BlendMode,
) -> Paint<'_> {
Paint {
shader: tiny_skia::Pattern::new(
pat_data.pixmap.as_ref(),
SpreadMode::Repeat,
FilterQuality::Bilinear,
1.0,
pat_data.transform,
),
anti_alias: true,
blend_mode,
..Paint::default()
}
}
pub(crate) fn paint_path(
path: &tiny_skia::Path,
state: &RenderState,
pixmap: &mut Pixmap,
fill: bool,
stroke: bool,
fill_rule: FillRule,
clip_mask: Option<&tiny_skia::Mask>,
) {
let ctm = state.ctm;
if fill {
if let Some(ref pat_data) = state.fill_pattern {
let paint = pattern_paint(pat_data, state.blend_mode);
if let Some(transformed_path) = path.clone().transform(ctm) {
pixmap.fill_path(
&transformed_path,
&paint,
fill_rule,
Transform::identity(),
clip_mask,
);
}
} else {
let mut paint = Paint::default();
paint.set_color(state.effective_fill_color());
paint.anti_alias = true;
paint.blend_mode = state.blend_mode;
pixmap.fill_path(path, &paint, fill_rule, ctm, clip_mask);
}
}
if stroke {
let sk_stroke = state.to_stroke();
if let Some(ref pat_data) = state.stroke_pattern {
let paint = pattern_paint(pat_data, state.blend_mode);
if let Some(transformed_path) = path.clone().transform(ctm) {
pixmap.stroke_path(
&transformed_path,
&paint,
&sk_stroke,
Transform::identity(),
clip_mask,
);
}
} else {
let mut paint = Paint::default();
paint.set_color(state.effective_stroke_color());
paint.anti_alias = true;
paint.blend_mode = state.blend_mode;
pixmap.stroke_path(path, &paint, &sk_stroke, ctm, clip_mask);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_accumulator_empty() {
let acc = PathAccumulator::new();
assert!(!acc.has_content());
}
#[test]
fn move_to_marks_content() {
let mut acc = PathAccumulator::new();
acc.move_to(0.0, 0.0);
assert!(acc.has_content());
}
#[test]
fn line_to_marks_content() {
let mut acc = PathAccumulator::new();
acc.move_to(0.0, 0.0);
acc.line_to(10.0, 10.0);
assert!(acc.has_content());
}
#[test]
fn rect_produces_path() {
let mut acc = PathAccumulator::new();
acc.rect(0.0, 0.0, 100.0, 50.0);
assert!(acc.has_content());
let path = acc.finish();
assert!(path.is_some());
let path = path.unwrap();
let bounds = path.bounds();
assert!((bounds.width() - 100.0).abs() < 0.1);
assert!((bounds.height() - 50.0).abs() < 0.1);
}
#[test]
fn cubic_to_marks_content() {
let mut acc = PathAccumulator::new();
acc.move_to(0.0, 0.0);
acc.cubic_to(10.0, 20.0, 30.0, 40.0, 50.0, 0.0);
assert!(acc.has_content());
assert!(acc.finish().is_some());
}
#[test]
fn cubic_to_y_variant() {
let mut acc = PathAccumulator::new();
acc.move_to(0.0, 0.0);
acc.cubic_to_y(10.0, 20.0, 50.0, 0.0);
assert!(acc.has_content());
assert!(acc.finish().is_some());
}
#[test]
fn close_on_rect() {
let mut acc = PathAccumulator::new();
acc.move_to(0.0, 0.0);
acc.line_to(10.0, 0.0);
acc.line_to(10.0, 10.0);
acc.close();
assert!(acc.finish().is_some());
}
#[test]
fn reset_clears_content() {
let mut acc = PathAccumulator::new();
acc.rect(0.0, 0.0, 10.0, 10.0);
assert!(acc.has_content());
acc.reset();
assert!(!acc.has_content());
}
#[test]
fn finish_empty_returns_none() {
let acc = PathAccumulator::new();
assert!(acc.finish().is_none());
}
#[test]
fn v_operator_uses_current_point() {
let mut acc_v = PathAccumulator::new();
acc_v.move_to(0.0, 100.0);
acc_v.cubic_to_v(50.0, 0.0, 100.0, 0.0);
let path_v = acc_v.finish().unwrap();
let mut acc_c = PathAccumulator::new();
acc_c.move_to(0.0, 100.0);
acc_c.cubic_to(0.0, 100.0, 50.0, 0.0, 100.0, 0.0);
let path_c = acc_c.finish().unwrap();
let bv = path_v.bounds();
let bc = path_c.bounds();
assert!((bv.left() - bc.left()).abs() < 0.1);
assert!((bv.top() - bc.top()).abs() < 0.1);
assert!((bv.right() - bc.right()).abs() < 0.1);
assert!((bv.bottom() - bc.bottom()).abs() < 0.1);
}
}