Skip to content

Commit 3990793

Browse files
committed
perf/simplify(pathtext): cached cumulative arc-length and inlined text-length
- _sample_bezierpath_at now binary-searches a cumulative arc-length table precomputed in _prepare_bezierpath, instead of scanning segments linearly per sample. - _cubic_inv_arclen uses 20 bisection iterations instead of 30 (≈1e-6 parameter precision, well below sub-pixel). - _layout_glyphs computes x_positions with a single-pass accumulator instead of cumsum(advances) .- advances. - total_text_len is now computed inside _place_glyphs_on_path instead of passed in (one fewer positional arg). - Trimmed a few WHAT-comments; removed an unreachable return.
1 parent 02b8c9c commit 3990793

File tree

1 file changed

+65
-49
lines changed

1 file changed

+65
-49
lines changed

Makie/src/basic_recipes/pathtext.jl

Lines changed: 65 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ function _cubic_inv_arclen(p0, p1, p2, p3, target, total_len, d = 0.0)
157157
target <= 0 && return 0.0
158158
target >= total_len && return 1.0
159159
lo, hi = 0.0, 1.0
160-
for _ in 1:30
160+
# 20 iterations of bisection → ≈ 1e-6 precision on the parameter, which is
161+
# below sub-pixel accuracy for any realistic font size.
162+
for _ in 1:20
161163
mid = 0.5 * (lo + hi)
162164
s = iszero(d) ? _cubic_arclen(p0, p1, p2, p3, 0.0, mid) :
163165
_cubic_offset_arclen(p0, p1, p2, p3, d, 0.0, mid)
@@ -199,6 +201,7 @@ struct _PreparedSegment
199201
p2::Point2d # control 2 (cubic)
200202
p3::Point2d # end (cubic)
201203
arclen::Float64 # (offset) arc length
204+
cum_end::Float64 # cumulative arc length at the end of this segment
202205
subpath_id::Int # incremented on each MoveTo (to detect sub-path gaps)
203206
end
204207

@@ -207,6 +210,7 @@ function _prepare_bezierpath(bp::BezierPath, d::Real = 0.0)
207210
segs = _PreparedSegment[]
208211
last_pt = Point2d(0, 0)
209212
subpath_id = 0
213+
cum = 0.0
210214
started = false
211215
for cmd in bp2.commands
212216
if cmd isa MoveTo
@@ -218,55 +222,63 @@ function _prepare_bezierpath(bp::BezierPath, d::Real = 0.0)
218222
elseif cmd isa LineTo
219223
started || (subpath_id += 1; started = true)
220224
len = norm(cmd.p - last_pt)
221-
push!(segs, _PreparedSegment(:line, last_pt, cmd.p, Point2d(0), Point2d(0), len, subpath_id))
225+
cum += len
226+
push!(segs, _PreparedSegment(:line, last_pt, cmd.p, Point2d(0), Point2d(0), len, cum, subpath_id))
222227
last_pt = cmd.p
223228
elseif cmd isa CurveTo
224229
started || (subpath_id += 1; started = true)
225230
len = iszero(d) ? _cubic_arclen(last_pt, cmd.c1, cmd.c2, cmd.p) :
226231
_cubic_offset_arclen(last_pt, cmd.c1, cmd.c2, cmd.p, d)
227-
push!(segs, _PreparedSegment(:cubic, last_pt, cmd.c1, cmd.c2, cmd.p, len, subpath_id))
232+
cum += len
233+
push!(segs, _PreparedSegment(:cubic, last_pt, cmd.c1, cmd.c2, cmd.p, len, cum, subpath_id))
228234
last_pt = cmd.p
229235
end
230236
end
231237
return segs
232238
end
233239

234-
function _total_arclen(segs::Vector{_PreparedSegment})
235-
return sum(s.arclen for s in segs; init = 0.0)
236-
end
240+
_total_arclen(segs::Vector{_PreparedSegment}) = isempty(segs) ? 0.0 : segs[end].cum_end
237241

238242
"""
239243
Sample a prepared BezierPath at arc-length `s`. Returns `(point, tangent)` or
240244
`nothing` if past the end. When `d ≠ 0`, positions are offset perpendicularly.
241245
"""
242246
function _sample_bezierpath_at(segs::Vector{_PreparedSegment}, s::Real, d::Real = 0.0)
243247
s < 0 && return nothing
244-
accum = 0.0
245-
for seg in segs
246-
if accum + seg.arclen >= s
247-
local_s = s - accum
248-
if seg.kind === :line
249-
frac = seg.arclen > 0 ? local_s / seg.arclen : 0.0
250-
v = seg.p1 - seg.p0
251-
len = norm(v)
252-
tangent = len > 0 ? Point2f(v[1] / len, v[2] / len) : Point2f(1, 0)
253-
pt = Point2f(seg.p0[1] + frac * v[1], seg.p0[2] + frac * v[2])
254-
if !iszero(d)
255-
nx, ny = -tangent[2], tangent[1]
256-
pt = pt + Float32(d) * Point2f(nx, ny)
257-
end
258-
return (pt, tangent, seg.subpath_id)
259-
else # :cubic
260-
t = _cubic_inv_arclen(seg.p0, seg.p1, seg.p2, seg.p3, local_s, seg.arclen, d)
261-
tangent = _cubic_unit_tangent(seg.p0, seg.p1, seg.p2, seg.p3, t)
262-
pt = iszero(d) ? Point2f(_cubic_eval(seg.p0, seg.p1, seg.p2, seg.p3, t)...) :
263-
_cubic_offset_point(seg.p0, seg.p1, seg.p2, seg.p3, t, d)
264-
return (pt, tangent, seg.subpath_id)
265-
end
248+
249+
# Binary search the segment whose cumulative arc-length range contains `s`.
250+
lo, hi = 1, length(segs)
251+
hi == 0 && return nothing
252+
while lo < hi
253+
mid = (lo + hi) >> 1
254+
if segs[mid].cum_end < s
255+
lo = mid + 1
256+
else
257+
hi = mid
266258
end
267-
accum += seg.arclen
268259
end
269-
return nothing
260+
seg = segs[lo]
261+
seg.cum_end < s && return nothing
262+
local_s = s - (seg.cum_end - seg.arclen)
263+
264+
if seg.kind === :line
265+
frac = seg.arclen > 0 ? local_s / seg.arclen : 0.0
266+
v = seg.p1 - seg.p0
267+
len = norm(v)
268+
tangent = len > 0 ? Point2f(v[1] / len, v[2] / len) : Point2f(1, 0)
269+
pt = Point2f(seg.p0[1] + frac * v[1], seg.p0[2] + frac * v[2])
270+
if !iszero(d)
271+
nx, ny = -tangent[2], tangent[1]
272+
pt = pt + Float32(d) * Point2f(nx, ny)
273+
end
274+
return (pt, tangent, seg.subpath_id)
275+
else # :cubic
276+
t = _cubic_inv_arclen(seg.p0, seg.p1, seg.p2, seg.p3, local_s, seg.arclen, d)
277+
tangent = _cubic_unit_tangent(seg.p0, seg.p1, seg.p2, seg.p3, t)
278+
pt = iszero(d) ? Point2f(_cubic_eval(seg.p0, seg.p1, seg.p2, seg.p3, t)...) :
279+
_cubic_offset_point(seg.p0, seg.p1, seg.p2, seg.p3, t, d)
280+
return (pt, tangent, seg.subpath_id)
281+
end
270282
end
271283

272284
# ==============================================================================
@@ -415,19 +427,21 @@ function _layout_richtext_for_path(text::RichText, fontsize, font, fonts)
415427
return GlyphCollection(reduce(vcat, lines))
416428
end
417429

418-
# Common helper: place glyphs along a path given their arc-length positions.
419-
# `sample_fn(s)` returns `(point, tangent)` or `nothing`.
420-
# `advances` are the horizontal advance widths per glyph (used to compute the
421-
# chord across each character for its rotation).
422-
# `y_offsets` (optional) are per-glyph perpendicular shifts (e.g. from sub/superscript baseline).
430+
# `sample_fn(s)` returns `(point, tangent, subpath_id)` or `nothing`.
431+
# `advances` are the horizontal advance widths per glyph; the chord between
432+
# arc-length `s` and `s + adv` determines the glyph's rotation (so wide letters
433+
# span the curvature naturally).
434+
# `y_offsets` (optional) are per-glyph perpendicular shifts from the path
435+
# baseline (e.g. sub/superscript displacement in RichText).
423436
function _place_glyphs_on_path(
424-
x_positions, advances, chars, sample_fn, frac, total_text_len, total_path_len;
437+
x_positions, advances, chars, sample_fn, frac, total_path_len;
425438
y_offsets = nothing,
426439
)
427440
positions = Point2f[]
428441
rotations = Quaternionf[]
429442
placed_chars = String[]
430443

444+
total_text_len = isempty(x_positions) ? 0.0f0 : x_positions[end] + advances[end]
431445
start_s = frac * (total_path_len - total_text_len)
432446

433447
for (i, (x, adv, c)) in enumerate(zip(x_positions, advances, chars))
@@ -436,9 +450,8 @@ function _place_glyphs_on_path(
436450
sample_start === nothing && break
437451
pt, start_tangent, start_subpath = sample_start
438452

439-
# Rotation from the chord spanning the character's advance width.
440-
# Falls back to the tangent at the start if the end sample is unavailable
441-
# or lands on a different sub-path (avoids bridging NaN gaps).
453+
# Fall back to the start tangent when the chord would bridge a NaN or
454+
# MoveTo gap between two sub-paths.
442455
sample_end = sample_fn(s0 + adv)
443456
tangent = if sample_end !== nothing
444457
pt_end, _, end_subpath = sample_end
@@ -479,18 +492,18 @@ function _parse_halign(ha)
479492
end
480493
end
481494

482-
# Compute perpendicular baseline shift from valign and font metrics (ascender/descender in pixels).
483-
# Positive result shifts text in the +normal direction (left of path travel).
495+
# Perpendicular baseline shift (in pixels) from valign and font metrics.
496+
# Positive result shifts to the left of the path's travel direction.
484497
function _valign_shift(va, fontsize, font)
485498
va === :baseline && return 0.0f0
486499
asc = Float32(FreeTypeAbstraction.ascender(font)) * fontsize
487500
desc = Float32(FreeTypeAbstraction.descender(font)) * fontsize # negative
488501
return if va === :bottom
489-
-desc # shift up so descender sits on path
502+
-desc
490503
elseif va === :top
491-
-asc # shift down so ascender sits on path
504+
-asc
492505
elseif va === :center
493-
-(asc + desc) / 2 # center of typographic extent on path
506+
-(asc + desc) / 2
494507
else
495508
throw(ArgumentError("Invalid valign $(repr(va)) for `pathtext`. Expected `:baseline`, `:bottom`, `:center`, or `:top`."))
496509
end
@@ -511,9 +524,13 @@ _empty_layout() = (Point2f[], Quaternionf[], String[], nothing)
511524
function _layout_glyphs(text::AbstractString, fontsize::Float32, font, fonts)
512525
chars = collect(text)
513526
advances = Float32[Float32(GlyphExtent(font, c).hadvance) * fontsize for c in chars]
514-
x_positions = cumsum(advances) .- advances
515-
y_offsets = nothing
516-
return (chars, x_positions, y_offsets, advances, nothing)
527+
x_positions = similar(advances)
528+
acc = 0.0f0
529+
@inbounds for i in eachindex(advances)
530+
x_positions[i] = acc
531+
acc += advances[i]
532+
end
533+
return (chars, x_positions, nothing, advances, nothing)
517534
end
518535

519536
function _layout_glyphs(text::RichText, fontsize::Float32, font, fonts)
@@ -565,10 +582,9 @@ function _pathtext_layout(pixel_path, text, fontsize, font, fonts, align, offset
565582
prepared === nothing && return _empty_layout()
566583
total_path_len, sample_fn = prepared
567584

568-
total_text_len = isempty(x_positions) ? 0.0f0 : x_positions[end] + advances[end]
569585
frac = _parse_halign(halign)
570586
pos, rot, placed = _place_glyphs_on_path(
571-
x_positions, advances, chars, sample_fn, frac, total_text_len, total_path_len;
587+
x_positions, advances, chars, sample_fn, frac, total_path_len;
572588
y_offsets,
573589
)
574590

0 commit comments

Comments
 (0)