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
3 changes: 3 additions & 0 deletions docs/basics/marks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,7 @@ line(..c)
<Parameter name="transform-shape" types="bool" default_value="true">
When `false` marks will not be stretched/affected by the current
transformation, marks will be placed after the path is transformed.
Under `perspective`, `false` keeps marks flat in paper space while still
scaling them by depth. Mark placement and path shortening also happen in that
projected paper space.
</Parameter>
70 changes: 62 additions & 8 deletions src/draw/projection.typ
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,40 @@
(ref-depth * x / w, ref-depth * y / w, z)
}

// Adapt mark placement for flat perspective marks by projecting the path first,
// then scaling mark-local geometry from the matching view-space tip depth.
#let _perspective-mark-placement-adapter(projection, scale-factor, transform, path) = {
let view-path = if transform != none {
drawable.apply-transform(transform, path).first()
} else {
path
}
let path = drawable.apply-transform(projection, view-path).first()
let path = drawable.apply-transform(matrix.transform-scale((1, 1, 0)), path).first()

let adjust-style = (style, mark-tip-info) => {
let view-point = path-util.segment-point-at(
view-path.segments,
mark-tip-info.subpath-index,
mark-tip-info.segment-index,
mark-tip-info.t,
)
let factor = scale-factor(view-point)
style = style
for key in ("length", "width", "inset") {
style.insert(key, style.at(key) * factor)
}
style
}

(
path: path,
transform: none,
projected: true,
adjust-style: adjust-style,
)
}

#let _sort-by-distance(drawables) = {
return drawables.sorted(key: d => {
let z = none
Expand Down Expand Up @@ -99,6 +133,9 @@
#let _resolve-reference-depth(ctx, body, view-rotation-matrix, distance, near) = {
let probe-ctx = ctx
probe-ctx.transform = view-rotation-matrix
// Probe the underlying scene only, otherwise mark placement can change
// the camera scaling when `transform-shape` toggles.
probe-ctx.ignore-marks = true
let (ctx: _, bounds: _, drawables: probe-drawables, elements: _) = process.many(
probe-ctx,
util.resolve-body(probe-ctx, body))
Expand All @@ -118,6 +155,9 @@

let probe-ctx = ctx
probe-ctx.transform = view-rotation-matrix
// Probe the underlying scene only, otherwise mark placement can change
// the camera scaling when `transform-shape` toggles.
probe-ctx.ignore-marks = true
let (ctx: _, bounds: _, drawables: probe-drawables, elements: _) = process.many(
probe-ctx,
util.resolve-body(probe-ctx, body))
Expand Down Expand Up @@ -163,13 +203,12 @@
// - cull-face (none,str): Enable back-face culling if set to `"cw"` for clockwise
// or `"ccw"` for counter-clockwise. Polygons of the specified order will not get drawn.
// - flatten (bool): Set $z=0$ for all geometry
#let _projection(body, view-matrix, projection-matrix, reset-transform: true, sorted: true, cull-face: "cw", flatten: false) = {
#let _projection(body, view-matrix, projection-matrix, mark-placement-adapter: none, reset-transform: true, sorted: true, cull-face: "cw", flatten: false) = {
(ctx => {
let transform = ctx.transform
let perspective-mode = type(projection-matrix) == function
let previous-perspective-mode = ctx.at("_perspective-projection", default: false)
if perspective-mode {
ctx._perspective-projection = true
let previous-mark-placement-adapter = ctx.at("mark-placement-adapter", default: none)
if mark-placement-adapter != none {
ctx.mark-placement-adapter = mark-placement-adapter
}

let local-ctx = ctx
Expand All @@ -193,10 +232,19 @@
drawables = _sort-by-distance(drawables)
}

if projection-matrix != none {
drawables = drawables.map(d => {
if drawable.TAG.projected in d.at("tags", default: ()) {
d
} else {
drawable.apply-transform(projection-matrix, d).first()
}
})
}

ctx.transform = transform
if perspective-mode {
drawable.apply-transform(projection-matrix, drawables)
ctx._perspective-projection = previous-perspective-mode
if mark-placement-adapter != none {
ctx.mark-placement-adapter = previous-mark-placement-adapter
}
if not reset-transform {
drawables = drawable.apply-transform(ctx.transform, drawables)
Expand Down Expand Up @@ -367,8 +415,14 @@

let ref-depth = _resolve-reference-depth(ctx, body, view-rotation-matrix, distance, near)
let projection = pt => _perspective-project-point(pt, near, ref-depth)
let scale-factor = pt => ref-depth / calc.max(-pt.at(2, default: 0.0), near)
let mark-placement-adapter = _perspective-mark-placement-adapter.with(
projection,
scale-factor,
)

_projection(body, view-matrix, projection,
mark-placement-adapter: mark-placement-adapter,
sorted: sorted,
cull-face: cull-face,
reset-transform: reset-transform)
Expand Down
2 changes: 2 additions & 0 deletions src/drawable.typ
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
hidden: "hidden",
no-bounds: "no-bounds",
mark: "mark",
// Drawable geometry already lives in projected paper space.
projected: "projected",
debug: "debug",
)

Expand Down
104 changes: 72 additions & 32 deletions src/mark.typ
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@
/// - segments (drawable): The path to place the mark on.
/// - is-end (bool): Start from the end of the path
/// -> dictionary Dictionary with the following keys: pt, distance and drawable.
#let place-mark-on-path(ctx, styles, segments, is-end: false) = {
#let place-mark-on-path(ctx, styles, segments, is-end: false, adjust-style: none) = {
if type(styles) != array {
styles = (styles,)
}
Expand Down Expand Up @@ -283,22 +283,28 @@
style = merge-flag(style, "harpoon")
style.mark = none

let mark = _eval-mark-shape-and-anchors(ctx, mark-fn(style), style)
let offset = style.at("offset", default: 0)
let inset = style.at("inset", default: 0)

let mark-tip-info = path-util.point-at(
segments, distance, reverse: is-end)

// Do not try to place this mark, if we failed to
// get a tip/base info.
if mark-tip-info == none {
continue
}

if adjust-style != none {
style = adjust-style(style, mark-tip-info)
}

let mark = _eval-mark-shape-and-anchors(ctx, mark-fn(style), style)
let inset = style.at("inset", default: 0)
let mark-base-info = if mark.length != 0 {
path-util.point-at(
segments, distance + mark.length - inset, reverse: is-end)
} else {
mark-tip-info
}

// Do not try to place this mark, if we failed to
// get a tip/base info.
if mark-tip-info == none or mark-base-info == none {
if mark-base-info == none {
continue
}

Expand Down Expand Up @@ -346,6 +352,36 @@
)
}

#let _adapt-mark-placement(ctx, style, transform, path) = {
let transform-shape = style.at("transform-shape", default: true)
let mark-placement-adapter = ctx.at("mark-placement-adapter", default: none)

let projected = false
let adjust-style = none

if not transform-shape and mark-placement-adapter != none {
let adapted = mark-placement-adapter(
transform,
path,
)
path = adapted.path
transform = adapted.transform
projected = adapted.at("projected", default: false)
adjust-style = adapted.at("adjust-style", default: none)
} else if not transform-shape and transform != none {
path = drawable.apply-transform(
matrix.mul-mat(matrix.transform-scale((1,1,0)), transform), path).first()
transform = none
}

(
path: path,
transform: transform,
projected: projected,
adjust-style: adjust-style,
)
}

/// Places marks along a path. Returns them as an {{array}} of {{drawable}}.
///
/// - ctx (context): The context object.
Expand All @@ -355,42 +391,44 @@
/// - add-path (bool): When `true` the shortened path will returned as the first {{drawable}} in the {{array}}
/// -> array
#let place-marks-along-path(ctx, style, transform, path, add-path: true) = {
let distance = (0, 0)
let snap-to = (none, none)
let drawables = ()
let perspective-mode = ctx.at("_perspective-projection", default: false)
if ctx.at("ignore-marks", default: false) {
return if add-path {
drawable.apply-transform(transform, path)
} else {
()
}
}

if style == none {
style = (start: none, end: none, symbol: none)
}
if perspective-mode {
style.transform-shape = true
}
let adapted = _adapt-mark-placement(ctx, style, transform, path)
path = adapted.path
transform = adapted.transform
let projected = adapted.projected
let adjust-style = adapted.adjust-style

let distance = (0, 0)
let snap-to = (none, none)
let drawables = ()

let both-symbol = style.at("symbol", default: none)
let start-symbol = style.at("start",
default: both-symbol)
let start-symbol = style.at("start", default: both-symbol)
if start-symbol == none {
start-symbol = both-symbol
}
let end-symbol = style.at("end",
default: both-symbol)
let end-symbol = style.at("end", default: both-symbol)
if end-symbol == none {
end-symbol = both-symbol
}

let (path, is-transformed) = if not style.at("transform-shape", default: true) and transform != none {
(drawable.apply-transform(
matrix.mul-mat(matrix.transform-scale((1,1,0)), transform), path).first(), true)
} else {
(path, false)
}

let segments = path.segments
if start-symbol != none {
let (drawables: start-drawables, distance: start-distance, pos: pt) = place-mark-on-path(
ctx,
process-style(ctx, style, "start", path-util.length(segments)),
segments
segments,
adjust-style: adjust-style,
)
drawables += start-drawables
distance.first() = start-distance
Expand All @@ -401,7 +439,8 @@
ctx,
process-style(ctx, style, "end", path-util.length(segments)),
segments,
is-end: true
is-end: true,
adjust-style: adjust-style,
)

drawables += end-drawables
Expand All @@ -421,11 +460,12 @@
drawables.insert(0, path)
}

// If not transformed pre mark placement,
// transform everything after mark placement.
if not is-transformed {
if transform != none {
drawables = drawable.apply-transform(transform, drawables)
}
if projected {
drawables = drawable.apply-tags(drawables, drawable.TAG.projected)
}

return drawables
}
37 changes: 32 additions & 5 deletions src/path-util.typ
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
/// - point (vector) The point on the path
/// - previous-point (vector) Point previous to point
/// - direction (vector) Normalized direction vector
/// - t (float) Relative segment parameter in $[0, 1]$
/// - 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.
Expand Down Expand Up @@ -205,12 +206,14 @@
let pt = args.last()
let length = vector.dist(origin, pt)
if length != 0 {
let t = calc.min(1, distance / length)
return (
vector.lerp(origin, pt, calc.min(1, distance / length)),
vector.norm(vector.sub(pt, origin)))
vector.lerp(origin, pt, t),
vector.norm(vector.sub(pt, origin)),
t)
} else {
return (
pt, (1, 0, 0))
pt, (1, 0, 0), 0.0)
}
} else if kind == "c" {
let (c1, c2, e) = args
Expand All @@ -219,7 +222,8 @@

return (
bezier.cubic-point(origin, e, c1, c2, t),
bezier.cubic-derivative(origin, e, c1, c2, t))
bezier.cubic-derivative(origin, e, c1, c2, t),
t)
}
}

Expand All @@ -232,7 +236,7 @@
origin = segments.at(segment-index - 1).last()
}

let (point, direction) = point-on-segment(
let (point, direction, t) = point-on-segment(
origin, segment, distance - travelled)
if reverse {
direction = vector.scale(direction, -1)
Expand All @@ -259,6 +263,7 @@
previous-point: origin,
point: point,
direction: direction,
t: t,
subpath-index: subpath-index,
segment-index: segment-index,
distance: relative-distance,
Expand All @@ -274,6 +279,28 @@
}
}

/// Returns the point on a specific path segment at parameter `t`.
///
/// - path (path): The path
/// - subpath-index (int): Subpath index
/// - segment-index (int): Segment index
/// - t (float): Relative segment parameter in $[0, 1]$
/// -> vector
#let segment-point-at(path, subpath-index, segment-index, t) = {
let (origin, _, segments) = path.at(subpath-index)
let (kind, ..args) = segments.at(segment-index)
if segment-index > 0 {
origin = segments.at(segment-index - 1).last()
}

return if kind == "l" {
vector.lerp(origin, args.last(), t)
} else if kind == "c" {
let (c1, c2, e) = args
bezier.cubic-point(origin, e, c1, c2, t)
}
}

/// Shorten a single line segment with a single point
/// by a given distance.
///
Expand Down
Binary file modified tests/projection/perspective/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.
Loading
Loading