summaryrefslogtreecommitdiff
path: root/tvg/src/path.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tvg/src/path.rs')
-rw-r--r--tvg/src/path.rs294
1 files changed, 294 insertions, 0 deletions
diff --git a/tvg/src/path.rs b/tvg/src/path.rs
new file mode 100644
index 0000000..d2bf4fb
--- /dev/null
+++ b/tvg/src/path.rs
@@ -0,0 +1,294 @@
+use std::io::{self, Read};
+
+use byteorder::ReadBytesExt;
+use num_enum::TryFromPrimitive;
+
+use crate::{header::TvgHeader, read_unit, read_varuint, Decode, Point};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)]
+#[repr(u8)]
+enum Sweep {
+ Right = 0,
+ Left = 1,
+}
+
+/// An instruction to move a hypothetical "pen".
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)]
+#[repr(u8)]
+enum InstructionKind {
+ /// A straight line is drawn from the current point to a new point.
+ Line = 0,
+ /// A straight horizontal line is drawn from the current point to a new x
+ /// coordinate.
+ HorizontalLine = 1,
+ /// A straight vertical line is drawn from the current point to a new y
+ /// coordiante.
+ VerticalLine = 2,
+ /// A cubic Bézier curve is drawn from the current point to a new point.
+ CubicBezier = 3,
+ /// A circle segment is drawn from current point to a new point.
+ ArcCircle = 4,
+ /// An ellipse segment is drawn from current point to a new point.
+ ArcEllipse = 5,
+ /// The path is closed, and a straight line is drawn to the starting point.
+ ClosePath = 6,
+ /// A quadratic Bézier curve is drawn from the current point to a new point.
+ QuadraticBezier = 7,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum InstructionData {
+ /// The line instruction draws a straight line to the position.
+ Line {
+ /// The end point of the line.
+ position: Point,
+ },
+ /// The horizontal line instruction draws a straight horizontal line to a
+ /// given x coordinate.
+ HorizontalLine {
+ /// The new x coordinate.
+ x: f64,
+ },
+ /// The vertical line instruction draws a straight vertical line to a given
+ /// y coordinate.
+ VerticalLine {
+ /// The new y coordinate.
+ y: f64,
+ },
+ /// The cubic bezier instruction draws a Bézier curve with two control
+ /// points.
+ ///
+ /// The curve is drawn between the current location and `point_1` with
+ /// `control_0` being the first control point and `control_1` being the
+ /// second one.
+ CubicBezier {
+ /// The first control point.
+ control_0: Point,
+ /// The second control point.
+ control_1: Point,
+ /// The end point of the Bézier curve.
+ point_1: Point,
+ },
+ /// Draws a circle segment between the current and the target point.
+ ///
+ /// The `radius` field determines the radius of the circle. If the distance
+ /// between the current point and `target` is larger than `radius`, the
+ /// distance is used as the radius.
+ ArcCircle {
+ /// If `true`, the large portion of the circle segment is drawn.
+ large_arc: bool,
+ /// Determines if the circle segment is left- or right bending.
+ sweep: Sweep,
+ /// The radius of the circle.
+ radius: f64,
+ /// The end point of the circle segment.
+ target: Point,
+ },
+ /// Draws an ellipse segment between the current and the target point.
+ ///
+ /// The `radius_x` and `radius_y` fields determine the both radii of the
+ /// ellipse. If the distance between the current point and target is not
+ /// enough to fit any ellipse segment between the two points, `radius_x`
+ /// and `radius_y` are scaled uniformly so that it fits exactly.
+ ArcEllipse {
+ /// If `true`, the large portion of the ellipse segment is drawn.
+ large_arc: bool,
+ /// Determines if the ellipse segment is left- or right bending.
+ sweep: Sweep,
+ /// The radius of the ellipse segment in the horizontal direction.
+ radius_x: f64,
+ /// The radius of the ellipse segment in the vertical direction.
+ radius_y: f64,
+ /// The rotation of the ellipse in mathematical negative direction, in
+ /// degrees.
+ rotation: f64,
+ /// The end point of the ellipse segment.
+ target: Point,
+ },
+ /// A straight line is drawn to the start location of the current segment.
+ /// This instruction doesn’t have additional data encoded.
+ ClosePath,
+ /// The quadratic bezier instruction draws a Bézier curve with a single
+ /// control point.
+ ///
+ /// The curve is drawn between the current location and `point_1` with
+ /// control being the control point.
+ QuadraticBezier {
+ /// The control point.
+ control: Point,
+ /// The end point of the Bézier curve.
+ target: Point,
+ },
+}
+
+impl InstructionData {
+ fn read(reader: &mut impl Read, header: &TvgHeader, kind: InstructionKind) -> io::Result<Self> {
+ match kind {
+ InstructionKind::Line => Self::read_line(reader, header),
+ InstructionKind::HorizontalLine => Self::read_horizontal_line(reader, header),
+ InstructionKind::VerticalLine => Self::read_vertical_line(reader, header),
+ InstructionKind::CubicBezier => Self::read_cubic_bezier(reader, header),
+ InstructionKind::ArcCircle => Self::read_arc_circle(reader, header),
+ InstructionKind::ArcEllipse => Self::read_arc_ellipse(reader, header),
+ InstructionKind::ClosePath => Ok(Self::ClosePath),
+ InstructionKind::QuadraticBezier => Self::read_quadratic_bezier(reader, header),
+ }
+ }
+
+ fn read_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::Line {
+ position: Point::read(reader, header)?,
+ })
+ }
+
+ fn read_horizontal_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::HorizontalLine {
+ x: read_unit(reader, header)?,
+ })
+ }
+
+ fn read_vertical_line(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::VerticalLine {
+ y: read_unit(reader, header)?,
+ })
+ }
+
+ fn read_cubic_bezier(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::CubicBezier {
+ control_0: Point::read(reader, header)?,
+ control_1: Point::read(reader, header)?,
+ point_1: Point::read(reader, header)?,
+ })
+ }
+
+ fn read_arc_header(reader: &mut impl Read, header: &TvgHeader) -> io::Result<(bool, Sweep)> {
+ // large_arc and sweep are stored in the same byte
+ let byte = reader.read_u8()?;
+ let large_arc = (byte & 1) != 0;
+ let sweep = match byte & 2 {
+ 0 => Sweep::Left,
+ _ => Sweep::Right,
+ };
+
+ Ok((large_arc, sweep))
+ }
+
+ fn read_arc_circle(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ let (large_arc, sweep) = Self::read_arc_header(reader, header)?;
+ let radius = read_unit(reader, header)?;
+ let target = Point::read(reader, header)?;
+
+ Ok(Self::ArcCircle {
+ large_arc,
+ sweep,
+ radius,
+ target,
+ })
+ }
+
+ fn read_arc_ellipse(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ let (large_arc, sweep) = Self::read_arc_header(reader, header)?;
+ let radius_x = read_unit(reader, header)?;
+ let radius_y = read_unit(reader, header)?;
+ let rotation = read_unit(reader, header)?;
+ let target = Point::read(reader, header)?;
+
+ Ok(Self::ArcEllipse {
+ large_arc,
+ sweep,
+ radius_x,
+ radius_y,
+ rotation,
+ target,
+ })
+ }
+
+ fn read_quadratic_bezier(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ Ok(Self::QuadraticBezier {
+ control: Point::read(reader, header)?,
+ target: Point::read(reader, header)?,
+ })
+ }
+}
+
+#[derive(Debug, Clone)]
+struct Instruction {
+ /// The width of the line the "pen" makes, if it makes one at all.
+ line_width: Option<f64>,
+ /// The arguments to the instruction.
+ data: InstructionData,
+}
+
+impl Instruction {
+ fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
+ let byte = reader.read_u8()?;
+ let instruction_kind =
+ InstructionKind::try_from_primitive(byte & 0b0000_0111).expect("invalid instruction");
+ let has_line_width = (byte & 0b0001_0000) != 0;
+
+ let line_width = has_line_width
+ .then(|| read_unit(reader, header))
+ .transpose()?;
+ let data = InstructionData::read(reader, header, instruction_kind)?;
+
+ Ok(Self { line_width, data })
+ }
+}
+
+#[derive(Debug, Clone)]
+struct Segment {
+ /// The starting point of the segment.
+ start: Point,
+ /// The list of instructions for tha segment.
+ instructions: Box<[Instruction]>,
+}
+
+impl Segment {
+ fn read(reader: &mut impl Read, header: &TvgHeader, segment_length: u32) -> io::Result<Self> {
+ let start = Point::read(reader, header)?;
+
+ let mut instructions = Vec::with_capacity(segment_length as usize);
+ for _ in 0..segment_length {
+ instructions.push(Instruction::read(reader, header)?)
+ }
+
+ Ok(Segment {
+ start,
+ instructions: instructions.into_boxed_slice(),
+ })
+ }
+}
+
+/// Paths describe instructions to create complex 2D graphics.
+///
+/// Each path segment generates a shape by moving a ”pen” around. The path this
+/// ”pen” takes is the outline of our segment. Each segment, the ”pen” starts
+/// at a defined position and is moved by instructions. Each instruction will
+/// leave the ”pen” at a new position. The line drawn by our ”pen” is the
+/// outline of the shape.
+#[derive(Debug, Clone)]
+pub struct Path {
+ segments: Box<[Segment]>,
+}
+
+impl Path {
+ pub(crate) fn read(
+ reader: &mut impl Read,
+ header: &TvgHeader,
+ segment_count: u32,
+ ) -> io::Result<Self> {
+ let mut segment_lengths = Vec::with_capacity(segment_count as usize);
+ for _ in 0..segment_count {
+ segment_lengths.push(read_varuint(reader)? + 1);
+ }
+
+ let mut segments = Vec::with_capacity(segment_count as usize);
+ for segment_length in segment_lengths {
+ segments.push(Segment::read(reader, header, segment_length)?);
+ }
+
+ Ok(Self {
+ segments: segments.into_boxed_slice(),
+ })
+ }
+}