diff --git a/README.md b/README.md index 755b863..106f8b5 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ or return value of a function) should be specified as one of the following: 10. An `Array` of tiles of any of the listed formats (including more `Array`s, which get flattened), meaning to stack multiple tiles on top of each other, where the first non-null tile defines the `viewBox` and size - (but all can influence the [`overflowBox`](#overflow-and-bounding-box)). + (but all can influence the [`boundingBox`](#overflow-and-bounding-box)). Use [`z-index`](#z-index-stacking-order-of-tiles) to control stacking order. Null items in the array get ignored, and an empty array acts like `null` @@ -366,7 +366,7 @@ The top-level code of your .js or .coffee mapping file can also call: Specify a [`z-index`](#z-index-stacking-order-of-tiles) to control the stacking order relative to other symbols or overlays/underlays. - Specify [`overflowBox`](#overflow-and-bounding-box) to increase the + Specify [`boundingBox`](#overflow-and-bounding-box) to increase the overall size of the rendered drawing. * `svgtiler.background(fillColor)` to set the default background color for the SVG drawing (implemented via a `` underneath the bounding box). @@ -552,19 +552,31 @@ When `overflow` is `visible`, `viewBox` still represents the size of the element in the [grid layout](#layout-algorithm), but allows the element's actual bounding box to be something else. To correctly set the bounding box of the overall SVG drawing, SVG Tiler -defines an additional `` attribute called `overflowBox`, which is like +defines an additional `` attribute called `boundingBox`, which is like `viewBox` but for specifying the actual bounding box of the content -(when they differ — `overflowBox` defaults to the value of `viewBox`). +(when they differ — `boundingBox` defaults to the value of `viewBox`). The `viewBox` of the overall SVG is set to the minimum rectangle -containing all tiles' `overflowBox`s. +containing all tiles' `boundingBox`s. For example, -`...` +`...` defines a tile that gets laid out as if it occupies the [0, 10] × [0, 10] square, but the tile can draw outside that square, and the overall drawing bounding box will be set as if the tile occupies the [−5, 15] × [−5, 15] square. +The `boundingBox` can also be *smaller* than the `viewBox`, in case the +`viewBox` needs to be larger for proper grid alignment but the particular +symbol doesn't actually use the whole space. +For example, `` +allocates 10×10 of space but may shrink down to 10×5 when used on the +top edge of the diagram (if no other symbols' bounding box extend above) +or 5×10 when used on the left edge of the diagram, +or 5×5 in the top-left corner. +You can also use the special value `boundingBox="none"` to specify that +the symbol should not influence the drawing's `viewBox` at all, +or `boundingBox="null -5 null 10"` to just affect the vertical extent (say). + Even zero-width and zero-height ``s will get rendered (unless `overflow="hidden"`). This can be useful for drawing grid outlines without affecting the overall grid layout, for example. @@ -573,6 +585,10 @@ height](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox), so SVG Tiler automatically works around this by using slightly positive widths and heights in the output `viewBox`.) +The `boundingBox` attribute used to be called `overflowBox` (prior to v3). +For backward compatibility, the old name is still supported, but in either +case it can cause both overflow and underflow relative to `viewBox`. + ## Autosizing Tiles Normally, a tile specifies its layout size by setting the diff --git a/examples/grid-graph/Maketile.args b/examples/grid-graph/Maketile.args index 535e0df..4fcbb32 100644 --- a/examples/grid-graph/Maketile.args +++ b/examples/grid-graph/Maketile.args @@ -1,3 +1,3 @@ -f ( flip_parity.coffee background.css path.coffee ips_example*asc ) -( background.css path.coffee ips_*path*asc ) +( background.css path.coffee ips_*path*asc ortho*.asc ) diff --git a/examples/grid-graph/README.md b/examples/grid-graph/README.md index 3b76ff4..f49f875 100644 --- a/examples/grid-graph/README.md +++ b/examples/grid-graph/README.md @@ -39,3 +39,9 @@ the vertex coloring, to match the figure in the paper. ![Underlying graph figure](ips_example_graph.svg) [ASCII input](ips_example_graph.asc), [SVG output](ips_example_graph.svg) + +## Orthogonal graph layout + +![Orthogonal graph layout figure](ortho_test.svg) + +[ASCII input](ortho_test.asc), [SVG output](ortho_test.svg) diff --git a/examples/grid-graph/ortho_test.asc b/examples/grid-graph/ortho_test.asc new file mode 100644 index 0000000..20baefd --- /dev/null +++ b/examples/grid-graph/ortho_test.asc @@ -0,0 +1,7 @@ + .-. + | | +.-O-O-. +| | | | +.-O-O-. + | | + .-. diff --git a/examples/grid-graph/ortho_test.svg b/examples/grid-graph/ortho_test.svg new file mode 100644 index 0000000..a14873d --- /dev/null +++ b/examples/grid-graph/ortho_test.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/grid-graph/path.coffee b/examples/grid-graph/path.coffee index c1d62f1..4fcca99 100644 --- a/examples/grid-graph/path.coffee +++ b/examples/grid-graph/path.coffee @@ -11,11 +11,11 @@ parity = null svgtiler.onInit -> parity = (share.flipParity ? 0) * 2 -blank = +blank = vertex = -> + boundingBox="#{-(size+vertexStroke)/2} #{-(size+vertexStroke)/2} #{size+vertexStroke} #{size+vertexStroke}"> @@ -32,13 +32,13 @@ arrow = svgtiler.def( horizontal = (start, end) -> + boundingBox="#{-(size+edgeStroke)/2} #{-edgeStroke/2} #{size+edgeStroke} #{edgeStroke}"> vertical = (start, end) -> + boundingBox="#{-edgeStroke/2} #{-(size+edgeStroke)/2} #{edgeStroke} #{size+edgeStroke}"> @@ -53,3 +53,19 @@ o: vertex '>': horizontal null, arrow '^': vertical arrow, null 'v': vertical null, arrow +'.': -> # connecting turn between -s and |s + + + {if @neighbor(-1, 0).includes '-' + + } + {if @neighbor(+1, 0).includes '-' + + } + {if @neighbor(0, -1).includes '|' + + } + {if @neighbor(0, +1).includes '|' + + } + diff --git a/examples/polyomino/outlines.cjsx b/examples/polyomino/outlines.cjsx index 7b93c51..92107d3 100644 --- a/examples/polyomino/outlines.cjsx +++ b/examples/polyomino/outlines.cjsx @@ -2,7 +2,7 @@ Border = (props) -> (key) -> - + {if @neighbor(-1,0).key != key } diff --git a/examples/polyomino/outlines.coffee b/examples/polyomino/outlines.coffee index 2ffc715..297c4a7 100644 --- a/examples/polyomino/outlines.coffee +++ b/examples/polyomino/outlines.coffee @@ -2,7 +2,7 @@ borderStyle = 'stroke="black" stroke-width="5" stroke-linecap="round"' (key) -> s = ''' - + ''' if @neighbor(-1,0).key != key s += """""" diff --git a/examples/polyomino/outlines.jsx b/examples/polyomino/outlines.jsx index 9254c62..10f8b97 100644 --- a/examples/polyomino/outlines.jsx +++ b/examples/polyomino/outlines.jsx @@ -2,7 +2,7 @@ const Border = (props) => ; (key, context) => { - return + return {context.neighbor(-1,0).key !== key && } {context.neighbor(+1,0).key !== key && diff --git a/examples/test/mapping.coffee b/examples/test/mapping.coffee index a3b0ddb..c5dc1dc 100644 --- a/examples/test/mapping.coffee +++ b/examples/test/mapping.coffee @@ -1,7 +1,7 @@ # Test svgtiler.Mappings being provided as mapping values export map1 = new svgtiler.Mapping (key) -> - diff --git a/examples/unicode/maze.txt b/examples/unicode/maze.txt index 3b7ec82..b5d52fe 100644 --- a/examples/unicode/maze.txt +++ b/examples/unicode/maze.txt @@ -1,4 +1,4 @@ -█ +█ 😊 👍🏽 diff --git a/examples/witness/witness.coffee b/examples/witness/witness.coffee index 0037070..9974b45 100644 --- a/examples/witness/witness.coffee +++ b/examples/witness/witness.coffee @@ -69,7 +69,7 @@ blank = -> start = (solution) -> -> s = """ - + """ if @neighbor(-1,0).includes('-s') + @neighbor(+1,0).includes('-s') + diff --git a/src/svgtiler.coffee b/src/svgtiler.coffee index 04b5c02..9866d57 100755 --- a/src/svgtiler.coffee +++ b/src/svgtiler.coffee @@ -274,19 +274,27 @@ parseDim = (x) -> console.warn "Unrecognized unit #{match[2]}" parseNum match[1] -parseBox = (box) -> +parseBox = (box, allowNull) -> return null unless box box = box.split /\s*[\s,]\s*/ .map parseNum - return null if null in box + return null if null in box unless allowNull box -extractOverflowBox = (xml) -> - ## Parse and return root overflowBox attribute. - ## Also remove it if present, so output is valid SVG. - box = xml.documentElement.getAttribute 'overflowBox' +extractBoundingBox = (xml) -> + ### + Parse and return root `boundingBox` attribute, + possibly under the old name of `overflowBox`. + Also remove them if present, so output is valid SVG. + ### + box = xml.documentElement.getAttribute('boundingBox') or + xml.documentElement.getAttribute('overflowBox') + xml.documentElement.removeAttribute 'boundingBox' xml.documentElement.removeAttribute 'overflowBox' - parseBox box + if box.toLowerCase().trim() == 'none' + [null, null, null, null] + else + parseBox box, true svgBBox = (dom) -> ## xxx Many unsupported features! @@ -306,9 +314,9 @@ svgBBox = (dom) -> when 'rect', 'image' ## For , should autodetect image size (#42) [parseNum(node.getAttribute 'x') ? 0 - parseNum(node.getAttribute 'y') ? 0 - parseNum(node.getAttribute 'width') ? 0 - parseNum(node.getAttribute 'height') ? 0] + parseNum(node.getAttribute 'y') ? 0 + parseNum(node.getAttribute 'width') ? 0 + parseNum(node.getAttribute 'height') ? 0] when 'circle' cx = parseNum(node.getAttribute 'cx') ? 0 cy = parseNum(node.getAttribute 'cy') ? 0 @@ -619,7 +627,7 @@ class SVGContent extends HasSettings ### Base helper for parsing SVG as specified in SVG Tiler: SVG strings, Preact VDOM, or filenames, with special handling of image files. - Automatically determines `width`, `height`, `viewBox`, `overflowBox`, + Automatically determines `width`, `height`, `viewBox`, `boundingBox`, and `zIndex` properties if specified in the SVG content, and sets `isEmpty` to indicate whether the SVG is a useless empty tag. In many cases (symbols and defs), acquires an `id` property via `setId`, @@ -843,7 +851,7 @@ class SVGContent extends HasSettings @dom.documentElement.setAttribute 'viewBox', @viewBox.join ' ' ## Special SVG Tiler attributes that get extracted from DOM - @overflowBox = extractOverflowBox @dom + @boundingBox = extractBoundingBox @dom @zIndex = extractZIndex @dom.documentElement #@isEmpty = @dom.documentElement.childNodes.length == 0 and # (@emptyWithId or not @dom.documentElement.hasAttribute 'id') and @@ -886,7 +894,7 @@ class SVGWrapped extends SVGContent else doc = @dom ## Allow top-level object to specify data. - ## `z-index` and `overflowBox` should already be extracted. + ## `z-index` and `boundingBox` should already be extracted. ## `width` and `height` have another meaning in e.g. s, ## so just transfer for tags where they are meaningless. for attribute in ['viewBox', 'width', 'height', 'overflow'] @@ -1674,7 +1682,7 @@ class Render extends HasSettings @styles = new Styles @getSetting 'styles' @defs = [] ## Accumulated rendering, in case add() called before makeDOM(): - @xMin = @yMin = @xMax = @yMax = 0 + @xMin = @yMin = @xMax = @yMax = null @layers = {} hrefAttr: -> hrefAttr @settings id: (key) -> #, noEscape) -> @@ -1724,20 +1732,34 @@ class Render extends HasSettings "svgtiler.add content from #{getContextString()}", content, @settings dom = content.makeDOM() return if content.isEmpty - if (box = content.overflowBox ? content.viewBox)? - @xMin = Math.min @xMin, box[0] - @yMin = Math.min @yMin, box[1] - @xMax = Math.max @xMax, box[0] + box[2] - @yMax = Math.max @yMax, box[1] + box[3] + if (box = content.boundingBox ? content.viewBox)? + @expandBox box @updateSize() @layers[content.zIndex] ?= [] if prepend @layers[content.zIndex].unshift dom.documentElement else @layers[content.zIndex].push dom.documentElement + expandBox: (box) -> + if box[0]? + @xMin = box[0] if not @xMin? or box[0] < @xMin + if box[2]? + x = box[0] + box[2] + @xMax = x if not @xMax? or x > @xMax + if box[1]? + @yMin = box[1] if not @yMin? or box[1] < @yMin + if box[3]? + y = box[1] + box[3] + @yMax = y if not @yMax? or y > @yMax updateSize: -> - @width = @xMax - @xMin - @height = @yMax - @yMin + if @xMin? and @xMax? + @width = @xMax - @xMin + else + @width = 0 + if @yMin? and @yMax? + @height = @yMax - @yMin + else + @height = 0 makeDOM: -> runWithRender @, => runWithContext (new Context @), => ### Main rendering engine, returning an xmldom object for the whole document. @@ -1863,19 +1885,25 @@ class Render extends HasSettings if symbol.autoHeight use.setAttribute 'height', (symbol.viewBox?[3] ? symbol.height) * scaleY - if symbol.overflowBox? - dx = (symbol.overflowBox[0] - symbol.viewBox[0]) * scaleX - dy = (symbol.overflowBox[1] - symbol.viewBox[1]) * scaleY - @xMin = Math.min @xMin, x + dx - @yMin = Math.min @yMin, y + dy - @xMax = Math.max @xMax, x + dx + symbol.overflowBox[2] * scaleX - @yMax = Math.max @yMax, y + dy + symbol.overflowBox[3] * scaleY + if symbol.boundingBox? + dx = (symbol.boundingBox[0] - symbol.viewBox[0]) * scaleX + dy = (symbol.boundingBox[1] - symbol.viewBox[1]) * scaleY + @expandBox [ + x + dx + y + dy + symbol.boundingBox[2] * scaleX + symbol.boundingBox[3] * scaleY + ] + else + @expandBox [tile.xMin, tile.yMin, tile.width, tile.height] x = tiles[0].xMax - @xMax = Math.max @xMax, x y += rowHeight - @yMax = Math.max @yMax, y ## Postprocess callbacks, which may use (and update) @width/@height + @xMin ?= 0 + @yMin ?= 0 + @xMax ?= 0 + @yMax ?= 0 @updateSize() @mappings.doPostprocess @