Status: accepted. The native render-cache gate composes the already accepted shaping and outline boundaries into KiCad-compatible polygon caches. Work is intentionally staged so a final contour mismatch remains attributable to shaping, outline extraction, curve decomposition, placement, or hole fracture.
BEZIER_POLY curve approximation in
libs/kimath/src/bezier_curves.cpp before any placement
transform. The text integration path is
common/font/outline_decomposer.cpp.ttf-parser face, the curve kernel, cursor advances, offsets,
anisotropic sizing, Y inversion, and caller origin into bounded raw text-run
contours. Fixed uharfbuzz and FontTools records keep this placement stage
attributable and explicitly unhinted.flatten_quadratic_bezier and
flatten_cubic_bezier operate in caller-declared finite
coordinates and reproduce the current Python/KiCad approximation, including
inflection-aware cubic splitting and KiCad's nonpositive-tolerance fallback.
The API is independent of font parsing and placement so the same kernel can
consume TrueType, CFF, or future close-to-format curves. Source provenance is
pinned to KiCad revision
5f555f4d63b970e410d567d1f79e05e8ce41b9d8, matching the Python
renderer's documented reference revision.
Retained points and decomposition work have separate inclusive limits. Quadratic segment counts are rejected before iteration when they exceed the work ceiling. Cubic fallback uses an explicit bounded stack rather than recursive call-stack growth. Every append and work item is charged before it is retained or processed; failures publish no output. Direct adversarial quadratic and high-curvature cubic exhaustion tests protect both paths.
FontOutlineFace validates the buffer, face, variation axes,
and hash once, then extracts every shaped glyph without reparsing or rehashing
per operation. The public wrapper continues to support independent single-
glyph calls. shape_text_contours_a0 keeps font bytes out of the
metadata request and applies KiCad's truncated 1433 face scaler and 1.4
outline compensation in a documented coordinate space.
Shaping glyph count and bytes, per-face outline commands, aggregate outline commands, curve work, peak temporary curve points, retained contours, and retained points are independently bounded. The temporary ceiling makes the maximum live contour memory the explicit sum of retained-output and one-curve scratch allowances instead of permitting every late curve another implicit full output-sized allocation. Missing outlines such as a space contribute their shaped advance but no contour; a backend failure after any outline callback is invalid font data, never whitespace. Failures return no partial output. This intermediate deliberately uses unhinted OpenType design-unit outlines; final KiCad save-oracle evidence remains responsible for deciding whether FreeType hinting creates material drift that requires a separate native backend.
layout_single_line_text_a0 measures and renders one shaped run
once, applies left/center/right width offsets and top/center/bottom offsets
using KiCad's 1.17 height factor, then mirrors on the X axis and
rotates clockwise around the authored position. Transform work mutates the
already-bounded contour output in place and creates no second point graph.
Nonfinite angles, aligned positions, and transformed points fail closed.
Horizontal layout supports both left-to-right and right-to-left shaped runs;
top-to-bottom and bottom-to-top shaping fail with a structured invalid-input
diagnostic until vertical-flow layout exists. Returned
advance_x/advance_y values remain the pre-layout
local shaped-run metrics after the contour points are positioned, mirrored,
and rotated.
HintedFontOutlineFace uses the safe-Rust Skrifa 0.46 embedded
TrueType hinting engine at KiCad's effective 358.25 pixels-per-em. Skrifa
returns pixel coordinates; the adapter applies four internal units per pixel,
equivalent to KiCad's FreeType 26.6 coordinates followed by its
72 / 1152 high-resolution scale. The existing unhinted
ttf-parser APIs and fixed design-unit records remain unchanged;
callers select the hinted cache operation explicitly.
Font bytes, metadata, variations, outline commands, and per-glyph hinting scratch memory have independent fail-closed ceilings. Scratch memory is sized before allocation, with inclusive exact and one-under tests. The initial hinted boundary deliberately rejects nonempty variation coordinates until a variable-font KiCad oracle is promoted. No package-owned unsafe code is used.
The dependency is the Google Fontations
Skrifa crate under
its MIT/Apache-2.0 terms. The workspace pins 0.46.0 in Cargo.lock,
reuses the existing read-fonts version, disables default features,
and enables only std; the unused autohint-shaping feature does not
enter the graph. KiCad Monkey continues to own all request validation,
resource limits, coordinate conversion, topology, and cache I/O.
layout_text_block_hinted_a0 mirrors KiCad's
FONT::getLinePositions and
OUTLINE_FONT::GetTextAsGlyphs behavior: newline-delimited lines
use the font-metrics 1.68 interline pitch and the authored line-
spacing multiplier; each line receives its own horizontal alignment;
vertical alignment uses the complete block height; and tabs advance to the
next 4 * 0.6-em stop. Mirroring and clockwise rotation are then
applied around the authored block origin.
The block rules are source-pinned to KiCad revision
d6ff4c23641ee5236b7c9fac19eb6af1849294f5 in
common/font/font.cpp, common/font/outline_font.cpp,
and include/font/font_metrics.h.
The block creates one validated HarfRust/Skrifa session and shapes each non-overlapping run once. Font bytes are neither rehashed nor reparsed per run. UTF-8 feature ranges are intersected and rebased in source order for each run. Aggregate feature inspections are charged before each collection scan, and matching feature applications are charged before cloning. Line, run, aggregate cloned metadata, feature inspection/application, glyph, outline-command, curve-work, contour, and point ceilings are all charged before publication; delimiter-heavy input stops at the configured span ceiling without a full delimiter-counting pass. The typed cache wrapper composes those ceilings with the existing text, topology, and cache-output limits.
Live native evidence currently covers left/top, center/center, rotated
mirrored, tabbed, explicit-line-spacing, right-aligned multi-glyph,
kerning, and whitespace blocks at tolerance 0.002. The hinted
path follows KiCad's integer VECTOR2I cursor semantics: each
Harf advance is scaled by 72 / 1152 into internal units and
truncated toward zero before the next glyph. This closes the prior Arial
SS alignment gap without widening the comparison tolerance.
Each line is parsed into KiCad's ^{}, _{}, and
~{} markup tree with the accepted Python
_parse_markup semantics: a marker immediately followed by
{ opens a group, } closes the innermost group, a
bare marker or unmatched } is literal, and unterminated groups
extend to the end of the line. Every retained text or group node is charged
against one block-wide markup-node ceiling before creation, and both the
parser and the layout walk use explicit heap stacks so nesting depth is
bounded by that same ceiling.
Subscript and superscript runs shape and hint on KiCad's rounded styled
face size round(1433 * 0.64) = 917; the hinted session builds
the second Skrifa hinting instance at 229.25 ppem only when the
block text contains a styled marker. Styled positions convert through the
917 scaler while the millimeter conversion stays on the base
1433 scale, which yields the 0.64 visual ratio.
Baseline offsets are -0.25 and +0.45 of the styled
scaler; the subscript flag wins for nested markers and offsets never
accumulate across nesting. Cursor quantization uses the styled
internal-unit scale.
An overbar group appends one stroke polygon after its children: the bar
runs from the group's start to its end advance, trimmed by
0.1 * size_x at each end, at 1.23 * size_y above
the baseline, stroked with the authored font thickness. Matching
CALLBACK_GAL::DrawGlyph(), the endpoints are transformed into
final coordinates first and the ERROR_INSIDE
TransformOvalToPolygon() port then strokes there with error
strokeWidth / 180, giving the constant 24-segment, 28-point
oval and preserving the reference point order under mirroring. A
non-positive stroke width suppresses the bar; retained bars are charged
against the aggregate contour and point ceilings before deferral.
Live native evidence covers Arial subscript, superscript, plain overbar,
and rotated mirrored overbar blocks at tolerance 0.002.
When the resolved face lacks a real bold or italic variant, KiCad
synthesizes the style in FreeType: fake italic installs a
FT_Set_Transform shear of -12 degrees with
xx = cos, xy = sin truncated to 16.16 fixed point
and identity y, applied after hinting and after shaping, so
advances never change; fake bold then runs
FT_Outline_Embolden(outline, 1 << 6) on the glyph's 26.6
control points with its return code ignored. The Rust port recovers each
hinted outline command onto the exact 26.6 grid
(round(value * 16) is exact because hinted values sit on the
1/64-pixel grid), then executes exact 32-bit integer ports of
FT_MulFix, FT_MulDiv,
FT_Vector_NormLen, FT_Outline_Get_Orientation, and
FT_Outline_EmboldenXY from the pinned FreeType 2.13.3 sources,
including their deliberate wrap-around arithmetic, with FT_Long
fixed at 32 bits to match the Windows ABI KiCad ships. Indeterminate
orientation leaves the outline unchanged, mirroring the ignored FreeType
error before any point has moved.
Two representational differences from FreeType's raw point array are
deliberate and bounded: the pen stream materializes implied on-curve
midpoints, which are collinear with their neighbouring controls and receive
the same lateral shift up to fixed-point normalization rounding, and the
pen's closing return-to-start point is folded back onto the contour's first
point before the integer pass so cyclic anchor walking sees FreeType's point
count. Which style is faked is resolved by the caller against the actual
face; the Rust request takes explicit fake_bold and
fake_italic flags, and the unhinted design-unit path rejects
them because the synthesis is defined on hinted 26.6 coordinates. The
collected point pass is bounded by the existing outline-command ceiling, and
out-of-range 26.6 coordinates fail closed.
Live native evidence covers Impact fake bold, Impact fake italic, and
Arial bold-italic resolving to the real bold face with only the italic
shear faked, all at tolerance 0.002.
Face resolution itself stays caller-side: the Rust request receives font
bytes, and the caller resolves board faces through KiCad's own bundled
fontconfig (the family:Bold/:Italic
FcNameParse match of FONTCONFIG::FindFont in
common/font/fontconfig.cpp) before any fallback, so a face not
installed on the machine substitutes to the same installed file the KiCad
oracle picks. Live Ubuntu Sans regular and bold substitution save-oracle
cases pin this.
fracture_text_contours_a0 trims one repeated closing point,
ignores contours with fewer than three retained points, assigns later
contours to the first containing exterior, and bridges holes using the
deterministic leftmost-edge ordering of the Python KiCad reference. Holes
bridge in ascending (min x, min y) order, matching KiCad's
fractureSingleCacheFriendly path sort. Each hole bridges at the
first strict-min-x point scanned cyclically from the hole's Simplify seam in
stored traversal order: KiCad scans stored order for the first point at the
strict minimum x with no tie-break, and Fracture()'s Clipper2
Simplify() emits every ring starting just past its min-y run
exit, so a vertical hole left edge bridges at its bottom vertex (Arial
D) while a hole whose seam reaches the upper min-x vertex first
bridges there (embedded Wavenumber W). Live double-hole (Arial
B), vertical-left-edge-hole (Arial D), and
corpus complex_font save-oracle cases pin this. One topology
call covers
exactly one glyph's polygon set: KiCad's saved-cache writer fractures each
drawn glyph independently and only when that glyph has holes
(if (glyph.HasHoles()) glyph.Fracture()), so cache composition
threads per-glyph contour group sizes from layout and scopes fracture to
each group. Within a holed group, Simplify() first emits every
ring in a fixed orientation (exteriors positive shoelace area in the cache
frame, holes negative) regardless of input winding — mirrored text
flips every ring's winding, so holed groups orientation-normalize before
the seam/bridge rules, while hole-free groups keep their stored winding;
live mirrored Arial D/R8 save-oracle cases and
mirrored corpus footprint texts pin this. Simplify() also
rewrites every
ring seam: rings restart one past the exit of the min-y run containing the
smallest-x min-y point, where a run is a maximal stretch of consecutive
stored points at the ring's minimum y and, with several min-y runs, the
smallest-x run wins, matching Clipper2's local-minima sweep order —
and Clipper2's
LocMinSorter reorders the group's outers by bottom-most vertex
(largest y first, ties smallest x, otherwise stable). Hole-free groups see
no Simplify() and keep font contour order and ring seams
untouched. Live Arial % (one glyph mixing a hole-free slash
outer with holed rings), Tp%, and i:j;!?
save-oracle cases pin this scoping. The fractured ring is assembled through
KiCad's SHAPE_LINE_CHAIN::Append, which drops points equal to
the last one: when a bridge split point coincides exactly with the hole's
bridge vertex (as happens at small glyph sizes where distinct outline points
collapse after scaling), the pair collapses to a single published point;
a coincident-bridge unit case and live small bold Arial
C203/R80 save-oracle cases pin this. The
pairwise containment and prior-edge bridge scans have independent work
ceilings; input points, edge slots, link traversal, polygons, and published
points are also bounded. Finite comparisons intentionally preserve Python's
signed-zero equality and stable tie ordering. Overflowed intersection or
distance intermediates are invalid input. The operation returns no partial
topology on any invalid input or resource-limit failure.
generate_text_render_cache_a0 composes the accepted horizontal
layout and topology stages into a typed cache containing resolved text,
authored angle, polygons, contours, and points. The cache is a KiCad
source-model type, not a public interchange DTO, so it does not create a new
TypeSpec or WASM contract. read_text_render_cache_a0 consumes the
lexical token stream directly and never builds a generic S-expression tree.
write_text_render_cache_a0 emits escaped text and nested
polygon/points forms directly into a byte-bounded buffer. Read/write limits
separately cover source, decoded text, polygons, contours, points, and output
bytes, and semantic read-write-read is stable.
The shape of the writer is pinned to KiCad revision
d6ff4c23641ee5236b7c9fac19eb6af1849294f5, function
PCB_IO_KICAD_SEXPR::formatRenderCache in
pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.cpp. Fixed byte
formatting evidence comes from Python RenderCache.to_sexp plus
build_sexp; final coordinate authority remains the KiCad save
oracle.
The current render-cache strata now establish live KiCad save-oracle
parity for representative plain single-line, multiline, tabbed, and explicit
line-spacing, right-aligned, kerning, whitespace, and markup Arial text,
plus fake bold/italic synthesis on Impact and the real-bold Arial face,
double-hole and vertical-left-edge hole fracture ordering, and per-glyph
fracture scoping with Simplify seam rewrite and outer reordering (the
former Arial % gap, now closed — it was per-glyph outer
output ordering plus hole-free ring seam rewriting, not geometry cleanup).
The Phase 5 exit gate adds a focused transient authored Arial board and live
save-oracle case. The accepted boundary does not claim exhaustive font,
composite-glyph, or every-corpus-object coverage. One known limitation sits in
the Python reference (not the Rust port): without a provisioned
KICAD_HARFBUZZ_DLL its pure-uharfbuzz fallback shaping drifts a
few microns per glyph at larger x positions; with the hb_ft path it matches
the oracle, and the native hinted path always quantizes advances like
KiCad. Those remain active parts
of render-cache-parity, not deferred packaging work. Font
discovery stays native-only and outside the platform-neutral byte-input
core; WASM packaging remains in its later selected-operation phase.