Path::getTransformToScaleToFit() from normalized path oddities


#1

I’ve been experimenting with paths and being able to design a path at a fixed size and convert it to any size, as per this thread: Scaling a path problem

I’ve run across something that I don’t quite understand why a rounded rect is not smoothly round (zoomed 400%):

Here’s the code to create it, which is easily tested via ProJucer especially when the window is scaled.:

Path PathStrokeHelper(Path& mPath, PathStrokeType pst)
{
    Path outline;
    //after applying the stroke you want, convert that stroked path into an outline
    pst.createStrokedPath(outline, mPath);
    //scale that outline so it is normalized (see ArrowButton class for why using normalization is smart)
    outline.applyTransform(outline.getTransformToScaleToFit(0, 0, 1, 1, false));
    return outline;
}
class TestPath : public Component
{
public:
    TestPath(){ setSize(60,30); }
    void paint(Graphics& g) override {
        Path border;
        border.addRoundedRectangle(0,0,30,30, 4, 4);
 //stroke border, normalize the resulting path.
        Path borderOutline = PathStrokeHelper(border, PathStrokeType(2));
 //scale the result from PathStrokeHelper back up to our original 30x30 dimensions.
        borderOutline.applyTransform(borderOutline.getTransformToScaleToFit(getLocalBounds().toFloat().withWidth(getWidth()/2), false));
        g.setColour(Colours::black);
        g.fillPath(borderOutline);
//draw a regular rounded rect via a Path for comparison
        border.applyTransform(border.getTransformToScaleToFit(getLocalBounds().toFloat().withWidth(getWidth()/2).withX(getWidth()/2).reduced(1), false));
        g.strokePath(border, PathStrokeType(2));
    }
};

here’s when you resize the component.

Any ideas what’s happening in the Path class that normalizing a path with cubic curves causes resolution of the curve to disappear when the path is scaled? I’m talking about how the curves for the rectangle on the left have become lineTo’s.


#2

ok, I dug a bit further, and it seems that the PathStrokeHelpers::createStroke() method is the culprit. It converts cubics and quadratics into line segments via the PathFlatteningIterator instead of maintaining them. @jules @fabian @jb1 any ideas for how to get around this?

/**
    Flattens a Path object into a series of straight-line sections.

    Use one of these to iterate through a Path object, and it will convert
    all the curves into line sections so it's easy to render or perform
    geometric operations on.

    @see Path
*/
class JUCE_API  PathFlatteningIterator
{
    static void createStroke (const float thickness, const PathStrokeType::JointStyle jointStyle,
                              const PathStrokeType::EndCapStyle endStyle,
                              Path& destPath, const Path& source,
                              const AffineTransform& transform,
                              const float extraAccuracy, const Arrowhead* const arrowhead)
    {
        //...

        // Iterate the path, creating a list of the
        // left/right-hand lines along either side of it...
        PathFlatteningIterator it (*sourcePath, transform, Path::defaultToleranceForMeasurement / extraAccuracy);
void PathStrokeType::createStrokedPath (Path& destPath, const Path& sourcePath,
                                        const AffineTransform& transform, const float extraAccuracy) const
{
    PathStrokeHelpers::createStroke (thickness, jointStyle, endStyle, destPath, sourcePath,
                                     transform, extraAccuracy, 0);
}

#3

Well if you create a tiny path and then stroke it, it’ll use a resolution that would be appropriate for drawing it at that size. Try scaling the path up and THEN creating the stroke.


#4

You know how you can select a vector graphic in a Vector Graphic Editor like Adobe Illustrator, and some handles appear on it, and you can drag them to resize the graphic, and the whole thing rescales/resizes proportionally, including all of the line thicknesses? I’m trying to enable that ability programmatically for ProJucer’s component preview, since you can dynamically resize your components. The struggle to make it work for the lineThickness parameter in methods like:

drawRoundedRectangle (Rectangle< float > rectangle, float cornerSize, float lineThickness) const
drawEllipse (Rectangle< float > area, float lineThickness) const
drawDashedLine (const Line< float > &line, const float *dashLengths, int numDashLengths, float lineThickness=1.0f, int dashIndexToStartFrom=0) const
strokePath (const Path &path, const PathStrokeType &strokeType, const AffineTransform &transform=AffineTransform()) const
drawLine(const Line< float > &line, float lineThickness) const

I suppose you could say I’m trying to figure out how to programmatically use Path::getTransformToScaleToFit() to let me emulate that resize/rescale/zoom feature in Projucer’s Live Component Preview, that is found in a lot of vector graphic creation programs.

heh, we need a getTransformToScaleToFit() for the lineThickness parameter :stuck_out_tongue: Now that I’ve figured out how to express what i’m actually trying to accomplish (that’s always half the battle, isn’t it?), any ideas on accomplishing that?

essentially how to do this:

g.drawRoundedRectangle(getLocalBounds().toFloat(), scalingCornerSize(), scalingLineThickness() );

#5

@jules In Adobe Illustrator, there is a preference option called “Scale Strokes and Effects”. This is what i’m trying to achieve, as demonstrated in this 40-second clip comparing Introjucer’s scaling with Illustrators:


#6

ok @daniel is a genius:

//sticking 100.f at the end is like upsampling the result 100x
PathStrokeType(2).createStrokedPath(borderOutline, border, AffineTransform(), 100.0f);
class TestPath : public Component
{
public:
    TestPath(){ setSize(60,30); }
    void paint(Graphics& g) override {
        Path border;
//define your estimated default size in fixed size
        border.addRoundedRectangle(0,0,30,30, 4, 4); 
        Path borderOutline;
//create a stroked version upsampled 100x
        PathStrokeType(2).createStrokedPath(borderOutline, border, AffineTransform(), 100.0f);
//transform to the window's size
        borderOutline.applyTransform(borderOutline.getTransformToScaleToFit(getLocalBounds().toFloat(), false));
        g.setColour(Colours::black);
//draw it
        g.fillPath(borderOutline);
    }
};

#7

There is a setting in PathStrokeType::createStrokedPath (Path & destPath, const Path & sourcePath, const AffineTransform & transform = AffineTransform(), float extraAccuracy = 1.0f) const to add more line segments than the stroke helper thinks to need.

But the better solution is to let Graphics create the stroke the very last moment, so it will pick the actual needed accuracy by itself, just as @jules was hinting…


#8
PathStrokeType(2).createStrokedPath(borderOutline, border, AffineTransform(), 100);

So, instead of upsampling 100x, somewhere between 1-6 looks ok, with 6 being the cleanest. anything bigger than 6 doesn’t look any different on this Retina MacBookPro i’m using:

int x = [some value between 1 and 6]
PathStrokeType(2).createStrokedPath(borderOutline, border, AffineTransform(), x);

So that’s a nice compromise. but it still doesn’t change the fact that we’re doing a hacky work-around for what I actually want to do, which is scale the ‘2’ in PathStrokeType(2)

check out what happens when I modify both the cornerSize/thickness (the x parameter) and the resolution of the Stroked Path:


#9

Like I said in my first reply, and like Daniel also told you: just make your path BIGGER and THEN add the stroke. Why are you persisting in trying to scale it afterwards?


#10

because I don’t know what the stroke thickness should be at the rescaled size.

I know what it should be when the rectangle is 30x30. It shoudl be 2. But when the rectangle is bigger? it shouldn’t be 2. it should be some scaled value. That’s why

Watch the video in my last post, and you’ll understand @jules


#11

You’re the one applying the scaling. How can you not know how much?

I did, and am none the wiser.


#12

Seems like you want to scale the line width together with the rectangles size, so this would be the easiest solution:

g.fillAll (Colours::lightgrey);

Path border;
float w = getWidth();
float h = getHeight();
float diagonal = sqrt (w * w + h * h);
float line  = diagonal * 0.05f; // arbitrary factor relative to the diagonal
float corner = diagonal * 0.1f;
border.addRoundedRectangle (line * 0.5, line * 0.5, w - line, h - line, corner, corner);

g.setColour (Colours::white);
g.fillPath(border);
g.setColour (Colours::black);
g.strokePath(border, PathStrokeType (line));

Sorry, didn’t get that in the first place…

HTH


#13

as I said, i know how I want things to look when it is 30x30. the stroke of 2 looks right. But if I resize the component (as i test in the vid with ProJucer), the stroke stays at 2 if I am simply going:

g.strokePath(path, PathStrokeType(2));

No matter how large or small I scale it in projucer, the stroke will ALWAYS BE 2.
So, basically, emulating the “Scale Strokes and Effects” parameter in Adobe Illustrator.

You could think of it like this:

sourceRectangle = {30x30, cornerSize=4, lineThickness=2};
g.drawRoundedRectangle(getLocalBounds(), 
             using sourceRectangle dimensions and params but scaled so it looks good at getLocalBounds() dimensions)

Actually, this has led to a request to improve ProJucer’s zoom feature @jules
I broke it all down here:


#14

If you want to stick to the AffineTransform approach, this could help you:

AffineTransform scale = AffineTransform::scale (5.7, 8.9);
float factor = scale.getScaleFactor();
Path border;
border.addRoundedRectangle (0, 0, 30, 30, 4 *factor, 4 * factor);
g.setColour (Colours::white);
g.fillPath (border, scale);
g.setColour (Colours::black);
g.strokePath (border, PathStrokeType (2 * factor), scale);

but your Projucer request I can’t comment… :wink:


#15

if I replace the contents of paint() in post #6 in this thread with your code @daniel, it doesn’t scale when I resize the component in ProJucer :-/

So, i think that createStrokedPath() is the way to go, or just do all my designing of my paths at massive sizes (like 600x600) and scale them down to the small final size (30x30). I had hoped to go the other way around. Zoom in via component resizing, while still designing the paths at the smaller size


#16

I figured a use-case would clarify for @jules and the other folks that developed the ProJucer’s Live Component Preview what I was trying to express with this thread:

Hopefully what I’m discussing in the vid will become a feature!


#17

If you just want to preview the component bigger, surely it’s better to just call setTransform (AffineTransform::scale (x)) on the component rather than modifying the actual stuff that it’s painting?

(And if this is just for previewing/tweaking, does it even matter whether the curves are smooth, since you’ll never actually see them at that scale…?)