add text-layer data model#14
Open
luckydye wants to merge 13 commits into
Open
Conversation
Introduces a declarative Layer::Text variant alongside Image/Crop/ Adjustment, plus a per-stack font registry. Glyph rasterization and GPU placement land in follow-up commits; the renderer currently skips text layers so the rest of the stack continues to render unchanged. - shade-lib: new `text` module with TextContent, TextStyle, TextSpan, FontEntry, TextAlign, TextAnchor; FNV-1a 64-bit content hashing for font dedup. - LayerStack gains `fonts` and `next_font_id`, both #[serde(default)] for backward-compat with pre-text documents. New methods: add_text_layer, add_font (dedups by hash), find_font_by_hash, remove_unused_fonts. - Renderer treats Text layers as no-ops via a diverging match arm. - shade-wasm/bridge.rs and shade-tauri/commands.rs exhaustive matches on `Layer` extended with Text arms.
Adds the CPU-side geometry pipeline that the GPU rasterizer (Slug-style
direct curve rendering, PR3) will consume. Pure CPU, fully unit-tested
without a font fixture or GPU.
- shade-lib now depends on ttf-parser 0.25 for OTF/TTF parsing.
- New `text_outline` module:
- QuadBezier / Rect / GlyphBand / GlyphCurves types in em-units.
- OutlineCollector implements ttf_parser::OutlineBuilder; widens
line_to into degenerate quadratics with midpoint controls and
recursively subdivides cubics into quadratics under a closed-form
error bound (Sederberg, sqrt(3)/36 · |Δ|), with a depth cap.
- quad_y_extent solves for the interior y' = 0 extremum so band
assignment includes apex bands the endpoints miss.
- outline_glyph(blob, glyph_id, num_bands) is the public entry point.
- 18 unit tests cover the builder, cubic subdivision (including the
degenerate S-curve that defeats midpoint-distance error proxies),
y-extent solving, and band assignment.
The composite kernel was multiplying mask × opacity but ignoring the layer's own alpha channel, so transparent regions of a layer still overrode the base. That works for opaque image layers but is wrong for text (alpha-coverage everywhere outside glyph fills) and for alpha-PNG image layers, which incorrectly blend zeroed RGB over the base. Multiply by layer.a so transparent pixels preserve the base, opaque pixels blend through, and partial coverage interpolates correctly. Note: GPU tests are skipped in sandboxed environments without a wgpu adapter; CI will exercise the visual path on real hardware.
Adds the CPU-side packing that turns text_outline's GlyphCurves into the four shared storage buffers consumed by the upcoming text render pass, plus a per-layer instance buffer encoder. All bytemuck Pod, fixed 16-byte-safe layouts, fully unit-tested. - GpuGlyphMeta (48 B), GpuBand (16 B), GpuPlacedGlyph (48 B), all Pod with compile-time size assertions. - GlyphBufferLayout accumulates curves (Vec<f32>, 6 per quad), bands (Vec<GpuBand>), the sparse band->curve index (Vec<u32>), and metas with a (FontId, glyph_id) lookup. - add_glyph is idempotent: repeated calls return the existing meta index without growing any buffer. - build_instances maps PlacedGlyph -> GpuPlacedGlyph by meta index, erroring with a descriptive message if a glyph wasn't registered. - 11 tests cover empty layout, single-glyph encoding, idempotency, multi-glyph offsets, empty (whitespace) glyphs, instance encoding, unregistered-glyph error path, band-curve index remapping, and bytemuck round-trip.
Adds the layout stage that bridges TextContent/TextStyle to a flat list
of PlacedGlyphs in canvas pixels. The GPU rasterizer (PR3c) consumes
this output unchanged via text_buffer::build_instances.
- Dep: cosmic-text 0.12 with default-features off; explicit features
["std", "swash", "shape-run-cache"] keep system-font discovery and
wasm-web bits out of the build, satisfying the "no web feature"
constraint while pulling shaping (rustybuzz) and rasterization
(swash) for future GPU-pass use.
- TextLayoutEngine owns a FontSystem populated from a manually built
fontdb::Database — no system probe — and a bidirectional id mapping
between shade FontId and fontdb::ID so shaped glyphs translate back
cleanly. layout() walks layout_runs and emits PlacedGlyphs at the
pen position (run.line_y + GPOS y_offset), with glyph.font_size as
the rendered px size.
- layout_text() is a one-shot convenience wrapper.
- Tests:
- 4 logic tests: empty content, unregistered font_id, empty font
map accepted, garbage blob rejected.
- 3 live cosmic-text tests using a system font (DejaVu/Loma/etc.):
LTR monotonicity, max_width forces a wrap, advance scales
proportionally with font size. Skip gracefully on minimal CI
where no font is installed.
After reading the reference shaders at EricLengyel/Slug (MIT, public- domain patent dedication), redesign the per-glyph CPU/GPU data layout to match what the Slug pixel shader actually consumes. Three concrete changes versus the prior single-axis design: 1. Two perpendicular band sets per glyph. The fragment shader needs an H band (curves crossing y=pixel.y) AND a V band (crossing x=pixel.x) to compute coverage on both axes; combining them is what gives Slug's analytic AA. GlyphCurves now carries h_bands + v_bands; the header buffer is laid out [h..., v...] per glyph so the shader's `glyphLoc.x + bandMax.y + 1 + bandIndex.x` arithmetic translates directly. 2. Uniform-stride bands driven by a per-glyph band_transform. Replace per-band y_min/y_max with a (scale_x, scale_y, offset_x, offset_y) tuple satisfying band_index = clamp(floor(coord·scale + offset), 0, band_max) — the exact formula from Slug's pixel shader. 3. Sort curves descending by perpendicular-axis max within each band. H bands sort by descending max-x, V bands by descending max-y. This enables the shader's left-of-pixel early-out (`if max_x · pixels_per_em.x < -0.5 break`). text_buffer changes: - GpuGlyphMeta grew from 48 B to 64 B; now carries band_headers_offset, band_max_x/y, band_transform, em_bbox, units_per_em. - GpuBand replaced by GpuBandHeader (8 B): just (curve_count, offset). - band_curves stores GLOBAL curve indices (so the shader doesn't need to know which glyph a curve belongs to). - add_glyph remaps each band's local indices into globals. Tests: - 11 text_outline tests including new x-extent solver, band-transform formula assertion, descending-sort order on both axes. - 9 text_buffer tests including h-then-v header layout, global index remapping across multiple glyphs, multi-band glyph header counts, and the existing idempotency / instance-encoding suite. 99/99 unit tests pass; downstream crates clean.
Ports the EricLengyel/Slug reference shaders (MIT, public-domain patent dedication 2026-03-17) to WGSL and wires them into a render pipeline that produces an Rgba16Float color attachment for the existing CompositePipeline to consume. shaders/text_glyph.wgsl - Vertex shader: instanced quad per placed glyph, six vertices forming two CCW triangles, sized to the glyph's em bbox plus a half-pixel margin in em-units. TrueType Y is flipped to screen Y. No dilation yet — will show ≤1-px cracks under heavy transforms; v1.1 follow-up. - Pixel shader: full Slug analytic-coverage path. fwidth-derived pixels-per-em, dual-axis band lookup, root-finding via CalcRootCode/SolveHorizPoly/SolveVertPoly, weighted xcov+ycov combination with min-of-magnitudes fallback. Outputs straight RGBA so the CompositePipeline contract is preserved. - Storage-buffer adaptation of Slug's 2D-texture data path: curves, band_headers, band_curves_idx, glyph_metas, instances all in one bind group. Defensive 256-curves-per-band loop cap. text_pipeline.rs - TextPipeline owns the render pipeline and bind-group layout. - process(layout, instances, w, h) acquires a work texture (now with RENDER_ATTACHMENT in WORK_TEXTURE_USAGE), uploads the five storage buffers (16-byte zero pad if any are empty so wgpu validation accepts the binding), encodes a render pass that clears to transparent and draws 6×N vertices, and returns the texture. - Standard src-over straight-alpha blending so adjacent glyph quads with overlapping margins composite cleanly without darkening. Tests: WGSL is parsed + validated under naga (re-exported via wgpu) at cargo-test time, no GPU required. Catches reserved-keyword and type-checking errors locally — already caught one (`meta` is reserved). 101 unit tests pass; downstream crates clean.
Replaces the placeholder `continue` arm with a full text-layer render
pass that produces an Rgba16Float `layer_result` consumed by the
existing CompositePipeline alongside Image / Crop / Adjustment layers.
shade-lib/src/renderer.rs
- Renderer gains a TextPipeline field, constructed in Renderer::new.
- New helper render_text_layer:
1. Build a TextLayoutEngine from stack.fonts (rebuilt per call for
v1; caching by stack.generation is a follow-up).
2. Lay out content+style → Vec<PlacedGlyph>.
3. Apply the layer transform's translation (rotation/scale deferred
until the vertex shader takes a matrix).
4. Extract outlines via outline_glyph for each unique glyph and pack
into a fresh GlyphBufferLayout. Whitespace glyphs are dropped
before instance encoding so the draw call stays tight.
5. Run TextPipeline::process to produce the layer texture.
6. Returns Option<Texture> — None means empty/all-whitespace, in
which case the composite step is skipped entirely.
- The Layer::Text match arm now calls this and falls through to the
unchanged composite path with the produced texture.
shade-lib/tests/text_layer_e2e.rs
- GPU + system-font skipping integration test, mirroring the existing
crop_rotation_e2e pattern. Two cases:
- "Hi" rendered black-on-white at 48px and asserts ≥16 dark pixels
survive the composite.
- Empty TextContent: every pixel stays near-white (the helper
short-circuits before TextPipeline runs).
101 unit tests + 2 integration tests pass; downstream crates clean.
On the sandboxed runner without an adapter both e2e tests skip cleanly;
real hardware in CI will exercise the full chain.
Wires the existing text-layer + font data model end-to-end through the bridge, web worker, and Inspector so users can create text layers, upload fonts, and edit content + style from the SolidJS UI. Backend (shade-wasm + shade-tauri, mirrored): - Engine methods on WasmEngine: add_font, list_fonts, add_text_layer, update_text_content, update_text_style, set_text_transform, prune_unused_fonts. - wasm-bindgen wrappers + Tauri commands for each. update_text_style takes a JSON patch so omitted fields stay untouched while max_width=null explicitly clears it. - LayerEntryInfo / get_stack_json now expose text content, style, and transform alongside crop/adjustment fields. Frontend (shade-ui): - TS bridge: addTextLayer, updateTextContent, updateTextStyle, setTextTransform, addFont, listFonts, pruneUnusedFonts. New interfaces TextLayerValues, TextStyleValues, TextStylePatch, TextTransformValues, FontInfo, plus TextAlignName/TextAnchorName. - editor-store gains a `fonts` field; editor-layers gains store helpers around the bridge calls and refreshFontList. - New TextLayerEditor SolidJS component: textarea for content, font picker with inline upload (.ttf/.otf/.ttc), sliders for size / line-height / letter-spacing, color picker (sRGB <-> linear), weight dropdown, italic toggle, alignment buttons, max-width input, and X/Y position fields. Color values round-trip through linear-sRGB exactly as stored on Layer::Text. - Inspector wires TextLayerEditor into the desktop and mobile selected -layer panels via a new selectedTextLayer() guard, and adds an "Add Text" button next to "Add Adjustment". When no font is registered the new layer uses font_id=0 as a placeholder; the user uploads a font from the editor's font picker. Verified: - `cargo test -p shade-lib --lib`: 101/101 pass. - `cargo test -p shade-wasm`: 4/4 pass. - `cargo check -p shade-wasm` clean. - `tsc --noEmit` on shade-ui clean. Pre-existing BinaryFile.ts errors in shade-web are unrelated and unchanged.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The Add Text button was visually identical to Add Adjustment and the text layer rows fell back to the unlabelled border-square placeholder, making them easy to overlook. Adds a "T" glyph in both the LayerTypeIcon for text layers and the icon column of the Add Text button so the entry points are visually distinct from the adjustment chain controls.
The switch was missing the "text" case, so getLayerDisplayName would throw whenever a text layer was in the stack. This crash inside the DesktopLayerList render prevented both Add Adjustment and Add Text buttons from appearing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Introduces a declarative Layer::Text variant alongside Image/Crop/
Adjustment, plus a per-stack font registry. Glyph rasterization and
GPU placement land in follow-up commits; the renderer currently skips
text layers so the rest of the stack continues to render unchanged.
textmodule with TextContent, TextStyle, TextSpan,FontEntry, TextAlign, TextAnchor; FNV-1a 64-bit content hashing for
font dedup.
fontsandnext_font_id, both #[serde(default)]for backward-compat with pre-text documents. New methods:
add_text_layer, add_font (dedups by hash), find_font_by_hash,
remove_unused_fonts.
on
Layerextended with Text arms.