We resolved **Issue 4.3 (Severe Per-Frame Allocation in Rendering
Loop)** by removing the intermediate `MeshData` structure and storing
`macroquad::prelude::Mesh` directly in
[`DrawCommand::Mesh`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs).
[`DrawCommand`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs)
- Dropped `Clone` and `Debug` derives from
[`Turtle`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs#L58)
and
[`DrawCommand`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs#L351),
accommodating Macroquad's `Mesh` which does not implement these
traits.
- Changed
[`DrawCommand::Mesh`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs#L353)
to the tuple variant `DrawCommand::Mesh(macroquad::prelude::Mesh)`.
- Deleted `struct MeshData` and its `to_mesh` method.
[`tessellation.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs)
- Renamed `build_mesh_data` to
[`build_mesh`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L35)
returning `macroquad::prelude::Mesh { vertices, indices, texture: None
}` directly.
- Updated
[`tessellate_polygon`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L71),
[`tessellate_multi_contour`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L116),
[`tessellate_stroke`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L214),
[`tessellate_circle`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L259),
and
[`tessellate_arc`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tessellation.rs#L308)
to return `Result<Mesh, Box<dyn std::error::Error>>`.
[`drawing.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs)
- In the main drawing loop
([`drawing.rs:36-38`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs#L36-L38)):
```rust
DrawCommand::Mesh(mesh) => {
draw_mesh(mesh);
}
```
Now borrows `&Mesh` directly from the command vector with **0 heap
allocations and 0 vector clones per frame**.
- Updated fill preview ([line
228](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs#L228)),
tween arc/center indicators ([lines 309,
329](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs#L309)),
and turtle shape drawing ([line
349](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs#L349))
to pass `&mesh` directly to `draw_mesh(&mesh)` without vector cloning.
[`execution.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/execution.rs)
- Updated
[`commands.push(DrawCommand::Mesh(mesh))`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/execution.rs#L135)
and
[`tessellate_command`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/execution.rs#L337).
- Updated
[`test_forward_left_forward`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/execution.rs#L487)
to instantiate `mut state` directly without dummy cloning.
---
```bash
cargo test --lib
```
Output:
```
running 10 tests
test circle_geometry::tests::test_circle_left_geometry ... ok
test circle_geometry::tests::test_circle_right_geometry ... ok
test general::angle::tests::degrees_to_radians_roundtrip ... ok
test general::angle::tests::from_integer ... ok
test execution::tests::test_forward_left_forward ... ok
test general::angle::tests::negation ... ok
test tweening::tests::test_animated_mode_pops_to_current_tween ... ok
test tweening::tests::test_instant_mode_drains_queue ... ok
test
tweening::tests::test_instant_mode_respects_batch_limit_and_retains_pending
... ok
test tweening::tests::test_streaming_append_commands_does_not_accumulate
... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.00s
```
All targets verified with `cargo check --all-targets` and `cargo clippy
--lib`.
361 lines
10 KiB
Rust
361 lines
10 KiB
Rust
//! Lyon tessellation utilities for turtle graphics
|
|
//!
|
|
//! This module provides helper functions to tessellate paths using Lyon,
|
|
//! which replaces the manual triangulation with GPU-optimized tessellation.
|
|
|
|
use lyon::math::{point, Point};
|
|
use lyon::path::{LineCap, LineJoin, Path};
|
|
use lyon::tessellation::{
|
|
BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, StrokeOptions,
|
|
StrokeTessellator, StrokeVertex, VertexBuffers,
|
|
};
|
|
use macroquad::prelude::*;
|
|
|
|
/// Convert macroquad Vec2 to Lyon Point
|
|
#[must_use]
|
|
pub(crate) fn to_lyon_point(v: Vec2) -> Point {
|
|
point(v.x, v.y)
|
|
}
|
|
|
|
/// Convert Lyon Point to macroquad Vec2
|
|
#[allow(dead_code)]
|
|
#[must_use]
|
|
pub(crate) fn to_macroquad_vec2(p: Point) -> Vec2 {
|
|
vec2(p.x, p.y)
|
|
}
|
|
|
|
/// Simple vertex type for Lyon tessellation
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub(crate) struct SimpleVertex {
|
|
pub(crate) position: [f32; 2],
|
|
}
|
|
|
|
/// Build mesh from Lyon tessellation
|
|
#[must_use]
|
|
pub(crate) fn build_mesh(
|
|
vertices: &[SimpleVertex],
|
|
indices: &[u16],
|
|
color: Color,
|
|
) -> Mesh {
|
|
let verts: Vec<Vertex> = vertices
|
|
.iter()
|
|
.map(|v| Vertex {
|
|
position: Vec3::new(v.position[0], v.position[1], 0.0),
|
|
uv: Vec2::ZERO,
|
|
color: [
|
|
(color.r * 255.0) as u8,
|
|
(color.g * 255.0) as u8,
|
|
(color.b * 255.0) as u8,
|
|
(color.a * 255.0) as u8,
|
|
],
|
|
normal: Vec4::ZERO,
|
|
})
|
|
.collect();
|
|
|
|
Mesh {
|
|
vertices: verts,
|
|
indices: indices.to_vec(),
|
|
texture: None,
|
|
}
|
|
}
|
|
|
|
/// Tessellate a polygon and return mesh
|
|
///
|
|
/// This automatically handles holes when the path crosses itself.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if no vertices are provided or if tessellation fails.
|
|
pub(crate) fn tessellate_polygon(
|
|
vertices: &[Vec2],
|
|
color: Color,
|
|
) -> Result<Mesh, Box<dyn std::error::Error>> {
|
|
if vertices.is_empty() {
|
|
return Err("No vertices provided".into());
|
|
}
|
|
|
|
// Build path
|
|
let mut builder = Path::builder();
|
|
builder.begin(to_lyon_point(vertices[0]));
|
|
for v in &vertices[1..] {
|
|
builder.line_to(to_lyon_point(*v));
|
|
}
|
|
builder.end(true); // Close the path
|
|
|
|
let path = builder.build();
|
|
|
|
// Tessellate with EvenOdd fill rule (automatic hole detection)
|
|
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
|
|
let mut tessellator = FillTessellator::new();
|
|
|
|
tessellator.tessellate_path(
|
|
&path,
|
|
&FillOptions::default().with_fill_rule(FillRule::EvenOdd),
|
|
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| SimpleVertex {
|
|
position: vertex.position().to_array(),
|
|
}),
|
|
)?;
|
|
|
|
Ok(build_mesh(
|
|
&geometry.vertices,
|
|
&geometry.indices,
|
|
color,
|
|
))
|
|
}
|
|
|
|
/// Tessellate multiple contours (outer boundary + holes) and return mesh
|
|
///
|
|
/// The first contour is the outer boundary, subsequent contours are holes.
|
|
/// Lyon's `EvenOdd` fill rule automatically creates holes where contours overlap.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if no contours are provided or if tessellation fails.
|
|
pub(crate) fn tessellate_multi_contour(
|
|
contours: &[Vec<Vec2>],
|
|
color: Color,
|
|
) -> Result<Mesh, Box<dyn std::error::Error>> {
|
|
if contours.is_empty() {
|
|
return Err("No contours provided".into());
|
|
}
|
|
|
|
let span = tracing::debug_span!("tessellate_multi_contour", contours = contours.len());
|
|
let _enter = span.enter();
|
|
|
|
tracing::debug!("Starting multi-contour tessellation");
|
|
|
|
// Build path with multiple sub-paths (contours)
|
|
let mut builder = Path::builder();
|
|
|
|
for (idx, contour) in contours.iter().enumerate() {
|
|
if contour.is_empty() {
|
|
tracing::warn!(contour_idx = idx, "Contour is empty, skipping");
|
|
continue;
|
|
}
|
|
|
|
tracing::trace!(
|
|
contour_idx = idx,
|
|
vertices = contour.len(),
|
|
first_x = contour[0].x,
|
|
first_y = contour[0].y,
|
|
"Processing contour"
|
|
);
|
|
if contour.len() > 1 {
|
|
tracing::trace!(
|
|
last_x = contour[contour.len() - 1].x,
|
|
last_y = contour[contour.len() - 1].y,
|
|
"Contour end vertex"
|
|
);
|
|
}
|
|
|
|
// Each contour is a separate closed sub-path
|
|
builder.begin(to_lyon_point(contour[0]));
|
|
for (i, v) in contour[1..].iter().enumerate() {
|
|
builder.line_to(to_lyon_point(*v));
|
|
if i < 3 || i >= contour.len() - 4 {
|
|
tracing::trace!(vertex_idx = i + 1, x = v.x, y = v.y, "Contour vertex");
|
|
} else if i == 3 {
|
|
tracing::trace!(
|
|
omitted = contour.len() - 7,
|
|
"Additional vertices omitted from trace"
|
|
);
|
|
}
|
|
}
|
|
builder.end(true); // Close this contour
|
|
tracing::trace!(contour_idx = idx, "Contour closed");
|
|
}
|
|
|
|
tracing::debug!("Building Lyon path");
|
|
let path = builder.build();
|
|
tracing::debug!("Path built successfully");
|
|
|
|
// Tessellate with EvenOdd fill rule - overlapping areas become holes
|
|
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
|
|
let mut tessellator = FillTessellator::new();
|
|
|
|
tracing::debug!("Starting tessellation with EvenOdd fill rule");
|
|
match tessellator.tessellate_path(
|
|
&path,
|
|
&FillOptions::default().with_fill_rule(FillRule::EvenOdd),
|
|
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| SimpleVertex {
|
|
position: vertex.position().to_array(),
|
|
}),
|
|
) {
|
|
Ok(()) => {
|
|
tracing::debug!(
|
|
vertices = geometry.vertices.len(),
|
|
indices = geometry.indices.len(),
|
|
triangles = geometry.indices.len() / 3,
|
|
"Tessellation successful"
|
|
);
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(error = %e, "Tessellation failed");
|
|
return Err(Box::new(e));
|
|
}
|
|
}
|
|
|
|
Ok(build_mesh(
|
|
&geometry.vertices,
|
|
&geometry.indices,
|
|
color,
|
|
))
|
|
}
|
|
|
|
/// Tessellate a stroked path and return mesh
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if no vertices are provided or if tessellation fails.
|
|
pub(crate) fn tessellate_stroke(
|
|
vertices: &[Vec2],
|
|
color: Color,
|
|
width: f32,
|
|
closed: bool,
|
|
) -> Result<Mesh, Box<dyn std::error::Error>> {
|
|
if vertices.is_empty() {
|
|
return Err("No vertices provided".into());
|
|
}
|
|
|
|
// Build path
|
|
let mut builder = Path::builder();
|
|
builder.begin(to_lyon_point(vertices[0]));
|
|
for v in &vertices[1..] {
|
|
builder.line_to(to_lyon_point(*v));
|
|
}
|
|
builder.end(closed);
|
|
let path = builder.build();
|
|
|
|
// Tessellate with round caps and joins for smooth lines
|
|
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
|
|
let mut tessellator = StrokeTessellator::new();
|
|
|
|
tessellator.tessellate_path(
|
|
&path,
|
|
&StrokeOptions::default()
|
|
.with_line_width(width)
|
|
.with_line_cap(LineCap::Round)
|
|
.with_line_join(LineJoin::Round),
|
|
&mut BuffersBuilder::new(&mut geometry, |vertex: StrokeVertex| SimpleVertex {
|
|
position: vertex.position().to_array(),
|
|
}),
|
|
)?;
|
|
|
|
Ok(build_mesh(
|
|
&geometry.vertices,
|
|
&geometry.indices,
|
|
color,
|
|
))
|
|
}
|
|
|
|
/// Tessellate a circle and return mesh
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if tessellation fails.
|
|
pub(crate) fn tessellate_circle(
|
|
center: Vec2,
|
|
radius: f32,
|
|
color: Color,
|
|
filled: bool,
|
|
stroke_width: f32,
|
|
) -> Result<Mesh, Box<dyn std::error::Error>> {
|
|
let mut builder = Path::builder();
|
|
builder.add_circle(to_lyon_point(center), radius, lyon::path::Winding::Positive);
|
|
let path = builder.build();
|
|
|
|
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
|
|
|
|
if filled {
|
|
let mut tessellator = FillTessellator::new();
|
|
tessellator.tessellate_path(
|
|
&path,
|
|
&FillOptions::default(),
|
|
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| SimpleVertex {
|
|
position: vertex.position().to_array(),
|
|
}),
|
|
)?;
|
|
} else {
|
|
let mut tessellator = StrokeTessellator::new();
|
|
tessellator.tessellate_path(
|
|
&path,
|
|
&StrokeOptions::default().with_line_width(stroke_width),
|
|
&mut BuffersBuilder::new(&mut geometry, |vertex: StrokeVertex| SimpleVertex {
|
|
position: vertex.position().to_array(),
|
|
}),
|
|
)?;
|
|
}
|
|
|
|
Ok(build_mesh(
|
|
&geometry.vertices,
|
|
&geometry.indices,
|
|
color,
|
|
))
|
|
}
|
|
|
|
/// Tessellate an arc (partial circle) and return mesh
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if tessellation fails.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn tessellate_arc(
|
|
center: Vec2,
|
|
radius: f32,
|
|
start_angle_degrees: f32,
|
|
arc_angle_degrees: f32,
|
|
color: Color,
|
|
stroke_width: f32,
|
|
segments: usize,
|
|
direction: crate::circle_geometry::CircleDirection,
|
|
) -> Result<Mesh, Box<dyn std::error::Error>> {
|
|
use crate::circle_geometry::arc_points;
|
|
|
|
let start_angle = start_angle_degrees.to_radians();
|
|
let sweep_angle = arc_angle_degrees.to_radians();
|
|
|
|
let mut builder = Path::builder();
|
|
|
|
// Start point of the arc (arc_points returns everything *after* this)
|
|
builder.begin(point(
|
|
center.x + radius * start_angle.cos(),
|
|
center.y + radius * start_angle.sin(),
|
|
));
|
|
|
|
// Remaining points — single source of truth for arc sampling
|
|
for pt in arc_points(
|
|
center,
|
|
radius,
|
|
start_angle,
|
|
sweep_angle,
|
|
segments,
|
|
direction,
|
|
) {
|
|
builder.line_to(point(pt.x, pt.y));
|
|
}
|
|
|
|
builder.end(false); // open arc, not a closed polygon
|
|
let path = builder.build();
|
|
|
|
// Tessellate stroke
|
|
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
|
|
let mut tessellator = StrokeTessellator::new();
|
|
|
|
tessellator.tessellate_path(
|
|
&path,
|
|
&StrokeOptions::default()
|
|
.with_line_width(stroke_width)
|
|
.with_line_cap(lyon::tessellation::LineCap::Round)
|
|
.with_line_join(lyon::tessellation::LineJoin::Round),
|
|
&mut BuffersBuilder::new(&mut geometry, |vertex: StrokeVertex| SimpleVertex {
|
|
position: vertex.position().to_array(),
|
|
}),
|
|
)?;
|
|
|
|
Ok(build_mesh(
|
|
&geometry.vertices,
|
|
&geometry.indices,
|
|
color,
|
|
))
|
|
}
|