openFrameworks icon indicating copy to clipboard operation
openFrameworks copied to clipboard

ofPolyline curveTo - The first point is off from vertex

Open kaua-melo opened this issue 3 years ago • 9 comments

Hi,

I just noticed that ofPolyline draws the first point slightly off from the position I set.

Screenshot 2022-02-02 at 15 44 11

The first point of the line isn’t in the center of the red circle P2, which is where I set the second vertex of the ofPolyline. It workds perfectly with the last point, which falls exactly in the middle of the third vertex (P3).

Here’s the code to reproduce that (everything inside draw() just to simplify testing):

void ofApp::draw()
{
  ofBackground(255);
  ofSetLineWidth(1);

  ofPoint p1(300, 100);
  ofPoint p2(450, 100);
  ofPoint p3(600, 100);
  ofPoint p4(750, 100);

  ofPolyline line;
  line.curveTo(p1);
  line.curveTo(p2);
  line.curveTo(p3);
  line.curveTo(p4);

  // Draw line
  ofSetColor(0);
  line.draw();

  // Draw Circles
  ofNoFill();
  ofSetColor(255, 0, 0);
  ofCircle(p1.x, p1.y, 5);
  ofCircle(p2.x, p2.y, 5);
  ofCircle(p3.x, p3.y, 5);
  ofCircle(p4.x, p4.y, 5);
}

I asked about that on OF forum: https://forum.openframeworks.cc/t/ofpolyline-curveto-the-first-point-is-off-from-vertex/39256

And Zach mentioned this:

good question. I took a look and believe this may be a bug in ofPolyline.inl (curveTo)

for (int i = 1; i <= curveResolution; i++){

should be (I think)

for (int i = 0; i <= curveResolution; i++){

can you open an issue on GitHub ? I think the fix is easy but it’s worth looking at a little longer (to see why that for loop starts from 1 and not 0

kaua-melo avatar Feb 02 '22 15:02 kaua-melo

I don't think this is a bug. I remember seeing this long ago and I figured out why, but cant recall any details. I will inspect and report back.

roymacdonald avatar Feb 03 '22 06:02 roymacdonald

thanks roy! one thing I was thinking about is that it's maybe not a bug if you've added points already to the polyline and you want to append a curve. in that case, skipping the first point might make sense to prevent you from duplicating that point. ie,

line.addVertex(p2 - ofPoint(0,100));
line.addVertex(p2);
// appending a curve 
line.curveTo(p1);
line.curveTo(p2);
line.curveTo(p3);
line.curveTo(p4);

ofZach avatar Feb 03 '22 10:02 ofZach

@ofZach right. it was something like that which I remember. Like having to put explicitly the first vertex of the curve. So far we have had the ofPolyline working like this for quite a while, so just simply changing the starting index in that for loop will definitively break things. You can notice that the same applies to bezier curves. I will check how is that this exactly works and add that to the documentation sounds like a better thing to do.

roymacdonald avatar Feb 03 '22 21:02 roymacdonald

@roymacdonald but how would someone, with the current ofPolyline.inl version, be able to draw this:

ofPolyline2

Instead of getting something like this?

ofPolyline

when using curveTo() ?

kaua-melo avatar Feb 04 '22 12:02 kaua-melo

would this work

line.addVertex(p2);
// appending a curve 
line.curveTo(p1);
line.curveTo(p2);
line.curveTo(p3);
line.curveTo(p4);

I think it's almost a kind of semantic question of if "curveTo" includes or doesn't include the starting point. I think the idea is that you'd be building a shape out of curve commands so you don't need to include the start. but I can see how it breaks when you use it just on it's own.

ofZach avatar Feb 04 '22 13:02 ofZach

Hi @kaua-melo Try the following code ofApp.h

#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{

	public:

		void draw();

		void keyReleased(int key);
		void mouseDragged(int x, int y, int button);
		void mousePressed(int x, int y, int button);
		void mouseReleased(int x, int y, int button);

		void mouseExited(int x, int y);
		
    bool bDragging = false;
    glm::vec3 pressOffset;
    
    vector<glm::vec3> p = {{100,100,0}, {200,100,0}, {300, 100,0}, {450, 100,0}, {600, 100,0}, {750, 100,0}, {800, 100,0} };
    size_t draggingIndex = 0;

    bool bUseBezier  = false;
    
};

ofApp.cpp

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::draw(){
    ofBackground(255);
    ofSetLineWidth(1);
    
    
    ofPolyline line;
    
    if(bUseBezier){
        //we first need to add where the line begins.
        line.addVertex(p[0]);
        line.bezierTo(p[1], p[2], p[3]);
        // no neeed to add a vertex before this call to bezierTo because it will use the last point in the previous call.
        line.bezierTo(p[4], p[5], p[6]);
        
        
        
    }else{
        line.addVertex(p[1]);
        for(auto& pt: p){
            line.curveTo(pt);
        }
    }
    
    // Draw line
    ofPushStyle();
    ofSetLineWidth(1);
    if(bUseBezier){
        ofSetColor(120);
        //these are just to visualize better which are the bezier control points
        ofDrawLine(p[0], p[1]);
        ofDrawLine(p[2], p[3]);
        ofDrawLine(p[3], p[4]);
        ofDrawLine(p[5], p[6]);
    }

    
    ofSetLineWidth(2);
    ofSetColor(0);
    line.draw();
    ofPopStyle();


    ofNoFill();
    
    
    //draw circles with their index
    for(size_t i = 0; i < p.size(); i++){
        ofSetColor(0);
        ofDrawBitmapString(ofToString(i), p[i].x - 5, p[i].y + 15);
        ofSetColor(255, 0, 0);
        ofDrawCircle(p[i].x, p[i].y, 5);
    }
    
    
    // just some onscreen info
    stringstream ss;
    ss << "press the space bar to toggle between bezier and curve" <<endl;
    ss << "Current mode: " << (bUseBezier?"bezier":"curve");
    ofDrawBitmapStringHighlight(ss.str(), 30, 30);
    
}


//--------------------------------------------------------------
void ofApp::keyReleased(int key){
    if(key == ' '){
        bUseBezier  ^= true;
    }
}


//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button){
    if(bDragging){
        p[draggingIndex] = glm::vec3(x, y, 0) - pressOffset;
    }
}

//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button){
    glm::vec3 m(x, y, 0);
    for(size_t i = 0; i < p.size(); i++){
        if(glm::distance(m, p[i])<= 5 ){
            draggingIndex = i;
            pressOffset = m-p[i];
            bDragging = true;
            break;
        }
        //           draggingIndex
    }
    //pressPoint
}

//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button){
    bDragging = false;
}

//--------------------------------------------------------------
void ofApp::mouseExited(int x, int y){
    bDragging = false;
}

So, the way curveTo works is that is an "auto" curve. Thus it creates the curve automatically without having to specify anything else than some points where you want it to pass through, but for it to work and draw a curve between 2 points it needs to have points before and after it, which are also defined with curveTo. Thus a minimum of 4 calls to curveTo need to be done to be able to draw a curve, then any additional call is just appended and a new section of the curve is drawn. This is why you have to pass the second point to the addVertex function in the following code

line.addVertex(p2);
// appending a curve 
line.curveTo(p1);
line.curveTo(p2);
line.curveTo(p3);
line.curveTo(p4);

I think that all this makes more sense when used in the bezierTo I feel.

Hope this helps

roymacdonald avatar Feb 04 '22 17:02 roymacdonald

@roymacdonald thanks for the explanation!

@ofZach , yes that works too :)

line.addVertex(p2);
// appending a curve 
line.curveTo(p1);
line.curveTo(p2);
line.curveTo(p3);
line.curveTo(p4);

I've used ofPolyline several times and never noticed any issue. But now I'm exploring an idea where I need to be very precise on the starting/ending tips of a curveTo and I couldn't understand why that was happening.

As @roymacdonald mentioned, if changing the for (int i = 1; i <= curveResolution; i++) on ofPolyline.inl would probably break other things, maybe the best thing to do would be to add a simple line.addVertex(p2); to the curveTo documentation page: https://openframeworks.cc/documentation/graphics/ofPolyline/#show_curveTo

For example here:

We end up with a line that starts at v0, heads to v1, then heads to v2 and finally ends at v3. But if we had instead done:

p.addVertex(v1); // We need to add the starting point of the curve (v1) when using only curveTo otherwise the first point will be drawn slightly off v1.

// Appending curve:
p.curveTo(v0); 
p.curveTo(v1); 
p.curveTo(v2); 
p.curveTo(v3);

kaua-melo avatar Feb 07 '22 11:02 kaua-melo

Hi, Yes, simply adding it to the documentation would be the way to go, I think. @kaua-melo can you add this bit to the documentation? you can do so here

roymacdonald avatar Feb 08 '22 01:02 roymacdonald

Hi @roymacdonald

Sure, I just added it here: https://github.com/openframeworks/ofSite/compare/master...kaua-melo:patch-1

kaua-melo avatar Feb 08 '22 07:02 kaua-melo