diff --git a/CHANGES.md b/CHANGES.md index ef8d54bf..121ac97d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ + # 0.2.2 +## Anchors +- Support for accessing anchors within groups. +- Support string shorthand for path and border anchors. + ## 3D - CeTZ gained some helper functions for drawing 3D figures in orthographic projection: `ortho`, `on-xy`, `on-xz` and `on-yz`. diff --git a/manual.pdf b/manual.pdf index 0c5c63b2..22feb2cf 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/manual.typ b/manual.typ index 30eb56ee..508210e6 100644 --- a/manual.typ +++ b/manual.typ @@ -407,19 +407,7 @@ for (c, s, f, cont) in ( Defines a point relative to a named element using anchors, see @anchors. #doc-style.show-parameter-block("name", "string", [The name of the element that you wish to use to specify a coordinate.], show-default: false) -#doc-style.show-parameter-block("anchor", ("number", "angle", "string", "ratio", "none"), [The anchor of the element. Strings are named anchors, angles are border anchors and numbers and ratios are path anchors. If not given the default anchor will be used, on most elements this is `center` but it can be different.]) - -You can also use implicit syntax of a dot separated string in the form `"name.anchor"` -for named anchors. - -```example -line((0,0), (3,2), name: "line") -circle("line.end", name: "circle") -rect("line.start", "circle.east") -``` - -Note, that not all elements provide border or path anchors! - +#doc-style.show-parameter-block("anchor", ("number", "angle", "string", "ratio", "none"), [The anchor of the element. Strings are named anchors, angles are border anchors and numbers and ratios are path anchors. If not given, the default anchor will be used, on most elements this is `center` but it can be different.]) ```example circle((0,0), name: "circle") @@ -430,6 +418,24 @@ content((name: "circle", anchor: 30%), box(fill: white, $ 30 % $)) // Anchor at 3.14 of the path content((name: "circle", anchor: 3.14), box(fill: white, $ p = 3.14 $)) ``` +Note, that not all elements provide border or path anchors! + +You can also use implicit syntax of a dot separated string in the form `"name.anchor"` for all anchors. Because named elements in groups act as anchors, you can also access them through this syntax. + +```example +group(name: "group", { + line((0,0), (3,2), name: "line") + circle("line.end", name: "circle") + rect("line.start", "circle.east") + + circle("circle.30deg", radius: 0.1, fill: red) +}) + +circle("group.circle.-1", radius: 0.1, fill: blue) +``` + + + == Tangent diff --git a/src/anchor.typ b/src/anchor.typ index 2bcfae66..6e62f989 100644 --- a/src/anchor.typ +++ b/src/anchor.typ @@ -103,7 +103,8 @@ border-anchors: false, path-anchors: false, radii: none, - path: none + path: none, + nested-anchors: false ) = { // Passing no callback is valid! if callback == auto { @@ -127,18 +128,37 @@ } let out = none + let nested-anchors = if type(anchor) == array { + if not nested-anchors { + anchor = anchor.join(".") + } else { + if anchor.len() > 1 { + anchor + } + anchor = anchor.first() + } + } else if nested-anchors and type(anchor) == str { + anchor = anchor.split(".") + if anchor.len() > 1 { + anchor + } + anchor = anchor.first() + } + if type(anchor) == str { if anchor in anchor-names or (anchor == "default" and default != none) { if anchor == "default" { anchor = default } - - out = callback(anchor) + + out = callback(if nested-anchors != none { nested-anchors } else { anchor }) } else if path-anchors and anchor in named-path-anchors { anchor = named-path-anchors.at(anchor) } else if border-anchors and anchor in named-border-anchors { anchor = named-border-anchors.at(anchor) + } else if util.str-is-number(anchor) { + anchor = util.str-to-number(if nested-anchors != none { nested-anchors.join(".") } else { anchor }) } else { panic( strfmt( @@ -150,6 +170,7 @@ ) } } + if out == none { if type(anchor) in (ratio, float, int) { assert(path-anchors, message: strfmt("Element '{}' does not support path anchors.", name)) diff --git a/src/coordinate.typ b/src/coordinate.typ index 0ac8ac17..534edcc2 100644 --- a/src/coordinate.typ +++ b/src/coordinate.typ @@ -58,12 +58,11 @@ // "name.anchor" // "name" let (name, anchor) = if type(c) == str { - let parts = c.split(".") - if parts.len() == 1 { - (parts.first(), "default") - } else { - (parts.slice(0, -1).join("."), parts.last()) + let (name, ..anchor) = c.split(".") + if anchor.len() == 0 { + anchor = "default" } + (name, anchor) } else { (c.name, c.at("anchor", default: "default")) } @@ -80,8 +79,6 @@ // Check if anchor is known let node = ctx.nodes.at(name) let pos = (node.anchors)(anchor) - assert(pos != none, - message: "Unknown anchor '" + repr(anchor) + "' for element '" + name + "'") let pos = util.revert-transform( ctx.transform, diff --git a/src/draw/grouping.typ b/src/draw/grouping.typ index 2255e5fe..65ba4d67 100644 --- a/src/draw/grouping.typ +++ b/src/draw/grouping.typ @@ -190,6 +190,8 @@ /// /// The default anchor is "center" but this can be overridden by using `anchor` to place a new anchor called "default". /// +/// Named elements within a group can also be accessed as string anchors, see @coordinate-anchor. +/// /// - body (elements, function): Elements to group together. A least one is required. A function that accepts `ctx` and returns elements is also accepted. /// - anchor (none, string): Anchor to position the group and it's children relative to. For translation the difference between the groups `"default"` anchor and the passed anchor is used. /// - name (none, string): @@ -206,16 +208,18 @@ let bounds = none let drawables = () let group-ctx = ctx - group-ctx.groups.push((anchors: (:))) + group-ctx.groups.push(()) (ctx: group-ctx, drawables, bounds) = process.many(group-ctx, util.resolve-body(group-ctx, body)) // Apply bounds padding - let bounds = if bounds != none { + bounds = if bounds != none { let padding = util.as-padding-dict(style.padding) - for (k, v) in padding { - padding.insert(k, util.resolve-number(ctx, v)) - } + padding = padding.pairs().map( + ((k, v)) => ( + (k): util.resolve-number(ctx, v) + ) + ).join() aabb.padded(bounds, padding) } @@ -234,16 +238,39 @@ bounds.low, )), close: true) (center, width, height, path) - } else { (none, none, none, none) } + } else { (none,) * 4 } - let anchors = group-ctx.groups.last().anchors + let children = group-ctx.groups.last().map(name => ((name): group-ctx.nodes.at(name))).join() + + // Children can be none if the groups array is empty + let anchors = if children != none { + children.pairs().map(((name, child)) => { + if "anchors" in child { + ((name): child.anchors) + } + }).join() + } else { + (:) + } let (transform, anchors) = anchor_.setup( - anchor => ( - if bounds != none { - (center: center, default: center) - } + anchors - ).at(anchor), + anchor => { + let (name, ..nested-anchors) = if type(anchor) == array { + anchor + } else { + (anchor,) + } + anchor = ( + if bounds != none { + (default: center, center: center) + } + anchors + ).at(name) + if type(anchor) == function { + anchor(if nested-anchors == () { "default" } else { nested-anchors }) + } else { + anchor + } + }, (anchors.keys() + if bounds != none { ("center",) }).dedup(), name: name, default: if bounds != none or "default" in anchors { "default" }, @@ -252,7 +279,9 @@ border-anchors: bounds != none, radii: (width, height), path: path, + nested-anchors: true ) + return ( ctx: ctx, name: name, @@ -288,8 +317,17 @@ ) let (ctx, position) = coordinate.resolve(ctx, position) position = util.apply-transform(ctx.transform, position) - ctx.groups.last().anchors.insert(name, position) - return (ctx: ctx, name: name, anchors: anchor_.setup(anchor => position, ("default",), default: "default", name: name, transform: none).last()) + return ( + ctx: ctx, + name: name, + anchors: anchor_.setup( + anchor => position, + ("default",), + default: "default", + name: name, + transform: none + ).last() + ) },) } @@ -314,25 +352,20 @@ anchors = anchors.filter(a => a in filter) } - let new = { - let d = (:) - for a in anchors { - d.insert(a, calc-anchors(a)) - } - d + let new = anchors.map(a => ((a): calc-anchors(a))).join() + if new == none { + new = () } // Add each anchor as own element for (k, v) in new { ctx.nodes.insert(k, (anchors: (name => { - if name == () { return ("default",) } + if name == () { ("default",) } else if name == "default" { v } }))) + ctx.groups.last().push(k) } - // Add anchors to group - ctx.groups.last().anchors += new - return (ctx: ctx) },) } diff --git a/src/process.typ b/src/process.typ index e9891f9c..bca3ccaa 100644 --- a/src/process.typ +++ b/src/process.typ @@ -30,6 +30,9 @@ } if "name" in element and type(element.name) == "string" and "anchors" in element { ctx.nodes.insert(element.name, element) + if ctx.groups.len() > 0 { + ctx.groups.last().push(element.name) + } } if ctx.debug and bounds != none { diff --git a/src/util.typ b/src/util.typ index 54e28d18..349abfed 100644 --- a/src/util.typ +++ b/src/util.typ @@ -385,3 +385,25 @@ } return body } + + +#let str-to-number-regex = regex("^(-?\d*\.?\d+)(cm|mm|pt|em|in|%|deg|rad)?$") +#let number-units = ( + "%": 1%, + "cm": 1cm, + "mm": 1mm, + "pt": 1pt, + "em": 1em, + "in": 1in, + "deg": 1deg, + "rad": 1rad +) +#let str-is-number(string) = string.match(str-to-number-regex) != none +#let str-to-number(string) = { + let (num, unit) = string.match(str-to-number-regex).captures + num = float(num) + if unit != none and unit in number-units { + num *= number-units.at(unit) + } + return num +} \ No newline at end of file diff --git a/tests/group/nested-anchor/ref/1.png b/tests/group/nested-anchor/ref/1.png index b2276d7c..3cbad83e 100644 Binary files a/tests/group/nested-anchor/ref/1.png and b/tests/group/nested-anchor/ref/1.png differ diff --git a/tests/group/nested-anchor/test.typ b/tests/group/nested-anchor/test.typ index fe559e00..9bc8a5d2 100644 --- a/tests/group/nested-anchor/test.typ +++ b/tests/group/nested-anchor/test.typ @@ -19,3 +19,22 @@ circle("g.p1", fill: blue) circle("g.p2", fill: red) })) + +#box(stroke: 2pt + red, canvas({ + import draw: * + + group(name: "parent", { + content((0,0), [Content], name: "content") + group(name: "child", { + circle((1,-2), fill: blue, name: "circle") + }) + }) + + rect("parent.content.south-west", + "parent.content.north-east", stroke: green) + rect( + "parent.child.circle.-45deg", + "parent.child.circle.135deg", + stroke: green + ) +})) \ No newline at end of file