Initial working versions of decimate and vectorize

This commit is contained in:
Oliver Davies
2025-10-05 15:23:18 -07:00
parent 074a33f58f
commit eb618004c6
11 changed files with 907 additions and 36 deletions

323
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"))'] }

View File

@@ -215,6 +215,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
// Add here
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),

View File

@@ -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 }

View File

@@ -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<usize> {
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<usize>) {
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
}

View File

@@ -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;

View File

@@ -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<TableRow<Vector>> {
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<BezPath> {
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<String> {
let mut elements = Vec::new();
let mut search_start = 0;
while let Some(relative_start) = svg_content[search_start..].find("<path") {
let start = search_start + relative_start;
let mut index = start + "<path".len();
let mut in_quotes = false;
let mut quote_char = '\0';
while index < svg_content.len() {
let ch = svg_content.as_bytes()[index] as char;
match ch {
'"' | '\'' if !in_quotes => {
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("<path") {
body = &body[start + "<path".len()..];
} else {
return attributes;
}
if let Some(end) = body.rfind('>') {
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::<f64>().ok() }
})
.collect::<Vec<_>>();
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<Vector>, source_row: &TableRowRef<'_, Raster<CPU>>) -> TableRow<Graphic> {
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(),
}
}

View File

@@ -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)]

View File

@@ -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<Raster<CPU>>,
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<Graphic> {
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<Vector>,
#[default(0.1)]
#[hard_min(0.001)]
tolerance: f64,
) -> Table<Vector> {
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<DVec2> = 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<I: 'n + Send + Clone>(
_: impl Ctx,

View File

@@ -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),

View File

@@ -132,6 +132,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, 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<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, 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]),