feat(pineal): cierra stub export — PlanRecorder + exporter SVG
Fase F: primer stub de pineal cerrado. pineal-render: - PlanRecorder — un Canvas que graba cada llamada como RenderCmd en un RenderPlan. Es el puente painter→backend-diferido y la infraestructura de testing (snapshot de planes). pineal-export: - svg::to_svg(plan, w, h) — RenderPlan → documento SVG completo. Cubre FillRect/StrokeRect/StrokeLine/StrokePolyline/DrawText + FillTriangleStrip (strip→polígonos con color promedio). XML-escape en texto. v1: clips ignorados (documentado). - pdf queda como placeholder documentado. Tests: 1 recorder + 4 svg (well-formed, primitivas, xml-escape, triangle-strip→polygons). cargo check --workspace verde. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
//! `PlanRecorder` — un [`Canvas`] que graba cada llamada como `RenderCmd`
|
||||
//! en un [`RenderPlan`], en vez de dibujar.
|
||||
//!
|
||||
//! Es el puente entre los painters (que hablan contra `Canvas`) y los
|
||||
//! backends diferidos: `pineal-export` consume el plan grabado y emite
|
||||
//! SVG; los tests de snapshot comparan planes.
|
||||
|
||||
use crate::{Canvas, Color, Point, Rect, RenderCmd, RenderPlan, StrokeStyle};
|
||||
|
||||
/// Canvas que materializa todo lo dibujado en un `RenderPlan`.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PlanRecorder {
|
||||
plan: RenderPlan,
|
||||
}
|
||||
|
||||
impl PlanRecorder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Consume el recorder y devuelve el plan acumulado.
|
||||
pub fn into_plan(self) -> RenderPlan {
|
||||
self.plan
|
||||
}
|
||||
|
||||
/// Acceso de sólo-lectura al plan en construcción.
|
||||
pub fn plan(&self) -> &RenderPlan {
|
||||
&self.plan
|
||||
}
|
||||
}
|
||||
|
||||
impl Canvas for PlanRecorder {
|
||||
fn push_clip(&mut self, rect: Rect) {
|
||||
self.plan.push(RenderCmd::PushClip(rect));
|
||||
}
|
||||
|
||||
fn pop_clip(&mut self) {
|
||||
self.plan.push(RenderCmd::PopClip);
|
||||
}
|
||||
|
||||
fn fill_rect(&mut self, rect: Rect, color: Color) {
|
||||
self.plan.push(RenderCmd::FillRect { rect, color });
|
||||
}
|
||||
|
||||
fn stroke_rect(&mut self, rect: Rect, stroke: StrokeStyle) {
|
||||
self.plan.push(RenderCmd::StrokeRect { rect, stroke });
|
||||
}
|
||||
|
||||
fn stroke_line(&mut self, a: Point, b: Point, stroke: StrokeStyle) {
|
||||
self.plan.push(RenderCmd::StrokeLine { a, b, stroke });
|
||||
}
|
||||
|
||||
fn stroke_polyline(&mut self, coords: &[f32], stroke: StrokeStyle) {
|
||||
self.plan.push(RenderCmd::StrokePolyline {
|
||||
coords: coords.to_vec(),
|
||||
stroke,
|
||||
});
|
||||
}
|
||||
|
||||
fn fill_triangle_strip(&mut self, coords: &[f32], colors: &[Color]) {
|
||||
self.plan.push(RenderCmd::FillTriangleStrip {
|
||||
coords: coords.to_vec(),
|
||||
colors: colors.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
fn draw_text(&mut self, p: Point, text: &str, color: Color, size_px: f32) {
|
||||
self.plan.push(RenderCmd::DrawText {
|
||||
p,
|
||||
text: text.to_string(),
|
||||
color,
|
||||
size_px,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn records_calls_in_order() {
|
||||
let mut rec = PlanRecorder::new();
|
||||
rec.fill_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::WHITE);
|
||||
rec.stroke_line(
|
||||
Point::new(0.0, 0.0),
|
||||
Point::new(10.0, 10.0),
|
||||
StrokeStyle::new(1.0, Color::BLACK),
|
||||
);
|
||||
let plan = rec.into_plan();
|
||||
assert_eq!(plan.cmds.len(), 2);
|
||||
assert!(matches!(plan.cmds[0], RenderCmd::FillRect { .. }));
|
||||
assert!(matches!(plan.cmds[1], RenderCmd::StrokeLine { .. }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user