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

canvas: Support custom coordinate systems #719

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion src/canvas.typ
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
marks: (
mnemonics: (:),
marks: (:),
)
),
// coordinate resolver
resolve-coordinate: none,
)

let (ctx, bounds, drawables) = process.many(ctx, body)
Expand Down
21 changes: 16 additions & 5 deletions src/coordinate.typ
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@
/// Figures out what system a coordinate belongs to and returns the corresponding string.
/// - c (coordinate): The coordinate to find the system of.
/// -> str
#let resolve-system(c) = {
#let resolve-system(ctx, c) = {
let t = if type(c) == dictionary {
let keys = c.keys()
let len = c.len()
Expand Down Expand Up @@ -319,10 +319,20 @@
/// - update (bool): Update the context's last position
/// -> array
#let resolve(ctx, ..coordinates, update: true) = {
let resolvers = if type(ctx.resolve-coordinate) == array {
ctx.resolve-coordinate
} else {
()
}

let result = ()
for c in coordinates.pos() {
let t = resolve-system(c)
let out = if t == "xyz" {
for resolver in resolvers.rev() {
c = resolver(ctx, c)
}

let t = resolve-system(ctx, c)
c = if t == "xyz" {
resolve-xyz(c)
} else if t == "previous" {
ctx.prev.pt
Expand All @@ -348,9 +358,10 @@
}.map(util.resolve-number.with(ctx))

if update {
ctx.prev.pt = out
ctx.prev.pt = c
}
result.push(out)

result.push(c)
}

return (ctx, ..result)
Expand Down
2 changes: 1 addition & 1 deletion src/draw.typ
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
#import "draw/styling.typ": set-style, fill, stroke, register-mark
#import "draw/shapes.typ": circle, circle-through, arc, arc-through, mark, line, grid, content, rect, bezier, bezier-through, catmull, hobby, merge-path
#import "draw/projection.typ": ortho, on-xy, on-xz, on-yz
#import "draw/util.typ": assert-version
#import "draw/util.typ": assert-version, register-coordinate-resolver
1 change: 0 additions & 1 deletion src/draw/grouping.typ
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,6 @@
assert(name != none and name != "" and not name.starts-with("."),
message: "Anchors must not be none, \"\" or start with \".\"!")

coordinate.resolve-system(position)
return (ctx => {
let (ctx, position) = coordinate.resolve(ctx, position)
position = util.apply-transform(ctx.transform, position)
Expand Down
33 changes: 1 addition & 32 deletions src/draw/shapes.typ
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@
assert.eq(style.pos(), (), message: "Unexpected positional arguments: " + repr(style.pos()))
style = style.named()

(a, b, c).map(coordinate.resolve-system)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think these should be removed, they help tell the user where they've entered an invalid coordinate

Copy link
Member

Choose a reason for hiding this comment

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

actually I see why they've been removed, thats a really difficult trade off.
Is there a way we could keep track of how many or which draw functions have been created and let the user know about it? Essentially create our own stack trace. It probably shouldn't go in this pr but I would like to at least explore it before this pr gets approved.

Copy link
Member Author

@johannes-wolf johannes-wolf Oct 18, 2024

Choose a reason for hiding this comment

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

We could add some trace list to the context, that gets shown with any panic/assert we do. Something like element information (function name + element name, if given). This might be slow though, as we have to append and merge that list for every element.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've created a PR for a possible implementation of such functionality here: #726.


return (ctx => {
let (ctx, a, b, c) = coordinate.resolve(ctx, a, b, c)

Expand Down Expand Up @@ -220,9 +218,6 @@
)
let style = style.named()

// Coordinate check
let t = coordinate.resolve-system(position)

let start-angle = if start == auto { stop - delta } else { start }
let stop-angle = if stop == auto { start + delta } else { stop }
// Border angles can break if the angle is 0.
Expand Down Expand Up @@ -445,8 +440,6 @@
to = ((rel: (to, 1), to: from))
}

(from, to).map(coordinate.resolve-system)

return (ctx => {
let (ctx, ..pts) = coordinate.resolve(ctx, from, to)
let style = styles.resolve(ctx.style, merge: style, root: "mark")
Expand Down Expand Up @@ -506,9 +499,6 @@

assert(pts.len() >= 2, message: "Line must have a minimum of two points")

// Coordinate check
let pts-system = pts.map(coordinate.resolve-system)

// Find the intersection between line a-b next to b
// if no intersection could be found, return a.
let element-line-intersection(ctx, elem, a, b) = {
Expand All @@ -534,6 +524,7 @@
return (ctx => {
let first-elem = pts.first()
let last-elem = pts.last()
let pts-system = pts.map(coordinate.resolve-system.with(ctx))
let (ctx, ..pts) = coordinate.resolve(ctx, ..pts)

// If the first/last element, test for intersection
Expand Down Expand Up @@ -610,8 +601,6 @@
/// ## Anchors
/// Supports border anchors.
#let grid(from, to, name: none, ..style) = {
(from, to).map(coordinate.resolve-system)

assert.eq(style.pos(), (), message: "Unexpected positional arguments: " + repr(style.pos()))
style = style.named()

Expand Down Expand Up @@ -770,16 +759,6 @@
panic("Expected 2 or 3 positional arguments, got " + str(args.len()))
}

coordinate.resolve-system(a)

if b != auto {
coordinate.resolve-system(b)
}

if type(angle) != typst-angle {
coordinate.resolve-system(angle)
}

return (ctx => {
let style = styles.resolve(ctx.style, merge: style, root: "content")
let padding = util.as-padding-dict(style.padding)
Expand Down Expand Up @@ -1016,9 +995,6 @@
/// Supports border and path anchors. It's default is the `"center"` anchor.
///
#let rect(a, b, name: none, anchor: none, ..style) = {
// Coordinate check
let t = (a, b).map(coordinate.resolve-system)

// No extra positional arguments from the style sink
assert.eq(
style.pos(),
Expand Down Expand Up @@ -1208,9 +1184,6 @@
)
let coordinates = (start, ..ctrl, end)

// Coordinates check
let t = coordinates.map(coordinate.resolve-system)

return (
ctx => {
let (ctx, start, ..ctrl, end) = coordinate.resolve(ctx, ..coordinates)
Expand Down Expand Up @@ -1310,8 +1283,6 @@

assert(pts.len() >= 2, message: "Catmull-rom curve requires at least two points. Got " + repr(pts.len()) + "instead.")

pts.map(coordinate.resolve-system)

return (ctx => {
let (ctx, ..pts) = coordinate.resolve(ctx, ..pts)
let style = styles.resolve(ctx.style, merge: style, root: "catmull")
Expand Down Expand Up @@ -1385,8 +1356,6 @@

assert(pts.len() >= 2, message: "Hobby curve requires at least two points. Got " + repr(pts.len()) + "instead.")

pts.map(coordinate.resolve-system)

return (ctx => {
let (ctx, ..pts) = coordinate.resolve(ctx, ..pts)
let style = styles.resolve(ctx.style, merge: style, root: "hobby")
Expand Down
4 changes: 0 additions & 4 deletions src/draw/transformations.typ
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,6 @@
///
/// - pt (coordinate): The coordinate to move to.
#let move-to(pt) = {
let t = coordinate.resolve-system(pt)

return (ctx => {
let (ctx, pt) = coordinate.resolve(ctx, pt)
return (ctx: ctx)
Expand All @@ -244,8 +242,6 @@
/// - bounds (vector): Viewport bounds vector that describes the inner width,
/// height and depth of the viewport
#let set-viewport(from, to, bounds: (1, 1, 1)) = {
(from, to).map(coordinate.resolve-system)

return (ctx => {
let bounds = vector.as-vec(bounds, init: (1, 1, 1))

Expand Down
38 changes: 38 additions & 0 deletions src/draw/util.typ
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,41 @@
return (ctx: ctx)
},)
}

/// Push a custom coordinate resolve function to the list of coordinate
/// resolvers. This resolver is scoped to the current context scope!
///
/// A coordinate resolver must be a function of the format `(context, coordinate) => coordinate`. And must _always_ return a valid coordinate or panic, in case of an error.
///
/// If multiple resolvers are registered, coordinates get passed through all
/// resolvers in reverse registering order. All coordinates get paased to cetz'
/// default coordinate resolvers.
///
/// ```typc example
/// register-coordinate-resolver((ctx, c) => {
/// if type(c) == dictionary and "log" in c {
/// c = c.log.map(n => calc.log(n, base: 10))
/// }
/// return c
/// })
///
/// circle((log: (10, 0)), radius: .25)
/// circle((log: (100, 0)), radius: .25)
/// circle((log: (1000, 0)), radius: .25)
/// ```
///
/// - resolver (function): The resolver function, taking a context and a single coordinate and returning a single coordinate
#let register-coordinate-resolver(resolver) = {
assert.eq(type(resolver), function,
message: "Coordinate resolver must be of type function (ctx, coordinate) => coordinate.")

return (ctx => {
if type(ctx.resolve-coordinate) == array {
ctx.resolve-coordinate.push(resolver)
} else {
ctx.resolve-coordinate = (resolver,)
}

return (ctx: ctx)
},)
}
6 changes: 0 additions & 6 deletions src/lib/decorations/brace.typ
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@
assert.eq(style.pos().len(), 0,
message: "Brace takes no additional positional arugments.")

// Validate coordinates
let _ = (start, end).map(coordinate.resolve-system)

group(name: name, ctx => {
// Resolve all coordinates
let (ctx, start, end) = coordinate.resolve(ctx, start, end)
Expand Down Expand Up @@ -195,9 +192,6 @@
name: none,
..style,
) = {
// Validate coordinates
let _ = (start, end).map(coordinate.resolve-system)

group(name: name, ctx => {
// Get styles and validate their types and values
let style = styles.resolve(ctx.style, merge: style.named(),
Expand Down
Binary file added tests/coordinate/custom/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.
26 changes: 26 additions & 0 deletions tests/coordinate/custom/test.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#set page(width: auto, height: auto)
#import "/src/lib.typ": *
#import "/tests/helper.typ": *

#test-case({
import draw: *
grid((-2,-1), (7,1), stroke: gray)

let log-resolver(ctx, coordinate) = {
if type(coordinate) == dictionary and "log" in coordinate {
coordinate = coordinate.log
coordinate = coordinate.map(n => calc.log(calc.max(n, util.float-epsilon), base: 10))
}

return coordinate
}

register-coordinate-resolver(log-resolver)

set-style(circle: (radius: .1))
for i in (.1, 1, 10, 100, 1000, 10000) {
let pt = (log: (i * 1, 1))
circle(pt)
content(pt, repr(i), anchor: "north", padding: (top: .5))
}
})
Loading