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([2, 1], [2, 1]) — column width pattern, row height pattern
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.)
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.
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).
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).
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 onviewBoxso heavily; if unspecified, we just assumexandystart 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
--twand--thcommand-line options to support2,1for 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,
layoutis probably a render phase likepreprocessthat is given the Render object and assigns{x, y, width, height}coordinates for each key/tile. How does it specify?render.coords.Tileobjects withi,j,keybut nosymbolyet, andrenderassignsx,y,width,heightproperties. This is roughly the currentContext... maybe this should be replaced by aLocationorCellclass, and we use this in place ofContexttoo? ThenTilemay not need to store layout information itself (or does it if sizes mismatch?); perhaps it could just refer to aCelland have a layer indexk...Contextto{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):viewBoxorwidthorheightall the time; the layout is effectively providingwidthandheight(similar towidth="auto"today), and you only needviewBoxto 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).xCenterorcontext.neighbor(1,1).xDelta; and hex grids could provide additional helpers likecontext.radiusandcontext.xCenterandcontext.vertices.context.viewBox.widthandcontext.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/cellwould be in global coordinates. I think most of the time you just want a cell'swidthandheightand 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.xandneighbor.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
viewBoxsetting. This could then be the defaultviewBoxargument forcontext.localize, so you wouldn't have to repeat it unless you're overridingviewBoxin the SVG. Options could include:<symbol>s can still manually specify aviewBoxfor overriding the default. This also helps reading SVG code with coordinates spec near the coordinates of drawing elements, but makeslocalizemore annoying to use.Backward Compatibility
<symbol>s can still manually specifywidthorheight, which would be an assertion about the layout, if there is a layout. (Note thatviewBoxisn't an assertion, as it could be used to change the coordinate system to have a different width/height.)widthandheight, 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 width="auto" and height="auto" asymmetric #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
Layoutmethods likepreRenderandpostRender? 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.anchorspecification. 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 likelayoutis 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/defaultexport, then nothing renders (butpreprocessandpostprocessstill 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).
OverrideMappingclass? For example:export map = new OverrideMapping [svgtiler.require('map1.txt'), svgtiler.require('map2.coffee')]. (You'd need to do more work to inherit thepreprocess/postprocessfrom the individual maps.)Keep in mind we might naturally want to mix and match some mapping files with just
mapwith 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 amaprenders 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 = trueor something...