X-Git-Url: https://main.carlh.net/gitweb/?a=blobdiff_plain;f=libs%2Fcanvas%2Fcurve.cc;h=280a3e3aaaf7688a04d41f0339ddf848bb3caca3;hb=0a3aa9c324e4292cfac4994849ed4dc5c2dbc035;hp=de988ee4b255bba524f85a28951ca585b07d83c0;hpb=a882e96db1367c26660fd3d3079e9e3e19b1e149;p=ardour.git diff --git a/libs/canvas/curve.cc b/libs/canvas/curve.cc index de988ee4b2..280a3e3aaa 100644 --- a/libs/canvas/curve.cc +++ b/libs/canvas/curve.cc @@ -17,6 +17,7 @@ */ +#include #include #include @@ -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,184 +65,184 @@ 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 context) const +Curve::interpolate () { - if (_outline) { - setup_outline_context (context); - render_path (area, context); - context->stroke (); - } + samples.clear (); + InterpolatedCurve::interpolate (_points, points_per_segment, CatmullRomCentripetal, false, samples); + n_samples = samples.size(); } -void -Curve::render_path (Rect const & area, Cairo::RefPtr context) const -{ - PolyItem::render_curve (area, context, first_control_points, second_control_points); -} - -void -Curve::compute_control_points (Points const& knots, - Points& firstControlPoints, - Points& secondControlPoints) +void +Curve::render (Rect const & area, Cairo::RefPtr context) const { - Points::size_type n = knots.size() - 1; - - if (n < 1) { + if (!_outline || _points.size() < 2 || !_bounding_box) { 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); - - return; - } - - // Calculate first Bezier control points - // Right hand side vector - - std::vector 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 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 const & rhs) -{ - std::vector::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::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::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 & pc) const { - Duple point = canvas_to_item (pc); + Duple point = window_to_item (pc); - /* XXX Hellaciously expensive ... */ + /* O(N) N = number of points, and not accurate */ for (Points::const_iterator p = _points.begin(); p != _points.end(); ++p) {