Skip to content

SVG import: <textPath> comes back as type on a path - #6

Merged
echelon merged 1 commit into
storytold:mainfrom
storybored379:pr/svg-import-text-on-path
Oct 4, 2026
Merged

echelon merged 1 commit into
storytold:mainfrom
storybored379:pr/svg-import-text-on-path

Conversation

@storybored379

Copy link
Copy Markdown
Contributor

VectorCraft exports type on a path as <text><textPath href="#text-path-N" startOffset="…%"> with the
path in <defs>, but the XML text fallback read every <text> as point type at the text element's
x/y. Text on a path therefore re-imported from VectorCraft's own SVG as a straight line of point type
at (0, 0), above the artboard and mostly out of view, and lost its path.

What changed

  • crates/svg/src/import.rs: a <textPath> child resolves its href (SVG 2 href or xlink:href) to the
    referenced <path>, bakes the text's user-space transform (and the path's own transform) into it, and
    reads startOffset (a percentage, or a length along the path) into TextKind::OnPath.
  • Crate docs mention the textPath case.

How it was found and tested

  • Reproduced end-to-end through the headless MCP server (vectorcraft-cli mcp --headless): type on a path → export SVG → open_file.
  • New test text_on_path_roundtrip (crates/svg/tests/roundtrip.rs): exports type on a curve, imports it and checks
    kind, text, start offset and the path's placement. Verified it fails on current main without the change
    (not type on a path: Point) and passes with it.
  • cargo test -p vectorcraft-svg, cargo clippy -p vectorcraft-svg --all-targets (no new warnings), cargo fmt --check,
    cargo xtask layers, cargo xtask wasm on current main.
  • Files: crates/svg/src/import.rs, crates/svg/src/lib.rs, crates/svg/tests/roundtrip.rs
  • Clean-room: implemented from the SVG 1.1/2 textPath specification only.

Found and fixed with Claude Code (AI-assisted).

🤖 Generated with Claude Code

VectorCraft exports type on a path as <text><textPath href="#text-path-N"
startOffset="…%"> with the path in <defs>, but the XML text fallback read
every <text> as point type at the text element's x/y. Text on a path
therefore re-imported as a straight line at (0, 0), above the artboard
and mostly out of view, and lost its path.

Resolve the textPath's href (SVG 2 href or xlink:href) to the <path>,
bake the text's user-space transform (and the path's own transform) into
it, and read startOffset (a percentage, or a length along the path) into
TextKind::OnPath.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@echelon
echelon merged commit cbf5dc9 into storytold:main Oct 4, 2026
@echelon

echelon commented Oct 4, 2026

Copy link
Copy Markdown
Member

Thanks @storybored379, merged! Type on a path now survives an SVG round trip. Handling both href and xlink:href, baking the path's own transform, and reading startOffset as either a percentage or a length is exactly the care this needed. I confirmed text_on_path_roundtrip fails without the change.

If you're not already there, come say hi in our Discord, we'd love to have you: https://discord.gg/artcraft

apexmiguel9-hub pushed a commit to apexmiguel9-hub/vectorcraft-android that referenced this pull request Oct 6, 2026
…lar shaders

MEDIDO, con la cadena de 35 frames del tombstone. Se lee de abajo arriba:

    storytold#15 egui_wgpu::Painter::set_window
    storytold#14 egui_wgpu::renderer::Renderer::new
    storytold#13 wgpu_core::...::CoreDevice::<dispatch>::DeviceInterface::create_render_pipeline
    storytold#12 wgpu_core::device::global::Global::device_create_render_pipeline
    storytold#11 wgpu_core::device::global::Global::device_create_general_render_pipeline
    storytold#10 wgpu_core::device::resource::Device::create_render_pipeline
    storytold#9 wgpu_core::device::resource::Device::create_render_pipeline_inner
    storytold#8 wgpu_hal::dynamic::device::vulkan::DynDevice::create_render_pipeline
    storytold#7 wgpu_hal::vulkan::device::Device::create_render_pipeline
    storytold#6 vulkan.mtk.so    (FragmentShaderCompileState::CompileUF()+1272)
    storytold#4 vulkan.mtk.so
    storytold#3 libufwriter.so   (BILParseStream+164)
    #00 libc.so          (abort+156)

**Los seis frames interiores no son de Rust, son del driver.** `egui_wgpu` pide su
primer pipeline, `wgpu` lo pasa a Vulkan, y `vulkan.mtk.so` aborta mientras compila
el fragment shader, dentro de `libufwriter.so` — el escritor de binarios del
compilador propietario, porque `BIL` es su IR.

## Y no es que a Vulkan le falte nada

Es lo que mas confunde, y conviene dejarlo escrito con numeros. Del perfil
`vp_gpuinfo` del Moto G56 5G (Android 16) y del informe de la base de datos de
Sascha Willems:

| | |
|---|---|
| `apiVersion` | **1.3.1023** (la base de datos lo da como 1.3.303) |
| extensiones | **129**, 61 de ellas `VK_KHR` |
| `VK_ANDROID_external_memory_android_hardware_buffer` | si |
| `driverID` | `VK_DRIVER_ID_IMAGINATION_PROPRIETARY`, PowerVR B-Series 25.1 |
| subgroup | 128, todas las operaciones |
| todas las versiones core 1.0-1.3 | soportadas |

O sea: **no falta ninguna feature, es un driver que conforms pero aborta.** Ni
siquiera hay mensaje: `PVR_ASSERT` viene compilado en release y es un `abort()` a
peloso, asi que el logcat no dice que construccion del SPIR-V le molesta. No hay
forma de averiguarlo desde aqui.

## Por que OpenGL no cuesta nada

Porque VectorCraft **rasteriza en CPU** con `vello_cpu`. La GPU solo presenta
pixeles que ya estan pintados. Toda la parte de Vulkan que de verdad importa —
compute shaders, para rasterizar en GPU— es justo la que este proyecto no usa, y lo
hizo a proposito para no depender del driver. Aqui el coste de GL es cero.

## Como queda

Una funcion, `wgpu_solo_opengl()`, que parte de
`WgpuSetupCreateNew::without_display_handle()` y cambia `instance_descriptor.backends`
a `Backends::GL`. Se escribe asi, modificando el campo, y no con sintaxis de
actualizacion de structs, porque no depende de que campos existan los que no se
nombran.

`eframe` reexporta `egui_wgpu` y `egui_wgpu::wgpu` (su `lib.rs:162`), asi que no
hacen falta dependencias nuevas.

**Queda anotado como una linea si el driver deja de abortar**, y el propio egui-wgpu
tiene ademas el interruptor `WGPU_BACKEND=opengl` para probarlo sin recompilar.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants