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

Nested anchors and String Shorthand for Path and Border Anchors #523

Merged
merged 6 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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`.

Expand Down
Binary file modified manual.pdf
Binary file not shown.
32 changes: 19 additions & 13 deletions manual.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down
27 changes: 24 additions & 3 deletions src/anchor.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand All @@ -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))
Expand Down
11 changes: 4 additions & 7 deletions src/coordinate.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
Expand All @@ -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,
Expand Down
81 changes: 57 additions & 24 deletions src/draw/grouping.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
}
Expand All @@ -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" },
Expand All @@ -252,7 +279,9 @@
border-anchors: bounds != none,
radii: (width, height),
path: path,
nested-anchors: true
)

return (
ctx: ctx,
name: name,
Expand Down Expand Up @@ -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()
)
},)
}

Expand All @@ -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)
},)
}
Expand Down
3 changes: 3 additions & 0 deletions src/process.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions src/util.typ
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,25 @@
}
return body
}


#let str-to-number-regex = regex("^(-?\d*\.?\d+)(cm|mm|pt|em|in|%|deg|rad)?$")
Copy link
Member

@johannes-wolf johannes-wolf Mar 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow scientific notation here?
^(-?\d*\.?\d+)([eE][+\-]?\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
}
Binary file modified tests/group/nested-anchor/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions tests/group/nested-anchor/test.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}))
Loading