Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom layout algorithm #90

Open
4 tasks
edemaine opened this issue Aug 31, 2022 · 0 comments
Open
4 tasks

Custom layout algorithm #90

edemaine opened this issue Aug 31, 2022 · 0 comments

Comments

@edemaine
Copy link
Owner

edemaine commented Aug 31, 2022

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

  • 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 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 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...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant