diff --git a/src/anchor.typ b/src/anchor.typ index 9f25a475e..82f92edb2 100644 --- a/src/anchor.typ +++ b/src/anchor.typ @@ -2,8 +2,6 @@ #import deps.oxifmt: strfmt #import "util.typ" -#import util: typst-length - #import "intersection.typ" #import "drawable.typ" #import "path-util.typ" diff --git a/src/canvas.typ b/src/canvas.typ index 57c7df766..d85cca9be 100644 --- a/src/canvas.typ +++ b/src/canvas.typ @@ -7,8 +7,6 @@ #import "process.typ" #import "version.typ" -#import util: typst-length - /// Sets up a canvas for drawing on. /// /// - length (length, ratio): Used to specify what 1 coordinate unit is. If given a ratio, that ratio is relative to the containing elements width! @@ -26,7 +24,7 @@ message: "Incorrect type for body: " + repr(type(body)), ) - assert(type(length) in (typst-length, ratio), message: "Expected `length` to be of type length or ratio, got " + repr(length)) + assert(type(length) in (std.length, ratio), message: "Expected `length` to be of type length or ratio, got " + repr(length)) let length = if type(length) == ratio { length * ly.width } else { @@ -39,6 +37,8 @@ version: version.version, length: length, debug: debug, + // Backtrace info (list of strings) + backtrace: (), // Previous element position & bbox prev: (pt: (0, 0, 0)), style: styles.default, @@ -58,7 +58,9 @@ marks: ( mnemonics: (:), marks: (:), - ) + ), + // coordinate resolver + resolve-coordinate: none, ) let (ctx, bounds, drawables) = process.many(ctx, body) @@ -120,7 +122,7 @@ vertices += pts } } - if type(drawable.stroke) == dictionary and "thickness" in drawable.stroke and type(drawable.stroke.thickness) != typst-length { + if type(drawable.stroke) == dictionary and "thickness" in drawable.stroke and type(drawable.stroke.thickness) != std.length { drawable.stroke.thickness *= length } path( @@ -130,7 +132,7 @@ ..vertices, ) } else if drawable.type == "content" { - let (width, height) = util.typst-measure(drawable.body) + let (width, height) = std.measure(drawable.body) move( dx: (drawable.pos.at(0) - bounds.low.at(0)) * length - width / 2, dy: (drawable.pos.at(1) - bounds.low.at(1)) * length - height / 2, diff --git a/src/coordinate.typ b/src/coordinate.typ index 7bd23113e..7d98b8710 100644 --- a/src/coordinate.typ +++ b/src/coordinate.typ @@ -1,5 +1,6 @@ #import "vector.typ" #import "util.typ" +#import "error.typ" #import "deps.typ" #import deps.oxifmt: strfmt @@ -68,7 +69,7 @@ } // Check if node is known - assert(name in ctx.nodes, + error.assert(ctx, name in ctx.nodes, message: "Unknown element '" + name + "' in elements " + repr(ctx.nodes.keys())) // Resolve length anchors @@ -137,7 +138,7 @@ // Distance between C and P let pc = vector.len(D) if pc < r { - panic("No tangent solution for element " + c.element + " and point " + repr(c.point)) + error.panic(ctx, "No tangent solution for element " + c.element + " and point " + repr(c.point)) } // Distance between P and X0 let d = r*r / pc @@ -251,7 +252,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() @@ -297,7 +298,7 @@ } if t == none { - panic("Failed to resolve coordinate: " + repr(c)) + error.panic(ctx, "Failed to resolve coordinate: " + repr(c)) } return t } @@ -319,10 +320,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 @@ -344,13 +355,14 @@ } else if t == "function" { resolve-function(resolve, ctx, c) } else { - panic("Failed to resolve coordinate of format: " + repr(c)) + error.panic(ctx, "Failed to resolve coordinate of format: " + repr(c)) }.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) diff --git a/src/draw.typ b/src/draw.typ index e46b87de9..cef4e6d2d 100644 --- a/src/draw.typ +++ b/src/draw.typ @@ -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 diff --git a/src/draw/grouping.typ b/src/draw/grouping.typ index d3dc791fa..c2038201d 100644 --- a/src/draw/grouping.typ +++ b/src/draw/grouping.typ @@ -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) diff --git a/src/draw/shapes.typ b/src/draw/shapes.typ index f52779cb5..c92ec3ada 100644 --- a/src/draw/shapes.typ +++ b/src/draw/shapes.typ @@ -1,6 +1,3 @@ -#let typst-angle = angle -#let typst-rotate = rotate - #import "/src/coordinate.typ" #import "/src/drawable.typ" #import "/src/styles.typ" @@ -16,6 +13,7 @@ #import "/src/mark-shapes.typ" as mark-shapes_ #import "/src/polygon.typ" #import "/src/aabb.typ" +#import "/src/error.typ" #import "transformations.typ": * #import "styling.typ": * @@ -52,6 +50,8 @@ let style = style.named() (ctx => { + ctx = error.add-element-backtrace(ctx, "circle", name) + let (ctx, pos) = coordinate.resolve(ctx, position) let style = styles.resolve(ctx.style, merge: style, root: "circle") let (rx, ry) = util.resolve-radius(style.radius).map(util.resolve-number.with(ctx)) @@ -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) - return (ctx => { let (ctx, a, b, c) = coordinate.resolve(ctx, a, b, c) @@ -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. @@ -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") @@ -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) = { @@ -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 @@ -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() @@ -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) @@ -794,7 +773,7 @@ (ctx, b) = coordinate.resolve(ctx, b) } - let angle = if type(angle) != typst-angle { + let angle = if type(angle) != std.angle { let c (ctx, c) = coordinate.resolve(ctx, angle) vector.angle2(a, c) @@ -930,7 +909,7 @@ aabb-width, aabb-height, border.segments, - typst-rotate(angle, + std.rotate(angle, reflow: true, origin: center + horizon, block( @@ -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(), @@ -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) @@ -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") @@ -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") diff --git a/src/draw/transformations.typ b/src/draw/transformations.typ index b8c07f020..7c9c748d7 100644 --- a/src/draw/transformations.typ +++ b/src/draw/transformations.typ @@ -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) @@ -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)) diff --git a/src/draw/util.typ b/src/draw/util.typ index 3f2c0d1df..3d4a99cb6 100644 --- a/src/draw/util.typ +++ b/src/draw/util.typ @@ -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) + },) +} diff --git a/src/error.typ b/src/error.typ new file mode 100644 index 000000000..186ca2450 --- /dev/null +++ b/src/error.typ @@ -0,0 +1,32 @@ +/// Add a backtrace entry for a single element +/// +/// - ctx (Context): The current context +/// - element (string): The elements type (e.g. circle) +/// - name (none,string): The elements name +/// -> context +#let add-element-backtrace(ctx, element, name) = { + let message = "element: " + element + if name != none { + message += ", name: " + name + } + + ctx.backtrace.push(message) + return ctx +} + +#let _get-backtrace-string(ctx) = { + if ctx != none and ctx.backtrace != () { + return ". Backtrace: " + ctx.backtrace.rev().join("; ") + } + return "" +} + +/// Panic but with cetz backtrace +#let panic(ctx, message) = { + std.panic(message + _get-backtrace-string(ctx)) +} + +/// Assert but with cetz backtrace +#let assert(ctx, cond, message: "") = { + std.assert(cond, message: message + _get-backtrace-string(ctx)) +} diff --git a/src/lib/decorations/brace.typ b/src/lib/decorations/brace.typ index 5571d38f4..4b9765299 100644 --- a/src/lib/decorations/brace.typ +++ b/src/lib/decorations/brace.typ @@ -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) @@ -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(), diff --git a/src/mark.typ b/src/mark.typ index 8e928464b..a47fe15cf 100644 --- a/src/mark.typ +++ b/src/mark.typ @@ -7,8 +7,6 @@ #import "mark-shapes.typ": get-mark #import "process.typ" -#import util: typst-length - /// Checks if a mark should be drawn according to the current style. /// - style (style): The current style. /// -> bool diff --git a/src/util.typ b/src/util.typ index 059fe32c9..07ca63d6d 100644 --- a/src/util.typ +++ b/src/util.typ @@ -9,10 +9,6 @@ /// Constant to be used as float rounding error #let float-epsilon = 0.000001 -#let typst-measure = measure -#let typst-length = length - - /// Multiplies vectors by a transformation matrix. If multiple vectors are given they are returned as an array, if only one vector is given only one will be returned, if a dictionary is given they will be returned in the dictionary with the same keys. /// /// - transform (matrix,function): The $4 \times 4$ transformation matrix or a function that accepts and returns a vector. @@ -200,7 +196,7 @@ /// - cnt (content): The content to measure. /// -> vector #let measure(ctx, cnt) = { - let size = typst-measure(cnt) + let size = std.measure(cnt) return ( calc.abs(size.width / ctx.length), calc.abs(size.height / ctx.length) @@ -275,11 +271,11 @@ let east = radii.at("east", default: auto) if north != auto or south != auto { - assert(west == auto and east == auto, + assert(ctx, west == auto and east == auto, message: "Corner radius north/south and west/east are mutually exclusive! Use per corner radii: north-west, .. instead.") } if west != auto or east != auto { - assert(north == auto and south == auto, + assert(ctx, north == auto and south == auto, message: "Corner radius north/south and west/east are mutually exclusive! Use per corner radii: north-west, .. instead.") } diff --git a/tests/coordinate/custom/ref/1.png b/tests/coordinate/custom/ref/1.png new file mode 100644 index 000000000..10c1cd307 Binary files /dev/null and b/tests/coordinate/custom/ref/1.png differ diff --git a/tests/coordinate/custom/test.typ b/tests/coordinate/custom/test.typ new file mode 100644 index 000000000..0c2137946 --- /dev/null +++ b/tests/coordinate/custom/test.typ @@ -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)) + } +})