Snap To Grid (beta7)
There was a suggestion in the mailing lists and in the bugtracker (#153) that Field should offer some kind of snap-to-grid mode for it's boxes. After all will Field enabling you to write code that can give the layout of code meaning (for example in time lines) surely a little help organizing layout is appropriate. While Field does have a super-secret auto guide system that opportunistically aligns elements with other elements (inspired by programs like the wonderful OmniOutliner — if you missed this, try dragging an element around with caps-lock on — it doesn't have a general guide and grid system.
We all know what a snap-to-grid mode looks like, we all know where this is heading. Is there an Adobe product without some kind of snap to grid / guide "feature"? I can say instantly that we'll need some menu items, some modes, probably a dialog box for setting up the grid, a view menu to put the 'grid on/off' button and so on. All this will need documentation. We'd then be able to stand back and say "Field supports guides and grids".
People are welcome to submit patches to this effect, but in general, we find this approach uninteresting at least, insufficiently interesting to write the code itself. It is not the Field way.
The Field way is to think about what lower-level support Field needs to have to allow you to make your own grid system by writing a little code from within Field. The end result is a grid / guide system that you write for your work that reflects, and enables you to reflect upon, grids and guide in your work. This approach is more complex than a checkbox marked "snap to grid"; and it "costs" Field a clear "feature"; it makes Field more troublesome to lecture about; it makes it harder to write books about and the documentation is longer. Great. But Field isn't an exercise in filling in some hypothetical feature table to compete against Adobe products. Remember live code changes everything, it's up to us to figure out what that means.
Extending the Guide system
The trick is to make extending the guide system that's already there when you hit caps-lock easy in code.
It's probably worth having a couple of boxes on the canvas and dragging them around with the mouse to see what we are aiming for:
This is an image of a box being dragged around (with caps-lock down). Field proposes an alignment that aligns the left edge of the thing being dragged to the right edge of a box already there.
What we're looking for, for a grid, is a way to offer other alignments. Here we go:
gridx = 200 gridy = 200 class anOffer(DynamicAlignment): def score(self, inside, on, original, current, next): an = anAlignment(newRect=Rect(0,0,0,0).setValue(current)) if (gridx): a = an.newRect.x-(an.newRect.x%gridx) b = an.newRect.x-(an.newRect.x%gridx)+gridx an.newRect.x = [a,b][Math.abs(an.newRect.x-a)>Math.abs(an.newRect.x-b)] if (gridy): a = an.newRect.y-(an.newRect.y%gridy) b = an.newRect.y-(an.newRect.y%gridy)+gridy an.newRect.y = [a,b][Math.abs(an.newRect.y-a)>Math.abs(an.newRect.y-b)] an.name = "at %i %i " % (int(an.newRect.x), int(an.newRect.y)) r2 = PLine().rect(an.newRect)(color=Vector4(0.25,0,0,0.25), filled=1,thickness=2) ad = PLine().m(*current.midpoint2()).l(*an.newRect.midpoint2())(color=Vector4(0.25,0,0,0.25), filled=1) ar = PLine(Arrows().getArrowForMiddle(ad.line, 25, 1))(color=Vector4(0.25,0,0,0.25), filled=1) an.toDraw = [r2.line, ad.line, ar.line] an.score=100/(1+Math.abs(current.x-an.newRect.x)); return self.drawableForDict(an)
Let's take this in chunks:
- class anOffer(DynamicAlignment): — we're making a subclass of DynamicAlignment which is the class that Field wants us to use to offer up alignment targets for the guide system.
- an = anAlignment(newRect=Rect(0,0,0,0).setValue(current)) — the score method of DynamicAlignment needs to use an AnAlignment filled in with the things that describe this guide mark. AnAlignment has the following interesting members — score which is how "good" this alignment is (this is used to weigh different possible alignments); name which is used to keep track of which alignments are on offer for a given drag operations; newRect which is the alignment that's actually on offer; toDraw a List of PLines that are to be draw to highlight this possible alignment. The rest of this code fleshes out this structure and returns it.
- if (gridx): — this starts the code that clamps the x (and then y) coordinates of the rectangle current to be on a grid gridx, gridy.
- an.name = — "names" this offered alignment. The name just has to be the same should this code produce the same proposed alignment.
- an.toDraw = [...] — here's the list of PLine's that need to be drawn to help the user (that's you) see where that dragged box is going to end up. We draw a rectangle (the destination), and a line with an arrow on it from where you are currently dragging it. We color it dark red because that's what the rest of Field's alignment system happens to use. Feel free to dump any other PLine based decoration into this list.
- an.score =100 / .... — the "score" for this alignment. We make a 'big' score so that our alignment beats everything else (which is usually around 1 or so). Still, our score decreases as we get further away from the grid points.
All we've done is declare a class, we need to tell Field to use it:
_self.guides_ = anOffer()
For those versed in the way Field operates this makes sense. Field often uses properties to extend the behavior of elements. Here we tell Field for the box that we are writing code in we'd like it to consider the class we've just made.
This code gets us this:
A visual grid
You ought to now be able to be able to edit the above code and turn it into anything you'd like — want your boxes to only be in prime number coordinates? off you go. Try doing that in Illustrator.
But writing this code for every box you want to have special alignment for is silly. Two approaches: firstly, you can read and understand [SelfAndProperties] and realize that, if you change the way that boxes delegate to each other, you can add this code to a 'parent' box for lots of boxes and get this behavior in all the boxes you'd like. Some boxes can stick to one grid, others to another, and yet more will be prime-number-philes.
The second, and more straightforward approach is to write this code somewhere (perhaps in an autoexec property).
_self.root.guides_ = anOffer()
This extends the functionality of the "root" of the sheet itself, offering up your grid to every element. Done.
What else can we do? Well, anything we want.
Here's a nice visual example: a grid that isn't a grid. You should take a moment to review how to draw things in Field.
First a function:
def closestPointToLines(element, x): d = 10000 dIs = None for p in element.lines: pp = PLine(p) cp = pp.closest(x).position() dd = cp.distanceFrom(x) if (dd<d): d = dd dIs = cp return (d, dIs)
This returns the point that's closest to a PLine (any PLine) inside an element. You can put this code anywhere, as long as you execute it.
Now some code that draws a set of lines — put this in a "new spline drawer" called "grid":
_self.lines.clear() drawGrid = 100 for x in range(0, 10): _self.lines.add(PLine().moveTo(x*drawGrid, 0).lineTo(x*drawGrid, 1000)(derived=0, color=Vector4(0,0,0,0.2)))
This draws some vertical lines:
And then finally, a new guide class:
grid = _self.find["grid"][0] class anOffer(DynamicAlignment): def score(self, inside, on, original, current, next): an = anAlignment(newRect=Rect(0,0,0,0).setValue(current)) cp = closestPointToLines(grid, Vector2(current.x, current.y+current.h/2)) an.newRect.x = cp[1].x an.newRect.y = cp[1].y-current.h/2 an.name = "at %i %i " % (int(an.newRect.x), int(an.newRect.y)) r2 = PLine().rect(an.newRect)(color=Vector4(0.25,0,0,0.25), filled=1,thickness=2) ad = PLine().m(*current.midpoint2()).l(*an.newRect.midpoint2())(color=Vector4(0.25,0,0,0.25), filled=1) ar = PLine(Arrows().getArrowForMiddle(ad.line, 25, 1))(color=Vector4(0.25,0,0,0.25), filled=1) an.toDraw = [r2.line, ad.line, ar.line] an.score=100/(1+cp[0]) return self.drawableForDict(an)
This is a lot like our previous guide setup, only it uses closestPointToLines on grid.
This gets us a visual grid that aligns elements:
The punchline
Here's the punchline. Because PLine systems are easily editable inside Field, add this line to your spline drawer "grid":
_self.tweaks()
Now you can edit the grid using the mouse (see BasicDrawing), and thus:
This hints at the payoff for the added complexity — indeed, even the added documentation of this page. Snap-to-grid becomes something other than a feature, given by "the Field developers" to you "the Field user". It becomes something unstable, up-for-grabs, customizable, unexpected.
Something you take ownership of.




