| // 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()); |
| } |
| } |
| } |
| } |
| } |