diff --git a/Cargo.lock b/Cargo.lock index 706ba1cfb6..8a829eacb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,12 +76,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-activity" version = "0.5.2" @@ -181,9 +175,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "arboard" @@ -233,9 +227,9 @@ dependencies = [ [[package]] name = "async-broadcast" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener 5.3.1", "event-listener-strategy", @@ -345,7 +339,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -380,7 +374,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -432,7 +426,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "itoa 1.0.14", "matchit", @@ -653,9 +647,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "serde", @@ -669,22 +663,22 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -776,9 +770,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.3" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "jobserver", "libc", @@ -1006,7 +1000,7 @@ dependencies = [ "gpu-executor", "graph-craft", "graphene-core", - "reqwest 0.12.9", + "reqwest 0.12.10", "serde_json", "wgpu-executor", ] @@ -1169,18 +1163,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1197,9 +1191,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -1241,7 +1235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -1251,7 +1245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -1281,7 +1275,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -1292,7 +1286,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -1330,7 +1324,7 @@ checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -1343,7 +1337,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -1391,7 +1385,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -1446,7 +1440,7 @@ dependencies = [ "dyn-any-derive", "glam", "log", - "reqwest 0.12.9", + "reqwest 0.12.10", ] [[package]] @@ -1456,7 +1450,7 @@ dependencies = [ "dyn-any", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -1518,14 +1512,14 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -1533,9 +1527,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", @@ -1710,6 +1704,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "font-types" version = "0.8.2" @@ -1769,7 +1769,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -1883,7 +1883,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -2278,13 +2278,13 @@ dependencies = [ [[package]] name = "gpu-descriptor" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" +checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" dependencies = [ "bitflags 2.6.0", "gpu-descriptor-types", - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -2331,7 +2331,7 @@ dependencies = [ "js-sys", "log", "num-traits", - "reqwest 0.12.9", + "reqwest 0.12.10", "rustc-hash 2.1.0", "serde", "serde_json", @@ -2430,7 +2430,7 @@ dependencies = [ "path-bool", "rand 0.8.5", "rand_chacha 0.3.1", - "reqwest 0.12.9", + "reqwest 0.12.10", "resvg", "rustc-hash 2.1.0", "serde", @@ -2494,7 +2494,7 @@ dependencies = [ "serde_json", "specta", "spin", - "thiserror 2.0.6", + "thiserror 2.0.9", "tokio", "usvg", "wasm-bindgen", @@ -2511,7 +2511,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -2656,21 +2656,14 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "heck" @@ -2807,9 +2800,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.31" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -2831,9 +2824,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -2852,13 +2845,13 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.2.0", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "rustls", "rustls-pki-types", @@ -2875,7 +2868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.31", + "hyper 0.14.32", "native-tls", "tokio", "tokio-native-tls", @@ -2889,7 +2882,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "native-tls", "tokio", @@ -2908,7 +2901,7 @@ dependencies = [ "futures-util", "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.1", + "hyper 1.5.2", "pin-project-lite", "socket2", "tokio", @@ -2938,7 +2931,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -3109,7 +3102,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -3502,9 +3495,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -3530,7 +3523,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", ] [[package]] @@ -3730,9 +3723,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", "simd-adler32", @@ -3751,9 +3744,9 @@ dependencies = [ [[package]] name = "naga" -version = "23.0.0" +version = "23.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5941e45a15b53aad4375eedf02033adb7a28931eedc31117faffa52e6a857e" +checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f" dependencies = [ "arrayvec", "bit-set", @@ -3909,7 +3902,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -3974,7 +3967,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -4035,7 +4028,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -4203,9 +4196,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -4255,7 +4248,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -4297,9 +4290,9 @@ dependencies = [ [[package]] name = "os_info" -version = "3.9.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ca711d8b83edbb00b44d504503cd247c9c0bd8b0fa2694f2a1a3d8165379ce" +checksum = "eb6651f4be5e39563c4fe5cc8326349eb99a25d805a3493f791d5bfd0269e430" dependencies = [ "log", "serde", @@ -4380,7 +4373,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", "smallvec", "windows-targets 0.52.6", ] @@ -4434,7 +4427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.6", + "thiserror 2.0.9", "ucd-trie", ] @@ -4458,7 +4451,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -4586,7 +4579,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -4694,9 +4687,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.15" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67582bd5b65bdff614270e2ea89a1cf15bef71245cc1e5f7ea126977144211d" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -4824,7 +4817,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -4903,7 +4896,7 @@ dependencies = [ "rustc-hash 2.1.0", "rustls", "socket2", - "thiserror 2.0.6", + "thiserror 2.0.9", "tokio", "tracing", ] @@ -4922,7 +4915,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.6", + "thiserror 2.0.9", "tinyvec", "tracing", "web-time 1.1.0", @@ -4930,9 +4923,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd4b1eff68bf27940dd39811292c49e007f4d0b4c357358dc9b0197be6b527" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" dependencies = [ "cfg_aliases 0.2.1", "libc", @@ -4944,9 +4937,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -5078,9 +5071,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea267d65fcb21e813c5824611103d18a9d84754a718e3603f60740dce0e3515" +checksum = "be4b40e5383c077b9eb19c80c64d47d1369479a136aeae9a23c3ea43e970407b" dependencies = [ "bytemuck", "font-types", @@ -5097,9 +5090,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -5179,7 +5172,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.31", + "hyper 0.14.32", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -5209,9 +5202,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "3d3536321cfc54baa8cf3e273d5e1f63f889067829c4b410fcdbac8ca7b80994" dependencies = [ "base64 0.22.1", "bytes", @@ -5223,7 +5216,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", @@ -5247,6 +5240,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", + "tower", "tower-service", "url", "wasm-bindgen", @@ -5381,9 +5375,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.19" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "ring", @@ -5413,9 +5407,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" dependencies = [ "web-time 1.1.0", ] @@ -5433,9 +5427,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rustybuzz" @@ -5537,9 +5531,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" dependencies = [ "core-foundation-sys", "libc", @@ -5567,18 +5561,18 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -5596,20 +5590,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "indexmap 2.7.0", "itoa 1.0.14", @@ -5636,7 +5630,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -5662,9 +5656,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ "base64 0.22.1", "chrono", @@ -5680,14 +5674,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -5709,7 +5703,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -5807,9 +5801,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skrifa" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79589c6a7f90a81270b6d4ced5c5e8dc77d3064e920adb35315ed79e82fbbe61" +checksum = "d3a16eb047396452019439e1d6eca13581c141275b7d743f0262f79d65c09c70" dependencies = [ "bytemuck", "read-fonts", @@ -5932,7 +5926,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -5956,7 +5950,7 @@ dependencies = [ [[package]] name = "spirv-std" version = "0.9.0" -source = "git+https://github.com/Rust-GPU/rust-gpu.git#1932353935338c0ac5b7b150fae0551ddeaa1dad" +source = "git+https://github.com/Rust-GPU/rust-gpu.git#bfa63c15a921357b38aa78986670c15f36df76dc" dependencies = [ "bitflags 1.3.2", "glam", @@ -5968,18 +5962,18 @@ dependencies = [ [[package]] name = "spirv-std-macros" version = "0.9.0" -source = "git+https://github.com/Rust-GPU/rust-gpu.git#1932353935338c0ac5b7b150fae0551ddeaa1dad" +source = "git+https://github.com/Rust-GPU/rust-gpu.git#bfa63c15a921357b38aa78986670c15f36df76dc" dependencies = [ "proc-macro2", "quote", "spirv-std-types", - "syn 1.0.109", + "syn 2.0.91", ] [[package]] name = "spirv-std-types" version = "0.9.0" -source = "git+https://github.com/Rust-GPU/rust-gpu.git#1932353935338c0ac5b7b150fae0551ddeaa1dad" +source = "git+https://github.com/Rust-GPU/rust-gpu.git#bfa63c15a921357b38aa78986670c15f36df76dc" [[package]] name = "stable_deref_trait" @@ -6084,9 +6078,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" dependencies = [ "proc-macro2", "quote", @@ -6116,7 +6110,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -6251,7 +6245,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -6534,11 +6528,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.9", ] [[package]] @@ -6549,18 +6543,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -6663,9 +6657,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -6702,7 +6696,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -6808,14 +6802,14 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", @@ -6870,7 +6864,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -6974,9 +6968,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" @@ -7146,7 +7140,7 @@ dependencies = [ "png", "skrifa", "static_assertions", - "thiserror 2.0.6", + "thiserror 2.0.9", "vello_encoding", "vello_shaders", "wgpu", @@ -7171,7 +7165,7 @@ source = "git+https://github.com/linebender/vello.git?rev=3275ec8#3275ec85d83118 dependencies = [ "bytemuck", "naga", - "thiserror 2.0.6", + "thiserror 2.0.9", "vello_encoding", ] @@ -7265,7 +7259,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", "wasm-bindgen-shared", ] @@ -7300,7 +7294,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7845,7 +7839,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -7856,7 +7850,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -7867,7 +7861,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -7878,7 +7872,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -8506,7 +8500,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", "synstructure", ] @@ -8557,7 +8551,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", "zvariant_utils", ] @@ -8590,7 +8584,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -8610,7 +8604,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", "synstructure", ] @@ -8639,7 +8633,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] [[package]] @@ -8659,9 +8653,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] @@ -8688,7 +8682,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", "zvariant_utils", ] @@ -8700,5 +8694,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.91", ] diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index f5482cdeb1..6629eb02aa 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -155,6 +155,14 @@ pub enum FrontendMessage { UpdateGraphViewOverlay { open: bool, }, + UpdateImportReorderIndex { + #[serde(rename = "importIndex")] + index: Option, + }, + UpdateExportReorderIndex { + #[serde(rename = "exportIndex")] + index: Option, + }, UpdateLayerWidths { #[serde(rename = "layerWidths")] layer_widths: HashMap, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 3258ecafe5..8f667e24ff 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -194,7 +194,7 @@ impl MessageHandler> for DocumentMessag } DocumentMessage::PropertiesPanel(message) => { let properties_panel_message_handler_data = PropertiesPanelMessageHandlerData { - network_interface: &self.network_interface, + network_interface: &mut self.network_interface, selection_network_path: &self.selection_network_path, document_name: self.name.as_str(), executor, @@ -391,6 +391,7 @@ impl MessageHandler> for DocumentMessag self.selection_network_path.clone_from(&self.breadcrumb_network_path); responses.add(NodeGraphMessage::SendGraph); responses.add(DocumentMessage::ZoomCanvasToFitAll); + responses.add(NodeGraphMessage::SetGridAlignedEdges); } DocumentMessage::Escape => { if self.node_graph_handler.drag_start.is_some() { @@ -1007,7 +1008,7 @@ impl MessageHandler> for DocumentMessag } responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateLayerPanel); - responses.add(NodeGraphMessage::UpdateInSelectedNetwork) + responses.add(NodeGraphMessage::UpdateInSelectedNetwork); } DocumentMessage::SetBlendModeForSelectedLayers { blend_mode } => { for layer in self.network_interface.selected_nodes(&[]).unwrap().selected_layers_except_artboards(&self.network_interface) { @@ -1018,6 +1019,7 @@ impl MessageHandler> for DocumentMessag self.graph_fade_artwork_percentage = percentage; responses.add(FrontendMessage::UpdateGraphFadeArtwork { percentage }); } + DocumentMessage::SetNodePinned { node_id, pinned } => { responses.add(DocumentMessage::StartTransaction); responses.add(NodeGraphMessage::SetPinned { node_id, pinned }); @@ -1224,21 +1226,13 @@ impl MessageHandler> for DocumentMessag .navigation_handler .calculate_offset_transform(ipp.viewport_bounds.center(), &network_metadata.persistent_metadata.navigation_metadata.node_graph_ptz); self.network_interface.set_transform(transform, &self.breadcrumb_network_path); - let imports = self.network_interface.frontend_imports(&self.breadcrumb_network_path).unwrap_or_default(); - let exports = self.network_interface.frontend_exports(&self.breadcrumb_network_path).unwrap_or_default(); - let add_import = self.network_interface.frontend_import_modify(&self.breadcrumb_network_path); - let add_export = self.network_interface.frontend_export_modify(&self.breadcrumb_network_path); responses.add(DocumentMessage::RenderRulers); responses.add(DocumentMessage::RenderScrollbars); responses.add(NodeGraphMessage::UpdateEdges); responses.add(NodeGraphMessage::UpdateBoxSelection); - responses.add(FrontendMessage::UpdateImportsExports { - imports, - exports, - add_import, - add_export, - }); + responses.add(NodeGraphMessage::UpdateImportsExports); + responses.add(FrontendMessage::UpdateNodeGraphTransform { transform: Transform { scale: transform.matrix2.x_axis.x, @@ -2081,7 +2075,7 @@ impl DocumentMessageHandler { /// Create a network interface with a single export fn default_document_network_interface() -> NodeNetworkInterface { let mut network_interface = NodeNetworkInterface::default(); - network_interface.add_export(TaggedValue::ArtboardGroup(graphene_core::ArtboardGroup::EMPTY), -1, "".to_string(), &[]); + network_interface.add_export(TaggedValue::ArtboardGroup(graphene_core::ArtboardGroup::EMPTY), -1, "", &[]); network_interface } 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 789adec155..d12acc3169 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -254,7 +254,11 @@ impl<'a> ModifyInputsContext<'a> { let mut existing_node_id = None; for upstream_node in upstream.collect::>() { // Check if this is the node we have been searching for. - if self.network_interface.reference(&upstream_node, &[]).is_some_and(|node_reference| node_reference == reference) { + if self + .network_interface + .reference(&upstream_node, &[]) + .is_some_and(|node_reference| *node_reference == Some(reference.to_string())) + { existing_node_id = Some(upstream_node); break; } 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 a5d8ef5c86..4aefe6bfc8 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 @@ -1,9 +1,9 @@ use super::node_properties; use super::utility_types::FrontendNodeType; use crate::messages::layout::utility_types::widget_prelude::*; -use crate::messages::portfolio::document::node_graph::node_properties::node_no_properties; use crate::messages::portfolio::document::utility_types::network_interface::{ - DocumentNodeMetadata, DocumentNodePersistentMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata, + DocumentNodeMetadata, DocumentNodePersistentMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata, NumberInputSettings, + PropertiesRow, Vec2InputSettings, WidgetOverride, }; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::Message; @@ -33,11 +33,44 @@ pub struct NodePropertiesContext<'a> { pub persistent_data: &'a PersistentData, pub responses: &'a mut VecDeque, pub executor: &'a mut NodeGraphExecutor, - pub network_interface: &'a NodeNetworkInterface, + pub network_interface: &'a mut NodeNetworkInterface, pub selection_network_path: &'a [NodeId], pub document_name: &'a str, } +impl NodePropertiesContext<'_> { + pub fn call_widget_override(&mut self, node_id: &NodeId, index: usize) -> Option> { + let Some(input_properties_row) = self.network_interface.input_properties_row(node_id, index, self.selection_network_path) else { + log::error!("Could not get input properties row in call_widget_override"); + return None; + }; + if let Some(widget_override) = &input_properties_row.widget_override { + let Some(widget_override_lambda) = INPUT_OVERRIDES.get(widget_override) else { + log::error!("Could not get widget override lambda in call_widget_override"); + return None; + }; + widget_override_lambda(*node_id, index, self) + .map(|layout_group| { + let Some(input_properties_row) = self.network_interface.input_properties_row(node_id, index, self.selection_network_path) else { + log::error!("Could not get input properties row in call_widget_override"); + return Vec::new(); + }; + if let Some(tooltip) = &input_properties_row.tooltip { + layout_group.into_iter().map(|widget| widget.with_tooltip(tooltip)).collect::>() + } else { + layout_group + } + }) + .map_err(|error| { + log::error!("Error in widget override lambda: {}", error); + }) + .ok() + } else { + None + } + } +} + /// Acts as a description for a [DocumentNode] before it gets instantiated as one. #[derive(Clone)] pub struct DocumentNodeDefinition { @@ -49,10 +82,14 @@ pub struct DocumentNodeDefinition { /// Definition specific data. In order for the editor to access this data, the reference will be used. pub category: &'static str, - pub properties: &'static (dyn Fn(&DocumentNode, NodeId, &mut NodePropertiesContext) -> Vec + Sync), /// User-facing description of the node's functionality. pub description: Cow<'static, str>, + + /// Node level overrides are stored based on the reference, not the instance. If the node is modified such that it becomes a local version + /// (for example an input is added), the reference is no longer to the definition, and the overrides are lost. + /// Most nodes should not use node based properties, since they are less flexible than input level properties. + pub properties: Option<&'static (dyn Fn(NodeId, &mut NodePropertiesContext) -> Vec + Sync)>, } // We use the once cell for lazy initialization to avoid the overhead of reconstructing the node list every time. @@ -64,7 +101,6 @@ static DOCUMENT_NODE_TYPES: once_cell::sync::Lazy> = /// The [`DocumentNode`] is the instance while these [`DocumentNodeDefinition`]s are the "classes" or "blueprints" from which the instances are built. fn static_nodes() -> Vec { let mut custom = vec![ - // TODO: Auto-generate this from its proto node macro DocumentNodeDefinition { identifier: "Default Network", category: "General", @@ -79,7 +115,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("A default node network you can use to create your own custom nodes."), - properties: &node_properties::node_no_properties, + properties: None, }, // TODO: Auto-generate this from its proto node macro DocumentNodeDefinition { @@ -92,13 +128,17 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string()], + input_properties: vec![PropertiesRow::with_override( + "In", + WidgetOverride::String("The identity node simply passes its data through.".to_string()), + )], output_names: vec!["Out".to_string()], ..Default::default() }, }, + description: Cow::Borrowed("The identity node passes its data through. You can use this to organize your node graph."), - properties: &|_document_node, _node_id, _context| node_properties::string_properties("The identity node simply passes its data through"), + properties: None, }, // TODO: Auto-generate this from its proto node macro DocumentNodeDefinition { @@ -113,13 +153,16 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string()], + input_properties: vec![PropertiesRow::with_override( + "In", + WidgetOverride::String("The Monitor node is used by the editor to access the data flowing through it.".to_string()), + )], output_names: vec!["Out".to_string()], ..Default::default() }, }, description: Cow::Borrowed("The Monitor node is used by the editor to access the data flowing through it."), - properties: &|_document_node, _node_id, _context| node_properties::string_properties("The Monitor node is used by the editor to access the data flowing through it"), + properties: None, }, DocumentNodeDefinition { identifier: "Merge", @@ -175,7 +218,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Graphical Data".to_string(), "Over".to_string()], + input_properties: vec!["Graphical Data".into(), "Over".into()], output_names: vec!["Out".to_string()], node_type_metadata: NodeTypePersistentMetadata::layer(IVec2::new(0, 0)), network_metadata: Some(NodeNetworkMetadata { @@ -226,7 +269,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("The Merge node combines graphical data through composition."), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Artboard", @@ -287,13 +330,29 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec![ - "Artboards".to_string(), - "Contents".to_string(), - "Location".to_string(), - "Dimensions".to_string(), - "Background".to_string(), - "Clip".to_string(), + input_properties: vec![ + PropertiesRow::with_override("Artboards", WidgetOverride::Hidden), + PropertiesRow::with_override("Contents", WidgetOverride::Hidden), + PropertiesRow::with_override( + "Location", + WidgetOverride::Vec2(Vec2InputSettings { + x: "X".to_string(), + y: "Y".to_string(), + unit: " px".to_string(), + ..Default::default() + }), + ), + PropertiesRow::with_override( + "Dimensions", + WidgetOverride::Vec2(Vec2InputSettings { + x: "W".to_string(), + y: "H".to_string(), + unit: " px".to_string(), + ..Default::default() + }), + ), + PropertiesRow::with_override("Background", WidgetOverride::Custom("artboard_background".to_string())), + "Clip".into(), ], output_names: vec!["Out".to_string()], node_type_metadata: NodeTypePersistentMetadata::layer(IVec2::new(0, 0)), @@ -337,7 +396,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("Creates a new Artboard which can be used as a working surface."), - properties: &node_properties::artboard_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Load Image", @@ -376,7 +435,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["api".to_string(), "path".to_string()], + input_properties: vec![PropertiesRow::with_override("api", WidgetOverride::Hidden), "URL".into()], output_names: vec!["Image Frame".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -418,7 +477,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("Loads an image from a given url."), - properties: &node_properties::load_image_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Create Canvas", @@ -483,7 +542,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("Creates a new canvas object."), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Draw Canvas", @@ -526,7 +585,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string()], + input_properties: vec!["In".into()], output_names: vec!["Canvas".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -576,7 +635,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("Draws raster data to a canvas element."), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Rasterize", @@ -626,7 +685,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Artwork".to_string(), "Footprint".to_string()], + input_properties: vec![PropertiesRow::with_override("Artwork", WidgetOverride::Hidden), "Footprint".into()], output_names: vec!["Canvas".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -668,7 +727,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("Rasterizes the given vector data"), - properties: &node_properties::rasterize_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Image Frame", @@ -703,7 +762,10 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string(), "Transform".to_string()], + input_properties: vec![PropertiesRow::with_override( + "Image", + WidgetOverride::String("Creates an embedded image with the given transform.".to_string()), + )], output_names: vec!["Image".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -737,7 +799,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("Creates an embedded image with the given transform."), - properties: &|_document_node, _node_id, _context| node_properties::string_properties("Creates an embedded image with the given transform"), + properties: None, }, DocumentNodeDefinition { identifier: "Noise Pattern", @@ -766,22 +828,22 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec![ - "Clip".to_string(), - "Seed".to_string(), - "Scale".to_string(), - "Noise Type".to_string(), - "Domain Warp Type".to_string(), - "Domain Warp Amplitude".to_string(), - "Fractal Type".to_string(), - "Fractal Octaves".to_string(), - "Fractal Lacunarity".to_string(), - "Fractal Gain".to_string(), - "Fractal Weighted Strength".to_string(), - "Fractal Ping Pong Strength".to_string(), - "Cellular Distance Function".to_string(), - "Cellular Return Type".to_string(), - "Cellular Jitter".to_string(), + input_properties: vec![ + "Clip".into(), + "Seed".into(), + PropertiesRow::with_override("Scale", WidgetOverride::Custom("noise_properties_scale".to_string())), + PropertiesRow::with_override("Noise Type", WidgetOverride::Custom("noise_properties_noise_type".to_string())), + PropertiesRow::with_override("Domain Warp Type", WidgetOverride::Custom("noise_properties_domain_warp_type".to_string())), + PropertiesRow::with_override("Domain Warp Amplitude", WidgetOverride::Custom("noise_properties_domain_warp_amplitude".to_string())), + PropertiesRow::with_override("Fractal Type", WidgetOverride::Custom("noise_properties_fractal_type".to_string())), + PropertiesRow::with_override("Fractal Octaves", WidgetOverride::Custom("noise_properties_fractal_octaves".to_string())), + PropertiesRow::with_override("Fractal Lacunarity", WidgetOverride::Custom("noise_properties_fractal_lacunarity".to_string())), + PropertiesRow::with_override("Fractal Gain", WidgetOverride::Custom("noise_properties_fractal_gain".to_string())), + PropertiesRow::with_override("Fractal Weighted Strength", WidgetOverride::Custom("noise_properties_fractal_weighted_strength".to_string())), + PropertiesRow::with_override("Fractal Ping Pong Strength", WidgetOverride::Custom("noise_properties_ping_pong_strength".to_string())), + PropertiesRow::with_override("Cellular Distance Function", WidgetOverride::Custom("noise_properties_cellular_distance_function".to_string())), + PropertiesRow::with_override("Cellular Return Type", WidgetOverride::Custom("noise_properties_cellular_return_type".to_string())), + PropertiesRow::with_override("Cellular Jitter", WidgetOverride::Custom("noise_properties_cellular_jitter".to_string())), ], output_names: vec!["Image".to_string()], network_metadata: Some(NodeNetworkMetadata { @@ -816,7 +878,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("Generates different noise patterns."), - properties: &node_properties::noise_pattern_properties, + properties: None, }, // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. // TODO: Auto-generate this from its proto node macro @@ -833,13 +895,16 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string(), "Stencil".to_string()], + input_properties: vec![ + PropertiesRow::with_override("Image", WidgetOverride::Hidden), + PropertiesRow::with_override("Stencil", WidgetOverride::Custom("mask_stencil".to_string())), + ], output_names: vec!["Image".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::mask_properties, + properties: None, }, // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. // TODO: Auto-generate this from its proto node macro @@ -857,13 +922,17 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string(), "Insertion".to_string(), "Replace".to_string()], + input_properties: vec![ + PropertiesRow::with_override("Image", WidgetOverride::Hidden), + PropertiesRow::with_override("Insertion", WidgetOverride::Hidden), + "Into".into(), + ], output_names: vec!["Image".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::insert_channel_properties, + properties: None, }, // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. DocumentNodeDefinition { @@ -882,13 +951,13 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["None".to_string(), "Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()], + input_properties: vec!["None".into(), "Red".into(), "Green".into(), "Blue".into(), "Alpha".into()], output_names: vec!["Image".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Split Channels", @@ -951,7 +1020,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string()], + input_properties: vec!["Image".into()], output_names: vec!["Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()], has_primary_output: false, network_metadata: Some(NodeNetworkMetadata { @@ -1002,7 +1071,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Brush", @@ -1037,7 +1106,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Background".to_string(), "Bounds".to_string(), "Trace".to_string(), "Cache".to_string()], + input_properties: vec!["Background".into(), "Bounds".into(), "Trace".into(), "Cache".into()], output_names: vec!["Image".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1071,7 +1140,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Memoize", @@ -1084,13 +1153,13 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string()], + input_properties: vec!["Image".into()], output_names: vec!["Image".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Memoize Impure", @@ -1103,13 +1172,13 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string()], + input_properties: vec!["Image".into()], output_names: vec!["Image".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Image", @@ -1134,7 +1203,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string()], + input_properties: vec![PropertiesRow::with_override("Image", WidgetOverride::String("A bitmap image is embedded in this node".to_string()))], output_names: vec!["Image".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1158,7 +1227,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &|_document_node, _node_id, _context| node_properties::string_properties("A bitmap image embedded in this node"), + properties: None, }, #[cfg(feature = "gpu")] DocumentNodeDefinition { @@ -1197,7 +1266,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string()], + input_properties: vec!["In".into()], output_names: vec!["Uniform".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1239,7 +1308,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Storage", @@ -1276,7 +1345,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string()], + input_properties: vec!["In".into()], output_names: vec!["Storage".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1318,7 +1387,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Create Output Buffer", @@ -1355,7 +1424,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string(), "In".to_string()], + input_properties: vec!["In".into(), "In".into()], output_names: vec!["Output Buffer".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1397,7 +1466,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, #[cfg(feature = "gpu")] DocumentNodeDefinition { @@ -1444,7 +1513,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string(), "In".to_string(), "In".to_string()], + input_properties: vec!["In".into(), "In".into(), "In".into()], output_names: vec!["Command Buffer".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1486,7 +1555,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, #[cfg(feature = "gpu")] DocumentNodeDefinition { @@ -1504,13 +1573,14 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Shader Handle".to_string(), "String".to_string(), "Bindgroup".to_string(), "Arc Shader Input".to_string()], + input_properties: vec!["Shader Handle".into(), "String".into(), "Bindgroup".into(), "Arc Shader Input".into()], output_names: vec!["Pipeline Layout".to_string()], ..Default::default() }, }, - properties: &node_properties::node_no_properties, + description: Cow::Borrowed("TODO"), + properties: None, }, #[cfg(feature = "gpu")] DocumentNodeDefinition { @@ -1548,7 +1618,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string()], + input_properties: vec!["In".into()], output_names: vec!["Pipeline Result".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1590,7 +1660,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, #[cfg(feature = "gpu")] DocumentNodeDefinition { @@ -1628,7 +1698,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string()], + input_properties: vec!["In".into()], output_names: vec!["Buffer".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1670,7 +1740,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, #[cfg(feature = "gpu")] DocumentNodeDefinition { @@ -1736,7 +1806,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, #[cfg(feature = "gpu")] DocumentNodeDefinition { @@ -1773,7 +1843,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Texture".to_string(), "Surface".to_string()], + input_properties: vec!["Texture".into(), "Surface".into()], output_names: vec!["Rendered Texture".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1807,7 +1877,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, #[cfg(feature = "gpu")] DocumentNodeDefinition { @@ -1846,7 +1916,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["In".to_string()], + input_properties: vec!["In".into()], output_names: vec!["Texture".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -1888,7 +1958,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, #[cfg(feature = "gpu")] DocumentNodeDefinition { @@ -1904,13 +1974,13 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string(), "Node".to_string()], + input_properties: vec!["Image".into(), "Node".into()], output_names: vec!["Image".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Extract", @@ -1922,13 +1992,13 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Node".to_string()], + input_properties: vec!["Node".into()], output_names: vec!["Document Node".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { // Aims for interoperable compatibility with: @@ -1948,13 +2018,18 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string(), "Brightness".to_string(), "Contrast".to_string(), "Use Classic".to_string()], + input_properties: vec![ + PropertiesRow::with_override("Image", WidgetOverride::Hidden), + PropertiesRow::with_override("Brightness", WidgetOverride::Custom("brightness".to_string())), + PropertiesRow::with_override("Brightness", WidgetOverride::Custom("contrast".to_string())), + "Use Classic".into(), + ], output_names: vec!["Image".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::brightness_contrast_properties, + properties: None, }, // Aims for interoperable compatibility with: // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=levl%27%20%3D%20Levels-,%27curv%27%20%3D%20Curves,-%27expA%27%20%3D%20Exposure @@ -1972,13 +2047,13 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string(), "Curve".to_string()], + input_properties: vec![PropertiesRow::with_override("Image", WidgetOverride::Hidden), "Curve".into()], output_names: vec!["Image".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::curves_properties, + properties: None, }, (*IMAGINATE_NODE).clone(), DocumentNodeDefinition { @@ -1996,13 +2071,33 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["None".to_string(), "Start".to_string(), "End".to_string()], + input_properties: vec![ + PropertiesRow::with_override("None", WidgetOverride::Hidden), + PropertiesRow::with_override( + "Start", + WidgetOverride::Vec2(Vec2InputSettings { + x: "X".to_string(), + y: "Y".to_string(), + unit: " px".to_string(), + ..Default::default() + }), + ), + PropertiesRow::with_override( + "End", + WidgetOverride::Vec2(Vec2InputSettings { + x: "X".to_string(), + y: "Y".to_string(), + unit: " px".to_string(), + ..Default::default() + }), + ), + ], output_names: vec!["Vector".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::line_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Spline", @@ -2019,13 +2114,16 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["None".to_string(), "Points".to_string()], + input_properties: vec![ + PropertiesRow::with_override("None", WidgetOverride::Hidden), + PropertiesRow::with_override("Points", WidgetOverride::Custom("spline_input".to_string())), + ], output_names: vec!["Vector".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::spline_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Path", @@ -2062,7 +2160,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Vector Data".to_string(), "Modification".to_string()], + input_properties: vec!["Vector Data".into(), "Modification".into()], output_names: vec!["Vector Data".to_string()], network_metadata: Some(NodeNetworkMetadata { persistent_metadata: NodeNetworkPersistentMetadata { @@ -2096,7 +2194,7 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::node_no_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Text", @@ -2119,20 +2217,41 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec![ - "Editor API".to_string(), - "Text".to_string(), - "Font".to_string(), - "Size".to_string(), - "Line Height".to_string(), - "Character Spacing".to_string(), + input_properties: vec![ + PropertiesRow::with_override("Editor API", WidgetOverride::Hidden), + PropertiesRow::with_override("Text", WidgetOverride::Custom("text_area".to_string())), + PropertiesRow::with_override("Font", WidgetOverride::Custom("text_font".to_string())), + PropertiesRow::with_override( + "Size", + WidgetOverride::Number(NumberInputSettings { + unit: Some(" px".to_string()), + min: Some(1.), + ..Default::default() + }), + ), + PropertiesRow::with_override( + "Line Height", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + step: Some(0.1), + ..Default::default() + }), + ), + PropertiesRow::with_override( + "Character Spacing", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + step: Some(0.1), + ..Default::default() + }), + ), ], output_names: vec!["Vector".to_string()], ..Default::default() }, }, description: Cow::Borrowed("TODO"), - properties: &node_properties::text_properties, + properties: None, }, DocumentNodeDefinition { identifier: "Transform", @@ -2206,20 +2325,37 @@ fn static_nodes() -> Vec { }, ..Default::default() }), - input_names: vec![ - "Vector Data".to_string(), - "Translation".to_string(), - "Rotation".to_string(), - "Scale".to_string(), - "Skew".to_string(), - "Pivot".to_string(), + input_properties: vec![ + PropertiesRow::with_override("Vector Data", WidgetOverride::Hidden), + PropertiesRow::with_override( + "Translation", + WidgetOverride::Vec2(Vec2InputSettings { + x: "X".to_string(), + y: "Y".to_string(), + unit: " px".to_string(), + ..Default::default() + }), + ), + PropertiesRow::with_override("Rotation", WidgetOverride::Custom("transform_rotation".to_string())), + PropertiesRow::with_override( + "Scale", + WidgetOverride::Vec2(Vec2InputSettings { + x: "W".to_string(), + y: "H".to_string(), + unit: "x".to_string(), + ..Default::default() + }), + ), + PropertiesRow::with_override("Skew", WidgetOverride::Hidden), + PropertiesRow::with_override("Pivot", WidgetOverride::Hidden), ], output_names: vec!["Data".to_string()], ..Default::default() }, }, - properties: &node_properties::transform_properties, + description: Cow::Borrowed("TODO"), + properties: None, }, DocumentNodeDefinition { identifier: "Boolean Operation", @@ -2283,13 +2419,14 @@ fn static_nodes() -> Vec { }, ..Default::default() }), - input_names: vec!["Group of Paths".to_string(), "Operation".to_string()], + input_properties: vec![PropertiesRow::with_override("Group of Paths", WidgetOverride::Hidden), "Operation".into()], output_names: vec!["Vector".to_string()], ..Default::default() }, }, - properties: &node_properties::boolean_operation_properties, + description: Cow::Borrowed("TODO"), + properties: None, }, DocumentNodeDefinition { identifier: "Copy to Points", @@ -2312,22 +2449,81 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec![ - "Points".to_string(), - "Instance".to_string(), - "Random Scale Min".to_string(), - "Random Scale Max".to_string(), - "Random Scale Bias".to_string(), - "Random Scale Seed".to_string(), - "Random Rotation".to_string(), - "Random Rotation Seed".to_string(), + input_properties: vec![ + PropertiesRow::with_override("Points", WidgetOverride::Hidden), + Into::::into("Instance").with_tooltip("Artwork to be copied and placed at each point"), + PropertiesRow::with_override( + "Random Scale Min", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + mode: NumberInputMode::Range, + range_min: Some(0.), + range_max: Some(2.), + unit: Some("x".to_string()), + ..Default::default() + }), + ) + .with_tooltip("Minimum range of randomized sizes given to each instance"), + PropertiesRow::with_override( + "Random Scale Max", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + mode: NumberInputMode::Range, + range_min: Some(0.), + range_max: Some(2.), + unit: Some("x".to_string()), + ..Default::default() + }), + ) + .with_tooltip("Minimum range of randomized sizes given to each instance"), + PropertiesRow::with_override( + "Random Scale Bias", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + mode: NumberInputMode::Range, + range_min: Some(-50.), + range_max: Some(50.), + ..Default::default() + }), + ) + .with_tooltip("Bias for the probability distribution of randomized sizes (0 is uniform, negatives favor more of small sizes, positives favor more of large sizes)"), + PropertiesRow::with_override( + "Random Scale Seed", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + is_integer: true, + ..Default::default() + }), + ) + .with_tooltip("Seed to determine unique variations on all the randomized instance sizes"), + PropertiesRow::with_override( + "Random Rotation", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + max: Some(360.), + mode: NumberInputMode::Range, + unit: Some("°".to_string()), + ..Default::default() + }), + ) + .with_tooltip("Range of randomized angles given to each instance, in degrees ranging from furthest clockwise to counterclockwise"), + PropertiesRow::with_override( + "Random Rotation Seed", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + is_integer: true, + ..Default::default() + }), + ) + .with_tooltip("Seed to determine unique variations on all the randomized instance angles"), ], output_names: vec!["Vector".to_string()], ..Default::default() }, }, - properties: &node_properties::copy_to_points_properties, + description: Cow::Borrowed("TODO"), + properties: None, }, DocumentNodeDefinition { identifier: "Sample Points", @@ -2412,19 +2608,44 @@ fn static_nodes() -> Vec { }, ..Default::default() }), - input_names: vec![ - "Vector Data".to_string(), - "Spacing".to_string(), - "Start Offset".to_string(), - "Stop Offset".to_string(), - "Adaptive Spacing".to_string(), + input_properties: vec![ + PropertiesRow::with_override("Vector Data", WidgetOverride::Hidden), + PropertiesRow::with_override( + "Spacing", + WidgetOverride::Number(NumberInputSettings { + min: Some(1.), + unit: Some(" px".to_string()), + ..Default::default() + }), + ) + .with_tooltip("Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled)"), + PropertiesRow::with_override( + "Start Offset", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + unit: Some(" px".to_string()), + ..Default::default() + }), + ) + .with_tooltip("Exclude some distance from the start of the path before the first instance"), + PropertiesRow::with_override( + "Stop Offset", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + unit: Some(" px".to_string()), + ..Default::default() + }), + ) + .with_tooltip("Exclude some distance from the end of the path after the last instance"), + Into::::into("Adaptive Spacing").with_tooltip("Round 'Spacing' to a nearby value that divides into the path length evenly"), ], output_names: vec!["Vector".to_string()], ..Default::default() }, }, - properties: &node_properties::sample_points_properties, + description: Cow::Borrowed("TODO"), + properties: None, }, DocumentNodeDefinition { identifier: "Scatter Points", @@ -2491,13 +2712,34 @@ fn static_nodes() -> Vec { }, ..Default::default() }), - input_names: vec!["Vector Data".to_string(), "Separation Disk Diameter".to_string(), "Seed".to_string()], + input_properties: vec![ + PropertiesRow::with_override("Vector Data", WidgetOverride::Hidden), + PropertiesRow::with_override( + "Separation Disk Diameter", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.01), + mode: NumberInputMode::Range, + range_min: Some(1.), + range_max: Some(100.), + ..Default::default() + }), + ), + PropertiesRow::with_override( + "Seed", + WidgetOverride::Number(NumberInputSettings { + min: Some(0.), + is_integer: true, + ..Default::default() + }), + ), + ], output_names: vec!["Vector".to_string()], ..Default::default() }, }, - properties: &node_properties::poisson_disk_points_properties, + description: Cow::Borrowed("TODO"), + properties: None, }, // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. DocumentNodeDefinition { @@ -2510,34 +2752,20 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Segmentation".to_string(), "Index".to_string()], + input_properties: vec![ + PropertiesRow::with_override("Segmentation", WidgetOverride::Hidden), + PropertiesRow::with_override("Index", WidgetOverride::Number(NumberInputSettings { min: Some(0.), ..Default::default() })), + ], output_names: vec!["Image".to_string()], ..Default::default() }, }, - properties: &node_properties::index_properties, + description: Cow::Borrowed("TODO"), + properties: None, }, ]; - type PropertiesLayout = &'static (dyn Fn(&DocumentNode, NodeId, &mut NodePropertiesContext) -> Vec + Sync); - let properties_overrides = [ - ("graphene_core::raster::adjustments::ChannelMixerNode", &node_properties::channel_mixer_properties as PropertiesLayout), - ("graphene_core::vector::FillNode", &node_properties::fill_properties as PropertiesLayout), - ("graphene_core::vector::StrokeNode", &node_properties::stroke_properties as PropertiesLayout), - ("graphene_core::vector::OffsetPathNode", &node_properties::offset_path_properties as PropertiesLayout), - ( - "graphene_core::raster::adjustments::SelectiveColorNode", - &node_properties::selective_color_properties as PropertiesLayout, - ), - ("graphene_core::ops::MathNode", &node_properties::math_properties as PropertiesLayout), - ("graphene_core::raster::ExposureNode", &node_properties::exposure_properties as PropertiesLayout), - ("graphene_core::vector::generator_nodes::RectangleNode", &node_properties::rectangle_properties as PropertiesLayout), - ("graphene_core::vector::AssignColorsNode", &node_properties::assign_colors_properties as PropertiesLayout), - ] - .into_iter() - .collect::>(); - // Remove struct generics for DocumentNodeDefinition { node_template, .. } in custom.iter_mut() { let NodeTemplate { @@ -2573,6 +2801,7 @@ fn static_nodes() -> Vec { category, fields, description, + properties, } = metadata; let Some(implementations) = &node_registry.get(&id) else { continue }; let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect(); @@ -2591,9 +2820,9 @@ fn static_nodes() -> Vec { let exposed = if index == 0 { *ty != fn_type!(()) } else { field.exposed }; match field.value_source { - ValueSource::None => {} - ValueSource::Default(data) => return NodeInput::value(TaggedValue::from_primitive_string(data, ty).unwrap_or(TaggedValue::None), exposed), - ValueSource::Scope(data) => return NodeInput::scope(Cow::Borrowed(data)), + RegistryValueSource::None => {} + RegistryValueSource::Default(data) => return NodeInput::value(TaggedValue::from_primitive_string(data, ty).unwrap_or(TaggedValue::None), exposed), + RegistryValueSource::Scope(data) => return NodeInput::scope(Cow::Borrowed(data)), }; if let Some(type_default) = TaggedValue::from_type(ty) { @@ -2603,33 +2832,6 @@ fn static_nodes() -> Vec { }) .collect(); - let properties = match properties_overrides.get(id.as_str()) { - Some(properties_function) => *properties_function, - None => { - let field_types: Vec<_> = fields.iter().zip(first_node_io.inputs.iter()).map(|(field, ty)| (field.clone(), ty.clone())).collect(); - let properties = move |document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext| { - let rows: Vec<_> = field_types - .iter() - .enumerate() - .skip(1) - .filter(|(_, (field, _))| !matches!(&field.value_source, ValueSource::Scope(_))) - .flat_map(|(index, (field, ty))| { - let number_options = (field.number_min, field.number_max, field.number_mode_range); - - node_properties::property_from_type(document_node, node_id, index, field.name, ty, context, number_options) - }) - .collect(); - - if rows.is_empty() { - return node_no_properties(document_node, node_id, context); - } - - rows - }; - Box::leak(Box::new(properties)) as PropertiesLayout - } - }; - let node = DocumentNodeDefinition { identifier: display_name, node_template: NodeTemplate { @@ -2642,7 +2844,16 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: fields.iter().map(|f| f.name.to_string()).collect(), + // TODO: Store information for input overrides in the node macro + input_properties: fields + .iter() + .map(|f| match f.widget_override { + RegistryWidgetOverride::None => f.name.into(), + RegistryWidgetOverride::Hidden => PropertiesRow::with_override(f.name, WidgetOverride::Hidden), + RegistryWidgetOverride::String(str) => PropertiesRow::with_override(f.name, WidgetOverride::String(str.to_string())), + RegistryWidgetOverride::Custom(str) => PropertiesRow::with_override(f.name, WidgetOverride::Custom(str.to_string())), + }) + .collect(), output_names: vec![output_type.to_string()], has_primary_output: true, locked: false, @@ -2651,7 +2862,7 @@ fn static_nodes() -> Vec { }, category: category.unwrap_or("UNCATEGORIZED"), description: Cow::Borrowed(description), - properties, + properties: properties.map(|properties| name_to_properties(properties)), }; custom.push(node); } @@ -2753,33 +2964,519 @@ pub static IMAGINATE_NODE: Lazy = Lazy::new(|| DocumentN }, ..Default::default() }), - input_names: vec![ - "Input Image".to_string(), - "Editor Api".to_string(), - "Controller".to_string(), - "Seed".to_string(), - "Resolution".to_string(), - "Samples".to_string(), - "Sampling Method".to_string(), - "Prompt Guidance".to_string(), - "Prompt".to_string(), - "Negative Prompt".to_string(), - "Adapt Input Image".to_string(), - "Image Creativity".to_string(), - "Inpaint".to_string(), - "Mask Blur".to_string(), - "Mask Starting Fill".to_string(), - "Improve Faces".to_string(), - "Tiling".to_string(), + input_properties: vec![ + "Input Image".into(), + "Editor Api".into(), + "Controller".into(), + "Seed".into(), + "Resolution".into(), + "Samples".into(), + "Sampling Method".into(), + "Prompt Guidance".into(), + "Prompt".into(), + "Negative Prompt".into(), + "Adapt Input Image".into(), + "Image Creativity".into(), + "Inpaint".into(), + "Mask Blur".into(), + "Mask Starting Fill".into(), + "Improve Faces".into(), + "Tiling".into(), ], output_names: vec!["Image".to_string()], ..Default::default() }, }, - properties: &node_properties::imaginate_properties, + description: Cow::Borrowed("TODO"), + properties: Some(&node_properties::imaginate_properties), }); +fn name_to_properties(properties_string: &str) -> &'static (dyn Fn(NodeId, &mut NodePropertiesContext) -> Vec + Sync) { + match properties_string { + "channel_mixer_properties" => &node_properties::channel_mixer_properties, + "fill_properties" => &node_properties::fill_properties, + "stroke_properties" => &node_properties::stroke_properties, + "offset_path_properties" => &node_properties::offset_path_properties, + "selective_color_properties" => &node_properties::selective_color_properties, + "exposure_properties" => &node_properties::exposure_properties, + "math_properties" => &node_properties::math_properties, + "rectangle_properties" => &node_properties::rectangle_properties, + _ => { + log::error!("Node properties not found for {properties_string}"); + &node_properties::node_no_properties + } + } +} + +static INPUT_OVERRIDES: once_cell::sync::Lazy Result, String> + Send + Sync>>> = + once_cell::sync::Lazy::new(static_input_properties); + +/// Defines the logic for inputs to display a custom properties panel widget. +fn static_input_properties() -> HashMap Result, String> + Send + Sync>> { + let mut map: HashMap Result, String> + Send + Sync>> = HashMap::new(); + map.insert("hidden".to_string(), Box::new(|_node_id, _index, _context| Ok(Vec::new()))); + map.insert( + "string".to_string(), + Box::new(|node_id, index, context| { + let Some(value) = context.network_interface.input_metadata(&node_id, index, "string_properties", context.selection_network_path) else { + return Err(format!("Could not get string properties for node {}", node_id)); + }; + let Some(string) = value.as_str() else { + return Err(format!("Could not downcast string properties for node {}", node_id)); + }; + Ok(node_properties::string_properties(string.to_string())) + }), + ); + map.insert( + "number".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let mut number_input = NumberInput::default(); + if let Some(unit) = context + .network_interface + .input_metadata(&node_id, index, "unit", context.selection_network_path) + .and_then(|value| value.as_str()) + { + number_input = number_input.unit(unit); + } + if let Some(min) = context + .network_interface + .input_metadata(&node_id, index, "min", context.selection_network_path) + .and_then(|value| value.as_f64()) + { + number_input = number_input.min(min); + } + if let Some(max) = context + .network_interface + .input_metadata(&node_id, index, "max", context.selection_network_path) + .and_then(|value| value.as_f64()) + { + number_input = number_input.max(max); + } + if let Some(step) = context + .network_interface + .input_metadata(&node_id, index, "step", context.selection_network_path) + .and_then(|value| value.as_f64()) + { + number_input = number_input.step(step); + } + if let Some(mode) = context.network_interface.input_metadata(&node_id, index, "mode", context.selection_network_path).map(|value| { + let mode: NumberInputMode = serde_json::from_value(value.clone()).unwrap(); + mode + }) { + number_input = number_input.mode(mode); + } + let blank_assist = context + .network_interface + .input_metadata(&node_id, index, "blank_assist", context.selection_network_path) + .and_then(|value| value.as_bool()) + .unwrap_or_else(|| { + log::error!("Could not get blank assist when displaying number input for node {node_id}, index {index}"); + true + }); + Ok(vec![LayoutGroup::Row { + widgets: node_properties::number_widget(document_node, node_id, index, input_name, number_input, blank_assist), + }]) + }), + ); + map.insert( + "vec2".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let x = context + .network_interface + .input_metadata(&node_id, index, "x", context.selection_network_path) + .and_then(|value| value.as_str()) + .unwrap_or_else(|| { + log::error!("Could not get x for vec2 input"); + "" + }); + let y = context + .network_interface + .input_metadata(&node_id, index, "y", context.selection_network_path) + .and_then(|value| value.as_str()) + .unwrap_or_else(|| { + log::error!("Could not get y for vec2 input"); + "" + }); + let unit = context + .network_interface + .input_metadata(&node_id, index, "unit", context.selection_network_path) + .and_then(|value| value.as_str()) + .unwrap_or_else(|| { + log::error!("Could not get unit for vec2 input"); + "" + }); + let min = context + .network_interface + .input_metadata(&node_id, index, "min", context.selection_network_path) + .and_then(|value| value.as_f64()); + + Ok(vec![node_properties::vec2_widget( + document_node, + node_id, + index, + input_name, + x, + y, + unit, + min, + node_properties::add_blank_assist, + )]) + }), + ); + map.insert( + "noise_properties_scale".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (_, coherent_noise_active, _, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; + let scale = node_properties::number_widget(document_node, node_id, index, input_name, NumberInput::default().min(0.).disabled(!coherent_noise_active), true); + Ok(vec![scale.into()]) + }), + ); + map.insert( + "noise_properties_noise_type".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let noise_type_row = node_properties::noise_type(document_node, node_id, index, input_name, true); + Ok(vec![noise_type_row, LayoutGroup::Row { widgets: Vec::new() }]) + }), + ); + map.insert( + "noise_properties_domain_warp_type".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (_, coherent_noise_active, _, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; + let domain_warp_type = node_properties::domain_warp_type(document_node, node_id, index, input_name, true, !coherent_noise_active); + Ok(vec![domain_warp_type.into()]) + }), + ); + map.insert( + "noise_properties_domain_warp_amplitude".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (_, coherent_noise_active, _, _, domain_warp_active, _) = node_properties::query_noise_pattern_state(node_id, context)?; + let domain_warp_amplitude = node_properties::number_widget( + document_node, + node_id, + index, + input_name, + NumberInput::default().min(0.).disabled(!coherent_noise_active || !domain_warp_active), + true, + ); + Ok(vec![domain_warp_amplitude.into(), LayoutGroup::Row { widgets: Vec::new() }]) + }), + ); + map.insert( + "noise_properties_fractal_type".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (_, coherent_noise_active, _, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; + let fractal_type_row = node_properties::fractal_type(document_node, node_id, index, input_name, true, !coherent_noise_active); + Ok(vec![fractal_type_row.into()]) + }), + ); + map.insert( + "noise_properties_fractal_octaves".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (fractal_active, coherent_noise_active, _, _, _, domain_warp_only_fractal_type_wrongly_active) = node_properties::query_noise_pattern_state(node_id, context)?; + let fractal_octaves = node_properties::number_widget( + document_node, + node_id, + index, + input_name, + NumberInput::default() + .mode_range() + .min(1.) + .max(10.) + .range_max(Some(4.)) + .is_integer(true) + .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + Ok(vec![fractal_octaves.into()]) + }), + ); + map.insert( + "noise_properties_fractal_lacunarity".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (fractal_active, coherent_noise_active, _, _, _, domain_warp_only_fractal_type_wrongly_active) = node_properties::query_noise_pattern_state(node_id, context)?; + let fractal_lacunarity = node_properties::number_widget( + document_node, + node_id, + index, + input_name, + NumberInput::default() + .mode_range() + .min(0.) + .range_max(Some(10.)) + .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + Ok(vec![fractal_lacunarity.into()]) + }), + ); + map.insert( + "noise_properties_fractal_gain".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (fractal_active, coherent_noise_active, _, _, _, domain_warp_only_fractal_type_wrongly_active) = node_properties::query_noise_pattern_state(node_id, context)?; + let fractal_gain = node_properties::number_widget( + document_node, + node_id, + index, + input_name, + NumberInput::default() + .mode_range() + .min(0.) + .range_max(Some(10.)) + .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + Ok(vec![fractal_gain.into()]) + }), + ); + map.insert( + "noise_properties_fractal_weighted_strength".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (fractal_active, coherent_noise_active, _, _, _, domain_warp_only_fractal_type_wrongly_active) = node_properties::query_noise_pattern_state(node_id, context)?; + let fractal_weighted_strength = node_properties::number_widget( + document_node, + node_id, + index, + input_name, + NumberInput::default() + .mode_range() + .min(0.) + .max(1.) // Defined for the 0-1 range + .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + Ok(vec![fractal_weighted_strength.into()]) + }), + ); + map.insert( + "noise_properties_ping_pong_strength".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (fractal_active, coherent_noise_active, _, ping_pong_active, _, domain_warp_only_fractal_type_wrongly_active) = node_properties::query_noise_pattern_state(node_id, context)?; + let fractal_ping_pong_strength = node_properties::number_widget( + document_node, + node_id, + index, + input_name, + NumberInput::default() + .mode_range() + .min(0.) + .range_max(Some(10.)) + .disabled(!ping_pong_active || !coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + Ok(vec![fractal_ping_pong_strength.into(), LayoutGroup::Row { widgets: Vec::new() }]) + }), + ); + map.insert( + "noise_properties_cellular_distance_function".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (_, coherent_noise_active, cellular_noise_active, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; + let cellular_distance_function_row = node_properties::cellular_distance_function(document_node, node_id, index, input_name, true, !coherent_noise_active || !cellular_noise_active); + Ok(vec![cellular_distance_function_row.into()]) + }), + ); + map.insert( + "noise_properties_cellular_return_type".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (_, coherent_noise_active, cellular_noise_active, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; + let cellular_return_type = node_properties::cellular_return_type(document_node, node_id, index, input_name, true, !coherent_noise_active || !cellular_noise_active); + Ok(vec![cellular_return_type.into()]) + }), + ); + map.insert( + "noise_properties_cellular_jitter".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (_, coherent_noise_active, cellular_noise_active, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; + let cellular_jitter = node_properties::number_widget( + document_node, + node_id, + index, + input_name, + NumberInput::default() + .mode_range() + .range_min(Some(0.)) + .range_max(Some(1.)) + .disabled(!coherent_noise_active || !cellular_noise_active), + true, + ); + Ok(vec![cellular_jitter.into()]) + }), + ); + map.insert( + "brightness".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let is_use_classic = document_node + .inputs + .iter() + .find_map(|input| match input.as_value() { + Some(&TaggedValue::Bool(use_classic)) => Some(use_classic), + _ => None, + }) + .unwrap_or(false); + let (b_min, b_max) = if is_use_classic { (-100., 100.) } else { (-100., 150.) }; + let brightness = node_properties::number_widget( + document_node, + node_id, + index, + input_name, + NumberInput::default().mode_range().range_min(Some(b_min)).range_max(Some(b_max)).unit("%").display_decimal_places(2), + true, + ); + Ok(vec![brightness.into()]) + }), + ); + map.insert( + "contrast".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let is_use_classic = document_node + .inputs + .iter() + .find_map(|input| match input.as_value() { + Some(&TaggedValue::Bool(use_classic)) => Some(use_classic), + _ => None, + }) + .unwrap_or(false); + let (c_min, c_max) = if is_use_classic { (-100., 100.) } else { (-50., 100.) }; + let contrast = node_properties::number_widget( + document_node, + node_id, + index, + input_name, + NumberInput::default().mode_range().range_min(Some(c_min)).range_max(Some(c_max)).unit("%").display_decimal_places(2), + true, + ); + Ok(vec![contrast.into()]) + }), + ); + map.insert( + "assign_colors_gradient".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let gradient_row = node_properties::color_widget(document_node, node_id, index, input_name, ColorButton::default().allow_none(false), true); + Ok(vec![gradient_row]) + }), + ); + map.insert( + "assign_colors_seed".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let randomize_enabled = node_properties::query_assign_colors_randomize(node_id, context)?; + let seed_row = node_properties::number_widget(document_node, node_id, index, input_name, NumberInput::default().min(0.).int().disabled(!randomize_enabled), true); + Ok(vec![seed_row.into()]) + }), + ); + map.insert( + "assign_colors_repeat_every_row".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let randomize_enabled = node_properties::query_assign_colors_randomize(node_id, context)?; + let repeat_every_row = node_properties::number_widget(document_node, node_id, index, input_name, NumberInput::default().min(0.).int().disabled(randomize_enabled), true); + Ok(vec![repeat_every_row.into()]) + }), + ); + map.insert( + "mask_stencil".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let mask = node_properties::color_widget(document_node, node_id, index, input_name, ColorButton::default(), true); + Ok(vec![mask]) + }), + ); + map.insert( + "spline_input".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + Ok(vec![LayoutGroup::Row { + widgets: node_properties::vec_dvec2_input(document_node, node_id, index, input_name, TextInput::default().centered(true), true), + }]) + }), + ); + map.insert( + "transform_rotation".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + + let mut widgets = node_properties::start_widgets(document_node, node_id, index, input_name, super::utility_types::FrontendGraphDataType::Number, true); + + let Some(input) = document_node.inputs.get(index) else { + return Err("Input not found in transform rotation input override".to_string()); + }; + if let Some(&TaggedValue::F64(val)) = input.as_non_exposed_value() { + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + NumberInput::new(Some(val.to_degrees())) + .unit("°") + .mode(NumberInputMode::Range) + .range_min(Some(-180.)) + .range_max(Some(180.)) + .on_update(node_properties::update_value( + |number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap().to_radians()), + node_id, + index, + )) + .on_commit(node_properties::commit_value) + .widget_holder(), + ]); + } + + Ok(vec![LayoutGroup::Row { widgets }]) + }), + ); + map.insert( + "text_area".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + Ok(vec![LayoutGroup::Row { + widgets: node_properties::text_area_widget(document_node, node_id, index, input_name, true), + }]) + }), + ); + map.insert( + "text_font".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + let (font, style) = node_properties::font_inputs(document_node, node_id, index, input_name, true); + let mut result = vec![LayoutGroup::Row { widgets: font }]; + if let Some(style) = style { + result.push(LayoutGroup::Row { widgets: style }); + } + Ok(result) + }), + ); + map.insert( + "artboard_background".to_string(), + Box::new(|node_id, index, context| { + let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?; + Ok(vec![node_properties::color_widget( + document_node, + node_id, + index, + input_name, + ColorButton::default().allow_none(false), + true, + )]) + }), + ); + map +} + pub fn resolve_document_node_type(identifier: &str) -> Option<&DocumentNodeDefinition> { DOCUMENT_NODE_TYPES.iter().find(|definition| definition.identifier == identifier) } @@ -2812,6 +3509,62 @@ impl DocumentNodeDefinition { } }); + //Ensure that the input properties are initialized for every Document Node input for every node + fn populate_input_properties(node_template: &mut NodeTemplate, mut path: Vec) { + if let Some(current_node) = path.pop() { + let DocumentNodeImplementation::Network(template_network) = &node_template.document_node.implementation else { + log::error!("Template network should always exist"); + return; + }; + let Some(nested_network) = template_network.nested_network(&path) else { + log::error!("Nested network should exist for path"); + return; + }; + let Some(input_length) = nested_network.nodes.get(¤t_node).map(|node| node.inputs.len()) else { + log::error!("Could not get current node in nested network"); + return; + }; + let Some(template_network_metadata) = &mut node_template.persistent_node_metadata.network_metadata else { + log::error!("Template should have metadata if it has network implementation"); + return; + }; + let Some(nested_network_metadata) = template_network_metadata.nested_metadata_mut(&path) else { + log::error!("Path is not valid for network"); + return; + }; + let Some(nested_node_metadata) = nested_network_metadata.persistent_metadata.node_metadata.get_mut(¤t_node) else { + log::error!("Path is not valid for network"); + return; + }; + nested_node_metadata.persistent_metadata.input_properties.resize_with(input_length, PropertiesRow::default); + + //Recurse over all sub nodes if the current node is a network implementation + let mut current_path = path.clone(); + current_path.push(current_node); + let DocumentNodeImplementation::Network(template_network) = &node_template.document_node.implementation else { + log::error!("Template network should always exist"); + return; + }; + if let Some(current_nested_network) = template_network.nested_network(¤t_path) { + for sub_node_id in current_nested_network.nodes.keys().cloned().collect::>() { + let mut sub_path = current_path.clone(); + sub_path.push(sub_node_id); + populate_input_properties(node_template, sub_path); + } + }; + } else { + // Base case + let input_len = node_template.document_node.inputs.len(); + node_template.persistent_node_metadata.input_properties.resize_with(input_len, PropertiesRow::default); + if let DocumentNodeImplementation::Network(node_template_network) = &node_template.document_node.implementation { + for sub_node_id in node_template_network.nodes.keys().cloned().collect::>() { + populate_input_properties(node_template, vec![sub_node_id]); + } + } + } + } + populate_input_properties(&mut template, Vec::new()); + // Set the reference to the node definition template.persistent_node_metadata.reference = Some(self.identifier.to_string()); template diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 80553e730e..4a62f8e3a7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -1,7 +1,7 @@ use super::utility_types::Direction; use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; +use crate::messages::portfolio::document::utility_types::network_interface::{ImportOrExport, InputConnector, NodeTemplate, OutputConnector}; use crate::messages::prelude::*; use glam::IVec2; @@ -98,6 +98,20 @@ pub enum NodeGraphMessage { shift: Key, }, PrintSelectedNodeCoordinates, + RemoveImport { + import_index: usize, + }, + RemoveExport { + export_index: usize, + }, + ReorderImport { + start_index: usize, + end_index: usize, + }, + ReorderExport { + start_index: usize, + end_index: usize, + }, RunDocumentGraph, ForceRunDocumentGraph, SelectedNodesAdd { @@ -153,6 +167,14 @@ pub enum NodeGraphMessage { TogglePreviewImpl { node_id: NodeId, }, + SetImportExportName { + name: String, + index: ImportOrExport, + }, + SetImportExportNameImpl { + name: String, + index: ImportOrExport, + }, ToggleSelectedAsLayersOrNodes, ToggleSelectedLocked, ToggleLocked { @@ -180,6 +202,7 @@ pub enum NodeGraphMessage { }, UpdateEdges, UpdateBoxSelection, + UpdateImportsExports, UpdateLayerPanel, UpdateNewNodeGraph, UpdateTypes { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 972035a750..b576348269 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -71,6 +71,12 @@ pub struct NodeGraphMessageHandler { auto_panning: AutoPanning, /// The node to preview on mouse up if alt-clicked preview_on_mouse_up: Option, + // The index of the import that is being moved + reordering_import: Option, + // The index of the export that is being moved + reordering_export: Option, + // The end index of the moved port + end_index: Option, } /// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network. @@ -99,8 +105,14 @@ impl<'a> MessageHandler> for NodeGrap responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] }); } - NodeGraphMessage::AddImport => network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, String::new(), breadcrumb_network_path), - NodeGraphMessage::AddExport => network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, String::new(), breadcrumb_network_path), + NodeGraphMessage::AddImport => { + network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, "", breadcrumb_network_path); + responses.add(NodeGraphMessage::SendGraph); + } + NodeGraphMessage::AddExport => { + network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, "", breadcrumb_network_path); + responses.add(NodeGraphMessage::SendGraph); + } NodeGraphMessage::Init => { responses.add(BroadcastMessage::SubscribeEvent { on: BroadcastEvent::SelectionChanged, @@ -298,26 +310,15 @@ impl<'a> MessageHandler> for NodeGrap { return; }; - let Some(network) = network_interface.network(selection_network_path) else { - log::error!("Could not get network in EnterNestedNetwork"); - return; - }; - - let Some(node) = network.nodes.get(&node_id) else { return }; - if let DocumentNodeImplementation::Network(_) = node.implementation { + if let Some(DocumentNodeImplementation::Network(_)) = network_interface.implementation(&node_id, selection_network_path) { responses.add(DocumentMessage::EnterNestedNetwork { node_id }); } } NodeGraphMessage::ExposeInput { node_id, input_index, new_exposed } => { - let Some(network) = network_interface.network(selection_network_path) else { - return; - }; - - let Some(node) = network.nodes.get(&node_id) else { + let Some(node) = network_interface.document_node(&node_id, selection_network_path) else { log::error!("Could not find node {node_id} in NodeGraphMessage::ExposeInput"); return; }; - let Some(mut input) = node.inputs.get(input_index).cloned() else { log::error!("Could not find input {input_index} in NodeGraphMessage::ExposeInput"); return; @@ -559,23 +560,6 @@ impl<'a> MessageHandler> for NodeGrap let node_graph_point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click); - let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else { - log::error!("Could not get modify import export in PointerDown"); - return; - }; - - if modify_import_export.add_export.intersect_point_no_stroke(node_graph_point) { - responses.add(DocumentMessage::AddTransaction); - responses.add(NodeGraphMessage::AddExport); - responses.add(NodeGraphMessage::SendGraph); - return; - } else if modify_import_export.add_import.intersect_point_no_stroke(node_graph_point) { - responses.add(DocumentMessage::AddTransaction); - responses.add(NodeGraphMessage::AddImport); - responses.add(NodeGraphMessage::SendGraph); - return; - } - if network_interface .layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Grip, selection_network_path) .is_some() @@ -651,6 +635,37 @@ impl<'a> MessageHandler> for NodeGrap return; } + let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else { + log::error!("Could not get modify import export in PointerDown"); + return; + }; + + if modify_import_export.add_import_export.clicked_input_port_from_point(node_graph_point).is_some() { + responses.add(DocumentMessage::AddTransaction); + responses.add(NodeGraphMessage::AddExport); + return; + } else if modify_import_export.add_import_export.clicked_output_port_from_point(node_graph_point).is_some() { + responses.add(DocumentMessage::AddTransaction); + responses.add(NodeGraphMessage::AddImport); + return; + } else if let Some(remove_import_index) = modify_import_export.remove_imports_exports.clicked_output_port_from_point(node_graph_point) { + responses.add(DocumentMessage::AddTransaction); + responses.add(NodeGraphMessage::RemoveImport { import_index: remove_import_index }); + return; + } else if let Some(remove_export_index) = modify_import_export.remove_imports_exports.clicked_input_port_from_point(node_graph_point) { + responses.add(DocumentMessage::AddTransaction); + responses.add(NodeGraphMessage::RemoveExport { export_index: remove_export_index }); + return; + } else if let Some(move_import_index) = modify_import_export.reorder_imports_exports.clicked_output_port_from_point(node_graph_point) { + responses.add(DocumentMessage::StartTransaction); + self.reordering_import = Some(move_import_index); + return; + } else if let Some(move_export_index) = modify_import_export.reorder_imports_exports.clicked_input_port_from_point(node_graph_point) { + responses.add(DocumentMessage::StartTransaction); + self.reordering_export = Some(move_export_index); + return; + } + self.selection_before_pointer_down = network_interface .selected_nodes(selection_network_path) .map(|selected_nodes| selected_nodes.selected_nodes().cloned().collect()) @@ -881,6 +896,46 @@ impl<'a> MessageHandler> for NodeGrap responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta, rubber_band: true }); } else if self.box_selection_start.is_some() { responses.add(NodeGraphMessage::UpdateBoxSelection); + } else if self.reordering_import.is_some() { + let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else { + log::error!("Could not get modify import export in PointerUp"); + return; + }; + // Find the first import that is below the mouse position + self.end_index = Some( + modify_import_export + .reorder_imports_exports + .output_ports() + .find_map(|(index, click_target)| { + let Some(position) = click_target.bounding_box().map(|bbox| (bbox[0].y + bbox[1].y) / 2.) else { + log::error!("Could not get bounding box for import: {index}"); + return None; + }; + (position > point.y).then_some(*index) + }) + .unwrap_or(modify_import_export.reorder_imports_exports.output_ports().count()), + ); + responses.add(FrontendMessage::UpdateImportReorderIndex { index: self.end_index }); + } else if self.reordering_export.is_some() { + let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else { + log::error!("Could not get modify import export in PointerUp"); + return; + }; + // Find the first export that is below the mouse position + self.end_index = Some( + modify_import_export + .reorder_imports_exports + .input_ports() + .find_map(|(index, click_target)| { + let Some(position) = click_target.bounding_box().map(|bbox| (bbox[0].y + bbox[1].y) / 2.) else { + log::error!("Could not get bounding box for export: {index}"); + return None; + }; + (position > point.y).then_some(*index) + }) + .unwrap_or(modify_import_export.reorder_imports_exports.input_ports().count()), + ); + responses.add(FrontendMessage::UpdateExportReorderIndex { index: self.end_index }); } } NodeGraphMessage::PointerUp => { @@ -1129,15 +1184,34 @@ impl<'a> MessageHandler> for NodeGrap } self.select_if_not_dragged = None; } - + // End of reordering an import + else if let (Some(moving_import), Some(end_index)) = (self.reordering_import, self.end_index) { + responses.add(NodeGraphMessage::ReorderImport { + start_index: moving_import, + end_index, + }); + responses.add(DocumentMessage::EndTransaction); + } + // End of reordering an export + else if let (Some(moving_export), Some(end_index)) = (self.reordering_export, self.end_index) { + responses.add(NodeGraphMessage::ReorderExport { + start_index: moving_export, + end_index, + }); + responses.add(DocumentMessage::EndTransaction); + } self.drag_start = None; self.begin_dragging = false; self.box_selection_start = None; self.wire_in_progress_from_connector = None; self.wire_in_progress_to_connector = None; + self.reordering_export = None; + self.reordering_import = None; responses.add(DocumentMessage::EndTransaction); responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); - responses.add(FrontendMessage::UpdateBox { box_selection: None }) + responses.add(FrontendMessage::UpdateBox { box_selection: None }); + responses.add(FrontendMessage::UpdateImportReorderIndex { index: None }); + responses.add(FrontendMessage::UpdateExportReorderIndex { index: None }); } NodeGraphMessage::PointerOutsideViewport { shift } => { if self.drag_start.is_some() || self.box_selection_start.is_some() { @@ -1186,6 +1260,26 @@ impl<'a> MessageHandler> for NodeGrap // } // } } + NodeGraphMessage::RemoveImport { import_index: usize } => { + network_interface.remove_import(usize, selection_network_path); + responses.add(NodeGraphMessage::SendGraph); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + NodeGraphMessage::RemoveExport { export_index: usize } => { + network_interface.remove_export(usize, selection_network_path); + responses.add(NodeGraphMessage::SendGraph); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + NodeGraphMessage::ReorderImport { start_index, end_index } => { + network_interface.reorder_import(start_index, end_index, selection_network_path); + responses.add(NodeGraphMessage::SendGraph); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + NodeGraphMessage::ReorderExport { start_index, end_index } => { + network_interface.reorder_export(start_index, end_index, selection_network_path); + responses.add(NodeGraphMessage::SendGraph); + responses.add(NodeGraphMessage::RunDocumentGraph); + } NodeGraphMessage::RunDocumentGraph => { responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false }); } @@ -1230,16 +1324,7 @@ impl<'a> MessageHandler> for NodeGrap let wires = Self::collect_wires(network_interface, breadcrumb_network_path); let nodes = self.collect_nodes(network_interface, breadcrumb_network_path); let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path); - let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default(); - let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default(); - let add_import = network_interface.frontend_import_modify(breadcrumb_network_path); - let add_export = network_interface.frontend_export_modify(breadcrumb_network_path); - responses.add(FrontendMessage::UpdateImportsExports { - imports, - exports, - add_import, - add_export, - }); + responses.add(NodeGraphMessage::UpdateImportsExports); responses.add(FrontendMessage::UpdateNodeGraph { nodes, wires }); responses.add(FrontendMessage::UpdateLayerWidths { layer_widths, @@ -1253,16 +1338,7 @@ impl<'a> MessageHandler> for NodeGrap if graph_view_overlay_open { network_interface.set_grid_aligned_edges(DVec2::new(ipp.viewport_bounds.bottom_right.x - ipp.viewport_bounds.top_left.x, 0.), breadcrumb_network_path); // Send the new edges to the frontend - let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default(); - let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default(); - let add_import = network_interface.frontend_import_modify(breadcrumb_network_path); - let add_export = network_interface.frontend_export_modify(breadcrumb_network_path); - responses.add(FrontendMessage::UpdateImportsExports { - imports, - exports, - add_import, - add_export, - }); + responses.add(NodeGraphMessage::UpdateImportsExports); } } NodeGraphMessage::SetInputValue { node_id, input_index, value } => { @@ -1272,7 +1348,10 @@ impl<'a> MessageHandler> for NodeGrap input, }); responses.add(PropertiesPanelMessage::Refresh); - if (!network_interface.reference(&node_id, selection_network_path).is_some_and(|reference| reference == "Imaginate") || input_index == 0) + if (!network_interface + .reference(&node_id, selection_network_path) + .is_some_and(|reference| *reference == Some("Imaginate".to_string())) + || input_index == 0) && network_interface.connected_to_output(&node_id, selection_network_path) { responses.add(NodeGraphMessage::RunDocumentGraph); @@ -1377,6 +1456,13 @@ impl<'a> MessageHandler> for NodeGrap NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => { network_interface.set_display_name(&node_id, alias, selection_network_path); } + NodeGraphMessage::SetImportExportName { name, index } => { + responses.add(DocumentMessage::StartTransaction); + responses.add(NodeGraphMessage::SetImportExportNameImpl { name, index }); + responses.add(DocumentMessage::EndTransaction); + responses.add(NodeGraphMessage::UpdateImportsExports); + } + NodeGraphMessage::SetImportExportNameImpl { name, index } => network_interface.set_import_export_name(name, index, breadcrumb_network_path), NodeGraphMessage::TogglePreview { node_id } => { responses.add(DocumentMessage::AddTransaction); responses.add(NodeGraphMessage::TogglePreviewImpl { node_id }); @@ -1427,13 +1513,7 @@ impl<'a> MessageHandler> for NodeGrap let node_ids = selected_nodes.selected_nodes().cloned().collect::>(); // If any of the selected nodes are pinned, unpin them all. Otherwise, pin them all. - let pinned = !node_ids.iter().all(|node_id| { - if let Some(node) = network_interface.node_metadata(node_id, breadcrumb_network_path) { - node.persistent_metadata.pinned - } else { - false - } - }); + let pinned = !node_ids.iter().all(|node_id| network_interface.is_pinned(node_id, breadcrumb_network_path)); responses.add(DocumentMessage::AddTransaction); for node_id in &node_ids { @@ -1442,9 +1522,6 @@ impl<'a> MessageHandler> for NodeGrap responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }); } NodeGraphMessage::ToggleSelectedVisibility => { - let Some(network) = network_interface.network(selection_network_path) else { - return; - }; let Some(selected_nodes) = network_interface.selected_nodes(selection_network_path) else { log::error!("Could not get selected nodes in NodeGraphMessage::ToggleSelectedLocked"); return; @@ -1452,7 +1529,7 @@ impl<'a> MessageHandler> for NodeGrap let node_ids = selected_nodes.selected_nodes().cloned().collect::>(); // If any of the selected nodes are hidden, show them all. Otherwise, hide them all. - let visible = !node_ids.iter().all(|node_id| network.nodes.get(node_id).is_some_and(|node| node.visible)); + let visible = !node_ids.iter().all(|node_id| network_interface.is_visible(node_id, selection_network_path)); responses.add(DocumentMessage::AddTransaction); for node_id in &node_ids { @@ -1461,16 +1538,7 @@ impl<'a> MessageHandler> for NodeGrap responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }); } NodeGraphMessage::ToggleVisibility { node_id } => { - let Some(network) = network_interface.network(selection_network_path) else { - return; - }; - - let Some(node) = network.nodes.get(&node_id) else { - log::error!("Cannot get node {node_id} in NodeGraphMessage::ToggleVisibility"); - return; - }; - - let visible = !node.visible; + let visible = !network_interface.is_visible(&node_id, selection_network_path); responses.add(DocumentMessage::AddTransaction); responses.add(NodeGraphMessage::SetVisibility { node_id, visible }); @@ -1552,6 +1620,39 @@ impl<'a> MessageHandler> for NodeGrap responses.add(FrontendMessage::UpdateBox { box_selection }) } } + NodeGraphMessage::UpdateImportsExports => { + let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default(); + let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default(); + let add_import = network_interface + .frontend_import_export_modify( + |modify_import_export_click_target| modify_import_export_click_target.add_import_export.output_ports().collect::>(), + breadcrumb_network_path, + ) + .into_iter() + .next(); + let add_export = network_interface + .frontend_import_export_modify( + |modify_import_export_click_target| modify_import_export_click_target.add_import_export.input_ports().collect::>(), + breadcrumb_network_path, + ) + .into_iter() + .next(); + // let remove_imports = network_interface.frontend_import_export_modify( + // |modify_import_export_click_target| modify_import_export_click_target.remove_imports_exports.output_ports().collect::>(), + // breadcrumb_network_path, + // ); + // let remove_exports = network_interface.frontend_import_export_modify( + // |modify_import_export_click_target| modify_import_export_click_target.remove_imports_exports.input_ports().collect::>(), + // breadcrumb_network_path, + // ); + responses.add(FrontendMessage::UpdateImportsExports { + imports, + exports, + add_import, + add_export, + }); + } + NodeGraphMessage::UpdateLayerPanel => { Self::update_layer_panel(network_interface, selection_network_path, collapsed, responses); } @@ -1671,14 +1772,7 @@ impl NodeGraphMessageHandler { let has_selection = selected_nodes.has_selected_nodes(); let selection_includes_layers = network_interface.selected_nodes(&[]).unwrap().selected_layers(network_interface.document_metadata()).count() > 0; let selection_all_locked = network_interface.selected_nodes(&[]).unwrap().selected_unlocked_layers(network_interface).count() == 0; - let selection_all_visible = selected_nodes.selected_nodes().all(|id| { - if let Some(node) = network.nodes.get(id) { - node.visible - } else { - error!("Could not get node {id} in update_selection_action_buttons"); - true - } - }); + let selection_all_visible = selected_nodes.selected_nodes().all(|node_id| network_interface.is_visible(node_id, breadcrumb_network_path)); let mut widgets = vec![ PopoverButton::new() @@ -1839,10 +1933,6 @@ impl NodeGraphMessageHandler { /// Collate the properties panel sections for a node graph pub fn collate_properties(context: &mut NodePropertiesContext) -> Vec { // If the selected nodes are in the document network, use the document network. Otherwise, use the nested network - let Some(network) = context.network_interface.network(context.selection_network_path) else { - warn!("No network in collate_properties"); - return Vec::new(); - }; let Some(selected_nodes) = context.network_interface.selected_nodes(context.selection_network_path) else { warn!("No selected nodes in collate_properties"); return Vec::new(); @@ -1868,25 +1958,13 @@ impl NodeGraphMessageHandler { match layers.len() { // If no layers are selected, show properties for all selected nodes 0 => { - let selected_nodes = nodes - .iter() - .filter_map(|node_id| { - network.nodes.get(node_id).map(|node| { - let pinned = if let Some(node) = context.network_interface.node_metadata(node_id, context.selection_network_path) { - node.persistent_metadata.pinned - } else { - error!("Could not get node {node_id} in collate_properties"); - false - }; - - node_properties::generate_node_properties(node, *node_id, pinned, context) - }) - }) - .collect::>(); + let selected_nodes = nodes.iter().map(|node_id| node_properties::generate_node_properties(*node_id, context)).collect::>(); if !selected_nodes.is_empty() { return selected_nodes; } + // TODO: Display properties for encapsulating node when no nodes are selected in a nested network + // This may require store a separate path for the properties panel let mut properties = vec![LayoutGroup::Row { widgets: vec![ Separator::new(SeparatorType::Related).widget_holder(), @@ -1900,23 +1978,22 @@ impl NodeGraphMessageHandler { ], }]; + let Some(network) = context.network_interface.network(context.selection_network_path) else { + warn!("No network in collate_properties"); + return Vec::new(); + }; // And if no nodes are selected, show properties for all pinned nodes let pinned_node_properties = network .nodes + .keys() + .cloned() + .collect::>() .iter() - .filter_map(|(node_id, node)| { - let pinned = if let Some(node) = context.network_interface.node_metadata(node_id, context.selection_network_path) { - node.persistent_metadata.pinned - } else { - error!("Could not get node {node_id} in collate_properties"); - false - }; - - if pinned { - Some(node_properties::generate_node_properties(node, *node_id, pinned, context)) - } else { - None - } + .filter_map(|node_id| { + context + .network_interface + .is_pinned(node_id, context.selection_network_path) + .then(|| node_properties::generate_node_properties(*node_id, context)) }) .collect::>(); @@ -1983,17 +2060,9 @@ impl NodeGraphMessageHandler { !context.network_interface.is_layer(node_id, context.selection_network_path) } }) - .filter_map(|(_, node_id)| network.nodes.get(&node_id).map(|node| (node, node_id))) - .map(|(node, node_id)| { - let pinned = if let Some(node) = context.network_interface.node_metadata(&node_id, context.selection_network_path) { - node.persistent_metadata.pinned - } else { - error!("Could not get node {node_id} in collate_properties"); - false - }; - - node_properties::generate_node_properties(node, node_id, pinned, context) - }) + .collect::>() + .iter() + .map(|(_, node_id)| node_properties::generate_node_properties(*node_id, context)) .collect::>(); layer_properties.extend(node_properties); @@ -2112,6 +2181,7 @@ impl NodeGraphMessageHandler { input.map(|input| FrontendGraphInput { data_type: FrontendGraphDataType::with_type(&input.ty), resolved_type: Some(format!("{:?} from {:?}", &input.ty, input.type_source)), + valid_types: input.valid_types.iter().map(|ty| ty.to_string()).collect(), name: input.name.unwrap_or_else(|| input.ty.nested_type().to_string()), connected_to: input.output_connector, }) @@ -2203,7 +2273,7 @@ impl NodeGraphMessageHandler { .node_metadata(&node_id, breadcrumb_network_path) .is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), can_be_layer: can_be_layer_lookup.contains(&node_id), - reference: network_interface.reference(&node_id, breadcrumb_network_path), + reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(), display_name: network_interface.frontend_display_name(&node_id, breadcrumb_network_path), primary_input, exposed_inputs, @@ -2273,7 +2343,7 @@ impl NodeGraphMessageHandler { || ( // Check if the last node in the chain has an exposed left input network_interface.upstream_flow_back_from_nodes(vec![node_id], &[], network_interface::FlowType::HorizontalFlow).last().is_some_and(|node_id| - network_interface.network(&[]).unwrap().nodes.get(&node_id).map_or_else(||{log::error!("Could not get node {node_id} in update_layer_panel"); false}, |node| { + network_interface.document_node(&node_id, &[]).map_or_else(||{log::error!("Could not get node {node_id} in update_layer_panel"); false}, |node| { if network_interface.is_layer(&node_id, &[]) { node.inputs.iter().filter(|input| input.is_exposed_to_frontend(true)).nth(1).is_some_and(|input| input.as_value().is_some()) } else { @@ -2284,7 +2354,7 @@ impl NodeGraphMessageHandler { let parents_visible = layer.ancestors(network_interface.document_metadata()).filter(|&ancestor| ancestor != layer).all(|layer| { if layer != LayerNodeIdentifier::ROOT_PARENT { - network_interface.network(&[]).unwrap().nodes.get(&layer.to_node()).map(|node| node.visible).unwrap_or_default() + network_interface.document_node(&layer.to_node(), &[]).map(|node| node.visible).unwrap_or_default() } else { true } @@ -2389,6 +2459,7 @@ struct InputLookup { name: Option, ty: Type, type_source: TypeSource, + valid_types: Vec, output_connector: Option, } @@ -2412,8 +2483,10 @@ fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface: } // Get the name from the metadata here (since it also requires a reference to the `network_interface`) - let name = network_interface.input_name(&node_id, index, breadcrumb_network_path); - + let name = network_interface + .input_name(&node_id, index, breadcrumb_network_path) + .filter(|s| !s.is_empty()) + .map(|name| name.to_string()); // Get the output connector that feeds into this input (done here as well for simplicity) let connector = OutputConnector::from_input(input); @@ -2430,13 +2503,21 @@ fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface: for (index, value) in value.iter_mut().enumerate() { // Skip not exposed inputs for efficiency let Some(value) = value else { continue }; - // Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`) let (ty, type_source) = network_interface.input_type(&InputConnector::node(node_id, index), breadcrumb_network_path); value.ty = ty; value.type_source = type_source; } } + + for (&node_id, value) in frontend_inputs_lookup.iter_mut() { + for (index, value) in value.iter_mut().enumerate() { + // Skip not exposed inputs for efficiency + let Some(value) = value else { continue }; + // Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`) + value.valid_types = network_interface.valid_input_types(&InputConnector::node(node_id, index), breadcrumb_network_path); + } + } frontend_inputs_lookup } @@ -2462,6 +2543,9 @@ impl Default for NodeGraphMessageHandler { deselect_on_pointer_up: None, auto_panning: Default::default(), preview_on_mouse_up: None, + reordering_export: None, + reordering_import: None, + end_index: None, } } } 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 1ca81ace6a..00f35c03bb 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -3,6 +3,7 @@ use super::document_node_definitions::{NodePropertiesContext, IMAGINATE_NODE}; use super::utility_types::FrontendGraphDataType; use crate::messages::layout::utility_types::widget_prelude::*; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::prelude::*; use graph_craft::document::value::TaggedValue; @@ -25,7 +26,7 @@ use graphene_std::transform::Footprint; use graphene_std::vector::misc::BooleanOperation; use graphene_std::vector::VectorData; -pub(crate) fn string_properties(text: impl Into) -> Vec { +pub(crate) fn string_properties(text: String) -> Vec { let widget = TextLabel::new(text).widget_holder(); vec![LayoutGroup::Row { widgets: vec![widget] }] } @@ -40,15 +41,15 @@ fn optionally_update_value(value: impl Fn(&T) -> Option + 'stati } } -fn update_value(value: impl Fn(&T) -> TaggedValue + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync { +pub fn update_value(value: impl Fn(&T) -> TaggedValue + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync { optionally_update_value(move |v| Some(value(v)), node_id, input_index) } -fn commit_value(_: &T) -> Message { +pub fn commit_value(_: &T) -> Message { DocumentMessage::AddTransaction.into() } -fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType, exposed: bool) -> WidgetHolder { +pub fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType, exposed: bool) -> WidgetHolder { ParameterExposeButton::new() .exposed(exposed) .data_type(data_type) @@ -65,7 +66,7 @@ fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType } // TODO: Remove this when we have proper entry row formatting that includes room for Assists. -fn add_blank_assist(widgets: &mut Vec) { +pub fn add_blank_assist(widgets: &mut Vec) { widgets.extend_from_slice(&[ // Custom CSS specific to the Properties panel converts this Section separator into the width of an assist (24px). Separator::new(SeparatorType::Section).widget_holder(), @@ -74,7 +75,7 @@ fn add_blank_assist(widgets: &mut Vec) { ]); } -fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, data_type: FrontendGraphDataType, blank_assist: bool) -> Vec { +pub fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, data_type: FrontendGraphDataType, blank_assist: bool) -> Vec { let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); return vec![]; @@ -87,16 +88,23 @@ fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, na widgets } -pub(crate) fn property_from_type( - document_node: &DocumentNode, - node_id: NodeId, - index: usize, - name: &str, - ty: &Type, - _context: &mut NodePropertiesContext, - number_options: (Option, Option, Option<(f64, f64)>), -) -> Vec { - let (mut number_min, mut number_max, range) = number_options; +pub(crate) fn property_from_type(node_id: NodeId, index: usize, ty: &Type, context: &mut NodePropertiesContext) -> Vec { + let Some(name) = context.network_interface.input_name(&node_id, index, context.selection_network_path) else { + log::warn!("A widget failed to be built for node {node_id}, index {index} because the input name could not be determined"); + return vec![]; + }; + + let Some(network) = context.network_interface.network(context.selection_network_path) else { + log::warn!("A widget failed to be built for node {node_id}, index {index} because the network could not be determined"); + return vec![]; + }; + + let Some(document_node) = network.nodes.get(&node_id) else { + log::warn!("A widget failed to be built for node {node_id}, index {index} because the document node does not exist"); + return vec![]; + }; + + let (mut number_min, mut number_max, range) = (None, None, None); let mut number_input = NumberInput::default(); if let Some((range_start, range_end)) = range { number_min = Some(range_start); @@ -124,122 +132,122 @@ pub(crate) fn property_from_type( // For all other types, use TypeId-based matching _ => { - if let Some(internal_id) = concrete_type.id { - use std::any::TypeId; - match internal_id { - x if x == TypeId::of::() => bool_widget(document_node, node_id, index, name, CheckboxInput::default(), true).into(), - x if x == TypeId::of::() => number_widget(document_node, node_id, index, name, number_input.min(min(f64::NEG_INFINITY)).max(max(f64::INFINITY)), true).into(), - x if x == TypeId::of::() => number_widget(document_node, node_id, index, name, number_input.int().min(min(0.)).max(max(f64::from(u32::MAX))), true).into(), - x if x == TypeId::of::() => number_widget(document_node, node_id, index, name, number_input.int().min(min(0.)), true).into(), - x if x == TypeId::of::() => text_widget(document_node, node_id, index, name, true).into(), - x if x == TypeId::of::() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(false), true), - x if x == TypeId::of::>() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(true), true), - x if x == TypeId::of::() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", None, add_blank_assist), - x if x == TypeId::of::() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", Some(0.), add_blank_assist), - x if x == TypeId::of::() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", None, add_blank_assist), - x if x == TypeId::of::>() => vec_f64_input(document_node, node_id, index, name, TextInput::default(), true).into(), - x if x == TypeId::of::>() => vec_dvec2_input(document_node, node_id, index, name, TextInput::default(), true).into(), - x if x == TypeId::of::() => { - let (font_widgets, style_widgets) = font_inputs(document_node, node_id, index, name, false); - font_widgets.into_iter().chain(style_widgets.unwrap_or_default()).collect::>().into() - } - x if x == TypeId::of::() => curves_widget(document_node, node_id, index, name, true), - x if x == TypeId::of::() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(false), true), - x if x == TypeId::of::() => vector_widget(document_node, node_id, index, name, true).into(), - x if x == TypeId::of::() => { - let widgets = footprint_widget(document_node, node_id, index); - let (last, rest) = widgets.split_last().expect("Footprint widget should return multiple rows"); - extra_widgets = rest.to_vec(); - last.clone() - } - x if x == TypeId::of::() => blend_mode(document_node, node_id, index, name, true), - x if x == TypeId::of::() => color_channel(document_node, node_id, index, name, true), - x if x == TypeId::of::() => rgba_channel(document_node, node_id, index, name, true), - x if x == TypeId::of::() => noise_type(document_node, node_id, index, name, true), - x if x == TypeId::of::() => fractal_type(document_node, node_id, index, name, true, false), - x if x == TypeId::of::() => cellular_distance_function(document_node, node_id, index, name, true, false), - x if x == TypeId::of::() => cellular_return_type(document_node, node_id, index, name, true, false), - x if x == TypeId::of::() => domain_warp_type(document_node, node_id, index, name, true, false), - x if x == TypeId::of::() => vec![DropdownInput::new(vec![vec![ - MenuListEntry::new("Relative") - .label("Relative") - .on_update(update_value(|_| TaggedValue::RelativeAbsolute(RelativeAbsolute::Relative), node_id, index)), - MenuListEntry::new("Absolute") - .label("Absolute") - .on_update(update_value(|_| TaggedValue::RelativeAbsolute(RelativeAbsolute::Absolute), node_id, index)), - ]]) - .widget_holder()] - .into(), - x if x == TypeId::of::() => line_cap_widget(document_node, node_id, index, name, true), - x if x == TypeId::of::() => line_join_widget(document_node, node_id, index, name, true), - x if x == TypeId::of::() => vec![DropdownInput::new(vec![vec![ - MenuListEntry::new("Solid") - .label("Solid") - .on_update(update_value(|_| TaggedValue::FillType(FillType::Solid), node_id, index)), - MenuListEntry::new("Gradient") - .label("Gradient") - .on_update(update_value(|_| TaggedValue::FillType(FillType::Gradient), node_id, index)), - ]]) - .widget_holder()] - .into(), - x if x == TypeId::of::() => vec![DropdownInput::new(vec![vec![ - MenuListEntry::new("Linear") - .label("Linear") - .on_update(update_value(|_| TaggedValue::GradientType(GradientType::Linear), node_id, index)), - MenuListEntry::new("Radial") - .label("Radial") - .on_update(update_value(|_| TaggedValue::GradientType(GradientType::Radial), node_id, index)), - ]]) - .widget_holder()] - .into(), - x if x == TypeId::of::() => boolean_operation_radio_buttons(document_node, node_id, index, name, true), - x if x == TypeId::of::() => centroid_widget(document_node, node_id, index), - x if x == TypeId::of::() => luminance_calculation(document_node, node_id, index, name, true), - x if x == TypeId::of::() => vec![DropdownInput::new( - ImaginateSamplingMethod::list() - .into_iter() - .map(|method| { - vec![MenuListEntry::new(format!("{:?}", method)).label(method.to_string()).on_update(update_value( - move |_| TaggedValue::ImaginateSamplingMethod(method), - node_id, - index, - ))] - }) - .collect(), - ) - .widget_holder()] - .into(), - x if x == TypeId::of::() => vec![DropdownInput::new( - ImaginateMaskStartingFill::list() - .into_iter() - .map(|fill| { - vec![MenuListEntry::new(format!("{:?}", fill)).label(fill.to_string()).on_update(update_value( - move |_| TaggedValue::ImaginateMaskStartingFill(fill), - node_id, - index, - ))] - }) - .collect(), - ) - .widget_holder()] - .into(), - _ => vec![TextLabel::new(format!("Unsupported type: {}", concrete_type.name)).widget_holder()].into(), + use std::any::TypeId; + match concrete_type.id { + Some(x) if x == TypeId::of::() => bool_widget(document_node, node_id, index, name, CheckboxInput::default(), true).into(), + Some(x) if x == TypeId::of::() => number_widget(document_node, node_id, index, name, number_input.min(min(f64::NEG_INFINITY)).max(max(f64::INFINITY)), true).into(), + Some(x) if x == TypeId::of::() => number_widget(document_node, node_id, index, name, number_input.int().min(min(0.)).max(max(f64::from(u32::MAX))), true).into(), + Some(x) if x == TypeId::of::() => number_widget(document_node, node_id, index, name, number_input.int().min(min(0.)), true).into(), + Some(x) if x == TypeId::of::() => text_widget(document_node, node_id, index, name, true).into(), + Some(x) if x == TypeId::of::() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(false), true), + Some(x) if x == TypeId::of::>() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(true), true), + Some(x) if x == TypeId::of::() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", None, add_blank_assist), + Some(x) if x == TypeId::of::() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", Some(0.), add_blank_assist), + Some(x) if x == TypeId::of::() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", None, add_blank_assist), + Some(x) if x == TypeId::of::>() => vec_f64_input(document_node, node_id, index, name, TextInput::default(), true).into(), + Some(x) if x == TypeId::of::>() => vec_dvec2_input(document_node, node_id, index, name, TextInput::default(), true).into(), + Some(x) if x == TypeId::of::() => { + let (font_widgets, style_widgets) = font_inputs(document_node, node_id, index, name, false); + font_widgets.into_iter().chain(style_widgets.unwrap_or_default()).collect::>().into() + } + Some(x) if x == TypeId::of::() => curves_widget(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(false), true), + Some(x) if x == TypeId::of::() => vector_widget(document_node, node_id, index, name, true).into(), + Some(x) if x == TypeId::of::() => { + let widgets = footprint_widget(document_node, node_id, index); + let (last, rest) = widgets.split_last().expect("Footprint widget should return multiple rows"); + extra_widgets = rest.to_vec(); + last.clone() + } + Some(x) if x == TypeId::of::() => blend_mode(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => color_channel(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => rgba_channel(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => noise_type(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => fractal_type(document_node, node_id, index, name, true, false), + Some(x) if x == TypeId::of::() => cellular_distance_function(document_node, node_id, index, name, true, false), + Some(x) if x == TypeId::of::() => cellular_return_type(document_node, node_id, index, name, true, false), + Some(x) if x == TypeId::of::() => domain_warp_type(document_node, node_id, index, name, true, false), + Some(x) if x == TypeId::of::() => vec![DropdownInput::new(vec![vec![ + MenuListEntry::new("Relative") + .label("Relative") + .on_update(update_value(|_| TaggedValue::RelativeAbsolute(RelativeAbsolute::Relative), node_id, index)), + MenuListEntry::new("Absolute") + .label("Absolute") + .on_update(update_value(|_| TaggedValue::RelativeAbsolute(RelativeAbsolute::Absolute), node_id, index)), + ]]) + .widget_holder()] + .into(), + Some(x) if x == TypeId::of::() => line_cap_widget(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => line_join_widget(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => vec![DropdownInput::new(vec![vec![ + MenuListEntry::new("Solid") + .label("Solid") + .on_update(update_value(|_| TaggedValue::FillType(FillType::Solid), node_id, index)), + MenuListEntry::new("Gradient") + .label("Gradient") + .on_update(update_value(|_| TaggedValue::FillType(FillType::Gradient), node_id, index)), + ]]) + .widget_holder()] + .into(), + Some(x) if x == TypeId::of::() => vec![DropdownInput::new(vec![vec![ + MenuListEntry::new("Linear") + .label("Linear") + .on_update(update_value(|_| TaggedValue::GradientType(GradientType::Linear), node_id, index)), + MenuListEntry::new("Radial") + .label("Radial") + .on_update(update_value(|_| TaggedValue::GradientType(GradientType::Radial), node_id, index)), + ]]) + .widget_holder()] + .into(), + Some(x) if x == TypeId::of::() => boolean_operation_radio_buttons(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => centroid_widget(document_node, node_id, index), + Some(x) if x == TypeId::of::() => luminance_calculation(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => vec![DropdownInput::new( + ImaginateSamplingMethod::list() + .into_iter() + .map(|method| { + vec![MenuListEntry::new(format!("{:?}", method)).label(method.to_string()).on_update(update_value( + move |_| TaggedValue::ImaginateSamplingMethod(method), + node_id, + index, + ))] + }) + .collect(), + ) + .widget_holder()] + .into(), + Some(x) if x == TypeId::of::() => vec![DropdownInput::new( + ImaginateMaskStartingFill::list() + .into_iter() + .map(|fill| { + vec![MenuListEntry::new(format!("{:?}", fill)).label(fill.to_string()).on_update(update_value( + move |_| TaggedValue::ImaginateMaskStartingFill(fill), + node_id, + index, + ))] + }) + .collect(), + ) + .widget_holder()] + .into(), + _ => { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, false); + widgets.extend_from_slice(&[TextLabel::new(format!("Unsupported type: {}", concrete_type.name)).widget_holder()]); + widgets.into() } - } else { - vec![TextLabel::new(format!("Unsupported type: {}", concrete_type.name)).widget_holder()].into() } } } } Type::Generic(_) => vec![TextLabel::new("Generic type (not supported)").widget_holder()].into(), - Type::Fn(_, out) => return property_from_type(document_node, node_id, index, name, out, _context, number_options), + Type::Fn(_, out) => return property_from_type(node_id, index, out, context), Type::Future(_) => vec![TextLabel::new("Future type (not supported)").widget_holder()].into(), }; extra_widgets.push(widgets); extra_widgets } -fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { +pub fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { @@ -258,7 +266,7 @@ fn text_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name widgets } -fn text_area_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { +pub fn text_area_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { @@ -277,7 +285,7 @@ fn text_area_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, widgets } -fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, checkbox_input: CheckboxInput, blank_assist: bool) -> Vec { +pub fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, checkbox_input: CheckboxInput, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { @@ -297,7 +305,7 @@ fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name widgets } -fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> Vec { +pub fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> Vec { let mut location_widgets = start_widgets(document_node, node_id, index, "Footprint", FrontendGraphDataType::General, true); location_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -438,7 +446,17 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) ] } -fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, x: &str, y: &str, unit: &str, min: Option, mut assist: impl FnMut(&mut Vec)) -> LayoutGroup { +pub fn vec2_widget( + document_node: &DocumentNode, + node_id: NodeId, + index: usize, + name: &str, + x: &str, + y: &str, + unit: &str, + min: Option, + mut assist: impl FnMut(&mut Vec), +) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, false); assist(&mut widgets); @@ -528,7 +546,7 @@ fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name LayoutGroup::Row { widgets } } -fn vec_f64_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_input: TextInput, blank_assist: bool) -> Vec { +pub fn vec_f64_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_input: TextInput, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist); let from_string = |string: &str| { @@ -557,7 +575,7 @@ fn vec_f64_input(document_node: &DocumentNode, node_id: NodeId, index: usize, na widgets } -fn vec_dvec2_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_props: TextInput, blank_assist: bool) -> Vec { +pub fn vec_dvec2_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_props: TextInput, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist); let from_string = |string: &str| { @@ -586,7 +604,7 @@ fn vec_dvec2_input(document_node: &DocumentNode, node_id: NodeId, index: usize, widgets } -fn font_inputs(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> (Vec, Option>) { +pub fn font_inputs(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> (Vec, Option>) { let mut first_widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let mut second_widgets = None; @@ -620,7 +638,7 @@ fn font_inputs(document_node: &DocumentNode, node_id: NodeId, index: usize, name (first_widgets, second_widgets) } -fn vector_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { +pub fn vector_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::VectorData, blank_assist); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -629,7 +647,7 @@ fn vector_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na widgets } -fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, number_props: NumberInput, blank_assist: bool) -> Vec { +pub fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, number_props: NumberInput, blank_assist: bool) -> Vec { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist); let Some(input) = document_node.inputs.get(index) else { @@ -668,7 +686,7 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na } // TODO: Generalize this instead of using a separate function per dropdown menu enum -fn color_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { +pub fn color_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -695,7 +713,7 @@ fn color_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, na LayoutGroup::Row { widgets }.with_tooltip("Color Channel") } -fn rgba_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { +pub fn rgba_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -723,7 +741,7 @@ fn rgba_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, nam } // TODO: Generalize this instead of using a separate function per dropdown menu enum -fn noise_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { +pub fn noise_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -749,7 +767,7 @@ fn noise_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: } // TODO: Generalize this instead of using a separate function per dropdown menu enum -fn fractal_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { +pub fn fractal_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -775,7 +793,7 @@ fn fractal_type(document_node: &DocumentNode, node_id: NodeId, index: usize, nam } // TODO: Generalize this instead of using a separate function per dropdown menu enum -fn cellular_distance_function(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { +pub fn cellular_distance_function(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -804,7 +822,7 @@ fn cellular_distance_function(document_node: &DocumentNode, node_id: NodeId, ind } // TODO: Generalize this instead of using a separate function per dropdown menu enum -fn cellular_return_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { +pub fn cellular_return_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -830,7 +848,7 @@ fn cellular_return_type(document_node: &DocumentNode, node_id: NodeId, index: us } // TODO: Generalize this instead of using a separate function per dropdown menu enum -fn domain_warp_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { +pub fn domain_warp_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -856,7 +874,7 @@ fn domain_warp_type(document_node: &DocumentNode, node_id: NodeId, index: usize, } // TODO: Generalize this instead of using a separate function per dropdown menu enum -fn blend_mode(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { +pub fn blend_mode(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -889,7 +907,7 @@ fn blend_mode(document_node: &DocumentNode, node_id: NodeId, index: usize, name: } // TODO: Generalize this for all dropdowns (also see blend_mode and channel_extration) -fn luminance_calculation(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { +pub fn luminance_calculation(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -916,7 +934,7 @@ fn luminance_calculation(document_node: &DocumentNode, node_id: NodeId, index: u LayoutGroup::Row { widgets }.with_tooltip("Formula used to calculate the luminance of a pixel") } -fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { +pub fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { @@ -946,7 +964,7 @@ fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: NodeId LayoutGroup::Row { widgets } } -fn line_cap_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { +pub fn line_cap_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -971,7 +989,7 @@ fn line_cap_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, LayoutGroup::Row { widgets } } -fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { +pub fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -996,7 +1014,7 @@ fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, LayoutGroup::Row { widgets } } -fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_button: ColorButton, blank_assist: bool) -> LayoutGroup { +pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_button: ColorButton, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); // Return early with just the label if the input is exposed to the graph, meaning we don't want to show the color picker widget in the Properties panel @@ -1005,7 +1023,6 @@ fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, nam }; widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - match &**tagged_value { TaggedValue::Color(color) => widgets.push( color_button @@ -1041,7 +1058,7 @@ fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, nam LayoutGroup::Row { widgets } } -fn curves_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { +pub fn curves_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { @@ -1060,7 +1077,7 @@ fn curves_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na LayoutGroup::Row { widgets } } -fn centroid_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> LayoutGroup { +pub fn centroid_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, "Centroid Type", FrontendGraphDataType::General, true); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); @@ -1093,40 +1110,34 @@ fn centroid_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) LayoutGroup::Row { widgets } } -pub(crate) fn load_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let url = text_widget(document_node, node_id, 1, "URL", true); - - vec![LayoutGroup::Row { widgets: url }] +pub fn get_document_node<'a>(node_id: NodeId, context: &'a NodePropertiesContext<'a>) -> Result<&'a DocumentNode, String> { + let network = context.network_interface.network(context.selection_network_path).ok_or("network not found in get_document_node")?; + network.nodes.get(&node_id).ok_or(format!("node {node_id} not found in get_document_node")) } -pub(crate) fn mask_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let mask = color_widget(document_node, node_id, 1, "Stencil", ColorButton::default(), true); - - vec![mask] +pub fn query_node_and_input_name<'a>(node_id: NodeId, input_index: usize, context: &'a NodePropertiesContext<'a>) -> Result<(&'a DocumentNode, &'a str), String> { + let document_node = get_document_node(node_id, context)?; + let input_name = context + .network_interface + .input_name(&node_id, input_index, context.selection_network_path) + .ok_or("input name not found in noise_properties_scale")?; + Ok((document_node, input_name)) } -pub(crate) fn insert_channel_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let color_channel = color_channel(document_node, node_id, 2, "Into", true); - - vec![color_channel] -} - -// Noise Type is commented out for now as there is only one type of noise (White Noise). -// As soon as there are more types of noise, this should be uncommented. -pub(crate) fn noise_pattern_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - // Get the current values of the inputs of interest so they can set whether certain inputs are disabled based on various conditions. - let current_noise_type = match &document_node.inputs[3].as_value() { +pub fn query_noise_pattern_state(node_id: NodeId, context: &NodePropertiesContext) -> Result<(bool, bool, bool, bool, bool, bool), String> { + let document_node = get_document_node(node_id, context)?; + let current_noise_type = document_node.inputs.iter().find_map(|input| match input.as_value() { Some(&TaggedValue::NoiseType(noise_type)) => Some(noise_type), _ => None, - }; - let current_domain_warp_type = match &document_node.inputs[4].as_value() { - Some(&TaggedValue::DomainWarpType(domain_warp_type)) => Some(domain_warp_type), - _ => None, - }; - let current_fractal_type = match &document_node.inputs[6].as_value() { + }); + let current_fractal_type = document_node.inputs.iter().find_map(|input| match input.as_value() { Some(&TaggedValue::FractalType(fractal_type)) => Some(fractal_type), _ => None, - }; + }); + let current_domain_warp_type = document_node.inputs.iter().find_map(|input| match input.as_value() { + Some(&TaggedValue::DomainWarpType(domain_warp_type)) => Some(domain_warp_type), + _ => None, + }); let fractal_active = current_fractal_type != Some(FractalType::None); let coherent_noise_active = current_noise_type != Some(NoiseType::WhiteNoise); let cellular_noise_active = current_noise_type == Some(NoiseType::Cellular); @@ -1135,218 +1146,38 @@ pub(crate) fn noise_pattern_properties(document_node: &DocumentNode, node_id: No let domain_warp_only_fractal_type_wrongly_active = !domain_warp_active && (current_fractal_type == Some(FractalType::DomainWarpIndependent) || current_fractal_type == Some(FractalType::DomainWarpProgressive)); - // All - let clip = LayoutGroup::Row { - widgets: bool_widget(document_node, node_id, 0, "Clip", CheckboxInput::default(), true), - }; - let seed = number_widget(document_node, node_id, 1, "Seed", NumberInput::default().min(0.).is_integer(true), true); - let scale = number_widget(document_node, node_id, 2, "Scale", NumberInput::default().min(0.).disabled(!coherent_noise_active), true); - let noise_type_row = noise_type(document_node, node_id, 3, "Noise Type", true); - - // Domain Warp - let domain_warp_type_row = domain_warp_type(document_node, node_id, 4, "Domain Warp Type", true, !coherent_noise_active); - let domain_warp_amplitude = number_widget( - document_node, - node_id, - 5, - "Domain Warp Amplitude", - NumberInput::default().min(0.).disabled(!coherent_noise_active || !domain_warp_active), - true, - ); - - // Fractal - let fractal_type_row = fractal_type(document_node, node_id, 6, "Fractal Type", true, !coherent_noise_active); - let fractal_octaves = number_widget( - document_node, - node_id, - 7, - "Fractal Octaves", - NumberInput::default() - .mode_range() - .min(1.) - .max(10.) - .range_max(Some(4.)) - .is_integer(true) - .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), - true, - ); - let fractal_lacunarity = number_widget( - document_node, - node_id, - 8, - "Fractal Lacunarity", - NumberInput::default() - .mode_range() - .min(0.) - .range_max(Some(10.)) - .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), - true, - ); - let fractal_gain = number_widget( - document_node, - node_id, - 9, - "Fractal Gain", - NumberInput::default() - .mode_range() - .min(0.) - .range_max(Some(10.)) - .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), - true, - ); - let fractal_weighted_strength = number_widget( - document_node, - node_id, - 10, - "Fractal Weighted Strength", - NumberInput::default() - .mode_range() - .min(0.) - .max(1.) // Defined for the 0-1 range - .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), - true, - ); - let fractal_ping_pong_strength = number_widget( - document_node, - node_id, - 11, - "Fractal Ping Pong Strength", - NumberInput::default() - .mode_range() - .min(0.) - .range_max(Some(10.)) - .disabled(!ping_pong_active || !coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), - true, - ); - - // Cellular - let cellular_distance_function_row = cellular_distance_function(document_node, node_id, 12, "Cellular Distance Function", true, !coherent_noise_active || !cellular_noise_active); - let cellular_return_type = cellular_return_type(document_node, node_id, 13, "Cellular Return Type", true, !coherent_noise_active || !cellular_noise_active); - let cellular_jitter = number_widget( - document_node, - node_id, - 14, - "Cellular Jitter", - NumberInput::default() - .mode_range() - .range_min(Some(0.)) - .range_max(Some(1.)) - .disabled(!coherent_noise_active || !cellular_noise_active), - true, - ); - - vec![ - // All - clip, - LayoutGroup::Row { widgets: seed }, - LayoutGroup::Row { widgets: scale }, - noise_type_row, - LayoutGroup::Row { widgets: Vec::new() }, - // Domain Warp - domain_warp_type_row, - LayoutGroup::Row { widgets: domain_warp_amplitude }, - LayoutGroup::Row { widgets: Vec::new() }, - // Fractal - fractal_type_row, - LayoutGroup::Row { widgets: fractal_octaves }, - LayoutGroup::Row { widgets: fractal_lacunarity }, - LayoutGroup::Row { widgets: fractal_gain }, - LayoutGroup::Row { widgets: fractal_weighted_strength }, - LayoutGroup::Row { widgets: fractal_ping_pong_strength }, - LayoutGroup::Row { widgets: Vec::new() }, - // Cellular - cellular_distance_function_row, - cellular_return_type, - LayoutGroup::Row { widgets: cellular_jitter }, - ] -} - -pub(crate) fn brightness_contrast_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let is_use_classic = match &document_node.inputs[3].as_value() { - Some(&TaggedValue::Bool(value)) => value, - _ => false, - }; - let ((b_min, b_max), (c_min, c_max)) = if is_use_classic { ((-100., 100.), (-100., 100.)) } else { ((-100., 150.), (-50., 100.)) }; - - let brightness = number_widget( - document_node, - node_id, - 1, - "Brightness", - NumberInput::default().mode_range().range_min(Some(b_min)).range_max(Some(b_max)).unit("%").display_decimal_places(2), - true, - ); - let contrast = number_widget( - document_node, - node_id, - 2, - "Contrast", - NumberInput::default().mode_range().range_min(Some(c_min)).range_max(Some(c_max)).unit("%").display_decimal_places(2), - true, - ); - let use_classic = bool_widget(document_node, node_id, 3, "Use Classic", CheckboxInput::default(), true); - - vec![ - LayoutGroup::Row { widgets: brightness }, - LayoutGroup::Row { widgets: contrast }, - LayoutGroup::Row { widgets: use_classic }, - ] -} - -pub(crate) fn curves_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let curves = curves_widget(document_node, node_id, 1, "Curve", true); - - vec![curves] + Ok(( + fractal_active, + coherent_noise_active, + cellular_noise_active, + ping_pong_active, + domain_warp_active, + domain_warp_only_fractal_type_wrongly_active, + )) } -pub(crate) fn _blur_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let radius = number_widget(document_node, node_id, 1, "Radius", NumberInput::default().min(0.).max(20.).int(), true); - let sigma = number_widget(document_node, node_id, 2, "Sigma", NumberInput::default().min(0.).max(10000.), true); - - vec![LayoutGroup::Row { widgets: radius }, LayoutGroup::Row { widgets: sigma }] +pub fn query_assign_colors_randomize(node_id: NodeId, context: &NodePropertiesContext) -> Result { + let document_node = get_document_node(node_id, context)?; + // This is safe since the node is a proto node and the implementation cannot be changed. + let randomize_index = 5; + Ok( + if let Some(&TaggedValue::Bool(randomize_enabled)) = &document_node.inputs.get(randomize_index).and_then(|input| input.as_value()) { + randomize_enabled + } else { + false + }, + ) } -pub(crate) fn assign_colors_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let fill_index = 1; - let stroke_index = 2; - let gradient_index = 3; - let reverse_index = 4; - let randomize_index = 5; - let seed_index = 6; - let repeat_every_index = 7; - - let fill_row = bool_widget(document_node, node_id, fill_index, "Fill", CheckboxInput::default(), true); - let stroke_row = bool_widget(document_node, node_id, stroke_index, "Stroke", CheckboxInput::default(), true); - let gradient_row = color_widget(document_node, node_id, gradient_index, "Gradient", ColorButton::default().allow_none(false), true); - let reverse_row = bool_widget(document_node, node_id, reverse_index, "Reverse", CheckboxInput::default(), true); - let randomize_enabled = if let Some(&TaggedValue::Bool(randomize_enabled)) = &document_node.inputs[randomize_index].as_value() { - randomize_enabled - } else { - false +pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in channel_mixer_properties: {err}"); + return Vec::new(); + } }; - let randomize_row = bool_widget(document_node, node_id, randomize_index, "Randomize", CheckboxInput::default(), true); - let seed_row = number_widget(document_node, node_id, seed_index, "Seed", NumberInput::default().min(0.).int().disabled(!randomize_enabled), true); - let repeat_every_row = number_widget( - document_node, - node_id, - repeat_every_index, - "Repeat Every", - NumberInput::default().min(0.).int().disabled(randomize_enabled), - true, - ); - - vec![ - LayoutGroup::Row { widgets: fill_row }, - LayoutGroup::Row { widgets: stroke_row }, - gradient_row, - LayoutGroup::Row { widgets: reverse_row }, - LayoutGroup::Row { widgets: randomize_row }, - LayoutGroup::Row { widgets: seed_row }, - LayoutGroup::Row { widgets: repeat_every_row }, - ] -} -pub(crate) fn channel_mixer_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { // Monochrome let monochrome_index = 1; let monochrome = bool_widget(document_node, node_id, monochrome_index, "Monochrome", CheckboxInput::default(), true); @@ -1446,7 +1277,14 @@ pub(crate) fn channel_mixer_properties(document_node: &DocumentNode, node_id: No layout } -pub(crate) fn selective_color_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { +pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in selective_color_properties: {err}"); + return Vec::new(); + } + }; // Colors choice let colors_index = 38; let mut colors = vec![TextLabel::new("Colors").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()]; @@ -1542,7 +1380,14 @@ pub(crate) fn _gpu_map_properties(document_node: &DocumentNode, node_id: NodeId, vec![LayoutGroup::Row { widgets: map }] } -pub(crate) fn exposure_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { +pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in exposure_properties: {err}"); + return Vec::new(); + } + }; let exposure = number_widget(document_node, node_id, 1, "Exposure", NumberInput::default().min(-20.).max(20.), true); let offset = number_widget(document_node, node_id, 2, "Offset", NumberInput::default().min(-0.5).max(0.5), true); let gamma_input = NumberInput::default().min(0.01).max(9.99).increment_step(0.1); @@ -1555,7 +1400,14 @@ pub(crate) fn exposure_properties(document_node: &DocumentNode, node_id: NodeId, ] } -pub(crate) fn rectangle_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { +pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in rectangle_properties: {err}"); + return Vec::new(); + } + }; let size_x_index = 1; let size_y_index = 2; let corner_rounding_type_index = 3; @@ -1680,81 +1532,16 @@ pub(crate) fn rectangle_properties(document_node: &DocumentNode, node_id: NodeId ] } -pub(crate) fn line_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let operand = |name: &str, index| vec2_widget(document_node, node_id, index, name, "X", "Y", " px", None, add_blank_assist); - vec![operand("Start", 1), operand("End", 2)] -} - -pub(crate) fn spline_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - vec![LayoutGroup::Row { - widgets: vec_dvec2_input(document_node, node_id, 1, "Points", TextInput::default().centered(true), true), - }] -} - -pub(crate) fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let translation = vec2_widget(document_node, node_id, 1, "Translation", "X", "Y", " px", None, add_blank_assist); - - let rotation = { - let index = 2; - - let mut widgets = start_widgets(document_node, node_id, index, "Rotation", FrontendGraphDataType::Number, true); - - let Some(input) = document_node.inputs.get(index) else { - log::warn!("A widget failed to be built because its node's input index is invalid."); - return vec![]; - }; - if let Some(&TaggedValue::F64(val)) = input.as_non_exposed_value() { - widgets.extend_from_slice(&[ - Separator::new(SeparatorType::Unrelated).widget_holder(), - NumberInput::new(Some(val.to_degrees())) - .unit("°") - .mode(NumberInputMode::Range) - .range_min(Some(-180.)) - .range_max(Some(180.)) - .on_update(update_value(|number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap().to_radians()), node_id, index)) - .on_commit(commit_value) - .widget_holder(), - ]); - } - - LayoutGroup::Row { widgets } - }; - - let scale = vec2_widget(document_node, node_id, 3, "Scale", "W", "H", "x", None, add_blank_assist); - - vec![translation, rotation, scale] -} -pub(crate) fn rasterize_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - footprint_widget(document_node, node_id, 1) -} - -pub(crate) fn text_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let text = text_area_widget(document_node, node_id, 1, "Text", true); - let (font, style) = font_inputs(document_node, node_id, 2, "Font", true); - let size = number_widget(document_node, node_id, 3, "Size", NumberInput::default().unit(" px").min(1.), true); - let line_height_ratio = number_widget(document_node, node_id, 4, "Line Height", NumberInput::default().min(0.).step(0.1), true); - let character_spacing = number_widget(document_node, node_id, 5, "Character Spacing", NumberInput::default().min(0.).step(0.1), true); - - let mut result = vec![LayoutGroup::Row { widgets: text }, LayoutGroup::Row { widgets: font }]; - if let Some(style) = style { - result.push(LayoutGroup::Row { widgets: style }); - } - result.push(LayoutGroup::Row { widgets: size }); - result.push(LayoutGroup::Row { widgets: line_height_ratio }); - result.push(LayoutGroup::Row { widgets: character_spacing }); - result -} - -pub(crate) fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { +pub(crate) fn imaginate_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { let imaginate_node = [context.selection_network_path, &[node_id]].concat(); let resolve_input = |name: &str| { IMAGINATE_NODE .default_node_template() .persistent_node_metadata - .input_names + .input_properties .iter() - .position(|input| input == name) + .position(|row| row.input_data.get("input_name").and_then(|v| v.as_str()) == Some(name)) .unwrap_or_else(|| panic!("Input {name} not found")) }; let seed_index = resolve_input("Seed"); @@ -1773,6 +1560,13 @@ pub(crate) fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId let faces_index = resolve_input("Improve Faces"); let tiling_index = resolve_input("Tiling"); + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in imaginate_properties: {err}"); + return Vec::new(); + } + }; let controller = &document_node.inputs[resolve_input("Controller")]; let server_status = { @@ -1983,13 +1777,19 @@ pub(crate) fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId ) .unwrap_or_default(); - let resolution = { - use graphene_std::imaginate::pick_safe_imaginate_resolution; + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in imaginate_properties: {err}"); + return Vec::new(); + } + }; + let resolution = { let mut widgets = start_widgets(document_node, node_id, resolution_index, "Resolution", FrontendGraphDataType::Number, false); let round = |size: DVec2| { - let (x, y) = pick_safe_imaginate_resolution(size.into()); + let (x, y) = graphene_std::imaginate::pick_safe_imaginate_resolution(size.into()); DVec2::new(x as f64, y as f64) }; @@ -2235,144 +2035,66 @@ pub(crate) fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId layout } -fn unknown_node_properties(reference: &String) -> Vec { - string_properties(format!("Node '{}' cannot be found in library", reference)) -} - -pub(crate) fn node_no_properties(_document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { +pub(crate) fn node_no_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { string_properties(if context.network_interface.is_layer(&node_id, context.selection_network_path) { - "Layer has no properties" + "Layer has no properties".to_string() } else { - "Node has no properties" + "Node has no properties".to_string() }) } -pub(crate) fn index_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let index = number_widget(document_node, node_id, 1, "Index", NumberInput::default().min(0.), true); +pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> LayoutGroup { + let mut layout = Vec::new(); - vec![LayoutGroup::Row { widgets: index }] -} - -pub(crate) fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId, pinned: bool, context: &mut NodePropertiesContext) -> LayoutGroup { - let reference = context.network_interface.reference(&node_id, context.selection_network_path).clone(); - let layout = if let Some(ref reference) = reference { - match super::document_node_definitions::resolve_document_node_type(reference) { - Some(document_node_type) => (document_node_type.properties)(document_node, node_id, context), - None => unknown_node_properties(reference), - } + if let Some(properties_override) = context + .network_interface + .reference(&node_id, context.selection_network_path) + .cloned() + .unwrap_or_default() + .as_ref() + .and_then(|reference| super::document_node_definitions::resolve_document_node_type(reference)) + .and_then(|definition| definition.properties) + { + layout = properties_override(node_id, context); } else { - node_no_properties(document_node, node_id, context) - }; - + let number_of_inputs = context.network_interface.number_of_inputs(&node_id, context.selection_network_path); + for input_index in 0..number_of_inputs { + let row = context.call_widget_override(&node_id, input_index).unwrap_or_else(|| { + let input_type = context.network_interface.input_type(&InputConnector::node(node_id, input_index), context.selection_network_path); + property_from_type(node_id, input_index, &input_type.0, context) + }); + layout.extend(row); + } + } + if layout.is_empty() { + layout = node_no_properties(node_id, context); + } + let name = context + .network_interface + .reference(&node_id, context.selection_network_path) + .cloned() + .unwrap_or_default() + .unwrap_or_default(); + let visible = context.network_interface.is_visible(&node_id, context.selection_network_path); + let pinned = context.network_interface.is_pinned(&node_id, context.selection_network_path); LayoutGroup::Section { - name: reference.unwrap_or_default(), - visible: document_node.visible, + name, + visible, pinned, id: node_id.0, layout, } } -pub(crate) fn boolean_operation_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let operation_index = 1; - let operation = boolean_operation_radio_buttons(document_node, node_id, operation_index, "Operation", true); - - vec![operation] -} - -pub(crate) fn copy_to_points_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let instance_index = 1; - let random_scale_min_index = 2; - let random_scale_max_index = 3; - let random_scale_bias_index = 4; - let random_scale_seed_index = 5; - let random_rotation_index = 6; - let random_rotation_seed_index = 7; - - let instance = vector_widget(document_node, node_id, instance_index, "Instance", true); - - let random_scale_min = number_widget( - document_node, - node_id, - random_scale_min_index, - "Random Scale Min", - NumberInput::default().min(0.).mode_range().range_min(Some(0.)).range_max(Some(2.)).unit("x"), - true, - ); - let random_scale_max = number_widget( - document_node, - node_id, - random_scale_max_index, - "Random Scale Max", - NumberInput::default().min(0.).mode_range().range_min(Some(0.)).range_max(Some(2.)).unit("x"), - true, - ); - let random_scale_bias = number_widget( - document_node, - node_id, - random_scale_bias_index, - "Random Scale Bias", - NumberInput::default().mode_range().range_min(Some(-50.)).range_max(Some(50.)), - true, - ); - let random_scale_seed = number_widget(document_node, node_id, random_scale_seed_index, "Random Scale Seed", NumberInput::default().int().min(0.), true); - - let random_rotation = number_widget( - document_node, - node_id, - random_rotation_index, - "Random Rotation", - NumberInput::default().min(0.).max(360.).mode_range().unit("°"), - true, - ); - let random_rotation_seed = number_widget(document_node, node_id, random_rotation_seed_index, "Random Rotation Seed", NumberInput::default().int().min(0.), true); - - vec![ - LayoutGroup::Row { widgets: instance }.with_tooltip("Artwork to be copied and placed at each point"), - LayoutGroup::Row { widgets: random_scale_min }.with_tooltip("Minimum range of randomized sizes given to each instance"), - LayoutGroup::Row { widgets: random_scale_max }.with_tooltip("Maximum range of randomized sizes given to each instance"), - LayoutGroup::Row { widgets: random_scale_bias } - .with_tooltip("Bias for the probability distribution of randomized sizes (0 is uniform, negatives favor more of small sizes, positives favor more of large sizes)"), - LayoutGroup::Row { widgets: random_scale_seed }.with_tooltip("Seed to determine unique variations on all the randomized instance sizes"), - LayoutGroup::Row { widgets: random_rotation }.with_tooltip("Range of randomized angles given to each instance, in degrees ranging from furthest clockwise to counterclockwise"), - LayoutGroup::Row { widgets: random_rotation_seed }.with_tooltip("Seed to determine unique variations on all the randomized instance angles"), - ] -} - -pub(crate) fn sample_points_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let spacing = number_widget(document_node, node_id, 1, "Spacing", NumberInput::default().min(1.).unit(" px"), true); - let start_offset = number_widget(document_node, node_id, 2, "Start Offset", NumberInput::default().min(0.).unit(" px"), true); - let stop_offset = number_widget(document_node, node_id, 3, "Stop Offset", NumberInput::default().min(0.).unit(" px"), true); - let adaptive_spacing = bool_widget(document_node, node_id, 4, "Adaptive Spacing", CheckboxInput::default(), true); - - vec![ - LayoutGroup::Row { widgets: spacing }.with_tooltip("Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled)"), - LayoutGroup::Row { widgets: start_offset }.with_tooltip("Exclude some distance from the start of the path before the first instance"), - LayoutGroup::Row { widgets: stop_offset }.with_tooltip("Exclude some distance from the end of the path after the last instance"), - LayoutGroup::Row { widgets: adaptive_spacing }.with_tooltip("Round 'Spacing' to a nearby value that divides into the path length evenly"), - ] -} - -pub(crate) fn poisson_disk_points_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let separation_disk_diameter_index = 1; - let seed_index = 2; - - let spacing = number_widget( - document_node, - node_id, - separation_disk_diameter_index, - "Separation Disk Diameter", - NumberInput::default().min(0.01).mode_range().range_min(Some(1.)).range_max(Some(100.)), - true, - ); - - let seed = number_widget(document_node, node_id, seed_index, "Seed", NumberInput::default().int().min(0.), true); - - vec![LayoutGroup::Row { widgets: spacing }, LayoutGroup::Row { widgets: seed }] -} - /// Fill Node Widgets LayoutGroup -pub(crate) fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { +pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in fill_properties: {err}"); + return Vec::new(); + } + }; let fill_index = 1; let backup_color_index = 2; let backup_gradient_index = 3; @@ -2547,7 +2269,14 @@ pub(crate) fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _co widgets } -pub fn stroke_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { +pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in fill_properties: {err}"); + return Vec::new(); + } + }; let color_index = 1; let weight_index = 2; let dash_lengths_index = 3; @@ -2586,7 +2315,14 @@ pub fn stroke_properties(document_node: &DocumentNode, node_id: NodeId, _context ] } -pub fn offset_path_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { +pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in offset_path_properties: {err}"); + return Vec::new(); + } + }; let distance_index = 1; let line_join_index = 2; let miter_limit_index = 3; @@ -2606,18 +2342,15 @@ pub fn offset_path_properties(document_node: &DocumentNode, node_id: NodeId, _co vec![LayoutGroup::Row { widgets: distance }, line_join, LayoutGroup::Row { widgets: miter_limit }] } -pub(crate) fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let location = vec2_widget(document_node, node_id, 2, "Location", "X", "Y", " px", None, add_blank_assist); - let dimensions = vec2_widget(document_node, node_id, 3, "Dimensions", "W", "H", " px", None, add_blank_assist); - let background = color_widget(document_node, node_id, 4, "Background", ColorButton::default().allow_none(false), true); - let clip = bool_widget(document_node, node_id, 5, "Clip", CheckboxInput::default(), true); - - let clip_row = LayoutGroup::Row { widgets: clip }; - - vec![location, dimensions, background, clip_row] -} +pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in offset_path_properties: {err}"); + return Vec::new(); + } + }; -pub fn math_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let expression_index = 1; let operation_b_index = 2; diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 9a19136a83..5fe34bd403 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -44,6 +44,8 @@ pub struct FrontendGraphInput { pub name: String, #[serde(rename = "resolvedType")] pub resolved_type: Option, + #[serde(rename = "validTypes")] + pub valid_types: Vec, #[serde(rename = "connectedTo")] pub connected_to: Option, } @@ -180,6 +182,8 @@ pub struct FrontendClickTargets { pub all_nodes_bounding_box: String, #[serde(rename = "importExportsBoundingBox")] pub import_exports_bounding_box: String, + #[serde(rename = "modifyImportExport")] + pub modify_import_export: Vec, } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_types.rs b/editor/src/messages/portfolio/document/properties_panel/utility_types.rs index 74dd563ddb..e2ffdacdfd 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_types.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_types.rs @@ -4,7 +4,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node use crate::node_graph_executor::NodeGraphExecutor; pub struct PropertiesPanelMessageHandlerData<'a> { - pub network_interface: &'a NodeNetworkInterface, + pub network_interface: &'a mut NodeNetworkInterface, pub selection_network_path: &'a [NodeId], pub document_name: &'a str, pub executor: &'a mut NodeGraphExecutor, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 5427a27ff4..8230d6ea45 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -6,6 +6,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Modify use crate::messages::portfolio::document::node_graph::document_node_definitions::{resolve_document_node_type, DocumentNodeDefinition}; use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput}; use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode; use bezier_rs::Subpath; use graph_craft::document::{value::TaggedValue, DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; @@ -14,6 +15,7 @@ use graphene_std::renderer::{ClickTarget, Quad}; use graphene_std::transform::Footprint; use graphene_std::vector::{PointId, VectorData, VectorModificationType}; use interpreted_executor::{dynamic_executor::ResolvedDocumentNodeTypes, node_registry::NODE_REGISTRY}; +use serde_json::{json, Value}; use glam::{DAffine2, DVec2, IVec2}; use std::collections::{HashMap, HashSet, VecDeque}; @@ -59,23 +61,41 @@ impl PartialEq for NodeNetworkInterface { // Public immutable getters for the network interface impl NodeNetworkInterface { + // TODO: Make private and use .field_name getter methods /// Gets the nested network based on network_path pub fn network(&self, network_path: &[NodeId]) -> Option<&NodeNetwork> { - self.network.nested_network(network_path) + let Some(network) = self.network.nested_network(network_path) else { + log::error!("Could not get nested network with path {network_path:?}"); + return None; + }; + Some(network) + } + + // TODO: Make private and use .field_name getter methods. For example network_interface.inputs(node_id, network_path) rather than getting the node then getting inputs + pub fn document_node(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&DocumentNode> { + let network = self.network(network_path)?; + let Some(node_metadata) = network.nodes.get(node_id) else { + log::error!("Could not get document node with id {node_id} in network {network_path:?}"); + return None; + }; + Some(node_metadata) } + // TODO: Make private and use .field_name getter methods /// The network metadata should always exist for the current network pub fn network_metadata(&self, network_path: &[NodeId]) -> Option<&NodeNetworkMetadata> { - self.network_metadata.nested_metadata(network_path) + let Some(network_metadata) = self.network_metadata.nested_metadata(network_path) else { + log::error!("Could not get nested network_metadata with path {network_path:?}"); + return None; + }; + Some(network_metadata) } + // TODO: Make private and use .field_name getter methods pub fn node_metadata(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&DocumentNodeMetadata> { - let Some(network_metadata) = self.network_metadata(network_path) else { - log::error!("Could not get nested network_metadata"); - return None; - }; + let network_metadata = self.network_metadata(network_path)?; let Some(node_metadata) = network_metadata.persistent_metadata.node_metadata.get(node_id) else { - log::error!("Could not get nested node_metadata for node {node_id} in network {network_path:?}"); + log::error!("Could not get node metadata for node {node_id} in network {network_path:?}"); return None; }; Some(node_metadata) @@ -122,8 +142,7 @@ impl NodeNetworkInterface { pub fn encapsulating_node(&self, network_path: &[NodeId]) -> Option<&DocumentNode> { let mut encapsulating_path = network_path.to_vec(); let encapsulating_node_id = encapsulating_path.pop()?; - let parent_network = self.network(&encapsulating_path)?; - let Some(encapsulating_node) = parent_network.nodes.get(&encapsulating_node_id) else { + let Some(encapsulating_node) = self.document_node(&encapsulating_node_id, &encapsulating_path) else { log::error!("Could not get encapsulating node in encapsulating_node"); return None; }; @@ -272,11 +291,7 @@ impl NodeNetworkInterface { } fn number_of_displayed_inputs(&self, node_id: &NodeId, network_path: &[NodeId]) -> usize { - let Some(network) = self.network(network_path) else { - log::error!("Could not get network in number_of_displayed_inputs"); - return 0; - }; - let Some(node) = network.nodes.get(node_id) else { + let Some(node) = self.document_node(node_id, network_path) else { log::error!("Could not get node {node_id} in number_of_displayed_inputs"); return 0; }; @@ -284,11 +299,7 @@ impl NodeNetworkInterface { } pub fn number_of_inputs(&self, node_id: &NodeId, network_path: &[NodeId]) -> usize { - let Some(network) = self.network(network_path) else { - log::error!("Could not get network in number_of_inputs"); - return 0; - }; - let Some(node) = network.nodes.get(node_id) else { + let Some(node) = self.document_node(node_id, network_path) else { log::error!("Could not get node {node_id} in number_of_inputs"); return 0; }; @@ -296,15 +307,11 @@ impl NodeNetworkInterface { } pub fn number_of_outputs(&self, node_id: &NodeId, network_path: &[NodeId]) -> usize { - let Some(network) = self.network(network_path) else { - log::error!("Could not get network in number_of_outputs"); - return 0; - }; - let Some(node) = network.nodes.get(node_id) else { + let Some(implementation) = self.implementation(node_id, network_path) else { log::error!("Could not get node {node_id} in number_of_outputs"); return 0; }; - match &node.implementation { + match &implementation { DocumentNodeImplementation::ProtoNode(_) => 1, DocumentNodeImplementation::Network(nested_network) => nested_network.exports.len(), DocumentNodeImplementation::Extract => 1, @@ -392,11 +399,7 @@ impl NodeNetworkInterface { /// Create a node template from an existing node. pub fn create_node_template(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option { - let Some(network) = self.network(network_path) else { - log::error!("Could not get network in create_node_template"); - return None; - }; - let Some(node) = network.nodes.get(node_id) else { + let Some(node) = self.document_node(node_id, network_path) else { log::error!("Could not get node {node_id} in create_node_template"); return None; }; @@ -477,11 +480,7 @@ impl NodeNetworkInterface { return output_type; } }; - let Some(current_network) = self.network(network_path) else { - log::error!("Could not get current network in input_type"); - return None; - }; - let Some(node) = current_network.nodes.get(&node_id) else { + let Some(node) = self.document_node(&node_id, network_path) else { log::error!("Could not get node {node_id} in input_type"); return None; }; @@ -533,13 +532,8 @@ impl NodeNetworkInterface { return (value.ty(), TypeSource::DocumentNodeDefault); } - let Some(network) = self.network(network_path) else { - log::error!("Could not get network in input_type"); - return (concrete!(()), TypeSource::Error("could not get network")); - }; - - let Some(node) = network.nodes.get(&node_id) else { - return (concrete!(()), TypeSource::Error("node id not in network")); + let Some(node) = self.document_node(&node_id, network_path) else { + return (concrete!(()), TypeSource::Error("node id {node_id:?} not in network {network_path:?}")); }; let node_id_path = [network_path.as_slice(), &[node_id]].concat(); @@ -598,6 +592,72 @@ impl NodeNetworkInterface { self.guess_type_from_node(&mut network_path.to_vec(), node_id, input_connector.input_index()) } + pub fn valid_input_types(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec { + let InputConnector::Node { node_id, input_index } = input_connector else { + // An export can have any type connected to it + return vec![graph_craft::generic!(T)]; + }; + let Some(implementation) = self.implementation(node_id, network_path) else { + log::error!("Could not get node implementation in valid_input_types"); + return Vec::new(); + }; + match implementation { + DocumentNodeImplementation::Network(nested_network) => { + let nested_path = [network_path, &[*node_id]].concat(); + let number_of_imports = self.number_of_inputs(node_id, network_path); + let Some(outward_wires) = self.outward_wires(&nested_path) else { + log::error!("Could not get outward wires in valid_input_types"); + return Vec::new(); + }; + let Some(inputs_from_import) = outward_wires.get(&OutputConnector::Import(*input_index)) else { + log::error!("Could not get inputs from import in valid_input_types"); + return Vec::new(); + }; + + let intersection: HashSet = inputs_from_import + .clone() + .iter() + .map(|input_connector| self.valid_input_types(input_connector, &nested_path)) + .map(|vec| vec.into_iter().collect::>()) + .fold(None, |acc: Option>, set| match acc { + Some(acc_set) => Some(acc_set.intersection(&set).cloned().collect()), + None => Some(set), + }) + .unwrap_or_default(); + + intersection.into_iter().collect::>() + } + + DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { + let Some(implementations) = NODE_REGISTRY.get(proto_node_identifier) else { + log::error!("Protonode {proto_node_identifier:?} not found in registry"); + return Vec::new(); + }; + let number_of_inputs = self.number_of_inputs(node_id, network_path); + implementations + .iter() + .filter_map(|(node_io, _)| { + let valid_implementation = (0..number_of_inputs).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| { + let input_type = self.input_type(&InputConnector::node(*node_id, iterator_index), network_path).0; + // Value inputs are stored as concrete, so they are compared to the nested type. Node inputs are stored as fn, so they are compared to the entire type. + // For example a node input of (Footprint) -> VectorData would not be compatible with () -> VectorData + node_io.inputs[iterator_index].clone().nested_type() == input_type || node_io.inputs[iterator_index] == input_type + }); + if valid_implementation { + Some(node_io.inputs[*input_index].clone()) + } else { + None + } + }) + .collect::>() + } + DocumentNodeImplementation::Extract => { + log::error!("Input types for extract node not supported"); + Vec::new() + } + } + } + /// Retrieves the output types for a given document node and its exports. /// /// This function traverses the node and its nested network structure (if applicable) to determine @@ -630,11 +690,7 @@ impl NodeNetworkInterface { /// This function assumes that export indices and node IDs always exist within their respective /// collections. It will panic if these assumptions are violated. pub fn output_types(&self, node_id: &NodeId, network_path: &[NodeId]) -> Vec> { - let Some(network) = self.network(network_path) else { - log::error!("Could not get network in output_types"); - return Vec::new(); - }; - let Some(node) = network.nodes.get(node_id) else { + let Some(implementation) = self.implementation(node_id, network_path) else { log::error!("Could not get node {node_id} in output_types"); return Vec::new(); }; @@ -642,7 +698,7 @@ impl NodeNetworkInterface { let mut output_types = Vec::new(); // If the node is not a protonode, get types by traversing across exports until a proto node is reached. - match &node.implementation { + match &implementation { graph_craft::document::DocumentNodeImplementation::Network(internal_network) => { for export in internal_network.exports.iter() { match export { @@ -716,9 +772,9 @@ impl NodeNetworkInterface { .filter_map(|(import_index, click_target)| { // Get import name from parent node metadata input, which must match the number of imports. // Empty string means to use type, or "Import + index" if type can't be determined - let import_name = self + let properties_row = self .encapsulating_node_metadata(network_path) - .and_then(|encapsulating_metadata| encapsulating_metadata.persistent_metadata.input_names.get(*import_index).cloned()) + .and_then(|encapsulating_metadata| encapsulating_metadata.persistent_metadata.input_properties.get(*import_index).cloned()) .unwrap_or_default(); let mut import_metadata = None; @@ -730,7 +786,15 @@ impl NodeNetworkInterface { let (input_type, type_source) = self.input_type(&InputConnector::node(encapsulating_node_id, *import_index), &encapsulating_path); let data_type = FrontendGraphDataType::with_type(&input_type); - let import_name = if import_name.is_empty() { input_type.clone().nested_type().to_string() } else { import_name }; + let Some(input_name) = properties_row.input_data.get("input_name").and_then(|input_name| input_name.as_str()) else { + log::error!("Could not get input_name in frontend_imports"); + return None; + }; + let import_name = if input_name.is_empty() { + input_type.clone().nested_type().to_string() + } else { + input_name.to_string() + }; let connected_to = self .outward_wires(network_path) @@ -827,6 +891,7 @@ impl NodeNetworkInterface { data_type: frontend_data_type, name: export_name, resolved_type: input_type.map(|(export_type, source)| format!("{export_type:?} from {source:?}")), + valid_types: self.valid_input_types(&InputConnector::Export(*export_index), network_path).iter().map(|ty| ty.to_string()).collect(), connected_to, }, click_target, @@ -837,36 +902,24 @@ impl NodeNetworkInterface { }) } - pub fn frontend_import_modify(&mut self, network_path: &[NodeId]) -> Option<(i32, i32)> { - (!network_path.is_empty()) - .then(|| { - self.modify_import_export(network_path).and_then(|modify_import_export_click_target| { - modify_import_export_click_target - .add_export - .bounding_box() - .map(|bounding_box| (bounding_box[0].x as i32, bounding_box[0].y as i32)) - }) - }) - .flatten() - } - - pub fn frontend_export_modify(&mut self, network_path: &[NodeId]) -> Option<(i32, i32)> { - (!network_path.is_empty()) - .then(|| { - self.modify_import_export(network_path).and_then(|modify_import_export_click_target| { - modify_import_export_click_target - .add_import - .bounding_box() - .map(|bounding_box| (bounding_box[0].x as i32, bounding_box[0].y as i32)) - }) + pub fn frontend_import_export_modify(&mut self, get_ports: F, network_path: &[NodeId]) -> Vec<(i32, i32)> + where + F: FnOnce(&ModifyImportExportClickTarget) -> Vec<&(usize, ClickTarget)>, + { + self.modify_import_export(network_path) + .map(|modify_import_export_click_target| { + get_ports(&modify_import_export_click_target) + .iter() + .filter_map(|(_, click_target)| click_target.bounding_box().map(|bounding_box| (bounding_box[0].x as i32, bounding_box[0].y as i32))) + .collect() }) - .flatten() + .unwrap_or_default() } pub fn height_from_click_target(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option { let mut node_height: Option = self .node_click_targets(node_id, network_path) - .and_then(|click_targets| click_targets.node_click_target.bounding_box()) + .and_then(|click_targets: &DocumentNodeClickTargets| click_targets.node_click_target.bounding_box()) .map(|bounding_box| ((bounding_box[1].y - bounding_box[0].y) / 24.) as u32); if !self.is_layer(node_id, network_path) { node_height = node_height.map(|height| height + 1); @@ -887,7 +940,7 @@ impl NodeNetworkInterface { .collect::>() { upstream_nodes_below_layer.insert(chain_node); - let Some(chain_node) = self.network(network_path).and_then(|network| network.nodes.get(&chain_node)) else { + let Some(chain_node) = self.document_node(&chain_node, network_path) else { log::error!("Could not get node {node_id} in upstream_nodes_below_layer"); continue; }; @@ -904,7 +957,7 @@ impl NodeNetworkInterface { // Get the node feeding into the left input of the chain let mut current_node_id = *node_id; loop { - let Some(current_node) = self.network(network_path).and_then(|network| network.nodes.get(¤t_node_id)) else { + let Some(current_node) = self.document_node(¤t_node_id, network_path) else { log::error!("Could not get node {node_id} in upstream_nodes_below_layer"); break; }; @@ -1043,17 +1096,41 @@ impl NodeNetworkInterface { } } - pub fn reference(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option { - self.node_metadata(node_id, network_path) - .and_then(|node_metadata| node_metadata.persistent_metadata.reference.as_ref().map(|reference| reference.to_string())) + pub fn reference(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&Option> { + let Some(node_metadata) = self.node_metadata(node_id, network_path) else { + log::error!("Could not get reference"); + return None; + }; + Some(&node_metadata.persistent_metadata.reference) + } + + pub fn implementation(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&DocumentNodeImplementation> { + let Some(node) = self.document_node(node_id, network_path) else { + log::error!("Could not get implementation"); + return None; + }; + Some(&node.implementation) } - // None means that the type will be used - pub fn input_name(&self, node_id: &NodeId, index: usize, network_path: &[NodeId]) -> Option { + pub fn input_name(&self, node_id: &NodeId, index: usize, network_path: &[NodeId]) -> Option<&str> { + let Some(value) = self.input_metadata(node_id, index, "input_name", network_path) else { + log::error!("Could not get input_name for node {node_id} index {index}"); + return None; + }; + value.as_str() + } + + pub fn input_properties_row(&self, node_id: &NodeId, index: usize, network_path: &[NodeId]) -> Option<&PropertiesRow> { self.node_metadata(node_id, network_path) - .and_then(|node_metadata| node_metadata.persistent_metadata.input_names.get(index)) - .cloned() - .filter(|s| !s.is_empty()) + .and_then(|node_metadata| node_metadata.persistent_metadata.input_properties.get(index)) + } + + pub fn input_metadata(&self, node_id: &NodeId, index: usize, field: &str, network_path: &[NodeId]) -> Option<&Value> { + let Some(input_row) = self.input_properties_row(node_id, index, network_path) else { + log::error!("Could not get node_metadata in get_input_metadata"); + return None; + }; + input_row.input_data.get(field) } // Use frontend display name instead @@ -1071,13 +1148,16 @@ impl NodeNetworkInterface { .expect("Could not get persistent node metadata in untitled_layer_label") .persistent_metadata .is_layer(); - let reference = self.reference(node_id, network_path); - let is_merge_node = reference.as_ref().is_some_and(|reference| reference == "Merge"); + let Some(reference) = self.reference(node_id, network_path) else { + log::error!("Could not get reference in untitled_layer_label"); + return "".to_string(); + }; + if self.display_name(node_id, network_path).is_empty() { - if is_layer && is_merge_node { + if is_layer && *reference == Some("Merge".to_string()) { "Untitled Layer".to_string() } else { - reference.unwrap_or("Untitled node".to_string()) + reference.clone().unwrap_or("Untitled node".to_string()) } } else { self.display_name(node_id, network_path) @@ -1092,13 +1172,17 @@ impl NodeNetworkInterface { node_metadata.persistent_metadata.locked } - pub fn is_visible(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - let Some(network) = self.network(network_path) else { - log::error!("Could not get nested network_metadata in is_visible"); + pub fn is_pinned(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + let Some(node_metadata) = self.node_metadata(node_id, network_path) else { + log::error!("Could not get persistent node metadata in is_pinned for node {node_id}"); return false; }; - let Some(node) = network.nodes.get(node_id) else { - log::error!("Could not get nested node_metadata in is_visible"); + node_metadata.persistent_metadata.pinned + } + + pub fn is_visible(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + let Some(node) = self.document_node(node_id, network_path) else { + log::error!("Could not get node in is_visible"); return false; }; node.visible @@ -1155,8 +1239,7 @@ impl NodeNetworkInterface { pub fn is_artboard(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { self.reference(node_id, network_path) - .as_ref() - .is_some_and(|reference| reference == "Artboard" && self.connected_to_output(node_id, &[])) + .is_some_and(|reference| *reference == Some("Artboard".to_string()) && self.connected_to_output(node_id, &[])) } pub fn all_artboards(&self) -> HashSet { @@ -1209,7 +1292,7 @@ impl NodeNetworkInterface { .ancestors(self.document_metadata()) .find(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT && self.is_artboard(&ancestor.to_node(), &[])) { - let artboard = self.network(&[]).unwrap().nodes.get(&artboard_node_identifier.to_node()); + let artboard = self.document_node(&artboard_node_identifier.to_node(), &[]); let clip_input = artboard.unwrap().inputs.get(5).unwrap(); if let NodeInput::Value { tagged_value, .. } = clip_input { if tagged_value.to_primitive_string() == "true" { @@ -1976,48 +2059,97 @@ impl NodeNetworkInterface { return; }; - let viewport_top_right = network_metadata - .persistent_metadata - .navigation_metadata - .node_graph_to_viewport - .inverse() - .transform_point2(rounded_network_edge_distance.exports_to_edge_distance); - let offset_from_top_right = if network - .exports - .first() - .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) - { - DVec2::new(2. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) - } else { - DVec2::new(4. * GRID_SIZE as f64, 0.) - }; + let mut reorder_imports_exports = Ports::new(); + let mut add_import_export = Ports::new(); + let mut remove_imports_exports = Ports::new(); - let bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_right; - let export_top_right = DVec2::new(viewport_top_right.x.max(bounding_box_top_right.x), viewport_top_right.y.min(bounding_box_top_right.y)); - let add_export_center = export_top_right + DVec2::new(0., network.exports.len() as f64 * 24.); - let add_export = ClickTarget::new(Subpath::new_ellipse(add_export_center - DVec2::new(8., 8.), add_export_center + DVec2::new(8., 8.)), 0.); + if !network_path.is_empty() { + let viewport_top_right = network_metadata + .persistent_metadata + .navigation_metadata + .node_graph_to_viewport + .inverse() + .transform_point2(rounded_network_edge_distance.exports_to_edge_distance); + let offset_from_top_right = if network + .exports + .first() + .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) + { + DVec2::new(2. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) + } else { + DVec2::new(4. * GRID_SIZE as f64, 0.) + }; - let viewport_top_left = network_metadata - .persistent_metadata - .navigation_metadata - .node_graph_to_viewport - .inverse() - .transform_point2(rounded_network_edge_distance.imports_to_edge_distance); + let bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_right; + let export_top_right: DVec2 = DVec2::new(viewport_top_right.x.max(bounding_box_top_right.x), viewport_top_right.y.min(bounding_box_top_right.y)); + let add_export_center = export_top_right + DVec2::new(0., network.exports.len() as f64 * 24.); + let add_export = ClickTarget::new( + Subpath::new_rounded_rect(add_export_center - DVec2::new(12., 12.), add_export_center + DVec2::new(12., 12.), [3.; 4]), + 0., + ); + add_import_export.insert_custom_input_port(0, add_export); - let offset_from_top_left = if network - .exports - .first() - .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) - { - DVec2::new(-4. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) - } else { - DVec2::new(-4. * GRID_SIZE as f64, 0.) - }; + let viewport_top_left = network_metadata + .persistent_metadata + .navigation_metadata + .node_graph_to_viewport + .inverse() + .transform_point2(rounded_network_edge_distance.imports_to_edge_distance); - let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left; - let import_top_left = DVec2::new(viewport_top_left.x.min(bounding_box_top_left.x), viewport_top_left.y.min(bounding_box_top_left.y)); - let add_import_center = import_top_left + DVec2::new(0., self.number_of_displayed_imports(network_path) as f64 * 24.); - let add_import = ClickTarget::new(Subpath::new_ellipse(add_import_center - DVec2::new(8., 8.), add_import_center + DVec2::new(8., 8.)), 0.); + let offset_from_top_left = if network + .exports + .first() + .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) + { + DVec2::new(-4. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) + } else { + DVec2::new(-4. * GRID_SIZE as f64, 0.) + }; + + let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left; + let import_top_left = DVec2::new(viewport_top_left.x.min(bounding_box_top_left.x), viewport_top_left.y.min(bounding_box_top_left.y)); + let add_import_center = import_top_left + DVec2::new(0., self.number_of_displayed_imports(network_path) as f64 * 24.); + let add_import = ClickTarget::new( + Subpath::new_rounded_rect(add_import_center - DVec2::new(12., 12.), add_import_center + DVec2::new(12., 12.), [3.; 4]), + 0., + ); + add_import_export.insert_custom_output_port(0, add_import); + + let Some(import_exports) = self.import_export_ports(network_path) else { + log::error!("Could not get import_export_ports in load_modify_import_export"); + return; + }; + + for (import_index, import_click_target) in import_exports.output_ports() { + let Some(import_bounding_box) = import_click_target.bounding_box() else { + log::error!("Could not get export bounding box in load_modify_import_export"); + continue; + }; + let reorder_import_center = (import_bounding_box[0] + import_bounding_box[1]) / 2. + DVec2::new(-12., 0.); + let remove_import_center = reorder_import_center + DVec2::new(-12., 0.); + + let reorder_import = ClickTarget::new(Subpath::new_rect(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.); + let remove_import = ClickTarget::new(Subpath::new_rect(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.); + + reorder_imports_exports.insert_custom_output_port(*import_index, reorder_import); + remove_imports_exports.insert_custom_output_port(*import_index, remove_import); + } + + for (export_index, export_click_target) in import_exports.input_ports() { + let Some(export_bounding_box) = export_click_target.bounding_box() else { + log::error!("Could not get export bounding box in load_modify_import_export"); + continue; + }; + let reorder_export_center = (export_bounding_box[0] + export_bounding_box[1]) / 2. + DVec2::new(12., 0.); + let remove_export_center = reorder_export_center + DVec2::new(12., 0.); + + let reorder_export = ClickTarget::new(Subpath::new_rect(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.); + let remove_export = ClickTarget::new(Subpath::new_rect(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.); + + reorder_imports_exports.insert_custom_input_port(*export_index, reorder_export); + remove_imports_exports.insert_custom_input_port(*export_index, remove_export); + } + } let Some(network_metadata) = self.network_metadata_mut(network_path) else { log::error!("Could not get current network in load_modify_import_export"); @@ -2025,12 +2157,9 @@ impl NodeNetworkInterface { }; network_metadata.transient_metadata.modify_import_export = TransientMetadata::Loaded(ModifyImportExportClickTarget { - add_export, - add_import, - remove_imports: Vec::new(), - remove_exports: Vec::new(), - move_import: Vec::new(), - move_export: Vec::new(), + add_import_export, + remove_imports_exports, + reorder_imports_exports, }); } @@ -2070,7 +2199,6 @@ impl NodeNetworkInterface { let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; // TODO: Eventually replace node graph top right with the footprint when trying to get the network edge distance let node_graph_top_right = network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right; - let target_exports_distance = node_graph_to_viewport.inverse().transform_point2(DVec2::new( node_graph_top_right.x - EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP as f64, node_graph_top_right.y + EXPORTS_TO_TOP_EDGE_PIXEL_GAP as f64, @@ -2153,7 +2281,6 @@ impl NodeNetworkInterface { .unwrap_or([DVec2::new(0., 0.), DVec2::new(0., 0.)]); let Some(network_metadata) = self.network_metadata_mut(network_path) else { return }; - network_metadata.transient_metadata.all_nodes_bounding_box = TransientMetadata::Loaded(all_nodes_bounding_box); } @@ -2370,11 +2497,7 @@ impl NodeNetworkInterface { log::error!("Could not get nested node_metadata in load_node_click_targets"); return; }; - let Some(network) = self.network(network_path) else { - log::error!("Could not get network in load_node_click_targets"); - return; - }; - let Some(document_node) = network.nodes.get(node_id) else { + let Some(document_node) = self.document_node(node_id, network_path) else { log::error!("Could not get document node in load_node_click_targets"); return; }; @@ -2709,6 +2832,19 @@ impl NodeNetworkInterface { let mut import_exports_bounding_box = String::new(); let _ = import_exports_target.subpath_to_svg(&mut import_exports_bounding_box, DAffine2::IDENTITY); + let mut modify_import_export = Vec::new(); + if let Some(modify_import_export_click_targets) = self.modify_import_export(network_path) { + for click_target in modify_import_export_click_targets + .add_import_export + .click_targets() + .chain(modify_import_export_click_targets.remove_imports_exports.click_targets()) + .chain(modify_import_export_click_targets.reorder_imports_exports.click_targets()) + { + let mut remove_string = String::new(); + let _ = click_target.subpath().subpath_to_svg(&mut remove_string, DAffine2::IDENTITY); + modify_import_export.push(remove_string); + } + } FrontendClickTargets { node_click_targets, layer_click_targets, @@ -2716,6 +2852,7 @@ impl NodeNetworkInterface { icon_click_targets, all_nodes_bounding_box, import_exports_bounding_box, + modify_import_export, } } @@ -2724,13 +2861,19 @@ impl NodeNetworkInterface { } pub fn is_eligible_to_be_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - let input_count = self.number_of_displayed_inputs(node_id, network_path); + let Some(node) = self.document_node(node_id, network_path) else { + log::error!("Could not get node {node_id} in is_eligible_to_be_layer"); + return false; + }; + let input_count = node.inputs.iter().take(2).filter(|input| input.is_exposed_to_frontend(network_path.is_empty())).count(); + let parameters_hidden = node.inputs.iter().skip(2).all(|input| !input.is_exposed_to_frontend(network_path.is_empty())); let output_count = self.number_of_outputs(node_id, network_path); self.node_metadata(node_id, network_path) .is_some_and(|node_metadata| node_metadata.persistent_metadata.has_primary_output) && output_count == 1 && (input_count <= 2) + && parameters_hidden } pub fn node_graph_ptz(&self, network_path: &[NodeId]) -> Option<&PTZ> { @@ -2885,11 +3028,7 @@ impl NodeNetworkInterface { match input_connector { InputConnector::Node { node_id, input_index } => { // Get the displayed index from the input index - let Some(network) = self.network(network_path) else { - log::error!("Could not get network in input_position"); - return None; - }; - let Some(node) = network.nodes.get(node_id) else { + let Some(node) = self.document_node(node_id, network_path) else { log::error!("Could not get node in input_position"); return None; }; @@ -3181,7 +3320,7 @@ impl NodeNetworkInterface { } /// Inserts a new export at insert index. If the insert index is -1 it is inserted at the end. The output_name is used by the encapsulating node. - pub fn add_export(&mut self, default_value: TaggedValue, insert_index: isize, output_name: String, network_path: &[NodeId]) { + pub fn add_export(&mut self, default_value: TaggedValue, insert_index: isize, output_name: &str, network_path: &[NodeId]) { let Some(network) = self.network_mut(network_path) else { log::error!("Could not get nested network in add_export"); return; @@ -3207,10 +3346,12 @@ impl NodeNetworkInterface { // There will not be an encapsulating node if the network is the document network if let Some(encapsulating_node_metadata) = self.encapsulating_node_metadata_mut(network_path) { if insert_index == -1 { - encapsulating_node_metadata.persistent_metadata.output_names.push(output_name); + encapsulating_node_metadata.persistent_metadata.output_names.push(output_name.to_string()); } else { - encapsulating_node_metadata.persistent_metadata.output_names.insert(insert_index as usize, output_name); + encapsulating_node_metadata.persistent_metadata.output_names.insert(insert_index as usize, output_name.to_string()); } + // Clear the reference to the nodes definition + encapsulating_node_metadata.persistent_metadata.reference = None; }; // Update the export ports and outward wires for the current network @@ -3236,16 +3377,12 @@ impl NodeNetworkInterface { } /// Inserts a new input at insert index. If the insert index is -1 it is inserted at the end. The output_name is used by the encapsulating node. - pub fn add_import(&mut self, default_value: TaggedValue, exposed: bool, insert_index: isize, input_name: String, network_path: &[NodeId]) { + pub fn add_import(&mut self, default_value: TaggedValue, exposed: bool, insert_index: isize, input_name: &str, network_path: &[NodeId]) { let mut encapsulating_network_path = network_path.to_vec(); let Some(node_id) = encapsulating_network_path.pop() else { log::error!("Cannot add import for document network"); return; }; - // Set the node to be a non layer if it is no longer eligible to be a layer - if !self.is_eligible_to_be_layer(&node_id, &encapsulating_network_path) && self.is_layer(&node_id, &encapsulating_network_path) { - self.set_to_node_or_layer(&node_id, &encapsulating_network_path, false); - } let Some(network) = self.network_mut(&encapsulating_network_path) else { log::error!("Could not get nested network in insert_input"); @@ -3265,37 +3402,349 @@ impl NodeNetworkInterface { self.transaction_modified(); + // Set the node to be a non layer if it is no longer eligible to be a layer + if !self.is_eligible_to_be_layer(&node_id, &encapsulating_network_path) && self.is_layer(&node_id, &encapsulating_network_path) { + self.set_to_node_or_layer(&node_id, &encapsulating_network_path, false); + } + let Some(node_metadata) = self.node_metadata_mut(&node_id, &encapsulating_network_path) else { log::error!("Could not get node_metadata in insert_input"); return; }; if insert_index == -1 { - node_metadata.persistent_metadata.input_names.push(input_name); + node_metadata.persistent_metadata.input_properties.push(input_name.into()); } else { - node_metadata.persistent_metadata.input_names.insert(insert_index as usize, input_name); + node_metadata.persistent_metadata.input_properties.insert(insert_index as usize, input_name.into()); } - // Update the internal network import ports and outwards connections (if has a network implementation) - if let Some(internal_network) = &mut node_metadata.persistent_metadata.network_metadata { - internal_network.transient_metadata.import_export_ports.unload(); - internal_network.transient_metadata.outward_wires.unload(); - } + // Clear the reference to the nodes definition + node_metadata.persistent_metadata.reference = None; - // Update the click targets for the node + // Update the metadata for the encapsulating node self.unload_node_click_targets(&node_id, &encapsulating_network_path); + self.unload_all_nodes_bounding_box(&encapsulating_network_path); + if encapsulating_network_path.is_empty() && (insert_index == 0 || insert_index == 1) { + self.load_structure(); + } + + // Unload the metadata for the nested network + self.unload_outward_wires(network_path); + self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); + } + + // First disconnects the export, then removes it + pub fn remove_export(&mut self, export_index: usize, network_path: &[NodeId]) { + let mut encapsulating_network_path = network_path.to_vec(); + let Some(parent_id) = encapsulating_network_path.pop() else { + log::error!("Cannot remove export for document network"); + return; + }; + + // Disconnect the removed export, and handle connections to the node which had its output removed + self.disconnect_input(&InputConnector::Export(export_index), network_path); + let number_of_outputs = self.number_of_outputs(&parent_id, &encapsulating_network_path); + for shifted_export in export_index..number_of_outputs { + let Some(encapsulating_outward_wires) = self.outward_wires(&encapsulating_network_path) else { + log::error!("Could not get outward wires in remove_export"); + return; + }; + let Some(downstream_connections_for_shifted_export) = encapsulating_outward_wires.get(&OutputConnector::node(parent_id, shifted_export)).cloned() else { + log::error!("Could not get downstream connections for shifted export in remove_export"); + return; + }; + for downstream_connection in downstream_connections_for_shifted_export { + self.disconnect_input(&downstream_connection, &encapsulating_network_path); + if shifted_export != export_index { + self.create_wire(&OutputConnector::node(parent_id, shifted_export - 1), &downstream_connection, &encapsulating_network_path); + } + } + } + + let Some(network) = self.network_mut(network_path) else { + log::error!("Could not get nested network in add_export"); + return; + }; + network.exports.remove(export_index); + + self.transaction_modified(); - // Update the transient network metadata bounding box for all nodes and outward wires + let Some(encapsulating_node_metadata) = self.node_metadata_mut(&parent_id, &encapsulating_network_path) else { + log::error!("Could not get encapsulating node metadata in remove_export"); + return; + }; + encapsulating_node_metadata.persistent_metadata.output_names.remove(export_index); + encapsulating_node_metadata.persistent_metadata.reference = None; + + // Update the metadata for the encapsulating node + self.unload_outward_wires(&encapsulating_network_path); + self.unload_node_click_targets(&parent_id, &encapsulating_network_path); self.unload_all_nodes_bounding_box(&encapsulating_network_path); + if !self.is_eligible_to_be_layer(&parent_id, &encapsulating_network_path) && self.is_layer(&parent_id, &encapsulating_network_path) { + self.set_to_node_or_layer(&parent_id, &encapsulating_network_path, false); + } + if encapsulating_network_path.is_empty() { + self.load_structure(); + } // Unload the metadata for the nested network self.unload_outward_wires(network_path); self.unload_import_export_ports(network_path); self.unload_modify_import_export(network_path); + } - // If the input is inserted as the first input, then it may have affected the document metadata structure - if encapsulating_network_path.is_empty() && (insert_index == 0 || insert_index == 1) { + // First disconnects the import, then removes it + pub fn remove_import(&mut self, import_index: usize, network_path: &[NodeId]) { + let mut encapsulating_network_path = network_path.to_vec(); + let Some(parent_id) = encapsulating_network_path.pop() else { + log::error!("Cannot remove export for document network"); + return; + }; + + let number_of_inputs = self.number_of_inputs(&parent_id, &encapsulating_network_path); + let Some(outward_wires) = self.outward_wires(network_path) else { + log::error!("Could not get outward wires in remove_import"); + return; + }; + let Some(outward_wires_for_import) = outward_wires.get(&OutputConnector::Import(import_index)).cloned() else { + log::error!("Could not get outward wires for import in remove_import"); + return; + }; + let mut new_import_mapping = Vec::new(); + for i in (import_index + 1)..number_of_inputs { + let Some(outward_wires_for_import) = outward_wires.get(&OutputConnector::Import(i)).cloned() else { + log::error!("Could not get outward wires for import in remove_import"); + return; + }; + for upstream_input_wire in outward_wires_for_import { + new_import_mapping.push((OutputConnector::Import(i - 1), upstream_input_wire)); + } + } + + // Disconnect all upstream connections + for outward_wire in outward_wires_for_import { + self.disconnect_input(&outward_wire, network_path); + } + // Shift inputs connected to to imports at a higher index down one + for (output_connector, input_wire) in new_import_mapping { + self.create_wire(&output_connector, &input_wire, network_path); + } + + let Some(network) = self.network_mut(&encapsulating_network_path) else { + log::error!("Could not get parent node in remove_import"); + return; + }; + let Some(node) = network.nodes.get_mut(&parent_id) else { + log::error!("Could not get node in remove_import"); + return; + }; + + node.inputs.remove(import_index); + + self.transaction_modified(); + + // There will not be an encapsulating node if the network is the document network + let Some(encapsulating_node_metadata) = self.node_metadata_mut(&parent_id, &encapsulating_network_path) else { + log::error!("Could not get encapsulating node metadata in remove_export"); + return; + }; + encapsulating_node_metadata.persistent_metadata.input_properties.remove(import_index); + encapsulating_node_metadata.persistent_metadata.reference = None; + + // Update the metadata for the encapsulating node + self.unload_outward_wires(&encapsulating_network_path); + self.unload_node_click_targets(&parent_id, &encapsulating_network_path); + self.unload_all_nodes_bounding_box(&encapsulating_network_path); + if !self.is_eligible_to_be_layer(&parent_id, &encapsulating_network_path) && self.is_layer(&parent_id, &encapsulating_network_path) { + self.set_to_node_or_layer(&parent_id, &encapsulating_network_path, false); + } + if encapsulating_network_path.is_empty() { self.load_structure(); } + + // Unload the metadata for the nested network + self.unload_outward_wires(network_path); + self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); + } + + /// The end index is before the export is removed, so moving to the end is the length of the current exports + pub fn reorder_export(&mut self, start_index: usize, mut end_index: usize, network_path: &[NodeId]) { + let mut encapsulating_network_path = network_path.to_vec(); + let Some(parent_id) = encapsulating_network_path.pop() else { + log::error!("Could not reorder export for document network"); + return; + }; + + let Some(network) = self.network_mut(network_path) else { + log::error!("Could not get nested network in reorder_export"); + return; + }; + if end_index > start_index { + end_index -= 1; + } + let export = network.exports.remove(start_index); + network.exports.insert(end_index, export); + + self.transaction_modified(); + + let Some(encapsulating_node_metadata) = self.node_metadata_mut(&parent_id, &encapsulating_network_path) else { + log::error!("Could not get encapsulating network_metadata in reorder_export"); + return; + }; + + let name = encapsulating_node_metadata.persistent_metadata.output_names.remove(start_index); + encapsulating_node_metadata.persistent_metadata.output_names.insert(end_index, name); + encapsulating_node_metadata.persistent_metadata.reference = None; + + // Update the metadata for the encapsulating network + self.unload_outward_wires(&encapsulating_network_path); + self.unload_stack_dependents(&encapsulating_network_path); + + // Node input at the start index is now at the end index + let Some(move_to_end_index) = self + .outward_wires(&encapsulating_network_path) + .and_then(|outward_wires| outward_wires.get(&OutputConnector::node(parent_id, start_index))) + .cloned() + else { + log::error!("Could not get outward wires in reorder_export"); + return; + }; + // Node inputs above the start index should be shifted down one + let last_output_index = self.number_of_outputs(&parent_id, &encapsulating_network_path) - 1; + for shift_output_down in (start_index + 1)..=last_output_index { + let Some(outward_wires) = self + .outward_wires(&encapsulating_network_path) + .and_then(|outward_wires| outward_wires.get(&OutputConnector::node(parent_id, shift_output_down))) + .cloned() + else { + log::error!("Could not get outward wires in reorder_export"); + return; + }; + for downstream_connection in &outward_wires { + self.disconnect_input(downstream_connection, &encapsulating_network_path); + self.create_wire(&OutputConnector::node(parent_id, shift_output_down - 1), downstream_connection, &encapsulating_network_path); + } + } + // Node inputs at or above the end index should be shifted up one + for shift_output_up in (end_index..last_output_index).rev() { + let Some(outward_wires) = self + .outward_wires(&encapsulating_network_path) + .and_then(|outward_wires| outward_wires.get(&OutputConnector::node(parent_id, shift_output_up))) + .cloned() + else { + log::error!("Could not get outward wires in reorder_export"); + return; + }; + for downstream_connection in &outward_wires { + self.disconnect_input(downstream_connection, &encapsulating_network_path); + self.create_wire(&OutputConnector::node(parent_id, shift_output_up + 1), downstream_connection, &encapsulating_network_path); + } + } + + // Move the connections to the moved export after all other ones have been shifted + for downstream_connection in &move_to_end_index { + self.disconnect_input(downstream_connection, &encapsulating_network_path); + self.create_wire(&OutputConnector::node(parent_id, end_index), downstream_connection, &encapsulating_network_path); + } + + // Update the metadata for the current network + self.unload_outward_wires(network_path); + self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); + self.unload_stack_dependents(network_path); + } + + /// The end index is before the import is removed, so moving to the end is the length of the current imports + pub fn reorder_import(&mut self, start_index: usize, mut end_index: usize, network_path: &[NodeId]) { + let mut encapsulating_network_path = network_path.to_vec(); + let Some(parent_id) = encapsulating_network_path.pop() else { + log::error!("Could not reorder import for document network"); + return; + }; + + let Some(encapsulating_network) = self.network_mut(&encapsulating_network_path) else { + log::error!("Could not get nested network in reorder_import"); + return; + }; + let Some(encapsulating_node) = encapsulating_network.nodes.get_mut(&parent_id) else { + log::error!("Could not get encapsulating node in reorder_import"); + return; + }; + + if end_index > start_index { + end_index -= 1; + } + let import = encapsulating_node.inputs.remove(start_index); + encapsulating_node.inputs.insert(end_index, import); + + self.transaction_modified(); + + let Some(encapsulating_node_metadata) = self.node_metadata_mut(&parent_id, &encapsulating_network_path) else { + log::error!("Could not get encapsulating network_metadata in reorder_import"); + return; + }; + + let properties_row = encapsulating_node_metadata.persistent_metadata.input_properties.remove(start_index); + encapsulating_node_metadata.persistent_metadata.input_properties.insert(end_index, properties_row); + encapsulating_node_metadata.persistent_metadata.reference = None; + + // Update the metadata for the outer network + self.unload_outward_wires(&encapsulating_network_path); + self.unload_stack_dependents(&encapsulating_network_path); + + // Node input at the start index is now at the end index + let Some(move_to_end_index) = self + .outward_wires(network_path) + .and_then(|outward_wires| outward_wires.get(&OutputConnector::Import(start_index))) + .cloned() + else { + log::error!("Could not get outward wires in reorder_import"); + return; + }; + // Node inputs above the start index should be shifted down one + let last_import_index = self.number_of_imports(network_path) - 1; + for shift_output_down in (start_index + 1)..=last_import_index { + let Some(outward_wires) = self + .outward_wires(network_path) + .and_then(|outward_wires| outward_wires.get(&OutputConnector::Import(shift_output_down))) + .cloned() + else { + log::error!("Could not get outward wires in reorder_import"); + return; + }; + for downstream_connection in &outward_wires { + self.disconnect_input(downstream_connection, &network_path); + self.create_wire(&OutputConnector::Import(shift_output_down - 1), downstream_connection, &network_path); + } + } + // Node inputs at or above the end index should be shifted up one + for shift_output_up in (end_index..last_import_index).rev() { + let Some(outward_wires) = self + .outward_wires(&network_path) + .and_then(|outward_wires| outward_wires.get(&OutputConnector::Import(shift_output_up))) + .cloned() + else { + log::error!("Could not get outward wires in reorder_import"); + return; + }; + for downstream_connection in &outward_wires { + self.disconnect_input(downstream_connection, &network_path); + self.create_wire(&OutputConnector::Import(shift_output_up + 1), downstream_connection, &network_path); + } + } + + // Move the connections to the moved export after all other ones have been shifted + for downstream_connection in &move_to_end_index { + self.disconnect_input(downstream_connection, &network_path); + self.create_wire(&OutputConnector::Import(end_index), downstream_connection, &network_path); + } + + // Update the metadata for the current network + self.unload_outward_wires(network_path); + self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); + self.unload_stack_dependents(network_path); } /// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts @@ -3701,9 +4150,6 @@ impl NodeNetworkInterface { log::error!("Could not get outward wires in delete_nodes"); return; }; - let Some(network) = self.network(network_path) else { - return; - }; let mut delete_nodes = HashSet::new(); for node_id in &nodes_to_delete { @@ -3730,7 +4176,7 @@ impl NodeNetworkInterface { // Continue traversing over the downstream sibling, if the current node is a sibling to a node that will be deleted and it is a layer else { for deleted_node_id in &nodes_to_delete { - let Some(downstream_node) = network.nodes.get(deleted_node_id) else { continue }; + let Some(downstream_node) = self.document_node(deleted_node_id, network_path) else { continue }; let Some(input) = downstream_node.inputs.first() else { continue }; if let NodeInput::Node { node_id, .. } = input { @@ -3801,13 +4247,8 @@ impl NodeNetworkInterface { // TODO: Add more logic to support retaining preview when removing references. Since there are so many edge cases/possible crashes, for now the preview is ended. self.stop_previewing(network_path); - let Some(network) = self.network(network_path) else { - log::error!("Could not get nested network in remove_references_from_network"); - return false; - }; - // Check whether the being-deleted node's first (primary) input is a node - let reconnect_to_input = network.nodes.get(node_id).and_then(|node| { + let reconnect_to_input = self.document_node(node_id, network_path).and_then(|node| { node.inputs .iter() .find(|input| input.is_exposed_to_frontend(network_path.is_empty())) @@ -3958,6 +4399,43 @@ impl NodeNetworkInterface { self.unload_node_click_targets(node_id, network_path); } + pub fn set_import_export_name(&mut self, name: String, index: ImportOrExport, network_path: &[NodeId]) { + let Some(encapsulating_node) = self.encapsulating_node_metadata_mut(network_path) else { + log::error!("Could not get encapsulating network in set_import_export_name"); + return; + }; + + let name_changed = match index { + ImportOrExport::Import(import_index) => { + let Some(input_properties) = encapsulating_node.persistent_metadata.input_properties.get_mut(import_index) else { + log::error!("Could not get input properties in set_import_export_name"); + return; + }; + // Only return true if the previous value is the same as the current value + input_properties + .input_data + .insert("input_name".to_string(), json!(name)) + .filter(|val| val.as_str().is_some_and(|old_name| *old_name == name)) + .is_none() + } + ImportOrExport::Export(export_index) => { + let Some(export_name) = encapsulating_node.persistent_metadata.output_names.get_mut(export_index) else { + log::error!("Could not get export_name in set_import_export_name"); + return; + }; + if *export_name == name { + false + } else { + *export_name = name; + true + } + } + }; + if name_changed { + self.transaction_modified(); + } + } + pub fn set_pinned(&mut self, node_id: &NodeId, network_path: &[NodeId], pinned: bool) { let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else { log::error!("Could not get node {node_id} in set_pinned"); @@ -4856,7 +5334,7 @@ impl NodeNetworkInterface { // If a non artboard layer is attempted to be connected to the exports, and there is already an artboard connected, then connect the layer to the artboard. if let Some(first_layer) = LayerNodeIdentifier::ROOT_PARENT.children(&self.document_metadata).next() { if parent == LayerNodeIdentifier::ROOT_PARENT - && !self.reference(&layer.to_node(), network_path).is_some_and(|reference| reference == "Artboard") + && !self.reference(&layer.to_node(), network_path).is_some_and(|reference| *reference == Some("Artboard".to_string())) && self.is_artboard(&first_layer.to_node(), network_path) { parent = first_layer; @@ -5217,6 +5695,12 @@ impl Default for TypeSource { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum ImportOrExport { + Import(usize), + Export(usize), +} + /// Represents an input connector with index based on the [`DocumentNode::inputs`] index, not the visible input index #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] pub enum InputConnector { @@ -5332,14 +5816,30 @@ impl Ports { .chain(self.output_ports.iter().map(|(_, click_target)| click_target)) } - pub fn insert_input_port_at_center(&mut self, input_index: usize, center: DVec2) { + pub fn input_ports(&self) -> impl Iterator { + self.input_ports.iter() + } + + pub fn output_ports(&self) -> impl Iterator { + self.output_ports.iter() + } + + fn insert_input_port_at_center(&mut self, input_index: usize, center: DVec2) { let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.)); - self.input_ports.push((input_index, ClickTarget::new(subpath, 0.))); + self.insert_custom_input_port(input_index, ClickTarget::new(subpath, 0.)); } - pub fn insert_output_port_at_center(&mut self, output_index: usize, center: DVec2) { + fn insert_custom_input_port(&mut self, input_index: usize, click_target: ClickTarget) { + self.input_ports.push((input_index, click_target)); + } + + fn insert_output_port_at_center(&mut self, output_index: usize, center: DVec2) { let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.)); - self.output_ports.push((output_index, ClickTarget::new(subpath, 0.))); + self.insert_custom_output_port(output_index, ClickTarget::new(subpath, 0.)); + } + + fn insert_custom_output_port(&mut self, output_index: usize, click_target: ClickTarget) { + self.output_ports.push((output_index, click_target)); } fn insert_node_input(&mut self, input_index: usize, row_index: usize, node_top_left: DVec2) { @@ -5524,15 +6024,12 @@ pub struct NodeNetworkTransientMetadata { #[derive(Debug, Clone)] pub struct ModifyImportExportClickTarget { - // Plus icon that appears below all imports/exports - pub add_import: ClickTarget, - pub add_export: ClickTarget, + // Plus icon that appears below all imports/exports, except in the document network + pub add_import_export: Ports, // Subtract icon that appears when hovering over an import/export - pub remove_imports: Vec, - pub remove_exports: Vec, + pub remove_imports_exports: Ports, // Grip drag icon that appears when hovering over an import/export - pub move_import: Vec, - pub move_export: Vec, + pub reorder_imports_exports: Ports, } #[derive(Debug, Clone)] @@ -5579,18 +6076,182 @@ impl PartialEq for DocumentNodeMetadata { } } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct NumberInputSettings { + pub unit: Option, + pub min: Option, + pub max: Option, + pub step: Option, + pub mode: NumberInputMode, + pub range_min: Option, + pub range_max: Option, + pub is_integer: bool, + pub blank_assist: bool, +} + +impl Default for NumberInputSettings { + fn default() -> Self { + NumberInputSettings { + unit: None, + min: None, + max: None, + step: None, + mode: NumberInputMode::default(), + range_min: None, + range_max: None, + is_integer: false, + blank_assist: true, + } + } +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct Vec2InputSettings { + pub x: String, + pub y: String, + pub unit: String, + pub min: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum WidgetOverride { + None, + Hidden, + String(String), + Number(NumberInputSettings), + Vec2(Vec2InputSettings), + Custom(String), +} + +// TODO: Custom deserialization/serialization to ensure number of properties row matches number of node inputs +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PropertiesRow { + /// A general datastore than can store key value pairs of any types for any input + /// TODO: This could be simplified to just Value, and key value pairs could be stored as the Object variant + pub input_data: HashMap, + // An input can override a widget, which would otherwise be automatically generated from the type + // The string is the identifier to the widget override function stored in INPUT_OVERRIDES + pub widget_override: Option, + // Tool tip when hovering over any widgets created by the properties row + pub tooltip: Option, +} + +impl Default for PropertiesRow { + fn default() -> Self { + "".into() + } +} + +impl PartialEq for PropertiesRow { + fn eq(&self, other: &Self) -> bool { + self.input_data == other.input_data && self.widget_override == other.widget_override + } +} + +impl From<&str> for PropertiesRow { + fn from(input_name: &str) -> Self { + PropertiesRow::with_override(input_name, WidgetOverride::None) + } +} + +// impl From for PropertiesRow { +// fn from(input_name: String) -> Self { +// PropertiesRow::with_override(&input_name, with_override) +// } +// } + +impl PropertiesRow { + pub fn with_override(input_name: &str, widget_override: WidgetOverride) -> Self { + let mut input_data = HashMap::new(); + input_data.insert("input_name".to_string(), Value::String(input_name.to_string())); + match widget_override { + WidgetOverride::None => PropertiesRow { + input_data, + widget_override: None, + tooltip: None, + }, + WidgetOverride::Hidden => PropertiesRow { + input_data, + widget_override: Some("hidden".to_string()), + tooltip: None, + }, + WidgetOverride::String(string_properties) => { + input_data.insert("string_properties".to_string(), Value::String(string_properties)); + PropertiesRow { + input_data, + widget_override: Some("string".to_string()), + tooltip: None, + } + } + WidgetOverride::Number(mut number_properties) => { + if let Some(unit) = number_properties.unit.take() { + input_data.insert("unit".to_string(), json!(unit)); + } + if let Some(min) = number_properties.min.take() { + input_data.insert("min".to_string(), json!(min)); + } + if let Some(max) = number_properties.max.take() { + input_data.insert("max".to_string(), json!(max)); + } + if let Some(step) = number_properties.step.take() { + input_data.insert("step".to_string(), json!(step)); + } + if let Some(range_min) = number_properties.range_min.take() { + input_data.insert("range_min".to_string(), json!(range_min)); + } + if let Some(range_max) = number_properties.range_max.take() { + input_data.insert("range_max".to_string(), json!(range_max)); + } + input_data.insert("mode".to_string(), json!(number_properties.mode)); + input_data.insert("is_integer".to_string(), Value::Bool(number_properties.is_integer)); + input_data.insert("blank_assist".to_string(), Value::Bool(number_properties.blank_assist)); + PropertiesRow { + input_data, + widget_override: Some("number".to_string()), + tooltip: None, + } + } + WidgetOverride::Vec2(vec2_properties) => { + input_data.insert("x".to_string(), json!(vec2_properties.x)); + input_data.insert("y".to_string(), json!(vec2_properties.y)); + input_data.insert("unit".to_string(), json!(vec2_properties.unit)); + if let Some(min) = vec2_properties.min { + input_data.insert("min".to_string(), json!(min)); + } + PropertiesRow { + input_data, + widget_override: Some("vec2".to_string()), + tooltip: None, + } + } + WidgetOverride::Custom(lambda_name) => PropertiesRow { + input_data, + widget_override: Some(lambda_name), + tooltip: None, + }, + } + } + + pub fn with_tooltip(mut self, tooltip: &str) -> Self { + self.tooltip = Some(tooltip.to_string()); + self + } +} + /// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct DocumentNodePersistentMetadata { /// The name of the node definition, as originally set by [`DocumentNodeDefinition`], used to display in the UI and to display the appropriate properties if no display name is set. /// Used during serialization/deserialization to prevent storing implementation or inputs (and possible other fields) if they are the same as the definition. + /// TODO: The reference is removed once the node is modified, since the node now stores its own implementation and inputs. + /// TODO: Implement node versioning so that references to old nodes can be updated to the new node definition. pub reference: Option, /// A name chosen by the user for this instance of the node. Empty indicates no given name, in which case the reference name is displayed to the user in italics. #[serde(default)] pub display_name: String, - /// Input/Output names may not be the same length as the number of inputs/outputs. They are the same as the nested networks Imports/Exports. - /// If the string is empty/DNE, then it uses the type. - pub input_names: Vec, + /// Stores metadata to override the properties in the properties panel for each input. These can either be generated automatically based on the type, or with a custom function. + /// Must match the length of node inputs + pub input_properties: Vec, pub output_names: Vec, /// Indicates to the UI if a primary output should be drawn for this node. /// True for most nodes, but the Split Channels node is an example of a node that has multiple secondary outputs but no primary output. @@ -5614,7 +6275,7 @@ impl Default for DocumentNodePersistentMetadata { DocumentNodePersistentMetadata { reference: None, display_name: String::new(), - input_names: Vec::new(), + input_properties: Vec::new(), output_names: Vec::new(), has_primary_output: true, pinned: false, diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index de134e5ab5..bbff57471e 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -177,7 +177,7 @@ impl<'a> NodeGraphLayer<'a> { /// Node id of a node if it exists in the layer's primary flow pub fn upstream_node_id_from_name(&self, node_name: &str) -> Option { self.horizontal_layer_flow() - .find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| reference == node_name)) + .find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| *reference == Some(node_name.to_string()))) } /// Find all of the inputs of a specific node within the layer's primary flow, up until the next layer is reached. @@ -185,7 +185,7 @@ impl<'a> NodeGraphLayer<'a> { self.horizontal_layer_flow() .skip(1)// Skip self .take_while(|node_id| !self.network_interface.is_layer(node_id,&[])) - .find(|node_id| self.network_interface.reference(node_id,&[]).is_some_and(|reference| reference == node_name)) + .find(|node_id| self.network_interface.reference(node_id,&[]).is_some_and(|reference| *reference == Some(node_name.to_string()))) .and_then(|node_id| self.network_interface.network(&[]).unwrap().nodes.get(&node_id).map(|node| &node.inputs)) } diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 6b86a36064..c3b221549c 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -277,7 +277,7 @@ impl BrushToolData { let Some(reference) = document.network_interface.reference(&node_id, &[]) else { continue; }; - if reference == "Brush" && node_id != layer.to_node() { + if *reference == Some("Brush".to_string()) && node_id != layer.to_node() { let points_input = node.inputs.get(2)?; let Some(TaggedValue::BrushStrokes(strokes)) = points_input.as_value() else { continue; @@ -285,7 +285,7 @@ impl BrushToolData { self.strokes.clone_from(strokes); return Some(layer); - } else if reference == "Transform" { + } else if *reference == Some("Transform".to_string()) { let upstream = document.metadata().upstream_transform(node_id); let pivot = DAffine2::from_translation(upstream.transform_point2(get_current_normalized_pivot(&node.inputs))); self.transform = pivot * get_current_transform(&node.inputs) * pivot.inverse() * self.transform; diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index bfa6592113..a8962bcb1d 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -1,6 +1,6 @@