2022-08-13 23:05:38 +00:00
|
|
|
use crate::matrix::Matrix;
|
2023-03-20 03:47:58 +00:00
|
|
|
use enum_map::Enum;
|
2020-08-21 06:25:31 +00:00
|
|
|
use smallvec::SmallVec;
|
2023-03-03 10:54:56 +00:00
|
|
|
use swf::{CharacterId, FillStyle, LineStyle, Rectangle, Shape, ShapeRecord, Twips};
|
2019-05-17 20:25:01 +00:00
|
|
|
|
2023-08-25 13:25:51 +00:00
|
|
|
/// Controls the accuracy of the approximated quadratic curve, when splitting up a cubic curve
|
|
|
|
const CUBIC_CURVE_TOLERANCE: f64 = 0.01;
|
|
|
|
|
2023-03-20 03:47:58 +00:00
|
|
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
|
|
pub enum FillRule {
|
|
|
|
EvenOdd,
|
|
|
|
NonZero,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Enum)]
|
|
|
|
pub enum GradientType {
|
|
|
|
Linear,
|
|
|
|
Radial,
|
|
|
|
Focal,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(feature = "tessellator")]
|
|
|
|
impl From<FillRule> for lyon::path::FillRule {
|
|
|
|
fn from(rule: FillRule) -> lyon::path::FillRule {
|
|
|
|
match rule {
|
|
|
|
FillRule::EvenOdd => lyon::path::FillRule::EvenOdd,
|
|
|
|
FillRule::NonZero => lyon::path::FillRule::NonZero,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-03-19 00:45:26 +00:00
|
|
|
|
2022-09-22 21:40:12 +00:00
|
|
|
pub fn calculate_shape_bounds(shape_records: &[swf::ShapeRecord]) -> swf::Rectangle<Twips> {
|
2019-05-12 16:55:48 +00:00
|
|
|
let mut bounds = swf::Rectangle {
|
2021-02-12 12:09:04 +00:00
|
|
|
x_min: Twips::new(i32::MAX),
|
|
|
|
y_min: Twips::new(i32::MAX),
|
|
|
|
x_max: Twips::new(i32::MIN),
|
|
|
|
y_max: Twips::new(i32::MIN),
|
2019-05-12 16:55:48 +00:00
|
|
|
};
|
2023-05-11 19:36:23 +00:00
|
|
|
let mut cursor = swf::Point::ZERO;
|
2019-05-12 16:55:48 +00:00
|
|
|
for record in shape_records {
|
|
|
|
match record {
|
|
|
|
swf::ShapeRecord::StyleChange(style_change) => {
|
2023-05-02 18:49:37 +00:00
|
|
|
if let Some(move_to) = &style_change.move_to {
|
2023-05-11 19:36:23 +00:00
|
|
|
cursor = *move_to;
|
|
|
|
bounds.x_min = bounds.x_min.min(cursor.x);
|
|
|
|
bounds.x_max = bounds.x_max.max(cursor.x);
|
|
|
|
bounds.y_min = bounds.y_min.min(cursor.y);
|
|
|
|
bounds.y_max = bounds.y_max.max(cursor.y);
|
2019-05-12 16:55:48 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-02 19:40:00 +00:00
|
|
|
swf::ShapeRecord::StraightEdge { delta } => {
|
2023-05-11 19:36:23 +00:00
|
|
|
cursor += *delta;
|
|
|
|
bounds.x_min = bounds.x_min.min(cursor.x);
|
|
|
|
bounds.x_max = bounds.x_max.max(cursor.x);
|
|
|
|
bounds.y_min = bounds.y_min.min(cursor.y);
|
|
|
|
bounds.y_max = bounds.y_max.max(cursor.y);
|
2019-05-12 16:55:48 +00:00
|
|
|
}
|
|
|
|
swf::ShapeRecord::CurvedEdge {
|
2023-05-02 19:40:00 +00:00
|
|
|
control_delta,
|
|
|
|
anchor_delta,
|
2019-05-12 16:55:48 +00:00
|
|
|
} => {
|
2023-05-11 19:36:23 +00:00
|
|
|
cursor += *control_delta;
|
2023-08-01 18:26:12 +00:00
|
|
|
let control = cursor;
|
2023-05-11 19:36:23 +00:00
|
|
|
cursor += *anchor_delta;
|
2023-08-01 18:26:12 +00:00
|
|
|
let anchor = cursor;
|
|
|
|
bounds = bounds.union(&quadratic_curve_bounds(
|
|
|
|
cursor,
|
|
|
|
Twips::ZERO,
|
|
|
|
control,
|
|
|
|
anchor,
|
|
|
|
));
|
2019-05-12 16:55:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if bounds.x_max < bounds.x_min || bounds.y_max < bounds.y_min {
|
2019-05-17 20:25:01 +00:00
|
|
|
bounds = Default::default();
|
2019-05-12 16:55:48 +00:00
|
|
|
}
|
|
|
|
bounds
|
|
|
|
}
|
2019-05-24 17:25:03 +00:00
|
|
|
|
|
|
|
/// `DrawPath` represents a solid fill or a stroke.
|
|
|
|
/// Fills are always closed paths, while strokes may be open or closed.
|
|
|
|
/// Closed paths will have the first point equal to the last point.
|
2023-03-19 00:45:26 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
2019-05-24 17:25:03 +00:00
|
|
|
pub enum DrawPath<'a> {
|
|
|
|
Stroke {
|
|
|
|
style: &'a LineStyle,
|
|
|
|
is_closed: bool,
|
|
|
|
commands: Vec<DrawCommand>,
|
|
|
|
},
|
|
|
|
Fill {
|
|
|
|
style: &'a FillStyle,
|
|
|
|
commands: Vec<DrawCommand>,
|
2023-03-19 00:45:26 +00:00
|
|
|
winding_rule: FillRule,
|
2019-05-24 17:25:03 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-04-17 10:14:31 +00:00
|
|
|
/// `DistilledShape` represents a ready-to-be-consumed collection of paths (both fills and strokes)
|
|
|
|
/// that has been converted down from another source (such as SWF's `swf::Shape` format).
|
2023-03-19 00:45:26 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
2020-04-17 10:14:31 +00:00
|
|
|
pub struct DistilledShape<'a> {
|
|
|
|
pub paths: Vec<DrawPath<'a>>,
|
2023-03-03 10:54:56 +00:00
|
|
|
pub shape_bounds: Rectangle<Twips>,
|
|
|
|
pub edge_bounds: Rectangle<Twips>,
|
2020-04-17 10:14:31 +00:00
|
|
|
pub id: CharacterId,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> From<&'a swf::Shape> for DistilledShape<'a> {
|
|
|
|
fn from(shape: &'a Shape) -> Self {
|
|
|
|
Self {
|
|
|
|
paths: ShapeConverter::from_shape(shape).into_commands(),
|
2023-03-03 10:54:56 +00:00
|
|
|
shape_bounds: shape.shape_bounds.clone(),
|
|
|
|
edge_bounds: shape.edge_bounds.clone(),
|
2020-04-17 10:14:31 +00:00
|
|
|
id: shape.id,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
/// `DrawCommands` trace the outline of a path.
|
|
|
|
/// Fills follow the even-odd fill rule, with opposite winding for holes.
|
2022-05-22 06:01:34 +00:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2019-05-24 17:25:03 +00:00
|
|
|
pub enum DrawCommand {
|
2023-05-11 20:03:39 +00:00
|
|
|
MoveTo(swf::Point<Twips>),
|
2023-05-11 20:16:21 +00:00
|
|
|
LineTo(swf::Point<Twips>),
|
2023-08-25 11:12:56 +00:00
|
|
|
QuadraticCurveTo {
|
2023-05-11 20:32:16 +00:00
|
|
|
control: swf::Point<Twips>,
|
|
|
|
anchor: swf::Point<Twips>,
|
2019-05-24 17:25:03 +00:00
|
|
|
},
|
2023-08-25 13:20:44 +00:00
|
|
|
CubicCurveTo {
|
|
|
|
control_a: swf::Point<Twips>,
|
|
|
|
control_b: swf::Point<Twips>,
|
|
|
|
anchor: swf::Point<Twips>,
|
|
|
|
},
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
|
2020-05-21 14:45:22 +00:00
|
|
|
impl DrawCommand {
|
2023-05-11 20:03:39 +00:00
|
|
|
pub fn end_point(&self) -> swf::Point<Twips> {
|
2020-05-21 14:45:22 +00:00
|
|
|
match self {
|
2023-05-11 20:32:16 +00:00
|
|
|
DrawCommand::MoveTo(point)
|
|
|
|
| DrawCommand::LineTo(point)
|
2023-08-25 13:20:44 +00:00
|
|
|
| DrawCommand::QuadraticCurveTo { anchor: point, .. }
|
|
|
|
| DrawCommand::CubicCurveTo { anchor: point, .. } => *point,
|
2020-05-21 14:45:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-22 06:01:34 +00:00
|
|
|
#[derive(Clone, Copy, Debug)]
|
2019-05-24 17:25:03 +00:00
|
|
|
struct Point {
|
|
|
|
x: Twips,
|
|
|
|
y: Twips,
|
|
|
|
is_bezier_control: bool,
|
|
|
|
}
|
|
|
|
|
2023-05-11 20:03:39 +00:00
|
|
|
impl From<Point> for swf::Point<Twips> {
|
|
|
|
fn from(point: Point) -> Self {
|
|
|
|
Self::new(point.x, point.y)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
/// A continuous series of edges in a path.
|
|
|
|
/// Fill segments are directed, because the winding determines the fill-rule.
|
|
|
|
/// Stroke segments are undirected.
|
|
|
|
#[derive(Clone, Debug)]
|
2019-05-24 17:25:03 +00:00
|
|
|
struct PathSegment {
|
|
|
|
pub points: Vec<Point>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PathSegment {
|
2023-05-14 17:36:31 +00:00
|
|
|
fn new(start: swf::Point<Twips>) -> Self {
|
2019-05-24 17:25:03 +00:00
|
|
|
Self {
|
|
|
|
points: vec![Point {
|
2023-05-14 17:36:31 +00:00
|
|
|
x: start.x,
|
|
|
|
y: start.y,
|
2019-05-24 17:25:03 +00:00
|
|
|
is_bezier_control: false,
|
|
|
|
}],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
fn reset(&mut self, start: swf::Point<Twips>) {
|
2022-01-26 07:04:01 +00:00
|
|
|
self.points.clear();
|
|
|
|
self.points.push(Point {
|
2023-05-14 17:36:31 +00:00
|
|
|
x: start.x,
|
|
|
|
y: start.y,
|
2022-01-26 07:04:01 +00:00
|
|
|
is_bezier_control: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
/// Flips the direction of the path segment.
|
|
|
|
/// Flash fill paths are dual-sided, with fill style 1 indicating the positive side
|
|
|
|
/// and fill style 0 indicating the negative. We have to flip fill style 0 paths
|
|
|
|
/// in order to link them to fill style 1 paths.
|
|
|
|
fn flip(&mut self) {
|
|
|
|
self.points.reverse();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds an edge to the end of the path segment.
|
|
|
|
fn add_point(&mut self, point: Point) {
|
|
|
|
self.points.push(point);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_empty(&self) -> bool {
|
|
|
|
self.points.len() <= 1
|
|
|
|
}
|
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
fn start(&self) -> Option<swf::Point<Twips>> {
|
|
|
|
Some((*self.points.first()?).into())
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
fn end(&self) -> Option<swf::Point<Twips>> {
|
|
|
|
Some((*self.points.last()?).into())
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn is_closed(&self) -> bool {
|
|
|
|
self.start() == self.end()
|
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
fn to_draw_commands(&self) -> impl '_ + Iterator<Item = DrawCommand> {
|
|
|
|
assert!(!self.is_empty());
|
|
|
|
let mut i = self.points.iter();
|
2023-01-09 15:00:39 +00:00
|
|
|
let first = i.next().expect("Points should not be empty");
|
2023-05-11 20:03:39 +00:00
|
|
|
std::iter::once(DrawCommand::MoveTo((*first).into())).chain(std::iter::from_fn(move || {
|
|
|
|
match i.next() {
|
2023-05-11 20:16:21 +00:00
|
|
|
Some(
|
|
|
|
point @ Point {
|
|
|
|
is_bezier_control: false,
|
|
|
|
..
|
|
|
|
},
|
|
|
|
) => Some(DrawCommand::LineTo((*point).into())),
|
2023-05-11 20:32:16 +00:00
|
|
|
Some(
|
|
|
|
point @ Point {
|
|
|
|
is_bezier_control: true,
|
|
|
|
..
|
|
|
|
},
|
|
|
|
) => {
|
2023-05-11 20:03:39 +00:00
|
|
|
let end = i.next().expect("Bezier without endpoint");
|
2023-08-25 11:12:56 +00:00
|
|
|
Some(DrawCommand::QuadraticCurveTo {
|
2023-05-11 20:32:16 +00:00
|
|
|
control: (*point).into(),
|
|
|
|
anchor: (*end).into(),
|
2023-05-11 20:03:39 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
None => None,
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The internal path structure used by ShapeConverter.
|
|
|
|
///
|
|
|
|
/// Each path is uniquely identified by its fill/stroke style. But Flash gives
|
|
|
|
/// the path edges as an "edge soup" -- they can arrive in an arbitrary order.
|
|
|
|
/// We have to link the edges together for each path. This structure contains
|
|
|
|
/// a list of path segment, and each time a path segment is added, it will try
|
|
|
|
/// to merge it with an existing segment.
|
2022-01-26 07:04:01 +00:00
|
|
|
#[derive(Clone, Debug)]
|
2019-05-24 17:25:03 +00:00
|
|
|
struct PendingPath {
|
|
|
|
/// The list of path segments for this fill/stroke.
|
|
|
|
/// For fills, this should turn into a list of closed paths when the shape is complete.
|
|
|
|
/// Strokes may or may not be closed.
|
|
|
|
segments: Vec<PathSegment>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PendingPath {
|
|
|
|
fn new() -> Self {
|
|
|
|
Self { segments: vec![] }
|
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
/// Adds a path segment to the path, attempting to link it to existing segments.
|
|
|
|
fn add_segment(&mut self, mut new_segment: PathSegment) {
|
2019-05-24 17:25:03 +00:00
|
|
|
if !new_segment.is_empty() {
|
2022-01-26 07:04:01 +00:00
|
|
|
// Try to link this segment onto existing segments with a matching endpoint.
|
|
|
|
// Both the start and the end points of the new segment can be linked.
|
|
|
|
let mut start_open = true;
|
|
|
|
let mut end_open = true;
|
|
|
|
let mut i = 0;
|
|
|
|
while (start_open || end_open) && i < self.segments.len() {
|
|
|
|
let other = &mut self.segments[i];
|
|
|
|
if start_open && other.end() == new_segment.start() {
|
|
|
|
other.points.extend_from_slice(&new_segment.points[1..]);
|
|
|
|
new_segment = self.segments.swap_remove(i);
|
|
|
|
start_open = false;
|
|
|
|
} else if end_open && new_segment.end() == other.start() {
|
|
|
|
std::mem::swap(&mut other.points, &mut new_segment.points);
|
|
|
|
other.points.extend_from_slice(&new_segment.points[1..]);
|
|
|
|
new_segment = self.segments.swap_remove(i);
|
|
|
|
end_open = false;
|
|
|
|
} else {
|
|
|
|
i += 1;
|
|
|
|
}
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
2022-01-26 07:04:01 +00:00
|
|
|
// The segment can't link to any further segments. Add it to list.
|
|
|
|
self.segments.push(new_segment);
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
fn push_path(&mut self, segment: PathSegment) {
|
|
|
|
self.segments.push(segment);
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
fn to_draw_commands(&self) -> impl '_ + Iterator<Item = DrawCommand> {
|
|
|
|
self.segments.iter().flat_map(PathSegment::to_draw_commands)
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
#[derive(Clone, Debug)]
|
2019-05-24 17:25:03 +00:00
|
|
|
pub struct ActivePath {
|
2022-01-26 07:04:01 +00:00
|
|
|
style_id: u32,
|
2019-05-24 17:25:03 +00:00
|
|
|
segment: PathSegment,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ActivePath {
|
2022-01-26 07:04:01 +00:00
|
|
|
fn new() -> Self {
|
2019-05-24 17:25:03 +00:00
|
|
|
Self {
|
2022-01-26 07:04:01 +00:00
|
|
|
style_id: 0,
|
2023-05-14 17:36:31 +00:00
|
|
|
segment: PathSegment::new(swf::Point::ZERO),
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn add_point(&mut self, point: Point) {
|
|
|
|
self.segment.add_point(point)
|
|
|
|
}
|
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
fn flush_fill(&mut self, start: swf::Point<Twips>, pending: &mut [PendingPath], flip: bool) {
|
2022-01-26 07:04:01 +00:00
|
|
|
if self.style_id > 0 && !self.segment.is_empty() {
|
|
|
|
if flip {
|
|
|
|
self.segment.flip();
|
|
|
|
}
|
|
|
|
pending[self.style_id as usize - 1].add_segment(self.segment.clone());
|
|
|
|
}
|
|
|
|
self.segment.reset(start);
|
|
|
|
}
|
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
fn flush_stroke(&mut self, start: swf::Point<Twips>, pending: &mut [PendingPath]) {
|
2022-01-26 07:04:01 +00:00
|
|
|
if self.style_id > 0 && !self.segment.is_empty() {
|
|
|
|
pending[self.style_id as usize - 1].push_path(self.segment.clone());
|
|
|
|
}
|
|
|
|
self.segment.reset(start);
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct ShapeConverter<'a> {
|
|
|
|
// SWF shape commands.
|
|
|
|
iter: std::slice::Iter<'a, swf::ShapeRecord>,
|
|
|
|
|
|
|
|
// Pen position.
|
2023-05-14 17:36:31 +00:00
|
|
|
cursor: swf::Point<Twips>,
|
2019-05-24 17:25:03 +00:00
|
|
|
|
|
|
|
// Fill styles and line styles.
|
|
|
|
// These change from StyleChangeRecords, and a flush occurs when these change.
|
|
|
|
fill_styles: &'a [swf::FillStyle],
|
|
|
|
line_styles: &'a [swf::LineStyle],
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
fill_style0: ActivePath,
|
|
|
|
fill_style1: ActivePath,
|
|
|
|
line_style: ActivePath,
|
2023-03-19 00:45:26 +00:00
|
|
|
winding_rule: FillRule,
|
2019-05-24 17:25:03 +00:00
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
// Paths. These get flushed for each new layer.
|
|
|
|
fills: Vec<PendingPath>,
|
|
|
|
strokes: Vec<PendingPath>,
|
2019-05-24 17:25:03 +00:00
|
|
|
|
|
|
|
// Output.
|
|
|
|
commands: Vec<DrawPath<'a>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> ShapeConverter<'a> {
|
|
|
|
const DEFAULT_CAPACITY: usize = 512;
|
|
|
|
|
|
|
|
fn from_shape(shape: &'a swf::Shape) -> Self {
|
|
|
|
ShapeConverter {
|
|
|
|
iter: shape.shape.iter(),
|
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
cursor: swf::Point::ZERO,
|
2019-05-24 17:25:03 +00:00
|
|
|
|
|
|
|
fill_styles: &shape.styles.fill_styles,
|
|
|
|
line_styles: &shape.styles.line_styles,
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
fill_style0: ActivePath::new(),
|
|
|
|
fill_style1: ActivePath::new(),
|
|
|
|
line_style: ActivePath::new(),
|
2019-05-24 17:25:03 +00:00
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
fills: vec![PendingPath::new(); shape.styles.fill_styles.len()],
|
|
|
|
strokes: vec![PendingPath::new(); shape.styles.line_styles.len()],
|
2019-05-24 17:25:03 +00:00
|
|
|
|
|
|
|
commands: Vec::with_capacity(Self::DEFAULT_CAPACITY),
|
2023-03-19 00:45:26 +00:00
|
|
|
|
2023-03-19 00:49:08 +00:00
|
|
|
winding_rule: if shape.flags.contains(swf::ShapeFlag::NON_ZERO_WINDING_RULE) {
|
2023-03-19 00:45:26 +00:00
|
|
|
FillRule::NonZero
|
|
|
|
} else {
|
|
|
|
FillRule::EvenOdd
|
|
|
|
},
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn into_commands(mut self) -> Vec<DrawPath<'a>> {
|
2021-06-21 16:29:52 +00:00
|
|
|
// As u32 is okay because SWF has a max of 65536 fills (TODO: should be u16?)
|
2020-10-10 22:22:19 +00:00
|
|
|
let mut num_fill_styles = self.fill_styles.len() as u32;
|
|
|
|
let mut num_line_styles = self.line_styles.len() as u32;
|
2019-05-24 17:25:03 +00:00
|
|
|
while let Some(record) = self.iter.next() {
|
|
|
|
match record {
|
|
|
|
ShapeRecord::StyleChange(style_change) => {
|
2023-05-02 18:49:37 +00:00
|
|
|
if let Some(move_to) = &style_change.move_to {
|
2023-05-14 17:36:31 +00:00
|
|
|
self.cursor = *move_to;
|
2019-05-24 17:25:03 +00:00
|
|
|
// We've lifted the pen, so we're starting a new path.
|
|
|
|
// Flush the previous path.
|
|
|
|
self.flush_paths();
|
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
if let Some(styles) = &style_change.new_styles {
|
2019-05-24 17:25:03 +00:00
|
|
|
// A new style list is also used to indicate a new drawing layer.
|
|
|
|
self.flush_layer();
|
2023-05-14 17:36:31 +00:00
|
|
|
self.fill_styles = &styles.fill_styles;
|
|
|
|
self.line_styles = &styles.line_styles;
|
2022-01-26 07:04:01 +00:00
|
|
|
self.fills
|
|
|
|
.resize_with(self.fill_styles.len(), PendingPath::new);
|
|
|
|
self.strokes
|
|
|
|
.resize_with(self.line_styles.len(), PendingPath::new);
|
2020-10-10 22:22:19 +00:00
|
|
|
num_fill_styles = self.fill_styles.len() as u32;
|
|
|
|
num_line_styles = self.line_styles.len() as u32;
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
if let Some(new_style_id) = style_change.fill_style_1 {
|
|
|
|
self.fill_style1
|
2023-05-14 17:36:31 +00:00
|
|
|
.flush_fill(self.cursor, &mut self.fills, false);
|
2022-01-26 07:04:01 +00:00
|
|
|
// Validate in case we index an invalid fill style.
|
|
|
|
// <= because fill ID 0 (no fill) is implicit, so the array is actually 1-based
|
|
|
|
self.fill_style1.style_id = if new_style_id <= num_fill_styles {
|
|
|
|
new_style_id
|
2019-05-24 17:25:03 +00:00
|
|
|
} else {
|
2022-01-26 07:04:01 +00:00
|
|
|
0
|
|
|
|
};
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
if let Some(new_style_id) = style_change.fill_style_0 {
|
|
|
|
self.fill_style0
|
2023-05-14 17:36:31 +00:00
|
|
|
.flush_fill(self.cursor, &mut self.fills, true);
|
2022-01-26 07:04:01 +00:00
|
|
|
self.fill_style0.style_id = if new_style_id <= num_fill_styles {
|
|
|
|
new_style_id
|
2019-05-24 17:25:03 +00:00
|
|
|
} else {
|
2022-01-26 07:04:01 +00:00
|
|
|
0
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-26 07:04:01 +00:00
|
|
|
if let Some(new_style_id) = style_change.line_style {
|
2023-05-14 17:36:31 +00:00
|
|
|
self.line_style.flush_stroke(self.cursor, &mut self.strokes);
|
2022-01-26 07:04:01 +00:00
|
|
|
self.line_style.style_id = if new_style_id <= num_line_styles {
|
|
|
|
new_style_id
|
2019-05-24 17:25:03 +00:00
|
|
|
} else {
|
2022-01-26 07:04:01 +00:00
|
|
|
0
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-05-02 19:40:00 +00:00
|
|
|
ShapeRecord::StraightEdge { delta } => {
|
2023-05-14 17:36:31 +00:00
|
|
|
self.cursor += *delta;
|
|
|
|
self.visit_point(false);
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
ShapeRecord::CurvedEdge {
|
2023-05-02 19:40:00 +00:00
|
|
|
control_delta,
|
|
|
|
anchor_delta,
|
2019-05-24 17:25:03 +00:00
|
|
|
} => {
|
2023-05-14 17:36:31 +00:00
|
|
|
self.cursor += *control_delta;
|
|
|
|
self.visit_point(true);
|
2019-05-24 17:25:03 +00:00
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
self.cursor += *anchor_delta;
|
|
|
|
self.visit_point(false);
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Flush any open paths.
|
|
|
|
self.flush_layer();
|
|
|
|
self.commands
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds a point to the current path for the active fills/strokes.
|
2023-05-14 17:36:31 +00:00
|
|
|
fn visit_point(&mut self, is_bezier_control: bool) {
|
|
|
|
let point = Point {
|
|
|
|
x: self.cursor.x,
|
|
|
|
y: self.cursor.y,
|
|
|
|
is_bezier_control,
|
|
|
|
};
|
2022-01-26 07:04:01 +00:00
|
|
|
if self.fill_style1.style_id > 0 {
|
|
|
|
self.fill_style1.add_point(point);
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
2022-01-26 07:04:01 +00:00
|
|
|
if self.fill_style0.style_id > 0 {
|
|
|
|
self.fill_style0.add_point(point);
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
2022-01-26 07:04:01 +00:00
|
|
|
if self.line_style.style_id > 0 {
|
|
|
|
self.line_style.add_point(point);
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// When the pen jumps to a new position, we reset the active path.
|
|
|
|
fn flush_paths(&mut self) {
|
|
|
|
// Move the current paths to the active list.
|
2022-01-26 07:04:01 +00:00
|
|
|
self.fill_style1
|
2023-05-14 17:36:31 +00:00
|
|
|
.flush_fill(self.cursor, &mut self.fills, false);
|
2022-01-26 07:04:01 +00:00
|
|
|
self.fill_style0
|
2023-05-14 17:36:31 +00:00
|
|
|
.flush_fill(self.cursor, &mut self.fills, true);
|
|
|
|
self.line_style.flush_stroke(self.cursor, &mut self.strokes);
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// When a new layer starts, all paths are flushed and turned into drawing commands.
|
2020-08-13 02:04:14 +00:00
|
|
|
fn flush_layer(&mut self) {
|
2019-05-24 17:25:03 +00:00
|
|
|
self.flush_paths();
|
|
|
|
|
|
|
|
// Draw fills, and then strokes.
|
2022-01-26 07:04:01 +00:00
|
|
|
// Paths are drawn in order of style id, not based on the order of the draw commands.
|
|
|
|
for (i, path) in self.fills.iter_mut().enumerate() {
|
2020-10-10 22:22:19 +00:00
|
|
|
// These invariants are checked above (any invalid/empty fill ID should not have been added).
|
2022-01-26 07:04:01 +00:00
|
|
|
debug_assert!(i < self.fill_styles.len());
|
|
|
|
if path.segments.is_empty() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let style = unsafe { self.fill_styles.get_unchecked(i) };
|
2019-05-24 17:25:03 +00:00
|
|
|
self.commands.push(DrawPath::Fill {
|
|
|
|
style,
|
2022-01-26 07:04:01 +00:00
|
|
|
commands: path.to_draw_commands().collect(),
|
2023-03-19 00:45:26 +00:00
|
|
|
winding_rule: self.winding_rule,
|
2019-05-24 17:25:03 +00:00
|
|
|
});
|
2022-01-26 07:04:01 +00:00
|
|
|
path.segments.clear();
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Strokes are drawn last because they always appear on top of fills in the same layer.
|
|
|
|
// Because path segments can either be open or closed, we convert each stroke segment into
|
|
|
|
// a separate draw command.
|
2022-01-26 07:04:01 +00:00
|
|
|
for (i, path) in self.strokes.iter_mut().enumerate() {
|
|
|
|
debug_assert!(i < self.line_styles.len());
|
|
|
|
let style = unsafe { self.line_styles.get_unchecked(i) };
|
|
|
|
for segment in &path.segments {
|
|
|
|
if segment.is_empty() {
|
|
|
|
continue;
|
|
|
|
}
|
2019-05-24 17:25:03 +00:00
|
|
|
self.commands.push(DrawPath::Stroke {
|
|
|
|
style,
|
|
|
|
is_closed: segment.is_closed(),
|
2022-01-26 07:04:01 +00:00
|
|
|
commands: segment.to_draw_commands().collect(),
|
2019-05-24 17:25:03 +00:00
|
|
|
});
|
|
|
|
}
|
2022-01-26 07:04:01 +00:00
|
|
|
path.segments.clear();
|
2019-05-24 17:25:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-21 06:25:31 +00:00
|
|
|
/* SHAPEFLAG HITTEST (point-in-contour)
|
|
|
|
*
|
|
|
|
* To determine whether a point is inside a shape, we shoot a ray on the +x axis and calculate a winding number based
|
|
|
|
* on the edges that intersect with the ray.
|
|
|
|
*
|
|
|
|
* For each edge:
|
|
|
|
* if the edge cross the ray downward (+y), we add 1 to the winding number.
|
|
|
|
* if the edge cross the ray upward (-y), we add -1 to the winding number.
|
|
|
|
*
|
2020-09-19 14:27:24 +00:00
|
|
|
* We must also handle intersection with edge endpoints consistently to avoid double counting:
|
2020-08-21 06:25:31 +00:00
|
|
|
* the initial point of an edge is considered for upwards rays.
|
|
|
|
* the final point of an edge is considered for downward rays.
|
|
|
|
*
|
|
|
|
* For SWF shapes, edges with fillstyle1 use clockwise winding, and edges with fillstyle0 use CCW winding (flip them).
|
|
|
|
* We ignore any edges with fills on both sides (interior edges).
|
|
|
|
*
|
|
|
|
* If the final winding number is odd, then the point is inside the shape (for default even-odd winding).
|
|
|
|
*
|
|
|
|
* For strokes, we calculate the distance to the line segment or curve and compare it to the stroke width.
|
|
|
|
* Note that Flash renders with a minimum stroke width of 1px (20 twips) that we must account for.
|
|
|
|
* TODO: We currently don't consider non-round endcaps or joins, or stroke scaling flags.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/// Test whether the given point in object space is contained within the contour of the given shape.
|
|
|
|
/// local_matrix is used to calculate the proper stroke widths.
|
|
|
|
pub fn shape_hit_test(
|
|
|
|
shape: &swf::Shape,
|
2023-05-11 20:42:11 +00:00
|
|
|
test_point: swf::Point<Twips>,
|
2020-08-21 06:25:31 +00:00
|
|
|
local_matrix: &Matrix,
|
|
|
|
) -> bool {
|
2023-05-11 20:42:11 +00:00
|
|
|
let mut cursor = swf::Point::ZERO;
|
2020-08-21 06:25:31 +00:00
|
|
|
let mut winding = 0;
|
|
|
|
|
2023-05-11 20:42:11 +00:00
|
|
|
let mut has_fill_style0 = false;
|
|
|
|
let mut has_fill_style1 = false;
|
2020-08-21 06:25:31 +00:00
|
|
|
|
2021-06-22 10:04:27 +00:00
|
|
|
let min_width = stroke_minimum_width(local_matrix);
|
2020-08-21 06:25:31 +00:00
|
|
|
let mut stroke_width = None;
|
|
|
|
let mut line_styles = &shape.styles.line_styles;
|
|
|
|
|
|
|
|
for record in &shape.shape {
|
|
|
|
match record {
|
|
|
|
swf::ShapeRecord::StyleChange(style_change) => {
|
|
|
|
// New styles indicates a new layer;
|
|
|
|
// Check if the point is within the current layer, then reset winding.
|
|
|
|
if let Some(new_styles) = &style_change.new_styles {
|
|
|
|
if winding & 0b1 != 0 {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
line_styles = &new_styles.line_styles;
|
|
|
|
winding = 0;
|
|
|
|
}
|
|
|
|
|
2023-05-02 18:49:37 +00:00
|
|
|
if let Some(move_to) = &style_change.move_to {
|
2023-05-11 20:42:11 +00:00
|
|
|
cursor = *move_to;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(i) = style_change.fill_style_0 {
|
|
|
|
has_fill_style0 = i > 0;
|
|
|
|
}
|
|
|
|
if let Some(i) = style_change.fill_style_1 {
|
|
|
|
has_fill_style1 = i > 0;
|
|
|
|
}
|
|
|
|
if let Some(i) = style_change.line_style {
|
|
|
|
stroke_width = if i > 0 {
|
|
|
|
// Flash renders strokes with a 1px minimum width.
|
|
|
|
if let Some(line_style) = line_styles.get(i as usize - 1) {
|
2022-04-09 18:17:40 +00:00
|
|
|
let width = line_style.width().get() as f64;
|
2020-08-21 06:25:31 +00:00
|
|
|
let scaled_width = 0.5 * width.max(min_width);
|
|
|
|
Some((scaled_width, scaled_width * scaled_width))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2023-05-02 19:40:00 +00:00
|
|
|
swf::ShapeRecord::StraightEdge { delta } => {
|
2023-05-11 20:42:11 +00:00
|
|
|
let end = cursor + *delta;
|
|
|
|
|
2020-08-21 06:25:31 +00:00
|
|
|
// If this edge has a fill style on only one-side, check for a crossing.
|
|
|
|
if has_fill_style1 {
|
|
|
|
if !has_fill_style0 {
|
2023-05-14 17:36:31 +00:00
|
|
|
winding += winding_number_line(test_point, cursor, end);
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
} else if has_fill_style0 {
|
2023-05-14 17:36:31 +00:00
|
|
|
winding += winding_number_line(test_point, end, cursor);
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(width) = stroke_width {
|
2023-05-14 17:36:31 +00:00
|
|
|
if hit_test_stroke(test_point, cursor, end, width) {
|
2020-08-21 06:25:31 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2023-05-11 20:42:11 +00:00
|
|
|
|
|
|
|
cursor = end;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
swf::ShapeRecord::CurvedEdge {
|
2023-05-02 19:40:00 +00:00
|
|
|
control_delta,
|
|
|
|
anchor_delta,
|
2020-08-21 06:25:31 +00:00
|
|
|
} => {
|
2023-05-11 20:42:11 +00:00
|
|
|
let control = cursor + *control_delta;
|
|
|
|
let anchor = control + *anchor_delta;
|
2020-08-21 06:25:31 +00:00
|
|
|
|
|
|
|
// If this edge has a fill style on only one-side, check for a crossing.
|
|
|
|
if has_fill_style1 {
|
|
|
|
if !has_fill_style0 {
|
2023-05-14 17:36:31 +00:00
|
|
|
winding += winding_number_curve(test_point, cursor, control, anchor);
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
} else if has_fill_style0 {
|
2023-05-14 17:36:31 +00:00
|
|
|
winding += winding_number_curve(test_point, anchor, control, cursor);
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(width) = stroke_width {
|
2023-05-14 17:36:31 +00:00
|
|
|
if hit_test_stroke_curve(test_point, cursor, control, anchor, width) {
|
2020-08-21 06:25:31 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-11 20:42:11 +00:00
|
|
|
cursor = anchor;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
winding & 0b1 != 0
|
|
|
|
}
|
|
|
|
|
2021-06-21 16:29:52 +00:00
|
|
|
/// Test whether the given point is contained within the paths specified by the draw commands.
|
2023-05-14 17:36:31 +00:00
|
|
|
pub fn draw_command_fill_hit_test(commands: &[DrawCommand], test_point: swf::Point<Twips>) -> bool {
|
2023-05-11 20:03:39 +00:00
|
|
|
let mut cursor = swf::Point::ZERO;
|
|
|
|
let mut fill_start = swf::Point::ZERO;
|
2020-08-21 06:25:31 +00:00
|
|
|
let mut winding = 0;
|
|
|
|
|
|
|
|
// Draw command only contains a single fill, so don't have to worry about fill styles.
|
|
|
|
for command in commands {
|
2023-05-11 20:03:39 +00:00
|
|
|
match command {
|
|
|
|
DrawCommand::MoveTo(move_to) => {
|
|
|
|
cursor = *move_to;
|
|
|
|
fill_start = *move_to;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
2023-05-11 20:16:21 +00:00
|
|
|
DrawCommand::LineTo(line_to) => {
|
2023-05-14 17:36:31 +00:00
|
|
|
winding += winding_number_line(test_point, cursor, *line_to);
|
2023-05-11 20:16:21 +00:00
|
|
|
cursor = *line_to;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
2023-08-25 11:12:56 +00:00
|
|
|
DrawCommand::QuadraticCurveTo { control, anchor } => {
|
2023-05-14 17:36:31 +00:00
|
|
|
winding += winding_number_curve(test_point, cursor, *control, *anchor);
|
2023-05-11 20:32:16 +00:00
|
|
|
cursor = *anchor;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
2023-08-25 13:20:44 +00:00
|
|
|
DrawCommand::CubicCurveTo {
|
|
|
|
control_a,
|
|
|
|
control_b,
|
|
|
|
anchor,
|
|
|
|
} => {
|
|
|
|
lyon_geom::CubicBezierSegment {
|
|
|
|
from: lyon_geom::Point::new(cursor.x.to_pixels(), cursor.y.to_pixels()),
|
|
|
|
ctrl1: lyon_geom::Point::new(control_a.x.to_pixels(), control_a.y.to_pixels()),
|
|
|
|
ctrl2: lyon_geom::Point::new(control_b.x.to_pixels(), control_b.y.to_pixels()),
|
|
|
|
to: lyon_geom::Point::new(anchor.x.to_pixels(), anchor.y.to_pixels()),
|
|
|
|
}
|
2023-08-25 13:25:51 +00:00
|
|
|
.for_each_quadratic_bezier(
|
|
|
|
CUBIC_CURVE_TOLERANCE,
|
|
|
|
&mut |quadratic_curve| {
|
|
|
|
winding += winding_number_curve(
|
|
|
|
test_point,
|
|
|
|
swf::Point::from_pixels(quadratic_curve.from.x, quadratic_curve.from.y),
|
|
|
|
swf::Point::from_pixels(quadratic_curve.ctrl.x, quadratic_curve.ctrl.y),
|
|
|
|
swf::Point::from_pixels(quadratic_curve.to.x, quadratic_curve.to.y),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
2023-08-25 13:20:44 +00:00
|
|
|
cursor = *anchor;
|
|
|
|
}
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-31 23:01:58 +00:00
|
|
|
if cursor != fill_start {
|
|
|
|
// Close fill.
|
2023-05-14 17:36:31 +00:00
|
|
|
winding += winding_number_line(test_point, cursor, fill_start);
|
2021-10-31 23:01:58 +00:00
|
|
|
}
|
|
|
|
|
2020-08-21 06:25:31 +00:00
|
|
|
winding & 0b1 != 0
|
|
|
|
}
|
|
|
|
|
2021-06-21 16:29:52 +00:00
|
|
|
/// Test whether the given point is contained within the strokes specified by the draw commands.
|
2020-08-21 06:25:31 +00:00
|
|
|
/// local_matrix is used to calculate the minimum stroke width.
|
|
|
|
pub fn draw_command_stroke_hit_test(
|
|
|
|
commands: &[DrawCommand],
|
|
|
|
stroke_width: Twips,
|
2023-05-14 17:36:31 +00:00
|
|
|
test_point: swf::Point<Twips>,
|
2020-08-21 06:25:31 +00:00
|
|
|
local_matrix: &Matrix,
|
|
|
|
) -> bool {
|
2021-06-22 10:04:27 +00:00
|
|
|
let stroke_min_width = stroke_minimum_width(local_matrix);
|
2020-08-21 06:25:31 +00:00
|
|
|
let stroke_width = 0.5 * f64::max(stroke_width.get().into(), stroke_min_width);
|
|
|
|
let stroke_widths = (stroke_width, stroke_width * stroke_width);
|
2023-05-11 20:03:39 +00:00
|
|
|
let mut cursor = swf::Point::ZERO;
|
2020-08-21 06:25:31 +00:00
|
|
|
for command in commands {
|
2023-05-11 20:03:39 +00:00
|
|
|
match command {
|
|
|
|
DrawCommand::MoveTo(move_to) => {
|
|
|
|
cursor = *move_to;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
2023-05-11 20:16:21 +00:00
|
|
|
DrawCommand::LineTo(line_to) => {
|
2023-05-14 17:36:31 +00:00
|
|
|
if hit_test_stroke(test_point, cursor, *line_to, stroke_widths) {
|
2020-08-21 06:25:31 +00:00
|
|
|
return true;
|
|
|
|
}
|
2023-05-11 20:16:21 +00:00
|
|
|
cursor = *line_to;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
2023-08-25 11:12:56 +00:00
|
|
|
DrawCommand::QuadraticCurveTo { control, anchor } => {
|
2023-05-14 17:36:31 +00:00
|
|
|
if hit_test_stroke_curve(test_point, cursor, *control, *anchor, stroke_widths) {
|
2020-08-21 06:25:31 +00:00
|
|
|
return true;
|
|
|
|
}
|
2023-05-11 20:32:16 +00:00
|
|
|
cursor = *anchor;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
2023-08-25 13:20:44 +00:00
|
|
|
DrawCommand::CubicCurveTo {
|
|
|
|
control_a,
|
|
|
|
control_b,
|
|
|
|
anchor,
|
|
|
|
} => {
|
|
|
|
let mut hit = false;
|
|
|
|
lyon_geom::CubicBezierSegment {
|
|
|
|
from: lyon_geom::Point::new(cursor.x.to_pixels(), cursor.y.to_pixels()),
|
|
|
|
ctrl1: lyon_geom::Point::new(control_a.x.to_pixels(), control_a.y.to_pixels()),
|
|
|
|
ctrl2: lyon_geom::Point::new(control_b.x.to_pixels(), control_b.y.to_pixels()),
|
|
|
|
to: lyon_geom::Point::new(anchor.x.to_pixels(), anchor.y.to_pixels()),
|
|
|
|
}
|
2023-08-25 13:25:51 +00:00
|
|
|
.for_each_quadratic_bezier(
|
|
|
|
CUBIC_CURVE_TOLERANCE,
|
|
|
|
&mut |quadratic_curve| {
|
|
|
|
if !hit
|
|
|
|
&& hit_test_stroke_curve(
|
|
|
|
test_point,
|
|
|
|
swf::Point::from_pixels(
|
|
|
|
quadratic_curve.from.x,
|
|
|
|
quadratic_curve.from.y,
|
|
|
|
),
|
|
|
|
swf::Point::from_pixels(
|
|
|
|
quadratic_curve.ctrl.x,
|
|
|
|
quadratic_curve.ctrl.y,
|
|
|
|
),
|
|
|
|
swf::Point::from_pixels(quadratic_curve.to.x, quadratic_curve.to.y),
|
|
|
|
stroke_widths,
|
|
|
|
)
|
|
|
|
{
|
|
|
|
hit = true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2023-08-25 13:20:44 +00:00
|
|
|
cursor = *anchor;
|
|
|
|
|
|
|
|
if hit {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Given a matrix, calculates the scale for stroke widths.
|
|
|
|
/// TODO: Verify the actual behavior; I think it's more like the average between scaleX and scaleY.
|
|
|
|
/// Does not yet support vertical/horizontal stroke scaling flags.
|
|
|
|
/// This might be better to add as a method to Matrix.
|
2021-06-22 10:04:27 +00:00
|
|
|
fn stroke_minimum_width(matrix: &Matrix) -> f64 {
|
2020-08-21 06:25:31 +00:00
|
|
|
let sx = (matrix.a * matrix.a + matrix.b * matrix.b).sqrt();
|
|
|
|
let sy = (matrix.c * matrix.c + matrix.d * matrix.d).sqrt();
|
2021-06-22 10:04:27 +00:00
|
|
|
let scale: f64 = sx.max(sy).into();
|
2020-08-21 06:25:31 +00:00
|
|
|
20.0 * scale
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns whether the given point is inside the stroked line segment.
|
|
|
|
/// `width_sq` should be the squared width of the stroke.
|
|
|
|
fn hit_test_stroke(
|
2023-05-14 17:36:31 +00:00
|
|
|
test_point: swf::Point<Twips>,
|
|
|
|
begin: swf::Point<Twips>,
|
|
|
|
end: swf::Point<Twips>,
|
2020-08-21 06:25:31 +00:00
|
|
|
(stroke_width, stroke_width_sq): (f64, f64),
|
|
|
|
) -> bool {
|
2023-05-14 17:36:31 +00:00
|
|
|
let px = test_point.x.get() as f64;
|
|
|
|
let py = test_point.y.get() as f64;
|
|
|
|
let x0 = begin.x.get() as f64;
|
|
|
|
let y0 = begin.y.get() as f64;
|
|
|
|
let x1 = end.x.get() as f64;
|
|
|
|
let y1 = end.y.get() as f64;
|
2020-08-21 06:25:31 +00:00
|
|
|
|
|
|
|
// Early exit: out of bounds
|
|
|
|
let x_min = x0.min(x1);
|
|
|
|
let x_max = x0.max(x1);
|
|
|
|
if px < x_min - stroke_width || px > x_max + stroke_width {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
let y_min = y0.min(y1);
|
|
|
|
let y_max = y0.max(y1);
|
|
|
|
if py < y_min - stroke_width || py > y_max + stroke_width {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
// AB is the segment from `begin` to `end` and P is `test_point`.
|
2020-08-21 06:25:31 +00:00
|
|
|
// P
|
|
|
|
// .
|
|
|
|
// .
|
|
|
|
// A----->B
|
|
|
|
// If AP dot AB is <= 0.0, then PA is pointing away from AB, so A is the closest point.
|
|
|
|
let abx = x1 - x0;
|
|
|
|
let aby = y1 - y0;
|
|
|
|
let apx = px - x0;
|
|
|
|
let apy = py - y0;
|
|
|
|
let dot_a = abx * apx + aby * apy;
|
|
|
|
let dist = if dot_a <= 0.0 {
|
|
|
|
apx * apx + apy * apy
|
|
|
|
} else {
|
|
|
|
// If BP dot AB is >= 0.0, then BP is pointing away from BA, so B is the closest point.
|
|
|
|
let bpx = px - x1;
|
|
|
|
let bpy = py - y1;
|
|
|
|
let dot_b = abx * bpx + aby * bpy;
|
|
|
|
if dot_b >= 0.0 {
|
|
|
|
bpx * bpx + bpy * bpy
|
|
|
|
} else {
|
|
|
|
// Otherwise, the closest point will be within the interval of the segment.
|
|
|
|
// Project the point onto the segment.
|
|
|
|
let len = abx * abx + aby * aby;
|
|
|
|
let ex = apx - dot_a * abx / len;
|
|
|
|
let ey = apy - dot_a * aby / len;
|
|
|
|
ex * ex + ey * ey
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
dist <= stroke_width_sq
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns whether the given point is inside the stroked bezier curve.
|
|
|
|
/// `width_sq` should be the squared width of the stroke.
|
|
|
|
fn hit_test_stroke_curve(
|
2023-05-14 17:36:31 +00:00
|
|
|
test_point: swf::Point<Twips>,
|
|
|
|
begin: swf::Point<Twips>,
|
|
|
|
control: swf::Point<Twips>,
|
|
|
|
anchor: swf::Point<Twips>,
|
2020-08-21 06:25:31 +00:00
|
|
|
(stroke_width, stroke_width_sq): (f64, f64),
|
|
|
|
) -> bool {
|
2023-05-14 17:36:31 +00:00
|
|
|
let px = test_point.x.get() as f64;
|
|
|
|
let py = test_point.y.get() as f64;
|
|
|
|
let x0 = begin.x.get() as f64;
|
|
|
|
let y0 = begin.y.get() as f64;
|
|
|
|
let x1 = control.x.get() as f64;
|
|
|
|
let y1 = control.y.get() as f64;
|
|
|
|
let x2 = anchor.x.get() as f64;
|
|
|
|
let y2 = anchor.y.get() as f64;
|
2020-08-21 06:25:31 +00:00
|
|
|
|
|
|
|
// Early exit: out of bounds
|
|
|
|
// TODO: Since this involves an expensive cubic, probably wortwhile to calculate the tight bounds for the curve:
|
|
|
|
// https://www.iquilezles.org/www/articles/bezierbbox/bezierbbox.htm
|
|
|
|
let x_min = x0.min(x1).min(x2);
|
|
|
|
let x_max = x0.max(x1).max(x2);
|
|
|
|
if px < x_min - stroke_width || px > x_max + stroke_width {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let y_min = y0.min(y1).min(y2);
|
|
|
|
let y_max = y0.max(y1).max(y2);
|
|
|
|
if py < y_min - stroke_width || py > y_max + stroke_width {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The closest point on the curve will be normal to the curve.
|
|
|
|
// The tangent of a quadratic bezier:
|
|
|
|
// C'(t) = -2 * (1-t) * P0 + 2 * (1-t) * P1 + 2*t*P2
|
|
|
|
// Dot product to determine when we are perpendicular to the tangent.
|
|
|
|
// (point - C(t)) . C'(t) = 0
|
|
|
|
// The result is a cubic polynomial that we can solve for.
|
|
|
|
// After solving this polynomial, we choose the t with [0, 1.0] that gives us the minimum distance
|
|
|
|
// (also considering the endcaps).
|
|
|
|
// via http://blog.gludion.com/2009/08/distance-to-quadratic-bezier-curve.html
|
|
|
|
|
|
|
|
let ax = x1 - x0;
|
|
|
|
let ay = y1 - y0;
|
|
|
|
let bx = x2 - x1 - ax;
|
|
|
|
let by = y2 - y1 - ay;
|
|
|
|
let mx = x0 - px;
|
|
|
|
let my = y0 - py;
|
|
|
|
|
|
|
|
let a = bx * bx + by * by;
|
|
|
|
let b = 3.0 * (ax * bx + ay * by);
|
|
|
|
let c = 2.0 * (ax * ax + ay * ay) + (mx * bx + my * by);
|
|
|
|
let d = mx * ax + my * ay;
|
|
|
|
|
|
|
|
let distance_to_curve = |t| -> f64 {
|
|
|
|
// Sample bezier at the given t and return distance to the point.
|
|
|
|
let comp_t = 1.0 - t;
|
|
|
|
let cx = comp_t * comp_t * x0 + 2.0 * comp_t * t * x1 + t * t * x2;
|
|
|
|
let cy = comp_t * comp_t * y0 + 2.0 * comp_t * t * y1 + t * t * y2;
|
|
|
|
let dx = cx - px;
|
|
|
|
let dy = cy - py;
|
|
|
|
dx * dx + dy * dy
|
|
|
|
};
|
|
|
|
|
|
|
|
// Test end-caps
|
|
|
|
let mut dist = distance_to_curve(0.0);
|
|
|
|
dist = dist.min(distance_to_curve(1.0));
|
|
|
|
|
|
|
|
// Test roots.
|
|
|
|
for t in solve_cubic(a, b, c, d) {
|
|
|
|
if t >= 0.0 && t <= 1.0 {
|
|
|
|
dist = dist.min(distance_to_curve(t));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dist <= stroke_width_sq
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculates the winding number for a line segment relative to the given point.
|
|
|
|
fn winding_number_line(
|
2023-05-14 17:36:31 +00:00
|
|
|
test_point: swf::Point<Twips>,
|
|
|
|
begin: swf::Point<Twips>,
|
|
|
|
end: swf::Point<Twips>,
|
2020-08-21 06:25:31 +00:00
|
|
|
) -> i32 {
|
2023-05-14 17:36:31 +00:00
|
|
|
let d0 = test_point - begin;
|
|
|
|
let d1 = end - begin;
|
2020-08-21 06:25:31 +00:00
|
|
|
|
|
|
|
// Adjust winding number if we are on the left side of the segment.
|
|
|
|
// An upward segment (-y) increments the winding number (including the initial endpoint).
|
|
|
|
// A downward segment (+y) decrements the winding number (including the final endpoint)
|
|
|
|
// Perp-dot indicates which side of the segment the point is on.
|
2023-05-14 17:36:31 +00:00
|
|
|
if begin.y < test_point.y {
|
2023-05-17 04:21:13 +00:00
|
|
|
if end.y >= test_point.y
|
|
|
|
&& (d1.dx.get() as i64) * (d0.dy.get() as i64)
|
|
|
|
> (d1.dy.get() as i64) * (d0.dx.get() as i64)
|
|
|
|
{
|
2023-05-14 17:36:31 +00:00
|
|
|
return 1;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
2023-05-17 04:21:13 +00:00
|
|
|
} else if end.y < test_point.y
|
|
|
|
&& (d1.dx.get() as i64) * (d0.dy.get() as i64) < (d1.dy.get() as i64) * (d0.dx.get() as i64)
|
|
|
|
{
|
2023-05-14 17:36:31 +00:00
|
|
|
return -1;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
0
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculates the winding number for a bezier curve around the given point.
|
|
|
|
fn winding_number_curve(
|
2023-05-14 17:36:31 +00:00
|
|
|
test_point: swf::Point<Twips>,
|
|
|
|
begin: swf::Point<Twips>,
|
|
|
|
control: swf::Point<Twips>,
|
|
|
|
anchor: swf::Point<Twips>,
|
2020-08-21 06:25:31 +00:00
|
|
|
) -> i32 {
|
|
|
|
// Intersect a ray on the +x axis with the quadratic bezier.
|
|
|
|
//
|
|
|
|
// Translate so the test point and ray is at the origin.
|
|
|
|
// The ray-curve intersection is solving the quadratic:
|
|
|
|
// y_0*(1-t)^2 + y_1*2*t*(1-t) + y_2*t^2 = 0
|
|
|
|
|
|
|
|
// However, there are two issues:
|
|
|
|
// 1) Solving the quadratic needs to be numerically robust, particularly near the endpoints 0.0 and 1.0, and as the curve is tangent to the ray.
|
|
|
|
// We use the "Citardauq" method for improved numerical stability.
|
2022-08-16 03:51:46 +00:00
|
|
|
// 2) The convention for including/excluding endpoints needs to act similarly to lines, with the initial point included if the curve is "upward",
|
|
|
|
// and the final point included if the curve is pointing "downward". This is complicated by the fact that the curve could be tangent to the ray
|
2020-08-21 06:25:31 +00:00
|
|
|
// at the endpoint (this is still considered "upward" or "downward" depending on the slope at earlier t).
|
|
|
|
// We solve this by splitting the curve into y-monotonic subcurves. This is helpful because
|
|
|
|
// a) each subcurve will have 1 intersection with the ray
|
|
|
|
// b) if the subcurve surrounds the ray, we know it has an intersection without having to check if t is in [0, 1]
|
2022-08-16 03:51:46 +00:00
|
|
|
// c) we know the winding of the segment upward/downward based on which root it contains
|
2020-08-21 06:25:31 +00:00
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
let d0 = begin - test_point;
|
|
|
|
let d1 = control - test_point;
|
|
|
|
let d2 = anchor - test_point;
|
2020-08-21 06:25:31 +00:00
|
|
|
|
|
|
|
// Early exit: all control points out of bounds.
|
2023-05-14 17:36:31 +00:00
|
|
|
if (d0.dy < Twips::ZERO && d1.dy < Twips::ZERO && d2.dy < Twips::ZERO)
|
|
|
|
|| (d0.dy > Twips::ZERO && d1.dy > Twips::ZERO && d2.dy > Twips::ZERO)
|
|
|
|
|| (d0.dx <= Twips::ZERO && d1.dx <= Twips::ZERO && d2.dx <= Twips::ZERO)
|
2020-08-21 06:25:31 +00:00
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2023-05-14 17:36:31 +00:00
|
|
|
let x0 = d0.dx.get() as f64;
|
|
|
|
let y0 = d0.dy.get() as f64;
|
|
|
|
let x1 = d1.dx.get() as f64;
|
|
|
|
let y1 = d1.dy.get() as f64;
|
|
|
|
let x2 = d2.dx.get() as f64;
|
|
|
|
let y2 = d2.dy.get() as f64;
|
2020-08-21 06:25:31 +00:00
|
|
|
|
|
|
|
let a = y0 - 2.0 * y1 + y2;
|
|
|
|
let b = 2.0 * (y1 - y0);
|
|
|
|
let c = y0;
|
|
|
|
|
2022-08-16 03:51:46 +00:00
|
|
|
let (t0, t1) = solve_quadratic(a, b, c);
|
|
|
|
let is_t0_valid = t0.is_finite();
|
|
|
|
let is_t1_valid = t1.is_finite();
|
|
|
|
if !is_t0_valid && !is_t1_valid {
|
2020-08-21 06:25:31 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Split the curve into two y-monotonic segments.
|
2022-08-16 03:51:46 +00:00
|
|
|
let mut winding = 0;
|
2020-08-21 06:25:31 +00:00
|
|
|
let ax = x0 - 2.0 * x1 + x2;
|
|
|
|
let bx = 2.0 * (x1 - x0);
|
2022-08-16 03:51:46 +00:00
|
|
|
let t_extrema = -0.5 * b / a;
|
|
|
|
let is_monotonic = t_extrema <= 0.0 || t_extrema >= 1.0;
|
|
|
|
if a >= 0.0 {
|
|
|
|
// Downward opening parabola.
|
|
|
|
let y_min = if is_monotonic {
|
|
|
|
y0.min(y2)
|
|
|
|
} else {
|
|
|
|
a * t_extrema * t_extrema + b * t_extrema + c
|
|
|
|
};
|
2020-08-21 06:25:31 +00:00
|
|
|
|
2022-08-16 03:51:46 +00:00
|
|
|
// First subcurve is moving upward, include initial point.
|
|
|
|
if is_t0_valid && y0 >= 0.0 && y_min < 0.0 {
|
|
|
|
// If curve point is to the right of the ray origin (x > 0), the ray will hit it.
|
|
|
|
// We don't have to check 0 <= t <= 1 check because we've already guaranteed that the subcurve
|
|
|
|
// straddles the ray.
|
|
|
|
let x = x0 + bx * t0 + ax * t0 * t0;
|
|
|
|
if x > 0.0 {
|
|
|
|
winding += 1;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
2022-08-16 03:51:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Second subcurve is moving downard, include final point.
|
|
|
|
if is_t1_valid && y_min < 0.0 && y2 >= 0.0 {
|
|
|
|
let x = x0 + bx * t1 + ax * t1 * t1;
|
|
|
|
if x > 0.0 {
|
|
|
|
winding -= 1;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
2022-08-16 03:51:46 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Upward opening parabola.
|
|
|
|
let y_max = if is_monotonic {
|
|
|
|
y0.max(y2)
|
2020-08-21 06:25:31 +00:00
|
|
|
} else {
|
2022-08-16 03:51:46 +00:00
|
|
|
a * t_extrema * t_extrema + b * t_extrema + c
|
2020-08-21 06:25:31 +00:00
|
|
|
};
|
|
|
|
|
2022-08-16 03:51:46 +00:00
|
|
|
// First subcurve is moving downward, include extrema point.
|
|
|
|
if is_t1_valid && y0 < 0.0 && y_max >= 0.0 {
|
|
|
|
let x = x0 + bx * t1 + ax * t1 * t1;
|
|
|
|
if x > 0.0 {
|
|
|
|
winding -= 1;
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-16 03:51:46 +00:00
|
|
|
// Second subcurve is moving upward, include extrema point.
|
|
|
|
if is_t0_valid && y_max >= 0.0 && y2 < 0.0 {
|
|
|
|
let x = x0 + bx * t0 + ax * t0 * t0;
|
|
|
|
if x > 0.0 {
|
|
|
|
winding += 1;
|
|
|
|
}
|
|
|
|
}
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
winding
|
|
|
|
}
|
|
|
|
|
|
|
|
const COEFFICIENT_EPSILON: f64 = 0.0000001;
|
|
|
|
|
|
|
|
/// Returns the roots of the quadratic ax^2 + bx + c = 0.
|
2022-08-16 03:51:46 +00:00
|
|
|
/// The roots may not be unique. NAN is returned for invalid roots. The first root will be where
|
|
|
|
/// the curve is sloping upward, the second root will be where the curve is slopping downward.
|
2020-08-21 06:25:31 +00:00
|
|
|
/// Uses the "Citardauq" formula for numerical stability.
|
|
|
|
/// See https://math.stackexchange.com/questions/866331
|
2022-08-16 03:51:46 +00:00
|
|
|
fn solve_quadratic(a: f64, b: f64, c: f64) -> (f64, f64) {
|
|
|
|
if a.abs() <= COEFFICIENT_EPSILON {
|
|
|
|
// Nearly linear, solve as linear equation.
|
|
|
|
if b >= 0.0 {
|
|
|
|
return (f64::NAN, -c / b);
|
|
|
|
} else {
|
|
|
|
return (-c / b, f64::NAN);
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-16 03:51:46 +00:00
|
|
|
let mut disc = b * b - 4.0 * a * c;
|
|
|
|
if disc < 0.0 {
|
|
|
|
return (f64::NAN, f64::NAN);
|
|
|
|
}
|
|
|
|
disc = disc.sqrt();
|
|
|
|
// Order the roots so that the first root is where the curve slopes upward,
|
|
|
|
// and the second root is where the root slopes downward.
|
|
|
|
if b >= 0.0 {
|
|
|
|
let root0 = (-b - disc) / (2.0 * a);
|
|
|
|
let root1 = c / (a * root0);
|
|
|
|
(root0, root1)
|
2020-08-21 06:25:31 +00:00
|
|
|
} else {
|
2022-08-16 03:51:46 +00:00
|
|
|
let root0 = (-b + disc) / (2.0 * a);
|
|
|
|
let root1 = c / (a * root0);
|
|
|
|
(root1, root0)
|
2020-08-21 06:25:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the roots of a cubic polynomial, ax^3 + bx^2 + cx + d = 0
|
|
|
|
/// from http://www.cplusplus.com/forum/beginner/234717/
|
|
|
|
/// The roots are not necessarily unique.
|
|
|
|
/// TODO: This probably isn't numerically robust
|
|
|
|
#[allow(clippy::many_single_char_names)]
|
|
|
|
fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> SmallVec<[f64; 3]> {
|
|
|
|
let mut roots = SmallVec::new();
|
|
|
|
|
|
|
|
if a.abs() <= COEFFICIENT_EPSILON {
|
|
|
|
// Fall back to quadratic formula.
|
2022-08-16 03:51:46 +00:00
|
|
|
let (t0, t1) = solve_quadratic(b, c, d);
|
2023-07-03 11:47:37 +00:00
|
|
|
#[allow(clippy::tuple_array_conversions)]
|
2022-08-16 03:51:46 +00:00
|
|
|
roots.extend_from_slice(&[t0, t1]);
|
2020-08-21 06:25:31 +00:00
|
|
|
return roots;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reduce to a "depressed cubic", x^3 + px + q = 0
|
|
|
|
// https://en.wikipedia.org/wiki/Cubic_equation#Cardano's_formula
|
|
|
|
let p = (b * b - 3.0 * a * c) / (9.0 * a * a);
|
|
|
|
let q = (9.0 * a * b * c - 27.0 * a * a * d - 2.0 * b * b * b) / (54.0 * a * a * a);
|
|
|
|
let offset = b / (3.0 * a);
|
|
|
|
let disc = p * p * p - q * q;
|
|
|
|
|
|
|
|
// The discriminant determines the number of real roots.
|
|
|
|
if disc > 0.0 {
|
|
|
|
let theta = f64::acos(q / (p * f64::sqrt(p)));
|
|
|
|
let r = 2.0 * f64::sqrt(p);
|
|
|
|
let t0 = r * f64::cos(theta / 3.0) - offset;
|
|
|
|
let t1 = r * f64::cos((theta + 2.0 * std::f64::consts::PI) / 3.0) - offset;
|
|
|
|
let t2 = r * f64::cos((theta + 4.0 * std::f64::consts::PI) / 3.0) - offset;
|
2022-09-10 08:07:48 +00:00
|
|
|
roots.extend([t0, t1, t2]);
|
2020-08-21 06:25:31 +00:00
|
|
|
} else {
|
|
|
|
let gamma1 = f64::cbrt(q + f64::sqrt(-disc));
|
|
|
|
let gamma2 = f64::cbrt(q - f64::sqrt(-disc));
|
|
|
|
|
|
|
|
let t0 = gamma1 + gamma2 - offset;
|
|
|
|
let t1 = -0.5 * (gamma1 + gamma2) - offset;
|
2021-04-15 04:55:06 +00:00
|
|
|
roots.push(t0);
|
2020-08-21 06:25:31 +00:00
|
|
|
if disc == 0.0 {
|
|
|
|
roots.push(t1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
roots
|
|
|
|
}
|
2020-09-14 20:49:08 +00:00
|
|
|
|
|
|
|
/// Converts an SWF glyph into an SWF shape, for ease of use by rendering backends.
|
|
|
|
pub fn swf_glyph_to_shape(glyph: &swf::Glyph) -> swf::Shape {
|
|
|
|
// Per SWF19 p.164, the FontBoundsTable can contain empty bounds for every glyph (reserved).
|
|
|
|
// SWF19 says this is true through SWFv7, but it seems like it might be generally true?
|
|
|
|
// In any case, we have to be sure to calculate the shape bounds ourselves to make a proper
|
|
|
|
// SVG.
|
|
|
|
let bounds = glyph
|
|
|
|
.bounds
|
2022-09-02 12:23:34 +00:00
|
|
|
.clone()
|
2020-09-14 20:49:08 +00:00
|
|
|
.filter(|b| b.x_min != b.x_max || b.y_min != b.y_max)
|
2022-09-02 12:23:34 +00:00
|
|
|
.unwrap_or_else(|| calculate_shape_bounds(&glyph.shape_records));
|
2020-09-14 20:49:08 +00:00
|
|
|
swf::Shape {
|
|
|
|
version: 2,
|
|
|
|
id: 0,
|
2022-09-02 12:23:34 +00:00
|
|
|
shape_bounds: bounds.clone(),
|
2020-09-14 20:49:08 +00:00
|
|
|
edge_bounds: bounds,
|
2023-03-19 00:49:08 +00:00
|
|
|
flags: swf::ShapeFlag::HAS_SCALING_STROKES | swf::ShapeFlag::NON_ZERO_WINDING_RULE,
|
2020-09-14 20:49:08 +00:00
|
|
|
styles: swf::ShapeStyles {
|
|
|
|
fill_styles: vec![swf::FillStyle::Color(swf::Color {
|
|
|
|
r: 255,
|
|
|
|
g: 255,
|
|
|
|
b: 255,
|
|
|
|
a: 255,
|
|
|
|
})],
|
|
|
|
line_styles: vec![],
|
|
|
|
},
|
|
|
|
shape: glyph.shape_records.clone(),
|
|
|
|
}
|
|
|
|
}
|
2022-05-17 01:27:52 +00:00
|
|
|
|
|
|
|
/// Scale mode used by strokes in a shape.
|
|
|
|
///
|
|
|
|
/// Determines how the line thickness is affected by the shape's transform.
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
pub enum LineScaleMode {
|
|
|
|
None = 0,
|
|
|
|
Horizontal,
|
|
|
|
Vertical,
|
|
|
|
Both,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Helper type for calculating line widths for a transformed shape.
|
|
|
|
pub struct LineScales<'a> {
|
|
|
|
matrix: &'a Matrix,
|
|
|
|
scales: Option<[f32; 4]>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> LineScales<'a> {
|
|
|
|
/// Create a new line scaler for the given matrix.
|
|
|
|
#[inline]
|
|
|
|
pub fn new(matrix: &'a Matrix) -> Self {
|
|
|
|
Self {
|
|
|
|
matrix,
|
|
|
|
scales: None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the final width of a line after transformation.
|
|
|
|
#[inline]
|
|
|
|
pub fn transform_width(&mut self, width: f32, scale_mode: LineScaleMode) -> f32 {
|
|
|
|
// Lazily calculate the scale to avoid doing so for shapes that have no strokes.
|
|
|
|
let scales = self.scales.get_or_insert_with(|| {
|
|
|
|
let line_scale_x = f32::abs(self.matrix.a + self.matrix.c);
|
|
|
|
let line_scale_y = f32::abs(self.matrix.b + self.matrix.d);
|
|
|
|
let line_scale =
|
|
|
|
((line_scale_x * line_scale_x + line_scale_y * line_scale_y) / 2.0).sqrt();
|
|
|
|
[1.0, line_scale_x, line_scale_y, line_scale]
|
|
|
|
});
|
|
|
|
let scaled_width = width * scales[scale_mode as usize];
|
|
|
|
// Flash draws all strokes with a minimum width of 1 pixel.
|
|
|
|
// This usually occurs in "hairline" strokes (exported with width of 1 twip).
|
|
|
|
scaled_width.max(1.0)
|
|
|
|
}
|
|
|
|
}
|
2023-05-17 04:21:13 +00:00
|
|
|
|
2023-08-01 18:26:12 +00:00
|
|
|
pub fn quadratic_curve_bounds(
|
|
|
|
start: swf::Point<Twips>,
|
|
|
|
stroke_width: Twips,
|
|
|
|
control: swf::Point<Twips>,
|
|
|
|
anchor: swf::Point<Twips>,
|
|
|
|
) -> Rectangle<Twips> {
|
|
|
|
// extremes
|
|
|
|
let from_x = start.x.to_pixels();
|
|
|
|
let from_y = start.y.to_pixels();
|
|
|
|
let anchor_x = anchor.x.to_pixels();
|
|
|
|
let anchor_y = anchor.y.to_pixels();
|
|
|
|
let control_x = control.x.to_pixels();
|
|
|
|
let control_y = control.y.to_pixels();
|
|
|
|
|
|
|
|
let mut min_x = from_x.min(anchor_x);
|
|
|
|
let mut min_y = from_y.min(anchor_y);
|
|
|
|
let mut max_x = from_x.max(anchor_x);
|
|
|
|
let mut max_y = from_y.max(anchor_y);
|
|
|
|
|
|
|
|
if control_x < min_x || control_x > max_x {
|
|
|
|
let t_x = ((from_x - control_x) / (from_x - (control_x * 2.0) + anchor_x)).clamp(0.0, 1.0);
|
|
|
|
let s_x = 1.0 - t_x;
|
|
|
|
let q_x = s_x * s_x * from_x + (s_x * 2.0) * t_x * control_x + t_x * t_x * anchor_x;
|
|
|
|
|
|
|
|
min_x = min_x.min(q_x);
|
|
|
|
max_x = max_x.max(q_x);
|
|
|
|
}
|
|
|
|
|
|
|
|
if control_y < min_y || control_y > max_y {
|
|
|
|
let t_y = ((from_y - control_y) / (from_y - (control_y * 2.0) + anchor_y)).clamp(0.0, 1.0);
|
|
|
|
let s_y = 1.0 - t_y;
|
|
|
|
let q_y = s_y * s_y * from_y + (s_y * 2.0) * t_y * control_y + t_y * t_y * anchor_y;
|
|
|
|
|
|
|
|
min_y = min_y.min(q_y);
|
|
|
|
max_y = max_y.max(q_y);
|
|
|
|
}
|
|
|
|
|
|
|
|
let radius = stroke_width / 2;
|
|
|
|
Rectangle::default()
|
|
|
|
.encompass(swf::Point::new(
|
|
|
|
Twips::from_pixels(min_x) - radius,
|
|
|
|
Twips::from_pixels(min_y) - radius,
|
|
|
|
))
|
|
|
|
.encompass(swf::Point::new(
|
|
|
|
Twips::from_pixels(max_x) + radius,
|
|
|
|
Twips::from_pixels(max_y) + radius,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
2023-08-25 13:20:44 +00:00
|
|
|
pub fn cubic_curve_bounds(
|
|
|
|
start: swf::Point<Twips>,
|
|
|
|
stroke_width: Twips,
|
|
|
|
control_a: swf::Point<Twips>,
|
|
|
|
control_b: swf::Point<Twips>,
|
|
|
|
anchor: swf::Point<Twips>,
|
|
|
|
) -> Rectangle<Twips> {
|
|
|
|
// [NA] Should we just move most of our math in this file to lyon_geom?
|
|
|
|
let bounds = lyon_geom::CubicBezierSegment {
|
|
|
|
from: lyon_geom::Point::new(start.x.to_pixels(), start.y.to_pixels()),
|
|
|
|
ctrl1: lyon_geom::Point::new(control_a.x.to_pixels(), control_a.y.to_pixels()),
|
|
|
|
ctrl2: lyon_geom::Point::new(control_b.x.to_pixels(), control_b.y.to_pixels()),
|
|
|
|
to: lyon_geom::Point::new(anchor.x.to_pixels(), anchor.y.to_pixels()),
|
|
|
|
}
|
|
|
|
.bounding_box();
|
|
|
|
|
|
|
|
let radius = stroke_width / 2;
|
|
|
|
Rectangle {
|
|
|
|
x_min: Twips::from_pixels(bounds.min.x) - radius,
|
|
|
|
x_max: Twips::from_pixels(bounds.max.x) + radius,
|
|
|
|
y_min: Twips::from_pixels(bounds.min.y) - radius,
|
|
|
|
y_max: Twips::from_pixels(bounds.max.y) + radius,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-17 04:21:13 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use swf::PointDelta;
|
|
|
|
|
|
|
|
const FILL_STYLES: [FillStyle; 1] = [FillStyle::Color(swf::Color {
|
|
|
|
r: 255,
|
|
|
|
g: 0,
|
|
|
|
b: 0,
|
|
|
|
a: 255,
|
|
|
|
})];
|
|
|
|
|
|
|
|
const LINE_STYLES: [LineStyle; 0] = [];
|
|
|
|
|
|
|
|
/// Convenience method to quickly make a shape,
|
|
|
|
fn build_shape(records: Vec<ShapeRecord>) -> swf::Shape {
|
|
|
|
let bounds = calculate_shape_bounds(&records);
|
|
|
|
swf::Shape {
|
|
|
|
version: 2,
|
|
|
|
id: 1,
|
|
|
|
shape_bounds: bounds.clone(),
|
|
|
|
edge_bounds: bounds,
|
|
|
|
flags: swf::ShapeFlag::HAS_SCALING_STROKES,
|
|
|
|
styles: swf::ShapeStyles {
|
|
|
|
fill_styles: FILL_STYLES.to_vec(),
|
|
|
|
line_styles: LINE_STYLES.to_vec(),
|
|
|
|
},
|
|
|
|
shape: records,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A simple solid square.
|
|
|
|
#[test]
|
|
|
|
fn basic_shape() {
|
|
|
|
let shape = build_shape(vec![
|
|
|
|
ShapeRecord::StyleChange(Box::new(swf::StyleChangeData {
|
|
|
|
move_to: Some(swf::Point::from_pixels(100.0, 100.0)),
|
|
|
|
fill_style_0: None,
|
|
|
|
fill_style_1: Some(1),
|
|
|
|
line_style: None,
|
|
|
|
new_styles: None,
|
|
|
|
})),
|
|
|
|
ShapeRecord::StraightEdge {
|
|
|
|
delta: PointDelta::from_pixels(100.0, 0.0),
|
|
|
|
},
|
|
|
|
ShapeRecord::StraightEdge {
|
|
|
|
delta: PointDelta::from_pixels(0.0, 100.0),
|
|
|
|
},
|
|
|
|
ShapeRecord::StraightEdge {
|
|
|
|
delta: PointDelta::from_pixels(-100.0, 0.0),
|
|
|
|
},
|
|
|
|
ShapeRecord::StraightEdge {
|
|
|
|
delta: PointDelta::from_pixels(0.0, -100.0),
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
let commands = ShapeConverter::from_shape(&shape).into_commands();
|
|
|
|
let expected = vec![DrawPath::Fill {
|
|
|
|
style: &FILL_STYLES[0],
|
|
|
|
commands: vec![
|
|
|
|
DrawCommand::MoveTo(swf::Point::from_pixels(100.0, 100.0)),
|
|
|
|
DrawCommand::LineTo(swf::Point::from_pixels(200.0, 100.0)),
|
|
|
|
DrawCommand::LineTo(swf::Point::from_pixels(200.0, 200.0)),
|
|
|
|
DrawCommand::LineTo(swf::Point::from_pixels(100.0, 200.0)),
|
|
|
|
DrawCommand::LineTo(swf::Point::from_pixels(100.0, 100.0)),
|
|
|
|
],
|
|
|
|
winding_rule: FillRule::EvenOdd,
|
|
|
|
}];
|
|
|
|
assert_eq!(commands, expected);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A solid square with one edge flipped (fillstyle0 instead of fillstyle1).
|
|
|
|
#[test]
|
|
|
|
fn flipped_edges() {
|
|
|
|
let shape = build_shape(vec![
|
|
|
|
ShapeRecord::StyleChange(Box::new(swf::StyleChangeData {
|
|
|
|
move_to: Some(swf::Point::from_pixels(100.0, 100.0)),
|
|
|
|
fill_style_0: None,
|
|
|
|
fill_style_1: Some(1),
|
|
|
|
line_style: None,
|
|
|
|
new_styles: None,
|
|
|
|
})),
|
|
|
|
ShapeRecord::StraightEdge {
|
|
|
|
delta: PointDelta::from_pixels(100.0, 0.0),
|
|
|
|
},
|
|
|
|
ShapeRecord::StraightEdge {
|
|
|
|
delta: PointDelta::from_pixels(0.0, 100.0),
|
|
|
|
},
|
|
|
|
ShapeRecord::StraightEdge {
|
|
|
|
delta: PointDelta::from_pixels(-100.0, 0.0),
|
|
|
|
},
|
|
|
|
ShapeRecord::StyleChange(Box::new(swf::StyleChangeData {
|
|
|
|
move_to: Some(swf::Point::from_pixels(100.0, 100.0)),
|
|
|
|
fill_style_0: Some(1),
|
|
|
|
fill_style_1: Some(0),
|
|
|
|
line_style: None,
|
|
|
|
new_styles: None,
|
|
|
|
})),
|
|
|
|
ShapeRecord::StraightEdge {
|
|
|
|
delta: PointDelta::from_pixels(0.0, 100.0),
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
let commands = ShapeConverter::from_shape(&shape).into_commands();
|
|
|
|
let expected = vec![DrawPath::Fill {
|
|
|
|
style: &FILL_STYLES[0],
|
|
|
|
commands: vec![
|
|
|
|
DrawCommand::MoveTo(swf::Point::from_pixels(100.0, 100.0)),
|
|
|
|
DrawCommand::LineTo(swf::Point::from_pixels(200.0, 100.0)),
|
|
|
|
DrawCommand::LineTo(swf::Point::from_pixels(200.0, 200.0)),
|
|
|
|
DrawCommand::LineTo(swf::Point::from_pixels(100.0, 200.0)),
|
|
|
|
DrawCommand::LineTo(swf::Point::from_pixels(100.0, 100.0)),
|
|
|
|
],
|
|
|
|
winding_rule: FillRule::EvenOdd,
|
|
|
|
}];
|
|
|
|
assert_eq!(commands, expected);
|
|
|
|
}
|
|
|
|
|
|
|
|
use swf::Twips;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_winding_number_line() {
|
|
|
|
fn test(
|
|
|
|
test_point: swf::Point<Twips>,
|
|
|
|
begin: swf::Point<Twips>,
|
|
|
|
end: swf::Point<Twips>,
|
|
|
|
expected: i32,
|
|
|
|
) {
|
|
|
|
let result = winding_number_line(test_point, begin, end);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
expected, result,
|
|
|
|
"result (winding number line) should match"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test data taken from a real-world case:
|
|
|
|
// https://github.com/ruffle-rs/ruffle/issues/11077
|
|
|
|
// Overflow bugs can make this test case fail.
|
|
|
|
test(
|
|
|
|
swf::Point::new(Twips::new(0), Twips::new(-665)),
|
|
|
|
swf::Point::new(Twips::new(44868), Twips::new(-41726)),
|
|
|
|
swf::Point::new(Twips::new(44868), Twips::new(8275)),
|
|
|
|
1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|