fix crash when copy'ing latent plugins
[ardour.git] / libs / canvas / curve.cc
index df65cf491ce2548019643f75d9c1094915908442..280a3e3aaaf7688a04d41f0339ddf848bb3caca3 100644 (file)
@@ -17,6 +17,7 @@
 
 */
 
+#include <cmath>
 #include <exception>
 #include <algorithm>
 
@@ -26,11 +27,37 @@ using namespace ArdourCanvas;
 using std::min;
 using std::max;
 
-Curve::Curve (Group* parent)
-       : Item (parent)
-       , PolyItem (parent)
+Curve::Curve (Canvas* c)
+       : PolyItem (c)
+       , n_samples (0)
+       , points_per_segment (16)
+       , curve_fill (None)
 {
+}
 
+Curve::Curve (Item* parent)
+       : PolyItem (parent)
+       , n_samples (0)
+       , points_per_segment (16)
+       , curve_fill (None)
+{
+}
+
+/** When rendering the curve, we will always draw a fixed number of straight
+ * line segments to span the x-axis extent of the curve. More segments:
+ * smoother visual rendering. Less rendering: closer to a visibily poly-line
+ * render.
+ */
+void
+Curve::set_points_per_segment (uint32_t n)
+{
+       /* this only changes our appearance rather than the bounding box, so we
+          just need to schedule a redraw rather than notify the parent of any
+          changes
+       */
+       points_per_segment = n;
+       interpolate ();
+       redraw ();
 }
 
 void
@@ -38,182 +65,196 @@ Curve::compute_bounding_box () const
 {
        PolyItem::compute_bounding_box ();
 
-       if (_bounding_box) {
-
-               bool have1 = false;
-               bool have2 = false;
-               
-               Rect bbox1;
-               Rect bbox2;
-
-               for (Points::const_iterator i = first_control_points.begin(); i != first_control_points.end(); ++i) {
-                       if (have1) {
-                               bbox1.x0 = min (bbox1.x0, i->x);
-                               bbox1.y0 = min (bbox1.y0, i->y);
-                               bbox1.x1 = max (bbox1.x1, i->x);
-                               bbox1.y1 = max (bbox1.y1, i->y);
-                       } else {
-                               bbox1.x0 = bbox1.x1 = i->x;
-                               bbox1.y0 = bbox1.y1 = i->y;
-                               have1 = true;
-                       }
-               }
-               
-               for (Points::const_iterator i = second_control_points.begin(); i != second_control_points.end(); ++i) {
-                       if (have2) {
-                               bbox2.x0 = min (bbox2.x0, i->x);
-                               bbox2.y0 = min (bbox2.y0, i->y);
-                               bbox2.x1 = max (bbox2.x1, i->x);
-                               bbox2.y1 = max (bbox2.y1, i->y);
-                       } else {
-                               bbox2.x0 = bbox2.x1 = i->x;
-                               bbox2.y0 = bbox2.y1 = i->y;
-                               have2 = true;
-                       }
-               }
-               
-               Rect u = bbox1.extend (bbox2);
-               _bounding_box = u.extend (_bounding_box.get());
-       }
-       
-       _bounding_box_dirty = false;
+       /* possibly add extents of any point indicators here if we ever do that */
 }
 
 void
 Curve::set (Points const& p)
 {
        PolyItem::set (p);
-
-       first_control_points.clear ();
-       second_control_points.clear ();
-
-       compute_control_points (_points, first_control_points, second_control_points);
+       interpolate ();
 }
 
 void
-Curve::render (Rect const & area, Cairo::RefPtr<Cairo::Context> context) const
-{
-       if (_outline) {
-               setup_outline_context (context);
-               render_path (area, context);
-               context->stroke ();
-       }
-}
-
-void 
-Curve::render_path (Rect const & area, Cairo::RefPtr<Cairo::Context> context) const
+Curve::interpolate ()
 {
-       std::cerr << whatami() << '/' << name << " render curve w/" << _points.size() << " points, " << first_control_points.size() << " first and "
-                 << second_control_points.size() << " second\n";
-       PolyItem::render_curve (area, context, first_control_points, second_control_points);
+       samples.clear ();
+       InterpolatedCurve::interpolate (_points, points_per_segment, CatmullRomCentripetal, false, samples);
+       n_samples = samples.size();
 }
 
-void 
-Curve::compute_control_points (Points const& knots,
-                              Points& firstControlPoints, 
-                              Points& secondControlPoints)
+void
+Curve::render (Rect const & area, Cairo::RefPtr<Cairo::Context> context) const
 {
-       Points::size_type n = knots.size() - 1;
-       
-       if (n < 1) {
-               return;
-       }
-       
-       if (n == 1) { 
-               /* Special case: Bezier curve should be a straight line. */
-               
-               Duple d;
-               
-               d.x = (2.0 * knots[0].x + knots[1].x) / 3;
-               d.y = (2.0 * knots[0].y + knots[1].y) / 3;
-               firstControlPoints.push_back (d);
-               
-               d.x = 2.0 * firstControlPoints[0].x - knots[0].x;
-               d.y = 2.0 * firstControlPoints[0].y - knots[0].y;
-               secondControlPoints.push_back (d);
-               
+       if (!_outline || _points.size() < 2 || !_bounding_box) {
                return;
        }
-       
-       // Calculate first Bezier control points
-       // Right hand side vector
-       
-       std::vector<double> rhs;
-       
-       rhs.assign (n, 0);
-       
-       // Set right hand side X values
-       
-       for (Points::size_type i = 1; i < n - 1; ++i) {
-               rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x;
-       }
-       rhs[0] = knots[0].x + 2 * knots[1].x;
-       rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0;
-       
-       // Get first control points X-values
-       double* x = solve (rhs);
-
-       // Set right hand side Y values
-       for (Points::size_type i = 1; i < n - 1; ++i) {
-               rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y;
-       }
-       rhs[0] = knots[0].y + 2 * knots[1].y;
-       rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0;
-       
-       // Get first control points Y-values
-       double* y = solve (rhs);
-       
-       for (Points::size_type i = 0; i < n; ++i) {
-               
-               firstControlPoints.push_back (Duple (x[i], y[i]));
-               
-               if (i < n - 1) {
-                       secondControlPoints.push_back (Duple (2 * knots [i + 1].x - x[i + 1], 
-                                                             2 * knots[i + 1].y - y[i + 1]));
-               } else {
-                       secondControlPoints.push_back (Duple ((knots [n].x + x[n - 1]) / 2,
-                                                             (knots[n].y + y[n - 1]) / 2));
+
+       Rect self = item_to_window (_bounding_box.get());
+       boost::optional<Rect> d = self.intersection (area);
+       assert (d);
+       Rect draw = d.get ();
+
+       /* Our approach is to always draw n_segments across our total size.
+        *
+        * This is very inefficient if we are asked to only draw a small
+        * section of the curve. For now we rely on cairo clipping to help
+        * with this.
+        */
+
+
+       setup_outline_context (context);
+
+       if (_points.size() == 2) {
+
+               /* straight line */
+
+               Duple window_space;
+
+               window_space = item_to_window (_points.front());
+               context->move_to (window_space.x, window_space.y);
+               window_space = item_to_window (_points.back());
+               context->line_to (window_space.x, window_space.y);
+
+
+               switch (curve_fill) {
+                       case None:
+                               context->stroke();
+                               break;
+                       case Inside:
+                               context->stroke_preserve ();
+                               window_space = item_to_window (Duple(_points.back().x, draw.height()));
+                               context->line_to (window_space.x, window_space.y);
+                               window_space = item_to_window (Duple(_points.front().x, draw.height()));
+                               context->line_to (window_space.x, window_space.y);
+                               context->close_path();
+                               setup_fill_context(context);
+                               context->fill ();
+                               break;
+                       case Outside:
+                               context->stroke_preserve ();
+                               window_space = item_to_window (Duple(_points.back().x, 0.0));
+                               context->line_to (window_space.x, window_space.y);
+                               window_space = item_to_window (Duple(_points.front().x, 0.0));
+                               context->line_to (window_space.x, window_space.y);
+                               context->close_path();
+                               setup_fill_context(context);
+                               context->fill ();
+                               break;
                }
-       }
-       
-       delete [] x;
-       delete [] y;
-}
 
-/** Solves a tridiagonal system for one of coordinates (x or y)
- *  of first Bezier control points.
- */
+       } else {
 
-double*  
-Curve::solve (std::vector<double> const & rhs) 
-{
-       std::vector<double>::size_type n = rhs.size();
-       double* x = new double[n]; // Solution vector.
-       double* tmp = new double[n]; // Temp workspace.
-       
-       double b = 2.0;
-
-       x[0] = rhs[0] / b;
-
-       for (std::vector<double>::size_type i = 1; i < n; i++) {
-               // Decomposition and forward substitution.
-               tmp[i] = 1 / b;
-               b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
-               x[i] = (rhs[i] - x[i - 1]) / b;
-       }
-       
-       for (std::vector<double>::size_type i = 1; i < n; i++) {
-               // Backsubstitution
-               x[n - i - 1] -= tmp[n - i] * x[n - i]; 
+               /* curve of at least 3 points */
+
+               /* x-axis limits of the curve, in window space coordinates */
+
+               Duple w1 = item_to_window (Duple (_points.front().x, 0.0));
+               Duple w2 = item_to_window (Duple (_points.back().x, 0.0));
+
+               /* clamp actual draw to area bound by points, rather than our bounding box which is slightly different */
+
+               context->save ();
+               context->rectangle (draw.x0, draw.y0, draw.width(), draw.height());
+               context->clip ();
+
+               /* expand drawing area by several pixels on each side to avoid cairo stroking effects at the boundary.
+                  they will still occur, but cairo's clipping will hide them.
+                */
+
+               draw = draw.expand (4.0);
+
+               /* now clip it to the actual points in the curve */
+
+               if (draw.x0 < w1.x) {
+                       draw.x0 = w1.x;
+               }
+
+               if (draw.x1 >= w2.x) {
+                       draw.x1 = w2.x;
+               }
+
+               /* find left and right-most sample */
+               Duple window_space;
+               Points::size_type left = 0;
+               Points::size_type right = n_samples;
+
+               for (Points::size_type idx = 0; idx < n_samples - 1; ++idx) {
+                       left = idx;
+                       window_space = item_to_window (Duple (samples[idx].x, 0.0));
+                       if (window_space.x >= draw.x0) break;
+               }
+               for (Points::size_type idx = n_samples; idx > left + 1; --idx) {
+                       window_space = item_to_window (Duple (samples[idx].x, 0.0));
+                       if (window_space.x <= draw.x1) break;
+                       right = idx;
+               }
+
+               /* draw line between samples */
+               window_space = item_to_window (Duple (samples[left].x, samples[left].y));
+               context->move_to (window_space.x, window_space.y);
+               for (uint32_t idx = left + 1; idx < right; ++idx) {
+                       window_space = item_to_window (Duple (samples[idx].x, samples[idx].y));
+                       context->line_to (window_space.x, window_space.y);
+               }
+
+               switch (curve_fill) {
+                       case None:
+                               context->stroke();
+                               break;
+                       case Inside:
+                               context->stroke_preserve ();
+                               window_space = item_to_window (Duple (samples[right-1].x, draw.height()));
+                               context->line_to (window_space.x, window_space.y);
+                               window_space = item_to_window (Duple (samples[left].x, draw.height()));
+                               context->line_to (window_space.x, window_space.y);
+                               context->close_path();
+                               setup_fill_context(context);
+                               context->fill ();
+                               break;
+                       case Outside:
+                               context->stroke_preserve ();
+                               window_space = item_to_window (Duple (samples[right-1].x, 0.0));
+                               context->line_to (window_space.x, window_space.y);
+                               window_space = item_to_window (Duple (samples[left].x, 0.0));
+                               context->line_to (window_space.x, window_space.y);
+                               context->close_path();
+                               setup_fill_context(context);
+                               context->fill ();
+                               break;
+               }
+               context->restore ();
        }
 
-       delete [] tmp;
-       
-       return x;
+#if 0
+       /* add points */
+       setup_outline_context (context);
+       for (Points::const_iterator p = _points.begin(); p != _points.end(); ++p) {
+               Duple window_space (item_to_window (*p));
+               context->arc (window_space.x, window_space.y, 5.0, 0.0, 2 * M_PI);
+               context->stroke ();
+       }
+#endif
 }
 
 bool
-Curve::covers (Duple const & point) const
+Curve::covers (Duple const & pc) const
 {
+       Duple point = window_to_item (pc);
+
+       /* O(N) N = number of points, and not accurate */
+
+       for (Points::const_iterator p = _points.begin(); p != _points.end(); ++p) {
+
+               const Coord dx = point.x - (*p).x;
+               const Coord dy = point.y - (*p).y;
+               const Coord dx2 = dx * dx;
+               const Coord dy2 = dy * dy;
+
+               if ((dx2 < 2.0 && dy2 < 2.0) || (dx2 + dy2 < 4.0)) {
+                       return true;
+               }
+       }
+
        return false;
 }