It's common to want a specific grid setup, e.g. "rows alternating widths of 2 and 1; columns alternating heights of 2 and 1" (similar to CSS grid box e.g. grid-template-columns/rows), and warn when tiles deviate from this. This would also mean we don't have to rely on viewBox so heavily; if unspecified, we just assume x and y start at 0 and go up to column width and row height. (Still need boundingBox.)
Specifying Layout
One natural way to specify this kind of pattern is to generalize --tw and --th command-line options to support 2,1 for repeating patterns.
In JavaScript, a natural way to specify this is as a custom layout algorithm, via export layout = -> .... There could be several algorithms provided for convenience:
export layout = svgtiler.grid(100, 100) — every cell is 100×100
export layout = svgtiler.hexGrid(10) — hex grid of radius 10
Should we use classes and instead use new svgtiler.GridLayout/new svgtiler.HexGridLayout?
In general, layout is probably a render phase like preprocess that is given the Render object and assigns {x, y, width, height} coordinates for each key/tile. How does it specify?
Could return an array of array of object
Could assign such an array directly to a new property like render.coords.
Maybe there are already some kind of Tile objects with i, j, key but no symbol yet, and render assigns x, y, width, height properties. This is roughly the current Context... maybe this should be replaced by a Location or Cell class, and we use this in place of Context too? Then Tile may not need to store layout information itself (or does it if sizes mismatch?); perhaps it could just refer to a Cell and have a layer index k...
Alternatively, a layout could be specified as a mapping from Context to {x, y, width, height}, maybe via svgtiler.layout(...) wrapper. This seems like a difficult way to design a layout though (pretty much how we've been doing it so far, which requires looking at your row and column and such).
The general approach here, which is very different from current SVG Tiler, is to do this layout before keys get expanded to tiles (i.e. doesn't need to access drawing.tiles). In many ways this would be much better than the current system (where tiles dictate their own size):
Tiles will no longer need to specify viewBox or width or height all the time; the layout is effectively providing width and height (similar to width="auto" today), and you only need viewBox to override the default coordinate system of [0, width] × [0, height].
Much bigger, symbols can be told what size they should be, instead of having to decide for themselves.
By contrast, it's currently hard to define a "blank" tile because it needs to figure out what size blank to render. With this proposal, a blank symbol (empty string or <symbol/>) would automatically gain the correct width and height according to the layout.
Beyond blanks, the layout of the current tile could be read via context (possibly replaced by Cell): context.xCenter, context.xMin, context.width, etc., as well as context.neighbor(1,1).xCenter or context.neighbor(1,1).xDelta; and hex grids could provide additional helpers like context.radius and context.xCenter and context.vertices.
These coordinates are relative to the layout boxes, not the bounding boxes, so might need to rename... Perhaps context.viewBox.width and context.viewBox.center.x, generalizing the 4-element array data structure currently used for viewBox. And this would give a way to override the default viewBox, e.g. context.viewBox.anchor('center').
Coordinate Localization
The coordinate data provided via context/location/cell would be in global coordinates. I think most of the time you just want a cell's width and height and a neighbor's xDelta/yDelta. But we could offer a method to localize coordinates to the cell's viewBox, which defaults to [0, width] × [0, height] but can be specified. For example, context.localize(neighbor.xCenter, neighbor.yCenter, "-#{context.width/2} -#{context.height/2} #{context.width} #{context.height}") localizes a neighbor's center coordinates to the specified origin-at-center viewBox, returning an {x, y} object I guess.
This makes me wonder whether we should instead use neighbor.center.x and neighbor.center.y (and similarly min (or topLeft?), max, etc.) so we can pass in neighbor.center. This would also match hex grid's vertices.
Automatic viewBox
We could also offer tools for automatic default viewBox setting. This could then be the default viewBox argument for context.localize, so you wouldn't have to repeat it unless you're overriding viewBox in the SVG. Options could include:
Fixed coordinate systems like always [0, 1] × [0, 1] or [−1, 1] × [−1, 1].
A version of this that matches the tile's aspect ratio, either reducing the smaller range or expanding the larger range (maybe according to a flag).
Matching coordinate systems — width and height match rendered width and height — but with specified anchor, e.g., center at 0,0 or top-left at 0,0 [default].
<symbol>s can still manually specify a viewBox for overriding the default. This also helps reading SVG code with coordinates spec near the coordinates of drawing elements, but makes localize more annoying to use.
Backward Compatibility
<symbol>s can still manually specify width or height, which would be an assertion about the layout, if there is a layout. (Note that viewBox isn't an assertion, as it could be used to change the coordinate system to have a different width/height.)
[ ] If there isn't a layout, though, we could require width and height, or viewBox, as we currently do, and then compute a layout after rendering all tiles by taking the max tile height/width within each row/column. And then ideally still supporting width="auto" which sets to this max; with luck this just works, actually, and fixes #46.
This is close to the current behavior, so mostly backward compatible. What it doesn't do is preserve the behavior that one too-wide tile shifts the entire rest of the row, but that seems bad anyway; instead it will just widen the column, which is still bad but probably better.
Note that this layout algorithm (like the existing layout algorithm) is special, as it cannot be run until after the rendering happens. Should we support a generalized form of late layout algorithms, perhaps via different Layout methods like preRender and postRender? It feels like the design of tiles is fundamentally different in the two schemes, so the main reason to support post-render is backward compatibility, so this may not be crucial.
[ ] Maybe by default, tiles are centered in their cell, or top-left aligned, but could override via an anchor specification. This can only happen in a post-render layout, so maybe not important.
Multiple Layouts
Precomputed layouts would also make it possible to render multiple "layers" into one output, either multiple entire drawing files (#97) or combining mappings where a tile gets rendered by multiple mappings (#83). We should reconsider whether combining mappings should be default behavior, at least in some cases like when all mappings specify their own layout.
If there is one layout per mapping file, perhaps they should all render separately and stack? To write a combining layer that matches the layout of another mapping file, could simply export {layout} from .... But this makes it hard to write a generic mix-in that conforms to whatever other layouts are on the command line. The default behavior from Backward Compatibility (when no layout is exported) could use the first mapping with a layout, if it exists, as the default layout for this mapping. Probably best to make this explicit via a special value like export layout = 'match' or 'previous' or 'next' (to use previous/next mapping that has a layout).
[ ] Proposal: All loaded mappings run fully on a drawing file. Each mapping processes the entire drawing and renders all tiles that are not null. If a particular tile doesn't get rendered by any mapping file, you get a warning. All rendered objects get stacked according to z-index as usual. Of course, if there's no map/default export, then nothing renders (but preprocess and postprocess still run as usual).
In general, we need to answer these questions:
What does a mapping without a layout do? Backward Compatibility behavior above.
What do multiple mapping files each with a layout do? They each render separately with their layout, and rendered objects stack.
What if we have a mix? Same
Note that, when layouts match up and no two mappings define the same tile, this is exactly the current behavior. When layouts match up but multiple mappings define a common tile, then we get define combining behavior (#83) which seems like more useful behavior in general. We can also have nonmatching layouts which could render different aspects of the same drawing, which could be interesting (e.g. rendering grid intersections, grid edges, and grid cells separately). I also like that listing on the command line multiple mapping files with the same layouts now becomes equivalent to putting those mappings into an array (see end of #83).
[ ] We'll want to provide a mechanism to merge mappings in the old way (last mapping to define a tile wins), so you can reproduce the old behavior. Maybe a OverrideMapping class? For example: export map = new OverrideMapping [svgtiler.require('map1.txt'), svgtiler.require('map2.coffee')]. (You'd need to do more work to inherit the preprocess/postprocess from the individual maps.)
Keep in mind we might naturally want to mix and match some mapping files with just map with some mapping files with just layout. This is possible via export layout = 'match'/'previous'/'next' in the files providing map.
Alternate Proposal: All loaded mappings init, but only the last one to define a map renders anything, using the last defined layout. This would often remove the need for parentheses. You can still mix map-only mappings with layout-only mappings, and still have side effects in init. You just usually don't need to unload mappings. The downside of course is it's harder to stack a mapping on top of others, e.g. to define a generic "grid" mapping (except via postprocess). This could be fixed via an export combine = true or something...
It's common to want a specific grid setup, e.g. "rows alternating widths of 2 and 1; columns alternating heights of 2 and 1" (similar to CSS grid box e.g.
grid-template-columns
/rows
), and warn when tiles deviate from this. This would also mean we don't have to rely onviewBox
so heavily; if unspecified, we just assumex
andy
start at 0 and go up to column width and row height. (Still needboundingBox
.)Specifying Layout
One natural way to specify this kind of pattern is to generalize
--tw
and--th
command-line options to support2,1
for repeating patterns.In JavaScript, a natural way to specify this is as a custom layout algorithm, via
export layout = -> ...
. There could be several algorithms provided for convenience:export layout = svgtiler.grid([2, 1], [2, 1])
— column width pattern, row height patternexport layout = svgtiler.grid(100, 100)
— every cell is 100×100export layout = svgtiler.hexGrid(10)
— hex grid of radius 10new svgtiler.GridLayout
/new svgtiler.HexGridLayout
?In general,
layout
is probably a render phase likepreprocess
that is given the Render object and assigns{x, y, width, height}
coordinates for each key/tile. How does it specify?render.coords
.Tile
objects withi
,j
,key
but nosymbol
yet, andrender
assignsx
,y
,width
,height
properties. This is roughly the currentContext
... maybe this should be replaced by aLocation
orCell
class, and we use this in place ofContext
too? ThenTile
may not need to store layout information itself (or does it if sizes mismatch?); perhaps it could just refer to aCell
and have a layer indexk
...Context
to{x, y, width, height}
, maybe viasvgtiler.layout(...)
wrapper. This seems like a difficult way to design a layout though (pretty much how we've been doing it so far, which requires looking at your row and column and such).The general approach here, which is very different from current SVG Tiler, is to do this layout before keys get expanded to tiles (i.e. doesn't need to access
drawing.tiles
). In many ways this would be much better than the current system (where tiles dictate their own size):viewBox
orwidth
orheight
all the time; the layout is effectively providingwidth
andheight
(similar towidth="auto"
today), and you only needviewBox
to override the default coordinate system of [0, width] × [0, height].<symbol/>
) would automatically gain the correct width and height according to the layout.Cell
):context.xCenter
,context.xMin
,context.width
, etc., as well ascontext.neighbor(1,1).xCenter
orcontext.neighbor(1,1).xDelta
; and hex grids could provide additional helpers likecontext.radius
andcontext.xCenter
andcontext.vertices
.context.viewBox.width
andcontext.viewBox.center.x
, generalizing the 4-element array data structure currently used forviewBox
. And this would give a way to override the defaultviewBox
, e.g.context.viewBox.anchor('center')
.Coordinate Localization
The coordinate data provided via
context
/location
/cell
would be in global coordinates. I think most of the time you just want a cell'swidth
andheight
and a neighbor'sxDelta
/yDelta
. But we could offer a method to localize coordinates to the cell'sviewBox
, which defaults to [0, width] × [0, height] but can be specified. For example,context.localize(neighbor.xCenter, neighbor.yCenter, "-#{context.width/2} -#{context.height/2} #{context.width} #{context.height}")
localizes a neighbor's center coordinates to the specified origin-at-center viewBox, returning an{x, y}
object I guess.This makes me wonder whether we should instead use
neighbor.center.x
andneighbor.center.y
(and similarlymin
(ortopLeft
?),max
, etc.) so we can pass inneighbor.center
. This would also match hex grid'svertices
.Automatic viewBox
We could also offer tools for automatic default
viewBox
setting. This could then be the defaultviewBox
argument forcontext.localize
, so you wouldn't have to repeat it unless you're overridingviewBox
in the SVG. Options could include:<symbol>
s can still manually specify aviewBox
for overriding the default. This also helps reading SVG code with coordinates spec near the coordinates of drawing elements, but makeslocalize
more annoying to use.Backward Compatibility
<symbol>
s can still manually specifywidth
orheight
, which would be an assertion about the layout, if there is a layout. (Note thatviewBox
isn't an assertion, as it could be used to change the coordinate system to have a different width/height.)width
andheight
, orviewBox
, as we currently do, and then compute a layout after rendering all tiles by taking the max tile height/width within each row/column. And then ideally still supportingwidth="auto"
which sets to this max; with luck this just works, actually, and fixes #46.This is close to the current behavior, so mostly backward compatible. What it doesn't do is preserve the behavior that one too-wide tile shifts the entire rest of the row, but that seems bad anyway; instead it will just widen the column, which is still bad but probably better.
Note that this layout algorithm (like the existing layout algorithm) is special, as it cannot be run until after the rendering happens. Should we support a generalized form of late layout algorithms, perhaps via different
Layout
methods likepreRender
andpostRender
? It feels like the design of tiles is fundamentally different in the two schemes, so the main reason to support post-render is backward compatibility, so this may not be crucial.anchor
specification. This can only happen in a post-render layout, so maybe not important.Multiple Layouts
Precomputed layouts would also make it possible to render multiple "layers" into one output, either multiple entire drawing files (#97) or combining mappings where a tile gets rendered by multiple mappings (#83). We should reconsider whether combining mappings should be default behavior, at least in some cases like when all mappings specify their own layout.
If there is one layout per mapping file, perhaps they should all render separately and stack? To write a combining layer that matches the layout of another mapping file, could simply
export {layout} from ...
. But this makes it hard to write a generic mix-in that conforms to whatever other layouts are on the command line.The default behavior from Backward Compatibility (when noProbably best to make this explicit via a special value likelayout
is exported) could use the first mapping with a layout, if it exists, as the default layout for this mapping.export layout = 'match'
or'previous'
or'next'
(to use previous/next mapping that has alayout
).map
/default
export, then nothing renders (butpreprocess
andpostprocess
still run as usual).In general, we need to answer these questions:
Note that, when layouts match up and no two mappings define the same tile, this is exactly the current behavior. When layouts match up but multiple mappings define a common tile, then we get define combining behavior (#83) which seems like more useful behavior in general. We can also have nonmatching layouts which could render different aspects of the same drawing, which could be interesting (e.g. rendering grid intersections, grid edges, and grid cells separately). I also like that listing on the command line multiple mapping files with the same layouts now becomes equivalent to putting those mappings into an array (see end of #83).
OverrideMapping
class? For example:export map = new OverrideMapping [svgtiler.require('map1.txt'), svgtiler.require('map2.coffee')]
. (You'd need to do more work to inherit thepreprocess
/postprocess
from the individual maps.)Keep in mind we might naturally want to mix and match some mapping files with just
map
with some mapping files with justlayout
. This is possible viaexport layout = 'match'
/'previous'
/'next'
in the files providingmap
.init
, but only the last one to define amap
renders anything, using the last definedlayout
. This would often remove the need for parentheses. You can still mixmap
-only mappings withlayout
-only mappings, and still have side effects ininit
. You just usually don't need to unload mappings. The downside of course is it's harder to stack a mapping on top of others, e.g. to define a generic "grid" mapping (except viapostprocess
). This could be fixed via anexport combine = true
or something...