edemaine / svgtiler

Tool for drawing diagrams on a grid, combining grids of SVGs into a big SVG figure
MIT License
61 stars 6 forks source link

Custom layout algorithm #90

Open edemaine opened 2 years ago

edemaine commented 2 years ago

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:

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?

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):

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:

<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:

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.