From eb618004c6964f3d5727ac5da91928da6d4fd875 Mon Sep 17 00:00:00 2001 From: Oliver Davies Date: Sun, 5 Oct 2025 15:23:18 -0700 Subject: [PATCH] Initial working versions of decimate and vectorize --- Cargo.lock | 323 ++++++++++++++-- Cargo.toml | 18 +- .../document/node_graph/node_properties.rs | 1 + node-graph/gcore/Cargo.toml | 2 + .../gcore/src/vector/algorithms/decimation.rs | 62 +++ node-graph/gcore/src/vector/algorithms/mod.rs | 2 + .../gcore/src/vector/algorithms/vectorize.rs | 357 ++++++++++++++++++ node-graph/gcore/src/vector/misc.rs | 2 + node-graph/gcore/src/vector/vector_nodes.rs | 173 +++++++++ node-graph/graph-craft/src/document/value.rs | 1 + .../interpreted-executor/src/node_registry.rs | 2 + 11 files changed, 907 insertions(+), 36 deletions(-) create mode 100644 node-graph/gcore/src/vector/algorithms/decimation.rs create mode 100644 node-graph/gcore/src/vector/algorithms/vectorize.rs diff --git a/Cargo.lock b/Cargo.lock index 9e319b16e3..e5bbff8a45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,12 +33,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" @@ -124,6 +136,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" @@ -405,6 +426,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" @@ -421,7 +453,7 @@ dependencies = [ "arrayvec", "log", "nom", - "num-rational", + "num-rational 0.4.2", "v_frame", ] @@ -443,7 +475,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "object", "rustc-demangle", "windows-targets 0.52.6", @@ -470,9 +502,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[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" @@ -652,7 +690,7 @@ source = "git+https://github.com/rust-gpu/cargo-gpu?rev=f969528e87baa17a7d48eecf dependencies = [ "anyhow", "cargo_metadata", - "clap", + "clap 4.5.46", "crossterm", "directories", "env_logger", @@ -814,6 +852,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" @@ -833,7 +886,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -871,7 +924,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width", + "unicode-width 0.2.1", ] [[package]] @@ -930,7 +983,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.1", "windows-sys 0.60.2", ] @@ -1044,7 +1097,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap", + "clap 4.5.46", "criterion-plot", "itertools 0.13.0", "num-traits", @@ -1161,6 +1214,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 = "deranged" version = "0.4.0" @@ -1302,7 +1365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de5f62da6c2850fd20cdefef7dd5389121869a88de8af3fda5a20bc849eafdd" dependencies = [ "bzip2", - "clap", + "clap 4.5.46", "indicatif", "regex", "semver", @@ -1497,7 +1560,7 @@ dependencies = [ "bit_field", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.8.9", "rayon-core", "smallvec", "zune-inflate", @@ -1517,6 +1580,9 @@ name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +dependencies = [ + "getrandom 0.2.16", +] [[package]] name = "fdeflate" @@ -1562,7 +1628,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", - "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 0.0.6", + "smallvec", ] [[package]] @@ -1882,6 +1959,16 @@ dependencies = [ "wasm-bindgen", ] +[[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" @@ -2060,7 +2147,7 @@ name = "graphene-cli" version = "0.1.0" dependencies = [ "chrono", - "clap", + "clap 4.5.46", "fern", "futures", "graph-craft", @@ -2085,7 +2172,7 @@ dependencies = [ "dyn-any", "glam", "graphene-core-shaders", - "image", + "image 0.25.6", "kurbo", "log", "lyon_geom", @@ -2103,6 +2190,8 @@ dependencies = [ "specta", "tinyvec", "tokio", + "visioncortex", + "vtracer", "wgpu", ] @@ -2163,7 +2252,7 @@ dependencies = [ "graphene-core", "graphene-core-shaders", "graphene-raster-nodes-shaders", - "image", + "image 0.25.6", "kurbo", "ndarray", "node-macro", @@ -2209,7 +2298,7 @@ dependencies = [ "graphene-path-bool", "graphene-raster-nodes", "graphene-svg-renderer", - "image", + "image 0.25.6", "log", "node-macro", "reqwest", @@ -2287,7 +2376,7 @@ dependencies = [ "graph-craft", "graphene-std", "graphite-editor", - "image", + "image 0.25.6", "ron", "serde", "thiserror 2.0.16", @@ -2311,7 +2400,7 @@ dependencies = [ "graph-craft", "graphene-std", "graphite-proc-macros", - "image", + "image 0.25.6", "interpreted-executor", "js-sys", "kurbo", @@ -2422,6 +2511,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" @@ -2740,6 +2838,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 0.1.22", + "num-iter", + "num-rational 0.3.2", + "num-traits", + "png 0.16.8", + "scoped_threadpool", + "tiff 0.6.1", +] + [[package]] name = "image" version = "0.25.6" @@ -2750,15 +2867,15 @@ dependencies = [ "byteorder-lite", "color_quant", "exr", - "gif", + "gif 0.13.3", "image-webp 0.2.4", "num-traits", - "png", + "png 0.17.16", "qoi", "ravif", "rayon", "rgb", - "tiff", + "tiff 0.9.1", "zune-core", "zune-jpeg", ] @@ -2832,7 +2949,7 @@ checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ "console", "portable-atomic", - "unicode-width", + "unicode-width 0.2.1", "unit-prefix", "web-time", ] @@ -2946,6 +3063,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.12.1" @@ -3026,6 +3152,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 = "jpeg-decoder" version = "0.3.2" @@ -3329,6 +3464,25 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[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" @@ -3577,6 +3731,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-rational" version = "0.4.2" @@ -3941,11 +4117,11 @@ dependencies = [ "criterion", "glam", "glob", - "image", + "image 0.25.6", "lyon_geom", "regex", "resvg", - "roots", + "roots 0.0.8", "rustc-hash 2.1.1", "slotmap", "smallvec", @@ -4124,6 +4300,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" @@ -4134,7 +4322,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -4145,7 +4333,7 @@ checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.2", "pin-project-lite", "rustix", "windows-sys 0.60.2", @@ -4673,7 +4861,7 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" dependencies = [ - "gif", + "gif 0.13.3", "image-webp 0.1.3", "log", "pico-args", @@ -4744,6 +4932,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 = "roots" version = "0.0.8" @@ -4941,6 +5135,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" @@ -5330,7 +5530,7 @@ version = "0.9.0" source = "git+https://github.com/rust-gpu/rust-gpu?rev=c12f216121820580731440ee79ebc7403d6ea04f#c12f216121820580731440ee79ebc7403d6ea04f" dependencies = [ "cargo_metadata", - "clap", + "clap 4.5.46", "log", "memchr", "notify", @@ -5394,6 +5594,12 @@ dependencies = [ "float-cmp", ] +[[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" @@ -5597,6 +5803,15 @@ dependencies = [ "winapi-util", ] +[[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 = "thiserror" version = "1.0.69" @@ -5646,6 +5861,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 0.1.22", + "miniz_oxide 0.4.4", + "weezl", +] + [[package]] name = "tiff" version = "0.9.1" @@ -5653,7 +5879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" dependencies = [ "flate2", - "jpeg-decoder", + "jpeg-decoder 0.3.2", "weezl", ] @@ -5699,7 +5925,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png", + "png 0.17.16", "tiny-skia-path", ] @@ -6090,6 +6316,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" @@ -6261,6 +6493,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "vello" version = "0.5.0" @@ -6270,7 +6508,7 @@ dependencies = [ "futures-intrusive", "log", "peniko", - "png", + "png 0.17.16", "skrifa 0.31.3", "static_assertions", "thiserror 2.0.16", @@ -6315,6 +6553,29 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "visioncortex" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbe154fc281d74b2322adc18de7f9bbb9c4318d58ef6c201007f3fd99df9fff" +dependencies = [ + "bit-vec 0.6.3", + "flo_curves", + "log", + "num-traits", +] + +[[package]] +name = "vtracer" +version = "0.6.4" +source = "git+https://github.com/otdavies/vtracer?rev=31a8b270f6ff825955cf3af888eae6fbfd36e146#31a8b270f6ff825955cf3af888eae6fbfd36e146" +dependencies = [ + "clap 2.34.0", + "fastrand", + "image 0.23.14", + "visioncortex", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -6626,7 +6887,7 @@ checksum = "f7b882196f8368511d613c6aeec80655160db6646aebddf8328879a88d54e500" dependencies = [ "arrayvec", "bit-set", - "bit-vec", + "bit-vec 0.8.0", "bitflags 2.9.3", "bytemuck", "cfg_aliases", diff --git a/Cargo.toml b/Cargo.toml index 73f856eea6..d4e02fd76a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,14 +56,17 @@ default-members = [ ] resolver = "2" +[patch.crates-io] +vtracer = { git = "https://github.com/otdavies/vtracer", rev = "31a8b270f6ff825955cf3af888eae6fbfd36e146" } + [workspace.dependencies] # Local dependencies dyn-any = { path = "libraries/dyn-any", features = [ - "derive", - "glam", - "reqwest", - "log-bad-types", - "rc", +"derive", +"glam", +"reqwest", +"log-bad-types", +"rc", ] } preprocessor = { path = "node-graph/preprocessor" } math-parser = { path = "libraries/math-parser" } @@ -195,9 +198,14 @@ open = "5.3" poly-cool = "0.3" spin = "0.10" clap = "4.5" +# Set compile type to rlib for wasm compatibility +visioncortex = { version = "0.8.8" } +vtracer = { version = "0.6.4" } spirv-std = { git = "https://github.com/rust-gpu/rust-gpu", rev = "c12f216121820580731440ee79ebc7403d6ea04f", features = ["bytemuck"] } cargo-gpu = { git = "https://github.com/rust-gpu/cargo-gpu", rev = "f969528e87baa17a7d48eecf4a6fcfdcaaf30566" } + + [workspace.lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] } 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 0dd2d5906c..33b37dbc2d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -215,6 +215,7 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + // Add here Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml index a549c1ed0d..4cd06bf56e 100644 --- a/node-graph/gcore/Cargo.toml +++ b/node-graph/gcore/Cargo.toml @@ -40,6 +40,8 @@ lyon_geom = { workspace = true } log = { workspace = true } base64 = { workspace = true } poly-cool = { workspace = true } +vtracer = { workspace = true } +visioncortex = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } diff --git a/node-graph/gcore/src/vector/algorithms/decimation.rs b/node-graph/gcore/src/vector/algorithms/decimation.rs new file mode 100644 index 0000000000..4c249f66bd --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/decimation.rs @@ -0,0 +1,62 @@ +use glam::DVec2; + +/// Implements the Ramer-Douglas-Peucker algorithm to find indices of points to keep. +pub fn ramer_douglas_peucker(points: &[DVec2], epsilon: f64) -> Vec { + if points.len() <= 2 { + return (0..points.len()).collect(); + } + + let mut kept_indices = Vec::new(); + rdp_recursive(points, 0, points.len() - 1, epsilon, &mut kept_indices); + kept_indices.sort_unstable(); + kept_indices.dedup(); + kept_indices +} + +pub fn rdp_recursive(points: &[DVec2], start_idx: usize, end_idx: usize, epsilon: f64, kept_indices: &mut Vec) { + if start_idx >= end_idx { + return; + } + + let mut dmax = 0.0; + let mut index = start_idx; + + // Find the point with maximum perpendicular distance from the line segment + for i in (start_idx + 1)..end_idx { + let d = perpendicular_distance(points[i], points[start_idx], points[end_idx]); + if d > dmax { + index = i; + dmax = d; + } + } + + // If max distance is greater than epsilon, recursively simplify + if dmax > epsilon { + rdp_recursive(points, start_idx, index, epsilon, kept_indices); + rdp_recursive(points, index, end_idx, epsilon, kept_indices); + } else { + // Keep only the endpoints + kept_indices.push(start_idx); + kept_indices.push(end_idx); + } +} + +/// Calculates the perpendicular distance from a point to a line defined by two endpoints. +pub fn perpendicular_distance(point: DVec2, line_start: DVec2, line_end: DVec2) -> f64 { + let dx = line_end.x - line_start.x; + let dy = line_end.y - line_start.y; + + let line_length_squared = dx * dx + dy * dy; + + // If the line segment is actually a point, return the distance to that point + if line_length_squared == 0.0 { + return point.distance(line_start); + } + + // Calculate perpendicular distance using the cross product formula: + // distance = |dy·px - dx·py + x2·y1 - y2·x1| / √(dx² + dy²) + let numerator = (dy * point.x - dx * point.y + line_end.x * line_start.y - line_end.y * line_start.x).abs(); + let denominator = line_length_squared.sqrt(); + + numerator / denominator +} diff --git a/node-graph/gcore/src/vector/algorithms/mod.rs b/node-graph/gcore/src/vector/algorithms/mod.rs index b9284f327d..97cc4980fb 100644 --- a/node-graph/gcore/src/vector/algorithms/mod.rs +++ b/node-graph/gcore/src/vector/algorithms/mod.rs @@ -1,5 +1,6 @@ pub mod bezpath_algorithms; mod contants; +pub mod decimation; pub mod instance; pub mod intersection; pub mod merge_by_distance; @@ -7,3 +8,4 @@ pub mod offset_subpath; pub mod poisson_disk; pub mod spline; pub mod util; +pub mod vectorize; diff --git a/node-graph/gcore/src/vector/algorithms/vectorize.rs b/node-graph/gcore/src/vector/algorithms/vectorize.rs new file mode 100644 index 0000000000..1296aecdd0 --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/vectorize.rs @@ -0,0 +1,357 @@ +use crate::{ + Graphic, + raster_types::{CPU, Raster}, + table::{Table, TableRow, TableRowRef}, + vector::Vector, + vector::VectorExt, + vector::style::Fill, +}; +use glam::{DAffine2, DVec2}; +use graphene_core_shaders::color::Color; +use kurbo::{BezPath, PathEl}; +use visioncortex::PathSimplifyMode; +use vtracer::{ColorMode, Config, Hierarchical}; + +/// Parses SVG path data and appends it to a Vector +pub fn parse_svg_paths_to_vector(svg_content: &str) -> Vec> { + let mut rows = Vec::new(); + + for element in extract_path_elements(svg_content) { + let attributes = parse_path_attributes(&element); + let Some(path_data) = attribute_value(&attributes, "d") else { continue }; + let Ok(bezpath) = BezPath::from_svg(path_data) else { continue }; + + let fill = parse_fill_attribute(attribute_value(&attributes, "fill")); + let transform = parse_transform_attribute(attribute_value(&attributes, "transform")); + + for subpath in split_bezpath_subpaths(&bezpath) { + if subpath.elements().is_empty() { + continue; + } + + let mut vector = Vector::default(); + vector.append_bezpath(subpath); + vector.style.set_fill(fill.clone()); + + let mut table_row = TableRow::new_from_element(vector); + table_row.transform = transform; + rows.push(table_row); + } + } + + rows +} + +fn split_bezpath_subpaths(bezpath: &BezPath) -> Vec { + let mut subpaths = Vec::new(); + let mut current = BezPath::new(); + + for element in bezpath.elements() { + match element { + PathEl::MoveTo(_) => { + if !current.is_empty() { + subpaths.push(std::mem::take(&mut current)); + } + current.push(*element); + } + PathEl::ClosePath => { + current.close_path(); + subpaths.push(std::mem::take(&mut current)); + } + _ => current.push(*element), + } + } + + if !current.is_empty() { + subpaths.push(current); + } + + subpaths +} + +pub fn extract_path_elements(svg_content: &str) -> Vec { + let mut elements = Vec::new(); + let mut search_start = 0; + + while let Some(relative_start) = svg_content[search_start..].find(" { + in_quotes = true; + quote_char = ch; + } + c if c == quote_char && in_quotes => { + in_quotes = false; + } + '>' if !in_quotes => { + elements.push(svg_content[start..=index].to_string()); + search_start = index + 1; + break; + } + _ => {} + } + + index += 1; + } + + if index >= svg_content.len() { + break; + } + } + + elements +} + +pub fn is_attribute_name_char(ch: char) -> bool { + ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | ':' | '.') +} + +pub fn parse_path_attributes(element: &str) -> Vec<(String, String)> { + let mut attributes = Vec::new(); + let mut body = element.trim(); + + if let Some(start) = body.find("') { + body = &body[..end]; + } + + body = body.trim(); + if let Some(stripped) = body.strip_suffix('/') { + body = stripped.trim_end(); + } + + let bytes = body.as_bytes(); + let mut index = 0; + + while index < bytes.len() { + while index < bytes.len() && bytes[index].is_ascii_whitespace() { + index += 1; + } + + if index >= bytes.len() || bytes[index] == b'/' { + break; + } + + let name_start = index; + while index < bytes.len() && is_attribute_name_char(body.as_bytes()[index] as char) { + index += 1; + } + + if name_start == index { + index += 1; + continue; + } + + let name = body[name_start..index].trim(); + + while index < bytes.len() && bytes[index].is_ascii_whitespace() { + index += 1; + } + + if index >= bytes.len() || bytes[index] != b'=' { + while index < bytes.len() && !bytes[index].is_ascii_whitespace() && bytes[index] != b'/' { + index += 1; + } + continue; + } + + index += 1; + while index < bytes.len() && bytes[index].is_ascii_whitespace() { + index += 1; + } + + if index >= bytes.len() { + break; + } + + let mut value = String::new(); + + if bytes[index] == b'"' || bytes[index] == b'\'' { + let quote = bytes[index]; + index += 1; + let value_start = index; + + while index < bytes.len() && bytes[index] != quote { + index += 1; + } + + value.push_str(body[value_start..index.min(bytes.len())].trim()); + if index < bytes.len() { + index += 1; + } + } else { + let value_start = index; + while index < bytes.len() && !bytes[index].is_ascii_whitespace() && bytes[index] != b'/' { + index += 1; + } + value.push_str(body[value_start..index].trim()); + } + + attributes.push((name.to_string(), value)); + } + + attributes +} + +pub fn attribute_value<'a>(attributes: &'a [(String, String)], name: &str) -> Option<&'a str> { + attributes.iter().find_map(|(key, value)| (key == name).then_some(value.as_str())) +} + +pub fn parse_fill_attribute(value: Option<&str>) -> Fill { + let Some(value) = value.map(str::trim) else { + return Fill::None; + }; + + if value.eq_ignore_ascii_case("none") { + return Fill::None; + } + + if let Some(hex) = value.strip_prefix('#') { + if let Some(color) = Color::from_rgb_str(hex) { + return Fill::Solid(color); + } + } else if let Some(color) = Color::from_rgb_str(value) { + return Fill::Solid(color); + } + + Fill::None +} + +pub fn transform_from_function(name: &str, arguments: &[f64]) -> DAffine2 { + match name { + "translate" => match arguments.len() { + 0 => DAffine2::IDENTITY, + 1 => DAffine2::from_translation(DVec2::new(arguments[0], 0.)), + _ => DAffine2::from_translation(DVec2::new(arguments[0], arguments[1])), + }, + "scale" => match arguments.len() { + 0 => DAffine2::IDENTITY, + 1 => DAffine2::from_scale(DVec2::splat(arguments[0])), + _ => DAffine2::from_scale(DVec2::new(arguments[0], arguments[1])), + }, + "rotate" => { + if arguments.is_empty() { + return DAffine2::IDENTITY; + } + + let angle = arguments[0].to_radians(); + if arguments.len() >= 3 { + let center = DVec2::new(arguments[1], arguments[2]); + DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(-center) + } else { + DAffine2::from_angle(angle) + } + } + "matrix" if arguments.len() == 6 => DAffine2::from_cols_array(&[arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]]), + _ => DAffine2::IDENTITY, + } +} + +pub fn parse_transform_attribute(value: Option<&str>) -> DAffine2 { + let Some(value) = value.map(str::trim) else { + return DAffine2::IDENTITY; + }; + let mut transform = DAffine2::IDENTITY; + let mut remaining = value; + + while let Some(open_paren) = remaining.find('(') { + let (name_part, after_name) = remaining.split_at(open_paren); + let name = name_part.trim(); + let after_name = &after_name[1..]; + + if let Some(close_paren) = after_name.find(')') { + let arguments = after_name[..close_paren] + .split(|c| matches!(c, ',' | ' ' | '\t')) + .filter_map(|token| { + let trimmed = token.trim(); + if trimmed.is_empty() { None } else { trimmed.parse::().ok() } + }) + .collect::>(); + + transform = transform * transform_from_function(name, &arguments); + remaining = &after_name[close_paren + 1..]; + } else { + break; + } + } + + transform +} + +pub fn color_mode_from_u32(value: u32) -> ColorMode { + match value { + 0 => ColorMode::Color, + 1 => ColorMode::Binary, + _ => ColorMode::Color, + } +} + +pub fn hierarchical_from_u32(value: u32) -> Hierarchical { + match value { + 0 => Hierarchical::Stacked, + 1 => Hierarchical::Cutout, + _ => Hierarchical::Stacked, + } +} + +pub fn simplify_mode_from_u32(value: u32) -> PathSimplifyMode { + match value { + 0 => PathSimplifyMode::None, + 1 => PathSimplifyMode::Polygon, + 2 => PathSimplifyMode::Spline, + _ => PathSimplifyMode::None, + } +} + +pub fn build_vectorize_config( + color_mode: u32, + hierarchical: u32, + path_simplify_mode: u32, + filter_speckle: u32, + color_precision: u32, + layer_difference: u32, + corner_threshold: f32, + length_threshold: f64, + max_iterations: u32, + splice_threshold: f32, + path_precision: u32, +) -> Config { + Config { + color_mode: color_mode_from_u32(color_mode), + hierarchical: hierarchical_from_u32(hierarchical), + mode: simplify_mode_from_u32(path_simplify_mode), + filter_speckle: filter_speckle as usize, + color_precision: color_precision as i32, + layer_difference: layer_difference as i32, + 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), + } +} + +pub fn vector_row_to_graphic_row(mut vector_row: TableRow, source_row: &TableRowRef<'_, Raster>) -> TableRow { + vector_row.transform = *source_row.transform * vector_row.transform; + vector_row.alpha_blending = source_row.alpha_blending.clone(); + vector_row.source_node_id = source_row.source_node_id.clone(); + + TableRow { + element: Graphic::Vector(Table::new_from_row(vector_row)), + transform: DAffine2::IDENTITY, + alpha_blending: Default::default(), + source_node_id: Default::default(), + } +} diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 291fc72c21..1c4a5a1236 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -74,6 +74,8 @@ pub enum ArcType { PieSlice, } +// Add enum here (widget Dropdown vs Radio) + #[repr(C)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] #[widget(Radio)] diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index ef649f0d12..f3ca6dd833 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -12,7 +12,9 @@ use crate::table::{Table, TableRow, TableRowMut}; use crate::transform::{Footprint, ReferencePoint, Transform}; use crate::vector::PointDomain; use crate::vector::algorithms::bezpath_algorithms::eval_pathseg_euclidean; +use crate::vector::algorithms::decimation::ramer_douglas_peucker; use crate::vector::algorithms::merge_by_distance::MergeByDistanceExt; +use crate::vector::algorithms::vectorize::{build_vectorize_config, parse_svg_paths_to_vector, vector_row_to_graphic_row}; use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType, is_linear}; use crate::vector::misc::{handles_to_segment, segment_to_handles}; use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; @@ -202,6 +204,177 @@ where content } +#[node_macro::node(category("Raster: Filter"), path(graphene_core::vector))] +async fn vectorize( + _: impl Ctx, + image: Table>, + color_mode: u32, + hierarchical: u32, + path_simplify_mode: u32, + #[default(4)] + #[hard_min(1.)] + filter_speckle: u32, + #[default(6)] + #[hard_min(1.)] + #[hard_max(8.)] + color_precision: u32, + #[default(16)] + #[hard_min(0.)] + #[hard_max(255.)] + layer_difference: u32, + #[default(60)] + #[hard_min(0.)] + #[hard_max(180.)] + corner_threshold: f32, + #[default(4.)] + #[hard_min(3.5)] + length_threshold: f64, + #[default(10)] + #[hard_min(1.)] + max_iterations: u32, + #[default(45)] + #[hard_min(0.)] + #[hard_max(180.)] + splice_threshold: f32, + #[default(8)] + #[hard_min(0.)] + path_precision: u32, +) -> Table { + let mut result_table = Table::new(); + + for row in image.iter() { + let raster = &row.element; + + if raster.width == 0 || raster.height == 0 { + continue; + } + + let (pixel_data, width, height) = raster.to_flat_u8(); + let color_image = vtracer::ColorImage { + pixels: pixel_data, + width: width as usize, + height: height as usize, + }; + + let config = build_vectorize_config( + color_mode, + hierarchical, + path_simplify_mode, + filter_speckle, + color_precision, + layer_difference, + corner_threshold, + length_threshold, + max_iterations, + splice_threshold, + path_precision, + ); + + match vtracer::convert(color_image, config) { + Ok(svg_file) => { + let svg_content = svg_file.to_string(); + log::trace!("Generated SVG content:\n{}", svg_content); + + let vector_rows = parse_svg_paths_to_vector(&svg_content); + + let mut graphic_table = Table::new(); + for vector_row in vector_rows { + graphic_table.push(vector_row_to_graphic_row(vector_row, &row)); + } + + let graphic_group = Graphic::Graphic(graphic_table); + let mut graphic_row = TableRow::new_from_element(graphic_group); + graphic_row.transform = *row.transform; + graphic_row.alpha_blending = *row.alpha_blending; + graphic_row.source_node_id = *row.source_node_id; + result_table.push(graphic_row); + } + Err(e) => { + log::error!("Vectorization failed: {}", e); + } + } + } + + result_table +} + +/// Simplifies vector paths using the Ramer-Douglas-Peucker algorithm for line decimation. +/// Reduces the number of points in a path while preserving its overall shape within a specified tolerance. +#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] +fn decimate( + _: impl Ctx, + source: Table, + #[default(0.1)] + #[hard_min(0.001)] + tolerance: f64, +) -> Table { + use crate::vector::misc::{dvec2_to_point, point_to_dvec2}; + use kurbo::Affine; + + source + .into_iter() + .map(|row| { + let transform = Affine::new(row.transform.to_cols_array()); + let vector = row.element; + + let mut new_vector = Vector { + style: vector.style.clone(), + upstream_nested_layers: vector.upstream_nested_layers.clone(), + ..Default::default() + }; + + // Process each bezpath in the vector + for mut bezpath in vector.stroke_bezpath_iter() { + // Apply transform to work in world space for accurate distance calculations + bezpath.apply_affine(transform); + + // Extract anchor points from the bezpath + let anchors: Vec = bezpath.segments().map(|seg| point_to_dvec2(seg.start())).collect(); + + // Handle the last point if the path isn't closed + let is_closed = bezpath.elements().last() == Some(&kurbo::PathEl::ClosePath); + let mut all_anchors = anchors; + if !is_closed { + if let Some(last_seg) = bezpath.segments().last() { + all_anchors.push(point_to_dvec2(last_seg.end())); + } + } + + if all_anchors.len() <= 2 { + // Can't simplify paths with 2 or fewer points + bezpath.apply_affine(transform.inverse()); + new_vector.append_bezpath(bezpath); + continue; + } + + // Apply RDP algorithm + let simplified_indices = ramer_douglas_peucker(&all_anchors, tolerance); + + // Build new bezpath from simplified points + let mut simplified_bezpath = kurbo::BezPath::new(); + if let Some(&first_idx) = simplified_indices.first() { + simplified_bezpath.move_to(dvec2_to_point(all_anchors[first_idx])); + + for &idx in simplified_indices.iter().skip(1) { + simplified_bezpath.line_to(dvec2_to_point(all_anchors[idx])); + } + + // Close the path if the original was closed and we have enough points + if is_closed && simplified_indices.len() > 2 { + simplified_bezpath.close_path(); + } + } + + // Transform back to local space + simplified_bezpath.apply_affine(transform.inverse()); + new_vector.append_bezpath(simplified_bezpath); + } + + TableRow { element: new_vector, ..row } + }) + .collect() +} + #[node_macro::node(category("Instancing"), path(graphene_core::vector))] async fn repeat( _: impl Ctx, diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 18e9533f4a..864d868dfc 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -240,6 +240,7 @@ tagged_value! { SelectiveColorChoice(graphene_raster_nodes::adjustments::SelectiveColorChoice), GridType(graphene_core::vector::misc::GridType), ArcType(graphene_core::vector::misc::ArcType), + // Add here MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm), PointSpacingType(graphene_core::vector::misc::PointSpacingType), SpiralType(graphene_core::vector::misc::SpiralType), diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 5a285f5298..8fac00df0a 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -132,6 +132,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::GridType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::ArcType]), + // Add here async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::MergeByDistanceAlgorithm]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::PointSpacingType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::FillType]), @@ -217,6 +218,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::raster::SelectiveColorChoice]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::GridType]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::ArcType]), + // add here async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::MergeByDistanceAlgorithm]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::PointSpacingType]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_core::vector::style::StrokeCap]),