create a svg log instead of caching the commands where they do not
belong...
This commit is contained in:
parent
156301f272
commit
ece26bfe04
@ -33,7 +33,7 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
|
|||||||
for turtle in &world.turtles {
|
for turtle in &world.turtles {
|
||||||
for cmd in &turtle.commands {
|
for cmd in &turtle.commands {
|
||||||
match cmd {
|
match cmd {
|
||||||
DrawCommand::Mesh { data, source: _ } => {
|
DrawCommand::Mesh { data } => {
|
||||||
draw_mesh(&data.to_mesh());
|
draw_mesh(&data.to_mesh());
|
||||||
}
|
}
|
||||||
DrawCommand::Text {
|
DrawCommand::Text {
|
||||||
@ -42,7 +42,6 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
|
|||||||
heading,
|
heading,
|
||||||
font_size,
|
font_size,
|
||||||
color,
|
color,
|
||||||
source: _,
|
|
||||||
} => {
|
} => {
|
||||||
draw_text_command(text, *position, *heading, *font_size, *color);
|
draw_text_command(text, *position, *heading, *font_size, *color);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,6 +81,7 @@ pub(crate) fn execute_command_side_effects(
|
|||||||
params: &mut TurtleParams,
|
params: &mut TurtleParams,
|
||||||
filling: &mut Option<FillState>,
|
filling: &mut Option<FillState>,
|
||||||
commands: &mut Vec<DrawCommand>,
|
commands: &mut Vec<DrawCommand>,
|
||||||
|
svg_log: &mut crate::state::SvgLog,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match command {
|
match command {
|
||||||
TurtleCommand::BeginFill => {
|
TurtleCommand::BeginFill => {
|
||||||
@ -131,18 +132,12 @@ pub(crate) fn execute_command_side_effects(
|
|||||||
contours = fill_state.contours.len(),
|
contours = fill_state.contours.len(),
|
||||||
"Successfully created fill mesh - persisting to commands"
|
"Successfully created fill mesh - persisting to commands"
|
||||||
);
|
);
|
||||||
commands.push(DrawCommand::Mesh {
|
commands.push(DrawCommand::Mesh { data: mesh_data });
|
||||||
data: mesh_data,
|
#[cfg(feature = "svg")]
|
||||||
source: crate::state::TurtleSource {
|
svg_log.push(crate::state::SvgRecord::Fill {
|
||||||
command: crate::commands::TurtleCommand::EndFill,
|
contours: fill_state.contours,
|
||||||
color: params.color,
|
|
||||||
fill_color: fill_state.fill_color,
|
fill_color: fill_state.fill_color,
|
||||||
pen_width: params.pen_width,
|
stroke_color: params.color,
|
||||||
start_position: fill_state.start_position,
|
|
||||||
end_position: fill_state.start_position,
|
|
||||||
start_heading: params.heading,
|
|
||||||
contours: Some(fill_state.contours.clone()),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
tracing::error!(turtle_id, "Failed to tessellate contours");
|
tracing::error!(turtle_id, "Failed to tessellate contours");
|
||||||
@ -177,6 +172,7 @@ pub(crate) fn execute_command_side_effects(
|
|||||||
|
|
||||||
TurtleCommand::Reset => {
|
TurtleCommand::Reset => {
|
||||||
commands.clear();
|
commands.clear();
|
||||||
|
svg_log.clear();
|
||||||
*filling = None;
|
*filling = None;
|
||||||
*params = TurtleParams::default();
|
*params = TurtleParams::default();
|
||||||
true
|
true
|
||||||
@ -189,16 +185,12 @@ pub(crate) fn execute_command_side_effects(
|
|||||||
heading: params.heading,
|
heading: params.heading,
|
||||||
font_size: *font_size,
|
font_size: *font_size,
|
||||||
color: params.color,
|
color: params.color,
|
||||||
source: crate::state::TurtleSource {
|
});
|
||||||
command: command.clone(),
|
#[cfg(feature = "svg")]
|
||||||
|
svg_log.push(crate::state::SvgRecord::Text {
|
||||||
|
text: text.clone(),
|
||||||
|
position: params.position,
|
||||||
color: params.color,
|
color: params.color,
|
||||||
fill_color: params.fill_color.unwrap_or(BLACK),
|
|
||||||
pen_width: params.pen_width,
|
|
||||||
start_position: params.position,
|
|
||||||
end_position: params.position,
|
|
||||||
start_heading: params.heading,
|
|
||||||
contours: None,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -340,19 +332,7 @@ pub(crate) fn tessellate_command(
|
|||||||
)
|
)
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
Some(DrawCommand::Mesh {
|
Some(DrawCommand::Mesh { data: mesh_data })
|
||||||
data: mesh_data,
|
|
||||||
source: crate::state::TurtleSource {
|
|
||||||
command: command.clone(),
|
|
||||||
color: start.color,
|
|
||||||
fill_color: start.fill_color.unwrap_or(BLACK),
|
|
||||||
pen_width: start.pen_width,
|
|
||||||
start_position: start.position,
|
|
||||||
end_position,
|
|
||||||
start_heading: start.heading,
|
|
||||||
contours: None,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::Circle {
|
TurtleCommand::Circle {
|
||||||
@ -380,19 +360,7 @@ pub(crate) fn tessellate_command(
|
|||||||
)
|
)
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
Some(DrawCommand::Mesh {
|
Some(DrawCommand::Mesh { data: mesh_data })
|
||||||
data: mesh_data,
|
|
||||||
source: crate::state::TurtleSource {
|
|
||||||
command: command.clone(),
|
|
||||||
color: start.color,
|
|
||||||
fill_color: start.fill_color.unwrap_or(BLACK),
|
|
||||||
pen_width: start.pen_width,
|
|
||||||
start_position: start.position,
|
|
||||||
end_position,
|
|
||||||
start_heading: start.heading,
|
|
||||||
contours: None,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// `produces_drawing()` guards entry — this arm is only reachable if
|
// `produces_drawing()` guards entry — this arm is only reachable if
|
||||||
@ -401,6 +369,48 @@ pub(crate) fn tessellate_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push an [`SvgRecord`] for a completed line or arc drawing command.
|
||||||
|
///
|
||||||
|
/// Only compiled when the `svg` feature is enabled.
|
||||||
|
/// Must be called at the same call sites as `tessellate_command` so that
|
||||||
|
/// `svg_log` stays in sync with `commands`.
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
pub(crate) fn push_svg_for_draw(
|
||||||
|
command: &TurtleCommand,
|
||||||
|
start: &TurtleParams,
|
||||||
|
end_position: Vec2,
|
||||||
|
svg_log: &mut crate::state::SvgLog,
|
||||||
|
) {
|
||||||
|
use crate::state::SvgRecord;
|
||||||
|
match command {
|
||||||
|
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
||||||
|
svg_log.push(SvgRecord::Line {
|
||||||
|
start: start.position,
|
||||||
|
end: end_position,
|
||||||
|
color: start.color,
|
||||||
|
pen_width: start.pen_width,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TurtleCommand::Circle {
|
||||||
|
radius,
|
||||||
|
angle,
|
||||||
|
direction,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
svg_log.push(SvgRecord::Arc {
|
||||||
|
start_position: start.position,
|
||||||
|
start_heading: start.heading,
|
||||||
|
radius: *radius,
|
||||||
|
angle: *angle,
|
||||||
|
direction: *direction,
|
||||||
|
color: start.color,
|
||||||
|
pen_width: start.pen_width,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute a single turtle command, updating state and adding draw commands.
|
/// Execute a single turtle command, updating state and adding draw commands.
|
||||||
#[tracing::instrument(skip(state))]
|
#[tracing::instrument(skip(state))]
|
||||||
pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
|
pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
|
||||||
@ -412,6 +422,7 @@ pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
|
|||||||
&mut state.params,
|
&mut state.params,
|
||||||
&mut state.filling,
|
&mut state.filling,
|
||||||
&mut state.commands,
|
&mut state.commands,
|
||||||
|
&mut state.svg_log,
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -429,8 +440,15 @@ pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
|
|||||||
&mut state.filling,
|
&mut state.filling,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Phase 4: tessellate and persist the committed drawing
|
// Phase 4: tessellate, push SVG record, and persist the committed drawing
|
||||||
if let Some(draw_cmd) = tessellate_command(command, &start_params, state.params.position) {
|
if let Some(draw_cmd) = tessellate_command(command, &start_params, state.params.position) {
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
push_svg_for_draw(
|
||||||
|
command,
|
||||||
|
&start_params,
|
||||||
|
state.params.position,
|
||||||
|
&mut state.svg_log,
|
||||||
|
);
|
||||||
state.commands.push(draw_cmd);
|
state.commands.push(draw_cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -479,6 +497,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
filling: None,
|
filling: None,
|
||||||
commands: Vec::new(),
|
commands: Vec::new(),
|
||||||
|
svg_log: crate::state::SvgLog::default(),
|
||||||
tween_controller: TweenController::default(),
|
tween_controller: TweenController::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
pub mod svg_export {
|
pub mod svg_export {
|
||||||
use crate::commands::TurtleCommand;
|
|
||||||
use crate::export::{DrawingExporter, ExportError};
|
use crate::export::{DrawingExporter, ExportError};
|
||||||
use crate::state::{DrawCommand, TurtleWorld};
|
use crate::state::{SvgRecord, TurtleWorld};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use svg::{
|
use svg::{
|
||||||
node::element::{Circle, Line, Polygon, Text as SvgText},
|
node::element::{Circle, Line, Text as SvgText},
|
||||||
Document,
|
Document,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,48 +36,50 @@ pub mod svg_export {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for turtle in &world.turtles {
|
for turtle in &world.turtles {
|
||||||
for cmd in &turtle.commands {
|
for record in &turtle.svg_log.records {
|
||||||
match cmd {
|
match record {
|
||||||
DrawCommand::Mesh { source, .. } => {
|
SvgRecord::Line {
|
||||||
match &source.command {
|
start,
|
||||||
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
end,
|
||||||
// Straight line — emit as SVG <line>
|
color,
|
||||||
let start = source.start_position;
|
pen_width,
|
||||||
let end = source.end_position;
|
} => {
|
||||||
update_bounds(
|
update_bounds(
|
||||||
&mut min_x, &mut max_x, &mut min_y, &mut max_y, start.x,
|
&mut min_x, &mut max_x, &mut min_y, &mut max_y, start.x, start.y,
|
||||||
start.y,
|
|
||||||
);
|
);
|
||||||
update_bounds(
|
update_bounds(
|
||||||
&mut min_x, &mut max_x, &mut min_y, &mut max_y, end.x,
|
&mut min_x, &mut max_x, &mut min_y, &mut max_y, end.x, end.y,
|
||||||
end.y,
|
|
||||||
);
|
);
|
||||||
let line = Line::new()
|
let line = Line::new()
|
||||||
.set("x1", start.x)
|
.set("x1", start.x)
|
||||||
.set("y1", start.y)
|
.set("y1", start.y)
|
||||||
.set("x2", end.x)
|
.set("x2", end.x)
|
||||||
.set("y2", end.y)
|
.set("y2", end.y)
|
||||||
.set("stroke", color_to_svg(source.color))
|
.set("stroke", color_to_svg(*color))
|
||||||
.set("stroke-width", source.pen_width);
|
.set("stroke-width", *pen_width);
|
||||||
doc = doc.add(line);
|
doc = doc.add(line);
|
||||||
}
|
}
|
||||||
TurtleCommand::Circle {
|
|
||||||
|
SvgRecord::Arc {
|
||||||
|
start_position,
|
||||||
|
start_heading,
|
||||||
radius,
|
radius,
|
||||||
angle,
|
angle,
|
||||||
direction,
|
direction,
|
||||||
..
|
color,
|
||||||
|
pen_width,
|
||||||
} => {
|
} => {
|
||||||
use crate::circle_geometry::CircleGeometry;
|
use crate::circle_geometry::CircleGeometry;
|
||||||
use crate::general::Radians;
|
use crate::general::Radians;
|
||||||
let geom = CircleGeometry::new(
|
let geom = CircleGeometry::new(
|
||||||
source.start_position,
|
*start_position,
|
||||||
Radians::new(source.start_heading),
|
Radians::new(*start_heading),
|
||||||
*radius,
|
*radius,
|
||||||
*direction,
|
*direction,
|
||||||
);
|
);
|
||||||
let center = geom.center;
|
let center = geom.center;
|
||||||
if (angle.value() - 360.0).abs() < 1e-3 {
|
// Include the bounding box of the full circle so partial arcs
|
||||||
// Full circle — emit as SVG <circle>
|
// are never clipped.
|
||||||
update_bounds(
|
update_bounds(
|
||||||
&mut min_x,
|
&mut min_x,
|
||||||
&mut max_x,
|
&mut max_x,
|
||||||
@ -95,35 +96,20 @@ pub mod svg_export {
|
|||||||
center.x + radius,
|
center.x + radius,
|
||||||
center.y + radius,
|
center.y + radius,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (angle.value() - 360.0).abs() < 1e-3 {
|
||||||
|
// Full circle — emit as <circle>
|
||||||
let circle = Circle::new()
|
let circle = Circle::new()
|
||||||
.set("cx", center.x)
|
.set("cx", center.x)
|
||||||
.set("cy", center.y)
|
.set("cy", center.y)
|
||||||
.set("r", *radius)
|
.set("r", *radius)
|
||||||
.set("stroke", color_to_svg(source.color))
|
.set("stroke", color_to_svg(*color))
|
||||||
.set("stroke-width", source.pen_width)
|
.set("stroke-width", *pen_width)
|
||||||
.set("fill", "none");
|
.set("fill", "none");
|
||||||
doc = doc.add(circle);
|
doc = doc.add(circle);
|
||||||
} else {
|
} else {
|
||||||
// Partial arc — emit as SVG <path> with A command
|
// Partial arc — emit as <path A …>
|
||||||
let start = source.start_position;
|
let end = geom.position_at_angle(angle.as_radians().value());
|
||||||
let end = source.end_position;
|
|
||||||
// For arcs, include the full circle bounds to ensure complete visibility
|
|
||||||
update_bounds(
|
|
||||||
&mut min_x,
|
|
||||||
&mut max_x,
|
|
||||||
&mut min_y,
|
|
||||||
&mut max_y,
|
|
||||||
center.x - radius,
|
|
||||||
center.y - radius,
|
|
||||||
);
|
|
||||||
update_bounds(
|
|
||||||
&mut min_x,
|
|
||||||
&mut max_x,
|
|
||||||
&mut min_y,
|
|
||||||
&mut max_y,
|
|
||||||
center.x + radius,
|
|
||||||
center.y + radius,
|
|
||||||
);
|
|
||||||
let large_arc = if angle.value() > 180.0 { 1 } else { 0 };
|
let large_arc = if angle.value() > 180.0 { 1 } else { 0 };
|
||||||
let sweep = match direction {
|
let sweep = match direction {
|
||||||
crate::circle_geometry::CircleDirection::Left => 0,
|
crate::circle_geometry::CircleDirection::Left => 0,
|
||||||
@ -131,31 +117,34 @@ pub mod svg_export {
|
|||||||
};
|
};
|
||||||
let d = format!(
|
let d = format!(
|
||||||
"M {} {} A {} {} 0 {} {} {} {}",
|
"M {} {} A {} {} 0 {} {} {} {}",
|
||||||
start.x,
|
start_position.x,
|
||||||
start.y,
|
start_position.y,
|
||||||
radius,
|
radius,
|
||||||
radius,
|
radius,
|
||||||
large_arc,
|
large_arc,
|
||||||
sweep,
|
sweep,
|
||||||
end.x,
|
end.x,
|
||||||
end.y
|
end.y,
|
||||||
);
|
);
|
||||||
let path = svg::node::element::Path::new()
|
let path = svg::node::element::Path::new()
|
||||||
.set("d", d)
|
.set("d", d)
|
||||||
.set("stroke", color_to_svg(source.color))
|
.set("stroke", color_to_svg(*color))
|
||||||
.set("stroke-width", source.pen_width)
|
.set("stroke-width", *pen_width)
|
||||||
.set("fill", "none");
|
.set("fill", "none");
|
||||||
doc = doc.add(path);
|
doc = doc.add(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TurtleCommand::EndFill => {
|
|
||||||
// Fill contours — emit as SVG <path> with evenodd fill rule
|
SvgRecord::Fill {
|
||||||
if let Some(contours) = &source.contours {
|
contours,
|
||||||
|
fill_color,
|
||||||
|
stroke_color,
|
||||||
|
} => {
|
||||||
for contour in contours {
|
for contour in contours {
|
||||||
for point in contour {
|
for point in contour {
|
||||||
update_bounds(
|
update_bounds(
|
||||||
&mut min_x, &mut max_x, &mut min_y, &mut max_y,
|
&mut min_x, &mut max_x, &mut min_y, &mut max_y, point.x,
|
||||||
point.x, point.y,
|
point.y,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,15 +154,9 @@ pub mod svg_export {
|
|||||||
if i > 0 {
|
if i > 0 {
|
||||||
d.push(' ');
|
d.push(' ');
|
||||||
}
|
}
|
||||||
d.push_str(&format!(
|
d.push_str(&format!("M {} {}", contour[0].x, contour[0].y));
|
||||||
"M {} {}",
|
|
||||||
contour[0].x, contour[0].y
|
|
||||||
));
|
|
||||||
for point in contour.iter().skip(1) {
|
for point in contour.iter().skip(1) {
|
||||||
d.push_str(&format!(
|
d.push_str(&format!(" L {} {}", point.x, point.y));
|
||||||
" L {} {}",
|
|
||||||
point.x, point.y
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
d.push_str(" Z");
|
d.push_str(" Z");
|
||||||
}
|
}
|
||||||
@ -181,63 +164,17 @@ pub mod svg_export {
|
|||||||
if !d.is_empty() {
|
if !d.is_empty() {
|
||||||
let path = svg::node::element::Path::new()
|
let path = svg::node::element::Path::new()
|
||||||
.set("d", d)
|
.set("d", d)
|
||||||
.set("fill", color_to_svg(source.fill_color))
|
.set("fill", color_to_svg(*fill_color))
|
||||||
.set("fill-rule", "evenodd")
|
.set("fill-rule", "evenodd")
|
||||||
.set("stroke", color_to_svg(source.color));
|
.set("stroke", color_to_svg(*stroke_color));
|
||||||
doc = doc.add(path);
|
doc = doc.add(path);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Fallback: no contour data — emit a dummy polygon
|
|
||||||
update_bounds(
|
|
||||||
&mut min_x,
|
|
||||||
&mut max_x,
|
|
||||||
&mut min_y,
|
|
||||||
&mut max_y,
|
|
||||||
source.start_position.x,
|
|
||||||
source.start_position.y,
|
|
||||||
);
|
|
||||||
update_bounds(
|
|
||||||
&mut min_x,
|
|
||||||
&mut max_x,
|
|
||||||
&mut min_y,
|
|
||||||
&mut max_y,
|
|
||||||
source.start_position.x + 10.0,
|
|
||||||
source.start_position.y + 10.0,
|
|
||||||
);
|
|
||||||
update_bounds(
|
|
||||||
&mut min_x,
|
|
||||||
&mut max_x,
|
|
||||||
&mut min_y,
|
|
||||||
&mut max_y,
|
|
||||||
source.start_position.x + 5.0,
|
|
||||||
source.start_position.y + 15.0,
|
|
||||||
);
|
|
||||||
let poly = Polygon::new()
|
|
||||||
.set(
|
|
||||||
"points",
|
|
||||||
format!(
|
|
||||||
"{},{} {},{} {},{}",
|
|
||||||
source.start_position.x,
|
|
||||||
source.start_position.y,
|
|
||||||
source.start_position.x + 10.0,
|
|
||||||
source.start_position.y + 10.0,
|
|
||||||
source.start_position.x + 5.0,
|
|
||||||
source.start_position.y + 15.0
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.set("fill", color_to_svg(source.fill_color))
|
|
||||||
.set("stroke", color_to_svg(source.color));
|
|
||||||
doc = doc.add(poly);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
SvgRecord::Text {
|
||||||
}
|
|
||||||
}
|
|
||||||
DrawCommand::Text {
|
|
||||||
text,
|
text,
|
||||||
position,
|
position,
|
||||||
source,
|
color,
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
update_bounds(
|
update_bounds(
|
||||||
&mut min_x, &mut max_x, &mut min_y, &mut max_y, position.x,
|
&mut min_x, &mut max_x, &mut min_y, &mut max_y, position.x,
|
||||||
@ -246,7 +183,7 @@ pub mod svg_export {
|
|||||||
let txt = SvgText::new()
|
let txt = SvgText::new()
|
||||||
.set("x", position.x)
|
.set("x", position.x)
|
||||||
.set("y", position.y)
|
.set("y", position.y)
|
||||||
.set("fill", color_to_svg(source.color))
|
.set("fill", color_to_svg(*color))
|
||||||
.add(svg::node::Text::new(text.clone()));
|
.add(svg::node::Text::new(text.clone()));
|
||||||
doc = doc.add(txt);
|
doc = doc.add(txt);
|
||||||
}
|
}
|
||||||
@ -261,7 +198,6 @@ pub mod svg_export {
|
|||||||
let view_box = format!("{} {} {} {}", min_x - 20.0, min_y - 20.0, width, height);
|
let view_box = format!("{} {} {} {}", min_x - 20.0, min_y - 20.0, width, height);
|
||||||
doc = doc.set("viewBox", view_box);
|
doc = doc.set("viewBox", view_box);
|
||||||
} else {
|
} else {
|
||||||
// Default viewBox if no elements
|
|
||||||
doc = doc.set("viewBox", "0 0 400 400");
|
doc = doc.set("viewBox", "0 0 400 400");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -299,6 +299,13 @@ impl TurtleApp {
|
|||||||
if let Some(draw_cmd) =
|
if let Some(draw_cmd) =
|
||||||
execution::tessellate_command(&completed_cmd, &tween_start, end_state.position)
|
execution::tessellate_command(&completed_cmd, &tween_start, end_state.position)
|
||||||
{
|
{
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
execution::push_svg_for_draw(
|
||||||
|
&completed_cmd,
|
||||||
|
&tween_start,
|
||||||
|
end_state.position,
|
||||||
|
&mut turtle.svg_log,
|
||||||
|
);
|
||||||
turtle.commands.push(draw_cmd);
|
turtle.commands.push(draw_cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,6 +66,9 @@ pub(crate) struct Turtle {
|
|||||||
// Drawing commands created by this turtle
|
// Drawing commands created by this turtle
|
||||||
pub(crate) commands: Vec<DrawCommand>,
|
pub(crate) commands: Vec<DrawCommand>,
|
||||||
|
|
||||||
|
// SVG draw-event log — populated alongside `commands`, consumed by the SVG exporter
|
||||||
|
pub(crate) svg_log: SvgLog,
|
||||||
|
|
||||||
// Animation controller for this turtle
|
// Animation controller for this turtle
|
||||||
pub(crate) tween_controller: TweenController,
|
pub(crate) tween_controller: TweenController,
|
||||||
}
|
}
|
||||||
@ -77,6 +80,7 @@ impl Default for Turtle {
|
|||||||
params: TurtleParams::default(),
|
params: TurtleParams::default(),
|
||||||
filling: None,
|
filling: None,
|
||||||
commands: Vec::new(),
|
commands: Vec::new(),
|
||||||
|
svg_log: SvgLog::default(),
|
||||||
tween_controller: TweenController::new(CommandQueue::new(), AnimationSpeed::default()),
|
tween_controller: TweenController::new(CommandQueue::new(), AnimationSpeed::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,6 +100,7 @@ impl Turtle {
|
|||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
// Clear all drawings
|
// Clear all drawings
|
||||||
self.commands.clear();
|
self.commands.clear();
|
||||||
|
self.svg_log.clear();
|
||||||
|
|
||||||
// Clear fill state
|
// Clear fill state
|
||||||
self.filling = None;
|
self.filling = None;
|
||||||
@ -123,6 +128,7 @@ impl Turtle {
|
|||||||
&mut self.params,
|
&mut self.params,
|
||||||
&mut self.filling,
|
&mut self.filling,
|
||||||
&mut self.commands,
|
&mut self.commands,
|
||||||
|
&mut self.svg_log,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,6 +283,70 @@ impl Turtle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The draw-event log for SVG export.
|
||||||
|
///
|
||||||
|
/// When the `svg` feature is **disabled** this is a zero-sized type (ZST) with
|
||||||
|
/// no fields — it compiles away entirely and adds zero overhead to `Turtle`.
|
||||||
|
/// When the feature is **enabled** it owns a `Vec<SvgRecord>` that the SVG
|
||||||
|
/// exporter consumes after rendering.
|
||||||
|
///
|
||||||
|
/// All methods are always callable so function signatures that accept
|
||||||
|
/// `&mut SvgLog` need no feature-gating at the parameter level.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub(crate) struct SvgLog {
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
pub(crate) records: Vec<SvgRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SvgLog {
|
||||||
|
pub(crate) fn clear(&mut self) {
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
self.records.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
pub(crate) fn push(&mut self, record: SvgRecord) {
|
||||||
|
self.records.push(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A drawing event captured for SVG export.
|
||||||
|
///
|
||||||
|
/// Only compiled when the `svg` feature is enabled.
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum SvgRecord {
|
||||||
|
/// A straight-line stroke.
|
||||||
|
Line {
|
||||||
|
start: Vec2,
|
||||||
|
end: Vec2,
|
||||||
|
color: Color,
|
||||||
|
pen_width: f32,
|
||||||
|
},
|
||||||
|
/// An arc or full-circle stroke.
|
||||||
|
Arc {
|
||||||
|
start_position: Vec2,
|
||||||
|
start_heading: f32,
|
||||||
|
radius: crate::general::Precision,
|
||||||
|
angle: crate::general::Degrees,
|
||||||
|
direction: crate::circle_geometry::CircleDirection,
|
||||||
|
color: Color,
|
||||||
|
pen_width: f32,
|
||||||
|
},
|
||||||
|
/// A filled region (potentially with holes via the even-odd rule).
|
||||||
|
Fill {
|
||||||
|
contours: Vec<Vec<crate::general::Coordinate>>,
|
||||||
|
fill_color: Color,
|
||||||
|
stroke_color: Color,
|
||||||
|
},
|
||||||
|
/// A text element.
|
||||||
|
Text {
|
||||||
|
text: String,
|
||||||
|
position: Vec2,
|
||||||
|
color: Color,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/// Cached mesh data that can be cloned and converted to Mesh when needed
|
/// Cached mesh data that can be cloned and converted to Mesh when needed
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct MeshData {
|
pub(crate) struct MeshData {
|
||||||
@ -295,35 +365,19 @@ impl MeshData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drawable elements in the world
|
/// Drawable elements in the world.
|
||||||
/// All drawing is done via Lyon-tessellated meshes for consistency and quality
|
/// All drawing is done via Lyon-tessellated meshes for consistency and quality.
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct TurtleSource {
|
|
||||||
pub(crate) command: crate::commands::TurtleCommand,
|
|
||||||
pub(crate) color: Color,
|
|
||||||
pub(crate) fill_color: Color,
|
|
||||||
pub(crate) pen_width: f32,
|
|
||||||
pub(crate) start_position: Vec2,
|
|
||||||
pub(crate) end_position: Vec2,
|
|
||||||
pub(crate) start_heading: f32,
|
|
||||||
pub(crate) contours: Option<Vec<Vec<crate::general::Coordinate>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum DrawCommand {
|
pub(crate) enum DrawCommand {
|
||||||
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
|
/// Pre-tessellated mesh data (lines, arcs, circles, polygons — all use this).
|
||||||
Mesh {
|
Mesh { data: MeshData },
|
||||||
data: MeshData,
|
/// Text rendering command.
|
||||||
source: TurtleSource,
|
|
||||||
},
|
|
||||||
/// Text rendering command
|
|
||||||
Text {
|
Text {
|
||||||
text: String,
|
text: String,
|
||||||
position: Vec2,
|
position: Vec2,
|
||||||
heading: f32,
|
heading: f32,
|
||||||
font_size: crate::general::FontSize,
|
font_size: crate::general::FontSize,
|
||||||
color: Color,
|
color: Color,
|
||||||
source: TurtleSource,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -109,6 +109,7 @@ impl TweenController {
|
|||||||
params: &mut TurtleParams,
|
params: &mut TurtleParams,
|
||||||
filling: &mut Option<FillState>,
|
filling: &mut Option<FillState>,
|
||||||
commands: &mut Vec<DrawCommand>,
|
commands: &mut Vec<DrawCommand>,
|
||||||
|
svg_log: &mut crate::state::SvgLog,
|
||||||
) -> Vec<(TurtleCommand, TurtleParams, TurtleParams)> {
|
) -> Vec<(TurtleCommand, TurtleParams, TurtleParams)> {
|
||||||
// In instant mode, execute commands up to the draw calls per frame limit
|
// In instant mode, execute commands up to the draw calls per frame limit
|
||||||
if let AnimationSpeed::Instant(max_draw_calls) = self.speed {
|
if let AnimationSpeed::Instant(max_draw_calls) = self.speed {
|
||||||
@ -131,7 +132,7 @@ impl TweenController {
|
|||||||
|
|
||||||
// Execute side-effect-only commands using centralized helper
|
// Execute side-effect-only commands using centralized helper
|
||||||
if crate::execution::execute_command_side_effects(
|
if crate::execution::execute_command_side_effects(
|
||||||
&command, turtle_id, params, filling, commands,
|
&command, turtle_id, params, filling, commands, svg_log,
|
||||||
) {
|
) {
|
||||||
continue; // Command fully handled
|
continue; // Command fully handled
|
||||||
}
|
}
|
||||||
@ -255,9 +256,9 @@ impl TweenController {
|
|||||||
|
|
||||||
// Execute side-effect-only commands using centralized helper
|
// Execute side-effect-only commands using centralized helper
|
||||||
if crate::execution::execute_command_side_effects(
|
if crate::execution::execute_command_side_effects(
|
||||||
&command, turtle_id, params, filling, commands,
|
&command, turtle_id, params, filling, commands, svg_log,
|
||||||
) {
|
) {
|
||||||
return self.update(turtle_id, params, filling, commands);
|
return self.update(turtle_id, params, filling, commands, svg_log);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return drawable commands using the original start and target params
|
// Return drawable commands using the original start and target params
|
||||||
@ -265,7 +266,7 @@ impl TweenController {
|
|||||||
return vec![(command, start_params.clone(), target_params.clone())];
|
return vec![(command, start_params.clone(), target_params.clone())];
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.update(turtle_id, params, filling, commands);
|
return self.update(turtle_id, params, filling, commands, svg_log);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@ -281,16 +282,16 @@ impl TweenController {
|
|||||||
params.speed = *new_speed;
|
params.speed = *new_speed;
|
||||||
self.speed = *new_speed;
|
self.speed = *new_speed;
|
||||||
if matches!(self.speed, AnimationSpeed::Instant(_)) {
|
if matches!(self.speed, AnimationSpeed::Instant(_)) {
|
||||||
return self.update(turtle_id, params, filling, commands);
|
return self.update(turtle_id, params, filling, commands, svg_log);
|
||||||
}
|
}
|
||||||
return self.update(turtle_id, params, filling, commands);
|
return self.update(turtle_id, params, filling, commands, svg_log);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Use centralized helper for side effects
|
// Use centralized helper for side effects
|
||||||
if crate::execution::execute_command_side_effects(
|
if crate::execution::execute_command_side_effects(
|
||||||
&command, turtle_id, params, filling, commands,
|
&command, turtle_id, params, filling, commands, svg_log,
|
||||||
) {
|
) {
|
||||||
return self.update(turtle_id, params, filling, commands);
|
return self.update(turtle_id, params, filling, commands, svg_log);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user