//+-------------------------------------------------------------------+
//|                                                         Path.mqh  |
//|                              Copyright 2026, Sandro Begashvili    |
//|                   https://www.mql5.com/en/users/sandrobegashvil   |
//|                          Telegram: https://t.me/MrMQL5Developer   |
//|                    Cairo5 - 2D vector engine - PART 2 code        |
//|                                                                   |
//|  THE GEOMETRY LAYER.                                              |
//|                                                                   |
//|  A path is a recipe for an outline, written with the same verbs   |
//|  every vector system since PostScript has used:                   |
//|                                                                   |
//|      MoveTo   - lift the pen and put it down somewhere new        |
//|      LineTo   - drag the pen in a straight line                   |
//|      Close    - mark this contour as closed                       |
//|                                                                   |
//|  A path holds NO color, NO thickness and NO pixels. It is pure    |
//|  geometry - a description of an outline, nothing more. Turning it |
//|  into pixels is the rasteriser's job, and that is Part 3.         |
//|                                                                   |
//|  THE COORDINATE CONTRACT                                          |
//|                                                                   |
//|  Coordinates are assumed to be finite. Nothing here validates     |
//|  them on every call - a per-vertex check would tax the one loop   |
//|  the rasteriser runs most often, to catch a mistake that belongs  |
//|  to the caller. When the numbers arrive from an indicator, a      |
//|  division or a user input, call IsFinite() once on the finished   |
//|  path. A NaN that reaches the rasteriser is very hard to trace    |
//|  back to the arithmetic that produced it.                         |
//|                                                                   |
//|  WHAT THIS FILE DELIBERATELY DOES NOT CONTAIN                     |
//|                                                                   |
//|    * curves     - CurveTo and ArcTo arrive in Parts 6 and 7. When |
//|                   they do, they will FLATTEN their curve into     |
//|                   short straight segments and append them to the  |
//|                   very same arrays this file already manages. The |
//|                   storage below does not change.                  |
//|    * edges      - converting contours into scanline edges is the  |
//|                   first thing Part 3 does.                        |
//|    * drawing    - there is not one pixel written anywhere here.   |
//|  AddThickLine(). A line with width is not a line - it is a        |
//|  QUADRILATERAL, so it is something this file can already          |
//|  describe. The helper turns one segment plus a thickness into     |
//|  four corners. The engine gains strokes without gaining a         |
//|  stroker, and the rasterizer learns nothing new.                  |
//+-------------------------------------------------------------------+
#ifndef CAIRO5_PATH_MQH
#define CAIRO5_PATH_MQH

//+-------------------------------------------------------------------+
//| SCairoPoint - one vertex.                                         |
//|                                                                   |
//| WHY DOUBLE AND NOT INT                                            |
//|                                                                   |
//| This is the single most consequential decision in the file.       |
//|                                                                   |
//| A rectangle whose left edge sits at x = 10.35 must cover 65% of   |
//| pixel column 10. If the coordinate is an int, that 0.35 is gone   |
//| before rendering even begins, and no rasteriser downstream can    |
//| recover it - the shape can only ever start exactly on a pixel     |
//| boundary.                                                         |
//|                                                                   |
//| Those fractional positions are what the anti-aliasing in Part 4   |
//| works with, and they are what allows an animation to move a       |
//| widget by a third of a pixel without it juddering. Integer        |
//| coordinates are precisely where CCanvas loses the ability to      |
//| produce a smooth edge.                                            |
//+-------------------------------------------------------------------+
struct SCairoPoint
  {
   double            x;
   double            y;
  };


//+-------------------------------------------------------------------+
//| SCairoEdge - one segment, pre-processed for scanline queries.     |
//|                                                                   |
//| The rasterizer asks exactly one question, millions of times:      |
//| "where does this segment cross the horizontal line at y?" So the  |
//| segment is stored in the form that answers it in one multiply and |
//| one add, rather than in the form it was written in.               |
//|                                                                   |
//| Every edge is stored TOP-DOWN, whichever way the original segment |
//| ran, so ytop is always the smaller y. The direction the segment   |
//| really ran is not lost - it is kept in dir, and that single       |
//| number is what the winding rule is made of.                       |
//+-------------------------------------------------------------------+
struct SCairoEdge
  {
   double            ytop;       // smaller y
   double            ybot;       // larger y
   double            xtop;       // x at ytop
   double            dxdy;       // slope: how far x moves per unit of y
   int               dir;        // +1 the original segment ran downward, -1 upward

   //--- x where this edge crosses the horizontal line at y.
   //--- One multiply and one add - which is the whole reason the
   //--- struct is laid out like this.
   double            XAt(const double y) const { return xtop + (y - ytop) * dxdy; }
  };

//+-------------------------------------------------------------------+
//| CCairoEdgeList - a growable array of edges, reused between fills. |
//|                                                                   |
//| The members are public and the class is deliberately dumb: it is  |
//| a buffer the rasterizer walks, not an abstraction. Clear() only   |
//| resets the count, so the memory survives to the next fill - the   |
//| same size / capacity split the path itself uses.                  |
//+-------------------------------------------------------------------+
class CCairoEdgeList
  {
public:
   SCairoEdge        m_edges[];
   int               m_count;

                     CCairoEdgeList();

   void              Clear();
   void              Reserve(const int capacity);
   void              Add(const double x0, const double y0,
                         const double x1, const double y1);
  };

//+-------------------------------------------------------------------+
//| Constructor                                                       |
//+-------------------------------------------------------------------+
CCairoEdgeList::CCairoEdgeList()
  {
   m_count = 0;
   ArrayResize(m_edges, 0);
  }

//+-------------------------------------------------------------------+
//| Reset the count. The allocation is kept on purpose.               |
//+-------------------------------------------------------------------+
void CCairoEdgeList::Clear()
  {
   m_count = 0;
  }

//+-------------------------------------------------------------------+
//| Ask for room for capacity edges before the first one arrives.     |
//|                                                                   |
//| The caller knows the answer in advance, so it can pay for one     |
//| allocation instead of a handful of growth steps. A refused resize |
//| is not fatal here: Add() checks the size on every append anyway,  |
//| so the list simply grows the slow way instead.                    |
//+-------------------------------------------------------------------+
void CCairoEdgeList::Reserve(const int capacity)
  {
   if(capacity > ArraySize(m_edges))
      ArrayResize(m_edges, capacity);
  }

//+-------------------------------------------------------------------+
//| Turn one segment into one edge.                                   |
//+-------------------------------------------------------------------+
void CCairoEdgeList::Add(const double x0, const double y0,
                         const double x1, const double y1)
  {
//--- a horizontal segment is crossed by no scanline, so it can
//--- never contribute a crossing. Dropping it here keeps the
//--- rasterizer's inner loop free of a special case.
   if(y0 == y1)
      return;

   const int i = m_count;

//--- grow in blocks of 64, not one slot at a time. A refused
//--- resize drops the edge instead of writing past the end.
   if(ArraySize(m_edges) < m_count + 1)
      if(ArrayResize(m_edges, m_count + 1, 64) < m_count + 1)
         return;

   m_count++;

   if(y0 < y1)
     {
      //--- the segment runs downward in screen space
      m_edges[i].dir  =  1;
      m_edges[i].ytop = y0;
      m_edges[i].ybot = y1;
      m_edges[i].xtop = x0;
      m_edges[i].dxdy = (x1 - x0) / (y1 - y0);
     }
   else
     {
      //--- the segment runs upward: store it flipped, and REMEMBER
      //--- the flip in dir - that memory is the winding number
      m_edges[i].dir  = -1;
      m_edges[i].ytop = y1;
      m_edges[i].ybot = y0;
      m_edges[i].xtop = x1;
      m_edges[i].dxdy = (x0 - x1) / (y0 - y1);
     }
  }

//+-------------------------------------------------------------------+
//| CCairoPath                                                        |
//|                                                                   |
//| STORAGE MODEL                                                     |
//|                                                                   |
//| One flat array holds the vertices of EVERY contour, back to back. |
//| A second, much smaller array records the index at which each      |
//| contour begins:                                                   |
//|                                                                   |
//|   m_points   [A0 A1 A2 A3][B0 B1 B2][C0 C1 C2 C3 C4]              |
//|   m_starts    0            4         7                            |
//|                                                                   |
//| So contour c runs from m_starts[c] up to m_starts[c+1], and the   |
//| last one runs to m_point_count. No nested arrays, no per-contour  |
//| objects, no allocation per contour - one growable array and an    |
//| index. Walking every vertex of a path is a single linear pass,    |
//| which is exactly what the rasteriser will want.                   |
//|                                                                   |
//| WHY MULTIPLE CONTOURS MATTER                                      |
//|                                                                   |
//| It is tempting to think one path = one shape. It is not, and the  |
//| difference is important:                                          |
//|                                                                   |
//|   * the letter "O" is two contours - the outside and the hole     |
//|   * a ring or a donut is two circles                              |
//|   * a border is an outline plus an inset copy of itself           |
//|   * an icon may be a dozen separate pieces                        |
//|                                                                   |
//| Each of those is ONE fill operation, not several. Getting the     |
//| storage right here is what makes holes possible in Part 5.        |
//|                                                                   |
//| OPEN AND CLOSED ARE DIFFERENT THINGS                              |
//|                                                                   |
//| A third array, one bool per contour, records whether the author   |
//| called Close(). That flag is NOT how a fill is closed - the       |
//| rasteriser in Part 3 joins the last vertex back to the first no   |
//| matter what, because an unclosed boundary has no inside. The flag |
//| records INTENT, which is a separate question and the one that     |
//| survives into everything after filling: a stroke needs it to      |
//| decide between a join and two end caps, an SVG importer carries   |
//| it, hit testing asks it. Reconstructing intent later by guessing  |
//| whether the first and last vertex happen to coincide is exactly   |
//| the kind of heuristic a geometry layer should never need.         |
//+-------------------------------------------------------------------+
class CCairoPath
  {
private:
   //--- THE SIZE / CAPACITY INVARIANT.
   //---
   //--- m_point_count is the number of VALID vertices. ArraySize( m_points )
   //--- is the CAPACITY, and it is always >= m_point_count - the tail beyond
   //--- the count is unwritten memory that a reserve left behind. The two
   //--- numbers differ on purpose, and the whole class depends on the
   //--- difference. Never walk a path with ArraySize(); always use
   //--- PointCount(), ContourCount() and the contour helpers.

   SCairoPoint       m_points[];       // vertices of every contour, back to back
   int               m_point_count;    // valid vertices, NOT ArraySize( m_points )
   int               m_starts[];       // index into m_points where each contour begins
   bool              m_closed[];       // did the author call Close() on contour c?
   int               m_contour_count;  // valid contours, NOT ArraySize( m_starts )
   double            m_cur_x;          // the "pen" position
   double            m_cur_y;
   bool              m_has_current;    // is there a pen position at all?
   bool              m_alloc_failed;   // sticky: some allocation was refused

   bool              AddPoint(const double x, const double y);

public:
                     CCairoPath();

   //--- LIFECYCLE
   //--- Clear() empties the path but keeps the memory it has already
   //--- been given; Release() hands that memory back. A path rebuilt
   //--- every frame wants the first, a path that is finished with wants
   //--- the second.

   void              Clear();
   void              Release();
   bool              IsEmpty() const;


   //--- HEALTH
   //--- IsValid()  - did every allocation this path asked for succeed?
   //--- IsFinite() - is every stored coordinate a real number?
   //--- Neither is checked automatically. Both are one call at the end
   //--- of building a path, which is where a caller can still do
   //--- something about the answer.

   bool              IsValid()  const { return !m_alloc_failed; }
   bool              IsFinite() const;

   //--- capacity: ask for the room up front when the count is known
   bool              ReservePoints(const int capacity);
   bool              ReserveContours(const int capacity);

   //--- the primitive verbs
   void              MoveTo(const double x, const double y);
   void              LineTo(const double x, const double y);
   void              Close();

   //--- where is the pen right now, and is there one at all?
   double            CurrentX() const { return m_cur_x; }
   double            CurrentY() const { return m_cur_y; }
   bool              HasCurrentPoint() const { return m_has_current; }

   //--- shape helpers: pure path construction, no rasterising
   void              AddRect(const double x, const double y, const double w, const double h);
   void              AddTriangle(const double x0, const double y0,
                                 const double x1, const double y1,
                                 const double x2, const double y2);
   void              AddPolygon(const double &xs[], const double &ys[], const int n);
   void              AddPolyline(const double &xs[], const double &ys[], const int n);

   //--- Part 5: one segment of a stroke, as a filled quadrilateral
   void              AddThickLine(const double x0, const double y0,
                     const double x1, const double y1,
                     const double thickness);
   //--- INSPECTION
   //--- Reading a path back is not needed to DRAW it - the rasteriser
   //--- will walk the arrays directly. These accessors exist so that a
   //--- demo, a test or a debug overlay can show what geometry was
   //--- actually produced, which is the whole of this article's demo
   //--- and, later, the clearest way to understand curve flattening.
   //--- There are two ways to read a vertex, and the difference is
   //--- deliberate. GetPoint() and GetContourInfo() range-check and
   //--- return false on a bad index: use those when the index came from
   //--- anywhere but a loop you can see. PointX() and PointY() do NOT
   //--- check - they are the fast pair the rasteriser calls once per
   //--- vertex per scanline, and passing them an index outside
   //--- 0..PointCount()-1 is an out-of-range access, not a caught error.

   int               PointCount() const   { return m_point_count;   }
   int               ContourCount() const { return m_contour_count; }

   bool              GetPoint(const int i, SCairoPoint &pt) const;
   bool              GetContourInfo(const int c, int &start, int &length, bool &closed) const;
   bool              IsContourClosed(const int c) const;

   double            PointX(const int i) const { return m_points[i].x; }     // unchecked
   double            PointY(const int i) const { return m_points[i].y; }     // unchecked

   int               ContourStart(const int c) const;
   int               ContourLength(const int c) const;

   //--- THE BRIDGE TO THE RASTERIZER
   //--- The one method that reads the whole path and writes something
   //--- else. It produces edges into a list the caller owns and reuses,
   //--- which is why it takes the list by reference instead of
   //--- returning one.

   void              BuildEdges(CCairoEdgeList &out);
  };

//+-------------------------------------------------------------------+
//| Constructor                                                       |
//+-------------------------------------------------------------------+
CCairoPath::CCairoPath()
  {
   Clear();
  }

//+-------------------------------------------------------------------+
//| Reset to an empty path - but KEEP the memory.                     |
//|                                                                   |
//| Setting the counts to zero is the entire reset: every vertex and  |
//| every flag beyond the count is unreachable, so there is nothing   |
//| to erase. What survives is the allocation, and that is the point. |
//|                                                                   |
//| A dashboard that rebuilds its geometry on every tick calls this   |
//| thousands of times an hour. Freeing the arrays here would mean    |
//| the same memory is released and re-acquired on every one of those |
//| rebuilds, to end up at the same size it had a moment earlier.     |
//| Keeping it means the second frame, and every frame after it, adds |
//| its vertices without a single reallocation.                       |
//+-------------------------------------------------------------------+
void CCairoPath::Clear()
  {
   m_point_count   = 0;
   m_contour_count = 0;
   m_cur_x         = 0.0;
   m_cur_y         = 0.0;
   m_has_current   = false;
   m_alloc_failed  = false;
  }

//+-------------------------------------------------------------------+
//| Clear, and hand the memory back to the terminal.                  |
//|                                                                   |
//| The other half of the pair. Use it when a path is genuinely done  |
//| with - a one-off import, a cached shape being dropped - and the   |
//| memory is worth more elsewhere than the next rebuild is worth     |
//| here. If you are unsure which of the two you want, you want       |
//| Clear().                                                          |
//+-------------------------------------------------------------------+
void CCairoPath::Release()
  {
   Clear();
   ArrayResize(m_points, 0);
   ArrayResize(m_starts, 0);
   ArrayResize(m_closed, 0);
  }

//+-------------------------------------------------------------------+
//| True when nothing has been added yet.                             |
//+-------------------------------------------------------------------+
bool CCairoPath::IsEmpty() const
  {
   return (m_contour_count == 0);
  }

//+-------------------------------------------------------------------+
//| Append a vertex and make it the current pen position.             |
//|                                                                   |
//| THE RESERVE ARGUMENT MATTERS.                                     |
//|                                                                   |
//| ArrayResize's third parameter is a reserve: it allocates extra    |
//| room beyond what was asked for, so the next 63 appends are free.  |
//| Without it, adding N points reallocates and copies N times, which |
//| turns a curve with 400 vertices - perfectly ordinary once Part 6  |
//| arrives - into 400 array copies.                                  |
//|                                                                   |
//| AND SO DOES THE RETURN VALUE.                                     |
//|                                                                   |
//| ArrayResize can fail. It is rare, it is usually the last thing on |
//| a developer's mind, and the cost of ignoring it is writing past   |
//| the end of an array - which in a graphical EA will not announce   |
//| itself at the point of the mistake, but somewhere later and       |
//| stranger. So the failure is refused here rather than written      |
//| through, m_alloc_failed remembers it, and IsValid() reports it.   |
//|                                                                   |
//| The bool exists for exactly one caller: MoveTo, which has already |
//| opened a contour by the time it calls this and has to take that   |
//| back if the vertex never arrives. Everywhere else the sticky flag |
//| is the better answer - checking every append would bury the       |
//| geometry in error handling to detect a condition that has already |
//| been recorded.                                                    |
//+-------------------------------------------------------------------+
bool CCairoPath::AddPoint(const double x, const double y)
  {
   const int i = m_point_count;

   if(ArraySize(m_points) < i + 1)
      if(ArrayResize(m_points, i + 1, 64) < i + 1)
        {
         m_alloc_failed = true;     // out of memory - refuse, do not write
         return false;
        }

   m_points[i].x = x;
   m_points[i].y = y;
   m_point_count++;

   m_cur_x       = x;
   m_cur_y       = y;
   m_has_current = true;

   return true;
  }

//+-------------------------------------------------------------------+
//| MoveTo - begin a NEW contour at (x, y).                           |
//|                                                                   |
//| Think of it as lifting the pen off the paper and putting it down  |
//| somewhere else. Everything drawn after this call belongs to a new |
//| outline, independent of the previous one, until the next MoveTo.  |
//|                                                                   |
//| Recording the start index - and an open flag - is all that        |
//| "beginning a contour" actually means.                             |
//|                                                                   |
//| The rollback at the end is not defensive noise. The contour is    |
//| counted before its first vertex exists, so a refused allocation   |
//| would otherwise leave a contour of length zero in the middle of   |
//| the path: something every loop downstream would have to know to   |
//| skip. Undoing the count keeps the promise that a contour which    |
//| exists has at least one vertex.                                   |
//+-------------------------------------------------------------------+
void CCairoPath::MoveTo(const double x, const double y)
  {
   const int c = m_contour_count;

   if(ArraySize(m_starts) < c + 1)
      if(ArrayResize(m_starts, c + 1, 16) < c + 1)
        {
         m_alloc_failed = true;
         return;
        }

   if(ArraySize(m_closed) < c + 1)
      if(ArrayResize(m_closed, c + 1, 16) < c + 1)
        {
         m_alloc_failed = true;
         return;
        }

   m_starts[c] = m_point_count;
   m_closed[c] = false;
   m_contour_count++;

   if(!AddPoint(x, y))
      m_contour_count--;      // <- the reason AddPoint returns bool:
//---   never leave an empty contour behind
  }

//+-------------------------------------------------------------------+
//| LineTo - straight segment from the pen to (x, y).                 |
//|                                                                   |
//| Calling LineTo with no pen down is not an error; it simply opens  |
//| a contour at that point, which is what the caller almost          |
//| certainly meant. Being forgiving here removes a whole class of    |
//| "why is my path empty" questions, and it is not an invention:     |
//| Cairo's own cairo_line_to behaves as cairo_move_to when there is  |
//| no current point. Matching it is the compatible choice, not the   |
//| lenient one.                                                      |
//|                                                                   |
//| Note that "no pen down" is a real piece of state, not a guess     |
//| from the contour count. After Close() the pen is lifted, so a     |
//| LineTo following a Close begins a new contour instead of adding a |
//| vertex to a contour the author already declared finished.         |
//+-------------------------------------------------------------------+
void CCairoPath::LineTo(const double x, const double y)
  {
   if(!m_has_current)
      MoveTo(x, y);
   else
      AddPoint(x, y);
  }

//+-------------------------------------------------------------------+
//| Close the current contour.                                        |
//|                                                                   |
//| It stores no vertex, and that is worth being precise about,       |
//| because "adds nothing to the array" and "does nothing" are not    |
//| the same statement.                                               |
//|                                                                   |
//| The closing SEGMENT is never stored, by anyone. When Part 3 turns |
//| contours into edges it joins the last vertex back to the first    |
//| unconditionally, because a fill needs a boundary that closes -    |
//| an open outline has no inside to fill. So no geometry is missing  |
//| here and none needs adding.                                       |
//|                                                                   |
//| What this function records is the author's INTENT, which the fill |
//| rule cannot tell us and must not be confused with. A polyline     |
//| that happens to end where it started is not a closed contour; a   |
//| triangle whose three vertices are far apart is. The difference is |
//| invisible in the vertex array and decides real behaviour later:   |
//| whether a stroke draws a join or two end caps at that vertex,     |
//| what an SVG exporter writes, what hit testing answers. Recovering |
//| it afterwards would mean guessing, and a geometry layer that      |
//| guesses is a geometry layer that is wrong occasionally.           |
//|                                                                   |
//| Following Cairo, the pen also returns to the start of the contour |
//| and is lifted: the contour is finished, so the next LineTo starts |
//| a new one rather than reopening this.                             |
//+-------------------------------------------------------------------+
void CCairoPath::Close()
  {
   if(m_contour_count == 0)
      return;

   const int c = m_contour_count - 1;
   m_closed[c] = true;             // record the intent - this is the whole point

//--- Cairo semantics: the pen returns to where this contour began.
//--- The contour is finished, so the next LineTo opens a new one.
   const int s = m_starts[c];
   if(s < m_point_count)
     {
      m_cur_x = m_points[s].x;
      m_cur_y = m_points[s].y;
     }
   m_has_current = false;
  }

//+-------------------------------------------------------------------+
//| Axis-aligned rectangle.                                           |
//|                                                                   |
//| Note that the library's very first "shape" is four lines of       |
//| arithmetic and no special support anywhere else. That is the      |
//| pattern every shape follows to the end of the series: a helper    |
//| emits vertices, and nothing downstream learns a new case.         |
//|                                                                   |
//| The vertex order - top-left, top-right, bottom-right, bottom-left |
//| - is the same rotational direction every helper in this library   |
//| uses. That matters in Part 5, where the non-zero fill rule reads  |
//| the RELATIVE direction of two contours: a hole has to be wound    |
//| against the outline that contains it. Emitting vertices in a      |
//| stable order is what makes that relationship something a caller   |
//| controls rather than discovers. Note that the library never       |
//| re-winds what it is given - AddPolygon stores the order it        |
//| receives, and it is the caller's to decide.                       |
//+-------------------------------------------------------------------+
void CCairoPath::AddRect(const double x, const double y, const double w, const double h)
  {
   MoveTo(x,     y);
   LineTo(x + w, y);
   LineTo(x + w, y + h);
   LineTo(x,     y + h);
   Close();
  }

//+-------------------------------------------------------------------+
//| Triangle from three arbitrary points.                             |
//+-------------------------------------------------------------------+
void CCairoPath::AddTriangle(const double x0, const double y0,
                             const double x1, const double y1,
                             const double x2, const double y2)
  {
   MoveTo(x0, y0);
   LineTo(x1, y1);
   LineTo(x2, y2);
   Close();
  }

//+-------------------------------------------------------------------+
//| Arbitrary CLOSED polygon from parallel x / y arrays.              |
//|                                                                   |
//| This is the escape hatch, and it is more powerful than it looks:  |
//| a star, a gear, a chevron, an arrow, a price chart's filled area, |
//| a hexagon - anything whose vertices you can compute, you can      |
//| describe. It is also, quietly, all a circle ever is. Part 7 will  |
//| simply compute the vertices for you.                              |
//|                                                                   |
//| The n < 3 guard belongs to this helper and not to the path model. |
//| A polygon is a shape with an inside, and two vertices enclose no  |
//| area, so there is nothing here to build. That is not a claim that |
//| paths must be closed - AddPolyline below stores exactly the same  |
//| vertices without closing them, and a two-point contour is a       |
//| perfectly legal path. The two helpers differ in one line, and     |
//| that line is the whole distinction.                               |
//+-------------------------------------------------------------------+
void CCairoPath::AddPolygon(const double &xs[], const double &ys[], const int n)
  {
//--- a FILLED polygon needs area; use AddPolyline for open shapes
   if(n < 3)
      return;

   if(ArraySize(xs) < n || ArraySize(ys) < n)         // fewer points than promised
      return;

   ReservePoints(m_point_count + n);                  // the count is known: ask once

   MoveTo(xs[0], ys[0]);

   for(int i = 1; i < n; i++)
      LineTo(xs[i], ys[i]);

   Close();
  }

//+-------------------------------------------------------------------+
//| Arbitrary OPEN polyline from parallel x / y arrays.               |
//|                                                                   |
//| The same vertices as AddPolygon, minus the Close(). An indicator  |
//| line, an equity curve, a zig-zag, the outline of a channel - none |
//| of these are shapes with an inside, and saying so is the point of |
//| having both helpers rather than one.                              |
//|                                                                   |
//| Two vertices are enough: a single segment is a legitimate path.   |
//+-------------------------------------------------------------------+
void CCairoPath::AddPolyline(const double &xs[], const double &ys[], const int n)
  {
//--- one point is not a line
   if(n < 2)
      return;

   if(ArraySize(xs) < n || ArraySize(ys) < n)
      return;

   ReservePoints(m_point_count + n);

   MoveTo(xs[0], ys[0]);

   for(int i = 1; i < n; i++)
      LineTo(xs[i], ys[i]);

//--- deliberately no Close(): this contour stays open, and the path
//--- now remembers that instead of leaving it to be guessed
  }

//+------------------------------------------------------------------+
//| AddThickLine - one segment of a stroke, as a closed quad.        |
//|                                                                  |
//| The engine has no stroker and is not going to get one. A line    |
//| with width IS a shape, so we build it out of the only thing the  |
//| rasterizer understands: a filled contour.                        |
//|                                                                  |
//| The normal of a direction (dx, dy) is (-dy, dx) - the same       |
//| vector turned ninety degrees. Make it one unit long, scale it by |
//| half the thickness, and push each endpoint both ways. That is    |
//| four corners.                                                    |
//|                                                                  |
//|       p0 + n  *-------------------------*  p1 + n                |
//|               |                         |                        |
//|         p0 ---+-------------------------+--- p1                  |
//|               |                         |                        |
//|       p0 - n  *-------------------------*  p1 - n                |
//|                                                                  |
//| The four corners are emitted in a CONSISTENT rotational order,   |
//| and that is not cosmetic. Every quad of a polyline must be wound |
//| the same way. If one of them is wound backwards, the overlap at  |
//| the joint sums to zero under the non-zero rule, and the stroke   |
//| gets a hole at that corner. The demo shows that failure on       |
//| purpose.                                                         |
//+------------------------------------------------------------------+
void CCairoPath::AddThickLine(const double x0, const double y0,
                              const double x1, const double y1,
                              const double thickness)
  {
   const double dx  = x1 - x0;
   const double dy  = y1 - y0;
   const double len = MathSqrt(dx * dx + dy * dy);

//--- a zero-length segment has no direction, so it has no normal
//--- and no quad. Returning is correct; dividing is not.
   if(len < 1e-9)
      return;

//--- the quad is four vertices in one new contour
   ReservePoints(m_point_count + 4);

//--- unit normal, scaled to half the thickness
   const double nx = -dy / len * (thickness * 0.5);
   const double ny =  dx / len * (thickness * 0.5);

   MoveTo(x0 + nx, y0 + ny);
   LineTo(x0 - nx, y0 - ny);
   LineTo(x1 - nx, y1 - ny);
   LineTo(x1 + nx, y1 + ny);
   Close();
  }

//+-------------------------------------------------------------------+
//| Index of the first vertex of contour c.                           |
//+-------------------------------------------------------------------+
int CCairoPath::ContourStart(const int c) const
  {
   if(c < 0 || c >= m_contour_count)
      return 0;

   return m_starts[c];
  }

//+-------------------------------------------------------------------+
//| Number of vertices in contour c.                                  |
//|                                                                   |
//| The last contour runs to the end of the point array; every other  |
//| one runs up to where the next contour begins. This is the only    |
//| slightly subtle consequence of the flat storage, and it is worth  |
//| the trade.                                                        |
//+-------------------------------------------------------------------+
int CCairoPath::ContourLength(const int c) const
  {
   if(c < 0 || c >= m_contour_count)
      return 0;

   const int start = m_starts[c];
   const int end   = (c + 1 < m_contour_count) ? m_starts[c + 1] : m_point_count;

   return end - start;
  }
//+-------------------------------------------------------------------+
//| Ask for room for `capacity` vertices in total.                    |
//|                                                                   |
//| AddPoint already grows in blocks of 64, so this is not about      |
//| avoiding reallocation altogether - it is about not doing it at    |
//| all when the answer is known in advance. A flattened curve, a     |
//| circle of 96 segments, a glyph outline, an imported SVG: in every |
//| one of those the vertex count is computed before the first vertex |
//| is stored, and one allocation of the right size beats six that    |
//| grow into it.                                                     |
//|                                                                   |
//| Note what "capacity" means here: this raises ArraySize above      |
//| m_point_count without adding a single valid vertex. PointCount()  |
//| does not change. That is the size / capacity invariant at the top |
//| of the class doing its job.                                       |
//+-------------------------------------------------------------------+
bool CCairoPath::ReservePoints(const int capacity)
  {
   if(capacity <= ArraySize(m_points))
      return true;

   if(ArrayResize(m_points, capacity) < capacity)
     {
      m_alloc_failed = true;
      return false;
     }

   return true;
  }

//+-------------------------------------------------------------------+
//| The same, for the two per-contour arrays.                         |
//|                                                                   |
//| Both are grown, and both are tested: m_starts and m_closed are    |
//| indexed by the same contour number, so a path in which one is     |
//| larger than the other is a path waiting to read a flag that was   |
//| never written.                                                    |
//+-------------------------------------------------------------------+
bool CCairoPath::ReserveContours(const int capacity)
  {
   if(capacity <= ArraySize(m_starts) && capacity <= ArraySize(m_closed))
      return true;

   if(ArrayResize(m_starts, capacity) < capacity)
     {
      m_alloc_failed = true;
      return false;
     }

   if(ArrayResize(m_closed, capacity) < capacity)
     {
      m_alloc_failed = true;
      return false;
     }

   return true;
  }

//+-------------------------------------------------------------------+
//| Did the author call Close() on this contour?                      |
//|                                                                   |
//| An out-of-range contour answers "not closed" rather than raising: |
//| the honest answer about a contour that does not exist.            |
//+-------------------------------------------------------------------+
bool CCairoPath::IsContourClosed(const int c) const
  {
   if(c < 0 || c >= m_contour_count)
      return false;

   return m_closed[c];
  }

//+-------------------------------------------------------------------+
//| Read one vertex, safely.                                          |
//|                                                                   |
//| The checked counterpart to PointX / PointY. It costs a comparison |
//| and it cannot go wrong, which is the right trade everywhere       |
//| except the rasteriser's inner loop.                               |
//+-------------------------------------------------------------------+
bool CCairoPath::GetPoint(const int i, SCairoPoint &pt) const
  {
   if(i < 0 || i >= m_point_count)
      return false;

   pt = m_points[i];
   return true;
  }

//+-------------------------------------------------------------------+
//| Everything about one contour, in a single checked call.           |
//|                                                                   |
//| Walking a path means asking three questions per contour - where   |
//| does it start, how long is it, was it closed - and getting them   |
//| individually invites the fourth mistake of asking about a contour |
//| that is not there. One call, one range check, three answers.      |
//+-------------------------------------------------------------------+
bool CCairoPath::GetContourInfo(const int c, int &start, int &length, bool &closed) const
  {
   if(c < 0 || c >= m_contour_count)
      return false;

   start  = ContourStart(c);
   length = ContourLength(c);
   closed = m_closed[c];

   return true;
  }

//+-------------------------------------------------------------------+
//| Is every stored coordinate a real number?                         |
//|                                                                   |
//| The one place the coordinate contract can be checked cheaply: one |
//| linear pass, on demand, on a finished path. Worth calling when    |
//| the vertices came from indicator buffers or from arithmetic that  |
//| can divide by zero - a NaN found here is a bug with an address,   |
//| while the same NaN found by the rasteriser is a blank panel with  |
//| no explanation.                                                   |
//+-------------------------------------------------------------------+
bool CCairoPath::IsFinite() const
  {
   for(int i = 0; i < m_point_count; i++)
      if(!MathIsValidNumber(m_points[i].x) ||
         !MathIsValidNumber(m_points[i].y))
         return false;

   return true;
  }

//+-------------------------------------------------------------------+
//| BuildEdges - turn every contour into edges.                       |
//|                                                                   |
//| EVERY CONTOUR IS CLOSED HERE, WHATEVER THE AUTHOR ASKED FOR.      |
//|                                                                   |
//| The modulo on the index wraps the last vertex of a contour back   |
//| to its first, so the closing segment nobody stored is produced    |
//| right here. It is produced for open contours too, and that is     |
//| not a contradiction of the closed flag. A fill needs a boundary   |
//| that closes, because an outline with a gap has no inside, so      |
//| filling an open contour can only mean filling the shape it would  |
//| be if it were closed.                                             |
//|                                                                   |
//| The two answers live side by side and both are correct. m_closed  |
//| says what the author wrote, and a stroke will read it in a later  |
//| part to choose between a joint and two end caps. This loop says   |
//| what a fill has to do, and that is not a matter of opinion.       |
//| After it returns, nothing downstream can tell an open contour     |
//| from a closed one - which is exactly why the flag had to be       |
//| recorded before this point instead of guessed after it.           |
//|                                                                   |
//| This function is also the last thing in the pipeline that knows   |
//| about contours. What leaves it is one flat list of segments with  |
//| no grouping at all.                                               |
//+-------------------------------------------------------------------+
void CCairoPath::BuildEdges(CCairoEdgeList &out)
  {
   out.Clear();

//--- a path that was refused memory is not geometry we can trust,
//--- and half a shape on screen is worse than no shape at all
   if(!IsValid())
      return;

//--- each contour emits exactly as many edges as it has vertices,
//--- so the total is known before the loop starts
   out.Reserve(m_point_count);

   for(int c = 0; c < m_contour_count; c++)
     {
      const int start = m_starts[c];
      const int count = ContourLength(c);

      //--- one vertex encloses no area, so it produces no edges
      if(count < 2)
         continue;

      for(int i = 0; i < count; i++)
        {
         const int a = start + i;
         const int b = start + ((i + 1) % count);       // wrap = auto-close

         out.Add(m_points[a].x, m_points[a].y,
                 m_points[b].x, m_points[b].y);
        }
     }
  }

#endif // CAIRO5_PATH_MQH
