diff --git a/Cargo.lock b/Cargo.lock index bafa4765fc..3e5ad37e95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,12 +27,24 @@ dependencies = [ "gimli", ] +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "ahash" version = "0.8.12" @@ -109,6 +121,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" version = "0.6.20" @@ -201,6 +222,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -238,7 +270,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "object", "rustc-demangle", "windows-targets 0.52.6", @@ -277,6 +309,12 @@ dependencies = [ "bit-vec 0.9.1", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -600,7 +638,7 @@ dependencies = [ "anyhow", "cargo_metadata 0.23.1", "cef-dll-sys", - "clap", + "clap 4.5.46", "libloading 0.9.0", "objc2 0.6.3", "plist", @@ -694,6 +732,21 @@ dependencies = [ "half", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width 0.1.14", + "vec_map", +] + [[package]] name = "clap" version = "4.5.46" @@ -713,7 +766,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -799,7 +852,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width", + "unicode-width 0.2.1", ] [[package]] @@ -858,7 +911,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.1", "windows-sys 0.60.2", ] @@ -944,7 +997,7 @@ dependencies = [ "dyn-any", "glam", "graphene-hash", - "image", + "image 0.25.6", "kurbo", "log", "lyon_geom", @@ -1021,9 +1074,9 @@ dependencies = [ "anes", "cast", "ciborium", - "clap", + "clap 4.5.46", "criterion-plot", - "itertools", + "itertools 0.13.0", "num-traits", "oorandom", "plotters", @@ -1042,7 +1095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" dependencies = [ "cast", - "itertools", + "itertools 0.13.0", ] [[package]] @@ -1154,6 +1207,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + [[package]] name = "delaunator" version = "1.1.0" @@ -1359,7 +1422,7 @@ version = "2.3.2" source = "git+https://github.com/timon-schelling/cef-rs.git?branch=graphite-149#877c9cd8f7776e6c52e365d501ad44c73e17117a" dependencies = [ "bzip2", - "clap", + "clap 4.5.46", "fs-err", "indicatif", "regex", @@ -1633,7 +1696,18 @@ checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "libz-rs-sys", - "miniz_oxide", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "flo_curves" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c227ffb4f98baa9073c033e23243089d0ed3dd1f5cd620894452074db48a29b" +dependencies = [ + "itertools 0.8.2", + "roots", + "smallvec", ] [[package]] @@ -1884,7 +1958,7 @@ version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ - "unicode-width", + "unicode-width 0.2.1", ] [[package]] @@ -1925,6 +1999,16 @@ dependencies = [ "r-efi 6.0.0", ] +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gif" version = "0.13.3" @@ -2054,6 +2138,7 @@ dependencies = [ "tsify", "url", "vector-nodes", + "vector-types", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2101,14 +2186,14 @@ name = "graphene-cli" version = "0.1.0" dependencies = [ "chrono", - "clap", + "clap 4.5.46", "document-container", "document-format", "fern", "futures", "graph-craft", "graphene-std", - "image", + "image 0.25.6", "interpreted-executor", "log", "preprocessor", @@ -2180,7 +2265,7 @@ dependencies = [ "graphene-core", "graphic-nodes", "graphic-types", - "image", + "image 0.25.6", "log", "math-nodes", "node-macro", @@ -2239,7 +2324,7 @@ name = "graphite-desktop" version = "0.1.0" dependencies = [ "bytemuck", - "clap", + "clap 4.5.46", "ctrlc", "derivative", "dirs", @@ -2346,7 +2431,7 @@ dependencies = [ "graphene-std", "graphite-editor", "graphite-wasm-wrapper", - "image", + "image 0.25.6", "keyboard-types", "serde", "serde_json", @@ -2376,7 +2461,7 @@ dependencies = [ "graphene-hash", "graphene-std", "graphite-proc-macros", - "image", + "image 0.25.6", "interpreted-executor", "js-sys", "kurbo", @@ -2567,6 +2652,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -2865,6 +2959,25 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif 0.11.4", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png 0.16.8", + "scoped_threadpool", + "tiff", +] + [[package]] name = "image" version = "0.25.6" @@ -2874,7 +2987,7 @@ dependencies = [ "bytemuck", "byteorder-lite", "color_quant", - "gif", + "gif 0.13.3", "num-traits", "png 0.17.16", "zune-core", @@ -2924,7 +3037,7 @@ checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ "console", "portable-atomic", - "unicode-width", + "unicode-width 0.2.1", "unit-prefix", "web-time", ] @@ -3062,6 +3175,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -3133,6 +3255,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.98" @@ -3447,6 +3578,25 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3725,6 +3875,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -4327,6 +4499,18 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + [[package]] name = "png" version = "0.17.16" @@ -4337,7 +4521,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -4350,7 +4534,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -4361,7 +4545,7 @@ checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.2", "pin-project-lite", "rustix", "windows-sys 0.60.2", @@ -4660,7 +4844,7 @@ dependencies = [ "glam", "graphene-hash", "graphene-resource", - "image", + "image 0.25.6", "kurbo", "ndarray", "no-std-types", @@ -4706,7 +4890,7 @@ dependencies = [ "dyn-any", "glam", "graphene-hash", - "image", + "image 0.25.6", "node-macro", "serde", "serde_json", @@ -4881,6 +5065,7 @@ dependencies = [ "num-traits", "parley", "serde", + "simplecss", "skrifa 0.42.1", "text-nodes", "usvg", @@ -5029,6 +5214,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "roots" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84348444bd7ad45729d0c49a4240d7cdc11c9d512c06c5ad1835c1ad4acda6db" + [[package]] name = "roxmltree" version = "0.20.0" @@ -5248,6 +5439,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.2.0" @@ -5783,6 +5980,12 @@ dependencies = [ "quote", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.11.1" @@ -5964,6 +6167,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width 0.1.14", +] + [[package]] name = "third-party-licenses" version = "0.0.0" @@ -6025,6 +6237,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + [[package]] name = "time" version = "0.3.41" @@ -6464,6 +6687,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.1" @@ -6597,6 +6826,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "vector-nodes" version = "0.1.0" @@ -6614,12 +6849,16 @@ dependencies = [ "node-macro", "qrcodegen", "rand", + "rendering", "repeat-nodes", "rustc-hash 2.1.1", "serde", "tokio", "tsify", + "usvg", "vector-types", + "visioncortex", + "vtracer", "wasm-bindgen", ] @@ -6647,6 +6886,8 @@ dependencies = [ "serde_json", "tinyvec", "tsify", + "visioncortex", + "vtracer", "wasm-bindgen", ] @@ -6698,6 +6939,29 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "visioncortex" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9a7e6cc136c7c79b4adbd311c46c323a19db7baf446822b5f90f84375934e3" +dependencies = [ + "bit-vec 0.6.3", + "flo_curves", + "num-traits", +] + +[[package]] +name = "vtracer" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2307187c5c99e7387f3158a29b0be541b99b06c5c492598b77644bf5603bd0c" +dependencies = [ + "clap 2.34.0", + "fastrand", + "image 0.23.14", + "visioncortex", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 94050f9b09..45a52fbb53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -216,6 +216,8 @@ petgraph = { version = "0.7", default-features = false, features = ["graphmap"] half = { version = "2.4", default-features = false, features = ["bytemuck"] } tinyvec = { version = "1", features = ["std"] } criterion = { version = "0.7", features = ["html_reports"] } +vtracer = { version = "0.6.5" } +visioncortex = { version = "0.8.9" } gungraun = { version = "0.18" } ndarray = "0.16" strum = { version = "0.27", features = ["derive"] } diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index aa2dc553b1..f211a8f10e 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -1,7 +1,5 @@ -use super::transform_utils; use super::utility_types::ModifyInputsContext; -use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP}; -use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::graph_operation::utility_types::{TransformIn, import_usvg_node}; use crate::messages::portfolio::document::node_graph::document_node_definitions::BLEND_PATH_INPUT_INDEX; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector}; @@ -11,10 +9,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils::get_c use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::{NodeId, NodeInput}; use graph_craft::list; -use graphene_std::renderer::convert_usvg_path::convert_usvg_path; -use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; -use graphene_std::{Artboard, Color}; +use graphene_std::Artboard; +use graphene_std::renderer::usvg_utils::{SvgGradientInfo, extract_gradient_spaces, extract_graphite_gradient_stops}; #[derive(ExtractField)] pub struct GraphOperationMessageContext<'a> { @@ -464,6 +460,7 @@ impl MessageHandler> for responses.add(NodeGraphMessage::SelectedNodesUpdated); responses.add(NodeGraphMessage::SendGraph); } + GraphOperationMessage::NewSvg { id, svg, @@ -527,619 +524,3 @@ struct ArtboardInfo { output_nodes: Vec, merge_node: NodeId, } - -fn usvg_color(c: usvg::Color, a: f32) -> Color { - // `usvg::Color` channels are u8 sRGB display values (gamma-encoded); lift to linear-light for the internal `Color` - Color::from_gamma_srgb_channels(c.red as f32 / 255., c.green as f32 / 255., c.blue as f32 / 255., a) -} - -fn usvg_transform(c: usvg::Transform) -> DAffine2 { - DAffine2::from_cols_array(&[c.sx as f64, c.ky as f64, c.kx as f64, c.sy as f64, c.tx as f64, c.ty as f64]) -} - -const GRAPHITE_NAMESPACE: &str = "https://graphite.art"; - -/// Gradient information pre-parsed from the raw SVG XML, carrying what usvg's simplified tree drops. -struct SvgGradientInfo { - /// Real stops, keyed by gradient element `id`, for gradients Graphite exported with midpoint curve data. - graphite_stops: HashMap, - /// Gradient spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property. - spaces: HashMap, -} - -/// Pre-parses the raw SVG XML to resolve each gradient's inherited `color-interpolation` property, which usvg's -/// tree does not carry. Only `linearRGB` selects the linear space; `auto` and `sRGB` (browsers treat the -/// user-agent-defined `auto` as `sRGB`) mean gamma, as does any unrecognized value. -fn extract_gradient_spaces(svg: &str) -> HashMap { - let mut result = HashMap::new(); - - // Quick check: gradients in an SVG that never mentions `color-interpolation` all take the sRGB default - if !svg.contains("color-interpolation") { - return result; - } - - let doc = match usvg::roxmltree::Document::parse(svg) { - Ok(doc) => doc, - Err(_) => return result, - }; - - // The document's ` - - - - - - - - "##; - - let spaces = extract_gradient_spaces(svg); - assert_eq!(spaces.get("from-type-rule"), Some(&GradientSpace::RgbLinear), "a type rule in a style block should reach the gradient"); - assert_eq!( - spaces.get("from-class-rule"), - Some(&GradientSpace::RgbGamma), - "the class rule should outrank the type rule by specificity" - ); - assert_eq!(spaces.get("exact"), Some(&GradientSpace::RgbLinear), "the ID rule should outrank the class rule"); - assert_eq!(spaces.get("inline-beats-rules"), Some(&GradientSpace::RgbLinear), "the inline style should beat every style block rule"); - assert_eq!( - spaces.get("rule-beats-attribute"), - Some(&GradientSpace::RgbGamma), - "a style block rule should beat the presentation attribute" - ); - } - - #[test] - fn repeated_and_important_declarations_resolve_by_cascade_order() { - let svg = r##" - - - - - "##; - - let spaces = extract_gradient_spaces(svg); - assert_eq!( - spaces.get("last-declaration-wins"), - Some(&GradientSpace::RgbLinear), - "the last of repeated inline declarations should win" - ); - assert_eq!( - spaces.get("important-beats-later"), - Some(&GradientSpace::RgbLinear), - "an `!important` declaration should beat a later normal one" - ); - assert_eq!( - spaces.get("important-rule-beats-inline"), - Some(&GradientSpace::RgbLinear), - "an `!important` style block rule should beat the inline style" - ); - } - - #[test] - fn color_interpolation_yields_nothing_when_never_declared() { - let svg = r##""##; - - assert!( - extract_gradient_spaces(svg).is_empty(), - "gradients without any declaration should fall back to the caller's gamma default" - ); - } -} diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 6fbce96bc8..80a42177bb 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -1,4 +1,5 @@ use super::transform_utils; +use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP}; use crate::messages::portfolio::document::node_graph::document_node_definitions::{ ARTBOARD_DIMENSIONS_INPUT_INDEX, ARTBOARD_LOCATION_INPUT_INDEX, DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type, }; @@ -16,6 +17,7 @@ use graph_craft::{ProtoNodeIdentifier, list}; use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; +use graphene_std::renderer::usvg_utils::{ParsedSvgNode, ParsedSvgPath, SvgGradientInfo, extract_usvg_node}; use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, Stroke}; @@ -993,3 +995,233 @@ impl<'a> ModifyInputsContext<'a> { } } } + +/// Import a usvg node as the root of an SVG import operation. +/// +/// Phase 1: Parse the usvg tree into a `ParsedSvgNode` tree (pure data, no graphite dependencies). +/// Phase 2: Walk the parsed tree and build graphite layers. +/// +/// The root layer uses the full `move_layer_to_stack` (with push/collision logic) to correctly +/// interact with any existing layers in the parent stack. All descendant layers use a lightweight +/// O(n) import path that skips collision detection and instead calculates positions directly from +/// the known tree structure. +pub fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, gradient_info: &SvgGradientInfo) { + // Phase 1: parse usvg tree into intermediate representation (pure, no ModifyInputsContext) + let parsed = extract_usvg_node(node, &gradient_info); + + // Phase 2: build graphite layers from parsed tree + import_parsed_svg_root(modify_inputs, parsed, id, parent, insert_index); +} + +/// Handle a leaf parsed SVG node (Path, Image, Text). Returns 0 (no subtree extent). +fn import_parsed_svg_leaf(modify_inputs: &mut ModifyInputsContext, node: ParsedSvgNode, layer: LayerNodeIdentifier) -> u32 { + match node { + ParsedSvgNode::Path(path) => { + import_parsed_svg_path(modify_inputs, *path, layer); + 0 + } + ParsedSvgNode::Image { .. } => { + warn!("Skip SVG image node"); + 0 + } + ParsedSvgNode::Text(text) => { + let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string()); + modify_inputs.insert_text(text.text, font, TypesettingConfig::default(), layer); + if text.transform != DAffine2::IDENTITY + && let Some(transform_node_id) = modify_inputs.existing_proto_node_id(graphene_std::transform_nodes::transform::IDENTIFIER, false) + { + transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, text.transform); + } + modify_inputs.fill_color_set(Some(Color::BLACK)); + 0 + } + ParsedSvgNode::Group(_) => unreachable!("import_parsed_svg_leaf called on Group"), + } +} + +/// Phase 2 root handler: build graphite layers for a parsed SVG root node. +fn import_parsed_svg_root(modify_inputs: &mut ModifyInputsContext, node: ParsedSvgNode, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize) { + let layer = modify_inputs.create_layer(id); + + modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); + modify_inputs.layer_node = Some(layer); + if let Some(upstream_layer) = layer.next_sibling(modify_inputs.network_interface.document_metadata()) { + modify_inputs.network_interface.shift_node(&upstream_layer.to_node(), IVec2::new(0, STACK_VERTICAL_GAP), &[]); + } + + match node { + ParsedSvgNode::Group(group) => { + // Collect child extents for O(n) position calculation + let mut child_extents_svg_order: Vec = Vec::new(); + let mut group_extents_map: HashMap> = HashMap::new(); + + // Enable import mode: skips expensive is_acyclic checks and per-node cache invalidation + // during wiring since we're building a known tree structure where cycles are impossible + modify_inputs.import = true; + + for child in group.children { + let extent = import_parsed_svg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, &mut group_extents_map); + child_extents_svg_order.push(extent); + } + + modify_inputs.import = false; + modify_inputs.layer_node = Some(layer); + + // Rebuild the layer tree once now that all wiring is complete + modify_inputs.network_interface.load_structure(); + + // Set positions for all imported descendants in a single O(n) pass + let parent_pos = modify_inputs.network_interface.position(&layer.to_node(), &[]).unwrap_or(IVec2::ZERO); + set_import_child_positions(modify_inputs.network_interface, layer, parent_pos, &child_extents_svg_order, &group_extents_map); + + // Invalidate caches once after all positions are set + modify_inputs.network_interface.unload_all_nodes_click_targets(&[]); + modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]); + } + _ => { + import_parsed_svg_leaf(modify_inputs, node, layer); + } + } +} + +/// Recursively build graphite layers for a parsed SVG node as a descendant of the root import layer. +/// Uses lightweight wiring (no push/collision) and returns the subtree extent for position calculation. +/// +/// The subtree extent represents the additional vertical grid units that this node's descendants +/// occupy below the node's position. This is used to calculate correct y_offsets between siblings. +fn import_parsed_svg_node_inner( + modify_inputs: &mut ModifyInputsContext, + node: ParsedSvgNode, + id: NodeId, + parent: LayerNodeIdentifier, + insert_index: usize, + group_extents_map: &mut HashMap>, +) -> u32 { + let layer = modify_inputs.create_layer(id); + modify_inputs.network_interface.move_layer_to_stack_for_import(layer, parent, insert_index, &[]); + modify_inputs.layer_node = Some(layer); + + match node { + ParsedSvgNode::Group(group) => { + let mut child_extents: Vec = Vec::new(); + for child in group.children { + let extent = import_parsed_svg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, group_extents_map); + child_extents.push(extent); + } + modify_inputs.layer_node = Some(layer); + + let n = child_extents.len(); + let total_extent = if n == 0 { + 0 + } else { + (2 * STACK_VERTICAL_GAP as u32) * n as u32 - STACK_VERTICAL_GAP as u32 + child_extents.iter().sum::() + }; + group_extents_map.insert(layer, child_extents); + total_extent + } + _ => import_parsed_svg_leaf(modify_inputs, node, layer), + } +} + +/// Apply a parsed SVG path to a graphite layer: insert vector geometry, transform, fill, stroke. +fn import_parsed_svg_path(modify_inputs: &mut ModifyInputsContext, path: ParsedSvgPath, layer: LayerNodeIdentifier) { + let has_transform = path.transform != DAffine2::IDENTITY; + modify_inputs.insert_vector(path.subpaths, layer, has_transform, path.fill_paint.is_some(), path.stroke.is_some()); + + if has_transform && let Some(transform_node_id) = modify_inputs.existing_proto_node_id(graphene_std::transform_nodes::transform::IDENTIFIER, false) { + transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, path.transform); + } + + if let Some(fill_paint) = path.fill_paint { + let fill_paint = fill_paint.element(0).expect("failed to access the first Graphic in List"); + match fill_paint { + Graphic::Color(color) => { + let color = color.clone_item(0).expect("failed to access the first color in List"); + modify_inputs.fill_color_set(Some(*color.element())); + } + Graphic::Gradient(gradient) => { + let gradient = gradient.clone_item(0).expect("failed to access the first gradient in List"); + let gradient_form: &GradientForm = gradient.attribute(graphene_std::ATTR_GRADIENT_FORM).expect("failed to access GradientForm of the first gradient"); + let settings = GradientSettings { + spread: *gradient.attribute(graphene_std::ATTR_GRADIENT_SPREAD).expect("failed to access GradientSpread of the first gradient"), + space: *gradient.attribute(graphene_std::ATTR_GRADIENT_SPACE).expect("failed to access GradientSpace of the first gradient"), + ..Default::default() + }; + let transform: &DAffine2 = gradient.attribute(graphene_std::ATTR_TRANSFORM).expect("failed to access DAffine2 of the first gradient"); + modify_inputs.fill_gradient_set(gradient.element().clone(), *gradient_form, settings, *transform); + } + _ => {} + } + } + if let Some(stroke) = path.stroke + && let Some(stroke_paint) = path.stroke_paint + { + let stroke_paint = stroke_paint.element(0).expect("failed to access the first Graphic in List"); + match stroke_paint { + Graphic::Color(color) => { + let color = color.clone_item(0).expect("failed to access the first color in List"); + modify_inputs.stroke_set(Some(*color.element()), stroke); + } + Graphic::Gradient(_) => {} + _ => {} + } + } +} + +/// Set correct positions for all imported layers in a single top-down O(n) pass. +/// +/// For each group's child stack: +/// - The top-of-stack child (last SVG child) gets an `Absolute` position at `(parent_x - LAYER_INDENT_OFFSET, parent_y + STACK_VERTICAL_GAP)` +/// - All other children get `Stack(y_offset)` where `y_offset` accounts for the subtree extent of the sibling above them in the stack, ensuring no overlap. +pub fn set_import_child_positions( + network_interface: &mut NodeNetworkInterface, + group: LayerNodeIdentifier, + group_pos: IVec2, + child_extents_svg_order: &[u32], + group_extents_map: &HashMap>, +) { + use crate::messages::portfolio::document::utility_types::network_interface::LayerPosition; + + let layer_children: Vec<_> = group.children(network_interface.document_metadata()).collect(); + let n = child_extents_svg_order.len(); + + if n == 0 || layer_children.is_empty() { + return; + } + + // Children in the layer tree are in stack order (top to bottom), which is the REVERSE of SVG order. + // SVG order: [s_0, s_1, ..., s_{n-1}] with extents [e_0, e_1, ..., e_{n-1}] + // Stack order: [s_{n-1}, s_{n-2}, ..., s_0 ] (top to bottom) + // + // For stack child at index i: + // - SVG index = n - 1 - i + // - Previous stack sibling's SVG index = n - i + // - y_offset = extent_of_previous_sibling + STACK_VERTICAL_GAP + + let child_x = group_pos.x - LAYER_INDENT_OFFSET; + let mut current_y = group_pos.y + STACK_VERTICAL_GAP; + + for (i, child_layer) in layer_children.iter().enumerate() { + let child_pos = IVec2::new(child_x, current_y); + + if i == 0 { + // Top of stack: set to `Absolute` position + network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Absolute(child_pos), &[]); + } else { + // Below top: set `Stack` with `y_offset` based on previous sibling's subtree extent + let prev_sibling_svg_index = n - i; + let y_offset = child_extents_svg_order[prev_sibling_svg_index] + STACK_VERTICAL_GAP as u32; + network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Stack(y_offset), &[]); + } + + // Recurse into group children to set their descendants' positions + if let Some(grandchild_extents) = group_extents_map.get(child_layer) { + set_import_child_positions(network_interface, *child_layer, child_pos, grandchild_extents, group_extents_map); + } + + // Advance `current_y` for the next child: node height (STACK_VERTICAL_GAP) + gap (STACK_VERTICAL_GAP) + subtree extent + let child_svg_index = n - 1 - i; + let child_extent = child_extents_svg_order[child_svg_index]; + current_y += 2 * STACK_VERTICAL_GAP + child_extent as i32; + } +} diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 3ab0dcc0f1..2a64a7dc1d 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -934,6 +934,7 @@ fn static_node_properties() -> NodeProperties { "monitor_properties".to_string(), Box::new(|_node_id, _context| node_properties::string_properties("Used internally by the editor to obtain a layer thumbnail.")), ); + map.insert("vectorize_properties".to_string(), Box::new(node_properties::vectorize_properties)); map } diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 2efeb8b207..201dacd2a1 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -36,7 +36,12 @@ use graphene_std::vector::style::{ FillChoice, Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation, }; +use graphene_std::vector::vectorize::{ + ColorModeInput, ColorPrecisionInput, CornerThresholdInput, FilterSpeckleInput, HierarchicalInput, LayerDifferenceInput, LengthThresholdInput, MaxIterationsInput, PathPrecisionInput, + PathSimplifyModeInput, SpliceThresholdInput, VectorizeModeInput, +}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification}; +use graphene_std::vector_types::vectorize_config; use graphene_std::{NodeParameter, ParameterRef}; pub(crate) fn string_properties(text: &str) -> Vec { @@ -2781,6 +2786,45 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> ] } +pub fn vectorize_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let vector_mode = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(node_id, VectorizeModeInput, true, context)) + .property_row(); + let color_mode = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(node_id, ColorModeInput, true, context)) + .property_row(); + let hierarchical = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(node_id, HierarchicalInput, true, context)) + .property_row(); + let filter_speckle = number_widget(ParameterWidgetsInfo::new(node_id, FilterSpeckleInput, true, context), NumberInput::default().int().min(0.)); + let color_precision = number_widget(ParameterWidgetsInfo::new(node_id, ColorPrecisionInput, true, context), NumberInput::default().int().min(0.).max(8.)); + let layer_difference = number_widget(ParameterWidgetsInfo::new(node_id, LayerDifferenceInput, true, context), NumberInput::default().int().min(0.)); + let path_simplify_mode = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(node_id, PathSimplifyModeInput, true, context)) + .property_row(); + let corner_threshold = number_widget(ParameterWidgetsInfo::new(node_id, CornerThresholdInput, true, context), NumberInput::default().int().min(0.)); + let length_threshold = number_widget(ParameterWidgetsInfo::new(node_id, LengthThresholdInput, true, context), NumberInput::default().min(0.)); + let max_iterations = number_widget(ParameterWidgetsInfo::new(node_id, MaxIterationsInput, true, context), NumberInput::default().int().min(0.)); + let splice_threshold = number_widget(ParameterWidgetsInfo::new(node_id, SpliceThresholdInput, true, context), NumberInput::default().int().min(0.)); + let path_precision = number_widget(ParameterWidgetsInfo::new(node_id, PathPrecisionInput, true, context), NumberInput::default().int().min(0.)); + // let separate_layers = bool_widget(ParameterWidgetsInfo::new(node_id, SeparateLayersInput::INDEX, true, context), CheckboxInput::default()); + vec![ + vector_mode, + color_mode, + hierarchical, + LayoutGroup::row(filter_speckle), + LayoutGroup::row(color_precision), + LayoutGroup::row(layer_difference), + path_simplify_mode, + // LayoutGroup::row(separate_layers), + LayoutGroup::row(corner_threshold), + LayoutGroup::row(length_threshold), + LayoutGroup::row(max_iterations), + LayoutGroup::row(splice_threshold), + LayoutGroup::row(path_precision), + ] +} + pub struct ParameterWidgetsInfo<'a> { network_interface: &'a NodeNetworkInterface, resources: &'a ResourceMessageHandler, diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index 688a0ea712..32c2c9968c 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -30,6 +30,7 @@ graphene-application-io = { workspace = true, features = ["serde"] } rendering = { workspace = true, features = ["serde"] } raster-nodes = { workspace = true, features = ["serde"] } vector-nodes = { workspace = true, features = ["serde"] } +vector-types = { workspace = true, features = ["serde"] } graphic-types = { workspace = true, features = ["serde"] } text-nodes = { workspace = true, features = ["serde"] } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 23d2ef25a4..00d0354397 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -566,6 +566,10 @@ tagged_value! { // Legacy #[serde(alias = "Fill")] LegacyFill(graphic_types::migrations::legacy::LegacyFill), + ColorMode(vector_types::vectorize_config::ColorMode), + Hierarchical(vector_types::vectorize_config::Hierarchical), + PathSimplifyMode(vector_types::vectorize_config::PathSimplifyMode), + VectorizeMode(vector_types::vectorize_config::VectorizeMode) } impl TaggedValue { diff --git a/node-graph/libraries/rendering/Cargo.toml b/node-graph/libraries/rendering/Cargo.toml index 13facc359c..d0db53dec2 100644 --- a/node-graph/libraries/rendering/Cargo.toml +++ b/node-graph/libraries/rendering/Cargo.toml @@ -30,6 +30,7 @@ graphic-types = { workspace = true } vello = { workspace = true } vello_encoding = { workspace = true } parley = { workspace = true } +simplecss = { workspace = true } skrifa = { workspace = true } # Optional workspace dependencies diff --git a/node-graph/libraries/rendering/src/convert_usvg_path.rs b/node-graph/libraries/rendering/src/convert_usvg_path.rs deleted file mode 100644 index 2f07db846b..0000000000 --- a/node-graph/libraries/rendering/src/convert_usvg_path.rs +++ /dev/null @@ -1,47 +0,0 @@ -use glam::DVec2; -use vector_types::subpath::{ManipulatorGroup, Subpath}; -use vector_types::vector::PointId; - -pub fn convert_usvg_path(path: &usvg::Path) -> Vec> { - let mut subpaths = Vec::new(); - let mut manipulators_list = Vec::new(); - - let mut points = path.data().points().iter(); - let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64); - - for verb in path.data().verbs() { - match verb { - usvg::tiny_skia_path::PathVerb::Move => { - subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), false)); - let Some(start) = points.next().map(to_vec) else { continue }; - manipulators_list.push(ManipulatorGroup::new(start, Some(start), Some(start))); - } - usvg::tiny_skia_path::PathVerb::Line => { - let Some(end) = points.next().map(to_vec) else { continue }; - manipulators_list.push(ManipulatorGroup::new(end, Some(end), Some(end))); - } - usvg::tiny_skia_path::PathVerb::Quad => { - let Some(handle) = points.next().map(to_vec) else { continue }; - let Some(end) = points.next().map(to_vec) else { continue }; - if let Some(last) = manipulators_list.last_mut() { - last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor)); - } - manipulators_list.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end))); - } - usvg::tiny_skia_path::PathVerb::Cubic => { - let Some(first_handle) = points.next().map(to_vec) else { continue }; - let Some(second_handle) = points.next().map(to_vec) else { continue }; - let Some(end) = points.next().map(to_vec) else { continue }; - if let Some(last) = manipulators_list.last_mut() { - last.out_handle = Some(first_handle); - } - manipulators_list.push(ManipulatorGroup::new(end, Some(second_handle), Some(end))); - } - usvg::tiny_skia_path::PathVerb::Close => { - subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true)); - } - } - } - subpaths.push(Subpath::new(manipulators_list, false)); - subpaths -} diff --git a/node-graph/libraries/rendering/src/lib.rs b/node-graph/libraries/rendering/src/lib.rs index 418f4e9c0c..5c69355066 100644 --- a/node-graph/libraries/rendering/src/lib.rs +++ b/node-graph/libraries/rendering/src/lib.rs @@ -1,6 +1,6 @@ -pub mod convert_usvg_path; pub mod render_ext; mod renderer; pub mod to_peniko; +pub mod usvg_utils; pub use renderer::*; diff --git a/node-graph/libraries/rendering/src/usvg_utils.rs b/node-graph/libraries/rendering/src/usvg_utils.rs new file mode 100644 index 0000000000..128c772eca --- /dev/null +++ b/node-graph/libraries/rendering/src/usvg_utils.rs @@ -0,0 +1,620 @@ +use std::collections::HashMap; + +use core_types::{ + ATTR_GRADIENT_FORM, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, Color, + list::{ATTR_FILL, ATTR_STROKE, ATTR_TRANSFORM, Item, List}, +}; +use glam::{DAffine2, DVec2}; +use graphic_types::graphic::set_paint_attribute_at; +use graphic_types::{Graphic, IntoGraphicList}; +use log::warn; +use vector_types::{ + Vector, + subpath::{ManipulatorGroup, Subpath}, + vector::{ + PointId, + style::{Gradient, GradientForm, GradientSpace, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}, + }, + vectorize_config, +}; + +pub fn convert_usvg_path(path: &usvg::Path) -> Vec> { + let mut subpaths = Vec::new(); + let mut manipulators_list = Vec::new(); + + let mut points = path.data().points().iter(); + let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64); + + for verb in path.data().verbs() { + match verb { + usvg::tiny_skia_path::PathVerb::Move => { + subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), false)); + let Some(start) = points.next().map(to_vec) else { continue }; + manipulators_list.push(ManipulatorGroup::new(start, Some(start), Some(start))); + } + usvg::tiny_skia_path::PathVerb::Line => { + let Some(end) = points.next().map(to_vec) else { continue }; + manipulators_list.push(ManipulatorGroup::new(end, Some(end), Some(end))); + } + usvg::tiny_skia_path::PathVerb::Quad => { + let Some(handle) = points.next().map(to_vec) else { continue }; + let Some(end) = points.next().map(to_vec) else { continue }; + if let Some(last) = manipulators_list.last_mut() { + last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor)); + } + manipulators_list.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end))); + } + usvg::tiny_skia_path::PathVerb::Cubic => { + let Some(first_handle) = points.next().map(to_vec) else { continue }; + let Some(second_handle) = points.next().map(to_vec) else { continue }; + let Some(end) = points.next().map(to_vec) else { continue }; + if let Some(last) = manipulators_list.last_mut() { + last.out_handle = Some(first_handle); + } + manipulators_list.push(ManipulatorGroup::new(end, Some(second_handle), Some(end))); + } + usvg::tiny_skia_path::PathVerb::Close => { + subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true)); + } + } + } + subpaths.push(Subpath::new(manipulators_list, false)); + subpaths +} + +pub fn convert_gradient_spread(spread_method: usvg::SpreadMethod) -> GradientSpread { + match spread_method { + usvg::SpreadMethod::Pad => GradientSpread::Pad, + usvg::SpreadMethod::Reflect => GradientSpread::Reflect, + usvg::SpreadMethod::Repeat => GradientSpread::Repeat, + } +} + +fn usvg_color(c: usvg::Color, a: f32) -> Color { + // `usvg::Color` channels are u8 sRGB display values (gamma-encoded); lift to linear-light for the internal `Color` + Color::from_gamma_srgb_channels(c.red as f32 / 255., c.green as f32 / 255., c.blue as f32 / 255., a) +} + +pub fn usvg_transform(c: usvg::Transform) -> DAffine2 { + DAffine2::from_cols_array(&[c.sx as f64, c.ky as f64, c.kx as f64, c.sy as f64, c.tx as f64, c.ty as f64]) +} + +const GRAPHITE_NAMESPACE: &str = "https://graphite.art"; + +/// Gradient information pre-parsed from the raw SVG XML, carrying what usvg's simplified tree drops. +pub struct SvgGradientInfo { + /// Real stops, keyed by gradient element `id`, for gradients Graphite exported with midpoint curve data. + pub graphite_stops: HashMap, + /// Gradient spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property. + pub spaces: HashMap, +} + +/// Pre-parses the raw SVG XML to resolve each gradient's inherited `color-interpolation` property, which usvg's +/// tree does not carry. Only `linearRGB` selects the linear space; `auto` and `sRGB` (browsers treat the +/// user-agent-defined `auto` as `sRGB`) mean gamma, as does any unrecognized value. +pub fn extract_gradient_spaces(svg: &str) -> HashMap { + let mut result = HashMap::new(); + + // Quick check: gradients in an SVG that never mentions `color-interpolation` all take the sRGB default + if !svg.contains("color-interpolation") { + return result; + } + + let doc = match usvg::roxmltree::Document::parse(svg) { + Ok(doc) => doc, + Err(_) => return result, + }; + + // The document's ` + + + + + + + + "##; + + let spaces = extract_gradient_spaces(svg); + assert_eq!(spaces.get("from-type-rule"), Some(&GradientSpace::RgbLinear), "a type rule in a style block should reach the gradient"); + assert_eq!( + spaces.get("from-class-rule"), + Some(&GradientSpace::RgbGamma), + "the class rule should outrank the type rule by specificity" + ); + assert_eq!(spaces.get("exact"), Some(&GradientSpace::RgbLinear), "the ID rule should outrank the class rule"); + assert_eq!(spaces.get("inline-beats-rules"), Some(&GradientSpace::RgbLinear), "the inline style should beat every style block rule"); + assert_eq!( + spaces.get("rule-beats-attribute"), + Some(&GradientSpace::RgbGamma), + "a style block rule should beat the presentation attribute" + ); + } + + #[test] + fn repeated_and_important_declarations_resolve_by_cascade_order() { + let svg = r##" + + + + + "##; + + let spaces = extract_gradient_spaces(svg); + assert_eq!( + spaces.get("last-declaration-wins"), + Some(&GradientSpace::RgbLinear), + "the last of repeated inline declarations should win" + ); + assert_eq!( + spaces.get("important-beats-later"), + Some(&GradientSpace::RgbLinear), + "an `!important` declaration should beat a later normal one" + ); + assert_eq!( + spaces.get("important-rule-beats-inline"), + Some(&GradientSpace::RgbLinear), + "an `!important` style block rule should beat the inline style" + ); + } + + #[test] + fn color_interpolation_yields_nothing_when_never_declared() { + let svg = r##""##; + + assert!( + extract_gradient_spaces(svg).is_empty(), + "gradients without any declaration should fall back to the caller's gamma default" + ); + } +} diff --git a/node-graph/libraries/vector-types/Cargo.toml b/node-graph/libraries/vector-types/Cargo.toml index 0e8784b584..9ecc4a33af 100644 --- a/node-graph/libraries/vector-types/Cargo.toml +++ b/node-graph/libraries/vector-types/Cargo.toml @@ -7,9 +7,10 @@ authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" [features] -default = ["serde"] +default = ["serde", "vectorize"] serde = ["dep:serde", "core-types/serde"] wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] +vectorize = ["dep:vtracer", "dep:visioncortex"] [dependencies] # Local dependencies @@ -36,6 +37,8 @@ tinyvec = { workspace = true } serde = { workspace = true, optional = true } tsify = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true } +vtracer = { workspace = true, optional = true } +visioncortex = { workspace = true, optional = true } fixedbitset = "0.5.7" [dev-dependencies] diff --git a/node-graph/libraries/vector-types/src/lib.rs b/node-graph/libraries/vector-types/src/lib.rs index d15d4b6a73..77690e52f5 100644 --- a/node-graph/libraries/vector-types/src/lib.rs +++ b/node-graph/libraries/vector-types/src/lib.rs @@ -5,6 +5,8 @@ pub mod gradient; pub mod math; pub mod subpath; pub mod vector; +#[cfg(feature = "vectorize")] +pub mod vectorize_config; // Re-export commonly used types at the crate root pub use core_types as gcore; diff --git a/node-graph/libraries/vector-types/src/vectorize_config.rs b/node-graph/libraries/vector-types/src/vectorize_config.rs new file mode 100644 index 0000000000..367a45bfb3 --- /dev/null +++ b/node-graph/libraries/vector-types/src/vectorize_config.rs @@ -0,0 +1,74 @@ +use dyn_any::DynAny; + +#[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[widget(Radio)] +pub enum ColorMode { + #[default] + Color, + Binary, +} + +impl ColorMode { + pub fn to_vtracer(&self) -> vtracer::ColorMode { + match self { + ColorMode::Color => vtracer::ColorMode::Color, + ColorMode::Binary => vtracer::ColorMode::Binary, + } + } +} + +#[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[widget(Radio)] +pub enum VectorizeMode { + #[default] + FullImage, + PathTrace, +} + +#[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[widget(Radio)] +pub enum Hierarchical { + #[default] + Stacked, + Cutout, +} + +impl Hierarchical { + pub fn to_vtracer(&self) -> vtracer::Hierarchical { + match self { + Hierarchical::Stacked => vtracer::Hierarchical::Stacked, + Hierarchical::Cutout => vtracer::Hierarchical::Cutout, + } + } +} + +#[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[widget(Radio)] +pub enum PathSimplifyMode { + None, + Polygon, + #[default] + Spline, +} + +impl PathSimplifyMode { + pub fn to_vtracer(&self) -> visioncortex::PathSimplifyMode { + match self { + PathSimplifyMode::None => visioncortex::PathSimplifyMode::None, + PathSimplifyMode::Polygon => visioncortex::PathSimplifyMode::Polygon, + PathSimplifyMode::Spline => visioncortex::PathSimplifyMode::Spline, + } + } +} diff --git a/node-graph/nodes/raster/src/lib.rs b/node-graph/nodes/raster/src/lib.rs index c3a7699c8c..b054411d3e 100644 --- a/node-graph/nodes/raster/src/lib.rs +++ b/node-graph/nodes/raster/src/lib.rs @@ -1,5 +1,4 @@ #![cfg_attr(not(feature = "std"), no_std)] - pub mod adjust; pub mod adjustments; pub mod blending_nodes; diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index dba094bd4e..04c60a35f4 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -19,6 +19,7 @@ vector-types = { workspace = true } graphic-types = { workspace = true } node-macro = { workspace = true } repeat-nodes = { workspace = true } +rendering = { workspace = true } # Workspace dependencies dyn-any = { workspace = true } @@ -29,6 +30,9 @@ rand = { workspace = true } rustc-hash = { workspace = true } log = { workspace = true } qrcodegen = { workspace = true } +usvg = {workspace = true } +vtracer = { workspace = true } +visioncortex = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 77d5a3d192..fb0bbd6682 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -19,6 +19,8 @@ use graphic_types::{Graphic, IntoGraphicList}; use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape}; use rand::{Rng, SeedableRng}; +use rendering::usvg_utils::{SvgGradientInfo, extract_all_paths, extract_usvg_node}; +use repeat_nodes::raster_types::Bitmap; use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; use vector_types::GradientForm; @@ -35,6 +37,8 @@ use vector_types::vector::misc::{ use vector_types::vector::style::{DashPattern, Gradient, GradientSettings, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; use vector_types::vector::{PointDomain, RegionDomain}; +use vector_types::vectorize_config; +use vtracer::{ColorImage, Config, convert}; /// Implemented for `List` types that contain vector items reachable via mutable access. /// Used by the whole-collection Assign Colors node so it can apply to either `List` or `List`. @@ -970,6 +974,100 @@ where result } +#[node_macro::node(category("Vector"), path(core_types::vector), properties("vectorize_properties"), memoize)] +pub fn vectorize( + _ctx: impl Ctx, + image: List>, + vectorize_mode: Item, + color_mode: Item, + hierarchical: Item, + #[default(10.)] filter_speckle: Item, + #[default(8.)] color_precision: Item, + #[default(48.)] layer_difference: Item, + path_simplify_mode: Item, + #[default(60.)] corner_threshold: Item, + #[default(4.)] length_threshold: Item, + #[default(10.)] max_iterations: Item, + #[default(45.)] splice_threshold: Item, + #[default(2.)] path_precision: Item, +) -> List { + let ( + vectorize_mode, + color_mode, + hierarchical, + filter_speckle, + color_precision, + layer_difference, + path_simplify_mode, + corner_threshold, + length_threshold, + max_iterations, + splice_threshold, + path_precision, + ) = ( + vectorize_mode.into_element(), + color_mode.into_element(), + hierarchical.into_element(), + filter_speckle.into_element(), + color_precision.into_element(), + layer_difference.into_element(), + path_simplify_mode.into_element(), + corner_threshold.into_element(), + length_threshold.into_element(), + max_iterations.into_element(), + splice_threshold.into_element(), + path_precision.into_element(), + ); + + image.into_iter().fold(List::new(), |mut vectors, row| { + let image_data = row.element(); + let color_image = ColorImage { + width: image_data.width() as usize, + height: image_data.height() as usize, + pixels: image_data.to_flat_u8().0, + }; + let config: Config = Config { + color_mode: color_mode.to_vtracer(), + hierarchical: hierarchical.to_vtracer(), + filter_speckle: filter_speckle as usize, + color_precision: color_precision as i32, + layer_difference: layer_difference as i32, + mode: path_simplify_mode.to_vtracer(), + corner_threshold: corner_threshold as i32, + length_threshold, + max_iterations: max_iterations as usize, + splice_threshold: splice_threshold as i32, + path_precision: Some(path_precision as u32), + }; + let vectorized_image = match convert(color_image, config) { + Ok(image) => image, + Err(e) => { + log::error!("Vectorization failed for image:\n{e}"); + return vectors; + } + }; + let image_svg = vectorized_image.to_string(); + let svg_tree = match usvg::Tree::from_str(&image_svg, &usvg::Options::default()) { + Ok(t) => t, + Err(e) => { + log::error!("Failed to create a usvg tree:\n{e}"); + return vectors; + } + }; + // log::debug!("vectorized_image: {}", image_svg); + + let gradient_info = SvgGradientInfo { + graphite_stops: HashMap::new(), + spaces: HashMap::new(), + }; + let parsed_root = extract_usvg_node(&usvg::Node::Group(Box::new(svg_tree.root().clone())), &gradient_info); + // let mut i = 1; + extract_all_paths(parsed_root, &mut vectors, vectorize_mode); + + vectors + }) +} + /// Automatically constructs tangents (Bézier handles) for anchor points in a vector path. #[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))] async fn auto_tangents(