FIX: Bezier curves: replace previous algorithm to convert the curve to segments. In some cases it was not working well. Opengl was using an other algo (using the curve properties to create segments) Now only the OpenGL algo is used, with optimization to reduce the number of segments.

This commit is contained in:
jean-pierre charras 2019-11-09 11:14:20 +01:00
parent c4023637df
commit 943858fd67
5 changed files with 71 additions and 454 deletions

View File

@ -1,8 +1,7 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2014 Jean-Pierre Charras, jp.charras at wanadoo.fr
* Copyright (C) 2014-2017 KiCad Developers, see CHANGELOG.TXT for contributors.
* Copyright (C) 2014-2019 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -26,362 +25,69 @@
/* routines to handle bezier curves */
/************************************/
#include <fctsys.h>
#include <bezier_curves.h>
static inline double calc_sq_distance( int x1, int y1, int x2, int y2 )
BEZIER_POLY::BEZIER_POLY( const std::vector<wxPoint>& aControlPoints )
{
int dx = x2 - x1;
int dy = y2 - y1;
for( unsigned ii = 0; ii < aControlPoints.size(); ++ii )
m_ctrlPts.emplace_back( VECTOR2D( aControlPoints[ii] ) );
return (double)dx * dx + (double)dy * dy;
}
static inline double sqrt_len( int dx, int dy )
{
return ((double)dx * dx) + ((double)dy * dy);
m_minSegLen = 0.0;
}
void BEZIER_POLY::GetPoly( std::vector<wxPoint>& aOutput, int aMinSegLen )
{
wxCHECK( !m_ctrlPts.empty(), /* void */ );
m_minSegLen = std::max( 1, aMinSegLen );
m_output = &aOutput;
m_output->clear();
m_output->push_back( wxPoint( m_ctrlPts.front() ) );
aOutput.clear();
std::vector<VECTOR2D> buffer;
GetPoly( buffer, double( aMinSegLen ) );
// Only quadratic and cubic Bezier curves are handled
if( m_ctrlPts.size() == 3 )
recursiveBezier( m_ctrlPts[0].x, m_ctrlPts[0].y,
m_ctrlPts[1].x, m_ctrlPts[1].y,
m_ctrlPts[2].x, m_ctrlPts[2].y, 0 );
else if( m_ctrlPts.size() == 4 )
recursiveBezier( m_ctrlPts[0].x, m_ctrlPts[0].y,
m_ctrlPts[1].x, m_ctrlPts[1].y,
m_ctrlPts[2].x, m_ctrlPts[2].y,
m_ctrlPts[3].x, m_ctrlPts[3].y, 0 );
m_output->push_back( wxPoint( m_ctrlPts.back() ) );
for( unsigned ii = 0; ii < buffer.size(); ++ii )
aOutput.emplace_back( wxPoint( int( buffer[ii].x ), int( buffer[ii].y ) ) );
}
void BEZIER_POLY::recursiveBezier( int x1, int y1, int x2, int y2,
int x3, int y3, unsigned int level )
void BEZIER_POLY::GetPoly( std::vector<VECTOR2D>& aOutput, double aMinSegLen )
{
if( level > recursion_limit )
return;
wxASSERT( m_ctrlPts.size() == 4 );
// FIXME Brute force method, use a better (recursive?) algorithm
// with a max error value.
// to optimize the number of segments
#define CURVE_POINTS 32
double dt = 1.0 / CURVE_POINTS;
// Calculate all the mid-points of the line segments
//----------------------
int x12 = (x1 + x2) / 2;
int y12 = (y1 + y2) / 2;
int x23 = (x2 + x3) / 2;
int y23 = (y2 + y3) / 2;
int x123 = (x12 + x23) / 2;
int y123 = (y12 + y23) / 2;
aOutput.clear();
aOutput.push_back( m_ctrlPts[0] );
int dx = x3 - x1;
int dy = y3 - y1;
double d = fabs( ((double) (x2 - x3) * dy) - ((double) (y2 - y3) * dx ) );
double da;
// If the Bezier curve is degenerated (straight line), skip that:
bool degenerated = m_ctrlPts[0] == m_ctrlPts[1] && m_ctrlPts[2] == m_ctrlPts[3];
if( d > curve_collinearity_epsilon )
if( !degenerated )
{
// Regular case
//-----------------
if( d * d <= distance_tolerance_square * (dx * dx + dy * dy) )
for( int ii = 1; ii < CURVE_POINTS; ii++ )
{
// If the curvature doesn't exceed the distance_tolerance value
// we tend to finish subdivisions.
//----------------------
if( angle_tolerance < curve_angle_tolerance_epsilon )
{
addSegment( wxPoint( x123, y123 ) );
return;
}
double t = dt * ii;
double omt = 1.0 - t;
double omt2 = omt * omt;
double omt3 = omt * omt2;
double t2 = t * t;
double t3 = t * t2;
// Angle & Cusp Condition
//----------------------
da = fabs( atan2( (double) ( y3 - y2 ), (double) ( x3 - x2 ) ) -
atan2( (double) ( y2 - y1 ), (double) ( x2 - x1 ) ) );
if( da >=M_PI )
da = 2 * M_PI - da;
VECTOR2D vertex = omt3 * m_ctrlPts[0]
+ 3.0 * t * omt2 * m_ctrlPts[1]
+ 3.0 * t2 * omt * m_ctrlPts[2]
+ t3 * m_ctrlPts[3];
if( da < angle_tolerance )
{
// Finally we can stop the recursion
//----------------------
addSegment( wxPoint( x123, y123 ) );
return;
}
}
}
else
{
// Collinear case
//------------------
da = sqrt_len(dx, dy);
if( da == 0 )
{
d = calc_sq_distance( x1, y1, x2, y2 );
}
else
{
d = ( (double)(x2 - x1) * dx + (double)(y2 - y1) * dy ) / da;
if( d > 0 && d < 1 )
{
// Simple collinear case, 1---2---3
// We can leave just two endpoints
return;
}
if( d <= 0 )
d = calc_sq_distance( x2, y2, x1, y1 );
else if( d >= 1 )
d = calc_sq_distance( x2, y2, x3, y3 );
else
d = calc_sq_distance( x2, y2, x1 + (int) d * dx,
y1 + (int) d * dy );
}
if( d < distance_tolerance_square )
{
addSegment( wxPoint( x2, y2 ) );
return;
// a minimal filter on the lenght of the segment being created:
// The offset from last point:
VECTOR2D delta = vertex - aOutput.back();
double dist = delta.EuclideanNorm();
if( dist > aMinSegLen )
aOutput.push_back( vertex );
}
}
// Continue subdivision
//----------------------
recursiveBezier( x1, y1, x12, y12, x123, y123, level + 1 );
recursiveBezier( x123, y123, x23, y23, x3, y3, level + 1 );
}
void BEZIER_POLY::recursiveBezier( int x1, int y1, int x2, int y2,
int x3, int y3, int x4, int y4, unsigned int level )
{
if( level > recursion_limit )
return;
// Calculate all the mid-points of the line segments
//----------------------
int x12 = (x1 + x2) / 2;
int y12 = (y1 + y2) / 2;
int x23 = (x2 + x3) / 2;
int y23 = (y2 + y3) / 2;
int x34 = (x3 + x4) / 2;
int y34 = (y3 + y4) / 2;
int x123 = (x12 + x23) / 2;
int y123 = (y12 + y23) / 2;
int x234 = (x23 + x34) / 2;
int y234 = (y23 + y34) / 2;
int x1234 = (x123 + x234) / 2;
int y1234 = (y123 + y234) / 2;
// Try to approximate the full cubic curve by a single straight line
//------------------
int dx = x4 - x1;
int dy = y4 - y1;
double d2 = fabs( (double) ( (x2 - x4) * dy - (y2 - y4) * dx ) );
double d3 = fabs( (double) ( (x3 - x4) * dy - (y3 - y4) * dx ) );
double da1, da2, k;
switch( (int(d2 > curve_collinearity_epsilon) << 1) +
int(d3 > curve_collinearity_epsilon) )
{
case 0:
// All collinear OR p1==p4
//----------------------
k = dx * dx + dy * dy;
if( k == 0 )
{
d2 = calc_sq_distance( x1, y1, x2, y2 );
d3 = calc_sq_distance( x4, y4, x3, y3 );
}
else
{
k = 1 / k;
da1 = x2 - x1;
da2 = y2 - y1;
d2 = k * (da1 * dx + da2 * dy);
da1 = x3 - x1;
da2 = y3 - y1;
d3 = k * (da1 * dx + da2 * dy);
if( d2 > 0 && d2 < 1 && d3 > 0 && d3 < 1 )
{
// Simple collinear case, 1---2---3---4
// We can leave just two endpoints
return;
}
if( d2 <= 0 )
d2 = calc_sq_distance( x2, y2, x1, y1 );
else if( d2 >= 1 )
d2 = calc_sq_distance( x2, y2, x4, y4 );
else
d2 = calc_sq_distance( x2, y2, x1 + (int) d2 * dx,
y1 + (int) d2 * dy );
if( d3 <= 0 )
d3 = calc_sq_distance( x3, y3, x1, y1 );
else if( d3 >= 1 )
d3 = calc_sq_distance( x3, y3, x4, y4 );
else
d3 = calc_sq_distance( x3, y3, x1 + (int) d3 * dx,
y1 + (int) d3 * dy );
}
if( d2 > d3 )
{
if( d2 < distance_tolerance_square )
{
addSegment( wxPoint( x2, y2 ) );
return;
}
}
else
{
if( d3 < distance_tolerance_square )
{
addSegment( wxPoint( x3, y3 ) );
return;
}
}
break;
case 1:
// p1,p2,p4 are collinear, p3 is significant
//----------------------
if( d3 * d3 <= distance_tolerance_square * sqrt_len(dx, dy) )
{
if( angle_tolerance < curve_angle_tolerance_epsilon )
{
addSegment( wxPoint( x23, y23 ) );
return;
}
// Angle Condition
//----------------------
da1 = fabs( atan2( (double) ( y4 - y3 ), (double) ( x4 - x3 ) ) -
atan2( (double) ( y3 - y2 ), (double) ( x3 - x2 ) ) );
if( da1 >= M_PI )
da1 = 2 * M_PI - da1;
if( da1 < angle_tolerance )
{
addSegment( wxPoint( x2, y2 ) );
addSegment( wxPoint( x3, y3 ) );
return;
}
if( cusp_limit != 0.0 )
{
if( da1 > cusp_limit )
{
addSegment( wxPoint( x3, y3 ) );
return;
}
}
}
break;
case 2:
// p1,p3,p4 are collinear, p2 is significant
//----------------------
if( d2 * d2 <= distance_tolerance_square * sqrt_len(dx, dy) )
{
if( angle_tolerance < curve_angle_tolerance_epsilon )
{
addSegment( wxPoint( x23, y23 ) );
return;
}
// Angle Condition
//----------------------
da1 = fabs( atan2( (double) ( y3 - y2 ), (double) ( x3 - x2 ) ) -
atan2( (double) ( y2 - y1 ), (double) ( x2 - x1 ) ) );
if( da1 >= M_PI )
da1 = 2 * M_PI - da1;
if( da1 < angle_tolerance )
{
addSegment( wxPoint( x2, y2 ) );
addSegment( wxPoint( x3, y3 ) );
return;
}
if( cusp_limit != 0.0 )
{
if( da1 > cusp_limit )
{
addSegment( wxPoint( x2, y2 ) );
return;
}
}
}
break;
case 3:
// Regular case
//-----------------
if( (d2 + d3) * (d2 + d3) <= distance_tolerance_square * sqrt_len(dx, dy) )
{
// If the curvature doesn't exceed the distance_tolerance value
// we tend to finish subdivisions.
//----------------------
if( angle_tolerance < curve_angle_tolerance_epsilon )
{
addSegment( wxPoint( x23, y23 ) );
return;
}
// Angle & Cusp Condition
//----------------------
k = atan2( (double) ( y3 - y2 ), (double) ( x3 - x2 ) );
da1 = fabs( k - atan2( (double) ( y2 - y1 ),
(double) ( x2 - x1 ) ) );
da2 = fabs( atan2( (double) ( y4 - y3 ),
(double) ( x4 - x3 ) ) - k );
if( da1 >= M_PI )
da1 = 2 * M_PI - da1;
if( da2 >= M_PI )
da2 = 2 * M_PI - da2;
if( da1 + da2 < angle_tolerance )
{
// Finally we can stop the recursion
//----------------------
addSegment( wxPoint( x23, y23 ) );
return;
}
if( cusp_limit != 0.0 )
{
if( da1 > cusp_limit )
{
addSegment( wxPoint( x2, y2 ) );
return;
}
if( da2 > cusp_limit )
{
addSegment( wxPoint( x3, y3 ) );
return;
}
}
}
break;
}
// Continue subdivision
//----------------------
recursiveBezier( x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1 );
recursiveBezier( x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1 );
if( aOutput.back() != m_ctrlPts[3] )
aOutput.push_back( m_ctrlPts[3] );
}

View File

@ -32,6 +32,7 @@
#include <gl_context_mgr.h>
#include <geometry/shape_poly_set.h>
#include <bitmap_base.h>
#include <bezier_curves.h>
#include <macros.h>
@ -1035,32 +1036,18 @@ void OPENGL_GAL::DrawPolygon( const SHAPE_LINE_CHAIN& aPolygon )
void OPENGL_GAL::DrawCurve( const VECTOR2D& aStartPoint, const VECTOR2D& aControlPointA,
const VECTOR2D& aControlPointB, const VECTOR2D& aEndPoint )
{
// FIXME The drawing quality needs to be improved
// FIXME Perhaps choose a quad/triangle strip instead?
// FIXME Brute force method, use a better (recursive?) algorithm
std::vector<VECTOR2D> output;
std::vector<VECTOR2D> pointCtrl;
std::deque<VECTOR2D> pointList;
pointCtrl.push_back( aStartPoint );
pointCtrl.push_back( aControlPointA );
pointCtrl.push_back( aControlPointB );
pointCtrl.push_back( aEndPoint );
double t = 0.0;
double dt = 1.0 / (double) CURVE_POINTS;
BEZIER_POLY converter( pointCtrl );
converter.GetPoly( output, GetLineWidth() );
for( int i = 0; i <= CURVE_POINTS; i++ )
{
double omt = 1.0 - t;
double omt2 = omt * omt;
double omt3 = omt * omt2;
double t2 = t * t;
double t3 = t * t2;
VECTOR2D vertex = omt3 * aStartPoint + 3.0 * t * omt2 * aControlPointA
+ 3.0 * t2 * omt * aControlPointB + t3 * aEndPoint;
pointList.push_back( vertex );
t += dt;
}
DrawPolyline( pointList );
DrawPolyline( &output[0], output.size() );
}

View File

@ -993,45 +993,16 @@ void ClipAndDrawPoly( EDA_RECT* aClipBox, wxDC* aDC, wxPoint aPoints[], int n )
}
void GRBezier( EDA_RECT* ClipBox,
wxDC* DC,
int x1,
int y1,
int x2,
int y2,
int x3,
int y3,
int width,
COLOR4D Color )
void GRBezier( EDA_RECT* aClipBox, wxDC* aDC,
std::vector<wxPoint>& aPoint,
int aWidth, COLOR4D aColor )
{
std::vector<wxPoint> points;
std::vector<wxPoint> output;
BEZIER_POLY converter( x1, y1, x2, y2, x3, y3 );
converter.GetPoly( points );
BEZIER_POLY converter( aPoint );
converter.GetPoly( output, aWidth );
GRPoly( ClipBox, DC, points.size(), &points[0], false, width, Color, Color );
}
void GRBezier( EDA_RECT* ClipBox,
wxDC* DC,
int x1,
int y1,
int x2,
int y2,
int x3,
int y3,
int x4,
int y4,
int width,
COLOR4D Color )
{
std::vector<wxPoint> points;
BEZIER_POLY converter( x1, y1, x2, y2, x3, y3, x4, y4 );
converter.GetPoly( points );
GRPoly( ClipBox, DC, points.size(), &points[0], false, width, Color, Color );
GRPoly( aClipBox, aDC, output.size(), &output[0], false, aWidth, aColor, aColor );
}

View File

@ -1,8 +1,7 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2009-2014 Jean-Pierre Charras, jp.charras at wanadoo.fr
* Copyright (C) 1992-2017 KiCad Developers, see CHANGELOG.TXT for contributors.
* Copyright (C) 2014-2019 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -27,6 +26,7 @@
#include <vector>
#include <wx/gdicmn.h>
#include <math/vector2d.h>
/**
* Bezier curves to polygon converter.
@ -35,32 +35,12 @@
class BEZIER_POLY
{
public:
/** cubic Bezier curve */
BEZIER_POLY( int x1, int y1, int x2, int y2, int x3, int y3 )
{
m_ctrlPts.emplace_back( x1, y1 );
m_ctrlPts.emplace_back( x2, y2 );
m_ctrlPts.emplace_back( x3, y3 );
m_output = nullptr;
m_minSegLen = 0;
}
BEZIER_POLY( const std::vector<wxPoint>& aControlPoints );
/** Quadratic and cubic Bezier curve */
BEZIER_POLY( int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4 )
{
m_ctrlPts.emplace_back( x1, y1 );
m_ctrlPts.emplace_back( x2, y2 );
m_ctrlPts.emplace_back( x3, y3 );
m_ctrlPts.emplace_back( x4, y4 );
m_output = nullptr;
m_minSegLen = 0;
}
BEZIER_POLY( const std::vector<wxPoint>& aControlPoints )
BEZIER_POLY( const std::vector<VECTOR2D>& aControlPoints )
: m_ctrlPts( aControlPoints )
{
m_output = nullptr;
m_minSegLen = 0;
m_minSegLen = 0.0;
}
/**
@ -71,40 +51,13 @@ public:
* (the last point is always generated)
*/
void GetPoly( std::vector<wxPoint>& aOutput, int aMinSegLen = 0 );
void GetPoly( std::vector<VECTOR2D>& aOutput, double aMinSegLen = 0.0 );
private:
int m_minSegLen;
double m_minSegLen;
///> Control points
std::vector<wxPoint> m_ctrlPts;
///> Pointer to the output vector
std::vector<wxPoint>* m_output;
void addSegment( const wxPoint& aSegment )
{
int seglen = std::abs( m_output->back().x - aSegment.x )
+ std::abs( m_output->back().y - aSegment.y );
// m_minSegLen is always > 0, so never store a 0 len segment
if( seglen >= m_minSegLen )
m_output->push_back( aSegment );
}
void recursiveBezier( int x1, int y1, int x2, int y2, int x3, int y3, unsigned int level );
void recursiveBezier( int x1, int y1, int x2, int y2,
int x3, int y3, int x4, int y4, unsigned int level );
// Conversion parameters
constexpr static double angle_tolerance = 0.0;
constexpr static double cusp_limit = 0.0;
constexpr static unsigned int recursion_limit = 12;
constexpr static double approximation_scale = 0.5; // 1
constexpr static double distance_tolerance_square = ( 0.5 / approximation_scale ) * ( 0.5 / approximation_scale );
constexpr static double curve_collinearity_epsilon = 1e-30;
constexpr static double curve_angle_tolerance_epsilon = 0.0001;
std::vector<VECTOR2D> m_ctrlPts;
};
#endif // BEZIER_CURVES_H

View File

@ -114,10 +114,10 @@ void GRLineTo( EDA_RECT* ClipBox, wxDC* DC,
void GRPoly( EDA_RECT* ClipBox, wxDC* DC, int n, wxPoint Points[], bool Fill,
int width, COLOR4D Color, COLOR4D BgColor );
void GRBezier( EDA_RECT* ClipBox, wxDC* DC, int x1, int y1, int x2, int y2,
int x3, int y3, int width, COLOR4D Color );
void GRBezier( EDA_RECT* ClipBox, wxDC* DC, int x1, int y1, int x2, int y2,
int x3, int y3, int x4, int y4, int width, COLOR4D Color );
/** Draw cubic (4 points: start control1, control2, end) bezier curve
*/
void GRBezier( EDA_RECT* aClipBox, wxDC* aDC, std::vector<wxPoint>& aPoints,
int aWidth, COLOR4D aColor );
/**
* Function GRClosedPoly