summaryrefslogtreecommitdiff
path: root/tvg/src/path.rs
blob: 2bc04480f22239b7b8e663f3beaf3d6cb506d91b (plain)
use std::io::Read;

use byteorder::ReadBytesExt;
use num_enum::TryFromPrimitive;
use raise::yeet;

use crate::{header::TvgHeader, read_unit, read_varuint, Decode, Point, TvgError};

/// Returns an error if the padding isn't zero
fn check_padding(padding: u8) -> Result<(), TvgError> {
	if padding != 0 {
		yeet!(TvgError::NonZeroPadding(padding))
	}

	Ok(())
}

#[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,
	) -> Result<Self, TvgError> {
		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) -> Result<Self, TvgError> {
		Ok(Self::Line {
			position: Point::read(reader, header)?,
		})
	}

	fn read_horizontal_line(reader: &mut impl Read, header: &TvgHeader) -> Result<Self, TvgError> {
		Ok(Self::HorizontalLine {
			x: read_unit(reader, header)?,
		})
	}

	fn read_vertical_line(reader: &mut impl Read, header: &TvgHeader) -> Result<Self, TvgError> {
		Ok(Self::VerticalLine {
			y: read_unit(reader, header)?,
		})
	}

	fn read_cubic_bezier(reader: &mut impl Read, header: &TvgHeader) -> Result<Self, TvgError> {
		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,
	) -> Result<(bool, Sweep), TvgError> {
		// 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,
		};

		check_padding((byte & 0b1111_1100) >> 2)?;

		Ok((large_arc, sweep))
	}

	fn read_arc_circle(reader: &mut impl Read, header: &TvgHeader) -> Result<Self, TvgError> {
		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) -> Result<Self, TvgError> {
		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) -> Result<Self, TvgError> {
		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) -> Result<Self, TvgError> {
		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;

		check_padding((byte & 0b0000_1000) >> 3)?;
		check_padding((byte & 0b1110_0000) >> 5)?;

		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,
	) -> Result<Self, TvgError> {
		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,
	) -> Result<Self, TvgError> {
		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(),
		})
	}
}