From 74bd30a171842f6ef698f4681b6f2704f7f2e46f Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 1 Sep 2025 16:39:27 +0200 Subject: [PATCH 1/3] coordinate: Add projection coordinate --- src/coordinate.typ | 30 ++++++++++++++++++ .../lerp}/ref/1.png | Bin .../lerp}/test.typ | 0 tests/coordinate/project/ref/1.png | Bin 0 -> 1800 bytes tests/coordinate/project/test.typ | 21 ++++++++++++ .../ref/1.png | Bin 23727 -> 21951 bytes .../test.typ | 11 ++----- 7 files changed, 53 insertions(+), 9 deletions(-) rename tests/{coordinate-lerp => coordinate/lerp}/ref/1.png (100%) rename tests/{coordinate-lerp => coordinate/lerp}/test.typ (100%) create mode 100644 tests/coordinate/project/ref/1.png create mode 100644 tests/coordinate/project/test.typ diff --git a/src/coordinate.typ b/src/coordinate.typ index a5c5c124..42495dd0 100644 --- a/src/coordinate.typ +++ b/src/coordinate.typ @@ -251,6 +251,30 @@ return vector.add(a, vector.scale(ab, distance)) } +// Resolve a projection coordinate. +// +// (project: p, onto: (a, b)) +// (p, "_|_", a, b) +// (p, "⟂", a, b) +#let resolve-project-point-on-line(resolve, ctx, c) = { + let (ctx, a, b, p) = if type(c) == dictionary { + let (project: p, onto: (a, b)) = c + (_, a, b) = resolve(ctx, a, b) + (ctx, p) = resolve(ctx, p) + (ctx, a, b, p) + } else { + let (p, _, a, b) = c + (_, a, b) = resolve(ctx, a, b) + (ctx, p) = resolve(ctx, p) + (ctx, a, b, p) + } + + let ap = vector.sub(p, a) + let ab = vector.sub(b, a) + + return vector.add(a, vector.scale(ab, vector.dot(ap, ab) / vector.dot(ab, ab))) +} + #let resolve-function(resolve, ctx, c) = { let (func, ..c) = c (ctx, ..c) = resolve(ctx, ..c) @@ -290,6 +314,8 @@ "relative" } else if len in (3, 4) and keys.all(k => k in ("a", "number", "angle", "abs", "b")) { "lerp" + } else if len == 2 and keys.all(k => k in ("project", "onto")) { + "project" } } else if type(c) == array { let len = c.len() @@ -304,6 +330,8 @@ "perpendicular" } else if len in (3, 4) and types.at(1) in (int, float, length, ratio) and (len == 3 or (len == 4 and types.at(2) == angle)) { "lerp" + } else if len == 4 and c.at(1) in ("_|_", "⟂") { + "project" } else if len >= 2 and types.first() == function { "function" } @@ -370,6 +398,8 @@ c } else if t == "lerp" { resolve-lerp(resolve, ctx, c) + } else if t == "project" { + resolve-project-point-on-line(resolve, ctx, c) } else if t == "function" { resolve-function(resolve, ctx, c) } else { diff --git a/tests/coordinate-lerp/ref/1.png b/tests/coordinate/lerp/ref/1.png similarity index 100% rename from tests/coordinate-lerp/ref/1.png rename to tests/coordinate/lerp/ref/1.png diff --git a/tests/coordinate-lerp/test.typ b/tests/coordinate/lerp/test.typ similarity index 100% rename from tests/coordinate-lerp/test.typ rename to tests/coordinate/lerp/test.typ diff --git a/tests/coordinate/project/ref/1.png b/tests/coordinate/project/ref/1.png new file mode 100644 index 0000000000000000000000000000000000000000..027a7199db8928378e0da928e478c9dfac63ebc5 GIT binary patch literal 1800 zcmbVM3saL<8qQjj5El{4MtZpr(tEMg{4#(84_Q%VVQP#kRMlVk{UomRv_%jT63}Oo-TqN&C!3$NU0{ z4u$9;K=r75^(cXF!1E1kHXDsb@7iicurM$6rWms`AQltKJd}2ID)LD(RGb(2Bq`$s z+f5tv^SrLxs5w6&X@1kL8()Hk21kBf8DE~7TU{`IT!W@2o-Yo4uW2$P|JD0WIF!tQ zQW(bcWR-{Ws1Jb0;|T;ph!wij5tj}-+3P9^4^B@{4+;tb0Dxm}OE5ESawZZER;8q* zY&oHijg75!!jJd>N)Km=GZBw8MWIk=Q&Y5w2?A+R-_VS(_i}J>2(q+5IMEQFItQJ) zzC#RPO;_`MknapM@%*n_sx-xyNV?TG&kH zc$Y??@hnB(eZRE;aNREJEcf<`qW=&*sqnKR%p|pZ&|jzR`=%!>mp@I`7G}+QS$a$m zc`&}KT_h35h-C0$`L({B6PD_{{PovuhuC9bhs_3(M9zDj$G^n$vvIAn^QXk(DM$JM zkbfan(5I~jsx(hii4P~-c-i)?vsNBAc+Rb@ z!9K-g5HBm{)*s>)ze5pUdU);(XQdaPxeDxl&xt!GsGon={CzdzZT|sTBy$_2?0m0# zHR$6wROG0oZg0)%@UXJHvCixD7nGzSg-N1ix8wb;`^u*Y^nZhS+f%+PyVeQsdJASl zL~H7vg1Rrdc3UnrxbS*?1+$kmXJ2DTZvk25b6X=xNa`=Ybz) z7$Sk@job0(4NmlaFz-U`!QZ&vmwqP`J90h$0Nd$!a2_3xvlE=uw{tI?xCCUE>r;mx zS?T9Vp)mjH_59Qd+0jI#u+a85#Z_R2bYl1zQh40f5SDv)DPOcp-LjGT@KP(yZcpX! zW-n`QDta*_18mLim=Qx-g2C`f_WqvGFB3#=>K1HxfgU_K!a@pjH&pIX&4fi8K-O*A zv3@DW6g+@xmiLs!wNt?-=FRe-KDo8xe(}qVE-4o9#lq==MhvL~knNub#=Ln^K5#3N zAe*jmQ4U;eZ?0!(e?ZhfZ%2t*;F6DsnB|4Sxo) z1lk&$=Av6Q)8|??$-AZ7zFO0mfEk*elXZTxdd>Y_6E95N%!5Jc;}=8G*q$;y{A8f5 zlet6Sq(57cfOWPGoGrm|dJp>TjXJ#yTk_i{zG1i}`jZQvy?oW+u~3-x6=iMZNY3ZX)1G88 z3w%k*)8pALAK%{j@!`H=`FfeI*Fg~ycnJ`K7v4M?io6SwVBwx?v%gL=M=Ki} z9lELl^9v;Jrp2zGk{dq0XZDp_PYMkCo*BNq+}4)t>T8Y^GDKq&97;@B#2Mhf<%1En gf8w9|KNJ;r3e6(7&?"), name: "v1") + line(o, b, mark: (end: ">"), name: "v2") + + line("v1.80%", (project: (), onto: ("v2.start", "v2.end")), + stroke: red) + + line("v1.60%", ((), "_|_", "v2.start", "v2.end"), + stroke: blue) + + line("v1.40%", ((), "⟂", "v2.start", "v2.end"), + stroke: green) +}) diff --git a/tests/line-element-element-intersection/ref/1.png b/tests/line-element-element-intersection/ref/1.png index 2e27fb8c2e9d2da74070678ac3f39051eb053299..e562a8553f2106e52df0a8b1c32b698d25cd9a48 100644 GIT binary patch literal 21951 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 literal 23727 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 J%J9U?e*hM)t04dY diff --git a/tests/line-element-element-intersection/test.typ b/tests/line-element-element-intersection/test.typ index c590b0ad..50e68e6d 100644 --- a/tests/line-element-element-intersection/test.typ +++ b/tests/line-element-element-intersection/test.typ @@ -83,19 +83,12 @@ return coord }) - scale(4) - arc((), start: 15deg, stop: 35deg, radius: 5mm, mode: "PIE", fill: color.mix((green, 20%), white), anchor: "origin") let orig = (0, 0) - let (o, a, b) = ((0, 0), (35deg, 1cm), (15deg, 1.25cm)) + let (o, a, b) = ((0, 0), (35deg, 2), (15deg, 2.25)) line(o, a, mark: (end: ">"), name: "v1") line(o, b, mark: (end: ">"), name: "v2") - // Because of a bug with `line`, curstom coordinates do not work properly, - // so we create a named anchor. - anchor("pt", ("v1.end", "perpendicular", "v2.start", "v2.end")) - //line("v1.end", "pt", stroke: red) - // #944: ERROR resolving coordinate: - line("v1.end", ("v1.end", "perpendicular", "v2.start", "v2.end"), + line("v1.80%", ((), "perpendicular", "v2.start", "v2.end"), stroke: red) }) From 0a8d53b75ca3eef4b1dc5c4660481cf5520b62ff Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 1 Sep 2025 16:46:31 +0200 Subject: [PATCH 2/3] docs: Add new coordinate system to docs --- CHANGES.md | 2 ++ docs/basics/coordinate-systems.mdx | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index bc2d2cc5..29c53590 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ - The `merge-path` element now support `mark:`; by default, marks of the source elements get removed (#922, #948) - The `intersections` element ignores mark shapes by default (see `ignore-marks:`) (#948) +- Added a new `(project: , onto: (, ))` + coordinate for projecting a point onto a line (short form: `(pt, "_|_", a, b)`) # 0.4.1 - Added a `n-star` shape for drawing n-pointed stars diff --git a/docs/basics/coordinate-systems.mdx b/docs/basics/coordinate-systems.mdx index ce259b02..83f2444d 100644 --- a/docs/basics/coordinate-systems.mdx +++ b/docs/basics/coordinate-systems.mdx @@ -376,6 +376,22 @@ circle(a, radius: .1, fill: black) line((a, .7, b), (a: (), b: a, number: .5, angle: 90deg), stroke: red) ``` +## Projection + +To project a point `pt` onto a line from `a` to `b`, you can use the +`(project: pt, onto: (a, b))` or short `(pt, "_|_", a, b)` coordinate. + +```typc exapmle +set-style(fill: black, radius: 0.1) + +circle(name: "A", (0, 0)) +circle(name: "B", (3, 1)) +circle(name: "P", (1.9, -1.6)) + +line("A", "B") +line("P", (project: "P", onto: ("A", "B"))) +``` + ## Function An array where the first element is a function and the rest are coordinates will cause the function to be called with the resolved coordinates. The resolved coordinates will be given as a vector that represents an xyz point in space. From 90eded637994b0a3a589977675252666d556a7f9 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 1 Sep 2025 19:11:11 +0200 Subject: [PATCH 3/3] path-mod: Basic Path-Modifier Implementation --- src/canvas.typ | 5 +- src/draw/shapes.typ | 40 ++++++++++++++++ src/path-modifier.typ | 54 ++++++++++++++++++++++ src/path-util.typ | 103 ++++++++++++++++++++++++++++++++++++++---- src/styles.typ | 23 ++++++---- 5 files changed, 206 insertions(+), 19 deletions(-) create mode 100644 src/path-modifier.typ diff --git a/src/canvas.typ b/src/canvas.typ index 9a25b9b6..df88880e 100644 --- a/src/canvas.typ +++ b/src/canvas.typ @@ -8,6 +8,7 @@ #import "styles.typ" #import "process.typ" #import "coordinate.typ" +#import "path-modifier.typ" /// Sets up a canvas for drawing on. /// @@ -63,8 +64,10 @@ mnemonics: (:), marks: (:), ), - // coordinate resolver + // Coordinate resolver resolve-coordinate: (), + // Path modifiers + path-modifiers: path-modifier.builtin, // Shared state that is not scoped by group/scope elements. // CeTZ itself does not use this dictionary for data. shared-state: (:), diff --git a/src/draw/shapes.typ b/src/draw/shapes.typ index bff5ab12..17331c8c 100644 --- a/src/draw/shapes.typ +++ b/src/draw/shapes.typ @@ -16,6 +16,7 @@ #import "/src/mark-shapes.typ" as mark-shapes_ #import "/src/polygon.typ" as polygon_ #import "/src/aabb.typ" +#import "/src/path-modifier.typ": apply-modifiers #import "transformations.typ": * #import "styling.typ": * @@ -81,6 +82,9 @@ stroke: style.stroke ) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let (transform, anchors) = anchor_.setup( (_) => center, ("center",), @@ -149,6 +153,9 @@ stroke: style.stroke ) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let (transform, anchors) = anchor_.setup( (anchor) => ( center: center, @@ -259,6 +266,9 @@ mode: style.mode ) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let sector-center = ( x - rx * calc.cos(start-angle), y - ry * calc.sin(start-angle), @@ -579,6 +589,9 @@ stroke: style.stroke, ) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + // Get bounds let (transform, anchors) = anchor_.setup( name => { @@ -650,6 +663,9 @@ fill: style.fill, stroke: style.stroke) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let edge-anchors = range(0, sides).map(i => "edge-" + str(i)) let corner-anchors = range(0, sides).map(i => "corner-" + str(i)) @@ -768,6 +784,9 @@ } } + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let edge-anchors = range(0, sides * 2).map(i => "edge-" + str(i)) let corner-anchors = range(0, sides * 2).map(i => "corner-" + str(i)) @@ -955,6 +974,9 @@ drawables += outer-strips.map(s => drawable.line-strip( s, stroke: style.stroke, close: at-border.all(v => v))) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let center = vector.lerp(from, to, .5) let (transform, anchors) = anchor_.setup( _ => center, @@ -1434,6 +1456,9 @@ fill: style.fill, stroke: style.stroke) } + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + // Calculate border anchors let center = vector.lerp(a, b, .5) let (width, height, ..) = size @@ -1514,6 +1539,9 @@ stroke: style.stroke, ) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let (transform, anchors) = anchor_.setup( anchor => ( ctrl-0: ctrl.at(0), @@ -1613,6 +1641,9 @@ fill-rule: style.fill-rule, stroke: style.stroke) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let (transform, anchors) = { let a = for (i, pt) in pts.enumerate() { (("pt-" + str(i)): pt) @@ -1687,6 +1718,9 @@ fill-rule: style.fill-rule, stroke: style.stroke) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let (transform, anchors) = { let a = for (i, pt) in pts.enumerate() { (("pt-" + str(i)): pt) @@ -1767,6 +1801,9 @@ fill: style.fill, fill-rule: style.fill-rule, stroke: style.stroke, subpaths) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let (transform, anchors) = anchor_.setup( name => { if name == "centroid" { @@ -1868,6 +1905,9 @@ let style = styles.resolve(ctx.style, merge: style) let drawables = drawable.path(fill: style.fill, fill-rule: style.fill-rule, stroke: style.stroke, subpaths) + // Apply modifiers + drawables = apply-modifiers(ctx, style, drawables) + let (transform, anchors) = anchor_.setup( name => { if name == "centroid" { diff --git a/src/path-modifier.typ b/src/path-modifier.typ new file mode 100644 index 00000000..adf296d2 --- /dev/null +++ b/src/path-modifier.typ @@ -0,0 +1,54 @@ +// Shorten or extend a path. +#let _shorten-path(ctx, style, path) = { + import "path-util.typ": shorten-to + + let shorten = style.at("shorten", default: (0, 0)) + if type(shorten) != array { + shorten = (shorten, shorten) + } + + // Early exit on zero lengths + if shorten.all(v => v in (0, 0%, 0pt)) { + return none + } + + // Do not attempt to shorten/extend closed paths + let (origin, closed, segments) = path.first() + if closed or segments == () { + return none + } + + return shorten-to(path, shorten, ignore-subpaths: true) +} + + +#let builtin = ( + shorten: _shorten-path, +) + +/// Apply all enabled modifiers onto a path. +/// +/// - ctx (context): +/// - style (style): +/// - path (path, array): A list of paths or a single path +/// -> path|array +#let apply-modifiers(ctx, style, path) = { + if type(path) == array { + return path.map(p => apply-modifiers(ctx, style, p)) + } + + let all-modifiers = ctx.at("path-modifiers", default: ()) + let enabled-modifiers = style.at("modifiers", default: ()) + + for name in enabled-modifiers { + assert(name in all-modifiers, + message: "No modifier named '" + name + "' registered.") + + let new-path = (all-modifiers.at(name))(ctx, style, path.segments) + if new-path != none { + path.segments = new-path + } + } + + return path +} diff --git a/src/path-util.typ b/src/path-util.typ index 6b50772e..2e5f9814 100644 --- a/src/path-util.typ +++ b/src/path-util.typ @@ -76,7 +76,11 @@ let (origin, _, segments) = path.first() let (kind, ..args) = segments.first() if kind == "l" { - return vector.dir(origin, args.last()) + let v = vector.sub(origin, args.last()) + if vector.len(v) != 0 { + return vector.norm(v) + } + return v } else if kind == "c" { let (c1, c2, e) = args return bezier.cubic-derivative(origin, e, c1, c2, 0) @@ -96,7 +100,11 @@ let (kind, ..args) = segments.last() if kind == "l" { - return vector.dir(origin, args.last()) + let v = vector.sub(origin, args.last()) + if vector.len(v) != 0 { + return vector.norm(v) + } + return v } else if kind == "c" { let (c1, c2, e) = args return bezier.cubic-derivative(e, origin, c2, c1, 0) @@ -193,6 +201,7 @@ /// - path (path): The path /// - distance (ratio, number): Distance along the path /// - reverse (bool): Travel from end to start +/// - clamp (bool): Clamp distance between 0%-100% /// - ignore-subpaths (bool): If false consider the whole path, including sub-paths /// /// -> none,dictionary Dictionary with the following keys: @@ -202,7 +211,7 @@ /// - subpath-index (int) Index of the subpath /// - segment-index (int) Index of the segment /// None is returned, if the path is empty/of length zero. -#let point-at(path, distance, reverse: false, samples: auto, ignore-subpaths: true) = { +#let point-at(path, distance, reverse: false, clamp: true, samples: auto, ignore-subpaths: true) = { if samples == auto { samples = number-of-samples(samples) } @@ -309,7 +318,7 @@ #let _shorten-line(origin, previous, args, distance, reverse: false) = { let pt = args.last() let length = vector.dist(previous, pt) - if length > 0 { + if length != 0 { let t = if reverse { 1 - distance / length } else { @@ -355,24 +364,87 @@ } } -/// Shorten a path on one or both sides +// Extend a path by `distance` by placing/extending straight +// lines at the start/end +// +// - path (path): Input path +// - distance (float): Distance to extend the path by +// - reverse (bool): If true, extend the beginning of the path instead +// of the end +// - samples (auto, int): Number of samples to use for sampling curves +// -> path +#let _extend-path(path, distance, reverse: false, samples: auto) = { + if reverse { + let (origin, closed, segments) = path.first() + if type(segments.first()) != array { panic(path) } + let (kind, ..args) = segments.first() + if kind == "l" { + let dir = vector.sub(args.last(), origin) + if vector.len(dir) != 0 { dir = vector.norm(dir) } + origin = vector.add(origin, vector.scale(dir, distance)) + return ((origin, false, segments),) + path.slice(1) + } else { + // We extend cubic beziers by just appending straight lines. + let (c1, c2, e) = args + let dir = bezier.cubic-derivative(origin, e, c1, c2, 0) + let old-origin = origin + origin = vector.add(origin, vector.scale(vector.norm(dir), distance)) + return ((origin, false, (("l", old-origin),) + segments),) + path.slice(1) + } + } else { + let (origin, closed, segments) = path.last() + let (kind, ..args) = segments.last() + let prev = if segments.len() > 1 { + segments.at(-2).last() + } else { + origin + } + + let last-segment = if kind == "l" { + let dir = vector.sub(prev, args.last()) + if vector.len(dir) != 0 { dir = vector.norm(dir) } + let end = vector.add(args.last(), vector.scale(dir, distance)) + + // Prepend a straight line + (("l", end),) + } else { + // We extend cubic beziers by just appending straight lines. + let (c1, c2, e) = args + let dir = bezier.cubic-derivative(prev, e, c1, c2, 1) + let end = vector.add(e, vector.scale(vector.norm(dir), -distance)) + + // We re-add the curve + some straight tail + (("c", c1, c2, e), ("l", end),) + } + + return path.slice(0, -1) + ((origin, false, segments.slice(0, -1) + last-segment),) + } +} + +/// Shorten or extend a path on one or both sides /// /// - path (Path): Path /// - distance (number,ratio,array): Distance to shorten the path by -/// - reverse (boolean): If true, start from the end +/// - reverse (bool): If true, start from the end +/// - ignore-subpaths (bool): Only shorten/extend the first sub-path /// - mode ('CURVED','LINEAR'): Shortening mode for cubic segments /// - samples (auto,int): Samples to take for measuring cubic segments /// - snap-to (none,array): Optional array of points to try to move the shortened segment to -#let shorten-to(path, distance, reverse: false, +#let shorten-to(path, distance, reverse: false, ignore-subpaths: true, mode: "CURVED", samples: auto, snap-to: none) = { let snap-to-threshold = 1e-4 + // Shortcut zero length + if distance == 0 or distance == 0% or distance == (0, 0) { + return path + } + // Shorten from both sides if type(distance) == array { let original-length = length(path) let (start, end) = distance.map(v => { if type(v) == ratio { - v * original-length + original-length * v / 100% } else { v } @@ -381,8 +453,23 @@ path = shorten-to(path, start, reverse: reverse, mode: mode, samples: samples, snap-to: if snap-to != none { snap-to.first() } else { none }) path = shorten-to(path, end, reverse: not reverse, mode: mode, samples: samples, snap-to: if snap-to != none { snap-to.last() } else { none }) return path + } else if type(distance) == ratio { + let length = length(path) + distance = length * distance / 100% + } + + // Extend the path + if distance < 0 { + if ignore-subpaths { + return _extend-path(path.slice(0, 1), distance, + reverse: reverse, samples: samples) + path.slice(1) + } else { + return _extend-path(path, distance, + reverse: reverse, samples: samples) + } } + // Shorten the path let point = point-at(path, distance, reverse: reverse) if point != none { // Find the subpath to modify diff --git a/src/styles.typ b/src/styles.typ index 9c29d6b8..eeaacc14 100644 --- a/src/styles.typ +++ b/src/styles.typ @@ -5,10 +5,7 @@ fill-rule: "non-zero", stroke: black + 1pt, radius: 1, - /// Bezier shortening mode: - /// - "LINEAR" Moving the affected point and it's next control point (like TikZ "quick" key) - /// - "CURVED" Preserving the bezier curve by calculating new control points - shorten: "LINEAR", + modifiers: ("shorten",), // Allowed values: // - none @@ -57,42 +54,44 @@ circle: ( radius: auto, stroke: auto, - fill: auto + fill: auto, + modifiers: auto, ), group: ( padding: auto, fill: auto, - stroke: auto + stroke: auto, ), line: ( mark: auto, fill: auto, fill-rule: auto, stroke: auto, + modifiers: auto, ), bezier: ( stroke: auto, fill: auto, fill-rule: auto, mark: auto, - shorten: auto, + modifiers: auto, ), catmull: ( tension: .5, mark: auto, - shorten: auto, stroke: auto, fill: auto, fill-rule: auto, + modifiers: auto, ), hobby: ( /// Curve start and end omega (curlyness) omega: (0,0), mark: auto, - shorten: auto, stroke: auto, fill: auto, fill-rule: auto, + modifiers: auto, ), rect: ( /// Rect corner radius that supports the following types: @@ -108,6 +107,7 @@ radius: 0, stroke: auto, fill: auto, + modifiers: auto, ), arc: ( // Supported values: @@ -119,13 +119,15 @@ mark: auto, stroke: auto, fill: auto, - radius: auto + radius: auto, + modifiers: auto, ), polygon: ( radius: auto, stroke: auto, fill: auto, fill-rule: auto, + modifiers: auto, ), n-star: ( radius: auto, @@ -133,6 +135,7 @@ fill: auto, // Connect inner points of the star show-inner: false, + modifiers: auto, ), content: ( padding: auto,