Skip to content
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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <coordinate>, onto: (<coordinate>, <coordinate>))`
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
Expand Down
16 changes: 16 additions & 0 deletions docs/basics/coordinate-systems.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Type>vector</Type> that represents an xyz point in space.
Expand Down
5 changes: 4 additions & 1 deletion src/canvas.typ
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#import "styles.typ"
#import "process.typ"
#import "coordinate.typ"
#import "path-modifier.typ"

/// Sets up a canvas for drawing on.
///
Expand Down Expand Up @@ -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: (:),
Expand Down
30 changes: 30 additions & 0 deletions src/coordinate.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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"
}
Expand Down Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions src/draw/shapes.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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": *
Expand Down Expand Up @@ -81,6 +82,9 @@
stroke: style.stroke
)

// Apply modifiers
drawables = apply-modifiers(ctx, style, drawables)

let (transform, anchors) = anchor_.setup(
(_) => center,
("center",),
Expand Down Expand Up @@ -149,6 +153,9 @@
stroke: style.stroke
)

// Apply modifiers
drawables = apply-modifiers(ctx, style, drawables)

let (transform, anchors) = anchor_.setup(
(anchor) => (
center: center,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -579,6 +589,9 @@
stroke: style.stroke,
)

// Apply modifiers
drawables = apply-modifiers(ctx, style, drawables)

// Get bounds
let (transform, anchors) = anchor_.setup(
name => {
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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" {
Expand Down
54 changes: 54 additions & 0 deletions src/path-modifier.typ
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading