Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 178 additions & 145 deletions core/src/avm2/globals/flash/display/graphics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use crate::display_object::TDisplayObject;
use crate::drawing::Drawing;
use crate::string::{AvmString, WStr};
use either::Either;
use ruffle_render::shape_utils::{DrawCommand, FillRule, GradientType};
use std::f64::consts::FRAC_1_SQRT_2;
use swf::{
Expand Down Expand Up @@ -1059,6 +1060,24 @@
.ok_or_else(|| make_error_2004(activation, Error2004Type::ArgumentError))?
};

// FIXME Triangles should be drawn using non-zero winding rule.
// When fixed, update output.expected.png of avm2/graphics_draw_triangles.
avm2_stub_method!(
activation,
"flash.display.Graphics",
"drawTriangles",
"winding behavior"
);

if uvt_data.is_some() {
avm2_stub_method!(
activation,
"flash.display.Graphics",
"drawTriangles",
"with uvt data"
);

Check warning on line 1078 in core/src/avm2/globals/flash/display/graphics.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (1073–1078)
}

draw_triangles_internal(
activation,
&mut drawing,
Expand All @@ -1072,6 +1091,134 @@
Ok(Value::Undefined)
}

/// Like [`slice::as_chunks`], but returns `None` if the slice length
/// is not a multiple of `N`.
trait AsChunksExact<T> {
fn as_chunks_exact<const N: usize>(&self) -> Option<&[[T; N]]>;
}

impl<T> AsChunksExact<T> for [T] {
fn as_chunks_exact<const N: usize>(&self) -> Option<&[[T; N]]> {
let (chunks, remainder) = self.as_chunks::<N>();

remainder.is_empty().then_some(chunks)
}
}

/// Parsed and validated triangle geometry for `Graphics.drawTriangles`.
///
/// # Invariants
///
/// * `vertices`, `indices`, and `triangles` are non-empty.
/// * Every value in `indices` is `< vertices.len()`. Flash stops at the first
/// out-of-bounds index, so any trailing triples that would have been out of
/// bounds are dropped at construction time.
enum TriangleData {
/// Triangles described by shared `vertices` and triples of indices into
/// that array.
Indexed {
vertices: Box<[Point<Twips>]>,
indices: Box<[[u32; 3]]>,
},
/// Triangles described as independent vertex triples.
Sequential { triangles: Box<[[Point<Twips>; 3]]> },
}

impl TriangleData {
/// Parses raw `Graphics.drawTriangles` arguments into a validated
/// [`TriangleData`].
///
/// Returns `Ok(None)` for inputs that produce no triangles (empty
/// vertices, empty indices, or all indices out of bounds).
fn new<'gc>(
activation: &mut Activation<'_, 'gc>,
vertices: &Object<'gc>,
indices: Option<&Object<'gc>>,
) -> Result<Option<Self>, Error<'gc>> {
let vertex_storage = vertices
.as_vector_storage()
.expect("vertices is not a Vector");

let vertex_pairs = vertex_storage
.storage()
.as_chunks_exact::<2>()
.ok_or_else(|| make_error_2004(activation, Error2004Type::ArgumentError))?;

if vertex_pairs.is_empty() {
return Ok(None);
}

if let Some(indices) = indices {
let indices_storage = indices
.as_vector_storage()
.expect("indices is not a Vector");

let index_triples = indices_storage
.storage()
.as_chunks_exact::<3>()
.ok_or_else(|| make_error_2004(activation, Error2004Type::ArgumentError))?;

if index_triples.is_empty() {
return Ok(None);
}

let vertices = vertex_pairs
.iter()
.map(|pair| make_point(pair))
.collect::<Box<_>>();

let num_vertices = vertices.len();

// `Vector.<int>` elements are always `Value::Integer`
let indices: Box<_> = index_triples
.iter()
.map(|[i0, i1, i2]| [i0.as_u32(), i1.as_u32(), i2.as_u32()])
// Flash stops at the first out-of-bounds index.
.take_while(|tri| tri.iter().all(|&i| (i as usize) < num_vertices))
.collect();

if indices.is_empty() {
return Ok(None);
}

Ok(Some(Self::Indexed { vertices, indices }))
} else {
let vertex_triples = vertex_pairs
.as_chunks_exact::<3>()
.ok_or_else(|| make_error_2004(activation, Error2004Type::ArgumentError))?;

let triangles = vertex_triples
.iter()
.map(|[p0, p1, p2]| [make_point(p0), make_point(p1), make_point(p2)])
.collect::<Box<_>>();

Ok(Some(Self::Sequential { triangles }))
}
}

fn iter_triangles(&self) -> impl Iterator<Item = [Point<Twips>; 3]> + '_ {
match self {
Self::Indexed { vertices, indices } => {
Either::Left(indices.iter().map(|&[i0, i1, i2]| {
[
vertices[i0 as usize],
vertices[i1 as usize],
vertices[i2 as usize],
]
}))
}
Self::Sequential { triangles } => Either::Right(triangles.iter().copied()),
}
}
}

fn make_point<'gc>([x, y]: &[Value<'gc>; 2]) -> Point<Twips> {
let x = Twips::from_pixels(x.as_f64());
let y = Twips::from_pixels(y.as_f64());

Point::new(x, y)
}

#[derive(Debug, Clone, Copy)]
enum TriangleCulling {
None,
Expand All @@ -1092,8 +1239,9 @@
}
}

fn cull(self, (a, b, c): Triangle) -> bool {
fn triangle_orientation((a, b, c): Triangle) -> i64 {
#[inline]
fn cull(self, points: [Point<Twips>; 3]) -> bool {
fn triangle_orientation([a, b, c]: [Point<Twips>; 3]) -> i64 {
let ax = a.x.get() as i64;
let ay = a.y.get() as i64;
let bx = b.x.get() as i64;
Expand All @@ -1105,166 +1253,34 @@

match self {
Self::None => false,
Self::Positive => triangle_orientation((a, b, c)) >= 0,
Self::Negative => triangle_orientation((a, b, c)) <= 0,
Self::Positive => triangle_orientation(points) >= 0,
Self::Negative => triangle_orientation(points) <= 0,
}
}
}

type Triangle = (Point<Twips>, Point<Twips>, Point<Twips>);

fn draw_triangles_internal<'gc>(
activation: &mut Activation<'_, 'gc>,
drawing: &mut Drawing,
vertices: &Object<'gc>,
indices: Option<&Object<'gc>>,
uvt_data: Option<&Object<'gc>>,
_uvt_data: Option<&Object<'gc>>,
culling: TriangleCulling,
) -> Result<(), Error<'gc>> {
// FIXME Triangles should be drawn using non-zero winding rule.
// When fixed, update output.expected.png of avm2/graphics_draw_triangles.
avm2_stub_method!(
activation,
"flash.display.Graphics",
"drawTriangles",
"winding behavior"
);

if uvt_data.is_some() {
avm2_stub_method!(
activation,
"flash.display.Graphics",
"drawTriangles",
"with uvt data"
);
}

let vertices = vertices
.as_vector_storage()
.expect("vertices is not a Vector");

if let Some(indices) = indices {
if !vertices.length().is_multiple_of(2) {
return Err(make_error_2004(activation, Error2004Type::ArgumentError));
}

let indices = indices
.as_vector_storage()
.expect("indices is not a Vector");

fn read_point<'gc>(
vertices: &VectorStorage<'gc>,
index: usize,
activation: &mut Activation<'_, 'gc>,
) -> Result<Point<Twips>, Error<'gc>> {
let x = {
let x = vertices
.get(2 * index, activation)?
.coerce_to_number(activation)?;
Twips::from_pixels(x)
};
let y = {
let y = vertices
.get(2 * index + 1, activation)?
.coerce_to_number(activation)?;
Twips::from_pixels(y)
};

Ok(Point::new(x, y))
}

fn next_triangle<'gc>(
vertices: &VectorStorage<'gc>,
indices: &mut impl Iterator<Item = Value<'gc>>,
activation: &mut Activation<'_, 'gc>,
) -> Option<Triangle> {
match (indices.next(), indices.next(), indices.next()) {
(Some(i0), Some(i1), Some(i2)) => {
let i0 = i0.coerce_to_u32(activation).ok()? as usize;
let i1 = i1.coerce_to_u32(activation).ok()? as usize;
let i2 = i2.coerce_to_u32(activation).ok()? as usize;

let p0 = read_point(vertices, i0, activation).ok()?;
let p1 = read_point(vertices, i1, activation).ok()?;
let p2 = read_point(vertices, i2, activation).ok()?;

Some((p0, p1, p2))
}
_ => None,
}
}

let indices = &mut indices.iter();

while let Some(triangle) = next_triangle(&vertices, indices, activation) {
draw_triangle_internal(triangle, drawing, culling);
}
} else {
if !vertices.length().is_multiple_of(6) {
return Err(make_error_2004(activation, Error2004Type::ArgumentError));
}

let mut vertices = vertices.iter();

fn read_point<'gc>(
vertices: &mut impl Iterator<Item = Value<'gc>>,
activation: &mut Activation<'_, 'gc>,
) -> Result<Option<Point<Twips>>, Error<'gc>> {
let x = {
let x = vertices.next();
let x = match x {
Some(x) => x.coerce_to_number(activation)?,
None => return Ok(None),
};
Twips::from_pixels(x)
};
let y = {
let y = vertices.next();
let y = match y {
Some(y) => y.coerce_to_number(activation)?,
None => return Ok(None),
};
Twips::from_pixels(y)
};

Ok(Some(Point::new(x, y)))
}

fn next_triangle<'gc>(
vertices: &mut impl Iterator<Item = Value<'gc>>,
activation: &mut Activation<'_, 'gc>,
) -> Result<Option<Triangle>, Error<'gc>> {
match (
read_point(vertices, activation)?,
read_point(vertices, activation)?,
read_point(vertices, activation)?,
) {
(Some(p0), Some(p1), Some(p2)) => Ok(Some((p0, p1, p2))),
_ => Ok(None),
}
}
let Some(data) = TriangleData::new(activation, vertices, indices)? else {
return Ok(());
};

while let Some(triangle) = next_triangle(&mut vertices, activation)? {
draw_triangle_internal(triangle, drawing, culling);
}
for [a, b, c] in data.iter_triangles().filter(|&tri| !culling.cull(tri)) {
drawing.draw_command(DrawCommand::MoveTo(a));
drawing.draw_command(DrawCommand::LineTo(b));
drawing.draw_command(DrawCommand::LineTo(c));
drawing.draw_command(DrawCommand::LineTo(a));
}

Ok(())
}

#[inline]
fn draw_triangle_internal((a, b, c): Triangle, drawing: &mut Drawing, culling: TriangleCulling) {
if culling.cull((a, b, c)) {
return;
}

drawing.draw_command(DrawCommand::MoveTo(a));

drawing.draw_command(DrawCommand::LineTo(b));
drawing.draw_command(DrawCommand::LineTo(c));
drawing.draw_command(DrawCommand::LineTo(a));
}

/// Implements `Graphics.drawGraphicsData`
pub fn draw_graphics_data<'gc>(
activation: &mut Activation<'_, 'gc>,
Expand Down Expand Up @@ -1598,6 +1614,23 @@
.as_object();

if let Some(vertices) = vertices {
// FIXME Triangles should be drawn using non-zero winding rule.
avm2_stub_method!(
activation,
"flash.display.Graphics",
"drawGraphicsData",
"GraphicsTrianglePath winding behavior"
);

if uvt_data.is_some() {
avm2_stub_method!(
activation,
"flash.display.Graphics",
"drawGraphicsData",
"GraphicsTrianglePath with uvt data"
);

Check warning on line 1631 in core/src/avm2/globals/flash/display/graphics.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (1626–1631)
}

draw_triangles_internal(
activation,
drawing,
Expand Down
Loading
Loading