Problem
The glyph grid currently uses the editable-glyph loading path for every visible and overscanned glyph. Each snapshot can carry every authored source layer, interpolation state, component dependencies, editable identities, anchors, and transforms. With Fraunces (697 glyphs and 8 masters), the directory appears before grid outlines finish hydrating, and entering a glyph competes with background preview work.
The grid needs one lightweight derived outline per glyph at its current internal design location. It must not load or retain editable Glyph models for grid cells.
Follow-up to #149.
Delivery order
TanStack Query infrastructure should land independently:
- Open a small PR from main that adds @tanstack/react-query and installs one renderer-level QueryClientProvider.
- Keep preview-specific query keys, chunking, and defaults out of that foundation PR.
- Merge the Query foundation PR.
- Rebase the glyph-preview/grid PR onto the updated main.
- Implement the Rust projection, transport, and grid adoption together in the rebased PR.
This keeps dependency/provider review separate from the font-domain and grid-performance change.
Design principles
- This is a read-only projection, not another glyph-loading API.
- One request contains many glyph IDs at one internal design location.
- Rust resolves interpolation and components and returns only the final preview path and advance.
- The operation does not populate FontStore or create live Glyph models.
- loadGlyph() remains the operation used when entering the editor.
- Source metrics remain separate: they vary by location but are shared by every glyph in a batch and already resolve synchronously from the Rust-built source-metrics interpolation model.
- External coordinates must be mapped before this operation. Preview resolution must not apply mappings a second time.
- TanStack Query is a renderer concern. Query concepts must not appear in Rust, bridge DTOs, workspace transport, or Font's method signature.
Proposed API surface
Canonical Rust/wire result
#[serde(rename_all = "camelCase")]
pub struct GlyphPreview {
pub glyph_id: GlyphId,
/// Flattened root and component contours at the resolved location.
pub svg_path: String,
pub x_advance: f64,
}
Native bridge
#[napi]
pub fn get_glyph_previews(
&self,
glyph_ids: Vec<NapiGlyphId>,
location: NapiLocation,
) -> Result<Vec<NapiGlyphPreview>>
Generated TypeScript:
export interface GlyphPreview {
glyphId: GlyphId;
svgPath: string;
xAdvance: number;
}
export interface BridgeApi {
getGlyphPreviews(
glyphIds: GlyphId[],
location: Location,
): GlyphPreview[];
}
No request or batch DTO is required. The location belongs to the call and later to the renderer query key.
Utility-process sync lane
type SyncCallMap = {
// ...
"workspace.glyphPreviews": {
request: {
glyphIds: GlyphId[];
location: Location;
};
response: GlyphPreview[];
};
};
WorkspaceHost forwards the request through the existing serialized workspace lane.
Renderer
class WorkspaceClient {
glyphPreviews(
glyphIds: readonly GlyphId[],
location: Location,
): Promise<GlyphPreview[]>;
}
class WorkspaceEditCoordinator {
readGlyphPreviews(
glyphIds: readonly GlyphId[],
location: Location,
): Promise<GlyphPreview[]>;
}
class Font {
glyphPreviews(
glyphIds: readonly GlyphId[],
location: AxisLocation,
): Promise<readonly GlyphPreview[]>;
}
Use glyphPreviews, not loadGlyphPreviews: the operation returns derived values and must not mutate the live model.
Glyph resolution semantics
For every requested glyph:
- Use the exact authored layer when one exists at the requested internal design location.
- Otherwise evaluate the Rust-built glyph interpolation model at that location.
- If interpolation is unavailable, use the same default/primary-layer fallback as the editor.
- Resolve components recursively at the same location, including interpolated component transforms and anchor attachment.
- Skip cyclic component branches without dropping non-cyclic contours.
- Return the root glyph's resolved xAdvance.
The resolver should be a pure Rust projection built on existing interpolation and composite machinery, not a separate bridge-only interpolation implementation.
An existing blank glyph returns an empty svgPath with its resolved xAdvance. Missing glyph IDs are omitted, matching getGlyphSnapshots; existing results preserve request order.
Why SVG path data
The consumer is an SVG grid preview. Returning svgPath avoids nested point and contour objects crossing NAPI and Electron IPC, avoids shipping source layers and interpolation models, avoids creating renderer GlyphGeometry and GlyphOutline models, and lets the browser consume the result directly.
This is a purpose-built preview projection, not a general vector geometry API. Do not add bounds or packed commands without another concrete consumer.
Grid query design
The grid already uses TanStack Virtual. Divide the catalog into stable preview chunks (initially 32 or 64 glyphs) and request only chunks intersecting the virtualized range.
Use TanStack Query's useQueries with a stable key for each document, design location, and chunk:
useQueries({
queries: visibleChunks.map((chunk) => ({
queryKey: [
"glyph-previews",
documentId,
locationKey(location),
chunk.id,
],
queryFn: () => font.glyphPreviews(chunk.glyphIds, location),
staleTime: Infinity,
gcTime: 30_000,
retry: false,
})),
});
Consequences:
- a late scrub response remains attached to its old location key;
- a scrolled-away response remains attached to its chunk key;
- identical in-flight requests are deduplicated;
- back-scrolling can reuse a recently resolved chunk;
- the component needs no generation ref, pending ref, or manual stale-result comparison.
Do not create a preview cache class or wrapper around Query.
TanStack Query does not prevent distinct scrub locations from starting distinct native calls. Start without another scheduling library. If lightweight bounded batches still enqueue too much work, address location throttling or latest-pending native scheduling as measured follow-up work.
Source metrics while scrubbing
Source metrics are evaluated once for the current design location, not repeated on every GlyphPreview. They remain synchronous and local.
The UI must not combine an old-location outline with new-location metrics. During a transition, either withhold unresolved previews or retain the previous coherent grid frame and resolve its metrics from that frame's location. Choose the visual transition during grid integration.
Preview PR scope after the Query foundation lands
Verification
Non-goals
- Preview cache or manager classes.
- Revision-stamped responses, diagnostics, or invalidation payloads.
- General-purpose contour or point identities.
- Server-side cancellation or prioritization in the first implementation.
- Optimizing initial Designspace/UFO import.
- Handling concurrent editor mutations while the grid is visible.
Problem
The glyph grid currently uses the editable-glyph loading path for every visible and overscanned glyph. Each snapshot can carry every authored source layer, interpolation state, component dependencies, editable identities, anchors, and transforms. With Fraunces (697 glyphs and 8 masters), the directory appears before grid outlines finish hydrating, and entering a glyph competes with background preview work.
The grid needs one lightweight derived outline per glyph at its current internal design location. It must not load or retain editable Glyph models for grid cells.
Follow-up to #149.
Delivery order
TanStack Query infrastructure should land independently:
This keeps dependency/provider review separate from the font-domain and grid-performance change.
Design principles
Proposed API surface
Canonical Rust/wire result
Native bridge
Generated TypeScript:
No request or batch DTO is required. The location belongs to the call and later to the renderer query key.
Utility-process sync lane
WorkspaceHost forwards the request through the existing serialized workspace lane.
Renderer
Use glyphPreviews, not loadGlyphPreviews: the operation returns derived values and must not mutate the live model.
Glyph resolution semantics
For every requested glyph:
The resolver should be a pure Rust projection built on existing interpolation and composite machinery, not a separate bridge-only interpolation implementation.
An existing blank glyph returns an empty svgPath with its resolved xAdvance. Missing glyph IDs are omitted, matching getGlyphSnapshots; existing results preserve request order.
Why SVG path data
The consumer is an SVG grid preview. Returning svgPath avoids nested point and contour objects crossing NAPI and Electron IPC, avoids shipping source layers and interpolation models, avoids creating renderer GlyphGeometry and GlyphOutline models, and lets the browser consume the result directly.
This is a purpose-built preview projection, not a general vector geometry API. Do not add bounds or packed commands without another concrete consumer.
Grid query design
The grid already uses TanStack Virtual. Divide the catalog into stable preview chunks (initially 32 or 64 glyphs) and request only chunks intersecting the virtualized range.
Use TanStack Query's useQueries with a stable key for each document, design location, and chunk:
Consequences:
Do not create a preview cache class or wrapper around Query.
TanStack Query does not prevent distinct scrub locations from starting distinct native calls. Start without another scheduling library. If lightweight bounded batches still enqueue too much work, address location throttling or latest-pending native scheduling as measured follow-up work.
Source metrics while scrubbing
Source metrics are evaluated once for the current design location, not repeated on every GlyphPreview. They remain synchronous and local.
The UI must not combine an old-location outline with new-location metrics. During a transition, either withhold unresolved previews or retain the previous coherent grid frame and resolve its metrics from that frame's location. Choose the visual transition during grid integration.
Preview PR scope after the Query foundation lands
Verification
Non-goals