diff --git a/ROADMAP.md b/ROADMAP.md index 341460fb8..a8612a057 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -144,7 +144,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist | M5 | Performance | ๐ŸŸก background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3ร— faster (50k paths: 722 โ†’ 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15โ€“25 | | M6 | Path operations (Pathfinder, Shape Builder, offsetโ€ฆ) | โœ… mostly done (Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder edge erase, large-offset bug open) | 3โ€“6 | | M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | ๐ŸŸก Character/Paragraph Styles, Area Type Options, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (searchable) and found by name on open/import, CJK names in the UI, Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects, tab stops + Tabs panel done; tab leaders, spell check, vertical type pending | 45โ€“60 | -| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ๐ŸŸก live Blends, Envelopes (warp/mesh/top object), Width tool, Liquify tools, Puppet Warp and Perspective Grid landed; fidelity pass under way (M8.2 tool options persist and Liquify tools share one brush, M8.1 Delete removes Width points and Puppet Warp pins, not the art, M8.8 Define Grid dialog with viewing angle, units, colours, M8.15 Envelope dialogs, Reset with Warp/Mesh, full Envelope Options) | 8โ€“12 | +| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ๐ŸŸก live Blends, Envelopes (warp/mesh/top object), Width tool, Liquify tools, Puppet Warp and Perspective Grid landed; fidelity pass under way (M8.2 tool options persist and Liquify tools share one brush, M8.1 Delete removes Width points and Puppet Warp pins, not the art, M8.8 Define Grid dialog with viewing angle, units, colours, M8.15 Envelope dialogs, Reset with Warp/Mesh, full Envelope Options, M8.16 enveloped type exports distorted) | 8โ€“12 | | M9 | Live effects (+ 3D & Materials) | ๐ŸŸก 2D effects done incl. Effect โ†’ Pathfinder and Color Adjustments; SVG Filters, Document Raster Effects Settings, 3D pending | 86โ€“135 | | M10 | Brushes, symbols, patterns, Repeat | ๐ŸŸก pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23โ€“37 | | M11 | Artboards & views (artboard panel/tool, Trim View, middle-button pan and print tiling done; multiple windows, presentation polish) | ๐ŸŸก | 15โ€“25 | diff --git a/crates/cad/src/scene.rs b/crates/cad/src/scene.rs index 11d15e43f..69dd14631 100644 --- a/crates/cad/src/scene.rs +++ b/crates/cad/src/scene.rs @@ -316,7 +316,7 @@ impl<'a> Scene<'a> { } // Live blends, envelopes, meshes and repeats are written as their evaluated art. NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => { - let g = vectorcraft_doc::live::expand_deep(n, None); + let g = vectorcraft_effects::expand_live_deep(n); for c in g.children().into_iter().flatten() { self.node(c, false); } diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 303ab40b8..97272f7eb 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -34,6 +34,7 @@ mod bake; mod clip; mod distort; mod group; +mod live; mod marks; mod raster; mod reshape; @@ -58,6 +59,7 @@ pub use group::{ OutlineHook, PATHFINDER_EFFECTS, evaluate_container, has_container_appearance, has_pathfinder, is_pathfinder, member_shapes, paints, pathfinder_children, }; +pub use live::{expand_live, expand_live_deep, text_outliner}; pub use marks::{CROP_MARKS, crop_marks_art, has_crop_marks}; pub use raster::{RasterFx, outset, raster_effects}; pub use reshape::{expand_outlined, needs_outline, outline_art, outline_text, reshape}; diff --git a/crates/effects/src/live.rs b/crates/effects/src/live.rs new file mode 100644 index 000000000..c2f0f8a2f --- /dev/null +++ b/crates/effects/src/live.rs @@ -0,0 +1,25 @@ +//! Live objects (blends, envelopes, gradient meshes, repeats) evaluated alike for the renderer and +//! every exporter, with what the pure evaluation in [`vectorcraft_doc::live`] can't do itself: type +//! inside an envelope distorts as its glyph outlines, run by run ([`outline_text`]). + +use vectorcraft_doc::Node; +use vectorcraft_doc::live::{self, Outliner}; + +use crate::outline_text; + +/// The text outliner live evaluation uses: type as its glyph outlines, one path per run in the +/// run's paint ([`outline_text`]). +pub fn text_outliner() -> Outliner<'static> { + Some(&outline_text) +} + +/// One level of evaluation of live object `n` ([`live::expand_live`]) with type outlined. +pub fn expand_live(n: &Node) -> Vec { + live::expand_live_with(n, text_outliner()) +} + +/// `n` with every live object in it replaced by its evaluated art ([`live::expand_deep`]) and type +/// inside envelopes outlined: what the exporters write for live objects. +pub fn expand_live_deep(n: &Node) -> Node { + live::expand_deep(n, text_outliner()) +} diff --git a/crates/engine/src/cmd/live.rs b/crates/engine/src/cmd/live.rs index f3d05c87f..fa52015bc 100644 --- a/crates/engine/src/cmd/live.rs +++ b/crates/engine/src/cmd/live.rs @@ -826,8 +826,7 @@ fn env_options(s: &mut Session, p: &Value) -> Result { /// transparency and opacity mask; the generated pieces get ids of their own. fn expand_envelope(d: &mut Document, id: NodeId) -> Result<()> { let n = d.node(id).cloned().ok_or(EngineError::NoNode(id))?; - let hook: &dyn Fn(&Node) -> Option = &vectorcraft_render::effects::outline_text; - let mut g = live::expanded_group(&n, Some(hook)); + let mut g = live::expanded_group(&n, vectorcraft_render::effects::text_outliner()); fix_ids(d, &mut g); // Outlined type repeats its object's id on its pieces. vectorcraft_render::effects::fresh_ids(d, &mut g, &mut Default::default()); diff --git a/crates/engine/src/tests_envelope.rs b/crates/engine/src/tests_envelope.rs index 54e000f99..42ac631ad 100644 --- a/crates/engine/src/tests_envelope.rs +++ b/crates/engine/src/tests_envelope.rs @@ -209,3 +209,60 @@ fn envelope_options_round_trip_natively_and_old_files_load() { } assert!(serde_json::to_value(&legacy).unwrap()["kind"].get("options").is_none()); } + +/// Regression: every exporter wrote type inside an envelope undistorted (no text outliner), so a +/// warped line of type exported the same whatever its bend. +#[test] +fn enveloped_type_exports_distorted_in_every_format() { + let mut s = session(); + let t = id_of(&s.execute("text.create", &json!({"x": 100, "y": 200, "text": "Envelope", "size": 48})).unwrap()); + sel(&mut s, &[t]); + s.execute("object.envelope.makeWithWarp", &json!({"style": "arch", "bend": 0})).unwrap(); + let export = + |s: &mut Session, format: &str| s.execute("document.export", &json!({"format": format})).unwrap()["dataBase64"].as_str().unwrap().to_string(); + for format in ["svg", "pdf", "eps", "emf", "dxf"] { + s.execute("object.envelope.options", &json!({"bend": 0})).unwrap(); + let flat = export(&mut s, format); + assert_eq!(export(&mut s, format), flat, "{format} exports are repeatable"); + s.execute("object.envelope.options", &json!({"bend": 80})).unwrap(); + assert_ne!(export(&mut s, format), flat, "{format}: the bend shows in the export"); + } + let svg = String::from_utf8(vectorcraft_format::base64_decode(&export(&mut s, "svg")).unwrap()).unwrap(); + assert!(!svg.contains(" 200 && p[1] < 60 { + assert!(left, "red at ({x}, {y})"); + red += 1; + } else if p[0] < 60 && p[1] < 60 && p[3] > 200 { + assert!(!left, "black at ({x}, {y})"); + black += 1; + } + } + } + assert!(red > 100 && black > 100, "{red} red, {black} black"); +} diff --git a/crates/eps/src/scene.rs b/crates/eps/src/scene.rs index bef20136b..50c8dbd60 100644 --- a/crates/eps/src/scene.rs +++ b/crates/eps/src/scene.rs @@ -292,7 +292,7 @@ impl<'a> Scene<'a> { } // Live blends, envelopes, meshes and repeats are written as their evaluated art. NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => { - let g = vectorcraft_doc::live::expand_deep(n, None); + let g = vectorcraft_effects::expand_live_deep(n); for c in g.children().into_iter().flatten() { self.node(c, false); } diff --git a/crates/metafile/src/scene.rs b/crates/metafile/src/scene.rs index 10077ad04..649e6c422 100644 --- a/crates/metafile/src/scene.rs +++ b/crates/metafile/src/scene.rs @@ -190,7 +190,7 @@ impl<'a> Scene<'a> { } // Live blends, envelopes, meshes and repeats are written as their evaluated art. NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => { - let g = vectorcraft_doc::live::expand_deep(n, None); + let g = vectorcraft_effects::expand_live_deep(n); for c in g.children().into_iter().flatten() { self.node(c, false); } diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 955270e97..268a877d4 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -983,7 +983,7 @@ impl Exporter<'_> { } // Live blends/envelopes/meshes export their evaluated (expanded) form. NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => { - let g = vectorcraft_doc::live::expand_deep(n, None); + let g = vectorcraft_effects::expand_live_deep(n); for c in g.children().into_iter().flatten() { self.node(s, c, page, false); } diff --git a/crates/render/src/live.rs b/crates/render/src/live.rs index 1af0de424..e1918f5c6 100644 --- a/crates/render/src/live.rs +++ b/crates/render/src/live.rs @@ -10,13 +10,13 @@ use std::collections::HashMap; use std::sync::Arc; -use vectorcraft_doc::live::{self, GradientMesh, MeshQuad}; +use vectorcraft_doc::live::{GradientMesh, MeshQuad}; use vectorcraft_doc::{Node, NodeKind}; -use vectorcraft_geom::{BezPath, PathData, Vec2}; +use vectorcraft_geom::{BezPath, Vec2}; use vello_cpu::RenderContext; use vello_cpu::peniko; -use crate::{Frame, Renderer, text_geom}; +use crate::{Frame, Renderer}; type Expanded = Arc>>; type MeshEntry = (Arc, Arc>, u64); @@ -41,30 +41,9 @@ impl LiveCache { } } -/// Text converted to one outline path (for envelopes): glyphs in document space, painted with the -/// first run's fill/stroke. -pub(crate) fn outline_text(n: &Node) -> Option { - let NodeKind::Text(t) = &n.kind else { return None }; - let g = text_geom(t); - let mut bp = g.all.clone(); - bp.apply_affine(t.xf); - let mut out = Node::path(n.id, PathData::from_bezpath(&bp), n.appearance.clone()); - if out.appearance.items.is_empty() - && let Some(r) = t.runs.first() - { - out.appearance = r.style.appearance(); - } - out.opacity = n.opacity; - out.blend = n.blend; - Some(out) -} - -/// Evaluate a live node one level (blend steps / envelope result, text outlined; mesh โ†’ flat -/// pieces). Non-live nodes return themselves. -pub fn expand_live(n: &Node) -> Vec { - let hook: &dyn Fn(&Node) -> Option = &outline_text; - live::expand_live_with(n, Some(hook)) -} +/// One level of evaluation of a live node (blend steps, envelope result with type outlined run by +/// run, mesh โ†’ flat pieces); the exporters evaluate them alike. Non-live nodes return themselves. +pub use crate::effects::expand_live; /// Subdivisions per patch for a patch about `size_px` device pixels across. fn mesh_level(m: &GradientMesh, bounds_px: f64) -> usize { @@ -82,8 +61,7 @@ impl Renderer { e.2 = self.live.stamp; return e.1.clone(); } - let hook: &dyn Fn(&Node) -> Option = &outline_text; - let v: Expanded = match crate::effects::pathfinder_children(a, Some(hook)) { + let v: Expanded = match crate::effects::pathfinder_children(a, crate::effects::text_outliner()) { Some(children) => Arc::new(children), None => Arc::new(expand_live(a).into_iter().map(Arc::new).collect()), }; diff --git a/crates/svg/src/export.rs b/crates/svg/src/export.rs index 44c8c498b..c380750ee 100644 --- a/crates/svg/src/export.rs +++ b/crates/svg/src/export.rs @@ -1290,7 +1290,7 @@ impl Writer<'_> { } // Live blends/envelopes/meshes export their evaluated (expanded) form. NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => { - let g = vectorcraft_doc::live::expand_deep(n, None); + let g = vectorcraft_effects::expand_live_deep(n); self.node_body(&g); } } diff --git a/docs/mcp.md b/docs/mcp.md index dedf38a17..b3d12fe99 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1785,6 +1785,8 @@ envelopes, the path for a top-object envelope); the content keeps the envelope's (a group around it carries them when there are several objects). `object.envelope.expand`, and `object.expand` with `object` on, replace envelopes by groups of their distorted content that keep the name, transparency, knockout and opacity mask. +Type inside an envelope distorts as its glyph outlines, run by run in each run's paint, on the canvas and in every +export (SVG, PDF, EPS, EMF/WMF, DXF). ```json {"name":"run_command","arguments":{"command":"object.envelope.makeWithWarp","params":{"style":"arch","bend":40}}}