Incorrect rendering of stroked path


#1

Jules…don’t hate me, lol! Here’s the output of the sample program:

[attachment=0]stroke.png[/attachment]

And here is the code that generates it

#include "juce.h"

struct Panel : Component
{
  ~Panel() { deleteAllChildren(); }
  void paint (Graphics& g)
  {
    g.setColour( Colours::grey );
    g.fillAll();

    int x=getWidth()/2;
    int top=getHeight()/2-20;
    int bottom=top+40;
    Point<float> pt[6];
    pt[0].setXY( x+1.5, top+17.5 );
    pt[1].setXY( x+8.5, top+1.5 );

    // these next two points form the messed up segment of the stroke
    pt[2].setXY( x+8.5, top+0.5 );
    pt[3].setXY( x-7.5, top+0.5 );

    pt[4].setXY( x-7.5, top+1.5 );
    pt[5].setXY( x-0.5, top+17.5 );
    Path p;
    p.startNewSubPath (pt[0].getX(), pt[0].getY());
    for( int i=1; i<6; i++ )
      p.lineTo (pt[i].getX(), pt[i].getY());
    p.closeSubPath();
    g.setColour (Colours::white);
    g.fillPath (p);
    g.fillPath (p,AffineTransform::translation(-24,0));
    g.setColour (Colours::black);
    g.strokePath (p, 1);
    g.strokePath (p, 1, AffineTransform::translation(24,0));
  }
};

struct MainWindow  : DocumentWindow, Button::Listener
{
  MainWindow()
  : DocumentWindow (JUCE_T("Test")
  , Colours::black
  , DocumentWindow::allButtons
  , true )
  {
    Panel* p = new Panel;
    p->setSize( 512, 384 );
    setContentComponent (p, true, true);
    centreWithSize (getWidth(), getHeight());
    setVisible( true );
  }
  ~MainWindow() {}

  void buttonClicked (Button* button)
  {
    Component* c = getContentComponent()->getChildComponent(1);
    c->setVisible (true);
    c->setTopLeftPosition (64, 64);
  }

  void closeButtonPressed() { JUCEApplication::quit(); }
};

struct MainApp : JUCEApplication
{
  MainApp() : mainWindow(0) { s_app=this; }
  ~MainApp() { s_app=0; }
  static MainApp& GetInstance() { return *s_app; }
  const String getApplicationName() { return JUCE_T("JuceTest"); }
  const String getApplicationVersion() { return JUCE_T("0.1.0"); }
  bool moreThanOneInstanceAllowed() { return true; }
  void anotherInstanceStarted (const String& commandLine) {}

  void initialise (const String& commandLine)
  {
    mainWindow = new MainWindow;
  }

  void shutdown()
  {
    delete mainWindow;
  }

  static MainApp* s_app;
  MainWindow* mainWindow;
};

MainApp* MainApp::s_app = 0;

START_JUCE_APPLICATION (MainApp)

#2

It’s the same thing you mentioned in a post a while back about a rounded rectangle - the stroke algorithm is producing some rounding errors as it subdivides the curves. You could try playing with the extraAccuracy parameter in the PathStrokeType::createStrokedPath methods.


#3

…actually, no it’s not. What’s happening is that the Path class is just optimising-out the very short line between your pt[1] and pt[2] when it’s shorter than 1 pixel. I’ll have a look at that, I think it’s probably something that need tweaking…


#4

Whoa…optimizing out some line segments? That sounds like a bad idea with good intentions. What if I scale the Path up using a transform?

Actually, I’m looking at juce_Path.cpp and I don’t see any of that so I guess you’re talking about the path stroker?

I took a look at the stroker and I’m sorry but the math is beyond me at the present time.


#5

Yes, the stroking code uses a limit to avoid numerical errors with very small values.

I need to take a look at the path flattening code anyway, now that the Graphics class may be drawing them with an arbitrary transformation, but in the meantime you can use the extraAccuracy value to fix it in your case.


#6

Alright, well extraAccuracy==1.4142136 solved the problem (note that 1.4142135 is not enough). Here is my utility function:

static void strokePath (Graphics& g,
                        Path& path,
                        const PathStrokeType& strokeType,
                        const AffineTransform& transform = AffineTransform::identity)
{
  Path stroke;
  strokeType.createStrokedPath (stroke, path, transform, 1.4142136);
  g.fillPath (stroke);
}

and here is the example program modified to use it:

#include "juce.h"

static void strokePath (Graphics& g,
                        Path& path,
                        const PathStrokeType& strokeType,
                        const AffineTransform& transform = AffineTransform::identity)
{
  Path stroke;
  strokeType.createStrokedPath (stroke, path, transform, 1.4142136);
  g.fillPath (stroke);
}

struct Panel : Component
{
  ~Panel() { deleteAllChildren(); }
  void paint (Graphics& g)
  {
    g.setColour( Colours::grey );
    g.fillAll();

    int x=getWidth()/2;
    int top=getHeight()/2-20;
    int bottom=top+40;
    Point<float> pt[6];
    pt[0].setXY( x+1.5, top+17.5 );
    pt[1].setXY( x+8.5, top+1.5 );

    // these next two points form the messed up segment of the stroke
    pt[2].setXY( x+8.5, top+0.5 );
    pt[3].setXY( x-7.5, top+0.5 );

    pt[4].setXY( x-7.5, top+1.5 );
    pt[5].setXY( x-0.5, top+17.5 );
    Path p;
    p.startNewSubPath (pt[0].getX(), pt[0].getY());
    for( int i=1; i<6; i++ )
      p.lineTo (pt[i].getX(), pt[i].getY());
    p.closeSubPath();
    g.setColour (Colours::white);
    g.fillPath (p);
    g.fillPath (p,AffineTransform::translation(-24,0));
    g.setColour (Colours::black);
    strokePath (g, p, 1);
    strokePath (g, p, 1, AffineTransform::translation(24,0));
  }
};

struct MainWindow  : DocumentWindow, Button::Listener
{
  MainWindow()
  : DocumentWindow (JUCE_T("Test")
  , Colours::black
  , DocumentWindow::allButtons
  , true )
  {
    Panel* p = new Panel;
    p->setSize( 512, 384 );
    setContentComponent (p, true, true);
    centreWithSize (getWidth(), getHeight());
    setVisible( true );
  }
  ~MainWindow() {}

  void buttonClicked (Button* button)
  {
    Component* c = getContentComponent()->getChildComponent(1);
    c->setVisible (true);
    c->setTopLeftPosition (64, 64);
  }

  void closeButtonPressed() { JUCEApplication::quit(); }
};

struct MainApp : JUCEApplication
{
  MainApp() : mainWindow(0) { s_app=this; }
  ~MainApp() { s_app=0; }
  static MainApp& GetInstance() { return *s_app; }
  const String getApplicationName() { return JUCE_T("JuceTest"); }
  const String getApplicationVersion() { return JUCE_T("0.1.0"); }
  bool moreThanOneInstanceAllowed() { return true; }
  void anotherInstanceStarted (const String& commandLine) {}

  void initialise (const String& commandLine)
  {
    mainWindow = new MainWindow;
  }

  void shutdown()
  {
    delete mainWindow;
  }

  static MainApp* s_app;
  MainWindow* mainWindow;
};

MainApp* MainApp::s_app = 0;

START_JUCE_APPLICATION (MainApp)

#7

This issue is resolved in the latest tip. Thanks!