Snap Point to pixel / grid


#1

I was doing some drawing using paths in which I found myself needing to add some short straight lines, I didn’t want to make several paths and switch between using drawHorizontalLine() on the Graphics context and I found that using path was sometimes causing the lines to draw on pixel boundaries. I found myself wanting to call a method on the Point object in order to have it snap to a specific pixel, therefore I implemented the following and thought it might be a useful addition to the Point class.

/** Snaps this point to the grid to ensure that the point does not appear between pixel boundaries */
Point<FloatType> snapToGrid() const noexcept
{
    return Point<FloatType> (std::floor (x) + static_cast<FloatType> (0.5),
                             std::floor (y) + static_cast<FloatType> (0.5));
}

I used FloatType rather than ValueType as the method only really makes sense when using some kind of float type, and it allows a user to call myPoint.snapTogrid() on a Point<int> and have it passed to a method that takes a Point<float> without accidently having that point be interpreted as being on a pixel boundary. Also it prevents a user making the mistake of calling myPoint.snapTogrid().toFloat() when they actually meant myPoint.toFloat().snapToGrid().

Another point to make, it may seem a mistake to use floor, but I believe this makes the most sense not only does this prevent an issue in which calling the method several times might add 1 each time if you rounded the number, but also for every fractional point > 0 && < 1 it will be closer to the pixel that lies on the boundary at 0.5 (hopefully that makes sense), and we always want 0, 0, to be 0.5, 0.5 if i’m not mistaken.


#2

The problem with this is that depending on the scaling of the monitor, your code may not actually snap to a pixel. To do this properly, I think snapToGrid would need to have access to the Window it is drawing to in order to figure out the scaling factor.


#3

Ah yes I see your point (no pun intended)! hmmm that’s annoying.


#4

OK I done a bunch of testing using HiDPI mode for testing retina on Mac and using setGlobalScaleFactor() and it seems pretty much perfect when compared with drawHorizontalLine() so it still seems useful to me. Is there any other tests worth doing?


#5

Yes. If the scaling factor is an integer number (as it always is on mac). On Windows it can be something like 1.7. In any case, we could just rename and modify the doc in your method.


#6

The Graphics object provides the best info about the physical pixel size, but beware! You could be drawing with a transform other than a simple proportional scale - it could be stretched, skewed, rotated, in which case the concept of pixel-alignment is meaningless!


#7

Ah yes also a good point! However the purpose for me was to have any straight lines in a Path not appear between boundaries in exactly the same way as drawHorizontalLine() does (which I realise just draws a rectangle), which would also be subject to the same conditions of being stretched, skewed, and/or rotated. You could argue it’s no longer a horizontal line because if the Graphics object has been stretched, skewed, and/or rotated, but I don’t think that detracts from the method - in the same way that the Point would be snapped to a grid it’s just that the grid it was snapped to has been stretched, skewed, and/or rotated. I recognise it’s not exactly the same as drawHorizontalLine() is in the Graphics object and snapToGrid() isn’t, but I think you get the point.

From my point of view it was nicer to have a single path and do lineTo(), cubicTo, lineTo, etc. rather than using the Graphics object for the lines and then making multiple paths for the beziers.

I was creating draw routines for several similar things and found I was repeating myself in order to prevent lines appearing between pixel boundaries (in the most basic of cases), I initially made a static function for transforming my Point for me but in this case it just seemed a much more elegant solution to add it to the Point class.

However I share your reservations and therefore maybe the naming and comment are too misleading, at the very least a warning/caution message should be added to the comment.

I’ll leave it upto yourselfs to decide if this is worth adding to the framework or not. Given that I have no evidence of anyone else asking for it, I suspect it wouldn’t be immensely useful, and if it’s too misleading then maybe it could cause more problems than it solves?


#8

I would have thought that a free standing function would be a better place for this, there’s just too many possibilities to make it a member function. Apart from all the scaling and transform issues discussed, snapToGrid also implies the notion of a ‘grid’. Surely this concept doesn’t really fit in the Point class?

What if you’re writing some kind of graphics editor application and your grid is larger than 1px?
Wouldn’t it be better to write a function that can take a grid size and snap to that?

You could also argue that as this isn’t mutating the Point it should be a free function, even if added to JUCE, in a similar way to STL. I understand the syntax is slightly different though (and eagerly await the day for unified call syntax).


#9

+1 to Dave’s comment.

I guess there’s an argument for a more generic snapping method in Point, which would snap the X and Y to a user-specified grid of any X or Y scale, not just 1x1. But I’m not sure really how often that’d be useful.


#10

OK thanks all, I’ll stick this back in a free function.