@@ -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)
203206end
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
232238end
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"""
239243Sample 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"""
242246function _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
270282end
271283
272284# ==============================================================================
@@ -415,19 +427,21 @@ function _layout_richtext_for_path(text::RichText, fontsize, font, fonts)
415427 return GlyphCollection (reduce (vcat, lines))
416428end
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).
423436function _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
480493end
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 .
484497function _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)
511524function _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 )
517534end
518535
519536function _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