mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Generalize layers as merge nodes to enable adjustment layers (#1712)
* WIP, backward traversal issues * Fix some tool issues * Remove debugging * Change some indices * WIP: new artboard node * WIP: add artboard node * WIP: Artboard node and create_artboard * WIP: Artboard node implementation complete * WIP: Artboards input for output node * Complete Artboard node * Generalize LayerNodeIdentifier, monitor_nodes support for Artboard node, adjust ResizeArtboard/ClearArtboards, move alias validation to Rust * Fix misaligned artboard click targets * Generalize/clarify create_layer and insert_between * non-negative dimensions for resize_artboard * Show artboards in layer panel * Generalize create_layer for layer output node * Generalize delete_layer/delete_artboard to NodeGraphMessage::DeleteNodes. Fixed upstream flow Iter * remove old primary_input function * Vertical node visuals, remove is_layer function, rename Layer node to Merge node, toggle display as layer exposed_value_count type fix Vertical node visuals, remove is_layer function, rename Layer node to Merge node, toggle display as layer * Fix demo artwork * Layer display context menu * Automatically select artboard, fix warnings * Improvements to context menu and layer invariant enforcement * Remove display_as_layer and update load_structure * Improve load_structure to show more layers, improve FlowIter, improve layer naming, layer rearrangement validation. * Clean up demo artwork using generalized layers * Improve design of Layers panel and graph nodes * MoveSelectedLayersTo rewrite to support generalized layer nodes * Include artboards in deepest_common_ancestor, fix resize_artboard/delete_artboard, sync artboard tool to layer panel * MoveSelectedLayersTo adjustments * Sync non layer node visibility with metadata * Include non layer nodes when moving/creating layer * Fix group layers and get_post_node_with_index * Include non layer nodes in UngroupSelectedLayers * GroupSelected for all selected nodes, UnGroupSelected position adjustments * Add grouping for layers in different folders * Fix hidden layers * Prevent node from connecting to itself, fix undo automatic node insertion, * Fix undo CreateEmptyFolder, fix grouping nested layer nodes * Formatting * Remove test and check if node is layer from network * Fix undo group layers * Check off roadmap * MoveUpstreamSiblingsToChild adjustments * Replace tabs with spaces, remove mut from argument * Final code review pass --------- Co-authored-by: 0hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
beb88d280c
commit
8d83fa7079
41 changed files with 1712 additions and 825 deletions
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
|
@ -40,6 +40,14 @@
|
|||
"eslint.format.enable": true,
|
||||
"eslint.workingDirectories": ["./frontend", "./website/other/bezier-rs-demos", "./website"],
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
// Svelte config
|
||||
"svelte.plugin.svelte.compilerWarnings": {
|
||||
// NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"css-unused-selector": "ignore",
|
||||
"vite-plugin-svelte-css-no-scopable-elements": "ignore",
|
||||
"a11y-no-static-element-interactions": "ignore",
|
||||
"a11y-no-noninteractive-element-interactions": "ignore"
|
||||
},
|
||||
// VS Code config
|
||||
"html.format.wrapLineLength": 200,
|
||||
"files.eol": "\n",
|
||||
|
|
171
Cargo.lock
generated
171
Cargo.lock
generated
|
@ -141,19 +141,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.3.2"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58"
|
||||
checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"core-graphics 0.23.2",
|
||||
"image",
|
||||
"image 0.25.1",
|
||||
"log",
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"objc2 0.5.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"thiserror",
|
||||
"windows-sys 0.48.0",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
|
@ -232,7 +231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb"
|
||||
dependencies = [
|
||||
"event-listener 5.3.0",
|
||||
"event-listener-strategy 0.5.1",
|
||||
"event-listener-strategy 0.5.2",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
@ -245,7 +244,7 @@ checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928"
|
|||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener 5.3.0",
|
||||
"event-listener-strategy 0.5.1",
|
||||
"event-listener-strategy 0.5.2",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
@ -265,9 +264,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.1.1"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1"
|
||||
checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
|
@ -355,9 +354,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.0"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
|
@ -651,23 +650,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68"
|
||||
dependencies = [
|
||||
"block-sys",
|
||||
"objc2",
|
||||
"objc2 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43ff7d91d3c1d568065b06c899777d1e48dcf76103a672a0adbc238a7f247f1e"
|
||||
dependencies = [
|
||||
"objc2 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
|
||||
checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-lock",
|
||||
"async-task",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1529,9 +1535,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3"
|
||||
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
|
||||
dependencies = [
|
||||
"event-listener 5.3.0",
|
||||
"pin-project-lite",
|
||||
|
@ -1548,9 +1554,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.0.2"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
|
@ -1601,9 +1607,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
|||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.29"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7"
|
||||
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
|
@ -2190,7 +2196,7 @@ checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c"
|
|||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"gpu-descriptor-types",
|
||||
"hashbrown 0.14.3",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2253,7 +2259,7 @@ dependencies = [
|
|||
"graph-craft",
|
||||
"graphene-core",
|
||||
"graphene-std",
|
||||
"image",
|
||||
"image 0.24.9",
|
||||
"interpreted-executor",
|
||||
"log",
|
||||
"serde",
|
||||
|
@ -2273,7 +2279,7 @@ dependencies = [
|
|||
"bytemuck",
|
||||
"dyn-any",
|
||||
"glam",
|
||||
"image",
|
||||
"image 0.24.9",
|
||||
"js-sys",
|
||||
"kurbo 0.11.0 (git+https://github.com/linebender/kurbo.git)",
|
||||
"log",
|
||||
|
@ -2309,7 +2315,7 @@ dependencies = [
|
|||
"gpu-executor",
|
||||
"graph-craft",
|
||||
"graphene-core",
|
||||
"image",
|
||||
"image 0.24.9",
|
||||
"image-compare",
|
||||
"js-sys",
|
||||
"log",
|
||||
|
@ -2368,7 +2374,7 @@ dependencies = [
|
|||
"graphene-core",
|
||||
"graphene-std",
|
||||
"graphite-proc-macros",
|
||||
"image",
|
||||
"image 0.24.9",
|
||||
"interpreted-executor",
|
||||
"log",
|
||||
"num_enum 0.6.1",
|
||||
|
@ -2517,9 +2523,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
|||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.3"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
|
@ -2807,9 +2813,9 @@ version = "0.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.3.0",
|
||||
"dispatch",
|
||||
"objc2",
|
||||
"objc2 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2856,6 +2862,18 @@ dependencies = [
|
|||
"jpeg-decoder",
|
||||
"num-traits",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"num-traits",
|
||||
"png",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
|
@ -2865,7 +2883,7 @@ version = "0.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c16b73efbb4b417b4e036cb6f0b0d31c43568a90ab575a4c7611c4396ec1a6b8"
|
||||
dependencies = [
|
||||
"image",
|
||||
"image 0.24.9",
|
||||
"itertools",
|
||||
"rayon",
|
||||
"thiserror",
|
||||
|
@ -2895,7 +2913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.3",
|
||||
"hashbrown 0.14.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
@ -3145,9 +3163,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
version = "0.2.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
|
@ -3833,7 +3851,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d"
|
||||
dependencies = [
|
||||
"objc-sys",
|
||||
"objc2-encode",
|
||||
"objc2-encode 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4b25e1034d0e636cd84707ccdaa9f81243d399196b8a773946dcffec0401659"
|
||||
dependencies = [
|
||||
"objc-sys",
|
||||
"objc2-encode 4.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-app-kit"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb79768a710a9a1798848179edb186d1af7e8a8679f369e4b8d201dd2a034047"
|
||||
dependencies = [
|
||||
"block2 0.5.0",
|
||||
"objc2 0.5.1",
|
||||
"objc2-core-data",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-data"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e092bc42eaf30a08844e6a076938c60751225ec81431ab89f5d1ccd9f958d6c"
|
||||
dependencies = [
|
||||
"block2 0.5.0",
|
||||
"objc2 0.5.1",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3842,6 +3893,22 @@ version = "3.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88658da63e4cc2c8adb1262902cd6af51094df0488b760d6fd27194269c0950a"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfaefe14254871ea16c7d88968c0ff14ba554712a20d76421eec52f0a7fb8904"
|
||||
dependencies = [
|
||||
"block2 0.5.0",
|
||||
"objc2 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc_exception"
|
||||
version = "0.1.2"
|
||||
|
@ -5001,9 +5068,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.198"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
|
||||
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -5021,9 +5088,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.198"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
|
||||
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -5086,9 +5153,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.8.0"
|
||||
version = "3.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c85f8e96d1d6857f13768fcbd895fcb06225510022a2774ed8b5150581847b0"
|
||||
checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"chrono",
|
||||
|
@ -5104,9 +5171,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.8.0"
|
||||
version = "3.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8b3a576c4eb2924262d5951a3b737ccaf16c931e39a2810c36f9a7e25575557"
|
||||
checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
|
@ -5345,9 +5412,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.6"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
|
||||
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
|
@ -5649,7 +5716,7 @@ dependencies = [
|
|||
"glib",
|
||||
"glib-sys",
|
||||
"gtk",
|
||||
"image",
|
||||
"image 0.24.9",
|
||||
"instant",
|
||||
"jni 0.20.0",
|
||||
"lazy_static",
|
||||
|
@ -7118,9 +7185,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wide"
|
||||
version = "0.7.16"
|
||||
version = "0.7.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81a1851a719f11d1d2fea40e15c72f6c00de8c142d7ac47c1441cc7e4d0d5bc6"
|
||||
checksum = "0f0e39d2c603fdc0504b12b458cf1f34e0b937ed2f4f2dc20796e3e86f34e11f"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"safe_arch",
|
||||
|
@ -7615,7 +7682,7 @@ dependencies = [
|
|||
"memmap2",
|
||||
"ndk 0.8.0",
|
||||
"ndk-sys 0.5.0+25.2.9519653",
|
||||
"objc2",
|
||||
"objc2 0.4.1",
|
||||
"once_cell",
|
||||
"orbclient",
|
||||
"percent-encoding",
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -359,54 +359,6 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(miri, ignore)]
|
||||
fn copy_paste_folder() {
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
const FOLDER_ID: NodeId = NodeId(3);
|
||||
|
||||
editor.handle_message(GraphOperationMessage::NewCustomLayer {
|
||||
id: FOLDER_ID,
|
||||
nodes: HashMap::new(),
|
||||
parent: LayerNodeIdentifier::ROOT,
|
||||
insert_index: -1,
|
||||
alias: String::new(),
|
||||
});
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![FOLDER_ID] });
|
||||
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 10., 10.);
|
||||
editor.drag_tool(ToolType::Freehand, 10., 20., 30., 40.);
|
||||
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![FOLDER_ID] });
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
clipboard: Clipboard::Internal,
|
||||
parent: LayerNodeIdentifier::ROOT,
|
||||
insert_index: -1,
|
||||
});
|
||||
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.metadata.all_layers().collect::<Vec<_>>();
|
||||
let [original_folder, original_freehand, original_line, original_ellipse, original_polygon, original_rect] = layers_before_copy[..] else {
|
||||
panic!("Layers before incorrect");
|
||||
};
|
||||
let [_, _, _, folder, freehand, line, ellipse, polygon, rect] = layers_after_copy[..] else {
|
||||
panic!("Layers after incorrect");
|
||||
};
|
||||
assert_eq!(original_folder, folder);
|
||||
assert_eq!(original_freehand, freehand);
|
||||
assert_eq!(original_line, line);
|
||||
assert_eq!(original_ellipse, ellipse);
|
||||
assert_eq!(original_polygon, polygon);
|
||||
assert_eq!(original_rect, rect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(miri, ignore)]
|
||||
/// - create rect, shape and ellipse
|
||||
|
|
|
@ -61,6 +61,7 @@ pub fn default_mapping() -> Mapping {
|
|||
entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes),
|
||||
entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedVisibility),
|
||||
entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedLocked),
|
||||
entry!(KeyDown(KeyL); modifiers=[Alt], action_dispatch=NodeGraphMessage::ToggleSelectedLayers),
|
||||
//
|
||||
// TransformLayerMessage
|
||||
entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
|
||||
|
|
|
@ -5,12 +5,13 @@ use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH};
|
|||
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::node_graph::NodeGraphHandlerData;
|
||||
use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options};
|
||||
use crate::messages::portfolio::document::properties_panel::utility_types::PropertiesPanelMessageHandlerData;
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{is_artboard, is_folder, DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{is_artboard, DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
|
@ -20,6 +21,7 @@ use crate::messages::tool::utility_types::ToolType;
|
|||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::FlowType;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, DocumentNodeMetadata, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graphene_core::raster::BlendMode;
|
||||
use graphene_core::raster::ImageFrame;
|
||||
|
@ -29,7 +31,7 @@ use graphene_core::vector::style::ViewMode;
|
|||
use graphene_core::{concrete, generic, ProtoNodeIdentifier};
|
||||
use graphene_std::wasm_application_io::WasmEditorApi;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
|
||||
use std::vec;
|
||||
|
||||
|
@ -238,7 +240,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
.enumerate()
|
||||
.find_map(|(index, item)| self.selected_nodes.selected_layers(self.metadata()).any(|x| x == item).then_some(index as isize))
|
||||
.unwrap_or(-1);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(GraphOperationMessage::NewCustomLayer {
|
||||
id,
|
||||
nodes: HashMap::new(),
|
||||
|
@ -252,7 +254,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
info!("{:#?}", self.network);
|
||||
}
|
||||
DocumentMessage::DeleteLayer { id } => {
|
||||
responses.add(GraphOperationMessage::DeleteLayer { id });
|
||||
responses.add(NodeGraphMessage::DeleteNodes { node_ids: vec![id], reconnect: true });
|
||||
responses.add_front(BroadcastEvent::ToolAbort);
|
||||
}
|
||||
DocumentMessage::DeleteSelectedLayers => {
|
||||
|
@ -289,7 +291,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
self.network(),
|
||||
&self
|
||||
.network()
|
||||
.upstream_flow_back_from_nodes(vec![node], false)
|
||||
.upstream_flow_back_from_nodes(vec![node], FlowType::UpstreamFlow)
|
||||
.enumerate()
|
||||
.map(|(index, (_, node_id))| (node_id, NodeId(index as u64)))
|
||||
.collect(),
|
||||
|
@ -356,9 +358,87 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
DocumentMessage::GroupSelectedLayers => {
|
||||
let parent = self
|
||||
.metadata()
|
||||
.deepest_common_ancestor(self.selected_nodes.selected_layers(self.metadata()), true)
|
||||
.deepest_common_ancestor(self.selected_nodes.selected_layers(self.metadata()), false)
|
||||
.unwrap_or(LayerNodeIdentifier::ROOT);
|
||||
|
||||
// Cancel grouping layers across different artboards
|
||||
// TODO: Group each set of layers for each artboard separately
|
||||
if parent == LayerNodeIdentifier::ROOT {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move layers in nested unselected folders above the first unselected parent folder
|
||||
let selected_layers = self.selected_nodes.selected_layers(self.metadata()).collect::<Vec<_>>();
|
||||
for layer in selected_layers.clone() {
|
||||
let mut first_unselected_parent_folder = layer.parent(&self.metadata).expect("Layer should always have parent");
|
||||
|
||||
// Find folder in parent child stack
|
||||
loop {
|
||||
// Loop until parent layer is deselected. Note that parent cannot be selected, since it is an ancestor of all selected layers
|
||||
if !selected_layers.iter().any(|selected_layer| *selected_layer == first_unselected_parent_folder) {
|
||||
break;
|
||||
}
|
||||
let Some(new_folder) = first_unselected_parent_folder.parent(&self.metadata) else {
|
||||
log::error!("Layer should always have parent");
|
||||
return;
|
||||
};
|
||||
first_unselected_parent_folder = new_folder;
|
||||
}
|
||||
|
||||
// Don't move nodes above new group folder parent
|
||||
if first_unselected_parent_folder == parent {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Disconnect above and below the old layer location
|
||||
self.disconnect_node(layer, responses);
|
||||
|
||||
// Move disconnected node to folder
|
||||
let folder_position = self
|
||||
.network
|
||||
.nodes
|
||||
.get(&first_unselected_parent_folder.to_node())
|
||||
.expect("Current folder should always exist")
|
||||
.metadata
|
||||
.position;
|
||||
|
||||
let Some(layer_to_move_node_mut) = self.network.nodes.get_mut(&layer.to_node()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
DocumentMessageHandler::disconnect_input(layer_to_move_node_mut, 0);
|
||||
layer_to_move_node_mut.metadata.position = folder_position;
|
||||
|
||||
// Insert node right above the folder
|
||||
// TODO: Use insert layer between message
|
||||
let Some((folder_downstream_node_id, folder_downstream_input_index)) = DocumentMessageHandler::get_downstream_node(&self.network, &self.metadata, first_unselected_parent_folder)
|
||||
else {
|
||||
log::error!("Downstream node should always exist when inserting layer");
|
||||
return;
|
||||
};
|
||||
let downstream_input = self
|
||||
.network
|
||||
.nodes
|
||||
.get_mut(&folder_downstream_node_id)
|
||||
.and_then(|node| node.inputs.get_mut(folder_downstream_input_index));
|
||||
let Some(NodeInput::Node { node_id, .. }) = downstream_input else {
|
||||
log::error!("Downstream node should have a node input");
|
||||
return;
|
||||
};
|
||||
*node_id = layer.to_node();
|
||||
|
||||
// Connect layer primary input to parent folder
|
||||
let Some(layer_node_input) = self.network.nodes.get_mut(&layer.to_node()).and_then(|node| node.inputs.get_mut(0)) else {
|
||||
log::error!("Layer should always have primary input");
|
||||
return;
|
||||
};
|
||||
*layer_node_input = NodeInput::node(first_unselected_parent_folder.to_node(), 0);
|
||||
|
||||
let upstream_shift = IVec2::new(0, 3);
|
||||
let mut modify_inputs = ModifyInputsContext::new(&mut self.network, &mut self.metadata, &mut self.node_graph_handler, responses);
|
||||
modify_inputs.shift_upstream(first_unselected_parent_folder.to_node(), upstream_shift, true);
|
||||
}
|
||||
|
||||
let calculated_insert_index = parent.children(self.metadata()).enumerate().find_map(|(index, direct_child)| {
|
||||
if self.selected_nodes.selected_layers(self.metadata()).any(|selected| selected == direct_child) {
|
||||
return Some(index as isize);
|
||||
|
@ -374,10 +454,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
});
|
||||
|
||||
let folder_id = NodeId(generate_uuid());
|
||||
|
||||
responses.add(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
responses.add(DocumentMessage::DeleteSelectedLayers);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(GraphOperationMessage::NewCustomLayer {
|
||||
id: folder_id,
|
||||
nodes: HashMap::new(),
|
||||
|
@ -385,12 +462,60 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
insert_index: calculated_insert_index.unwrap_or(-1),
|
||||
alias: String::new(),
|
||||
});
|
||||
responses.add(PortfolioMessage::PasteIntoFolder {
|
||||
clipboard: Clipboard::Internal,
|
||||
parent: LayerNodeIdentifier::new_unchecked(folder_id),
|
||||
insert_index: -1,
|
||||
|
||||
// Create a vec of nodes to move with all selected layers in the parent layer child stack, as well as each non layer sibling directly upstream of the selected layer
|
||||
let mut nodes_to_move = Vec::new();
|
||||
|
||||
// Skip over horizontal non layer node chain that feeds into parent
|
||||
let Some(mut current_stack_node_id) = parent.first_child(&self.metadata).and_then(|current_stack_node| Some(current_stack_node.to_node())) else {
|
||||
log::error!("Folder should always have child");
|
||||
return;
|
||||
};
|
||||
let current_stack_node_id = &mut current_stack_node_id;
|
||||
|
||||
loop {
|
||||
let mut current_stack_node = self.network.nodes.get(current_stack_node_id).expect("Current stack node id should always be a node");
|
||||
|
||||
// Check if the current stack node is a selected layer
|
||||
if self
|
||||
.selected_nodes
|
||||
.selected_layers(&self.metadata)
|
||||
.any(|selected_node_id| selected_node_id.to_node() == *current_stack_node_id)
|
||||
{
|
||||
nodes_to_move.push(*current_stack_node_id);
|
||||
|
||||
// Push all non layer sibling nodes directly upstream of the selected layer
|
||||
loop {
|
||||
let Some(NodeInput::Node { node_id, .. }) = current_stack_node.inputs.get(0) else { break };
|
||||
|
||||
let next_node = self.network.nodes.get(node_id).expect("Stack node id should always be a node");
|
||||
|
||||
// If the next node is a layer, immediately break and leave current stack node as the non layer node
|
||||
if next_node.is_layer {
|
||||
break;
|
||||
}
|
||||
|
||||
*current_stack_node_id = *node_id;
|
||||
current_stack_node = next_node;
|
||||
|
||||
nodes_to_move.push(*current_stack_node_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Get next node
|
||||
let Some(NodeInput::Node { node_id, .. }) = current_stack_node.inputs.get(0) else { break };
|
||||
*current_stack_node_id = *node_id;
|
||||
}
|
||||
|
||||
responses.add(GraphOperationMessage::MoveUpstreamSiblingsToChild {
|
||||
new_parent: folder_id,
|
||||
upstream_sibling_ids: nodes_to_move,
|
||||
});
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![folder_id] });
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
DocumentMessage::ImaginateGenerate => responses.add(PortfolioMessage::SubmitGraphRender { document_id }),
|
||||
DocumentMessage::ImaginateRandom { imaginate_node, then_generate } => {
|
||||
|
@ -430,22 +555,96 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
});
|
||||
}
|
||||
DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => {
|
||||
let selected_layers = self.selected_nodes.selected_layers(self.metadata()).collect::<Vec<_>>();
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// Disallow trying to insert into self
|
||||
let selected_layers = self.selected_nodes.selected_layers(self.metadata()).collect::<Vec<_>>();
|
||||
// Disallow trying to insert into self.
|
||||
if selected_layers.iter().any(|&layer| parent.ancestors(self.metadata()).any(|ancestor| ancestor == layer)) {
|
||||
return;
|
||||
}
|
||||
// Artboards can only have the Output node as the parent.
|
||||
if selected_layers.iter().any(|&layer| self.metadata.is_artboard(layer)) && parent != LayerNodeIdentifier::ROOT {
|
||||
return;
|
||||
}
|
||||
// Disallow inserting layers between artboards. Since only artboards can output to Output node, the layer parent cannot be the output.
|
||||
if !selected_layers.iter().any(|&layer| self.metadata.is_artboard(layer)) && parent == LayerNodeIdentifier::ROOT {
|
||||
return;
|
||||
}
|
||||
|
||||
let insert_index = self.update_insert_index(&selected_layers, parent, insert_index);
|
||||
|
||||
responses.add(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
responses.add(DocumentMessage::DeleteSelectedLayers);
|
||||
responses.add(PortfolioMessage::PasteIntoFolder {
|
||||
clipboard: Clipboard::Internal,
|
||||
parent,
|
||||
insert_index,
|
||||
});
|
||||
let binding = self.metadata.shallowest_unique_layers(self.selected_nodes.selected_layers(&self.metadata));
|
||||
let get_last_elements = binding.iter().map(|x| x.last().expect("empty path")).collect::<Vec<_>>();
|
||||
|
||||
let mut run_document_graph_after = false;
|
||||
|
||||
// TODO: The `.collect()` is necessary to avoid borrowing issues with `self`. See if this can be avoided to improve performance.
|
||||
let ordered_last_elements = self.metadata.all_layers().filter(|layer| get_last_elements.contains(&layer)).rev().collect::<Vec<_>>();
|
||||
for layer_to_move in ordered_last_elements {
|
||||
if !run_document_graph_after && self.network.connected_to_output(layer_to_move.to_node()) {
|
||||
run_document_graph_after = true;
|
||||
}
|
||||
|
||||
// Part 1: Disconnect layer to move and reconnect downstream node to upstream sibling if it exists.
|
||||
self.disconnect_node(layer_to_move, responses);
|
||||
|
||||
// Part 2: Reconnect layer_to_move to new parent at insert index.
|
||||
let (post_node_id, pre_node_id, post_node_input_index) = ModifyInputsContext::get_post_node_with_index(&self.network, parent.to_node(), insert_index);
|
||||
|
||||
// Layer_to_move should always correspond to a node.
|
||||
let Some(layer_to_move_node) = self.network.nodes.get(&layer_to_move.to_node()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Move current layer to post node.
|
||||
let post_node = self.network.nodes.get(&post_node_id).expect("Post node id should always refer to a node");
|
||||
let current_position = layer_to_move_node.metadata.position;
|
||||
let new_position = post_node.metadata.position;
|
||||
|
||||
// If moved to top of a layer stack, move to the left of the post node. The stack will be shifted down later.
|
||||
// If moved within a stack, move directly on the post node. The rest of the stack will be shifted down later.
|
||||
let offset_to_post_node = if insert_index == 0 {
|
||||
new_position - current_position - IVec2::new(8, 0)
|
||||
} else {
|
||||
new_position - current_position
|
||||
};
|
||||
|
||||
let mut modify_inputs = ModifyInputsContext::new(&mut self.network, &mut self.metadata, &mut self.node_graph_handler, responses);
|
||||
modify_inputs.shift_upstream(layer_to_move.to_node(), offset_to_post_node, true);
|
||||
|
||||
// Update post_node input to layer_to_move.
|
||||
// TODO: Use insert layer between message
|
||||
let post_node_mut = self.network.nodes.get_mut(&post_node_id).expect("Post node id should always refer to a node");
|
||||
if let Some(NodeInput::Node { node_id, .. }) = post_node_mut.inputs.get_mut(post_node_input_index) {
|
||||
*node_id = layer_to_move.to_node();
|
||||
} else if let Some(node_input) = post_node_mut.inputs.get_mut(post_node_input_index) {
|
||||
*node_input = NodeInput::node(layer_to_move.to_node(), 0);
|
||||
}
|
||||
|
||||
let Some(layer_to_move_node_mut) = self.network.nodes.get_mut(&layer_to_move.to_node()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(pre_node_id) = pre_node_id {
|
||||
// If pre node exists, connect layer_to_move sibling input to that node.
|
||||
if let Some(node_input) = layer_to_move_node_mut.inputs.get_mut(0) {
|
||||
*node_input = NodeInput::node(pre_node_id, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// shift stack down, starting at the moved node.
|
||||
let mut modify_inputs: ModifyInputsContext = ModifyInputsContext::new(&mut self.network, &mut self.metadata, &mut self.node_graph_handler, responses);
|
||||
let shift = IVec2::new(0, 3);
|
||||
modify_inputs.shift_upstream(layer_to_move.to_node(), shift, true);
|
||||
|
||||
self.metadata.load_structure(&self.network, &mut self.selected_nodes);
|
||||
}
|
||||
|
||||
if run_document_graph_after {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
DocumentMessage::NudgeSelectedLayers {
|
||||
delta_x,
|
||||
|
@ -795,56 +994,100 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
DocumentMessage::UngroupSelectedLayers => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let mut run_document_graph_after = false;
|
||||
|
||||
let folder_paths = self.metadata().folders_sorted_by_most_nested(self.selected_nodes.selected_layers(self.metadata()));
|
||||
let mut ungrouped_folders = HashSet::new();
|
||||
|
||||
for folder in folder_paths {
|
||||
// Select all the children of the folder
|
||||
let selected = folder
|
||||
.descendants(self.metadata())
|
||||
.filter_map(|descendant| {
|
||||
if ungrouped_folders.contains(&descendant.to_node()) {
|
||||
return None;
|
||||
if !run_document_graph_after && self.network.connected_to_output(folder.to_node()) {
|
||||
run_document_graph_after = true;
|
||||
}
|
||||
|
||||
// Cannot ungroup artboard
|
||||
let folder_node = self.network.nodes.get(&folder.to_node()).expect("Folder node should always exist");
|
||||
if folder_node.is_artboard() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get first child layer node that feeds into the secondary input for the folder
|
||||
let Some(child_layer_node_id) = folder.first_child(&self.metadata).map(|child_layer| child_layer.to_node()) else {
|
||||
log::error!("Folder should always have a child");
|
||||
return;
|
||||
};
|
||||
|
||||
// Move child_layer stack x position to folder stack
|
||||
let child_layer_node = self.network.nodes.get(&child_layer_node_id).expect("Child node should always exist for layer");
|
||||
let offset = folder_node.metadata.position - child_layer_node.metadata.position;
|
||||
let mut modify_inputs = ModifyInputsContext::new(&mut self.network, &mut self.metadata, &mut self.node_graph_handler, responses);
|
||||
modify_inputs.shift_upstream(child_layer_node_id, offset, true);
|
||||
|
||||
// Set the input for the node downstream of folder to the first layer node
|
||||
let Some((downstream_node_id, downstream_input_index)) = DocumentMessageHandler::get_downstream_node(&self.network, &self.metadata, folder) else {
|
||||
log::error!("Downstream node should always exist when moving layer");
|
||||
continue;
|
||||
};
|
||||
let Some(NodeInput::Node { node_id, .. }) = self
|
||||
.network
|
||||
.nodes
|
||||
.get_mut(&downstream_node_id)
|
||||
.expect("downstream node should always exist")
|
||||
.inputs
|
||||
.get_mut(downstream_input_index)
|
||||
else {
|
||||
log::error!("Could not get downstream node input");
|
||||
continue;
|
||||
};
|
||||
*node_id = child_layer_node_id;
|
||||
|
||||
// Get the node that feeds into the primary input for the folder (if it exists)
|
||||
if let Some(NodeInput::Node { node_id, .. }) = self.network.nodes.get(&folder.to_node()).expect("Folder should always exist").inputs.get(0) {
|
||||
let upstream_sibling_id = *node_id;
|
||||
|
||||
// Get the node at the bottom of the first layer node stack
|
||||
let mut last_child_node_id = child_layer_node_id;
|
||||
loop {
|
||||
let Some(NodeInput::Node { node_id, .. }) = self.network.nodes.get(&last_child_node_id).expect("Child node should always exist").inputs.get(0) else {
|
||||
break;
|
||||
};
|
||||
last_child_node_id = *node_id;
|
||||
}
|
||||
|
||||
let parent = descendant.parent(self.metadata()).expect("No parent");
|
||||
if parent != folder && !ungrouped_folders.contains(&parent.to_node()) {
|
||||
return None;
|
||||
}
|
||||
// Connect the primary input of the bottom layer to the node to the upstream sibling
|
||||
let Some(node_input) = self.network.nodes.get_mut(&last_child_node_id).expect("Last child node should always exist").inputs.get_mut(0) else {
|
||||
log::error!("Could not get last child node primary input");
|
||||
continue;
|
||||
};
|
||||
*node_input = NodeInput::node(upstream_sibling_id, 0);
|
||||
|
||||
Some(descendant.to_node())
|
||||
})
|
||||
.collect::<Vec<NodeId>>();
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: selected });
|
||||
// Shift upstream_sibling down by the height of the child layer stack
|
||||
let top_of_stack = self.network.nodes.get(&child_layer_node_id).expect("Child layer should always exist for child layer id");
|
||||
let bottom_of_stack = self.network.nodes.get(&last_child_node_id).expect("Last child layer should always exist for last child layer id");
|
||||
let target_distance = bottom_of_stack.metadata.position.y - top_of_stack.metadata.position.y;
|
||||
|
||||
// Copy them
|
||||
responses.add(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
let folder_node = self.network.nodes.get(&folder.to_node()).expect("Folder node should always exist");
|
||||
let upstream_sibling_node = self.network.nodes.get(&upstream_sibling_id).expect("Upstream sibling node should always exist");
|
||||
let current_distance = upstream_sibling_node.metadata.position.y - folder_node.metadata.position.y;
|
||||
|
||||
// Paste them into the folder above
|
||||
let insert_index = folder
|
||||
.parent(self.metadata())
|
||||
.unwrap_or(LayerNodeIdentifier::ROOT)
|
||||
.children(self.metadata())
|
||||
.enumerate()
|
||||
.find_map(|(index, item)| (item == folder).then_some(index as isize))
|
||||
.unwrap_or(-1);
|
||||
responses.add(PortfolioMessage::PasteIntoFolder {
|
||||
clipboard: Clipboard::Internal,
|
||||
parent: folder.parent(self.metadata()).unwrap_or(LayerNodeIdentifier::ROOT),
|
||||
insert_index,
|
||||
let y_offset = target_distance - current_distance + 3;
|
||||
let mut modify_inputs = ModifyInputsContext::new(&mut self.network, &mut self.metadata, &mut self.node_graph_handler, responses);
|
||||
modify_inputs.shift_upstream(upstream_sibling_id, IVec2::new(0, y_offset), true);
|
||||
}
|
||||
|
||||
// Delete folder and all horizontal inputs
|
||||
responses.add(NodeGraphMessage::DeleteNodes {
|
||||
node_ids: vec![folder.to_node()],
|
||||
reconnect: true,
|
||||
});
|
||||
|
||||
// Mark the folder for deletion
|
||||
ungrouped_folders.insert(folder.to_node());
|
||||
}
|
||||
|
||||
for id in ungrouped_folders {
|
||||
responses.add(GraphOperationMessage::DeleteLayer { id });
|
||||
if run_document_graph_after {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
responses.add(DocumentMessage::CommitTransaction);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
DocumentMessage::UpdateDocumentTransform { transform } => {
|
||||
self.metadata.document_to_viewport = transform;
|
||||
|
||||
responses.add(DocumentMessage::RenderRulers);
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
@ -901,7 +1144,10 @@ impl DocumentMessageHandler {
|
|||
|
||||
/// Find the deepest layer given in the sorted array (by returning the one which is not a folder from the list of layers under the click location).
|
||||
pub fn find_deepest(&self, node_list: &[LayerNodeIdentifier], network: &NodeNetwork) -> Option<LayerNodeIdentifier> {
|
||||
node_list.iter().find(|&&layer| !is_folder(layer, network)).copied()
|
||||
node_list
|
||||
.iter()
|
||||
.find(|&&layer| !network.nodes.get(&layer.to_node()).map(|node| node.layer_has_child_layers(network)).unwrap_or_default())
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Find any layers sorted by index that are under the given location in viewport space.
|
||||
|
@ -912,7 +1158,12 @@ impl DocumentMessageHandler {
|
|||
/// Find layers under the location in viewport space that was clicked, listed by their depth in the layer tree hierarchy.
|
||||
pub fn click_list(&self, viewport_location: DVec2, network: &NodeNetwork) -> Vec<LayerNodeIdentifier> {
|
||||
let mut node_list = self.click_list_any(viewport_location, network);
|
||||
node_list.truncate(node_list.iter().position(|&layer| !is_folder(layer, network)).unwrap_or(0) + 1);
|
||||
node_list.truncate(
|
||||
node_list
|
||||
.iter()
|
||||
.position(|&layer| !network.nodes.get(&layer.to_node()).map(|node| node.layer_has_child_layers(network)).unwrap_or_default())
|
||||
.unwrap_or(0) + 1,
|
||||
);
|
||||
node_list
|
||||
}
|
||||
|
||||
|
@ -1134,23 +1385,109 @@ impl DocumentMessageHandler {
|
|||
self.saved_hash = None;
|
||||
}
|
||||
}
|
||||
// TODO: Replace with disconnect message
|
||||
pub fn disconnect_input(layer_to_disconnect_node: &mut DocumentNode, input_index: usize) {
|
||||
let Some(node_type) = resolve_document_node_type(&layer_to_disconnect_node.name) else {
|
||||
warn!("Node {} not in library", layer_to_disconnect_node.name);
|
||||
return;
|
||||
};
|
||||
let Some(existing_input) = layer_to_disconnect_node.inputs.get_mut(input_index) else {
|
||||
warn!("Node does not have and input at the selected index");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut default_input = node_type.inputs[input_index].default.clone();
|
||||
if let NodeInput::Value { exposed, .. } = &mut default_input {
|
||||
*exposed = existing_input.is_exposed();
|
||||
}
|
||||
*existing_input = default_input;
|
||||
}
|
||||
|
||||
pub fn get_downstream_node(network: &NodeNetwork, metadata: &DocumentMetadata, layer_to_move: LayerNodeIdentifier) -> Option<(NodeId, usize)> {
|
||||
let mut downstream_layer = None;
|
||||
if let Some(previous_sibling) = layer_to_move.previous_sibling(metadata) {
|
||||
downstream_layer = Some((previous_sibling.to_node(), false))
|
||||
} else if let Some(parent) = layer_to_move.parent(metadata) {
|
||||
downstream_layer = Some((parent.to_node(), true))
|
||||
};
|
||||
|
||||
// Downstream layer should always exist
|
||||
let Some((downstream_layer_node_id, downstream_layer_is_parent)) = downstream_layer else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Horizontal traversal if layer_to_move is the top of its layer stack, primary traversal if not
|
||||
let flow_type = if downstream_layer_is_parent { FlowType::HorizontalFlow } else { FlowType::PrimaryFlow };
|
||||
|
||||
network
|
||||
.upstream_flow_back_from_nodes(vec![downstream_layer_node_id], flow_type)
|
||||
.find(|(node, node_id)| {
|
||||
// Get secondary input only if it is the downstream_layer_node_id, the parent of layer to move, and a layer node (parent might be output)
|
||||
let is_parent_layer = downstream_layer_is_parent && downstream_layer_node_id == *node_id && node.is_layer;
|
||||
let node_input_index = if is_parent_layer { 1 } else { 0 };
|
||||
|
||||
node.inputs.get(node_input_index).is_some_and(|node_input| {
|
||||
if let NodeInput::Node { node_id, .. } = node_input {
|
||||
*node_id == layer_to_move.to_node()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
})
|
||||
.map(|(downstream_node, downstream_node_id)| {
|
||||
let is_parent_layer = downstream_layer_is_parent && downstream_layer_node_id == downstream_node_id && downstream_node.is_layer;
|
||||
let downstream_input_index = if is_parent_layer { 1 } else { 0 };
|
||||
|
||||
(downstream_node_id, downstream_input_index)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: move into message
|
||||
pub fn disconnect_node(&mut self, layer_to_disconnect: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||
let Some((downstream_node_id, downstream_input_index)) = DocumentMessageHandler::get_downstream_node(&self.network, &self.metadata, layer_to_disconnect) else {
|
||||
log::error!("Downstream node should always exist when moving layer");
|
||||
return;
|
||||
};
|
||||
|
||||
let layer_to_move_sibling_input = self.network.nodes.get(&layer_to_disconnect.to_node()).and_then(|node| node.inputs.get(0));
|
||||
if let Some(NodeInput::Node { node_id, .. }) = layer_to_move_sibling_input {
|
||||
let upstream_sibling_id = node_id.clone();
|
||||
let Some(downstream_node) = self.network.nodes.get_mut(&downstream_node_id) else { return };
|
||||
|
||||
if let Some(NodeInput::Node { node_id, .. }) = downstream_node.inputs.get_mut(downstream_input_index) {
|
||||
*node_id = upstream_sibling_id;
|
||||
}
|
||||
|
||||
let upstream_shift = IVec2::new(0, -3);
|
||||
let mut modify_inputs = ModifyInputsContext::new(&mut self.network, &mut self.metadata, &mut self.node_graph_handler, responses);
|
||||
|
||||
modify_inputs.shift_upstream(upstream_sibling_id, upstream_shift, true);
|
||||
} else {
|
||||
// Disconnect node directly downstream if upstream sibling doesn't exist
|
||||
let Some(downstream_node) = self.network.nodes.get_mut(&downstream_node_id) else { return };
|
||||
DocumentMessageHandler::disconnect_input(downstream_node, downstream_input_index);
|
||||
}
|
||||
|
||||
let Some(to_move) = self.network.nodes.get_mut(&layer_to_disconnect.to_node()) else { return };
|
||||
DocumentMessageHandler::disconnect_input(to_move, 0);
|
||||
}
|
||||
/// When working with an insert index, deleting the layers may cause the insert index to point to a different location (if the layer being deleted was located before the insert index).
|
||||
///
|
||||
/// This function updates the insert index so that it points to the same place after the specified `layers` are deleted.
|
||||
fn update_insert_index(&self, layers: &[LayerNodeIdentifier], parent: LayerNodeIdentifier, insert_index: isize) -> isize {
|
||||
fn update_insert_index(&self, layers: &[LayerNodeIdentifier], parent: LayerNodeIdentifier, insert_index: isize) -> usize {
|
||||
let take_amount = if insert_index < 0 { usize::MAX } else { insert_index as usize };
|
||||
let layer_ids_above = parent.children(self.metadata()).take(take_amount);
|
||||
layer_ids_above.filter(|layer_id| !layers.contains(layer_id)).count() as isize
|
||||
layer_ids_above.filter(|layer_id| !layers.contains(layer_id)).count() as usize
|
||||
}
|
||||
|
||||
/// Finds the parent folder which, based on the current selections, should be the container of any newly added layers.
|
||||
pub fn new_layer_parent(&self) -> LayerNodeIdentifier {
|
||||
self.metadata()
|
||||
.deepest_common_ancestor(self.selected_nodes.selected_layers(self.metadata()), false)
|
||||
.deepest_common_ancestor(self.selected_nodes.selected_layers(self.metadata()), true)
|
||||
.unwrap_or_else(|| self.metadata().active_artboard())
|
||||
}
|
||||
|
||||
/// Loads layer resources such as creating the blob URLs for the images and loading all of the fonts in the document
|
||||
/// Loads layer resources such as creating the blob URLs for the images and loading all of the fonts in the document.
|
||||
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>) {
|
||||
let mut fonts = HashSet::new();
|
||||
for (_node_id, node) in self.network.recursive_nodes() {
|
||||
|
@ -1572,7 +1909,7 @@ impl DocumentMessageHandler {
|
|||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
//
|
||||
IconButton::new("NewLayer", 24)
|
||||
.tooltip("New Folder/Layer")
|
||||
.tooltip("New Layer")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
|
||||
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
|
||||
.widget_holder(),
|
||||
|
@ -1649,7 +1986,6 @@ impl DocumentMessageHandler {
|
|||
|
||||
// If moving down, insert below this layer. If moving up, insert above this layer.
|
||||
let insert_index = if relative_index_offset < 0 { neighbor_index } else { neighbor_index + 1 } as isize;
|
||||
|
||||
responses.add(DocumentMessage::MoveSelectedLayersTo { parent, insert_index });
|
||||
}
|
||||
|
||||
|
|
|
@ -64,6 +64,10 @@ pub enum GraphOperationMessage {
|
|||
layer: LayerNodeIdentifier,
|
||||
strokes: Vec<BrushStroke>,
|
||||
},
|
||||
MoveUpstreamSiblingsToChild {
|
||||
new_parent: NodeId,
|
||||
upstream_sibling_ids: Vec<NodeId>,
|
||||
},
|
||||
NewArtboard {
|
||||
id: NodeId,
|
||||
artboard: Artboard,
|
||||
|
@ -100,12 +104,6 @@ pub enum GraphOperationMessage {
|
|||
location: IVec2,
|
||||
dimensions: IVec2,
|
||||
},
|
||||
DeleteLayer {
|
||||
id: NodeId,
|
||||
},
|
||||
DeleteArtboard {
|
||||
id: NodeId,
|
||||
},
|
||||
ClearArtboards,
|
||||
NewSvg {
|
||||
id: NodeId,
|
||||
|
|
|
@ -103,10 +103,72 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
|
|||
modify_inputs.brush_modify(strokes);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::MoveUpstreamSiblingsToChild { new_parent, upstream_sibling_ids } => {
|
||||
// Start with the furthest upstream node, move it as a child of the new folder, and continue downstream for each layer in vec
|
||||
for node_to_move in upstream_sibling_ids.iter().rev() {
|
||||
// Connect pre node to post node, or disconnect pre node if post node doesn't exist
|
||||
let mut pre_node_id = new_parent;
|
||||
loop {
|
||||
let Some(NodeInput::Node { node_id, .. }) = document_network.nodes.get(&pre_node_id).and_then(|node| node.inputs.get(0)) else {
|
||||
log::error!("End of stack should never be reached");
|
||||
return;
|
||||
};
|
||||
if *node_id == *node_to_move {
|
||||
break;
|
||||
}
|
||||
pre_node_id = *node_id;
|
||||
}
|
||||
|
||||
if let Some(NodeInput::Node { node_id, .. }) = document_network.nodes.get(&node_to_move).and_then(|node| node.inputs.get(0)) {
|
||||
let post_node_id = *node_id;
|
||||
let Some(NodeInput::Node { node_id, .. }) = document_network.nodes.get_mut(&pre_node_id).and_then(|node| node.inputs.get_mut(0)) else {
|
||||
log::error!("Pre node should always have primary input");
|
||||
return;
|
||||
};
|
||||
*node_id = post_node_id;
|
||||
} else {
|
||||
DocumentMessageHandler::disconnect_input(document_network.nodes.get_mut(&pre_node_id).expect("Upstream sibling should always exist"), 0);
|
||||
}
|
||||
|
||||
// Connect upstream sibling to the secondary input of the parent
|
||||
let Some(parent_secondary_input) = document_network.nodes.get(&new_parent).and_then(|node| node.inputs.get(1)) else {
|
||||
log::error!("Could not get child node input for current node");
|
||||
return;
|
||||
};
|
||||
|
||||
// Insert upstream_sibling_node at top of group stack
|
||||
if let NodeInput::Node { node_id, .. } = parent_secondary_input {
|
||||
// If there is already a node at the top of the stack, insert upstream_sibling_node in between
|
||||
let current_child = *node_id;
|
||||
let Some(upstream_sibling_input) = document_network.nodes.get_mut(&node_to_move).and_then(|node| node.inputs.get_mut(0)) else {
|
||||
log::error!("Could not get upstream sibling node input");
|
||||
return;
|
||||
};
|
||||
*upstream_sibling_input = NodeInput::node(current_child, 0);
|
||||
}
|
||||
|
||||
let Some(parent_secondary_input_mut) = document_network.nodes.get_mut(&new_parent).and_then(|node| node.inputs.get_mut(1)) else {
|
||||
log::error!("Could not get child node input for current node");
|
||||
return;
|
||||
};
|
||||
|
||||
*parent_secondary_input_mut = NodeInput::node(*node_to_move, 0);
|
||||
}
|
||||
|
||||
let Some(most_upstream_sibling) = upstream_sibling_ids.last() else {
|
||||
return;
|
||||
};
|
||||
DocumentMessageHandler::disconnect_input(document_network.nodes.get_mut(&most_upstream_sibling).expect("Upstream sibling should always exist"), 0);
|
||||
|
||||
let top_of_stack = upstream_sibling_ids.first().expect("Upstream nodes to move cannot be empty");
|
||||
let upstream_shift = IVec2::new(-8, 0);
|
||||
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
|
||||
modify_inputs.shift_upstream(*top_of_stack, upstream_shift, true);
|
||||
}
|
||||
GraphOperationMessage::NewArtboard { id, artboard } => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
|
||||
if let Some(layer) = modify_inputs.create_layer(id, modify_inputs.document_network.original_outputs()[0].node_id, 0, 0) {
|
||||
modify_inputs.insert_artboard(artboard, layer);
|
||||
if let Some(artboard_id) = modify_inputs.create_artboard(id, artboard) {
|
||||
responses.add_front(NodeGraphMessage::SelectedNodesSet { nodes: vec![artboard_id] });
|
||||
}
|
||||
load_network_structure(document_network, document_metadata, selected_nodes, collapsed);
|
||||
}
|
||||
|
@ -128,8 +190,6 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
|
|||
insert_index,
|
||||
alias,
|
||||
} => {
|
||||
trace!("Inserting new layer {id} as a child of {parent:?} at index {insert_index}");
|
||||
|
||||
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
|
||||
|
||||
if let Some(layer) = modify_inputs.create_layer_with_insert_index(id, insert_index, parent) {
|
||||
|
@ -164,11 +224,13 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
|
|||
|
||||
if let Some(layer_node) = modify_inputs.document_network.nodes.get_mut(&layer) {
|
||||
if let Some(&input) = new_ids.get(&NodeId(0)) {
|
||||
layer_node.inputs[0] = NodeInput::node(input, 0)
|
||||
layer_node.inputs[1] = NodeInput::node(input, 0);
|
||||
}
|
||||
}
|
||||
|
||||
modify_inputs.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
} else {
|
||||
error!("Creating new custom layer failed");
|
||||
}
|
||||
|
||||
load_network_structure(document_network, document_metadata, selected_nodes, collapsed);
|
||||
|
@ -199,37 +261,20 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
|
|||
modify_inputs.resize_artboard(location, dimensions);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::DeleteLayer { id } => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
|
||||
modify_inputs.delete_layer(id, selected_nodes, false);
|
||||
load_network_structure(document_network, document_metadata, selected_nodes, collapsed);
|
||||
}
|
||||
GraphOperationMessage::DeleteArtboard { id } => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
|
||||
if let Some(artboard_id) = modify_inputs.document_network.nodes.get(&id).and_then(|node| node.inputs[0].as_node()) {
|
||||
modify_inputs.delete_artboard(artboard_id, selected_nodes);
|
||||
} else {
|
||||
warn!("Artboard does not exist");
|
||||
}
|
||||
modify_inputs.delete_layer(id, selected_nodes, true);
|
||||
load_network_structure(document_network, document_metadata, selected_nodes, collapsed);
|
||||
}
|
||||
GraphOperationMessage::ClearArtboards => {
|
||||
let mut modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
|
||||
let layer_nodes = modify_inputs.document_network.nodes.iter().filter(|(_, node)| node.is_layer()).map(|(id, _)| *id).collect::<Vec<_>>();
|
||||
for layer in layer_nodes {
|
||||
let artboards = modify_inputs
|
||||
.document_network
|
||||
.upstream_flow_back_from_nodes(vec![layer], true)
|
||||
.filter_map(|(node, _id)| if node.is_artboard() { Some(_id) } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
if artboards.is_empty() {
|
||||
continue;
|
||||
}
|
||||
for artboard in artboards {
|
||||
modify_inputs.delete_artboard(artboard, selected_nodes);
|
||||
}
|
||||
modify_inputs.delete_layer(layer, selected_nodes, true);
|
||||
let modify_inputs = ModifyInputsContext::new(document_network, document_metadata, node_graph, responses);
|
||||
let artboard_nodes = modify_inputs
|
||||
.document_network
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|(_, node)| node.is_artboard())
|
||||
.map(|(id, _)| *id)
|
||||
.collect::<Vec<_>>();
|
||||
for artboard in artboard_nodes {
|
||||
responses.add(NodeGraphMessage::DeleteNodes {
|
||||
node_ids: vec![artboard],
|
||||
reconnect: true,
|
||||
});
|
||||
}
|
||||
load_network_structure(document_network, document_metadata, selected_nodes, collapsed);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use bezier_rs::Subpath;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput, NodeNetwork};
|
||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
|
@ -73,7 +72,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
let mut document = Self::new(document_network, document_metadata, node_graph, responses);
|
||||
|
||||
let mut id = id;
|
||||
while !document.document_network.nodes.get(&id)?.is_layer() {
|
||||
while !document.document_network.nodes.get(&id)?.is_layer {
|
||||
id = document.outwards_links.get(&id)?.first().copied()?;
|
||||
}
|
||||
|
||||
|
@ -87,18 +86,28 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
update_input(&mut document_node.inputs, node_id, self.document_metadata);
|
||||
}
|
||||
|
||||
pub fn insert_between(&mut self, id: NodeId, pre: NodeOutput, post: NodeOutput, mut node: DocumentNode, input: usize, output: usize, shift_upstream: IVec2) -> Option<NodeId> {
|
||||
pub fn insert_between(
|
||||
&mut self,
|
||||
id: NodeId,
|
||||
mut new_node: DocumentNode,
|
||||
new_node_input: NodeInput,
|
||||
new_node_input_index: usize,
|
||||
post_node_id: NodeId,
|
||||
post_node_input: NodeInput,
|
||||
post_node_input_index: usize,
|
||||
shift_upstream: IVec2,
|
||||
) -> Option<NodeId> {
|
||||
assert!(!self.document_network.nodes.contains_key(&id), "Creating already existing node");
|
||||
let pre_node = self.document_network.nodes.get_mut(&pre.node_id)?;
|
||||
node.metadata.position = pre_node.metadata.position;
|
||||
let pre_node = self.document_network.nodes.get_mut(&new_node_input.as_node().expect("Input should reference a node"))?;
|
||||
new_node.metadata.position = pre_node.metadata.position;
|
||||
|
||||
let post_node = self.document_network.nodes.get_mut(&post.node_id)?;
|
||||
node.inputs[input] = NodeInput::node(pre.node_id, pre.node_output_index);
|
||||
post_node.inputs[post.node_output_index] = NodeInput::node(id, output);
|
||||
let post_node = self.document_network.nodes.get_mut(&post_node_id)?;
|
||||
new_node.inputs[new_node_input_index] = new_node_input;
|
||||
post_node.inputs[post_node_input_index] = post_node_input;
|
||||
|
||||
self.document_network.nodes.insert(id, node);
|
||||
self.document_network.nodes.insert(id, new_node);
|
||||
|
||||
self.shift_upstream(id, shift_upstream);
|
||||
self.shift_upstream(id, shift_upstream, false);
|
||||
|
||||
Some(id)
|
||||
}
|
||||
|
@ -114,70 +123,113 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
Some(new_id)
|
||||
}
|
||||
|
||||
pub fn skip_artboards(&self, output: &mut NodeOutput) -> Option<(NodeId, usize)> {
|
||||
while let NodeInput::Node { node_id, output_index, .. } = &self.document_network.nodes.get(&output.node_id)?.inputs[output.node_output_index] {
|
||||
let sibling_node = self.document_network.nodes.get(node_id)?;
|
||||
if sibling_node.name != "Artboard" {
|
||||
return Some((*node_id, *output_index));
|
||||
/// Starts at any folder, or the output, and skips layer nodes based on insert_index. Non layer nodes are always skipped. Returns the post node id, pre node id, and the input index.
|
||||
/// -----> Post node input_index: 0
|
||||
/// | if skip_layer_nodes == 0, return (Post node, Some(Layer1), 1)
|
||||
/// -> Layer1 input_index: 1
|
||||
/// ↑ if skip_layer_nodes == 1, return (Layer1, Some(Layer2), 0)
|
||||
/// -> Layer2 input_index: 2
|
||||
/// ↑
|
||||
/// -> NonLayerNode
|
||||
/// ↑ if skip_layer_nodes == 2, return (NonLayerNode, Some(Layer3), 0)
|
||||
/// -> Layer3 input_index: 3
|
||||
/// if skip_layer_nodes == 3, return (Layer3, None, 0)
|
||||
pub fn get_post_node_with_index(network: &NodeNetwork, mut post_node_id: NodeId, insert_index: usize) -> (NodeId, Option<NodeId>, usize) {
|
||||
let mut post_node_input_index = if post_node_id == NodeId(0) { 0 } else { 1 };
|
||||
|
||||
// Skip layers based on skip_layer_nodes, which inserts the new layer at a certain index of the layer stack.
|
||||
let mut current_index = 0;
|
||||
loop {
|
||||
if current_index == insert_index {
|
||||
break;
|
||||
}
|
||||
*output = NodeOutput::new(*node_id, *output_index)
|
||||
let next_node_in_stack_id = network
|
||||
.nodes
|
||||
.get(&post_node_id)
|
||||
.expect("Post node should always exist")
|
||||
.inputs
|
||||
.get(post_node_input_index)
|
||||
.and_then(|input| if let NodeInput::Node { node_id, .. } = input { Some(node_id.clone()) } else { None });
|
||||
|
||||
if let Some(next_node_in_stack_id) = next_node_in_stack_id {
|
||||
// Only increment index for layer nodes
|
||||
let next_node_in_stack = network.nodes.get(&next_node_in_stack_id).expect("Stack node should always exist");
|
||||
if next_node_in_stack.is_layer {
|
||||
current_index += 1;
|
||||
}
|
||||
|
||||
post_node_id = next_node_in_stack_id;
|
||||
|
||||
// Input as a sibling to the Layer node above
|
||||
post_node_input_index = 0;
|
||||
} else {
|
||||
log::error!("Error creating layer: insert_index out of bounds");
|
||||
break;
|
||||
};
|
||||
}
|
||||
None
|
||||
|
||||
// Move post_node to the end of the non layer chain that feeds into post_node, such that pre_node is the layer node at index 1 + insert_index
|
||||
let mut post_node = network.nodes.get(&post_node_id).expect("Post node should always exist");
|
||||
let mut pre_node_id = post_node
|
||||
.inputs
|
||||
.get(post_node_input_index)
|
||||
.and_then(|input| if let NodeInput::Node { node_id, .. } = input { Some(node_id.clone()) } else { None });
|
||||
|
||||
// Skip until pre_node is either a layer or does not exist
|
||||
while let Some(pre_node_id_value) = pre_node_id {
|
||||
let pre_node = network.nodes.get(&pre_node_id_value).expect("pre_node_id should be a layer");
|
||||
if !pre_node.is_layer {
|
||||
post_node = pre_node;
|
||||
post_node_id = pre_node_id_value;
|
||||
pre_node_id = post_node
|
||||
.inputs
|
||||
.get(0)
|
||||
.and_then(|input| if let NodeInput::Node { node_id, .. } = input { Some(node_id.clone()) } else { None });
|
||||
post_node_input_index = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(post_node_id, pre_node_id, post_node_input_index)
|
||||
}
|
||||
|
||||
pub fn create_layer(&mut self, new_id: NodeId, output_node_id: NodeId, input_index: usize, skip_layer_nodes: usize) -> Option<NodeId> {
|
||||
pub fn create_layer(&mut self, new_id: NodeId, output_node_id: NodeId, skip_layer_nodes: usize) -> Option<NodeId> {
|
||||
assert!(!self.document_network.nodes.contains_key(&new_id), "Creating already existing layer");
|
||||
|
||||
let mut output = NodeOutput::new(output_node_id, input_index);
|
||||
let mut sibling_layer = None;
|
||||
let mut shift = IVec2::new(0, 3);
|
||||
// Locate the node output of the first sibling layer to the new layer
|
||||
if let Some((node_id, output_index)) = self.skip_artboards(&mut output) {
|
||||
let sibling_node = self.document_network.nodes.get(&node_id)?;
|
||||
if sibling_node.is_layer() {
|
||||
// There is already a layer node
|
||||
sibling_layer = Some(NodeOutput::new(node_id, 0));
|
||||
} else {
|
||||
// The user has connected another node to the output. Insert a layer node between the output and the node.
|
||||
let node = resolve_document_node_type("Layer").expect("Layer node").default_document_node();
|
||||
let node_id = self.insert_between(NodeId(generate_uuid()), NodeOutput::new(node_id, output_index), output, node, 0, 0, IVec2::new(-8, 0))?;
|
||||
sibling_layer = Some(NodeOutput::new(node_id, 0));
|
||||
}
|
||||
// Get the node which the new layer will output to (post node). First check if the output_node_id is the Output node, and set the output_node_id to the top-most artboard,
|
||||
// if there is one. Then skip layers based on skip_layer_nodes from the post_node.
|
||||
// TODO: Smarter placement of layers into artboards https://github.com/GraphiteEditor/Graphite/issues/1507
|
||||
|
||||
// Skip some layer nodes
|
||||
for _ in 0..skip_layer_nodes {
|
||||
if let Some(old_sibling) = &sibling_layer {
|
||||
output = NodeOutput::new(old_sibling.node_id, 1);
|
||||
sibling_layer = self.document_network.nodes.get(&old_sibling.node_id)?.inputs[1].as_node().map(|node| NodeOutput::new(node, 0));
|
||||
shift = IVec2::new(0, 3);
|
||||
let mut post_node_id = output_node_id;
|
||||
if post_node_id == NodeId(0) {
|
||||
// Check if an artboard is connected, and switch post node to the artboard.
|
||||
if let Some(NodeInput::Node { node_id, .. }) = &self.document_network.nodes.get(&post_node_id).expect("Output node should always exist").inputs.get(0) {
|
||||
let input_node = self.document_network.nodes.get(&node_id).expect("First input node should exist");
|
||||
if input_node.is_artboard() {
|
||||
post_node_id = *node_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at top of stack
|
||||
}
|
||||
let (post_node_id, pre_node_id, post_node_input_index) = Self::get_post_node_with_index(self.document_network, post_node_id, skip_layer_nodes);
|
||||
let new_layer_node = resolve_document_node_type("Merge").expect("Merge node").default_document_node();
|
||||
if let Some(pre_node_id) = pre_node_id {
|
||||
self.insert_between(
|
||||
new_id,
|
||||
new_layer_node,
|
||||
NodeInput::node(pre_node_id, 0),
|
||||
0, // pre_node is a sibling so it connects to the first input
|
||||
post_node_id,
|
||||
NodeInput::node(new_id, 0),
|
||||
post_node_input_index,
|
||||
IVec2::new(0, 3),
|
||||
);
|
||||
} else {
|
||||
shift = IVec2::new(-8, 3);
|
||||
let offset = if post_node_input_index == 1 { IVec2::new(-8, 3) } else { IVec2::new(0, 3) };
|
||||
self.insert_node_before(new_id, post_node_id, post_node_input_index, new_layer_node, offset);
|
||||
}
|
||||
|
||||
// Create node
|
||||
let layer_node = resolve_document_node_type("Layer").expect("Layer node").default_document_node();
|
||||
let new_id = if let Some(sibling_layer) = sibling_layer {
|
||||
self.insert_between(new_id, sibling_layer, output, layer_node, 1, 0, shift)
|
||||
} else {
|
||||
self.insert_node_before(new_id, output.node_id, output.node_output_index, layer_node, shift)
|
||||
};
|
||||
|
||||
// Update the document metadata structure
|
||||
if let Some(new_id) = new_id {
|
||||
let parent = if self.document_network.nodes.get(&output_node_id).is_some_and(|node| node.is_layer()) {
|
||||
LayerNodeIdentifier::new(output_node_id, self.document_network)
|
||||
} else {
|
||||
LayerNodeIdentifier::ROOT
|
||||
};
|
||||
let new_child = LayerNodeIdentifier::new(new_id, self.document_network);
|
||||
parent.push_front_child(self.document_metadata, new_child);
|
||||
}
|
||||
|
||||
new_id
|
||||
Some(new_id)
|
||||
}
|
||||
|
||||
pub fn create_layer_with_insert_index(&mut self, new_id: NodeId, insert_index: isize, parent: LayerNodeIdentifier) -> Option<NodeId> {
|
||||
|
@ -188,13 +240,17 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
} else {
|
||||
parent.to_node()
|
||||
};
|
||||
self.create_layer(new_id, output_node_id, 0, skip_layer_nodes)
|
||||
self.create_layer(new_id, output_node_id, skip_layer_nodes)
|
||||
}
|
||||
|
||||
pub fn insert_artboard(&mut self, artboard: Artboard, layer: NodeId) -> Option<NodeId> {
|
||||
/// Creates an artboard that outputs to the output node.
|
||||
pub fn create_artboard(&mut self, new_id: NodeId, artboard: Artboard) -> Option<NodeId> {
|
||||
let output_node_id = self.document_network.original_outputs()[0].node_id;
|
||||
|
||||
let artboard_node = resolve_document_node_type("Artboard").expect("Node").to_document_node_default_inputs(
|
||||
[
|
||||
None,
|
||||
Some(NodeInput::value(TaggedValue::ArtboardGroup(graphene_std::ArtboardGroup::EMPTY), true)),
|
||||
Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_core::GraphicGroup::EMPTY), true)),
|
||||
Some(NodeInput::value(TaggedValue::IVec2(artboard.location), false)),
|
||||
Some(NodeInput::value(TaggedValue::IVec2(artboard.dimensions), false)),
|
||||
Some(NodeInput::value(TaggedValue::Color(artboard.background), false)),
|
||||
|
@ -202,10 +258,35 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
],
|
||||
Default::default(),
|
||||
);
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
self.insert_node_before(NodeId(generate_uuid()), layer, 0, artboard_node, IVec2::new(-8, 0))
|
||||
}
|
||||
|
||||
// Get node that feeds into output. If it exists, connect the new artboard node in between. Else connect the new artboard directly to output.
|
||||
let output_node_primary_input = self.document_network.nodes.get(&output_node_id)?.inputs.get(0);
|
||||
let created_node_id = if let NodeInput::Node { node_id, .. } = &output_node_primary_input? {
|
||||
let pre_node = self.document_network.nodes.get(node_id)?;
|
||||
// If the node currently connected the Output is an artboard, connect to input 0 (Artboards input) of the new artboard. Else connect to the Over input.
|
||||
let artboard_input_index = if pre_node.is_artboard() { 0 } else { 1 };
|
||||
|
||||
self.insert_between(
|
||||
new_id,
|
||||
artboard_node,
|
||||
NodeInput::node(*node_id, 0),
|
||||
artboard_input_index,
|
||||
output_node_id,
|
||||
NodeInput::node(new_id, 0),
|
||||
0,
|
||||
IVec2::new(0, 3),
|
||||
)
|
||||
} else {
|
||||
self.insert_node_before(new_id, output_node_id, 0, artboard_node, IVec2::new(-8, 3))
|
||||
};
|
||||
|
||||
if let Some(new_id) = created_node_id {
|
||||
let new_child = LayerNodeIdentifier::new_unchecked(new_id);
|
||||
LayerNodeIdentifier::ROOT.push_front_child(self.document_metadata, new_child);
|
||||
}
|
||||
|
||||
created_node_id
|
||||
}
|
||||
pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<ManipulatorGroupId>>, layer: NodeId) {
|
||||
let shape = {
|
||||
let node_type = resolve_document_node_type("Shape").expect("Shape node does not exist");
|
||||
|
@ -216,7 +297,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_document_node();
|
||||
|
||||
let stroke_id = NodeId(generate_uuid());
|
||||
self.insert_node_before(stroke_id, layer, 0, stroke, IVec2::new(-8, 0));
|
||||
self.insert_node_before(stroke_id, layer, 1, stroke, IVec2::new(-8, 0));
|
||||
let fill_id = NodeId(generate_uuid());
|
||||
self.insert_node_before(fill_id, stroke_id, 0, fill, IVec2::new(-8, 0));
|
||||
let transform_id = NodeId(generate_uuid());
|
||||
|
@ -241,7 +322,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_document_node();
|
||||
|
||||
let stroke_id = NodeId(generate_uuid());
|
||||
self.insert_node_before(stroke_id, layer, 0, stroke, IVec2::new(-8, 0));
|
||||
self.insert_node_before(stroke_id, layer, 1, stroke, IVec2::new(-8, 0));
|
||||
let fill_id = NodeId(generate_uuid());
|
||||
self.insert_node_before(fill_id, stroke_id, 0, fill, IVec2::new(-8, 0));
|
||||
let transform_id = NodeId(generate_uuid());
|
||||
|
@ -259,7 +340,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_document_node();
|
||||
|
||||
let transform_id = NodeId(generate_uuid());
|
||||
self.insert_node_before(transform_id, layer, 0, transform, IVec2::new(-8, 0));
|
||||
self.insert_node_before(transform_id, layer, 1, transform, IVec2::new(-8, 0));
|
||||
|
||||
let image_id = NodeId(generate_uuid());
|
||||
self.insert_node_before(image_id, transform_id, 0, image, IVec2::new(-8, 0));
|
||||
|
@ -267,8 +348,12 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
pub fn shift_upstream(&mut self, node_id: NodeId, shift: IVec2) {
|
||||
pub fn shift_upstream(&mut self, node_id: NodeId, shift: IVec2, shift_self: bool) {
|
||||
let mut shift_nodes = HashSet::new();
|
||||
if shift_self {
|
||||
shift_nodes.insert(node_id);
|
||||
}
|
||||
|
||||
let mut stack = vec![node_id];
|
||||
while let Some(node_id) = stack.pop() {
|
||||
let Some(node) = self.document_network.nodes.get(&node_id) else { continue };
|
||||
|
@ -295,11 +380,12 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
return;
|
||||
};
|
||||
|
||||
let input_index = if output_node.is_layer { 1 } else { 0 };
|
||||
let metadata = output_node.metadata.clone();
|
||||
let new_input = output_node.inputs.first().cloned().filter(|input| input.as_node().is_some());
|
||||
let new_input = output_node.inputs.get(input_index).cloned().filter(|input| input.as_node().is_some());
|
||||
let node_id = NodeId(generate_uuid());
|
||||
|
||||
output_node.inputs[0] = NodeInput::node(node_id, 0);
|
||||
output_node.inputs[input_index] = NodeInput::node(node_id, 0);
|
||||
|
||||
let Some(node_type) = resolve_document_node_type(name) else {
|
||||
warn!("Node type \"{name}\" doesn't exist");
|
||||
|
@ -309,7 +395,11 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
update_input(&mut new_document_node.inputs, node_id, self.document_metadata);
|
||||
self.document_network.nodes.insert(node_id, new_document_node);
|
||||
|
||||
let upstream_nodes = self.document_network.upstream_flow_back_from_nodes(vec![node_id], true).map(|(_, id)| id).collect::<Vec<_>>();
|
||||
let upstream_nodes = self
|
||||
.document_network
|
||||
.upstream_flow_back_from_nodes(vec![node_id], graph_craft::document::FlowType::HorizontalFlow)
|
||||
.map(|(_, id)| id)
|
||||
.collect::<Vec<_>>();
|
||||
for node_id in upstream_nodes {
|
||||
let Some(node) = self.document_network.nodes.get_mut(&node_id) else { continue };
|
||||
node.metadata.position.x -= 8;
|
||||
|
@ -323,7 +413,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
.upstream_flow_back_from_nodes(
|
||||
self.layer_node
|
||||
.map_or_else(|| self.document_network.exports.iter().map(|output| output.node_id).collect(), |id| vec![id]),
|
||||
true,
|
||||
graph_craft::document::FlowType::HorizontalFlow,
|
||||
)
|
||||
.find(|(node, _)| node.name == name)
|
||||
.map(|(_, id)| id);
|
||||
|
@ -348,7 +438,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
.upstream_flow_back_from_nodes(
|
||||
self.layer_node
|
||||
.map_or_else(|| self.document_network.exports.iter().map(|output| output.node_id).collect(), |id| vec![id]),
|
||||
true,
|
||||
graph_craft::document::FlowType::HorizontalFlow,
|
||||
)
|
||||
.filter(|(node, _)| node.name == name)
|
||||
.map(|(_, id)| id)
|
||||
|
@ -525,159 +615,20 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
|
||||
pub fn resize_artboard(&mut self, location: IVec2, dimensions: IVec2) {
|
||||
self.modify_inputs("Artboard", false, |inputs, _node_id, _metadata| {
|
||||
inputs[1] = NodeInput::value(TaggedValue::IVec2(location), false);
|
||||
inputs[2] = NodeInput::value(TaggedValue::IVec2(dimensions), false);
|
||||
let mut dimensions = dimensions;
|
||||
let mut location = location;
|
||||
|
||||
if dimensions.x < 0 {
|
||||
dimensions.x *= -1;
|
||||
location.x += dimensions.x;
|
||||
}
|
||||
if dimensions.y < 0 {
|
||||
dimensions.y *= -1;
|
||||
location.y += dimensions.y;
|
||||
}
|
||||
|
||||
inputs[2] = NodeInput::value(TaggedValue::IVec2(location), false);
|
||||
inputs[3] = NodeInput::value(TaggedValue::IVec2(dimensions), false);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn delete_layer(&mut self, id: NodeId, selected_nodes: &mut SelectedNodes, is_artboard_layer: bool) {
|
||||
let Some(node) = self.document_network.nodes.get(&id) else {
|
||||
warn!("Deleting layer node that does not exist");
|
||||
return;
|
||||
};
|
||||
|
||||
let layer_node = LayerNodeIdentifier::new(id, self.document_network);
|
||||
let child_layers = layer_node.descendants(self.document_metadata).map(|layer| layer.to_node()).collect::<Vec<_>>();
|
||||
layer_node.delete(self.document_metadata);
|
||||
|
||||
// An artboard layer is a layer node to which an artboard node was connected.
|
||||
// However, since this method is called after `delete_artboard`, the artboard node is already deleted.
|
||||
// So, instead of a single ordinary node, we have a stack of layers connected to the current artboard layer through `node_inputs[0]` (instead of `node_inputs[1]`).
|
||||
let is_artboard_layer = if is_artboard_layer && matches!(&node.inputs[0], NodeInput::Value { .. }) {
|
||||
false
|
||||
} else {
|
||||
is_artboard_layer
|
||||
};
|
||||
|
||||
// For an artboard layer, the new input is the top of the stack of layers that is connected to it through `node_inputs[0]`.
|
||||
// For an ordinary layer, the new input is the next layer in the current stack of layers, which is connected to it through `node_inputs[1]`.
|
||||
let new_input = if is_artboard_layer && !matches!(&node.inputs[0], NodeInput::Value { .. }) {
|
||||
node.inputs[0].clone()
|
||||
} else {
|
||||
node.inputs[1].clone()
|
||||
};
|
||||
let deleted_position = node.metadata.position;
|
||||
|
||||
if let Some(new_input_id) = is_artboard_layer.then(|| new_input.as_node()).flatten() {
|
||||
// This is the artboard layer that will be connected to the bottom of the stack of layers that is connected to the current artboard layer to be deleted.
|
||||
// This will move the stack into the "main stack" of layers that leads to the output.
|
||||
let new_input_artboard_layer = node.inputs[1].clone();
|
||||
|
||||
// Find the last layer node in the stack of layers that is connected to the current artboard layer to be deleted.
|
||||
let mut final_layer_node_id = new_input_id;
|
||||
|
||||
let nodes = &self.document_network.nodes;
|
||||
while let Some(input_id) = nodes.get(&final_layer_node_id).and_then(|input_node| input_node.inputs.get(1).and_then(|x| x.as_node())) {
|
||||
final_layer_node_id = input_id;
|
||||
}
|
||||
|
||||
// Connect `new_input_artboard_layer` to `final_layer_node`
|
||||
if let Some(final_layer_node) = self.document_network.nodes.get_mut(&final_layer_node_id) {
|
||||
final_layer_node.inputs[1] = new_input_artboard_layer.clone();
|
||||
}
|
||||
|
||||
// Shift the position of the stack of layers connected to `new_input_artboard_layer` to the bottom of `final_layer_node`
|
||||
if let Some(final_layer_node) = self.document_network.nodes.get(&final_layer_node_id) {
|
||||
if let Some(new_input_artboard_layer_id) = new_input_artboard_layer.as_node() {
|
||||
if let Some(new_input_artboard_layer_node) = self.document_network.nodes.get(&new_input_artboard_layer_id) {
|
||||
let shift = final_layer_node.metadata.position - new_input_artboard_layer_node.metadata.position + IVec2::new(0, 3);
|
||||
|
||||
let node_ids = self
|
||||
.document_network
|
||||
.upstream_flow_back_from_nodes(vec![new_input_artboard_layer_id], false)
|
||||
.map(|(_, id)| id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for node_id in node_ids {
|
||||
let Some(node) = self.document_network.nodes.get_mut(&node_id) else { continue };
|
||||
node.metadata.position += shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all nodes that the layer to be deleted is connected to
|
||||
for post_node in self.outwards_links.get(&id).unwrap_or(&Vec::new()) {
|
||||
let Some(node) = self.document_network.nodes.get_mut(post_node) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Update the inputs of these nodes by replacing the layer to be deleted with `new_input`
|
||||
for input in &mut node.inputs {
|
||||
if let NodeInput::Node { node_id, .. } = input {
|
||||
if *node_id == id {
|
||||
*input = new_input.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut delete_nodes = vec![id];
|
||||
for (_node, id) in self.document_network.upstream_flow_back_from_nodes([vec![id], child_layers].concat(), true) {
|
||||
// Don't delete the node if it's an artboard layer or if other layers depend on it.
|
||||
if is_artboard_layer || self.outwards_links.get(&id).is_some_and(|nodes| nodes.len() > 1) {
|
||||
break;
|
||||
}
|
||||
// Delete the node if it is connected to only the current layer
|
||||
if self.outwards_links.get(&id).is_some_and(|outwards| outwards.len() == 1) {
|
||||
delete_nodes.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
for node_id in &delete_nodes {
|
||||
self.document_network.nodes.remove(node_id);
|
||||
}
|
||||
|
||||
// Shift the position of the nodes that are connected to the deleted nodes
|
||||
if let Some(node_id) = new_input.as_node() {
|
||||
if let Some(shift) = self.document_network.nodes.get(&node_id).map(|node| deleted_position - node.metadata.position) {
|
||||
for node_id in self.document_network.upstream_flow_back_from_nodes(vec![node_id], false).map(|(_, id)| id).collect::<Vec<_>>() {
|
||||
let Some(node) = self.document_network.nodes.get_mut(&node_id) else { continue };
|
||||
node.metadata.position += shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected_nodes.retain_selected_nodes(|id| !delete_nodes.contains(id));
|
||||
|
||||
// Update the outwards links
|
||||
self.outwards_links = self.document_network.collect_outwards_links();
|
||||
self.responses.add(BroadcastEvent::SelectionChanged);
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
pub fn delete_artboard(&mut self, id: NodeId, selected_nodes: &mut SelectedNodes) {
|
||||
let Some(node) = self.document_network.nodes.get(&id) else {
|
||||
warn!("Deleting artboard node that does not exist");
|
||||
return;
|
||||
};
|
||||
|
||||
let new_input = node.inputs[0].clone();
|
||||
|
||||
// Get all nodes that the artboard is connected to
|
||||
for post_node in self.outwards_links.get(&id).unwrap_or(&Vec::new()) {
|
||||
let Some(node) = self.document_network.nodes.get_mut(post_node) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Update the inputs of these nodes by replacing the artboard with `new_input`
|
||||
for input in &mut node.inputs {
|
||||
if let NodeInput::Node { node_id, .. } = input {
|
||||
if *node_id == id {
|
||||
*input = new_input.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the artboard node
|
||||
self.document_network.nodes.remove(&id);
|
||||
selected_nodes.retain_selected_nodes(|&node_id| id != node_id);
|
||||
|
||||
// Update the outwards links
|
||||
self.outwards_links = self.document_network.collect_outwards_links();
|
||||
self.responses.add(BroadcastEvent::SelectionChanged);
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ pub struct NodePropertiesContext<'a> {
|
|||
pub struct DocumentNodeDefinition {
|
||||
pub name: &'static str,
|
||||
pub category: &'static str,
|
||||
pub is_layer: bool,
|
||||
pub implementation: DocumentNodeImplementation,
|
||||
pub inputs: Vec<DocumentInputType>,
|
||||
pub outputs: Vec<DocumentOutputType>,
|
||||
|
@ -94,6 +95,7 @@ impl Default for DocumentNodeDefinition {
|
|||
Self {
|
||||
name: Default::default(),
|
||||
category: Default::default(),
|
||||
is_layer: false,
|
||||
implementation: Default::default(),
|
||||
inputs: Default::default(),
|
||||
outputs: Default::default(),
|
||||
|
@ -191,10 +193,11 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
..Default::default()
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
name: "Layer",
|
||||
name: "Merge",
|
||||
category: "General",
|
||||
is_layer: true,
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
imports: vec![NodeId(0), NodeId(2)],
|
||||
imports: vec![NodeId(2), NodeId(0)],
|
||||
exports: vec![NodeOutput::new(NodeId(2), 0)],
|
||||
nodes: [
|
||||
(
|
||||
|
@ -207,6 +210,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
},
|
||||
),
|
||||
// The monitor node is used to display a thumbnail in the UI.
|
||||
// TODO: Check if thumbnail is reversed
|
||||
(
|
||||
NodeId(1),
|
||||
DocumentNode {
|
||||
|
@ -233,18 +237,64 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
}),
|
||||
inputs: vec![
|
||||
DocumentInputType::value("Graphical Data", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
|
||||
DocumentInputType::value("Stack", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
|
||||
DocumentInputType::value("Over", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
|
||||
],
|
||||
outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::GraphicGroup)],
|
||||
properties: node_properties::layer_no_properties,
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
name: "Artboard",
|
||||
category: "General",
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _, _>"),
|
||||
is_layer: true,
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
imports: vec![NodeId(2), NodeId(0), NodeId(0), NodeId(0), NodeId(0), NodeId(0)],
|
||||
exports: vec![NodeOutput::new(NodeId(2), 0)],
|
||||
nodes: [
|
||||
(
|
||||
NodeId(0),
|
||||
DocumentNode {
|
||||
name: "To Artboard".to_string(),
|
||||
manual_composition: Some(concrete!(Footprint)),
|
||||
inputs: vec![
|
||||
NodeInput::Network(concrete!(graphene_core::GraphicGroup)),
|
||||
NodeInput::Network(concrete!(TaggedValue)),
|
||||
NodeInput::Network(concrete!(TaggedValue)),
|
||||
NodeInput::Network(concrete!(TaggedValue)),
|
||||
NodeInput::Network(concrete!(TaggedValue)),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _, _>"),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
// The monitor node is used to display a thumbnail in the UI.
|
||||
// TODO: Check if thumbnail is reversed
|
||||
(
|
||||
NodeId(1),
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
..monitor_node()
|
||||
},
|
||||
),
|
||||
(
|
||||
NodeId(2),
|
||||
DocumentNode {
|
||||
name: "Add to Artboards".to_string(),
|
||||
manual_composition: Some(concrete!(Footprint)),
|
||||
inputs: vec![
|
||||
NodeInput::node(NodeId(1), 0),
|
||||
NodeInput::Network(graphene_core::Type::Fn(Box::new(concrete!(Footprint)), Box::new(concrete!(ArtboardGroup)))),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::AddArtboardNode<_, _>"),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
inputs: vec![
|
||||
DocumentInputType::value("Graphic Group", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
|
||||
DocumentInputType::value("Artboards", TaggedValue::ArtboardGroup(ArtboardGroup::EMPTY), true),
|
||||
DocumentInputType::value("Over", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
|
||||
DocumentInputType::value("Location", TaggedValue::IVec2(glam::IVec2::ZERO), false),
|
||||
DocumentInputType::value("Dimensions", TaggedValue::IVec2(glam::IVec2::new(1920, 1080)), false),
|
||||
DocumentInputType::value("Background", TaggedValue::Color(Color::WHITE), false),
|
||||
|
@ -252,7 +302,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
],
|
||||
outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::Artboard)],
|
||||
properties: node_properties::artboard_properties,
|
||||
manual_composition: Some(concrete!(Footprint)),
|
||||
..Default::default()
|
||||
},
|
||||
// TODO: Does this need an internal Cull node to be added to its implementation?
|
||||
|
@ -2936,6 +2985,7 @@ impl DocumentNodeDefinition {
|
|||
|
||||
DocumentNode {
|
||||
name: self.name.to_string(),
|
||||
is_layer: self.is_layer,
|
||||
inputs,
|
||||
manual_composition: self.manual_composition.clone(),
|
||||
has_primary_output: self.has_primary_output,
|
||||
|
@ -2953,7 +3003,7 @@ impl DocumentNodeDefinition {
|
|||
self.to_document_node(inputs, metadata)
|
||||
}
|
||||
|
||||
/// Converts the [DocumentNodeDefinition] type to a [DocumentNode], completely default
|
||||
/// Converts the [DocumentNodeDefinition] type to a [DocumentNode], completely default.
|
||||
pub fn default_document_node(&self) -> DocumentNode {
|
||||
self.to_document_node(self.inputs.iter().map(|input| input.default.clone()), DocumentNodeMetadata::default())
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ pub enum NodeGraphMessage {
|
|||
y: i32,
|
||||
},
|
||||
Cut,
|
||||
DeleteNode {
|
||||
node_id: NodeId,
|
||||
DeleteNodes {
|
||||
node_ids: Vec<NodeId>,
|
||||
reconnect: bool,
|
||||
},
|
||||
DeleteSelectedNodes {
|
||||
|
@ -40,6 +40,9 @@ pub enum NodeGraphMessage {
|
|||
node: NodeId,
|
||||
},
|
||||
DuplicateSelectedNodes,
|
||||
EnforceLayerHasNoMultiParams {
|
||||
node_id: NodeId,
|
||||
},
|
||||
ExitNestedNetwork {
|
||||
depth_of_nesting: usize,
|
||||
},
|
||||
|
@ -52,6 +55,15 @@ pub enum NodeGraphMessage {
|
|||
node_id: NodeId,
|
||||
document_node: DocumentNode,
|
||||
},
|
||||
InsertNodeBetween {
|
||||
post_node_id: NodeId,
|
||||
post_node_input_index: usize,
|
||||
insert_node_output_index: usize,
|
||||
insert_node_id: NodeId,
|
||||
insert_node_input_index: usize,
|
||||
pre_node_output_index: usize,
|
||||
pre_node_id: NodeId,
|
||||
},
|
||||
MoveSelectedNodes {
|
||||
displacement_x: i32,
|
||||
displacement_y: i32,
|
||||
|
@ -89,18 +101,10 @@ pub enum NodeGraphMessage {
|
|||
ShiftNode {
|
||||
node_id: NodeId,
|
||||
},
|
||||
ToggleSelectedVisibility,
|
||||
ToggleVisibility {
|
||||
node_id: NodeId,
|
||||
},
|
||||
SetVisibility {
|
||||
node_id: NodeId,
|
||||
visible: bool,
|
||||
},
|
||||
ToggleSelectedLocked,
|
||||
ToggleLocked {
|
||||
node_id: NodeId,
|
||||
},
|
||||
SetLocked {
|
||||
node_id: NodeId,
|
||||
locked: bool,
|
||||
|
@ -113,12 +117,25 @@ pub enum NodeGraphMessage {
|
|||
node_id: NodeId,
|
||||
name: String,
|
||||
},
|
||||
SetToNodeOrLayer {
|
||||
node_id: NodeId,
|
||||
is_layer: bool,
|
||||
},
|
||||
ToggleLocked {
|
||||
node_id: NodeId,
|
||||
},
|
||||
TogglePreview {
|
||||
node_id: NodeId,
|
||||
},
|
||||
TogglePreviewImpl {
|
||||
node_id: NodeId,
|
||||
},
|
||||
ToggleSelectedLayers,
|
||||
ToggleSelectedLocked,
|
||||
ToggleSelectedVisibility,
|
||||
ToggleVisibility {
|
||||
node_id: NodeId,
|
||||
},
|
||||
UpdateNewNodeGraph,
|
||||
UpdateTypes {
|
||||
#[serde(skip)]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use graph_craft::document::{DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput, Source};
|
||||
use graph_craft::document::{DocumentNode, FlowType, NodeId, NodeInput, NodeNetwork, NodeOutput, Source};
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use graphene_core::*;
|
||||
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
|
||||
|
@ -11,7 +11,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
|||
use crate::messages::portfolio::document::graph_operation::load_network_structure;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_types::{resolve_document_node_type, DocumentInputType, NodePropertiesContext};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerClassification, LayerPanelEntry, SelectedNodes};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry, SelectedNodes};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use glam::IVec2;
|
||||
|
@ -66,6 +66,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
new_layer: selected_nodes.selected_layers(document_metadata).next(),
|
||||
});
|
||||
}
|
||||
responses.add(ArtboardToolMessage::UpdateSelectedArtboard);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
NodeGraphMessage::ConnectNodesByLink {
|
||||
|
@ -139,14 +140,83 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
responses.add(NodeGraphMessage::Copy);
|
||||
responses.add(NodeGraphMessage::DeleteSelectedNodes { reconnect: true });
|
||||
}
|
||||
NodeGraphMessage::DeleteNode { node_id, reconnect } => {
|
||||
self.remove_node(document_network, selected_nodes, node_id, responses, reconnect);
|
||||
}
|
||||
NodeGraphMessage::DeleteSelectedNodes { reconnect } => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
NodeGraphMessage::DeleteNodes { node_ids, reconnect } => {
|
||||
let mut delete_nodes = HashSet::new();
|
||||
|
||||
for node_id in selected_nodes.selected_nodes().copied() {
|
||||
responses.add(NodeGraphMessage::DeleteNode { node_id, reconnect });
|
||||
for node_id in &node_ids {
|
||||
delete_nodes.insert(*node_id);
|
||||
|
||||
if !reconnect {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(node) = document_network.nodes.get(&node_id) else {
|
||||
continue;
|
||||
};
|
||||
let child_id = node.inputs.get(1).and_then(|input| if let NodeInput::Node { node_id, .. } = input { Some(node_id) } else { None });
|
||||
let Some(child_id) = child_id else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let outward_links = document_network.collect_outwards_links();
|
||||
|
||||
for (_, upstream_id) in document_network.upstream_flow_back_from_nodes(vec![*child_id], graph_craft::document::FlowType::UpstreamFlow) {
|
||||
// TODO: move into a document_network function .is_sole_dependent. This function does a downstream traversal starting from the current node,
|
||||
// TODO: and only traverses for nodes that are not in the delete_nodes set. If all downstream nodes converge to some node in the delete_nodes set,
|
||||
// TODO: then it is a sole dependent. If the output node is eventually reached, then it is not a sole dependent. This means disconnected branches
|
||||
// TODO: that do not feed into the delete_nodes set or the output node will be deleted.
|
||||
|
||||
let mut stack = vec![upstream_id];
|
||||
let mut can_delete = true;
|
||||
|
||||
// TODO: Add iteration limit to force break in case of infinite while loop
|
||||
while let Some(current_node) = stack.pop() {
|
||||
if let Some(downstream_nodes) = outward_links.get(¤t_node) {
|
||||
for downstream_node in downstream_nodes {
|
||||
if document_network.original_outputs_contain(*downstream_node) {
|
||||
can_delete = false;
|
||||
} else if !delete_nodes.contains(downstream_node) {
|
||||
stack.push(*downstream_node);
|
||||
}
|
||||
// Continue traversing over the downstream sibling, which happens if the current node is a sibling to a node in node_ids
|
||||
else {
|
||||
for deleted_node_id in &node_ids {
|
||||
let Some(output_node) = document_network.nodes.get(&deleted_node_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(input) = output_node.inputs.get(0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let NodeInput::Node { node_id, .. } = input {
|
||||
if *node_id == current_node {
|
||||
stack.push(*deleted_node_id);
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if can_delete {
|
||||
delete_nodes.insert(upstream_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for delete_node_id in delete_nodes {
|
||||
let Some(delete_node) = document_network.nodes.get(&delete_node_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if delete_node.is_layer {
|
||||
// Delete node from document metadata
|
||||
let layer_node = LayerNodeIdentifier::new(delete_node_id, document_network);
|
||||
layer_node.delete(document_metadata);
|
||||
}
|
||||
|
||||
self.remove_node(document_network, selected_nodes, delete_node_id, responses, reconnect);
|
||||
}
|
||||
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
|
@ -155,6 +225,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
|
||||
// There is no need to call `load_network_structure()` since the metadata is already updated
|
||||
}
|
||||
// Deletes selected_nodes. If `reconnect` is true, then all children nodes (secondary input) of the selected nodes are deleted and the siblings (primary input/output) are reconnected.
|
||||
// If `reconnect` is false, then only the selected nodes are deleted and not reconnected.
|
||||
NodeGraphMessage::DeleteSelectedNodes { reconnect } => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(NodeGraphMessage::DeleteNodes {
|
||||
node_ids: selected_nodes.selected_nodes().copied().collect(),
|
||||
reconnect,
|
||||
});
|
||||
}
|
||||
NodeGraphMessage::DisconnectNodes { node_id, input_index } => {
|
||||
let Some(network) = document_network.nested_network(&self.network) else {
|
||||
|
@ -169,16 +250,16 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
warn!("Node {} not in library", node.name);
|
||||
return;
|
||||
};
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let Some((input_index, existing_input)) = node.inputs.iter().enumerate().filter(|(_, input)| input.is_exposed()).nth(input_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut input = node_type.inputs[input_index].default.clone();
|
||||
if let NodeInput::Value { exposed, .. } = &mut input {
|
||||
*exposed = existing_input.is_exposed();
|
||||
}
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
|
||||
|
||||
if network.connected_to_output(node_id) {
|
||||
|
@ -224,6 +305,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
self.update_selected(document_network, document_metadata, selected_nodes, responses);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::EnforceLayerHasNoMultiParams { node_id } => {
|
||||
if !self.eligible_to_be_layer(document_network, node_id) {
|
||||
responses.add(NodeGraphMessage::SetToNodeOrLayer { node_id: node_id, is_layer: false })
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::ExitNestedNetwork { depth_of_nesting } => {
|
||||
selected_nodes.clear_selected_nodes();
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
|
@ -260,8 +346,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
};
|
||||
}
|
||||
}
|
||||
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
|
||||
|
||||
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
|
||||
responses.add(NodeGraphMessage::EnforceLayerHasNoMultiParams { node_id });
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
|
@ -270,6 +357,57 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
network.nodes.insert(node_id, document_node);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::InsertNodeBetween {
|
||||
post_node_id,
|
||||
post_node_input_index,
|
||||
insert_node_output_index,
|
||||
insert_node_id,
|
||||
insert_node_input_index,
|
||||
pre_node_output_index,
|
||||
pre_node_id,
|
||||
} => {
|
||||
let Some(network) = document_network.nested_network(&self.network) else {
|
||||
error!("No network");
|
||||
return;
|
||||
};
|
||||
let Some(post_node) = network.nodes.get(&post_node_id) else {
|
||||
error!("Post node not found");
|
||||
return;
|
||||
};
|
||||
let Some((post_node_input_index, _)) = post_node.inputs.iter().enumerate().filter(|input| input.1.is_exposed()).nth(post_node_input_index) else {
|
||||
error!("Failed to find input index {post_node_input_index} on node {post_node_id:#?}");
|
||||
return;
|
||||
};
|
||||
let Some(insert_node) = network.nodes.get(&insert_node_id) else {
|
||||
error!("Insert node not found");
|
||||
return;
|
||||
};
|
||||
let Some((insert_node_input_index, _)) = insert_node.inputs.iter().enumerate().filter(|input| input.1.is_exposed()).nth(insert_node_input_index) else {
|
||||
error!("Failed to find input index {insert_node_input_index} on node {insert_node_id:#?}");
|
||||
return;
|
||||
};
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let post_input = NodeInput::node(insert_node_id, insert_node_output_index);
|
||||
responses.add(NodeGraphMessage::SetNodeInput {
|
||||
node_id: post_node_id,
|
||||
input_index: post_node_input_index,
|
||||
input: post_input,
|
||||
});
|
||||
|
||||
let insert_input = NodeInput::node(pre_node_id, pre_node_output_index);
|
||||
responses.add(NodeGraphMessage::SetNodeInput {
|
||||
node_id: insert_node_id,
|
||||
input_index: insert_node_input_index,
|
||||
input: insert_input,
|
||||
});
|
||||
|
||||
if network.connected_to_output(insert_node_id) {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
NodeGraphMessage::MoveSelectedNodes { displacement_x, displacement_y } => {
|
||||
let Some(network) = document_network.nested_network_mut(&self.network) else {
|
||||
warn!("No network");
|
||||
|
@ -281,7 +419,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
node.metadata.position += IVec2::new(displacement_x, displacement_y)
|
||||
}
|
||||
}
|
||||
self.send_graph(network, graph_view_overlay_open, document_metadata, selected_nodes, collapsed, responses);
|
||||
|
||||
// Since document structure doesn't change, just update the nodes
|
||||
if graph_view_overlay_open {
|
||||
let links = Self::collect_links(network);
|
||||
let nodes = self.collect_nodes(&links, network);
|
||||
responses.add(FrontendMessage::UpdateNodeGraph { nodes, links });
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::PasteNodes { serialized_nodes } => {
|
||||
let Some(network) = document_network.nested_network(&self.network) else {
|
||||
|
@ -523,6 +667,37 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
document_metadata.load_structure(document_network, selected_nodes);
|
||||
self.update_selection_action_buttons(document_network, document_metadata, selected_nodes, responses);
|
||||
}
|
||||
NodeGraphMessage::ToggleSelectedLayers => {
|
||||
let Some(network) = document_network.nested_network_mut(&self.network) else { return };
|
||||
|
||||
for node_id in selected_nodes.selected_nodes() {
|
||||
let Some(node) = network.nodes.get_mut(&node_id) else { continue };
|
||||
|
||||
if node.has_primary_output {
|
||||
responses.add(NodeGraphMessage::SetToNodeOrLayer {
|
||||
node_id: *node_id,
|
||||
is_layer: !node.is_layer,
|
||||
});
|
||||
}
|
||||
|
||||
if network.connected_to_output(*node_id) {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer } => {
|
||||
if is_layer && !self.eligible_to_be_layer(document_network, node_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(network) = document_network.nested_network_mut(&self.network) else { return };
|
||||
|
||||
if let Some(node) = network.nodes.get_mut(&node_id) {
|
||||
node.is_layer = is_layer;
|
||||
}
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
}
|
||||
NodeGraphMessage::SetName { node_id, name } => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(NodeGraphMessage::SetNameImpl { node_id, name });
|
||||
|
@ -585,7 +760,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
impl NodeGraphMessageHandler {
|
||||
pub fn actions_with_node_graph_open(&self, graph_open: bool) -> ActionList {
|
||||
if self.has_selection && graph_open {
|
||||
actions!(NodeGraphMessageDiscriminant; ToggleSelectedVisibility, ToggleSelectedLocked, DuplicateSelectedNodes, DeleteSelectedNodes, Cut, Copy)
|
||||
actions!(NodeGraphMessageDiscriminant; ToggleSelectedVisibility, ToggleSelectedLocked, ToggleSelectedLayers, DuplicateSelectedNodes, DeleteSelectedNodes, Cut, Copy)
|
||||
} else if self.has_selection {
|
||||
actions!(NodeGraphMessageDiscriminant; ToggleSelectedVisibility, ToggleSelectedLocked)
|
||||
} else {
|
||||
|
@ -669,7 +844,7 @@ impl NodeGraphMessageHandler {
|
|||
let (mut layers, mut nodes) = (Vec::new(), Vec::new());
|
||||
for node_id in selected_nodes.selected_nodes() {
|
||||
if let Some(layer_or_node) = network.nodes.get(node_id) {
|
||||
if layer_or_node.is_layer() {
|
||||
if layer_or_node.is_layer {
|
||||
layers.push(*node_id);
|
||||
} else {
|
||||
nodes.push(*node_id);
|
||||
|
@ -688,16 +863,16 @@ impl NodeGraphMessageHandler {
|
|||
1 => {
|
||||
let nodes_not_upstream_of_layer = nodes
|
||||
.into_iter()
|
||||
.filter(|&selected_node_id| !network.is_node_upstream_of_another_by_primary_flow(layers[0], selected_node_id));
|
||||
.filter(|&selected_node_id| !network.is_node_upstream_of_another_by_horizontal_flow(layers[0], selected_node_id));
|
||||
if nodes_not_upstream_of_layer.count() > 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Iterate through all the upstream nodes, but stop when we reach another layer (since that's a point where we switch from horizontal to vertical flow)
|
||||
network
|
||||
.upstream_flow_back_from_nodes(vec![layers[0]], true)
|
||||
.upstream_flow_back_from_nodes(vec![layers[0]], graph_craft::document::FlowType::HorizontalFlow)
|
||||
.enumerate()
|
||||
.take_while(|(i, (node, _))| if *i == 0 { true } else { !node.is_layer() })
|
||||
.take_while(|(i, (node, _))| if *i == 0 { true } else { !node.is_layer })
|
||||
.map(|(_, (node, node_id))| node_properties::generate_node_properties(node, node_id, context))
|
||||
.collect()
|
||||
}
|
||||
|
@ -733,7 +908,10 @@ impl NodeGraphMessageHandler {
|
|||
}
|
||||
|
||||
fn collect_nodes(&self, links: &[FrontendNodeLink], network: &NodeNetwork) -> Vec<FrontendNode> {
|
||||
let connected_node_to_output_lookup = links.iter().map(|link| ((link.link_start, link.link_start_output_index), link.link_end)).collect::<HashMap<_, _>>();
|
||||
let connected_node_to_output_lookup = links
|
||||
.iter()
|
||||
.map(|link| ((link.link_start, link.link_start_output_index), (link.link_end, link.link_end_input_index)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
for (&node_id, node) in &network.nodes {
|
||||
|
@ -769,22 +947,27 @@ impl NodeGraphMessageHandler {
|
|||
let exposed_inputs = inputs.filter(|(input, _)| input.is_exposed()).map(|(_, input_type)| input_type).collect();
|
||||
|
||||
// Outputs
|
||||
let mut outputs = document_node_definition.outputs.iter().enumerate().map(|(index, output_type)| FrontendGraphOutput {
|
||||
data_type: output_type.data_type,
|
||||
name: output_type.name.to_string(),
|
||||
resolved_type: self.resolved_types.outputs.get(&Source { node: node_path.clone(), index }).map(|output| format!("{output:?}")),
|
||||
connected: connected_node_to_output_lookup.get(&(node_id, index)).copied(),
|
||||
let mut outputs = document_node_definition.outputs.iter().enumerate().map(|(index, output_type)| {
|
||||
let (connected, connected_index) = connected_node_to_output_lookup.get(&(node_id, index)).copied().map(|(a, b)| (Some(a), Some(b))).unwrap_or((None, None));
|
||||
|
||||
FrontendGraphOutput {
|
||||
data_type: output_type.data_type,
|
||||
name: output_type.name.to_string(),
|
||||
resolved_type: self.resolved_types.outputs.get(&Source { node: node_path.clone(), index }).map(|output| format!("{output:?}")),
|
||||
connected,
|
||||
connected_index,
|
||||
}
|
||||
});
|
||||
let primary_output = node.has_primary_output.then(|| outputs.next()).flatten();
|
||||
let exposed_outputs = outputs.collect::<Vec<_>>();
|
||||
|
||||
// Errors
|
||||
let errors = self.node_graph_errors.iter().find(|error| error.node_path.starts_with(&node_path)).map(|error| error.error.clone());
|
||||
|
||||
nodes.push(FrontendNode {
|
||||
id: node_id,
|
||||
is_layer: node.is_layer(),
|
||||
alias: node.alias.clone(),
|
||||
is_layer: node.is_layer,
|
||||
can_be_layer: self.eligible_to_be_layer(network, node_id),
|
||||
alias: Self::untitled_layer_label(node),
|
||||
name: node.name.clone(),
|
||||
primary_input,
|
||||
exposed_inputs,
|
||||
|
@ -802,17 +985,8 @@ impl NodeGraphMessageHandler {
|
|||
|
||||
fn update_layer_panel(network: &NodeNetwork, metadata: &DocumentMetadata, collapsed: &CollapsedLayers, responses: &mut VecDeque<Message>) {
|
||||
for (&node_id, node) in &network.nodes {
|
||||
if node.is_layer() {
|
||||
if node.is_layer {
|
||||
let layer = LayerNodeIdentifier::new(node_id, network);
|
||||
let layer_classification = {
|
||||
if metadata.is_artboard(layer) {
|
||||
LayerClassification::Artboard
|
||||
} else if metadata.is_folder(layer) {
|
||||
LayerClassification::Folder
|
||||
} else {
|
||||
LayerClassification::Layer
|
||||
}
|
||||
};
|
||||
|
||||
let parents_visible = layer
|
||||
.ancestors(metadata)
|
||||
|
@ -826,12 +1000,21 @@ impl NodeGraphMessageHandler {
|
|||
|
||||
let data = LayerPanelEntry {
|
||||
id: node_id,
|
||||
layer_classification,
|
||||
children_allowed:
|
||||
// The layer has other layers as children along the secondary input's horizontal flow
|
||||
layer.has_children(metadata)
|
||||
|| (
|
||||
// At least one secondary input is exposed on this layer node
|
||||
node.inputs.iter().skip(1).any(|input| input.is_exposed()) &&
|
||||
// But nothing is connected to it, since we only get 1 item (ourself) when we ask for the flow from the secondary input
|
||||
network.upstream_flow_back_from_nodes(vec![node_id], FlowType::HorizontalFlow).count() == 1
|
||||
),
|
||||
children_present: layer.has_children(metadata),
|
||||
expanded: layer.has_children(metadata) && !collapsed.0.contains(&layer),
|
||||
has_children: layer.has_children(metadata),
|
||||
depth: layer.ancestors(metadata).count() - 1,
|
||||
parent_id: layer.parent(metadata).map(|parent| parent.to_node()),
|
||||
name: network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(),
|
||||
name: node.name.clone(),
|
||||
alias: Self::untitled_layer_label(node),
|
||||
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
|
||||
visible: node.visible,
|
||||
parents_visible,
|
||||
|
@ -844,10 +1027,10 @@ impl NodeGraphMessageHandler {
|
|||
}
|
||||
|
||||
fn send_graph(&self, network: &NodeNetwork, graph_open: bool, metadata: &mut DocumentMetadata, selected_nodes: &mut SelectedNodes, collapsed: &CollapsedLayers, responses: &mut VecDeque<Message>) {
|
||||
metadata.load_structure(network, selected_nodes);
|
||||
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
// TODO: Move update_layer_panel into message so load structure here can be removed, since load structure is already called in DocumentStructureChanged
|
||||
metadata.load_structure(network, selected_nodes);
|
||||
Self::update_layer_panel(network, metadata, collapsed, responses);
|
||||
if graph_open {
|
||||
let links = Self::collect_links(network);
|
||||
|
@ -880,9 +1063,8 @@ impl NodeGraphMessageHandler {
|
|||
// Check whether the being-deleted node's first (primary) input is a node
|
||||
if let Some(node) = network.nodes.get(&deleting_node_id) {
|
||||
// Reconnect to the node below when deleting a layer node.
|
||||
let reconnect_from_input_index = if node.is_layer() { 1 } else { 0 };
|
||||
if matches!(&node.inputs.get(reconnect_from_input_index), Some(NodeInput::Node { .. })) {
|
||||
reconnect_to_input = Some(node.inputs[reconnect_from_input_index].clone());
|
||||
if matches!(&node.inputs.get(0), Some(NodeInput::Node { .. })) {
|
||||
reconnect_to_input = Some(node.inputs[0].clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -959,6 +1141,26 @@ impl NodeGraphMessageHandler {
|
|||
.filter_map(|(&id, &new)| network.nodes.get(&id).map(|node| (new, node.clone())))
|
||||
.map(move |(new, node)| (new, node.map_ids(Self::default_node_input, new_ids)))
|
||||
}
|
||||
|
||||
pub fn eligible_to_be_layer(&self, document_network: &NodeNetwork, node_id: NodeId) -> bool {
|
||||
let Some(network) = document_network.nested_network(&self.network) else { return false };
|
||||
let Some(node) = network.nodes.get(&node_id) else { return false };
|
||||
let Some(definition) = resolve_document_node_type(&node.name) else { return false };
|
||||
|
||||
let exposed_value_count = node.inputs.iter().filter(|input| if let NodeInput::Value { exposed, .. } = input { *exposed } else { false }).count();
|
||||
let node_input_count = node.inputs.iter().filter(|input| if let NodeInput::Node { .. } = input { true } else { false }).count();
|
||||
let input_count = node_input_count + exposed_value_count;
|
||||
let output_count = definition.outputs.len();
|
||||
|
||||
// TODO: Eventually allow nodes at the bottom of a stack to be layers, where `input_count` is 0
|
||||
node.has_primary_output && output_count == 1 && (input_count == 1 || input_count == 2)
|
||||
}
|
||||
|
||||
fn untitled_layer_label(node: &DocumentNode) -> String {
|
||||
(node.alias != "")
|
||||
.then_some(node.alias.clone())
|
||||
.unwrap_or(if node.is_layer && node.name == "Merge" { "Untitled Layer".to_string() } else { node.name.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NodeGraphMessageHandler {
|
||||
|
|
|
@ -2236,12 +2236,8 @@ fn unknown_node_properties(document_node: &DocumentNode) -> Vec<LayoutGroup> {
|
|||
string_properties(format!("Node '{}' cannot be found in library", document_node.name))
|
||||
}
|
||||
|
||||
pub fn node_no_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
string_properties("Node has no properties")
|
||||
}
|
||||
|
||||
pub fn layer_no_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
string_properties("Layer has no properties")
|
||||
pub fn node_no_properties(document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
string_properties(if document_node.is_layer { "Layer has no properties" } else { "Node has no properties" })
|
||||
}
|
||||
|
||||
pub fn index_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
|
@ -2423,12 +2419,11 @@ pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context:
|
|||
}
|
||||
|
||||
pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let location = vec2_widget(document_node, node_id, 1, "Location", "X", "Y", " px", None, add_blank_assist);
|
||||
let dimensions = vec2_widget(document_node, node_id, 2, "Dimensions", "W", "H", " px", None, add_blank_assist);
|
||||
let background = color_widget(document_node, node_id, 3, "Background", ColorButton::default().allow_none(false), true);
|
||||
let clip = LayoutGroup::Row {
|
||||
widgets: bool_widget(document_node, node_id, 4, "Clip", true),
|
||||
};
|
||||
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", true);
|
||||
let clip = LayoutGroup::Row { widgets: clip };
|
||||
vec![location, dimensions, background, clip]
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ impl FrontendGraphDataType {
|
|||
TaggedValue::Color(_) => Self::Color,
|
||||
TaggedValue::RcSubpath(_) | TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::Subpath,
|
||||
TaggedValue::GraphicGroup(_) => Self::GraphicGroup,
|
||||
TaggedValue::Artboard(_) => Self::Artboard,
|
||||
TaggedValue::Artboard(_) | TaggedValue::ArtboardGroup(_) => Self::Artboard,
|
||||
TaggedValue::Palette(_) => Self::Palette,
|
||||
_ => Self::General,
|
||||
}
|
||||
|
@ -66,6 +66,8 @@ pub struct FrontendGraphOutput {
|
|||
#[serde(rename = "resolvedType")]
|
||||
pub resolved_type: Option<String>,
|
||||
pub connected: Option<NodeId>,
|
||||
#[serde(rename = "connectedIndex")]
|
||||
pub connected_index: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
|
@ -73,6 +75,8 @@ pub struct FrontendNode {
|
|||
pub id: graph_craft::document::NodeId,
|
||||
#[serde(rename = "isLayer")]
|
||||
pub is_layer: bool,
|
||||
#[serde(rename = "canBeLayer")]
|
||||
pub can_be_layer: bool,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
#[serde(rename = "primaryInput")]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::nodes::SelectedNodes;
|
||||
|
||||
use graph_craft::document::{DocumentNode, NodeId, NodeNetwork};
|
||||
use graph_craft::document::FlowType;
|
||||
use graph_craft::document::{NodeId, NodeNetwork};
|
||||
use graphene_core::renderer::ClickTarget;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::transform::Footprint;
|
||||
|
@ -92,14 +93,14 @@ impl DocumentMetadata {
|
|||
sorted_layers
|
||||
}
|
||||
|
||||
/// Ancestor that is shared by all layers and that is deepest (more nested). Default may be the root.
|
||||
/// Ancestor that is shared by all layers and that is deepest (more nested). Default may be the root. Skips selected non-folder, non-artboard layers
|
||||
pub fn deepest_common_ancestor(&self, layers: impl Iterator<Item = LayerNodeIdentifier>, include_self: bool) -> Option<LayerNodeIdentifier> {
|
||||
layers
|
||||
.map(|layer| {
|
||||
let mut layer_path = layer.ancestors(self).collect::<Vec<_>>();
|
||||
layer_path.reverse();
|
||||
|
||||
if include_self || !self.folders.contains(&layer) {
|
||||
if !include_self || !self.is_artboard(layer) && !self.is_folder(layer) {
|
||||
layer_path.pop();
|
||||
}
|
||||
|
||||
|
@ -147,43 +148,52 @@ impl DocumentMetadata {
|
|||
impl DocumentMetadata {
|
||||
/// Loads the structure of layer nodes from a node graph.
|
||||
pub fn load_structure(&mut self, graph: &NodeNetwork, selected_nodes: &mut SelectedNodes) {
|
||||
fn first_child_layer<'a>(graph: &'a NodeNetwork, node: &DocumentNode) -> Option<(&'a DocumentNode, NodeId)> {
|
||||
graph.upstream_flow_back_from_nodes(vec![node.inputs[0].as_node()?], true).find(|(node, _)| node.is_layer())
|
||||
}
|
||||
|
||||
self.structure = HashMap::from_iter([(LayerNodeIdentifier::ROOT, NodeRelations::default())]);
|
||||
self.artboards = HashSet::new();
|
||||
self.folders = HashSet::new();
|
||||
self.hidden = HashSet::new();
|
||||
self.locked = HashSet::new();
|
||||
|
||||
let id = graph.exports[0].node_id;
|
||||
let Some(output_node) = graph.nodes.get(&id) else {
|
||||
return;
|
||||
};
|
||||
let Some((layer_node, node_id)) = first_child_layer(graph, output_node) else {
|
||||
return;
|
||||
};
|
||||
let parent = LayerNodeIdentifier::ROOT;
|
||||
let mut stack = vec![(layer_node, node_id, parent)];
|
||||
while let Some((node, node_id, parent)) = stack.pop() {
|
||||
let mut current = Some((node, node_id));
|
||||
while let Some(&(current_node, current_node_id)) = current.as_ref() {
|
||||
let current_layer_id = LayerNodeIdentifier::new_unchecked(current_node_id);
|
||||
if !self.structure.contains_key(¤t_layer_id) {
|
||||
parent.push_child(self, current_layer_id);
|
||||
// Refers to output node: NodeId(0)
|
||||
let output_node_id = graph.exports[0].node_id;
|
||||
|
||||
if let Some((child_node, child_id)) = first_child_layer(graph, current_node) {
|
||||
stack.push((child_node, child_id, current_layer_id));
|
||||
}
|
||||
let mut awaiting_horizontal_flow = vec![(output_node_id, LayerNodeIdentifier::ROOT)];
|
||||
let mut awaiting_primary_flow = vec![];
|
||||
|
||||
if is_artboard(current_layer_id, graph) {
|
||||
self.artboards.insert(current_layer_id);
|
||||
}
|
||||
if is_folder(current_layer_id, graph) {
|
||||
self.folders.insert(current_layer_id);
|
||||
}
|
||||
while let Some((horizontal_root_node_id, mut parent_layer_node)) = awaiting_horizontal_flow.pop() {
|
||||
let horizontal_flow_iter = graph.upstream_flow_back_from_nodes(vec![horizontal_root_node_id], FlowType::HorizontalFlow);
|
||||
// Skip the horizontal_root_node_id node
|
||||
for (current_node, current_node_id) in horizontal_flow_iter.skip(1) {
|
||||
if !current_node.visible {
|
||||
self.hidden.insert(current_node_id);
|
||||
}
|
||||
|
||||
if current_node.locked {
|
||||
self.locked.insert(current_node_id);
|
||||
}
|
||||
|
||||
if current_node.is_layer {
|
||||
let current_layer_node = LayerNodeIdentifier::new(current_node_id, graph);
|
||||
if !self.structure.contains_key(¤t_layer_node) {
|
||||
awaiting_primary_flow.push((current_node_id, parent_layer_node));
|
||||
parent_layer_node.push_child(self, current_layer_node);
|
||||
parent_layer_node = current_layer_node;
|
||||
|
||||
if is_artboard(current_layer_node, graph) {
|
||||
self.artboards.insert(current_layer_node);
|
||||
}
|
||||
|
||||
if graph.nodes.get(¤t_layer_node.to_node()).map(|node| node.layer_has_child_layers(graph)).unwrap_or_default() {
|
||||
self.folders.insert(current_layer_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some((primary_root_node_id, parent_layer_node)) = awaiting_primary_flow.pop() {
|
||||
let primary_flow_iter = graph.upstream_flow_back_from_nodes(vec![primary_root_node_id], FlowType::PrimaryFlow);
|
||||
// Skip the primary_root_node_id node
|
||||
for (current_node, current_node_id) in primary_flow_iter.skip(1) {
|
||||
if !current_node.visible {
|
||||
self.hidden.insert(current_node_id);
|
||||
}
|
||||
|
@ -191,11 +201,26 @@ impl DocumentMetadata {
|
|||
if current_node.locked {
|
||||
self.locked.insert(current_node_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the sibling below
|
||||
let construct_layer_node = ¤t_node.inputs[1];
|
||||
current = construct_layer_node.as_node().and_then(|id| graph.nodes.get(&id).filter(|node| node.is_layer()).map(|node| (node, id)));
|
||||
if current_node.is_layer {
|
||||
// Create a new layer for the top of each stack, and add it as a child to the previous parent
|
||||
let current_layer_node = LayerNodeIdentifier::new(current_node_id, graph);
|
||||
if !self.structure.contains_key(¤t_layer_node) {
|
||||
parent_layer_node.push_child(self, current_layer_node);
|
||||
|
||||
// The layer nodes for the horizontal flow is itself
|
||||
awaiting_horizontal_flow.push((current_node_id, current_layer_node));
|
||||
|
||||
if is_artboard(current_layer_node, graph) {
|
||||
self.artboards.insert(current_layer_node);
|
||||
}
|
||||
|
||||
if graph.nodes.get(¤t_layer_node.to_node()).map(|node| node.layer_has_child_layers(graph)).unwrap_or_default() {
|
||||
self.folders.insert(current_layer_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,7 +248,7 @@ impl DocumentMetadata {
|
|||
pub fn transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 {
|
||||
layer
|
||||
.ancestors(self)
|
||||
.filter_map(|layer| self.upstream_transforms.get(&layer.to_node()))
|
||||
.filter_map(|ancestor_layer| self.upstream_transforms.get(&ancestor_layer.to_node()))
|
||||
.copied()
|
||||
.map(|(footprint, transform)| footprint.transform * transform)
|
||||
.next()
|
||||
|
@ -351,7 +376,7 @@ impl LayerNodeIdentifier {
|
|||
#[track_caller]
|
||||
pub fn new(node_id: NodeId, network: &NodeNetwork) -> Self {
|
||||
debug_assert!(
|
||||
node_id == LayerNodeIdentifier::ROOT.to_node() || network.nodes.get(&node_id).is_some_and(|node| node.is_layer()),
|
||||
node_id == LayerNodeIdentifier::ROOT.to_node() || network.nodes.get(&node_id).is_some_and(|node| node.is_layer),
|
||||
"Layer identifier constructed from non-layer node {node_id}: {:#?}",
|
||||
network.nodes.get(&node_id)
|
||||
);
|
||||
|
@ -622,15 +647,8 @@ struct NodeRelations {
|
|||
// ================
|
||||
|
||||
pub fn is_artboard(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool {
|
||||
network.upstream_flow_back_from_nodes(vec![layer.to_node()], true).any(|(node, _)| node.is_artboard())
|
||||
}
|
||||
|
||||
pub fn is_folder(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool {
|
||||
network.nodes.get(&layer.to_node()).and_then(|node| node.inputs.first()).is_some_and(|input| input.as_node().is_none())
|
||||
|| network
|
||||
.upstream_flow_back_from_nodes(vec![layer.to_node()], true)
|
||||
.skip(1)
|
||||
.any(|(node, _)| node.is_artboard() || node.is_layer())
|
||||
let Some(node) = network.nodes.get(&layer.to_node()) else { return false };
|
||||
node.is_artboard()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -30,24 +30,18 @@ impl serde::Serialize for JsRawBuffer {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)]
|
||||
pub enum LayerClassification {
|
||||
#[default]
|
||||
Folder,
|
||||
Artboard,
|
||||
Layer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)]
|
||||
pub struct LayerPanelEntry {
|
||||
pub id: NodeId,
|
||||
pub name: String,
|
||||
pub alias: String,
|
||||
pub tooltip: String,
|
||||
#[serde(rename = "layerClassification")]
|
||||
pub layer_classification: LayerClassification,
|
||||
#[serde(rename = "childrenAllowed")]
|
||||
pub children_allowed: bool,
|
||||
#[serde(rename = "childrenPresent")]
|
||||
pub children_present: bool,
|
||||
pub expanded: bool,
|
||||
#[serde(rename = "hasChildren")]
|
||||
pub has_children: bool,
|
||||
pub depth: usize,
|
||||
pub visible: bool,
|
||||
#[serde(rename = "parentsVisible")]
|
||||
pub parents_visible: bool,
|
||||
|
@ -56,7 +50,6 @@ pub struct LayerPanelEntry {
|
|||
pub parents_unlocked: bool,
|
||||
#[serde(rename = "parentId")]
|
||||
pub parent_id: Option<NodeId>,
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)]
|
||||
|
|
|
@ -186,7 +186,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
let node = layer.to_node();
|
||||
let previous_alias = active_document.network().nodes.get(&node).map(|node| node.alias.clone()).unwrap_or_default();
|
||||
|
||||
let Some(node) = active_document.network().nodes.get(&node).and_then(|node| node.inputs.first()).and_then(|input| input.as_node()) else {
|
||||
let Some(node) = active_document
|
||||
.network()
|
||||
.nodes
|
||||
.get(&node)
|
||||
.and_then(|node| if node.is_layer { node.inputs.get(1) } else { node.inputs.get(0) })
|
||||
.and_then(|input| input.as_node())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
|
@ -195,7 +201,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
active_document.network(),
|
||||
&active_document
|
||||
.network()
|
||||
.upstream_flow_back_from_nodes(vec![node], false)
|
||||
.upstream_flow_back_from_nodes(vec![node], graph_craft::document::FlowType::UpstreamFlow)
|
||||
.enumerate()
|
||||
.map(|(index, (_, node_id))| (node_id, NodeId(index as u64)))
|
||||
.collect(),
|
||||
|
@ -656,9 +662,7 @@ impl PortfolioMessageHandler {
|
|||
return;
|
||||
};
|
||||
|
||||
self.executor.poll_node_graph_evaluation(active_document, responses).unwrap_or_else(|e| {
|
||||
log::error!("Error while evaluating node graph: {e}");
|
||||
|
||||
if self.executor.poll_node_graph_evaluation(active_document, responses).is_err() {
|
||||
let error = r#"
|
||||
<rect x="50%" y="50%" width="480" height="100" transform="translate(-240 -50)" rx="4" fill="var(--color-error-red)" />
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="18" fill="var(--color-2-mildblack)">
|
||||
|
@ -669,6 +673,6 @@ impl PortfolioMessageHandler {
|
|||
// It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed.
|
||||
.to_string();
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg: error });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,11 +157,11 @@ pub fn get_opacity(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -
|
|||
}
|
||||
|
||||
pub fn get_fill_id(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, document_network).node_id("Fill")
|
||||
NodeGraphLayer::new(layer, document_network).upstream_node_id_from_name("Fill")
|
||||
}
|
||||
|
||||
pub fn get_text_id(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, document_network).node_id("Text")
|
||||
NodeGraphLayer::new(layer, document_network).upstream_node_id_from_name("Text")
|
||||
}
|
||||
|
||||
/// Gets properties from the Text node
|
||||
|
@ -233,21 +233,21 @@ impl<'a> NodeGraphLayer<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return an iterator up the primary flow of the layer
|
||||
pub fn primary_layer_flow(&self) -> impl Iterator<Item = (&'a DocumentNode, NodeId)> {
|
||||
self.node_graph.upstream_flow_back_from_nodes(vec![self.layer_node], true)
|
||||
/// Return an iterator up the horizontal flow of the layer
|
||||
pub fn horizontal_layer_flow(&self) -> impl Iterator<Item = (&'a DocumentNode, NodeId)> {
|
||||
self.node_graph.upstream_flow_back_from_nodes(vec![self.layer_node], graph_craft::document::FlowType::HorizontalFlow)
|
||||
}
|
||||
|
||||
/// Node id of a node if it exists in the layer's primary flow
|
||||
pub fn node_id(&self, node_name: &str) -> Option<NodeId> {
|
||||
self.primary_layer_flow().find(|(node, _id)| node.name == node_name).map(|(_node, id)| id)
|
||||
pub fn upstream_node_id_from_name(&self, node_name: &str) -> Option<NodeId> {
|
||||
self.horizontal_layer_flow().find(|(node, _)| node.name == node_name).map(|(_, id)| id)
|
||||
}
|
||||
|
||||
/// Find all of the inputs of a specific node within the layer's primary flow, up until the next layer is reached.
|
||||
pub fn find_node_inputs(&self, node_name: &str) -> Option<&'a Vec<NodeInput>> {
|
||||
self.primary_layer_flow()
|
||||
.skip(1)
|
||||
.take_while(|(node, _)| !node.is_layer())
|
||||
self.horizontal_layer_flow()
|
||||
.skip(1)// Skip self
|
||||
.take_while(|(node, _)| !node.is_layer)
|
||||
.find(|(node, _)| node.name == node_name)
|
||||
.map(|(node, _id)| &node.inputs)
|
||||
}
|
||||
|
|
|
@ -432,16 +432,16 @@ pub fn are_manipulator_handles_colinear<Id: bezier_rs::Identifier>(group: &bezie
|
|||
|
||||
pub fn get_layer_snap_points(layer: LayerNodeIdentifier, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>) {
|
||||
let document = snap_data.document;
|
||||
|
||||
if document.metadata().is_artboard(layer) {
|
||||
} else if document.metadata().is_folder(layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if document.metadata().is_folder(layer) {
|
||||
for child in layer.descendants(document.metadata()) {
|
||||
get_layer_snap_points(child, snap_data, points);
|
||||
}
|
||||
} else {
|
||||
// Skip empty paths
|
||||
if document.metadata.layer_outline(layer).next().is_none() {
|
||||
return;
|
||||
}
|
||||
} else if document.metadata.layer_outline(layer).next().is_some() {
|
||||
let to_document = document.metadata.transform_to_document(layer);
|
||||
for subpath in document.metadata.layer_outline(layer) {
|
||||
subpath_anchor_snap_points(layer, subpath, snap_data, points, to_document);
|
||||
|
|
|
@ -3,7 +3,6 @@ use crate::application::generate_uuid;
|
|||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
|
||||
|
@ -24,6 +23,7 @@ pub enum ArtboardToolMessage {
|
|||
Overlays(OverlayContext),
|
||||
|
||||
// Tool-specific messages
|
||||
UpdateSelectedArtboard,
|
||||
DeleteSelected,
|
||||
NudgeSelected { delta_x: f64, delta_y: f64 },
|
||||
PointerDown,
|
||||
|
@ -125,7 +125,7 @@ impl ArtboardToolData {
|
|||
|
||||
let mut intersections = document
|
||||
.click_xray(input.mouse.position)
|
||||
.filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.network, "Artboard"));
|
||||
.filter(|&layer| document.network.nodes.get(&layer.to_node()).map_or(false, |document_node| document_node.is_artboard()));
|
||||
|
||||
if let Some(intersection) = intersections.next() {
|
||||
self.selected_artboard = Some(intersection);
|
||||
|
@ -136,6 +136,8 @@ impl ArtboardToolData {
|
|||
bounding_box_manager.transform = document.metadata().document_to_viewport;
|
||||
}
|
||||
|
||||
responses.add_front(NodeGraphMessage::SelectedNodesSet { nodes: vec![intersection.to_node()] });
|
||||
|
||||
true
|
||||
} else {
|
||||
self.selected_artboard = None;
|
||||
|
@ -278,10 +280,8 @@ impl Fsm for ArtboardToolFsmState {
|
|||
});
|
||||
} else {
|
||||
let id = NodeId(generate_uuid());
|
||||
tool_data.selected_artboard = Some(LayerNodeIdentifier::new_unchecked(id));
|
||||
|
||||
//tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
//tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
tool_data.selected_artboard = Some(LayerNodeIdentifier::new_unchecked(id));
|
||||
|
||||
responses.add(GraphOperationMessage::NewArtboard {
|
||||
id,
|
||||
|
@ -377,11 +377,13 @@ impl Fsm for ArtboardToolFsmState {
|
|||
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
(_, ArtboardToolMessage::UpdateSelectedArtboard) => {
|
||||
tool_data.selected_artboard = document.selected_nodes.selected_layers(document.metadata()).find(|layer| document.metadata().is_artboard(*layer));
|
||||
self
|
||||
}
|
||||
(_, ArtboardToolMessage::DeleteSelected) => {
|
||||
if let Some(artboard) = tool_data.selected_artboard.take() {
|
||||
let id = artboard.to_node();
|
||||
responses.add(GraphOperationMessage::DeleteArtboard { id });
|
||||
}
|
||||
tool_data.selected_artboard.take();
|
||||
responses.add(NodeGraphMessage::DeleteSelectedNodes { reconnect: true });
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
(_, ArtboardToolMessage::NudgeSelected { delta_x, delta_y }) => {
|
||||
|
|
|
@ -267,8 +267,8 @@ impl BrushToolData {
|
|||
};
|
||||
|
||||
self.layer = Some(layer);
|
||||
for (node, node_id) in document.network().upstream_flow_back_from_nodes(vec![layer.to_node()], true) {
|
||||
if node.name == "Brush" {
|
||||
for (node, node_id) in document.network().upstream_flow_back_from_nodes(vec![layer.to_node()], graph_craft::document::FlowType::HorizontalFlow) {
|
||||
if node.name == "Brush" && node_id != layer.to_node() {
|
||||
let points_input = node.inputs.get(2)?;
|
||||
let NodeInput::Value {
|
||||
tagged_value: TaggedValue::BrushStrokes(strokes),
|
||||
|
|
|
@ -325,7 +325,7 @@ impl SelectToolData {
|
|||
document.network(),
|
||||
&document
|
||||
.network()
|
||||
.upstream_flow_back_from_nodes(vec![node], false)
|
||||
.upstream_flow_back_from_nodes(vec![node], graph_craft::document::FlowType::UpstreamFlow)
|
||||
.enumerate()
|
||||
.map(|(index, (_, node_id))| (node_id, NodeId(index as u64)))
|
||||
.collect(),
|
||||
|
@ -365,8 +365,9 @@ impl SelectToolData {
|
|||
|
||||
// Delete the duplicated layers
|
||||
for layer_ancestors in document.metadata().shallowest_unique_layers(self.layers_dragging.iter().copied()) {
|
||||
responses.add(GraphOperationMessage::DeleteLayer {
|
||||
id: layer_ancestors.last().unwrap().to_node(),
|
||||
responses.add(NodeGraphMessage::DeleteNodes {
|
||||
node_ids: vec![layer_ancestors.last().unwrap().to_node()],
|
||||
reconnect: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -275,14 +275,28 @@ impl NodeRuntime {
|
|||
continue;
|
||||
};
|
||||
|
||||
enum IntrospectedData<'a> {
|
||||
GraphicElement(&'a graphene_core::GraphicElement),
|
||||
Artboard(&'a graphene_core::Artboard),
|
||||
}
|
||||
|
||||
let introspected_data_output = introspected_data
|
||||
.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElement>>()
|
||||
.and_then(|io_data| Some(IntrospectedData::GraphicElement(&io_data.output)))
|
||||
.or_else(|| {
|
||||
introspected_data
|
||||
.downcast_ref::<IORecord<Footprint, graphene_core::Artboard>>()
|
||||
.and_then(|io_data| Some(IntrospectedData::Artboard(&io_data.output)))
|
||||
});
|
||||
|
||||
let graphic_element = match introspected_data_output {
|
||||
Some(IntrospectedData::GraphicElement(graphic_element)) => Some(graphic_element.clone()),
|
||||
Some(IntrospectedData::Artboard(artboard)) => Some(artboard.clone().into()),
|
||||
_ => None,
|
||||
};
|
||||
// If this is `GraphicElement` data:
|
||||
// Regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
|
||||
if let Some(io_data) = introspected_data.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElement>>() {
|
||||
let graphic_element = &io_data.output;
|
||||
|
||||
// UPDATE CLICK TARGETS
|
||||
|
||||
// Get the previously stored click targets and wipe them out, then regenerate them
|
||||
if let Some(graphic_element) = graphic_element {
|
||||
let click_targets = self.click_targets.entry(parent_network_node_id).or_default();
|
||||
click_targets.clear();
|
||||
graphic_element.add_click_targets(click_targets);
|
||||
|
|
4
frontend/assets/icon-16px-solid/stack.svg
Normal file
4
frontend/assets/icon-16px-solid/stack.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.4,9.7l-1.9-1-1.2.7,2,1.1-6,3.3c-.3.1-.6.1-.9,0l-6-3.3,2-1.1-1.2-.7-1.9,1c-.7.4-.7,1.1,0,1.5l6.6,3.6c.6.3,1.2.3,1.8,0l6.6-3.6c.7-.4.7-1.1,0-1.5Z" />
|
||||
<path d="M7.1,9.9L.6,6.3c-.7-.4-.7-1.1,0-1.5L7.1,1.2c.6-.3,1.2-.3,1.8,0l6.6,3.6c.7.4.7,1.1,0,1.5l-6.6,3.6c-.6.3-1.2.3-1.8,0Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 360 B |
|
@ -34,7 +34,7 @@ TypeScript files which serve as the JS interface to the WASM bindings for the ed
|
|||
|
||||
Instantiates the WASM and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the WASM bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same WASM module instance. The function returns an object where `raw` is the WASM module, `instance` is the editor, and `subscriptions` is the subscription router (described below).
|
||||
|
||||
`initWasm()` occurs in `main.ts` right before the Svelte application exists, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the state providers described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.raw`, `editor.instance`, or `editor.subscriptions`.
|
||||
`initWasm()` occurs in `main.ts` right before the Svelte application exists, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the state providers described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.raw`, `editor.handle`, or `editor.subscriptions`.
|
||||
|
||||
### Message definitions: `messages.ts`
|
||||
|
||||
|
|
|
@ -138,6 +138,8 @@
|
|||
linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(#ffffff, #ffffff);
|
||||
--color-transparent-checkered-background-size: 16px 16px;
|
||||
--color-transparent-checkered-background-position: 0 0, 8px 8px;
|
||||
--color-transparent-checkered-background-size-mini: 8px 8px;
|
||||
--color-transparent-checkered-background-position-mini: 0 0, 4px 4px;
|
||||
|
||||
--background-inactive-stripes: repeating-linear-gradient(
|
||||
-45deg,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerStructureJs, UpdateLayersPanelOptionsLayout } from "@graphite/wasm-communication/messages";
|
||||
import type { DataBuffer, LayerClassification, LayerPanelEntry } from "@graphite/wasm-communication/messages";
|
||||
import type { DataBuffer, LayerPanelEntry } from "@graphite/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
@ -165,7 +165,7 @@
|
|||
|
||||
const name = (e.target instanceof HTMLInputElement && e.target.value) || "";
|
||||
editor.handle.setLayerName(listing.entry.id, name);
|
||||
listing.entry.name = name;
|
||||
listing.entry.alias = name;
|
||||
}
|
||||
|
||||
async function onEditLayerNameDeselect(listing: LayerListingInfo) {
|
||||
|
@ -174,7 +174,7 @@
|
|||
layers = layers;
|
||||
|
||||
// Set it back to the original name if the user didn't enter a new name
|
||||
if (document.activeElement instanceof HTMLInputElement) document.activeElement.value = listing.entry.name;
|
||||
if (document.activeElement instanceof HTMLInputElement) document.activeElement.value = listing.entry.alias;
|
||||
|
||||
// Deselect the text so it doesn't appear selected while the input field becomes disabled and styled to look like regular text
|
||||
window.getSelection()?.removeAllRanges();
|
||||
|
@ -203,10 +203,6 @@
|
|||
editor.handle.deselectAllLayers();
|
||||
}
|
||||
|
||||
function isNestingLayer(layerClassification: LayerClassification) {
|
||||
return layerClassification === "Folder" || layerClassification === "Artboard";
|
||||
}
|
||||
|
||||
function calculateDragIndex(tree: LayoutCol, clientY: number, select?: () => void): DraggingData {
|
||||
const treeChildren = tree.div()?.children;
|
||||
const treeOffset = tree.div()?.getBoundingClientRect().top;
|
||||
|
@ -248,7 +244,7 @@
|
|||
}
|
||||
// Inserting below current row
|
||||
else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) {
|
||||
if (isNestingLayer(layer.layerClassification)) {
|
||||
if (layer.childrenAllowed) {
|
||||
insertParentId = layer.id;
|
||||
insertDepth = layer.depth;
|
||||
insertIndex = 0;
|
||||
|
@ -381,7 +377,6 @@
|
|||
classes={{
|
||||
selected: fakeHighlight !== undefined ? fakeHighlight === listing.entry.id : $nodeGraph.selected.includes(listing.entry.id),
|
||||
"insert-folder": (draggingData?.highlightFolder || false) && draggingData?.insertParentId === listing.entry.id,
|
||||
"nesting-layer": isNestingLayer(listing.entry.layerClassification),
|
||||
}}
|
||||
styles={{ "--layer-indent-levels": `${listing.entry.depth - 1}` }}
|
||||
data-layer
|
||||
|
@ -391,32 +386,29 @@
|
|||
on:dragstart={(e) => draggable && dragStart(e, listing)}
|
||||
on:click={(e) => selectLayerWithModifiers(e, listing)}
|
||||
>
|
||||
{#if isNestingLayer(listing.entry.layerClassification)}
|
||||
{#if listing.entry.childrenAllowed}
|
||||
<button
|
||||
class="expand-arrow"
|
||||
class:expanded={listing.entry.expanded}
|
||||
disabled={!listing.entry.hasChildren}
|
||||
disabled={!listing.entry.childrenPresent}
|
||||
on:click|stopPropagation={() => handleExpandArrowClick(listing.entry.id)}
|
||||
tabindex="0"
|
||||
/>
|
||||
{#if listing.entry.layerClassification === "Artboard"}
|
||||
<IconLabel icon="Artboard" class={"layer-type-icon"} />
|
||||
{:else if listing.entry.layerClassification === "Folder"}
|
||||
<IconLabel icon="Folder" class={"layer-type-icon"} />
|
||||
{/if}
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
|
||||
{@html $nodeGraph.thumbnails.get(listing.entry.id)}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
|
||||
{@html $nodeGraph.thumbnails.get(listing.entry.id)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if listing.entry.name === "Artboard"}
|
||||
<IconLabel icon="Artboard" class={"layer-type-icon"} />
|
||||
{/if}
|
||||
<LayoutRow class="layer-name" on:dblclick={() => onEditLayerName(listing)}>
|
||||
<input
|
||||
data-text-input
|
||||
type="text"
|
||||
value={listing.entry.name}
|
||||
placeholder={listing.entry.layerClassification}
|
||||
value={listing.entry.alias}
|
||||
placeholder={listing.entry.name}
|
||||
disabled={!listing.editingName}
|
||||
on:blur={() => onEditLayerNameDeselect(listing)}
|
||||
on:keydown={(e) => e.key === "Escape" && onEditLayerNameDeselect(listing)}
|
||||
|
@ -503,11 +495,7 @@
|
|||
border-radius: 2px;
|
||||
height: 32px;
|
||||
margin: 0 4px;
|
||||
padding-left: calc(4px + var(--layer-indent-levels) * 16px);
|
||||
|
||||
&.nesting-layer {
|
||||
padding-left: calc(var(--layer-indent-levels) * 16px);
|
||||
}
|
||||
padding-left: calc(var(--layer-indent-levels) * 16px);
|
||||
|
||||
&.selected {
|
||||
background: var(--color-4-dimgray);
|
||||
|
@ -557,17 +545,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.layer-type-icon {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 36px;
|
||||
height: 24px;
|
||||
background: white;
|
||||
margin-left: 4px;
|
||||
border-radius: 2px;
|
||||
flex: 0 0 auto;
|
||||
background: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size-mini);
|
||||
background-position: var(--color-transparent-checkered-background-position-mini);
|
||||
|
||||
&:first-child {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: calc(100% - 4px);
|
||||
|
@ -576,6 +566,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.layer-type-icon {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 8px;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
flex: 1 1 100%;
|
||||
margin: 0 8px;
|
||||
|
|
|
@ -9,8 +9,10 @@
|
|||
import type { FrontendNodeLink, FrontendNodeType, FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
@ -47,7 +49,9 @@
|
|||
let disconnecting: { nodeId: bigint; inputIndex: number; linkIndex: number } | undefined = undefined;
|
||||
let nodeLinkPaths: LinkPath[] = [];
|
||||
let searchTerm = "";
|
||||
let nodeListLocation: { x: number; y: number } | undefined = undefined;
|
||||
let contextMenuOpenCoordinates: { x: number; y: number } | undefined = undefined;
|
||||
let toggleDisplayAsLayerNodeId: bigint | undefined = undefined;
|
||||
let toggleDisplayAsLayerCurrentlyIsNode: boolean = false;
|
||||
|
||||
let inputs: SVGSVGElement[][] = [];
|
||||
let outputs: SVGSVGElement[][] = [];
|
||||
|
@ -58,8 +62,8 @@
|
|||
$: gridSpacing = calculateGridSpacing(transform.scale);
|
||||
$: dotRadius = 1 + Math.floor(transform.scale - 0.5 + 0.001) / 2;
|
||||
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
|
||||
$: nodeListX = ((nodeListLocation?.x || 0) + transform.x) * transform.scale;
|
||||
$: nodeListY = ((nodeListLocation?.y || 0) + transform.y) * transform.scale;
|
||||
$: contextMenuX = ((contextMenuOpenCoordinates?.x || 0) + transform.x) * transform.scale;
|
||||
$: contextMenuY = ((contextMenuOpenCoordinates?.y || 0) + transform.y) * transform.scale;
|
||||
|
||||
let appearAboveMouse = false;
|
||||
let appearRightOfMouse = false;
|
||||
|
@ -69,8 +73,8 @@
|
|||
if (!bounds) return;
|
||||
const { width, height } = bounds;
|
||||
|
||||
appearRightOfMouse = nodeListX > width - ADD_NODE_MENU_WIDTH;
|
||||
appearAboveMouse = nodeListY > height - ADD_NODE_MENU_HEIGHT;
|
||||
appearRightOfMouse = contextMenuX > width - ADD_NODE_MENU_WIDTH;
|
||||
appearAboveMouse = contextMenuY > height - ADD_NODE_MENU_HEIGHT;
|
||||
})();
|
||||
|
||||
$: linkPathInProgress = createLinkPathInProgress(linkInProgressFromConnector, linkInProgressToConnector);
|
||||
|
@ -127,7 +131,7 @@
|
|||
const to = linkInProgressToConnector instanceof SVGSVGElement ? connectorToNodeIndex(linkInProgressToConnector) : undefined;
|
||||
|
||||
const linkStart = $nodeGraph.nodes.find((n) => n.id === from?.nodeId)?.isLayer || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === to?.nodeId)?.isLayer && to?.index !== 0) || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === to?.nodeId)?.isLayer && to?.index == 0) || false;
|
||||
return createWirePath(linkInProgressFromConnector, linkInProgressToConnector, linkStart, linkEnd);
|
||||
}
|
||||
return undefined;
|
||||
|
@ -169,7 +173,7 @@
|
|||
if (disconnecting?.linkIndex === index) return [];
|
||||
|
||||
const linkStart = $nodeGraph.nodes.find((n) => n.id === link.linkStart)?.isLayer || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === link.linkEnd)?.isLayer && link.linkEndInputIndex !== 0n) || false;
|
||||
const linkEnd = ($nodeGraph.nodes.find((n) => n.id === link.linkEnd)?.isLayer && Number(link.linkEndInputIndex) == 0) || false;
|
||||
|
||||
return [createWirePath(nodeOutput, nodeInput.getBoundingClientRect(), linkStart, linkEnd)];
|
||||
});
|
||||
|
@ -253,8 +257,9 @@
|
|||
|
||||
function createWirePath(outputPort: SVGSVGElement, inputPort: SVGSVGElement | DOMRect, verticalOut: boolean, verticalIn: boolean): LinkPath {
|
||||
const inputPortRect = inputPort instanceof DOMRect ? inputPort : inputPort.getBoundingClientRect();
|
||||
const outputPortRect = outputPort.getBoundingClientRect();
|
||||
|
||||
const pathString = buildWirePathString(outputPort.getBoundingClientRect(), inputPortRect, verticalOut, verticalIn);
|
||||
const pathString = buildWirePathString(outputPortRect, inputPortRect, verticalOut, verticalIn);
|
||||
const dataType = outputPort.getAttribute("data-datatype") || "general";
|
||||
|
||||
return { pathString, dataType, thick: verticalIn && verticalOut };
|
||||
|
@ -310,7 +315,7 @@
|
|||
|
||||
function keydown(e: KeyboardEvent) {
|
||||
if (e.key.toLowerCase() === "escape") {
|
||||
nodeListLocation = undefined;
|
||||
contextMenuOpenCoordinates = undefined;
|
||||
document.removeEventListener("keydown", keydown);
|
||||
linkInProgressFromConnector = undefined;
|
||||
// linkInProgressFromLayerTop = undefined;
|
||||
|
@ -319,7 +324,7 @@
|
|||
}
|
||||
|
||||
function loadNodeList(e: PointerEvent, graphBounds: DOMRect) {
|
||||
nodeListLocation = {
|
||||
contextMenuOpenCoordinates = {
|
||||
x: (e.clientX - graphBounds.x) / transform.scale - transform.x,
|
||||
y: (e.clientY - graphBounds.y) / transform.scale - transform.y,
|
||||
};
|
||||
|
@ -340,23 +345,33 @@
|
|||
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
|
||||
const nodeIdString = node?.getAttribute("data-node") || undefined;
|
||||
const nodeId = nodeIdString ? BigInt(nodeIdString) : undefined;
|
||||
const nodeList = (e.target as HTMLElement).closest("[data-node-list]") as HTMLElement | undefined;
|
||||
const contextMenu = (e.target as HTMLElement).closest("[data-context-menu]") as HTMLElement | undefined;
|
||||
|
||||
// Create the add node popup on right click, then exit
|
||||
if (rmb) {
|
||||
toggleDisplayAsLayerNodeId = undefined;
|
||||
|
||||
if (node) {
|
||||
toggleDisplayAsLayerNodeId = nodeId;
|
||||
toggleDisplayAsLayerCurrentlyIsNode = !($nodeGraph.nodes.find((node) => node.id === nodeId)?.isLayer || false);
|
||||
}
|
||||
|
||||
const graphBounds = graph?.getBoundingClientRect();
|
||||
if (!graphBounds) return;
|
||||
|
||||
loadNodeList(e, graphBounds);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user is clicking on the add nodes list, exit here
|
||||
if (lmb && nodeList) return;
|
||||
// If the user is clicking on the add nodes list or context menu, exit here
|
||||
if (lmb && contextMenu) return;
|
||||
|
||||
// Since the user is clicking elsewhere in the graph, ensure the add nodes list is closed
|
||||
if (lmb) {
|
||||
nodeListLocation = undefined;
|
||||
contextMenuOpenCoordinates = undefined;
|
||||
linkInProgressFromConnector = undefined;
|
||||
toggleDisplayAsLayerNodeId = undefined;
|
||||
// linkInProgressFromLayerTop = undefined;
|
||||
// linkInProgressFromLayerBottom = undefined;
|
||||
}
|
||||
|
@ -474,7 +489,7 @@
|
|||
if (panning) {
|
||||
transform.x += e.movementX / transform.scale;
|
||||
transform.y += e.movementY / transform.scale;
|
||||
} else if (linkInProgressFromConnector && !nodeListLocation) {
|
||||
} else if (linkInProgressFromConnector && !contextMenuOpenCoordinates) {
|
||||
const target = e.target as Element | undefined;
|
||||
const dot = (target?.closest(`[data-port="input"]`) || undefined) as SVGSVGElement | undefined;
|
||||
if (dot) {
|
||||
|
@ -545,6 +560,20 @@
|
|||
editor.handle.toggleLayerVisibility(id);
|
||||
}
|
||||
|
||||
function toggleLayerDisplay(displayAsLayer: boolean) {
|
||||
let node = $nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId);
|
||||
if (node !== undefined) {
|
||||
contextMenuOpenCoordinates = undefined;
|
||||
editor.handle.setToNodeOrLayer(node.id, displayAsLayer);
|
||||
toggleDisplayAsLayerCurrentlyIsNode = !($nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId)?.isLayer || false);
|
||||
toggleDisplayAsLayerNodeId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) {
|
||||
return $nodeGraph.nodes.find((node) => node.id === toggleDisplayAsLayerNodeId)?.canBeLayer || false;
|
||||
}
|
||||
|
||||
function connectorToNodeIndex(svg: SVGSVGElement): { nodeId: bigint; index: number } | undefined {
|
||||
const node = svg.closest("[data-node]");
|
||||
|
||||
|
@ -568,7 +597,7 @@
|
|||
const selectedNodeId = $nodeGraph.selected[0];
|
||||
const selectedNode = nodesContainer?.querySelector(`[data-node="${String(selectedNodeId)}"]`) || undefined;
|
||||
|
||||
// Check that neither the input or output of the selected node are already connected.
|
||||
// Check that neither the primary input or output of the selected node are already connected.
|
||||
const notConnected = $nodeGraph.links.findIndex((link) => link.linkStart === selectedNodeId || (link.linkEnd === selectedNodeId && link.linkEndInputIndex === BigInt(0))) === -1;
|
||||
const input = selectedNode?.querySelector(`[data-port="input"]`) || undefined;
|
||||
const output = selectedNode?.querySelector(`[data-port="output"]`) || undefined;
|
||||
|
@ -589,13 +618,16 @@
|
|||
const selectedNodeBounds = selectedNode.getBoundingClientRect();
|
||||
const containerBoundsBounds = theNodesContainer.getBoundingClientRect();
|
||||
|
||||
return editor.handle.rectangleIntersects(
|
||||
new Float64Array(wireCurveLocations.map((loc) => loc.x)),
|
||||
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
|
||||
selectedNodeBounds.top - containerBoundsBounds.y,
|
||||
selectedNodeBounds.left - containerBoundsBounds.x,
|
||||
selectedNodeBounds.bottom - containerBoundsBounds.y,
|
||||
selectedNodeBounds.right - containerBoundsBounds.x,
|
||||
return (
|
||||
link.linkEnd != selectedNodeId &&
|
||||
editor.handle.rectangleIntersects(
|
||||
new Float64Array(wireCurveLocations.map((loc) => loc.x)),
|
||||
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
|
||||
selectedNodeBounds.top - containerBoundsBounds.y,
|
||||
selectedNodeBounds.left - containerBoundsBounds.x,
|
||||
selectedNodeBounds.bottom - containerBoundsBounds.y,
|
||||
selectedNodeBounds.right - containerBoundsBounds.x,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -603,8 +635,7 @@
|
|||
if (link) {
|
||||
const isLayer = $nodeGraph.nodes.find((n) => n.id === selectedNodeId)?.isLayer;
|
||||
|
||||
editor.handle.connectNodesByLink(link.linkStart, 0, selectedNodeId, isLayer ? 1 : 0);
|
||||
editor.handle.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
|
||||
editor.handle.insertNodeBetween(link.linkEnd, Number(link.linkEndInputIndex), 0, selectedNodeId, 0, Number(link.linkStartOutputIndex), link.linkStart);
|
||||
if (!isLayer) editor.handle.shiftNode(selectedNodeId);
|
||||
}
|
||||
}
|
||||
|
@ -629,17 +660,17 @@
|
|||
}
|
||||
} else if (linkInProgressFromConnector && !initialDisconnecting) {
|
||||
// If the add node menu is already open, we don't want to open it again
|
||||
if (nodeListLocation) return;
|
||||
if (contextMenuOpenCoordinates) return;
|
||||
|
||||
const graphBounds = graph?.getBoundingClientRect();
|
||||
if (!graphBounds) return;
|
||||
|
||||
// Create the node list, which should set nodeListLocation to a valid value
|
||||
loadNodeList(e, graphBounds);
|
||||
if (!nodeListLocation) return;
|
||||
let nodeListLocation2: { x: number; y: number } = nodeListLocation;
|
||||
if (!contextMenuOpenCoordinates) return;
|
||||
let contextMenuLocation2: { x: number; y: number } = contextMenuOpenCoordinates;
|
||||
|
||||
linkInProgressToConnector = new DOMRect((nodeListLocation2.x + transform.x) * transform.scale + graphBounds.x, (nodeListLocation2.y + transform.y) * transform.scale + graphBounds.y);
|
||||
linkInProgressToConnector = new DOMRect((contextMenuLocation2.x + transform.x) * transform.scale + graphBounds.x, (contextMenuLocation2.y + transform.y) * transform.scale + graphBounds.y);
|
||||
|
||||
return;
|
||||
} else if (draggingNodes) {
|
||||
|
@ -665,13 +696,13 @@
|
|||
}
|
||||
|
||||
function createNode(nodeType: string) {
|
||||
if (!nodeListLocation) return;
|
||||
if (!contextMenuOpenCoordinates) return;
|
||||
|
||||
const inputNodeConnectionIndex = 0;
|
||||
const x = Math.round(nodeListLocation.x / GRID_SIZE);
|
||||
const y = Math.round(nodeListLocation.y / GRID_SIZE) - 1;
|
||||
const x = Math.round(contextMenuOpenCoordinates.x / GRID_SIZE);
|
||||
const y = Math.round(contextMenuOpenCoordinates.y / GRID_SIZE) - 1;
|
||||
const inputConnectedNodeID = editor.handle.createNode(nodeType, x, y);
|
||||
nodeListLocation = undefined;
|
||||
contextMenuOpenCoordinates = undefined;
|
||||
|
||||
if (!linkInProgressFromConnector) return;
|
||||
const from = connectorToNodeIndex(linkInProgressFromConnector);
|
||||
|
@ -702,17 +733,22 @@
|
|||
return borderMask(boxes, nodeWidth, nodeHeight);
|
||||
}
|
||||
|
||||
function layerBorderMask(nodeWidth: number): string {
|
||||
function layerBorderMask(nodeWidthFromThumbnail: number, nodeChainAreaLeftExtension: number): string {
|
||||
const NODE_HEIGHT = 2 * 24;
|
||||
const THUMBNAIL_WIDTH = 72 + 8 * 2;
|
||||
const FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT = 2;
|
||||
|
||||
const nodeWidth = nodeWidthFromThumbnail + nodeChainAreaLeftExtension;
|
||||
|
||||
const boxes: { x: number; y: number; width: number; height: number }[] = [];
|
||||
|
||||
// Left input
|
||||
boxes.push({ x: -8, y: 16, width: 16, height: 16 });
|
||||
if (nodeChainAreaLeftExtension > 0) {
|
||||
boxes.push({ x: -8, y: 16, width: 16, height: 16 });
|
||||
}
|
||||
|
||||
// Thumbnail
|
||||
boxes.push({ x: 28, y: -FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT, width: THUMBNAIL_WIDTH, height: NODE_HEIGHT + FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT * 2 });
|
||||
boxes.push({ x: nodeChainAreaLeftExtension - 8, y: -FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT, width: THUMBNAIL_WIDTH, height: NODE_HEIGHT + FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT * 2 });
|
||||
|
||||
// Right visibility button
|
||||
boxes.push({ x: nodeWidth - 12, y: (NODE_HEIGHT - 24) / 2, width: 24, height: 24 });
|
||||
|
@ -745,33 +781,63 @@
|
|||
style:--dot-radius={`${dotRadius}px`}
|
||||
>
|
||||
<!-- Right click menu for adding nodes -->
|
||||
{#if nodeListLocation}
|
||||
{#if contextMenuOpenCoordinates}
|
||||
<LayoutCol
|
||||
class="node-list"
|
||||
data-node-list
|
||||
class="context-menu"
|
||||
data-context-menu
|
||||
styles={{
|
||||
transform: `translate(${appearRightOfMouse ? -100 : 0}%, ${appearAboveMouse ? -100 : 0}%)`,
|
||||
left: `${nodeListX}px`,
|
||||
top: `${nodeListY}px`,
|
||||
width: `${ADD_NODE_MENU_WIDTH}px`,
|
||||
height: `${ADD_NODE_MENU_HEIGHT}px`,
|
||||
left: `${contextMenuX}px`,
|
||||
top: `${contextMenuY}px`,
|
||||
...(toggleDisplayAsLayerNodeId === undefined
|
||||
? {
|
||||
transform: `translate(${appearRightOfMouse ? -100 : 0}%, ${appearAboveMouse ? -100 : 0}%)`,
|
||||
width: `${ADD_NODE_MENU_WIDTH}px`,
|
||||
height: `${ADD_NODE_MENU_HEIGHT}px`,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||
<div class="list-results" on:wheel|passive|stopPropagation>
|
||||
{#each nodeCategories as nodeCategory}
|
||||
<details open={nodeCategory[1].open}>
|
||||
<summary>
|
||||
<TextLabel>{nodeCategory[0]}</TextLabel>
|
||||
</summary>
|
||||
{#each nodeCategory[1].nodes as nodeType}
|
||||
<TextButton label={nodeType.name} action={() => createNode(nodeType.name)} />
|
||||
{/each}
|
||||
</details>
|
||||
{:else}
|
||||
<TextLabel>No search results</TextLabel>
|
||||
{/each}
|
||||
</div>
|
||||
{#if toggleDisplayAsLayerNodeId === undefined}
|
||||
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||
<div class="list-results" on:wheel|passive|stopPropagation>
|
||||
{#each nodeCategories as nodeCategory}
|
||||
<details open={nodeCategory[1].open}>
|
||||
<summary>
|
||||
<TextLabel>{nodeCategory[0]}</TextLabel>
|
||||
</summary>
|
||||
{#each nodeCategory[1].nodes as nodeType}
|
||||
<TextButton label={nodeType.name} action={() => createNode(nodeType.name)} />
|
||||
{/each}
|
||||
</details>
|
||||
{:else}
|
||||
<TextLabel>No search results</TextLabel>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<LayoutRow class="toggle-layer-or-node">
|
||||
<TextLabel>Display as</TextLabel>
|
||||
<RadioInput
|
||||
selectedIndex={toggleDisplayAsLayerCurrentlyIsNode ? 0 : 1}
|
||||
entries={[
|
||||
{
|
||||
value: "node",
|
||||
label: "Node",
|
||||
action: () => {
|
||||
toggleLayerDisplay(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
value: "layer",
|
||||
label: "Layer",
|
||||
action: () => {
|
||||
toggleLayerDisplay(true);
|
||||
},
|
||||
},
|
||||
]}
|
||||
disabled={!canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId)}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
<!-- Node connection links -->
|
||||
|
@ -801,6 +867,7 @@
|
|||
style:--data-color={`var(--color-data-${node.primaryOutput?.dataType || "general"})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`}
|
||||
style:--label-width={labelWidthGridCells}
|
||||
style:--node-chain-area-left-extension={node.exposedInputs.length === 0 ? 0 : 1.5}
|
||||
data-node={node.id}
|
||||
bind:this={nodeElements[nodeIndex]}
|
||||
>
|
||||
|
@ -808,29 +875,6 @@
|
|||
<span class="node-error faded" transition:fade={FADE_TRANSITION} data-node-error>{node.errors}</span>
|
||||
<span class="node-error hover" transition:fade={FADE_TRANSITION} data-node-error>{node.errors}</span>
|
||||
{/if}
|
||||
<div class="node-chain" />
|
||||
<!-- Layer input port (from left) -->
|
||||
<div class="input ports">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
data-datatype={node.primaryInput?.dataType}
|
||||
style:--data-color={`var(--color-data-${node.primaryInput?.dataType})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`}
|
||||
bind:this={inputs[nodeIndex][0]}
|
||||
>
|
||||
{#if node.primaryInput}
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\nConnected to ${node.primaryInput?.connected || "nothing"}`}</title>
|
||||
{/if}
|
||||
{#if node.primaryInput?.connected}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(node.id)}
|
||||
{@html $nodeGraph.thumbnails.get(node.id)}
|
||||
|
@ -847,10 +891,10 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
|
||||
bind:this={outputs[nodeIndex][0]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${node.primaryOutput.connected || "nothing"}`}</title>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${`${node.primaryOutput.connected}, port index ${node.primaryOutput.connectedIndex}` || "nothing"}`}</title>
|
||||
{#if node.primaryOutput.connected}
|
||||
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
|
||||
{#if $nodeGraph.nodes.find((n) => n.id === node.primaryOutput?.connected)?.isLayer}
|
||||
{#if Number(node.primaryOutput?.connectedIndex) === 0 && $nodeGraph.nodes.find((n) => n.id === node.primaryOutput?.connected)?.isLayer}
|
||||
<path d="M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
{:else}
|
||||
|
@ -864,15 +908,17 @@
|
|||
viewBox="0 0 8 12"
|
||||
class="port bottom"
|
||||
data-port="input"
|
||||
data-datatype={stackDataInput.dataType}
|
||||
style:--data-color={`var(--color-data-${stackDataInput.dataType})`}
|
||||
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType}-dim)`}
|
||||
bind:this={inputs[nodeIndex][1]}
|
||||
data-datatype={node.primaryInput?.dataType}
|
||||
style:--data-color={`var(--color-data-${node.primaryInput?.dataType})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`}
|
||||
bind:this={inputs[nodeIndex][0]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(stackDataInput)}\nConnected to ${stackDataInput.connected || "nothing"}`}</title>
|
||||
{#if stackDataInput.connected}
|
||||
{#if node.primaryInput}
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\nConnected to ${node.primaryInput?.connected || "nothing"}`}</title>
|
||||
{/if}
|
||||
{#if node.primaryInput?.connected}
|
||||
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
|
||||
{#if $nodeGraph.nodes.find((n) => n.id === stackDataInput.connected)?.isLayer}
|
||||
{#if $nodeGraph.nodes.find((n) => n.id === node.primaryInput?.connected)?.isLayer}
|
||||
<path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
{:else}
|
||||
|
@ -880,10 +926,32 @@
|
|||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Layer input port (from left) -->
|
||||
{#if node.exposedInputs.length > 0}
|
||||
<div class="input ports">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
data-datatype={stackDataInput.dataType}
|
||||
style:--data-color={`var(--color-data-${stackDataInput.dataType})`}
|
||||
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType}-dim)`}
|
||||
bind:this={inputs[nodeIndex][1]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(stackDataInput)}\nConnected to ${stackDataInput.connected || "nothing"}`}</title>
|
||||
{#if stackDataInput.connected}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="details">
|
||||
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
|
||||
<span title={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined} bind:offsetWidth={layerNameLabelWidths[String(node.id)]}>
|
||||
{node.alias || "Layer"}
|
||||
{node.alias}
|
||||
</span>
|
||||
</div>
|
||||
<IconButton
|
||||
|
@ -898,7 +966,10 @@
|
|||
<defs>
|
||||
<clipPath id={clipPathId}>
|
||||
<!-- Keep this equation in sync with the equivalent one in the CSS rule for `.layer { width: ... }` below -->
|
||||
<path clip-rule="evenodd" d={layerBorderMask(36 + 72 + 8 + 24 * Math.max(3, labelWidthGridCells) + 8 + 12 + extraWidthToReachGridMultiple)} />
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d={layerBorderMask(72 + 8 + 24 * Math.max(3, labelWidthGridCells) + 8 + 12 + extraWidthToReachGridMultiple, node.exposedInputs.length === 0 ? 0 : 36)}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
@ -997,7 +1068,7 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
|
||||
bind:this={outputs[nodeIndex][0]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${node.primaryOutput.connected || "nothing"}`}</title>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\nConnected to ${`${node.primaryOutput.connected}, port index ${node.primaryOutput.connectedIndex}` || "nothing"}`}</title>
|
||||
{#if node.primaryOutput.connected}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
|
@ -1016,7 +1087,7 @@
|
|||
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
|
||||
bind:this={outputs[nodeIndex][outputIndex + (node.primaryOutput ? 1 : 0)]}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(parameter)}\nConnected to ${parameter.connected || "nothing"}`}</title>
|
||||
<title>{`${dataTypeTooltip(parameter)}\nConnected to ${`${parameter.connected}, port index ${parameter.connectedIndex}` || "nothing"}`}</title>
|
||||
{#if parameter.connected}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
|
@ -1077,13 +1148,14 @@
|
|||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.node-list {
|
||||
.context-menu {
|
||||
width: max-content;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
z-index: 3;
|
||||
background-color: var(--color-3-darkgray);
|
||||
border-radius: 4px;
|
||||
|
||||
.text-input {
|
||||
flex: 0 0 auto;
|
||||
|
@ -1093,11 +1165,15 @@
|
|||
.list-results {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
// Together with the `margin-right: 4px;` on `details` below, this keeps a gap between the listings and the scrollbar
|
||||
margin-right: -4px;
|
||||
|
||||
details {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// Together with the `margin-right: -4px;` on `.list-results` above, this keeps a gap between the listings and the scrollbar
|
||||
margin-right: 4px;
|
||||
|
||||
&[open] summary .text-label::before {
|
||||
transform: rotate(90deg);
|
||||
|
@ -1133,6 +1209,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-layer-or-node .text-label {
|
||||
line-height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.wires {
|
||||
|
@ -1294,10 +1375,12 @@
|
|||
|
||||
.layer {
|
||||
border-radius: 8px;
|
||||
--half-visibility-button: 12px;
|
||||
--extra-width-to-reach-grid-multiple: 8px;
|
||||
--node-chain-area-left-extension: 0;
|
||||
// Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above
|
||||
width: calc(36px + 72px + 8px + 24px * Max(3, var(--label-width)) + 8px + var(--half-visibility-button) + var(--extra-width-to-reach-grid-multiple));
|
||||
width: calc(72px + 8px + 24px * Max(3, var(--label-width)) + 8px + 12px + var(--extra-width-to-reach-grid-multiple));
|
||||
padding-left: calc(var(--node-chain-area-left-extension) * 24px);
|
||||
margin-left: calc((1.5 - var(--node-chain-area-left-extension)) * 24px);
|
||||
|
||||
&::after {
|
||||
border: 1px solid var(--color-5-dullgray);
|
||||
|
@ -1309,10 +1392,6 @@
|
|||
background: rgba(66, 66, 66, 0.4);
|
||||
}
|
||||
|
||||
.node-chain {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
background: var(--color-2-mildblack);
|
||||
border: 1px solid var(--data-color-dim);
|
||||
|
@ -1368,7 +1447,7 @@
|
|||
|
||||
.visibility {
|
||||
position: absolute;
|
||||
right: calc(-1 * var(--half-visibility-button));
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
.visibility,
|
||||
|
|
|
@ -144,6 +144,7 @@ import Reload from "@graphite-frontend/assets/icon-16px-solid/reload.svg";
|
|||
import Rescale from "@graphite-frontend/assets/icon-16px-solid/rescale.svg";
|
||||
import Reset from "@graphite-frontend/assets/icon-16px-solid/reset.svg";
|
||||
import Settings from "@graphite-frontend/assets/icon-16px-solid/settings.svg";
|
||||
import Stack from "@graphite-frontend/assets/icon-16px-solid/stack.svg";
|
||||
import Trash from "@graphite-frontend/assets/icon-16px-solid/trash.svg";
|
||||
import ViewModeNormal from "@graphite-frontend/assets/icon-16px-solid/view-mode-normal.svg";
|
||||
import ViewModeOutline from "@graphite-frontend/assets/icon-16px-solid/view-mode-outline.svg";
|
||||
|
@ -217,6 +218,7 @@ const SOLID_16PX = {
|
|||
Rescale: { svg: Rescale, size: 16 },
|
||||
Reset: { svg: Reset, size: 16 },
|
||||
Settings: { svg: Settings, size: 16 },
|
||||
Stack: { svg: Stack, size: 16 },
|
||||
Trash: { svg: Trash, size: 16 },
|
||||
ViewModeNormal: { svg: ViewModeNormal, size: 16 },
|
||||
ViewModeOutline: { svg: ViewModeOutline, size: 16 },
|
||||
|
|
|
@ -100,11 +100,15 @@ export class FrontendGraphOutput {
|
|||
readonly resolvedType!: string | undefined;
|
||||
|
||||
readonly connected!: bigint | undefined;
|
||||
|
||||
readonly connectedIndex!: bigint | undefined;
|
||||
}
|
||||
|
||||
export class FrontendNode {
|
||||
readonly isLayer!: boolean;
|
||||
|
||||
readonly canBeLayer!: boolean;
|
||||
|
||||
readonly id!: bigint;
|
||||
|
||||
readonly alias!: string;
|
||||
|
@ -606,16 +610,23 @@ export class UpdateDocumentLayerDetails extends JsMessage {
|
|||
}
|
||||
|
||||
export class LayerPanelEntry {
|
||||
id!: bigint;
|
||||
|
||||
name!: string;
|
||||
|
||||
alias!: string;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
|
||||
layerClassification!: LayerClassification;
|
||||
childrenAllowed!: boolean;
|
||||
|
||||
childrenPresent!: boolean;
|
||||
|
||||
expanded!: boolean;
|
||||
|
||||
hasChildren!: boolean;
|
||||
@Transform(({ value }: { value: bigint }) => Number(value))
|
||||
depth!: number;
|
||||
|
||||
visible!: boolean;
|
||||
|
||||
|
@ -626,15 +637,8 @@ export class LayerPanelEntry {
|
|||
parentsUnlocked!: boolean;
|
||||
|
||||
parentId!: bigint | undefined;
|
||||
|
||||
id!: bigint;
|
||||
|
||||
@Transform(({ value }: { value: bigint }) => Number(value))
|
||||
depth!: number;
|
||||
}
|
||||
|
||||
export type LayerClassification = "Folder" | "Artboard" | "Layer";
|
||||
|
||||
export class DisplayDialogDismiss extends JsMessage {}
|
||||
|
||||
export class Font {
|
||||
|
|
|
@ -35,6 +35,7 @@ export default defineConfig({
|
|||
svelte({
|
||||
preprocess: [sveltePreprocess()],
|
||||
onwarn(warning, defaultHandler) {
|
||||
// NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
const suppressed = ["css-unused-selector", "vite-plugin-svelte-css-no-scopable-elements", "a11y-no-static-element-interactions", "a11y-no-noninteractive-element-interactions"];
|
||||
if (suppressed.includes(warning.code)) return;
|
||||
|
||||
|
|
|
@ -523,8 +523,8 @@ impl EditorHandle {
|
|||
#[wasm_bindgen(js_name = moveLayerInTree)]
|
||||
pub fn move_layer_in_tree(&self, insert_parent_id: Option<u64>, insert_index: Option<usize>) {
|
||||
let insert_parent_id = insert_parent_id.map(NodeId);
|
||||
|
||||
let parent = insert_parent_id.map(LayerNodeIdentifier::new_unchecked).unwrap_or_default();
|
||||
|
||||
let message = DocumentMessage::MoveSelectedLayersTo {
|
||||
parent,
|
||||
insert_index: insert_index.map(|x| x as isize).unwrap_or(-1),
|
||||
|
@ -568,6 +568,30 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Inserts node in-between two other nodes
|
||||
#[wasm_bindgen(js_name = insertNodeBetween)]
|
||||
pub fn insert_node_between(
|
||||
&self,
|
||||
post_node_id: u64,
|
||||
post_node_input_index: usize,
|
||||
insert_node_output_index: usize,
|
||||
insert_node_id: u64,
|
||||
insert_node_input_index: usize,
|
||||
pre_node_output_index: usize,
|
||||
pre_node_id: u64,
|
||||
) {
|
||||
let message = NodeGraphMessage::InsertNodeBetween {
|
||||
post_node_id: NodeId(post_node_id),
|
||||
post_node_input_index,
|
||||
insert_node_output_index,
|
||||
insert_node_id: NodeId(insert_node_id),
|
||||
insert_node_input_index,
|
||||
pre_node_output_index,
|
||||
pre_node_id: NodeId(pre_node_id),
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Shifts the node and its children to stop nodes going on top of each other
|
||||
#[wasm_bindgen(js_name = shiftNode)]
|
||||
pub fn shift_node(&self, node_id: u64) {
|
||||
|
@ -686,6 +710,14 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Toggle display type for a layer
|
||||
#[wasm_bindgen(js_name = setToNodeOrLayer)]
|
||||
pub fn set_to_node_or_layer(&self, id: u64, is_layer: bool) {
|
||||
let node_id = NodeId(id);
|
||||
let message = NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
||||
pub fn inject_imaginate_poll_server_status(&self) {
|
||||
self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
|
||||
|
|
|
@ -106,6 +106,34 @@ impl Artboard {
|
|||
}
|
||||
}
|
||||
|
||||
/// Contains multiple artboards.
|
||||
#[derive(Clone, Default, Debug, Hash, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ArtboardGroup {
|
||||
pub artboards: Vec<Artboard>,
|
||||
}
|
||||
|
||||
impl ArtboardGroup {
|
||||
pub const EMPTY: Self = Self { artboards: Vec::new() };
|
||||
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_artboard(&mut self, artboard: Artboard) {
|
||||
self.artboards.push(artboard);
|
||||
}
|
||||
|
||||
pub fn get_graphic_group(&self) -> GraphicGroup {
|
||||
let mut graphic_group = GraphicGroup::EMPTY;
|
||||
for artboard in self.artboards.clone() {
|
||||
let graphic_element: GraphicElement = artboard.into();
|
||||
graphic_group.push(graphic_element);
|
||||
}
|
||||
graphic_group
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConstructLayerNode<GraphicElement, Stack> {
|
||||
graphic_element: GraphicElement,
|
||||
stack: Stack,
|
||||
|
@ -157,6 +185,24 @@ async fn construct_artboard<Fut: Future<Output = GraphicGroup>>(
|
|||
clip,
|
||||
}
|
||||
}
|
||||
pub struct AddArtboardNode<Artboard, ArtboardGroup> {
|
||||
artboard: Artboard,
|
||||
artboards: ArtboardGroup,
|
||||
}
|
||||
|
||||
#[node_fn(AddArtboardNode)]
|
||||
async fn add_artboard<Data: Into<Artboard>, Fut1: Future<Output = Data>, Fut2: Future<Output = ArtboardGroup>>(
|
||||
footprint: Footprint,
|
||||
artboard: impl Node<Footprint, Output = Fut1>,
|
||||
mut artboards: impl Node<Footprint, Output = Fut2>,
|
||||
) -> ArtboardGroup {
|
||||
let artboard = self.artboard.eval(footprint).await;
|
||||
let mut artboards = self.artboards.eval(footprint).await;
|
||||
|
||||
artboards.add_artboard(artboard.into());
|
||||
|
||||
artboards
|
||||
}
|
||||
|
||||
impl From<ImageFrame<Color>> for GraphicElement {
|
||||
fn from(mut image_frame: ImageFrame<Color>) -> Self {
|
||||
|
|
|
@ -447,7 +447,7 @@ impl GraphicElementRendered for Artboard {
|
|||
}
|
||||
|
||||
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
|
||||
let subpath = Subpath::new_rect(self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2());
|
||||
click_targets.push(ClickTarget { stroke_width: 0., subpath });
|
||||
}
|
||||
|
||||
|
@ -456,6 +456,23 @@ impl GraphicElementRendered for Artboard {
|
|||
}
|
||||
}
|
||||
|
||||
impl GraphicElementRendered for crate::ArtboardGroup {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
self.get_graphic_group().render_svg(render, render_params);
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.get_graphic_group().bounding_box(transform)
|
||||
}
|
||||
|
||||
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
self.get_graphic_group().add_click_targets(click_targets);
|
||||
}
|
||||
|
||||
fn contains_artboard(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
impl GraphicElementRendered for ImageFrame<Color> {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
let transform: String = format_transform_matrix(self.transform * render.transform);
|
||||
|
|
|
@ -3,7 +3,8 @@ use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, ProtoNodeInput};
|
|||
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
pub use graphene_core::uuid::generate_uuid;
|
||||
use graphene_core::{GraphicGroup, ProtoNodeIdentifier, Type};
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::{ArtboardGroup, GraphicGroup, ProtoNodeIdentifier, Type};
|
||||
|
||||
use glam::IVec2;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
@ -76,8 +77,8 @@ pub struct DocumentNode {
|
|||
/// - Concrete example: a node that takes an image as primary input will get that image data from an upstream node that produces image output data and is evaluated first before being fed downstream.
|
||||
///
|
||||
/// This is achieved by automatically inserting `ComposeNode`s, which run the first node with the overall input and then feed the resulting output into the second node.
|
||||
/// The `ComposeNode` is basically a function composition operator: the parentheses in `F(G(x))` or circle math operator in `(G ∘ F)(x)`.
|
||||
/// For flexability, instead of being a language construct, Graphene splits out composition itself as its own low-level node so that behavior can be overridden.
|
||||
/// The `ComposeNode` is basically a function composition operator: the parentheses in `F(G(x))` or circle math operator in `(F ∘ G)(x)`.
|
||||
/// For flexibility, instead of being a language construct, Graphene splits out composition itself as its own low-level node so that behavior can be overridden.
|
||||
/// The `ComposeNode`s are then inserted during the graph rewriting step for nodes that don't opt out with `manual_composition`.
|
||||
/// Instead of node `G` feeding into node `F` feeding as the result back to the caller,
|
||||
/// the graph is rewritten so nodes `G` and `F` both feed as lambdas into the parameters of a `ComposeNode` which calls `F(G(input))` and returns the result to the caller.
|
||||
|
@ -159,6 +160,9 @@ pub struct DocumentNode {
|
|||
pub has_primary_output: bool,
|
||||
// A nested document network or a proto-node identifier.
|
||||
pub implementation: DocumentNodeImplementation,
|
||||
/// User chosen state for displaying this as a left-to-right node or bottom-to-top layer.
|
||||
#[serde(default)]
|
||||
pub is_layer: bool,
|
||||
/// Represents the eye icon for hiding/showing the node in the graph UI. When hidden, a node gets replaced with an identity node during the graph flattening step.
|
||||
#[serde(default = "return_true")]
|
||||
pub visible: bool,
|
||||
|
@ -212,6 +216,7 @@ impl Default for DocumentNode {
|
|||
manual_composition: Default::default(),
|
||||
has_primary_output: true,
|
||||
implementation: Default::default(),
|
||||
is_layer: false,
|
||||
visible: true,
|
||||
locked: Default::default(),
|
||||
metadata: Default::default(),
|
||||
|
@ -348,21 +353,24 @@ impl DocumentNode {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn is_layer(&self) -> bool {
|
||||
// TODO: Use something more robust than checking against a string.
|
||||
// TODO: Or, more fundamentally separate the concept of a layer from a node.
|
||||
self.name == "Layer"
|
||||
}
|
||||
|
||||
pub fn is_artboard(&self) -> bool {
|
||||
// TODO: Use something more robust than checking against a string.
|
||||
// TODO: Or, more fundamentally separate the concept of a layer from a node.
|
||||
self.name == "Artboard"
|
||||
}
|
||||
|
||||
pub fn is_folder(&self, network: &NodeNetwork) -> bool {
|
||||
let input_connection = self.inputs.get(0).and_then(|input| input.as_node()).and_then(|node_id| network.nodes.get(&node_id));
|
||||
input_connection.map(|node| node.is_layer()).unwrap_or(false)
|
||||
// TODO: Is this redundant with `LayerNodeIdentifier::has_children()`? Consider removing this in favor of that.
|
||||
/// Determines if a document node acting as a layer has any nested children where its secondary input eventually leads to a layer along horizontal flow.
|
||||
pub fn layer_has_child_layers(&self, network: &NodeNetwork) -> bool {
|
||||
if !self.is_layer {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.inputs.iter().skip(1).any(|input| {
|
||||
input.as_node().map_or(false, |node_id| {
|
||||
network.upstream_flow_back_from_nodes(vec![node_id], FlowType::HorizontalFlow).any(|(node, _)| node.is_layer)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -538,6 +546,16 @@ pub struct NodeNetwork {
|
|||
pub previous_outputs: Option<Vec<NodeOutput>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum FlowType {
|
||||
/// Iterate over all upstream nodes from every input (the primary and all secondary).
|
||||
UpstreamFlow,
|
||||
/// Iterate over nodes connected to the primary input.
|
||||
PrimaryFlow,
|
||||
/// Iterate over the secondary input for layer nodes and primary input for non layer nodes.
|
||||
HorizontalFlow,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for NodeNetwork {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.imports.hash(state);
|
||||
|
@ -741,18 +759,18 @@ impl NodeNetwork {
|
|||
self.previous_outputs.as_ref().map(|outputs| outputs.iter().any(|output| output.node_id == node_id))
|
||||
}
|
||||
|
||||
/// Gives an iterator to all nodes connected to the given nodes by all inputs (primary or primary + secondary depending on `only_follow_primary` choice), traversing backwards upstream starting from the given node's inputs.
|
||||
pub fn upstream_flow_back_from_nodes(&self, node_ids: Vec<NodeId>, only_follow_primary: bool) -> impl Iterator<Item = (&DocumentNode, NodeId)> {
|
||||
/// Gives an iterator to all nodes connected to the given nodes (inclusive) by all inputs (primary or primary + secondary depending on `only_follow_primary` choice), traversing backwards upstream starting from the given node's inputs.
|
||||
pub fn upstream_flow_back_from_nodes(&self, node_ids: Vec<NodeId>, flow_type: FlowType) -> impl Iterator<Item = (&DocumentNode, NodeId)> {
|
||||
FlowIter {
|
||||
stack: node_ids,
|
||||
network: self,
|
||||
only_follow_primary,
|
||||
flow_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// In the network `X -> Y -> Z`, `is_node_upstream_of_another_by_primary_flow(Z, X)` returns true.
|
||||
pub fn is_node_upstream_of_another_by_primary_flow(&self, node: NodeId, potentially_upstream_node: NodeId) -> bool {
|
||||
self.upstream_flow_back_from_nodes(vec![node], true).any(|(_, id)| id == potentially_upstream_node)
|
||||
pub fn is_node_upstream_of_another_by_horizontal_flow(&self, node: NodeId, potentially_upstream_node: NodeId) -> bool {
|
||||
self.upstream_flow_back_from_nodes(vec![node], FlowType::HorizontalFlow).any(|(_, id)| id == potentially_upstream_node)
|
||||
}
|
||||
|
||||
/// Check there are no cycles in the graph (this should never happen).
|
||||
|
@ -789,11 +807,14 @@ impl NodeNetwork {
|
|||
}
|
||||
}
|
||||
|
||||
/// Iterate over the primary inputs of nodes, so in the case of `a -> b -> c`, this would yield `c, b, a` if we started from `c`.
|
||||
/// Iterate over upstream nodes. The behavior changes based on the `flow_type` that's set.
|
||||
/// - [`FlowType::UpstreamFlow`]: iterates over all upstream nodes from every input (the primary and all secondary).
|
||||
/// - [`FlowType::PrimaryFlow`]: iterates along the horizontal inputs of nodes, so in the case of a node chain `a -> b -> c`, this would yield `c, b, a` if we started from `c`.
|
||||
/// - [`FlowType::HorizontalFlow`]: iterates over the secondary input for layer nodes and primary input for non layer nodes.
|
||||
struct FlowIter<'a> {
|
||||
stack: Vec<NodeId>,
|
||||
network: &'a NodeNetwork,
|
||||
only_follow_primary: bool,
|
||||
flow_type: FlowType,
|
||||
}
|
||||
impl<'a> Iterator for FlowIter<'a> {
|
||||
type Item = (&'a DocumentNode, NodeId);
|
||||
|
@ -802,8 +823,9 @@ impl<'a> Iterator for FlowIter<'a> {
|
|||
let node_id = self.stack.pop()?;
|
||||
|
||||
if let Some(document_node) = self.network.nodes.get(&node_id) {
|
||||
let take = if self.only_follow_primary { 1 } else { usize::MAX };
|
||||
let inputs = document_node.inputs.iter().take(take);
|
||||
let skip = if self.flow_type == FlowType::HorizontalFlow && document_node.is_layer { 1 } else { 0 };
|
||||
let take = if self.flow_type == FlowType::UpstreamFlow { usize::MAX } else { 1 };
|
||||
let inputs = document_node.inputs.iter().skip(skip).take(take);
|
||||
|
||||
let node_ids = inputs.filter_map(|input| if let NodeInput::Node { node_id, .. } = input { Some(node_id) } else { None });
|
||||
|
||||
|
@ -940,12 +962,8 @@ impl NodeNetwork {
|
|||
if !node.visible && node.implementation != identity_node {
|
||||
node.implementation = identity_node;
|
||||
|
||||
if node.is_layer() {
|
||||
// Connect layer node to the graphic group below
|
||||
node.inputs.drain(..1);
|
||||
} else {
|
||||
node.inputs.drain(1..);
|
||||
}
|
||||
// Connect layer node to the graphic group below
|
||||
node.inputs.drain(1..);
|
||||
self.nodes.insert(id, node);
|
||||
|
||||
return;
|
||||
|
@ -1169,37 +1187,39 @@ impl NodeNetwork {
|
|||
/// However, in the case of the default input, we must insert a node that takes an input of `Footprint` and returns `GraphicGroup::Empty`, in order to satisfy the type system.
|
||||
/// This is because the standard value node takes in `()`.
|
||||
pub fn resolve_empty_stacks(&mut self) {
|
||||
const EMPTY_STACK: &str = "Empty Stack";
|
||||
for value in [
|
||||
TaggedValue::GraphicGroup(GraphicGroup::EMPTY),
|
||||
TaggedValue::VectorData(VectorData::empty()),
|
||||
TaggedValue::ArtboardGroup(ArtboardGroup::EMPTY),
|
||||
] {
|
||||
const EMPTY_STACK: &str = "Empty Stack";
|
||||
|
||||
let new_id = generate_uuid();
|
||||
let mut used = false;
|
||||
let new_id = generate_uuid();
|
||||
let mut used = false;
|
||||
|
||||
// We filter out the newly inserted empty stack in case `resolve_empty_stacks` runs multiple times.
|
||||
for node in self.nodes.values_mut().filter(|node| node.name != EMPTY_STACK) {
|
||||
for input in &mut node.inputs {
|
||||
if let NodeInput::Value {
|
||||
tagged_value: TaggedValue::GraphicGroup(graphic_group),
|
||||
..
|
||||
} = input
|
||||
{
|
||||
if *graphic_group == GraphicGroup::EMPTY {
|
||||
*input = NodeInput::node(NodeId(new_id), 0);
|
||||
used = true;
|
||||
// We filter out the newly inserted empty stack in case `resolve_empty_stacks` runs multiple times.
|
||||
for node in self.nodes.values_mut().filter(|node| node.name != EMPTY_STACK) {
|
||||
for input in &mut node.inputs {
|
||||
if let NodeInput::Value { tagged_value, .. } = input {
|
||||
if *tagged_value == value {
|
||||
*input = NodeInput::node(NodeId(new_id), 0);
|
||||
used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only insert the node if necessary.
|
||||
if used {
|
||||
let new_node = DocumentNode {
|
||||
name: EMPTY_STACK.to_string(),
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::transform::CullNode<_>"),
|
||||
manual_composition: Some(concrete!(graphene_core::transform::Footprint)),
|
||||
inputs: vec![NodeInput::value(TaggedValue::GraphicGroup(graphene_core::GraphicGroup::EMPTY), false)],
|
||||
..Default::default()
|
||||
};
|
||||
self.nodes.insert(NodeId(new_id), new_node);
|
||||
// Only insert the node if necessary.
|
||||
if used {
|
||||
let new_node = DocumentNode {
|
||||
name: EMPTY_STACK.to_string(),
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::transform::CullNode<_>"),
|
||||
manual_composition: Some(concrete!(graphene_core::transform::Footprint)),
|
||||
inputs: vec![NodeInput::value(value, false)],
|
||||
..Default::default()
|
||||
};
|
||||
self.nodes.insert(NodeId(new_id), new_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ pub enum TaggedValue {
|
|||
DocumentNode(DocumentNode),
|
||||
GraphicGroup(graphene_core::GraphicGroup),
|
||||
Artboard(graphene_core::Artboard),
|
||||
ArtboardGroup(graphene_core::ArtboardGroup),
|
||||
Curve(graphene_core::raster::curve::Curve),
|
||||
IVec2(glam::IVec2),
|
||||
SurfaceFrame(graphene_core::SurfaceFrame),
|
||||
|
@ -148,6 +149,7 @@ impl Hash for TaggedValue {
|
|||
Self::DocumentNode(x) => x.hash(state),
|
||||
Self::GraphicGroup(x) => x.hash(state),
|
||||
Self::Artboard(x) => x.hash(state),
|
||||
Self::ArtboardGroup(x) => x.hash(state),
|
||||
Self::Curve(x) => x.hash(state),
|
||||
Self::IVec2(x) => x.hash(state),
|
||||
Self::SurfaceFrame(x) => x.hash(state),
|
||||
|
@ -214,6 +216,7 @@ impl<'a> TaggedValue {
|
|||
TaggedValue::DocumentNode(x) => Box::new(x),
|
||||
TaggedValue::GraphicGroup(x) => Box::new(x),
|
||||
TaggedValue::Artboard(x) => Box::new(x),
|
||||
TaggedValue::ArtboardGroup(x) => Box::new(x),
|
||||
TaggedValue::Curve(x) => Box::new(x),
|
||||
TaggedValue::IVec2(x) => Box::new(x),
|
||||
TaggedValue::SurfaceFrame(x) => Box::new(x),
|
||||
|
@ -292,6 +295,7 @@ impl<'a> TaggedValue {
|
|||
TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode),
|
||||
TaggedValue::GraphicGroup(_) => concrete!(graphene_core::GraphicGroup),
|
||||
TaggedValue::Artboard(_) => concrete!(graphene_core::Artboard),
|
||||
TaggedValue::ArtboardGroup(_) => concrete!(graphene_core::ArtboardGroup),
|
||||
TaggedValue::Curve(_) => concrete!(graphene_core::raster::curve::Curve),
|
||||
TaggedValue::IVec2(_) => concrete!(glam::IVec2),
|
||||
TaggedValue::SurfaceFrame(_) => concrete!(graphene_core::SurfaceFrame),
|
||||
|
|
|
@ -11,7 +11,7 @@ use graphene_core::value::{ClonedNode, CopiedNode, ValueNode};
|
|||
use graphene_core::vector::brush_stroke::BrushStroke;
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::{application_io::SurfaceHandle, SurfaceFrame, WasmSurfaceHandleFrame};
|
||||
use graphene_core::{concrete, generic, Artboard, GraphicGroup};
|
||||
use graphene_core::{concrete, generic, Artboard, ArtboardGroup, GraphicGroup};
|
||||
use graphene_core::{fn_type, raster::*};
|
||||
use graphene_core::{Cow, ProtoNodeIdentifier, Type};
|
||||
use graphene_core::{Node, NodeIO, NodeIOTypes};
|
||||
|
@ -345,6 +345,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicGroup, fn_params: [Footprint => graphene_core::GraphicGroup]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicElement, fn_params: [Footprint => graphene_core::GraphicElement]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: Artboard, fn_params: [Footprint => graphene_core::Artboard]),
|
||||
async_node!(graphene_std::wasm_application_io::LoadResourceNode<_>, input: WasmEditorApi, output: Arc<[u8]>, params: [String]),
|
||||
register_node!(graphene_std::wasm_application_io::DecodeImageNode, input: Arc<[u8]>, params: []),
|
||||
async_node!(graphene_std::wasm_application_io::CreateSurfaceNode, input: WasmEditorApi, output: Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>, params: []),
|
||||
|
@ -678,6 +679,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => VectorData, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => GraphicGroup, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Artboard, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ArtboardGroup, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => ImageFrame<Color>, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => VectorData, () => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [() => GraphicGroup, () => Arc<WasmSurfaceHandle>]),
|
||||
|
@ -726,6 +728,25 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
)],
|
||||
register_node!(graphene_core::transform::CullNode<_>, input: Footprint, params: [Artboard]),
|
||||
register_node!(graphene_core::transform::CullNode<_>, input: Footprint, params: [ImageFrame<Color>]),
|
||||
vec![(
|
||||
ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"),
|
||||
|args| {
|
||||
Box::pin(async move {
|
||||
let mut args = args.clone();
|
||||
args.reverse();
|
||||
let node = <graphene_core::transform::CullNode<_>>::new(graphene_std::any::input_node::<ArtboardGroup>(args.pop().expect("Not enough arguments provided to construct node")));
|
||||
let any: DynAnyNode<Footprint, _, _> = graphene_std::any::DynAnyNode::new(node);
|
||||
Box::new(any) as Box<dyn for<'i> NodeIO<'i, graph_craft::proto::Any<'i>, Output = core::pin::Pin<Box<dyn core::future::Future<Output = graph_craft::proto::Any<'i>> + 'i>>> + '_>
|
||||
})
|
||||
},
|
||||
{
|
||||
let node = <graphene_core::transform::CullNode<_>>::new(graphene_std::any::PanicNode::<(), ArtboardGroup>::new());
|
||||
let params = vec![fn_type!((), ArtboardGroup)];
|
||||
let mut node_io = <graphene_core::transform::CullNode<_> as NodeIO<'_, Footprint>>::to_node_io(&node, params);
|
||||
node_io.input = concrete!(<Footprint as StaticType>::Static);
|
||||
node_io
|
||||
},
|
||||
)],
|
||||
vec![(
|
||||
ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"),
|
||||
|args| {
|
||||
|
@ -776,6 +797,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
register_node!(graphene_core::ToGraphicElementNode, input: GraphicGroup, params: []),
|
||||
register_node!(graphene_core::ToGraphicElementNode, input: Artboard, params: []),
|
||||
async_node!(graphene_core::ConstructArtboardNode<_, _, _, _, _>, input: Footprint, output: Artboard, fn_params: [Footprint => GraphicGroup, () => glam::IVec2, () => glam::IVec2, () => Color, () => bool]),
|
||||
async_node!(graphene_core::AddArtboardNode<_, _>, input: Footprint, output: ArtboardGroup, fn_params: [Footprint => Artboard, Footprint => ArtboardGroup]),
|
||||
];
|
||||
let mut map: HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
|
||||
for (id, c, types) in node_types.into_iter().flatten() {
|
||||
|
|
|
@ -113,7 +113,7 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
|
|||
<div class="informational ongoing heading" title="Began February 2024" data-year="2024">
|
||||
<h3>— Alpha 3 —</h3>
|
||||
</div>
|
||||
<div class="informational ongoing" title="Development Ongoing">
|
||||
<div class="informational complete" title="Development Complete">
|
||||
<img class="atlas" style="--atlas-index: 3" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
||||
<span>Stackable adjustment layers</span>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue