blob: c1a3ef3fa586d265048097e944c49fcfb652ba47 [file] [log] [blame]
// Copyright 2022 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use bytemuck::{Pod, Zeroable};
use peniko::kurbo::{Cap, Join, Shape, Stroke};
use peniko::Fill;
use super::Monoid;
/// Data structure encoding stroke or fill style.
#[derive(Clone, Copy, Debug, Zeroable, Pod, Default, PartialEq)]
#[repr(C)]
pub struct Style {
/// Encodes the stroke and fill style parameters. This field is split into two 16-bit
/// parts:
///
/// - flags: u16 - encodes fill vs stroke, even-odd vs non-zero fill mode for fills and cap
/// and join style for strokes. See the FLAGS_* constants below for more
/// information.
/// ```text
/// flags: |style|fill|join|start cap|end cap|reserved|
/// bits: 0 1 2-3 4-5 6-7 8-15
/// ```
///
/// - miter_limit: u16 - The miter limit for a stroke, encoded in binary16 (half) floating
/// point representation. This field is only meaningful for the
/// `Join::Miter` join style. It's ignored for other stroke styles and
/// fills.
pub flags_and_miter_limit: u32,
/// Encodes the stroke width. This field is ignored for fills.
pub line_width: f32,
}
impl Style {
/// 0 for a fill, 1 for a stroke
pub const FLAGS_STYLE_BIT: u32 = 0x8000_0000;
/// 0 for non-zero, 1 for even-odd
pub const FLAGS_FILL_BIT: u32 = 0x4000_0000;
/// Encodings for join style:
/// - 0b00 -> bevel
/// - 0b01 -> miter
/// - 0b10 -> round
pub const FLAGS_JOIN_BITS_BEVEL: u32 = 0;
pub const FLAGS_JOIN_BITS_MITER: u32 = 0x1000_0000;
pub const FLAGS_JOIN_BITS_ROUND: u32 = 0x2000_0000;
pub const FLAGS_JOIN_MASK: u32 = 0x3000_0000;
/// Encodings for cap style:
/// - 0b00 -> butt
/// - 0b01 -> square
/// - 0b10 -> round
pub const FLAGS_CAP_BITS_BUTT: u32 = 0;
pub const FLAGS_CAP_BITS_SQUARE: u32 = 0x0100_0000;
pub const FLAGS_CAP_BITS_ROUND: u32 = 0x0200_0000;
pub const FLAGS_START_CAP_BITS_BUTT: u32 = Self::FLAGS_CAP_BITS_BUTT << 2;
pub const FLAGS_START_CAP_BITS_SQUARE: u32 = Self::FLAGS_CAP_BITS_SQUARE << 2;
pub const FLAGS_START_CAP_BITS_ROUND: u32 = Self::FLAGS_CAP_BITS_ROUND << 2;
pub const FLAGS_END_CAP_BITS_BUTT: u32 = Self::FLAGS_CAP_BITS_BUTT;
pub const FLAGS_END_CAP_BITS_SQUARE: u32 = Self::FLAGS_CAP_BITS_SQUARE;
pub const FLAGS_END_CAP_BITS_ROUND: u32 = Self::FLAGS_CAP_BITS_ROUND;
pub const FLAGS_START_CAP_MASK: u32 = 0x0C00_0000;
pub const FLAGS_END_CAP_MASK: u32 = 0x0300_0000;
pub const MITER_LIMIT_MASK: u32 = 0xFFFF;
pub fn from_fill(fill: Fill) -> Self {
let fill_bit = match fill {
Fill::NonZero => 0,
Fill::EvenOdd => Self::FLAGS_FILL_BIT,
};
Self {
flags_and_miter_limit: fill_bit,
line_width: 0.,
}
}
pub fn from_stroke(stroke: &Stroke) -> Self {
let style = Self::FLAGS_STYLE_BIT;
let join = match stroke.join {
Join::Bevel => Self::FLAGS_JOIN_BITS_BEVEL,
Join::Miter => Self::FLAGS_JOIN_BITS_MITER,
Join::Round => Self::FLAGS_JOIN_BITS_ROUND,
};
let start_cap = match stroke.start_cap {
Cap::Butt => Self::FLAGS_START_CAP_BITS_BUTT,
Cap::Square => Self::FLAGS_START_CAP_BITS_SQUARE,
Cap::Round => Self::FLAGS_START_CAP_BITS_ROUND,
};
let end_cap = match stroke.end_cap {
Cap::Butt => Self::FLAGS_END_CAP_BITS_BUTT,
Cap::Square => Self::FLAGS_END_CAP_BITS_SQUARE,
Cap::Round => Self::FLAGS_END_CAP_BITS_ROUND,
};
let miter_limit = crate::math::f32_to_f16(stroke.miter_limit as f32) as u32;
Self {
flags_and_miter_limit: style | join | start_cap | end_cap | miter_limit,
line_width: stroke.width as f32,
}
}
#[cfg(test)]
fn fill(&self) -> Option<Fill> {
if self.is_fill() {
Some(
if (self.flags_and_miter_limit & Self::FLAGS_FILL_BIT) == 0 {
Fill::NonZero
} else {
Fill::EvenOdd
},
)
} else {
None
}
}
#[cfg(test)]
fn stroke_width(&self) -> Option<f64> {
if self.is_fill() {
return None;
}
Some(self.line_width.into())
}
#[cfg(test)]
fn stroke_join(&self) -> Option<Join> {
if self.is_fill() {
return None;
}
let join = self.flags_and_miter_limit & Self::FLAGS_JOIN_MASK;
Some(match join {
Self::FLAGS_JOIN_BITS_BEVEL => Join::Bevel,
Self::FLAGS_JOIN_BITS_MITER => Join::Miter,
Self::FLAGS_JOIN_BITS_ROUND => Join::Round,
_ => unreachable!("unsupported join encoding"),
})
}
#[cfg(test)]
fn stroke_start_cap(&self) -> Option<Cap> {
if self.is_fill() {
return None;
}
let cap = self.flags_and_miter_limit & Self::FLAGS_START_CAP_MASK;
Some(match cap {
Self::FLAGS_START_CAP_BITS_BUTT => Cap::Butt,
Self::FLAGS_START_CAP_BITS_SQUARE => Cap::Square,
Self::FLAGS_START_CAP_BITS_ROUND => Cap::Round,
_ => unreachable!("unsupported start cap encoding"),
})
}
#[cfg(test)]
fn stroke_end_cap(&self) -> Option<Cap> {
if self.is_fill() {
return None;
}
let cap = self.flags_and_miter_limit & Self::FLAGS_END_CAP_MASK;
Some(match cap {
Self::FLAGS_END_CAP_BITS_BUTT => Cap::Butt,
Self::FLAGS_END_CAP_BITS_SQUARE => Cap::Square,
Self::FLAGS_END_CAP_BITS_ROUND => Cap::Round,
_ => unreachable!("unsupported end cap encoding"),
})
}
#[cfg(test)]
fn stroke_miter_limit(&self) -> Option<u16> {
if self.is_fill() {
return None;
}
Some((self.flags_and_miter_limit & Self::MITER_LIMIT_MASK) as u16)
}
#[cfg(test)]
fn is_fill(&self) -> bool {
(self.flags_and_miter_limit & Self::FLAGS_STYLE_BIT) == 0
}
}
/// Line segment (after flattening, before tiling).
#[derive(Clone, Copy, Debug, Zeroable, Pod, Default)]
#[repr(C)]
pub struct LineSoup {
pub path_ix: u32,
pub _padding: u32,
pub p0: [f32; 2],
pub p1: [f32; 2],
}
/// Line segment (after flattening, before tiling).
#[derive(Clone, Copy, Debug, Zeroable, Pod, Default)]
#[repr(C)]
pub struct SegmentCount {
pub line_ix: u32,
// This could more accurately be modeled as:
// segment_within_line: u16,
// segment_within_slice: u16,
// However, here we mirror the way it's written in WGSL
pub counts: u32,
}
/// Path segment.
#[derive(Clone, Copy, Debug, Zeroable, Pod, Default)]
#[repr(C)]
pub struct PathSegment {
// Points are relative to tile origin
pub point0: [f32; 2],
pub point1: [f32; 2],
pub y_edge: f32,
pub _padding: u32,
}
/// Path segment type.
///
/// The values of the segment types are equivalent to the number of associated
/// points for each segment in the path data stream.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Pod, Zeroable)]
#[repr(C)]
pub struct PathSegmentType(pub u8);
impl PathSegmentType {
/// Line segment.
pub const LINE_TO: Self = Self(0x1);
/// Quadratic segment.
pub const QUAD_TO: Self = Self(0x2);
/// Cubic segment.
pub const CUBIC_TO: Self = Self(0x3);
}
/// Path tag representation.
#[derive(Copy, Clone, PartialEq, Eq, Pod, Zeroable)]
#[repr(C)]
pub struct PathTag(pub u8);
impl PathTag {
/// 32-bit floating point line segment.
///
/// This is equivalent to `(PathSegmentType::LINE_TO | PathTag::F32_BIT)`.
pub const LINE_TO_F32: Self = Self(0x9);
/// 32-bit floating point quadratic segment.
///
/// This is equivalent to `(PathSegmentType::QUAD_TO | PathTag::F32_BIT)`.
pub const QUAD_TO_F32: Self = Self(0xa);
/// 32-bit floating point cubic segment.
///
/// This is equivalent to `(PathSegmentType::CUBIC_TO | PathTag::F32_BIT)`.
pub const CUBIC_TO_F32: Self = Self(0xb);
/// 16-bit integral line segment.
pub const LINE_TO_I16: Self = Self(0x1);
/// 16-bit integral quadratic segment.
pub const QUAD_TO_I16: Self = Self(0x2);
/// 16-bit integral cubic segment.
pub const CUBIC_TO_I16: Self = Self(0x3);
/// Transform marker.
pub const TRANSFORM: Self = Self(0x20);
/// Path marker.
pub const PATH: Self = Self(0x10);
/// Style setting.
pub const STYLE: Self = Self(0x40);
/// Bit that marks a segment that is the end of a subpath.
pub const SUBPATH_END_BIT: u8 = 0x4;
/// Bit for path segments that are represented as f32 values. If unset
/// they are represented as i16.
const F32_BIT: u8 = 0x8;
/// Mask for bottom 3 bits that contain the [`PathSegmentType`].
const SEGMENT_MASK: u8 = 0x3;
/// Returns true if the tag is a segment.
pub fn is_path_segment(self) -> bool {
self.path_segment_type().0 != 0
}
/// Returns true if this is a 32-bit floating point segment.
pub fn is_f32(self) -> bool {
self.0 & Self::F32_BIT != 0
}
/// Returns true if this segment ends a subpath.
pub fn is_subpath_end(self) -> bool {
self.0 & Self::SUBPATH_END_BIT != 0
}
/// Sets the subpath end bit.
pub fn set_subpath_end(&mut self) {
self.0 |= Self::SUBPATH_END_BIT;
}
/// Returns the segment type.
pub fn path_segment_type(self) -> PathSegmentType {
PathSegmentType(self.0 & Self::SEGMENT_MASK)
}
}
/// Monoid for the path tag stream.
#[derive(Copy, Clone, Pod, Zeroable, Default, Debug)]
#[repr(C)]
pub struct PathMonoid {
/// Index into transform stream.
pub trans_ix: u32,
/// Path segment index.
pub pathseg_ix: u32,
/// Offset into path segment stream.
pub pathseg_offset: u32,
/// Index into style stream.
pub style_ix: u32,
/// Index of containing path.
pub path_ix: u32,
}
impl Monoid for PathMonoid {
type SourceValue = u32;
/// Reduces a packed 32-bit word containing 4 tags.
fn new(tag_word: u32) -> Self {
let mut c = Self::default();
let point_count = tag_word & 0x3030303;
c.pathseg_ix = ((point_count * 7) & 0x4040404).count_ones();
c.trans_ix = (tag_word & (PathTag::TRANSFORM.0 as u32 * 0x1010101)).count_ones();
let n_points = point_count + ((tag_word >> 2) & 0x1010101);
let mut a = n_points + (n_points & (((tag_word >> 3) & 0x1010101) * 15));
a += a >> 8;
a += a >> 16;
c.pathseg_offset = a & 0xff;
c.path_ix = (tag_word & (PathTag::PATH.0 as u32 * 0x1010101)).count_ones();
let style_size = (std::mem::size_of::<Style>() / std::mem::size_of::<u32>()) as u32;
c.style_ix = (tag_word & (PathTag::STYLE.0 as u32 * 0x1010101)).count_ones() * style_size;
c
}
/// Monoid combination.
fn combine(&self, other: &Self) -> Self {
Self {
trans_ix: self.trans_ix + other.trans_ix,
pathseg_ix: self.pathseg_ix + other.pathseg_ix,
pathseg_offset: self.pathseg_offset + other.pathseg_offset,
style_ix: self.style_ix + other.style_ix,
path_ix: self.path_ix + other.path_ix,
}
}
}
/// Cubic path segment.
#[derive(Copy, Clone, Pod, Zeroable, Debug, Default)]
#[repr(C)]
pub struct Cubic {
pub p0: [f32; 2],
pub p1: [f32; 2],
pub p2: [f32; 2],
pub p3: [f32; 2],
pub stroke: [f32; 2],
pub path_ix: u32,
pub flags: u32,
}
/// Path bounding box.
#[derive(Copy, Clone, Pod, Zeroable, Default, Debug)]
#[repr(C)]
pub struct PathBbox {
/// Minimum x value.
pub x0: i32,
/// Minimum y value.
pub y0: i32,
/// Maximum x value.
pub x1: i32,
/// Maximum y value.
pub y1: i32,
/// Style flags
pub draw_flags: u32,
/// Index into the transform stream.
pub trans_ix: u32,
}
/// Tiled path object.
#[derive(Copy, Clone, Pod, Zeroable, Debug, Default)]
#[repr(C)]
pub struct Path {
/// Bounding box in tiles.
pub bbox: [u32; 4],
/// Offset (in u32s) to tile rectangle.
pub tiles: u32,
_padding: [u32; 3],
}
/// Tile object.
#[derive(Copy, Clone, Pod, Zeroable, Debug, Default)]
#[repr(C)]
pub struct Tile {
/// Accumulated backdrop at the left edge of the tile.
pub backdrop: i32,
/// An enum that holds either the count of the number of path
/// segments in this tile, or an index to the beginning of an
/// allocated slice of `PathSegment` objects. In the latter case,
/// the bits are inverted.
pub segment_count_or_ix: u32,
}
/// Encoder for path segments.
pub struct PathEncoder<'a> {
tags: &'a mut Vec<PathTag>,
data: &'a mut Vec<u8>,
n_segments: &'a mut u32,
n_paths: &'a mut u32,
first_point: [f32; 2],
first_start_tangent_end: [f32; 2],
state: PathState,
n_encoded_segments: u32,
is_fill: bool,
}
#[derive(PartialEq)]
enum PathState {
Start,
MoveTo,
NonemptySubpath,
}
impl<'a> PathEncoder<'a> {
/// Creates a new path encoder for the specified path tags and data.
///
/// If `is_fill` is true, ensures that all subpaths are closed. Otherwise, the path is treated
/// as a stroke and an additional "stroke cap marker" segment is inserted at the end of every
/// subpath.
///
/// Stroke Encoding
/// ---------------
/// Every subpath within a stroked path is terminated with a "stroke cap marker" segment. This
/// segment tells the GPU stroker whether to draw a cap or a join based on the topology of the
/// path:
///
/// 1. This marker segment is encoded as a `quad-to` (2 additional points) for an open path and
/// a `line-to` (1 additional point) for a closed path. An open path gets drawn with a start
/// and end cap. A closed path gets drawn with a single join in place of the caps where the
/// subpath's start and end control points meet.
///
/// 2. The marker segment tells the GPU flattening stage how to render caps and joins while
/// processing each path segment in parallel. All subpaths end with the marker segment which
/// is the only segment that has the `SUBPATH_END_BIT` set to 1.
///
/// The algorithm is as follows:
///
/// a) If a GPU thread is processing a regular segment (i.e. `SUBPATH_END_BIT` is 0), it
/// outputs the offset curves for the segment. If the segment is immediately followed by
/// the marker segment, then the same thread draws an end cap if the subpath is open
/// (i.e. the marker is a quad-to) or a join if the subpath is closed (i.e. the marker is
/// a line-to) using the tangent encoded in the marker segment.
///
/// If the segment is immediately followed by another regular segment, then the thread
/// draws a join using the start tangent of the neighboring segment.
///
/// b) If a GPU thread is processing the marker segment (i.e. `SUBPATH_END_BIT` is 1), then
/// it draws a start cap using the information encoded in the segment IF the subpath is
/// open (i.e. the marker is a quad-to). If the subpath is closed (i.e. the marker is a
/// line-to), the thread draws nothing.
pub fn new(
tags: &'a mut Vec<PathTag>,
data: &'a mut Vec<u8>,
n_segments: &'a mut u32,
n_paths: &'a mut u32,
is_fill: bool,
) -> Self {
Self {
tags,
data,
n_segments,
n_paths,
first_point: [0.0, 0.0],
first_start_tangent_end: [0.0, 0.0],
state: PathState::Start,
n_encoded_segments: 0,
is_fill,
}
}
/// Encodes a move, starting a new subpath.
pub fn move_to(&mut self, x: f32, y: f32) {
if self.is_fill {
self.close();
}
let buf = [x, y];
let bytes = bytemuck::bytes_of(&buf);
if self.state == PathState::MoveTo {
let new_len = self.data.len() - 8;
self.data.truncate(new_len);
} else if self.state == PathState::NonemptySubpath {
if !self.is_fill {
self.insert_stroke_cap_marker_segment(false);
}
if let Some(tag) = self.tags.last_mut() {
tag.set_subpath_end();
}
}
self.first_point = buf;
self.data.extend_from_slice(bytes);
self.state = PathState::MoveTo;
}
/// Encodes a line.
pub fn line_to(&mut self, x: f32, y: f32) {
if self.state == PathState::Start {
if self.n_encoded_segments == 0 {
// This copies the behavior of kurbo which treats an initial line, quad
// or curve as a move.
self.move_to(x, y);
return;
}
self.move_to(self.first_point[0], self.first_point[1]);
}
if self.state == PathState::MoveTo {
// Ensure that we don't end up with a zero-length start tangent.
let Some((x, y)) = self.start_tangent_for_curve((x, y), None, None) else {
return;
};
self.first_start_tangent_end = [x, y];
}
// Drop the segment if its length is zero
if self.is_zero_length_segment((x, y), None, None) {
return;
}
let buf = [x, y];
let bytes = bytemuck::bytes_of(&buf);
self.data.extend_from_slice(bytes);
self.tags.push(PathTag::LINE_TO_F32);
self.state = PathState::NonemptySubpath;
self.n_encoded_segments += 1;
}
/// Encodes a quadratic bezier.
pub fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
if self.state == PathState::Start {
if self.n_encoded_segments == 0 {
self.move_to(x2, y2);
return;
}
self.move_to(self.first_point[0], self.first_point[1]);
}
if self.state == PathState::MoveTo {
// Ensure that we don't end up with a zero-length start tangent.
let Some((x, y)) = self.start_tangent_for_curve((x1, y1), Some((x2, y2)), None) else {
return;
};
self.first_start_tangent_end = [x, y];
}
// Drop the segment if its length is zero
if self.is_zero_length_segment((x1, y1), Some((x2, y2)), None) {
return;
}
let buf = [x1, y1, x2, y2];
let bytes = bytemuck::bytes_of(&buf);
self.data.extend_from_slice(bytes);
self.tags.push(PathTag::QUAD_TO_F32);
self.state = PathState::NonemptySubpath;
self.n_encoded_segments += 1;
}
/// Encodes a cubic bezier.
pub fn cubic_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
if self.state == PathState::Start {
if self.n_encoded_segments == 0 {
self.move_to(x3, y3);
return;
}
self.move_to(self.first_point[0], self.first_point[1]);
}
if self.state == PathState::MoveTo {
// Ensure that we don't end up with a zero-length start tangent.
let Some((x, y)) =
self.start_tangent_for_curve((x1, y1), Some((x2, y2)), Some((x3, y3)))
else {
return;
};
self.first_start_tangent_end = [x, y];
}
// Drop the segment if its length is zero
if self.is_zero_length_segment((x1, y1), Some((x2, y2)), Some((x3, y3))) {
return;
}
let buf = [x1, y1, x2, y2, x3, y3];
let bytes = bytemuck::bytes_of(&buf);
self.data.extend_from_slice(bytes);
self.tags.push(PathTag::CUBIC_TO_F32);
self.state = PathState::NonemptySubpath;
self.n_encoded_segments += 1;
}
/// Closes the current subpath.
pub fn close(&mut self) {
match self.state {
PathState::Start => return,
PathState::MoveTo => {
let new_len = self.data.len() - 8;
self.data.truncate(new_len);
self.state = PathState::Start;
return;
}
PathState::NonemptySubpath => (),
}
let len = self.data.len();
if len < 8 {
// can't happen
return;
}
let first_bytes = bytemuck::bytes_of(&self.first_point);
if &self.data[len - 8..len] != first_bytes {
self.data.extend_from_slice(first_bytes);
self.tags.push(PathTag::LINE_TO_F32);
self.n_encoded_segments += 1;
}
if !self.is_fill {
self.insert_stroke_cap_marker_segment(true);
}
if let Some(tag) = self.tags.last_mut() {
tag.set_subpath_end();
}
self.state = PathState::Start;
}
/// Encodes a shape.
pub fn shape(&mut self, shape: &impl Shape) {
self.path_elements(shape.path_elements(0.1));
}
/// Encodes a path iterator
pub fn path_elements(&mut self, path: impl Iterator<Item = peniko::kurbo::PathEl>) {
use peniko::kurbo::PathEl;
for el in path {
match el {
PathEl::MoveTo(p0) => self.move_to(p0.x as f32, p0.y as f32),
PathEl::LineTo(p0) => self.line_to(p0.x as f32, p0.y as f32),
PathEl::QuadTo(p0, p1) => {
self.quad_to(p0.x as f32, p0.y as f32, p1.x as f32, p1.y as f32);
}
PathEl::CurveTo(p0, p1, p2) => self.cubic_to(
p0.x as f32,
p0.y as f32,
p1.x as f32,
p1.y as f32,
p2.x as f32,
p2.y as f32,
),
PathEl::ClosePath => self.close(),
}
}
}
/// Completes path encoding and returns the actual number of encoded segments.
///
/// If `insert_path_marker` is true, encodes the [`PathTag::PATH`] tag to signify
/// the end of a complete path object. Setting this to false allows encoding
/// multiple paths with differing transforms for a single draw object.
pub fn finish(mut self, insert_path_marker: bool) -> u32 {
if self.is_fill {
self.close();
}
if self.state == PathState::MoveTo {
let new_len = self.data.len() - 8;
self.data.truncate(new_len);
}
if self.n_encoded_segments != 0 {
if !self.is_fill && self.state == PathState::NonemptySubpath {
self.insert_stroke_cap_marker_segment(false);
}
if let Some(tag) = self.tags.last_mut() {
tag.set_subpath_end();
}
*self.n_segments += self.n_encoded_segments;
if insert_path_marker {
self.tags.push(PathTag::PATH);
*self.n_paths += 1;
}
}
self.n_encoded_segments
}
fn insert_stroke_cap_marker_segment(&mut self, is_closed: bool) {
assert!(!self.is_fill);
assert!(self.state == PathState::NonemptySubpath);
if is_closed {
// We expect that the most recently encoded pair of coordinates in the path data stream
// contain the first control point in the path segment (see `PathEncoder::close`).
// Hence a line-to encoded here should embed the subpath's start tangent.
self.line_to(
self.first_start_tangent_end[0],
self.first_start_tangent_end[1],
);
} else {
self.quad_to(
self.first_point[0],
self.first_point[1],
self.first_start_tangent_end[0],
self.first_start_tangent_end[1],
);
}
}
fn last_point(&self) -> Option<(f32, f32)> {
let len = self.data.len();
if len < 8 {
return None;
}
let pts: &[f32; 2] = bytemuck::from_bytes(&self.data[len - 8..len]);
Some((pts[0], pts[1]))
}
fn is_zero_length_segment(
&self,
p1: (f32, f32),
p2: Option<(f32, f32)>,
p3: Option<(f32, f32)>,
) -> bool {
let p0 = self.last_point().unwrap();
let p2 = p2.unwrap_or(p1);
let p3 = p3.unwrap_or(p1);
let x_min = p0.0.min(p1.0.min(p2.0.min(p3.0)));
let x_max = p0.0.max(p1.0.max(p2.0.max(p3.0)));
let y_min = p0.1.min(p1.1.min(p2.1.min(p3.1)));
let y_max = p0.1.max(p1.1.max(p2.1.max(p3.1)));
!(x_max - x_min > EPSILON || y_max - y_min > EPSILON)
}
// Returns the end point of the start tangent of a curve starting at `(x0, y0)`, or `None` if the
// curve is degenerate / has zero-length. The inputs are a sequence of control points that can
// represent a line, a quadratic Bezier, or a cubic Bezier. Lines and quadratic Beziers can be
// passed to this function by simply setting the invalid control point degrees equal to `None`.
//
// `self.first_point` is always treated as the first control point of the curve.
fn start_tangent_for_curve(
&self,
p1: (f32, f32),
p2: Option<(f32, f32)>,
p3: Option<(f32, f32)>,
) -> Option<(f32, f32)> {
let p0 = (self.first_point[0], self.first_point[1]);
let p2 = p2.unwrap_or(p0);
let p3 = p3.unwrap_or(p0);
let pt = if (p1.0 - p0.0).abs() > EPSILON || (p1.1 - p0.1).abs() > EPSILON {
p1
} else if (p2.0 - p0.0).abs() > EPSILON || (p2.1 - p0.1).abs() > EPSILON {
p2
} else if (p3.0 - p0.0).abs() > EPSILON || (p3.1 - p0.1).abs() > EPSILON {
p3
} else {
return None;
};
Some(pt)
}
}
#[cfg(feature = "full")]
impl skrifa::outline::OutlinePen for PathEncoder<'_> {
fn move_to(&mut self, x: f32, y: f32) {
self.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.line_to(x, y);
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.quad_to(cx0, cy0, x, y);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.cubic_to(cx0, cy0, cx1, cy1, x, y);
}
fn close(&mut self) {
self.close();
}
}
const EPSILON: f32 = 1e-12;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fill_style() {
assert_eq!(Some(Fill::NonZero), Style::from_fill(Fill::NonZero).fill());
assert_eq!(Some(Fill::EvenOdd), Style::from_fill(Fill::EvenOdd).fill());
assert_eq!(None, Style::from_stroke(&Stroke::default()).fill());
}
#[test]
fn test_stroke_style() {
assert_eq!(None, Style::from_fill(Fill::NonZero).stroke_width());
assert_eq!(None, Style::from_fill(Fill::EvenOdd).stroke_width());
let caps = [Cap::Butt, Cap::Square, Cap::Round];
let joins = [Join::Bevel, Join::Miter, Join::Round];
for start in caps {
for end in caps {
for join in joins {
let stroke = Stroke::new(1.0)
.with_start_cap(start)
.with_end_cap(end)
.with_join(join)
.with_miter_limit(0.);
let encoded = Style::from_stroke(&stroke);
assert_eq!(Some(stroke.width), encoded.stroke_width());
assert_eq!(Some(stroke.join), encoded.stroke_join());
assert_eq!(Some(stroke.start_cap), encoded.stroke_start_cap());
assert_eq!(Some(stroke.end_cap), encoded.stroke_end_cap());
assert_eq!(Some(0), encoded.stroke_miter_limit());
}
}
}
}
}