///|
pub(all) enum PathCommand2D {
MoveTo(@math.Vec2)
LineTo(@math.Vec2)
QuadraticTo(@math.Vec2, @math.Vec2)
CubicTo(@math.Vec2, @math.Vec2, @math.Vec2)
Close
} derive(Eq, Debug)
///|
pub(all) struct PathGeometry2D {
commands : Array[PathCommand2D]
bounds_position : @math.Vec2
bounds_size : @math.Vec2
} derive(Eq, Debug)
///|
pub(all) enum PathStretch2D {
None
Fill
Uniform
} derive(Eq, Debug)
///|
pub(all) struct PathTessellation2D {
fill_vertices : Array[@math.Vec2]
stroke_vertices : Array[@math.Vec2]
} derive(Eq, Debug)
///|
priv struct PathContour2D {
points : Array[@math.Vec2]
mut closed : Bool
}
///|
fn path_point_transform(
point : @math.Vec2,
scale : @math.Vec2,
offset : @math.Vec2,
) -> @math.Vec2 {
@math.Vec2(point.0 * scale.0 + offset.0, point.1 * scale.1 + offset.1)
}
///|
fn path_flatten_contours(
geometry : PathGeometry2D,
scale : @math.Vec2,
offset : @math.Vec2,
curve_segments : Int,
) -> Array[PathContour2D] {
let contours : Array[PathContour2D] = []
let mut current : PathContour2D? = None
let mut current_point = @math.Vec2::zero()
let mut subpath_start = @math.Vec2::zero()
for command in geometry.commands {
match command {
MoveTo(point) => {
match current {
Some(contour) if !contour.points.is_empty() => contours.push(contour)
_ => ()
}
let transformed = path_point_transform(point, scale, offset)
current = Some({ points: [transformed], closed: false })
current_point = point
subpath_start = point
}
LineTo(point) => {
match current {
Some(contour) =>
contour.points.push(path_point_transform(point, scale, offset))
None => ()
}
current_point = point
}
QuadraticTo(control, point) => {
match current {
Some(contour) =>
for step in 1..<=curve_segments {
let t = step.to_double() / curve_segments.to_double()
let inverse = 1.0 - t
let flattened = current_point.scalar_mul(inverse * inverse) +
control.scalar_mul(2.0 * inverse * t) +
point.scalar_mul(t * t)
contour.points.push(
path_point_transform(flattened, scale, offset),
)
}
None => ()
}
current_point = point
}
CubicTo(control1, control2, point) => {
match current {
Some(contour) =>
for step in 1..<=curve_segments {
let t = step.to_double() / curve_segments.to_double()
let inverse = 1.0 - t
let flattened = current_point.scalar_mul(
inverse * inverse * inverse,
) +
control1.scalar_mul(3.0 * inverse * inverse * t) +
control2.scalar_mul(3.0 * inverse * t * t) +
point.scalar_mul(t * t * t)
contour.points.push(
path_point_transform(flattened, scale, offset),
)
}
None => ()
}
current_point = point
}
Close => {
match current {
Some(contour) => contour.closed = true
None => ()
}
current_point = subpath_start
}
}
}
match current {
Some(contour) if !contour.points.is_empty() => contours.push(contour)
_ => ()
}
contours
}
///|
fn path_polygon_signed_area(points : Array[@math.Vec2]) -> Double {
let mut area = 0.0
for index in 0.. Bool {
let ab = (b.0 - a.0) * (point.1 - a.1) - (b.1 - a.1) * (point.0 - a.0)
let bc = (c.0 - b.0) * (point.1 - b.1) - (c.1 - b.1) * (point.0 - b.0)
let ca = (a.0 - c.0) * (point.1 - c.1) - (a.1 - c.1) * (point.0 - c.0)
let has_negative = ab < -0.0000001 || bc < -0.0000001 || ca < -0.0000001
let has_positive = ab > 0.0000001 || bc > 0.0000001 || ca > 0.0000001
!(has_negative && has_positive)
}
///|
fn path_triangulate_contour(points : Array[@math.Vec2]) -> Array[@math.Vec2] {
if points.length() < 3 {
return []
}
let winding = if path_polygon_signed_area(points) >= 0.0 { 1.0 } else { -1.0 }
let remaining : Array[Int] = []
for index in 0.. 3 &&
guard_counter < points.length() * points.length() {
let mut ear_index = -1
for index in 0.. Array[@math.Vec2] {
if contour.points.length() < 2 || stroke_width <= 0.0 {
return []
}
let vertices : Array[@math.Vec2] = []
let half_width = stroke_width * 0.5
let segment_count = if contour.closed {
contour.points.length()
} else {
contour.points.length() - 1
}
for index in 0.. PathTessellation2D {
let physical_scale = output_scale.max(1.0)
let largest_scale = scale.0.abs().max(scale.1.abs()).max(0.000001)
let curve_segments = (8.0 +
largest_scale *
physical_scale *
geometry.bounds_size.0.max(geometry.bounds_size.1) /
12.0)
.ceil()
.to_int()
.clamp(min=8, max=64)
let contours = path_flatten_contours(geometry, scale, offset, curve_segments)
let fill_vertices : Array[@math.Vec2] = []
let stroke_vertices : Array[@math.Vec2] = []
for contour in contours {
if contour.closed {
for vertex in path_triangulate_contour(contour.points) {
fill_vertices.push(vertex)
}
}
for vertex in path_stroke_contour(contour, stroke_width) {
stroke_vertices.push(vertex)
}
}
{ fill_vertices, stroke_vertices }
}
///|
pub fn tessellate_colored_path_geometry(
geometry : PathGeometry2D,
scale : @math.Vec2,
offset : @math.Vec2,
stroke_width : Double,
color : @render.Color,
output_scale? : Double = 1.0,
) -> Array[ColoredTriangle2D] {
let physical_scale = output_scale.max(1.0)
let largest_scale = scale.0.abs().max(scale.1.abs()).max(0.000001)
let curve_segments = (8.0 +
largest_scale *
physical_scale *
geometry.bounds_size.0.max(geometry.bounds_size.1) /
12.0)
.ceil()
.to_int()
.clamp(min=8, max=64)
let contours = path_flatten_contours(geometry, scale, offset, curve_segments)
let triangles : Array[ColoredTriangle2D] = []
let transparent_color = { ..color, a: 0.0 }
let antialias_width = 1.0 / physical_scale
for contour in contours {
if contour.closed {
let fill = path_triangulate_contour(contour.points)
for index in 0..<(fill.length() / 3) {
let start = index * 3
triangles.push({
a: { position: fill[start], color },
b: { position: fill[start + 1], color },
c: { position: fill[start + 2], color },
})
}
let winding = if path_polygon_signed_area(contour.points) >= 0.0 {
1.0
} else {
-1.0
}
for index in 0..