aoer_plotty_rs/context/
mod.rs

1//! Provides the [`crate::context::Context`] struct which gives us a canvas-style drawing
2//! context which provides plotter-ready SVG files. See `Context` for more details, and examples.
3use embed_doc_image::embed_doc_image;
4use std::error::Error;
5
6use crate::context::line_filter::LineFilter;
7// use crate::context::geo_filter::GeoFilter;
8use crate::errors::ContextError;
9use crate::geo_types::clip::{try_to_geos_geometry, LineClip};
10use crate::geo_types::{shapes, ToGeos};
11use crate::prelude::{Arrangement, HatchPattern, LineHatch, ToSvg};
12use cubic_spline::{Points, SplineOpts};
13use font_kit::font::Font;
14use font_kit::hinting::HintingOptions;
15use geo::map_coords::MapCoords;
16use geo::prelude::BoundingRect;
17use geo_types::{
18    coord, Coord as Coordinate, Geometry, GeometryCollection, LineString, MultiLineString, Point,
19    Polygon, Rect,
20};
21use geos::{Geom, GeometryTypes};
22pub use kurbo::BezPath;
23use kurbo::PathEl;
24pub use kurbo::Point as BezPoint;
25use nalgebra::{Affine2, Matrix3};
26use nannou::prelude::PI_F64;
27use std::f64::consts::PI;
28use std::sync::Arc;
29use svg::Document;
30
31pub mod operation;
32
33use operation::{OPLayer, Operation};
34
35pub mod glyph_proxy;
36
37use glyph_proxy::GlyphProxy;
38
39pub mod typography;
40
41use crate::geo_types::flatten::FlattenPolygons;
42use typography::Typography;
43
44pub mod line_filter;
45
46/// # Context
47///
48/// A Context is a _drawing_ context, used to perform operations against a
49/// pseudo-canvas. Those operations are later collected up and turned into
50/// an SVG, including line strokes, fills, and all the other useful tools
51/// that we need to drive a plotter robot.
52///
53/// # Example
54///
55/// ```rust
56/// use aoer_plotty_rs::context::Context;
57///
58/// let mut ctx = Context::new();
59/// ctx.stroke("black")
60///    .fill("red")
61///    .pen(0.5)
62///    .outline(Some(5.0))
63///    .poly(vec![(0.0,0.0),
64///           (25.0,0.0),
65///           (25.0,25.0),
66///           (0.0,25.0)],
67/// vec![])
68///    .outline(None)
69///    .hatch(135.0)
70///    .stroke("blue")
71///    .fill("yellow")
72///    .circle(12.5,12.5, 5.0)
73///    .push()
74///    .hatch(180.0)
75///    .stroke("red")
76///    .fill("green")
77///    .circle(17.5,12.5,2.5)
78///    .pop().unwrap()
79///    .hatch(0.0)
80///    .clip(true)
81///    .circle(7.5,12.5,2.5)
82///    .clip(false)
83///    .stroke("brown")
84///    .pen(1.0)
85///    .line(0.0, 0.0, 3.0, 3.0)
86///    .pen(0.1)
87///    .outline(Some(1.0))
88///    .stroke("pink")
89///    .line(3.0, 0.0, 0.0, 3.0)
90///    .stroke("purple")
91///    .spline(&vec![(0.0, 25.0), (0.0, 25.0), (10.0, 20.0), (20.0,25.0), (25.0, 25.0)],
92///            8, 0.5)
93///    .push()  // Prepare for this transformation stuff...
94///    .transform(Some(
95///        &(Context::translate_matrix(25.0, 25.0)
96///        * Context::rotate_matrix(45.0)
97///        * Context::scale_matrix(1.0, 0.5)
98///    ))) // Holy crap we can multiply these?! ;)
99///    .stroke("cyan")
100///    .circle(0.0, 0.0, 8.0)
101///    .pop().unwrap() // We're back to purple and regular coords
102///    .outline(None)
103///     .stroke("green")
104///     .regular_poly(8, 80.0, 80.0, 20.0, 0.0)
105///     .star_poly(5, 30.0, 80.0, 10.0, 20.0, 0.0)
106/// ;
107/// ```
108/// ![context_basic][context_basic]
109#[embed_doc_image("context_basic", "images/context_basic.png")]
110#[derive(Clone, Debug)]
111pub struct Context {
112    operations: Vec<Operation>,
113    accuracy: f64,
114    font: Option<Font>,
115    transformation: Option<Affine2<f64>>,
116    stroke_color: Option<String>,
117    outline_stroke: Option<f64>,
118    fill_color: Option<String>,
119    line_join: String,
120    line_cap: String,
121    pen_width: f64,
122    mask: Option<Geometry<f64>>,
123    clip_previous: bool,
124    hatch_pattern: Arc<Box<dyn HatchPattern>>,
125    hatch_angle: f64,
126    hatch_scale: Option<f64>,
127    stroke_filter: Option<Arc<Box<dyn LineFilter>>>,
128    hatch_filter: Option<Arc<Box<dyn LineFilter>>>,
129    stack: Vec<Context>,
130}
131
132impl Context {
133    /// Stroke filter: Modify the lines of the stroke for subsequent operations.
134    pub fn stroke_filter(
135        &mut self,
136        filter: Option<Arc<Box<dyn LineFilter>>>, //Option<fn(&MultiLineString<f64>) -> MultiLineString<f64>>,
137    ) -> &mut Self {
138        // self.stroke_filter = match filter {
139        //     Some(ifilter) => Some(Box::leak(Box::new(ifilter))),
140        //     None => None,
141        // };
142        self.stroke_filter = filter;
143        self
144    }
145
146    pub fn hatch_filter(
147        &mut self,
148        filter: Option<Arc<Box<dyn LineFilter>>>, //Option<fn(&MultiLineString<f64>) -> MultiLineString<f64>>,
149    ) -> &mut Self {
150        self.hatch_filter = filter;
151        self
152    }
153
154    /// Set accuracy (allowed tolerance) in mm
155    pub fn accuracy(&mut self, accuracy: f64) -> &mut Self {
156        self.accuracy = accuracy;
157        self
158    }
159
160    /// Default font
161    pub fn default_font() -> Font {
162        let font_data =
163            include_bytes!("../../resources/fonts/ReliefSingleLine-Regular.ttf").to_vec();
164        Font::from_bytes(Arc::new(font_data), 0).unwrap() // We know this font is OK
165    }
166
167    /// Finalize Arrangement
168    pub fn finalize_arrangement(&self, arrangement: &Arrangement<f64>) -> Arrangement<f64> {
169        if let Ok(bounds) = self.bounds() {
170            arrangement.finalize(&bounds)
171        } else {
172            Arrangement::unit(&arrangement.viewbox())
173        }
174    }
175
176    /// Viewbox helper. Useful to create an arbitrary viewbox for
177    /// your SVGs.
178    pub fn viewbox(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect<f64> {
179        Rect::new(coord! {x: x0, y: y0}, coord! {x: x1, y: y1})
180    }
181
182    /// Helper to create a scaling matrix
183    pub fn scale_matrix(sx: f64, sy: f64) -> Affine2<f64> {
184        Affine2::from_matrix_unchecked(Matrix3::new(sx, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 1.0))
185    }
186
187    /// Unit matrix. Basically a no-op
188    pub fn unit_matrix() -> Affine2<f64> {
189        Affine2::from_matrix_unchecked(Matrix3::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0))
190    }
191
192    /// Helper to create a translation matrix
193    pub fn translate_matrix(tx: f64, ty: f64) -> Affine2<f64> {
194        Affine2::from_matrix_unchecked(Matrix3::new(1.0, 0.0, tx, 0.0, 1.0, ty, 0.0, 0.0, 1.0))
195    }
196
197    /// Angle is in degrees because I am a terrible person.
198    /// Also, compass degrees. For an SVG anyhow. I am a bastard.
199    pub fn rotate_matrix(degrees: f64) -> Affine2<f64> {
200        let angle = PI * (degrees / 180.0);
201        Affine2::from_matrix_unchecked(Matrix3::new(
202            angle.cos(),
203            -angle.sin(),
204            0.0,
205            angle.sin(),
206            angle.cos(),
207            0.0,
208            0.0,
209            0.0,
210            1.0,
211        ))
212    }
213
214    /// I can haz a new default drawing context?
215    pub fn new() -> Context {
216        Context {
217            operations: vec![],
218            accuracy: 0.1, // 0.1mm should be close enough for anybody
219            transformation: None,
220            font: Some(Context::default_font()),
221            stroke_color: Some("black".to_string()),
222            outline_stroke: None,
223            fill_color: Some("black".to_string()),
224            line_join: "round".to_string(),
225            line_cap: "round".to_string(),
226            pen_width: 0.5,
227            mask: None,
228            clip_previous: false,
229            hatch_pattern: Arc::new(Box::new(LineHatch {})), //Hatches::line(),
230            hatch_angle: 0.0,
231            hatch_scale: None,
232            stroke_filter: None,
233            hatch_filter: None,
234            stack: vec![],
235        }
236    }
237
238    /// Bounds returns a Rect defining the bounds of all operations drawn on the context.
239    /// Note: Since this has to iterate over ALL geometry in the drawing, it's kind of expensive.
240    /// I'll probably cache this per operation at some point, but for now it's pricy.
241    pub fn bounds(&self) -> Result<Rect<f64>, Box<dyn Error>> {
242        let mut pmin = Point::new(f64::MAX, f64::MAX);
243        let mut pmax = Point::new(f64::MIN, f64::MIN);
244        for operation in &self.operations {
245            let tmp_bounds = operation.content.bounding_rect();
246            if let Some(bounds) = tmp_bounds {
247                pmin = Point::new(pmin.y().min(bounds.min().y), pmin.y().min(bounds.min().y));
248                pmax = Point::new(pmax.x().max(bounds.max().x), pmax.y().max(bounds.max().y));
249            }
250        }
251        if pmin == Point::new(f64::MAX, f64::MAX) || pmax == Point::new(f64::MIN, f64::MIN) {
252            Err(Box::new(geo_types::Error::MismatchedGeometry {
253                expected: "Context with content",
254                found: "Empty context",
255            }))
256        } else {
257            Ok(Rect::new(pmin.0, pmax.0))
258        }
259    }
260
261    /// Masks any further operations with a clipping polygon. Only items
262    /// inside the clipping poly will be used.
263    pub fn mask_poly(
264        &mut self,
265        exterior: Vec<(f64, f64)>,
266        interiors: Vec<Vec<(f64, f64)>>,
267    ) -> &mut Self {
268        let mask = Geometry::Polygon(Polygon::<f64>::new(
269            LineString::new(exterior.iter().map(|(x, y)| coord! {x:*x, y:*y}).collect()),
270            interiors
271                .iter()
272                .map(|interior| {
273                    LineString::<f64>::new(
274                        interior
275                            .iter()
276                            .map(|(x, y)| coord! {x:*x, y:*y})
277                            .collect::<Vec<Coordinate<f64>>>(),
278                    )
279                })
280                .collect(),
281        ));
282        self.set_mask(&Some(mask));
283        self
284    }
285
286    pub fn mask_box(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) -> &mut Self {
287        self.mask_poly(
288            vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)],
289            vec![],
290        )
291    }
292
293    /// Sets the mask to Geometry, or None.
294    pub fn set_mask(&mut self, mask: &Option<Geometry<f64>>) -> &mut Self {
295        self.mask = match mask {
296            Some(maskgeo) => Some(match &self.transformation {
297                Some(affine) => maskgeo.map_coords(|xy| Operation::xform_coord(&xy, affine)),
298                None => maskgeo.clone(),
299            }),
300            None => mask.clone(),
301        };
302        self
303    }
304
305    /// Pushes the current context onto the stack.
306    pub fn push(&mut self) -> &mut Self {
307        self.stack.push(Self {
308            operations: vec![],
309            accuracy: self.accuracy.clone(),
310            font: match &self.font {
311                Some(font) => Some(font.clone()),
312                None => None,
313            },
314            transformation: match self.transformation.clone() {
315                Some(transformation) => Some(transformation),
316                None => None,
317            },
318            stroke_color: self.stroke_color.clone(),
319            outline_stroke: self.outline_stroke.clone(),
320            fill_color: self.fill_color.clone(),
321            line_join: self.line_join.clone(),
322            line_cap: self.line_cap.clone(),
323            pen_width: self.pen_width.clone(),
324            mask: self.mask.clone(),
325            clip_previous: self.clip_previous.clone(),
326            hatch_pattern: self.hatch_pattern.clone(),
327            hatch_angle: self.hatch_angle,
328            hatch_scale: self.hatch_scale,
329            stroke_filter: self.stroke_filter.clone(),
330            hatch_filter: self.hatch_filter.clone(),
331            stack: vec![],
332        });
333        self
334    }
335
336    /// Pops the previous context off the stack
337    pub fn pop(&mut self) -> Result<&mut Self, ContextError> {
338        let other = self.stack.pop().ok_or(ContextError::PoppedEmptyStack)?;
339        self.transformation = match other.transformation.clone() {
340            Some(transformation) => Some(transformation),
341            None => None,
342        };
343        self.accuracy = other.accuracy.clone();
344        self.stroke_color = other.stroke_color.clone();
345        self.outline_stroke = other.outline_stroke.clone();
346        self.fill_color = other.fill_color.clone();
347        self.line_join = other.line_join.clone();
348        self.line_cap = other.line_cap.clone();
349        self.pen_width = other.pen_width.clone();
350        self.hatch_angle = other.hatch_angle;
351        self.clip_previous = other.clip_previous.clone();
352        self.stroke_filter = other.stroke_filter.clone();
353        self.hatch_filter = other.hatch_filter.clone();
354        Ok(self)
355    }
356
357    /// Set the transformation matrix for subsequent ops. Take a look at the transformation
358    /// helpers ([`crate::context::Context::scale_matrix`],
359    /// [`crate::context::Context::translate_matrix`], and
360    /// [`crate::context::Context::rotate_matrix`], which are great
361    /// if you don't want to generate your own unsafe Affine2 transformations. Also, usefully,
362    /// these transformations can be COMPOSED via multiplication. Note that the order of the
363    /// compositions is right-to-left, so the last in the chain of multiplications is the
364    /// first one to be performed. See the example in context_basic.rs for more info.
365    pub fn transform(&mut self, transformation: Option<&Affine2<f64>>) -> &mut Self {
366        self.transformation = match transformation {
367            Some(tx) => Some(tx.clone()),
368            None => None,
369        };
370        self
371    }
372
373    /// Similar to transform, but multiplies the CURRENT transformation matrix by the
374    /// new one. If the current matrix is None, then multiplies by the UNIT matrix.
375    /// This is really useful for stepping through relative positions, or rotations.
376    /// Couples well with push/pop to make an addition relative to current matrix,
377    /// then resetting to origin.
378    pub fn mul_transform(&mut self, transformation: &Affine2<f64>) -> &mut Self {
379        let base = match self.transformation.clone() {
380            Some(tx) => tx,
381            None => Context::unit_matrix(),
382        };
383
384        self.transformation = Some(transformation * base);
385        self
386    }
387
388    /// Adds any arbitrary Geometry type (geo_types geometry)
389    fn add_operation(&mut self, geometry: Geometry<f64>) {
390        let geometry = geometry.flatten();
391        let op = Operation {
392            content: geometry,
393            rendered: (MultiLineString::new(vec![]), MultiLineString::new(vec![])),
394            accuracy: self.accuracy.clone(),
395            transformation: self.transformation.clone(),
396            stroke_color: self.stroke_color.clone(),
397            outline_stroke: self.outline_stroke.clone(),
398            fill_color: self.fill_color.clone(),
399            line_join: self.line_join.clone(),
400            line_cap: self.line_cap.clone(),
401            pen_width: self.pen_width.clone(),
402            mask: self.mask.clone(),
403            clip_previous: self.clip_previous.clone(),
404            hatch_pattern: self.hatch_pattern.clone(),
405            hatch_angle: self.hatch_angle,
406            hatch_scale: self.hatch_scale,
407            stroke_filter: self.stroke_filter.clone(),
408            hatch_filter: self.hatch_filter.clone(),
409        };
410        let op = op.render();
411        self.operations.push(op);
412    }
413
414    /// Adds a geometry to the operations list. Has some checking to make it safe
415    /// for general users.
416    pub fn geometry(&mut self, geometry: &Geometry<f64>) -> &mut Self {
417        match &geometry {
418            Geometry::LineString(_ls) => {
419                self.add_operation(geometry.clone());
420            }
421            Geometry::MultiLineString(_mls) => {
422                self.add_operation(geometry.clone());
423            }
424            Geometry::Polygon(_poly) => {
425                self.add_operation(geometry.clone());
426            }
427            Geometry::MultiPolygon(_mp) => {
428                self.add_operation(geometry.clone());
429            }
430            Geometry::Rect(rect) => self.add_operation(Geometry::Polygon(Polygon::new(
431                LineString::new(vec![
432                    coord! {x: rect.min().x, y: rect.min().y},
433                    coord! {x: rect.max().x, y: rect.min().y},
434                    coord! {x: rect.max().x, y: rect.max().y},
435                    coord! {x: rect.min().x, y: rect.max().y},
436                    coord! {x: rect.min().x, y: rect.min().y},
437                ]),
438                vec![],
439            ))),
440            Geometry::Triangle(tri) => self.add_operation(Geometry::Polygon(Polygon::new(
441                LineString::new(vec![
442                    coord! {x: tri.0.x, y:tri.0.y},
443                    coord! {x: tri.1.x, y:tri.1.y},
444                    coord! {x: tri.2.x, y:tri.2.y},
445                    coord! {x: tri.0.x, y:tri.0.y},
446                ]),
447                vec![],
448            ))),
449            Geometry::GeometryCollection(collection) => {
450                // println!("Adding geom collection: {:?}", &collection);
451                for item in collection {
452                    // println!("Adding geo collection item: {:?}", &item);
453                    self.geometry(item);
454                }
455            }
456            Geometry::Line(line) => self.add_operation(Geometry::LineString(LineString(vec![
457                coord! {x: line.start.x, y: line.start.y},
458                coord! {x: line.end.x, y: line.end.y},
459            ]))),
460            Geometry::Point(pt) => {
461                self.circle(pt.0.x, pt.0.y, self.pen_width / 2.0);
462            }
463            Geometry::MultiPoint(points) => {
464                for pt in points {
465                    self.circle(pt.0.x, pt.0.y, self.pen_width / 2.0);
466                }
467            }
468        };
469        self
470    }
471
472    /// Draws a simple line from x0,y0 to x1,y1
473    pub fn line(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) -> &mut Self {
474        self.add_operation(Geometry::LineString(LineString::<f64>::new(vec![
475            coord! {x: x0, y: y0},
476            coord! {x: x1, y: y1},
477        ])));
478        self
479    }
480
481    /// Draws a line of text
482    pub fn typography(
483        &mut self,
484        text: &String,
485        x0: f64,
486        y0: f64,
487        typography: &Typography,
488    ) -> &mut Self {
489        let typ = typography.clone();
490        let geo = typ
491            .render(text, self.accuracy)
492            .unwrap_or(Geometry::GeometryCollection(GeometryCollection(vec![])))
493            .map_coords(|co| coord!(x: x0 + co.x.clone(), y: y0 - (co.y.clone())));
494        // println!("The geo is: {:?}", &geo);
495        self.geometry(&geo);
496        self
497    }
498
499    /// Glyph
500    /// Draws a single glyph on the Context, at 0,0
501    pub fn glyph(&mut self, glyph: char, close: bool) -> &mut Self {
502        if let Some(font) = &self.font {
503            let mut proxy = GlyphProxy::new(close);
504            let glyph_id = font.glyph_for_char(glyph).unwrap_or(32);
505            font.outline(glyph_id, HintingOptions::None, &mut proxy)
506                .unwrap();
507            // println!("Proxy path is: {:?}", &proxy.path());
508            self.path(&proxy.path());
509        }
510        // println!("Last op: {:?}", self.operations.last().unwrap().content);
511        self
512    }
513
514    /// Way more useful path interface. Uses Kurbo's BezierPath module.
515    /// After creation, uses GEOS polygonize_full to generate polygons
516    /// and line segments from the drawing, ensuring that we can have
517    /// filled geometry as an output.
518    pub fn path(&mut self, bezier: &BezPath) -> &mut Self {
519        // Eventually, this should generate polygons. Holes are tricky tho.
520        let mut segments: MultiLineString<f64> = MultiLineString::new(vec![]);
521        let mut lastpoint = kurbo::Point::new(0.0, 0.0);
522        let add_segment = |el: PathEl| match el {
523            PathEl::MoveTo(pos) => {
524                segments
525                    .0
526                    .push(LineString::new(vec![coord! {x: pos.x, y: pos.y}]));
527                lastpoint = pos.clone();
528            }
529            PathEl::LineTo(pos) => {
530                if let Some(line) = segments.0.last_mut() {
531                    line.0.push(coord! {x: pos.x, y: pos.y});
532                }
533            }
534            PathEl::ClosePath => {
535                if let Some(line) = segments.0.last_mut() {
536                    line.0.push(coord! {x: lastpoint.x, y: lastpoint.y});
537                }
538            }
539            _ => panic!("Unexpected/Impossible segment type interpolating a bezier path!"),
540        };
541
542        bezier.flatten(self.accuracy, add_segment);
543        let tmp_gtgeo = Geometry::MultiLineString(segments);
544        let tmp_geos = tmp_gtgeo.to_geos();
545        let out_gtgeo: Geometry<f64> = match tmp_geos {
546            Ok(geos_geom) => {
547                // TODO: Copy the improved implementation from the typography module, maybe
548                // generalize it as well.
549                if let Ok((poly_geo, _cuts_geo, _dangles_geo, invalid_geo)) =
550                    geos_geom.polygonize_full()
551                {
552                    // if let Some(dangles) = &dangles_geo {println!("Dangles: {:?}", dangles.to_wkt().unwrap());}
553                    // if let Some(cuts) = &cuts_geo {println!("Cuts: {:?}", cuts.to_wkt().unwrap());}
554                    // if let Some(invalid) = &invalid_geo {
555                    // println!("Invalid: {:?}", invalid.to_wkt().unwrap());
556                    // }
557                    let out_gtgeo = match invalid_geo {
558                        None => Geometry::try_from(&poly_geo).unwrap_or(tmp_gtgeo.clone()),
559                        Some(invalid) => {
560                            // println!("Invalid: {:?}", invalid.to_wkt().unwrap());
561                            Geometry::GeometryCollection(GeometryCollection::new_from(vec![
562                                Geometry::try_from(&poly_geo).unwrap_or(tmp_gtgeo.clone()),
563                                Geometry::try_from(&invalid).unwrap_or(
564                                    Geometry::GeometryCollection(GeometryCollection::new_from(
565                                        vec![],
566                                    )),
567                                ),
568                            ]))
569                        }
570                    };
571                    // println!("Polygonzed: {:?}", &out_gtgeo);
572                    out_gtgeo
573                } else {
574                    // println!("Couldn't convert to geos polys");
575                    tmp_gtgeo.clone()
576                }
577            }
578            Err(_err) => {
579                // println!("Couldn't convert to geos at all");
580                tmp_gtgeo.clone()
581            }
582        };
583
584        // println!("Out GTGEO is {:?}", &out_gtgeo);
585        self.add_operation(out_gtgeo);
586        self
587    }
588
589    /// Generates a spline from a set of points and renders as a
590    /// multi line string. Doesn't do errors very well, just
591    /// silently fails to draw.
592    /// First and last point in points are NOT drawn, and set the 'tension'
593    /// points which the line pulls from.
594    pub fn spline(
595        &mut self,
596        points: &Vec<(f64, f64)>,
597        num_interpolated_segments: u32,
598        tension: f64,
599    ) -> &mut Self {
600        let spline_opts = SplineOpts::new()
601            .num_of_segments(num_interpolated_segments)
602            .tension(tension);
603        let points = match Points::try_from(points) {
604            Ok(pts) => pts,
605            Err(_e) => return self,
606        };
607        let spline = cubic_spline::calc_spline(&points, &spline_opts);
608        match spline {
609            Ok(spts) => {
610                self.add_operation(Geometry::LineString(LineString::<f64>::new(
611                    spts.get_ref()
612                        .iter()
613                        .map(|pt| coord! {x: pt.x, y: pt.y})
614                        .collect(),
615                )));
616                self
617            }
618            Err(_e) => self,
619        }
620    }
621
622    /// centerpoint arc
623    /// Draw an arc around x0,y0 with the given radius, from deg0 to deg1. Arcs will always be
624    /// coords oriented clockwise from "north" on an SVG. ie: 45 to 135 will be NE to SE.
625    pub fn arc_center(&mut self, x0: f64, y0: f64, radius: f64, deg0: f64, deg1: f64) -> &mut Self {
626        let ls = crate::geo_types::shapes::arc_center(x0, y0, radius, deg0, deg1);
627        let ls = ls.map_coords(|co| coord!(x: co.x.clone(), y: -co.y.clone()));
628
629        //let ls = ls.map_coords(|(x, y)| (x.clone(), -y.clone()));
630        self.add_operation(Geometry::LineString(ls));
631
632        self
633    }
634
635    /// What it says on the box. Draws a simple rectangle on the context.
636    pub fn rect(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) -> &mut Self {
637        self.add_operation(Geometry::Polygon(Polygon::<f64>::new(
638            LineString::new(vec![
639                coord! {x: x0, y: y0},
640                coord! {x: x1, y: y0},
641                coord! {x: x1, y: y1},
642                coord! {x: x0, y: y1},
643                coord! {x: x0, y: y0},
644            ]),
645            vec![],
646        )));
647        self
648    }
649
650    /// Draws a polygon
651    pub fn poly(
652        &mut self,
653        exterior: Vec<(f64, f64)>,
654        interiors: Vec<Vec<(f64, f64)>>,
655    ) -> &mut Self {
656        self.add_operation(Geometry::Polygon(Polygon::<f64>::new(
657            LineString::new(exterior.iter().map(|(x, y)| coord! {x:*x, y:*y}).collect()),
658            interiors
659                .iter()
660                .map(|interior| {
661                    LineString::<f64>::new(
662                        interior
663                            .iter()
664                            .map(|(x, y)| coord! {x:*x, y:*y})
665                            .collect::<Vec<Coordinate<f64>>>(),
666                    )
667                })
668                .collect(),
669        )));
670        self
671    }
672
673    /// Draws a circle. Actually just buffers a point, and returns a polygon
674    /// which it draws on the context.
675    pub fn circle(&mut self, x0: f64, y0: f64, radius: f64) -> &mut Self {
676        self.add_operation(shapes::circle(x0, y0, radius));
677        self
678    }
679
680    /// Circumscribed regular polygon. The vertices of the polygon will be situated on a
681    /// circle defined by the given radius. Polygon will be centered at x,y.
682    pub fn regular_poly(
683        &mut self,
684        sides: usize,
685        x: f64,
686        y: f64,
687        radius: f64,
688        rotation: f64,
689    ) -> &mut Self {
690        let geo = shapes::regular_poly(sides, x, y, radius, rotation);
691        self.add_operation(geo);
692        self
693    }
694
695    /// Regular star polygon. This is effectively a _star_ shape with the number of points indicated,
696    /// and with inner and outer radiuses which correspond to the valleys and tips of the star
697    /// respectively. Note: this is not a star polygon in the strict mathematical sense. This is
698    /// just a polygon that is in the shape of a star. I may or may not get to regular star polygons
699    /// (in the canonical mathematical sense) at some point.
700    pub fn star_poly(
701        &mut self,
702        sides: usize,
703        x: f64,
704        y: f64,
705        inner_radius: f64,
706        outer_radius: f64,
707        rotation: f64,
708    ) -> &mut Self {
709        if sides < 3 {
710            return self;
711        };
712        let mut exterior = LineString::<f64>::new(vec![]);
713        for i in 0..sides {
714            let angle_a = rotation - PI_F64 / 2.0
715                + (f64::from(i as i32) / f64::from(sides as i32)) * (2.0 * PI_F64);
716            let angle_b = rotation - PI_F64 / 2.0
717                + ((f64::from(i as i32) + 0.5) / f64::from(sides as i32)) * (2.0 * PI_F64);
718            exterior.0.push(coord! {
719            x: x+angle_a.cos() * outer_radius,
720            y: y+angle_a.sin() * outer_radius});
721            exterior.0.push(coord! {
722            x: x+angle_b.cos() * inner_radius,
723            y: y+angle_b.sin() * inner_radius});
724        }
725        // and close it...
726        exterior.0.push(coord! {
727        x: x+(rotation - PI_F64/2.0).cos() * outer_radius,
728        y: y+(rotation - PI_F64/2.0).sin() * outer_radius});
729        self.add_operation(Geometry::Polygon(Polygon::new(exterior, vec![])));
730        self
731    }
732
733    /// Sets the clipping state. Any subsequent objects will clip their predecessors.
734    /// Note that this is an EXPENSIVE operation, so you might want to leave it off
735    /// if you're sure you won't have intersections.
736    pub fn clip(&mut self, clip: bool) -> &mut Self {
737        self.clip_previous = clip;
738        self
739    }
740
741    /// Sets the stroke color
742    pub fn stroke(&mut self, color: &str) -> &mut Self {
743        self.stroke_color = Some(color.to_string());
744        self
745    }
746
747    pub fn no_stroke(&mut self) -> &mut Self {
748        self.stroke_color = None;
749        self
750    }
751
752    /// Sets the fill color
753    pub fn fill(&mut self, color: &str) -> &mut Self {
754        self.fill_color = Some(color.to_string());
755        self
756    }
757
758    pub fn no_fill(&mut self) -> &mut Self {
759        self.fill_color = None;
760        self
761    }
762
763    /// Sets the hatch state, either None for no hatching or
764    /// Some(angle) to set a hatching angle. Will use the current
765    /// pen width as the spacing between hatch lines.
766    pub fn hatch(&mut self, angle: f64) -> &mut Self {
767        self.hatch_angle = angle;
768        self
769    }
770
771    /// Sets the pen width
772    pub fn pen(&mut self, width: f64) -> &mut Self {
773        self.pen_width = width;
774        self
775    }
776
777    /// Set the hatch pattern
778    //pub fn pattern(&mut self, pattern: Hatches) -> &mut Self {
779    pub fn pattern(&mut self, pattern: Arc<Box<dyn HatchPattern>>) -> &mut Self {
780        self.hatch_pattern = pattern;
781        self
782    }
783
784    /// Set the hatch pattern scale (spacing), or use the pen width if None.
785    pub fn hatch_scale(&mut self, scale: Option<f64>) -> &mut Self {
786        self.hatch_scale = scale;
787        self
788    }
789
790    /// Neat option. Instead of drawing a complex poly, why not just
791    /// set outline, and have the lines/polys/whatever you subsequently
792    /// draw get buffered and hatched to imitate a really thick pen?
793    /// I knew you'd like this one :D
794    pub fn outline(&mut self, stroke: Option<f64>) -> &mut Self {
795        self.outline_stroke = stroke;
796        self
797    }
798
799    /// Flatten will take a context and "flatten" together all polygons
800    /// of a given color and "depth". What that means is that we watch for
801    /// changes to fill/color/etc, and set those as boundaries. Then every
802    /// geometry within a set of boundaries is flattened as "unions" into
803    /// a single geometry. This is nice because overlapping polygons get
804    /// turned into a single unified polygon, and their fills are no longer
805    /// disjoint (and they don't have unexpected overlapping boundary lines).
806    /// See the 10_hello example for more details.
807    /// Unlike the other methods, this one generates an entirely new context
808    /// including a NEW HISTORY, so you can't use push/pop to go back, and
809    /// the individual operations are (obviously) lost.
810    pub fn flatten(&self) -> Self {
811        let mut new_ctx = Context::new();
812        new_ctx.add_operation(Geometry::MultiLineString(MultiLineString::new(vec![])));
813        let mut last_operation = new_ctx.operations[0].clone();
814        let tmp_gt_op = Geometry::GeometryCollection(GeometryCollection::new_from(vec![]));
815        let mut current_geometry = try_to_geos_geometry(&tmp_gt_op).unwrap_or(
816            geos::Geometry::create_empty_collection(GeometryTypes::GeometryCollection)
817                .expect("Failed to generate default geos::geometry"),
818        );
819        for operation in self.operations.iter() {
820            if operation.consistent(&last_operation) {
821                // Union current_geometry with the operation
822                let cgeo = try_to_geos_geometry(&operation.content).unwrap_or(
823                    geos::Geometry::create_empty_collection(GeometryTypes::GeometryCollection)
824                        .expect("Failed to generate default geos::geometry"),
825                );
826                current_geometry =
827                    geos::Geometry::create_geometry_collection(vec![current_geometry, cgeo])
828                        .expect("Cannot append geometry into collection.");
829            } else {
830                // Duplicate the state into the context, and create a new current_geometry bundle
831                new_ctx.stroke_color = operation.stroke_color.clone();
832                new_ctx.outline_stroke = operation.outline_stroke.clone();
833                new_ctx.fill_color = operation.fill_color.clone();
834                new_ctx.line_join = operation.line_join.clone();
835                new_ctx.line_cap = operation.line_cap.clone();
836                new_ctx.pen_width = operation.pen_width.clone();
837                new_ctx.clip_previous = operation.clip_previous.clone();
838                new_ctx.hatch_pattern = operation.hatch_pattern.clone();
839                new_ctx.hatch_angle = operation.hatch_angle;
840
841                current_geometry = current_geometry.unary_union().unwrap_or(current_geometry);
842
843                new_ctx.geometry(&geo_types::Geometry::try_from(current_geometry).unwrap_or(
844                    Geometry::GeometryCollection(GeometryCollection::new_from(vec![])),
845                ));
846                last_operation = operation.clone();
847                current_geometry = try_to_geos_geometry(&operation.content).unwrap_or(
848                    geos::Geometry::create_empty_collection(GeometryTypes::GeometryCollection)
849                        .expect("If we failed to convert or fallback, something is very wrong."),
850                );
851            }
852        }
853        // get the last one.
854        current_geometry = current_geometry.unary_union().unwrap_or(current_geometry);
855        new_ctx.geometry(&geo_types::Geometry::try_from(current_geometry).unwrap_or(
856            Geometry::GeometryCollection(GeometryCollection::new_from(vec![])),
857        ));
858        new_ctx
859    }
860
861    pub fn to_geo(&self) -> Result<Geometry<f64>, Box<dyn Error>> {
862        let mut all: Vec<Geometry<f64>> = vec![];
863        for operation in &self.operations {
864            all.push(operation.content.clone().into());
865        }
866        Ok(Geometry::GeometryCollection(
867            GeometryCollection::<f64>::new_from(all),
868        ))
869    }
870
871    /// Generate layers of perimeters and fills
872    pub fn to_layers(&self) -> Vec<OPLayer> {
873        let mut oplayers: Vec<OPLayer> = vec![];
874        for op in &self.operations {
875            let (mut stroke, mut fill) = op.rendered.clone();
876            // Cull all the empty crap
877            stroke.0.retain(|item| item.0.len() > 1);
878            fill.0.retain(|item| item.0.len() > 1);
879
880            oplayers.push(OPLayer {
881                stroke_lines: stroke,
882                fill_lines: fill,
883                stroke: op.stroke_color.clone(),
884                fill: op.fill_color.clone(),
885                stroke_width: op.pen_width,
886                stroke_linejoin: op.line_join.clone(),
887                stroke_linecap: op.line_cap.clone(),
888            });
889        }
890        assert_eq!(&self.operations.len(), &oplayers.len());
891
892        // Iterate the layers, and clip their predecessors where appropriate.
893        // NOTE: CLIPPING IS S_L_O_W AF.
894        if self.operations.len() > 1 {
895            for i in 0..(self.operations.len() - 1) {
896                for j in (i + 1)..self.operations.len() {
897                    if self.operations[j].clip_previous {
898                        oplayers[i].stroke_lines =
899                            Geometry::MultiLineString(oplayers[i].stroke_lines.clone())
900                                .clipwith(&self.operations[j].content)
901                                .unwrap_or(MultiLineString::<f64>::new(vec![]));
902                        oplayers[i].fill_lines =
903                            Geometry::MultiLineString(oplayers[i].fill_lines.clone())
904                                .clipwith(&self.operations[j].content)
905                                .unwrap_or(oplayers[i].fill_lines.clone());
906                    }
907                }
908            }
909        }
910        oplayers
911    }
912
913    /// Take this giant complex thing and generate and SVG Document, or an error. Whatever.
914    pub fn to_svg(&self, arrangement: &Arrangement<f64>) -> Result<Document, ContextError> {
915        let oplayers = self.to_layers();
916
917        let mut svg =
918            arrangement
919                .create_svg_document()
920                .or(Err(ContextError::SvgGenerationError(
921                    "Failed to create raw svg doc".into(),
922                )
923                .into()))?;
924
925        let mut id = 0;
926        for oplayer in oplayers {
927            if !oplayer.stroke_lines.0.is_empty() {
928                let optimizer = crate::optimizer::Optimizer::new(
929                    oplayer.stroke_width * 2.,
930                    crate::optimizer::OptimizationStrategy::Greedy,
931                );
932
933                let slines_opt = optimizer.optimize(&optimizer.merge(&oplayer.stroke_lines));
934                let slines = slines_opt.to_path(&arrangement);
935                svg = svg.add(
936                    slines
937                        .set("id", format!("outline-{}", id))
938                        .set("fill", "none")
939                        .set(
940                            "stroke",
941                            match &oplayer.stroke {
942                                Some(color) => color.clone(),
943                                None => "none".to_string(),
944                            },
945                        )
946                        .set("stroke-width", oplayer.stroke_width)
947                        .set("stroke-linejoin", oplayer.stroke_linejoin.clone())
948                        .set("stroke-linecap", oplayer.stroke_linecap.clone()),
949                );
950            }
951            if !oplayer.fill_lines.0.is_empty() {
952                let optimizer = crate::optimizer::Optimizer::new(
953                    oplayer.stroke_width,
954                    crate::optimizer::OptimizationStrategy::Greedy,
955                );
956                let fill_opt = optimizer.optimize(&oplayer.fill_lines);
957                let flines = fill_opt.to_path(&arrangement);
958                svg = svg.add(
959                    flines
960                        .set("id", format!("fill-{}", id))
961                        .set("fill", "none")
962                        .set(
963                            "stroke",
964                            match &oplayer.stroke {
965                                Some(color) => color.clone(),
966                                None => "none".to_string(),
967                            },
968                        )
969                        .set("stroke-width", oplayer.stroke_width)
970                        .set("stroke-linejoin", oplayer.stroke_linejoin.clone())
971                        .set("stroke-linecap", oplayer.stroke_linecap.clone()),
972                );
973                id = id + 1;
974            }
975        }
976        Ok(svg)
977    }
978}
979
980#[cfg(test)]
981mod test {
982    use crate::prelude::NoHatch;
983
984    use super::*;
985    use geo_types::{Rect, Triangle};
986
987    #[test]
988    fn test_context_new() {
989        let context = Context::new();
990        assert_eq!(context.transformation, None);
991    }
992
993    #[test]
994    fn test_minimal_rect() {
995        let mut context = Context::new();
996        context
997            .stroke("red")
998            .pen(0.8)
999            .fill("blue")
1000            .hatch(45.0)
1001            .rect(10.0, 10.0, 50.0, 50.0);
1002        let arrangement = Arrangement::FitCenterMargin(
1003            10.0,
1004            Rect::new(coord! {x: 0.0, y: 0.0}, coord! {x:100.0, y:100.0}),
1005            false,
1006        );
1007        context.to_svg(&arrangement).unwrap();
1008    }
1009
1010    #[test]
1011    fn test_arc_c() {
1012        let mut context = Context::new();
1013        context
1014            .stroke("red")
1015            .pen(0.8)
1016            .fill("blue")
1017            .hatch(45.0)
1018            .arc_center(0.0, 0.0, 10.0, 45.0, 180.0);
1019        let arrangement = Arrangement::unit(&Rect::new(
1020            coord! {x: 0.0, y: 0.0},
1021            coord! {x:100.0, y:100.0},
1022        ));
1023        let svg1 = context.to_svg(&arrangement).unwrap();
1024        let mut context = Context::new();
1025        context
1026            .stroke("red")
1027            .pen(0.8)
1028            .fill("blue")
1029            .hatch(45.0)
1030            .arc_center(0.0, 0.0, 10.0, 180.0, 45.0);
1031        let arrangement = Arrangement::unit(&Rect::new(
1032            coord! {x: 0.0, y: 0.0},
1033            coord! {x:100.0, y:100.0},
1034        ));
1035        let svg2 = context.to_svg(&arrangement).unwrap();
1036        // Make sure that order of angles is irrelevant
1037        assert_eq!(svg2.to_string(), svg1.to_string());
1038    }
1039
1040    #[test]
1041    fn test_minimal_poly() {
1042        let mut context = Context::new();
1043        context.stroke("red");
1044        context.pen(0.8);
1045        context.fill("blue");
1046        context.hatch(45.0);
1047        context.poly(vec![(10.0, 10.0), (50.0, 10.0), (25.0, 25.0)], vec![]);
1048        let arrangement = Arrangement::FitCenterMargin(
1049            10.0,
1050            Rect::new(coord! {x: 0.0, y: 0.0}, coord! {x:100.0, y:100.0}),
1051            false,
1052        );
1053        context.to_svg(&arrangement).unwrap();
1054    }
1055
1056    #[test]
1057    fn test_regular_poly() {
1058        let mut context = Context::new();
1059        context.stroke("red");
1060        context.pen(0.8);
1061        context.fill("blue");
1062        context.hatch(45.0);
1063        context.regular_poly(4, 50.0, 50.0, 100.0, 0.0);
1064        let arrangement = Arrangement::FitCenterMargin(
1065            10.0,
1066            Rect::new(coord! {x: 0.0, y: 0.0}, coord! {x:100.0, y:100.0}),
1067            false,
1068        );
1069        context.to_svg(&arrangement).unwrap();
1070    }
1071
1072    #[test]
1073    fn test_5_pointed_star() {
1074        let mut context = Context::new();
1075        context
1076            .stroke("red")
1077            .pen(0.8)
1078            .fill("blue")
1079            .hatch(45.0)
1080            .star_poly(5, 50.0, 50.0, 20.0, 40.0, 0.0);
1081    }
1082
1083    #[test]
1084    fn test_flatten_simple() {
1085        let mut context = Context::new();
1086        context.stroke("red");
1087        context.pen(0.5);
1088        context.fill("blue");
1089        context.pattern(Arc::new(Box::new(NoHatch {})));
1090        context.hatch(45.0);
1091        context.rect(10.0, 10.0, 30.0, 30.0);
1092        context.rect(20.0, 20.0, 40.0, 40.0);
1093        context = context.flatten();
1094        let arrangement = Arrangement::unit(&Rect::new(
1095            coord! {x: 0.0, y: 0.0},
1096            coord! {x:100.0, y:100.0},
1097        ));
1098        let svg = context.to_svg(&arrangement).unwrap();
1099        assert_eq!(svg.to_string(),
1100                   concat!(
1101                   "<svg height=\"100mm\" viewBox=\"0 0 100 100\" width=\"100mm\" xmlns=\"http://www.w3.org/2000/svg\">\n",
1102                   "<path d=\"M30,10 L10,10 L10,30 L20,30 L20,40 L40,40 L40,20 L30,20 L30,10\" fill=\"none\" id=\"outline-0\" ",
1103                   "stroke=\"red\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n</svg>"
1104                   ));
1105    }
1106
1107    #[test]
1108    fn test_flatten_complex() {
1109        let mut context = Context::new();
1110        context.stroke("red");
1111        context.pen(0.5);
1112        context.fill("blue");
1113        // context.hatch(45.0);
1114        // context.pattern(LineHatch::gen());
1115        context.pattern(Arc::new(Box::new(NoHatch {})));
1116        context.rect(10.0, 10.0, 30.0, 30.0);
1117        context.rect(20.0, 20.0, 40.0, 40.0);
1118        context.rect(32.0, 32.0, 48.0, 48.0);
1119        context
1120            .stroke("black")
1121            .clip(true)
1122            .rect(22.0, 22.0, 38.0, 38.0);
1123
1124        context = context.flatten();
1125        let arrangement = Arrangement::unit(&Rect::new(
1126            coord! {x: 0.0, y: 0.0},
1127            coord! {x:100.0, y:100.0},
1128        ));
1129        let svg = context.to_svg(&arrangement).unwrap();
1130        println!("svg: {}", svg.to_string());
1131        assert_eq!(
1132            svg.to_string(),
1133            concat!(
1134                "<svg height=\"100mm\" viewBox=\"0 0 100 100\" width=\"100mm\" xmlns=\"http://www.w3.org/2000/svg\">\n",
1135                "<path d=\"\" fill=\"none\" id=\"outline-0\" stroke=\"black\" ",
1136                "stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n<path d=\"\" fill=\"none\" id=\"fill-0\" stroke=\"black\" stroke-linecap=\"round\" ",
1137                "stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n<path d=\"M30,10 L10,10 L10,30 L20,30 L20,40 L32,40 L32,48 L48,48 L48,32 L40,32 L40,20 L30,20 L30,10\" ",
1138                "fill=\"none\" id=\"outline-1\" stroke=\"black\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n<path d=\"\" fill=\"none\" ",
1139                "id=\"fill-1\" stroke=\"black\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n<path d=\"M22,22 L38,22 L38,38 L22,38 L22,22\" ",
1140                "fill=\"none\" id=\"outline-2\" stroke=\"black\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n</svg>",
1141            )
1142        );
1143        // assert_eq!(svg.to_string(),
1144        //            concat!(
1145        //            "<svg height=\"100mm\" viewBox=\"0 0 100 100\" width=\"100mm\" xmlns=\"http://www.w3.org/2000/svg\">\n",
1146        //            "<path d=\"\" fill=\"none\" id=\"outline-0\" stroke=\"black\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n",
1147        //            "<path d=\"\" fill=\"none\" id=\"fill-0\" stroke=\"black\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n",
1148        //            "<path d=\"M30,10 L10,10 L10,30 L20,30 L20,40 L32,40 L32,48 L48,48 L48,32 L40,32 L40,20 L30,20 L30,10\" fill=\"none\" id=\"outline-1\" ",
1149        //            "stroke=\"black\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n",
1150        //            "<path d=\"\" fill=\"none\" id=\"fill-1\" stroke=\"blue\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n",
1151        //            "<path d=\"M22,22 L38,22 L38,38 L22,38 L22,22\" fill=\"none\" id=\"outline-2\" stroke=\"black\" stroke-linecap=\"round\" ",
1152        //            "stroke-linejoin=\"round\" stroke-width=\"0.5\"/>\n</svg>"
1153        //            ));
1154    }
1155
1156    #[test]
1157    fn test_to_geo() {
1158        let mut context = Context::new();
1159        context
1160            .stroke("red")
1161            .pen(0.8)
1162            .fill("blue")
1163            .hatch(45.0)
1164            .rect(10.0, 10.0, 90.0, 90.0);
1165
1166        let foo = context.to_geo().unwrap();
1167        // println!("OFOO IS {:?}", &foo);
1168        let mut context = Context::new();
1169        context.stroke("green");
1170        context.pen(0.8);
1171        context.fill("purple");
1172        context.geometry(&foo);
1173        let arrangement = Arrangement::unit(&Rect::new(
1174            coord! {x: 0.0, y: 0.0},
1175            coord! {x:100.0, y:100.0},
1176        ));
1177        /*let svg =*/
1178        context.to_svg(&arrangement).unwrap();
1179        // println!("svg : {}", svg.to_string());
1180    }
1181
1182    #[test]
1183    fn test_various_geometry() {
1184        let mut context = Context::new();
1185        context.stroke("red");
1186        context.pen(0.8);
1187        context.fill("blue");
1188        context.pattern(Arc::new(Box::new(NoHatch {})));
1189        context
1190            .hatch(45.0)
1191            .geometry(&Geometry::Polygon(Polygon::new(
1192                LineString::new(vec![
1193                    coord! {x: 0.0, y:0.0},
1194                    coord! {x: 100.0, y:0.0},
1195                    coord! {x:100.0, y:100.0},
1196                    coord! {x:0.0, y:100.0},
1197                    coord! {x: 0.0, y:0.0},
1198                ]),
1199                vec![],
1200            )))
1201            .geometry(&Geometry::Triangle(Triangle::new(
1202                coord! {x: 0.0, y:0.0},
1203                coord! {x: 100.0, y:0.0},
1204                coord! {x:100.0, y:100.0},
1205            )));
1206        let arrangement = Arrangement::unit(&Rect::new(
1207            coord! {x: 0.0, y: 0.0},
1208            coord! {x:100.0, y:100.0},
1209        ));
1210        let svg = context.to_svg(&arrangement).unwrap();
1211        assert_eq!(svg.to_string(), concat!(
1212        "<svg height=\"100mm\" viewBox=\"0 0 100 100\" width=\"100mm\" xmlns=\"http://www.w3.org/2000/svg\">\n",
1213        "<path d=\"M0,0 L100,0 L100,100 L0,100 L0,0\" fill=\"none\" id=\"outline-0\" stroke=\"red\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.8\"/>\n",
1214        "<path d=\"M0,0 L100,0 L100,100 L0,0\" fill=\"none\" id=\"outline-0\" stroke=\"red\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"0.8\"/>\n",
1215        "</svg>"
1216        ));
1217    }
1218
1219    #[test]
1220    fn test_bezier_path() {
1221        let mut context = Context::new();
1222        // .path()
1223        let mut path = BezPath::new();
1224        path.move_to(BezPoint::new(20.0, 20.0));
1225        path.line_to(BezPoint::new(80.0, 20.0));
1226        path.curve_to(
1227            BezPoint::new(80.0, 40.0),
1228            BezPoint::new(90.0, 50.0),
1229            BezPoint::new(80.0, 60.0),
1230        );
1231        path.line_to(BezPoint::new(50.0, 80.0));
1232        path.quad_to(BezPoint::new(30.0, 50.0), BezPoint::new(25.0, 30.0));
1233        path.close_path();
1234        context
1235            .stroke("red")
1236            .pen(0.8)
1237            .fill("blue")
1238            .pattern(Arc::new(Box::new(LineHatch {})))
1239            .hatch(45.0)
1240            .path(&path);
1241
1242        let arrangement = Arrangement::unit(&Rect::new(
1243            coord! {x: 0.0, y: 0.0},
1244            coord! {x:100.0, y:100.0},
1245        ));
1246        let _svg = context.to_svg(&arrangement).unwrap();
1247        // println!("SVG: {}", svg.to_string());
1248    }
1249
1250    #[test]
1251    fn test_single_glyph() {
1252        let mut context = Context::new();
1253        // .path()
1254        context
1255            .stroke("red")
1256            .pen(0.8)
1257            .fill("blue")
1258            .pattern(Arc::new(Box::new(LineHatch {})))
1259            .hatch(45.0)
1260            .glyph('X', false)
1261            .glyph('O', false);
1262
1263        let arrangement = Arrangement::unit(&Rect::new(
1264            coord! {x: 0.0, y: 0.0},
1265            coord! {x:100.0, y:100.0},
1266        ));
1267        let _svg = context.to_svg(&arrangement).unwrap();
1268    }
1269}