├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── msrv.yml │ ├── publish.yml │ └── static.yml ├── .gitignore ├── CHANGELOG ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── demo.png ├── demo ├── Cargo.toml └── index.html ├── examples └── demo.rs ├── logo.png ├── logo.svg └── src ├── lib.rs ├── ui.rs └── ui ├── background_pattern.rs ├── effect.rs ├── pin.rs ├── scale.rs ├── state.rs ├── viewer.rs └── wire.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [zakarumych] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/msrv.yml: -------------------------------------------------------------------------------- 1 | name: Test MSRV 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited] 6 | push: 7 | branches: [ "main" ] 8 | paths: 9 | - ".github/workflows/msrv.yml" 10 | - "demo/**" 11 | - "examples/**" 12 | - "src/**" 13 | - "Cargo.toml" 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Install MSRV 22 | run: rustup install 1.85 23 | - name: Test on MSRV 24 | run: cargo +1.85 test 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | build: 11 | environment: 12 | name: public 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Set up cache 16 | uses: actions/cache@v4 17 | with: 18 | path: | 19 | ~/.cargo/.crates.toml 20 | ~/.cargo/.crates2.json 21 | ~/.cargo/bin/ 22 | ~/.cargo/registry/index/ 23 | ~/.cargo/registry/cache/ 24 | ~/.cargo/git/db/ 25 | target/ 26 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 27 | - uses: actions/checkout@v4 28 | - name: Build 29 | run: cargo build --verbose --all 30 | - name: Run tests 31 | run: cargo test --verbose --all 32 | - name: login 33 | run: cargo login ${{ secrets.CRATES_IO_TOKEN }} 34 | - name: publish 35 | run: cargo publish 36 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to Github Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | paths: 8 | - "demo/**" 9 | - "examples/**" 10 | - "src/**" 11 | - "Cargo.toml" 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Set up cache 21 | uses: actions/cache@v4 22 | with: 23 | path: | 24 | ~/.cargo/.crates.toml 25 | ~/.cargo/.crates2.json 26 | ~/.cargo/bin/ 27 | ~/.cargo/registry/index/ 28 | ~/.cargo/registry/cache/ 29 | ~/.cargo/git/db/ 30 | target/ 31 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Install WASM toolchain 35 | run: rustup target add wasm32-unknown-unknown 36 | - name: Install trunk 37 | run: cargo install trunk 38 | - name: Build project 39 | run: trunk build --release --verbose --public-url "/${{ github.event.repository.name }}" ./demo/index.html --dist ./demo/dist 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v3 42 | with: 43 | path: './demo/dist' 44 | deploy: 45 | needs: build 46 | 47 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 48 | permissions: 49 | pages: write # to deploy to Pages 50 | id-token: write # to verify the deployment originates from an appropriate source 51 | 52 | # Deploy to the github-pages environment 53 | environment: 54 | name: github-pages 55 | url: ${{ steps.deployment.outputs.page_url }} 56 | 57 | # Specify runner + deployment step 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Deploy to GitHub Pages 61 | id: deployment 62 | uses: actions/deploy-pages@v4 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | 17 | # Added by cargo 18 | 19 | /target 20 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.8.0] - Unreleased 9 | 10 | ### Added 11 | 12 | - `SnarlViewer::current_transform` method to receive and possibly modify the current transform of the snarl's UI layer. 13 | 14 | ### Changed 15 | 16 | - Snarl is now scaled using egui's layer scaling mechanism. 17 | Removes layout twitching when zooming in and out. 18 | Simplifies interface of `SnarlViewer`. 19 | 20 | ## [0.7.1] - 19.02.2025 21 | 22 | ### Added 23 | 24 | - `SnarlPin::pin_rect` method allows custom type to override default pin position and size. 25 | 26 | ## [0.7.0] - 19.02.2025 27 | 28 | ### Changed 29 | 30 | - `SnarlViewer::show_input` and `SnarlViewer::show_output` now returns `impl SnarlPin` 31 | that allows fully custom pin drawing. 32 | This replaces `SnarlViewer::draw_input_pin` and `SnarlViewer::draw_output_pin`. 33 | 34 | ### Removed 35 | 36 | - `SnarlViewer::draw_input_pin` and `SnarlViewer::draw_output_pin` are removed. 37 | Return custom type from `SnarlViewer::show_input` and `SnarlViewer::show_output` instead. 38 | 39 | ## [0.6.0] - 20.12.2024 40 | 41 | ### Changed 42 | 43 | - Zooming now uses egui's zoom_delta from Input. 44 | This means that zooming is now performed by ctrl+scroll (cmd+scroll on mac) or pinching. 45 | Previously it was performed by scrolling alone, which collided with scrolling through content in the node's widgets and didn't work for touch controls. 46 | 47 | ### Added 48 | 49 | - NodeLayout enum 50 | To control layout of nodes in the graph 51 | Can be set globally in SnarlStyle and overridden per node with SnarlViewer::node_layout 52 | Defaults to NodeLayout::Basic which is the previous layout 53 | NodeLayout::Sandwich is a new layout that places inputs, body and outputs vertically with inputs on top and outputs on bottom 54 | NodeLayout::FlippedSandwich is the same as Sandwich but with outputs on top and inputs on bottom 55 | 56 | - SnarlViewer::draw_input_pin/draw_output_pin can be used to override how pins are drawn. 57 | Default implementation matches old behavior. 58 | This mechanism is meant to replace PinShape::Custom that was removed. 59 | 60 | - SnarlViewer::draw_node_background can be used to override how node background is drawn. 61 | Default implementation matches old behavior. 62 | This mechanism is meant to replace BackgroundPattern::Custom that was removed. 63 | 64 | - PinPlacement style option in SnarlStyle 65 | This option controls how pins are placed in the node 66 | Inside - pins are placed inside the node frame - default, old behavior 67 | Edge - pin centers are placed on the edge of the node frame 68 | Outside - pins are placed outside the node frame with specified margin 69 | 70 | ### Removed 71 | 72 | - BackgroundPattern::Custom is removed. 73 | It contained opaque function to draw custom background pattern 74 | and permitted !Send and !Sync captures which made SnarlStyle !Send and !Sync as well 75 | 76 | - PinShape::Custom is removed. 77 | It is replaced by SnarlViewer::draw_input_pin/draw_output_pin which is more flexible. 78 | 79 | - BasicPinShape is removed. SnarlStyle::pin_shape has PinShape type now. 80 | 81 | ### Fixed 82 | 83 | - Crash after centering graph when no nodes are present and adding a node afterwards -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [workspace] 3 | members = ["demo"] 4 | resolver = "3" 5 | 6 | [workspace.package] 7 | rust-version = "1.85" 8 | edition = "2024" 9 | license = "MIT OR Apache-2.0" 10 | documentation = "https://docs.rs/egui-snarl" 11 | repository = "https://github.com/zakarumych/egui-snarl" 12 | 13 | [workspace.dependencies] 14 | egui = { version = "0.31" } 15 | eframe = { version = "0.31" } 16 | egui_extras = { version = "0.31" } 17 | syn = { version = "2" } 18 | serde = { version = "1" } 19 | serde_json = { version = "1" } 20 | 21 | egui-probe = { version = "0.8.0", git = "https://github.com/zakarumych/egui-probe" } 22 | egui-scale = { version = "0.1.0" } 23 | wasm-bindgen-futures = "0.4" 24 | web-sys = "0.3.70" 25 | 26 | [package] 27 | name = "egui-snarl" 28 | version = "0.8.0" 29 | edition.workspace = true 30 | rust-version.workspace = true 31 | license.workspace = true 32 | documentation.workspace = true 33 | repository.workspace = true 34 | description = "Node-graphs for egui" 35 | readme = "README.md" 36 | keywords = ["egui", "node", "graph", "ui", "node-graph"] 37 | categories = ["gui", "visualization"] 38 | 39 | [features] 40 | serde = ["dep:serde", "egui/serde", "slab/serde"] 41 | 42 | [dependencies] 43 | egui.workspace = true 44 | slab = { version = "0.4" } 45 | serde = { workspace = true, features = ["derive"], optional = true } 46 | 47 | egui-probe = { workspace = true, features = ["derive"], optional = true } 48 | egui-scale.workspace = true 49 | 50 | [dev-dependencies] 51 | eframe = { workspace = true, features = ["serde", "persistence"] } 52 | egui_extras = { workspace = true, features = ["all_loaders"] } 53 | serde_json.workspace = true 54 | syn = { workspace = true, features = ["extra-traits"] } 55 | 56 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 57 | wasm-bindgen-futures.workspace = true 58 | 59 | [[example]] 60 | name = "demo" 61 | required-features = ["serde", "egui-probe"] 62 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Zakarum 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zakarum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egui-snarl 2 | 3 | [![docs.rs](https://img.shields.io/docsrs/egui-snarl?style=for-the-badge)](https://docs.rs/egui-snarl/) 4 | [![Crates.io Total Downloads](https://img.shields.io/crates/d/egui-snarl?style=for-the-badge)](https://crates.io/crates/egui-snarl) 5 | [![Discord](https://img.shields.io/discord/1270330377847832646?style=for-the-badge&logo=discord)](https://discord.com/channels/1270330377847832646/1318880578132770869) 6 | 7 | Crate for creating and manipulating node-graph UIs. 8 | It contains all necessary features and more are planned. 9 | 10 | # Why "snarl"? 11 | 12 | Because that's how any complex visual graph looks like. 13 | 14 | # Features 15 | 16 | - Typed data-only nodes. 17 | `Snarl` is parametrized by the type of the data nodes hold. 18 | This type may be an enum of the node variants or customizeable node constructor type, 19 | it's always author's choice. 20 | 21 | - Viewer trait to define behavior and add extra data to UI routine. 22 | `SnarlViewer` trait is parametrized by the type node type. 23 | It decides node's title UI, how many pins node has and fills pin's UI content. 24 | Demo example showcase how pin can have drag integer value, text input, button or image, 25 | there's no limitations since each pin's content is whatever viewer puts in provided `egui::Ui`. 26 | 27 | - Node layout is divided to five spaces with both pre-defined and custom content. 28 | 1. Header - top of the node, features collapsing button if `SnarlStyle::collapsible` is true and user-defined content - label with node name by default. 29 | 2. Input pins are placed on the left below the header. 30 | Each pin contains a shape on the left edge, shape can be intracted with to connect/disconnect the pin and user-defined content after it. 31 | 3. Body of the node is placed in the center, it is optional but comes in handy for some node kinds. Contains only user-defined content. 32 | 4. Output pins are placed on the right below the header. 33 | Same as input pins but pin shape goes to the right edge. 34 | 5. Footer is placed below other spaces, similar to body it is optional and contains only user-defined content. 35 | 36 | - Context menus for nodes and graph background. 37 | Right-clicking on node, if configured, opens context menu filled by viewer's method. The method is provided with `Snarl` reference and node id. It may be used to add menu options to remove node, configure it or anything else. 38 | Right-clicking on background, if configured, opens context menu filled by viewer's method. The method is provided with `Snarl` reference. It may be used to add/remove nodes configure whole graph or anything else. 39 | 40 | - UI scaling. 41 | `egui` does not support UI scaling, but to provide best UX `egui-snarl` supports scaling 42 | via scaling of independent UI elements, this works with some artefacts. 43 | 44 | - User controlled responses for wire connections. 45 | When new wire is connected in UI the viewer is notified and decides what happens. 46 | It may create that connection, ignore it, add more nodes, play beep sound or send e-mails. 47 | 48 | - Multiconnections. 49 | Connect or reconnect many pins at once. 50 | When dragging new wire, hover over pin of the same side while holding Shift to add wire from it into bundle. 51 | Start dragging from pin while holding Ctrl (Cmd on Macos) to yank existing wires from it and drag in to another pin. 52 | 53 | - Beautiful wires between nodes. 54 | `egui-snarl` use carefuly crafted formula to draw wires that can be customized to scale differently using `SnarlStyle`. 55 | 56 | - Configurable background pattern. 57 | Having blank color background may be desirable, however some faint background with pattern helps filling visual emptiness. 58 | Configure background in `SnarlStyle`, use provided patterns like `Grid` or custom function. 59 | 60 | - Serialization. 61 | `Snarl` structure stores only the graph with placed nodes and wires between them. 62 | This makes it suitable for easy serialization and deserialization. 63 | It supports `serde` so pick your own format. 64 | 65 | # Example 66 | 67 | `demo` example shows some of the features of the crate. 68 | Run it with `cargo run --example=demo --features="serde egui-probe"`. 69 | 70 | [![demo](./demo.png)](./demo.png) 71 | 72 | # Web Demos 73 | 74 | Snarl Demo GUI by @zakarumych 75 | 76 | https://zakarumych.github.io/egui-snarl/ 77 | 78 | Noise GUI by @attackgoat 79 | 80 | https://attackgoat.github.io/noise_gui/ 81 | 82 | # Videos 83 | 84 | Codotaku Logic by @ilyas-taouaou made in a [livestream](https://www.youtube.com/watch?v=zigPWkPm00U). 85 | 86 | https://github.com/ilyas-taouaou/codotaku_logic 87 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakarumych/egui-snarl/449295aad135b9605a81c17dda42b0196b2093ca/demo.png -------------------------------------------------------------------------------- /demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "demo" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | publish = false 6 | 7 | [[bin]] 8 | name = "demo" 9 | path = "../examples/demo.rs" 10 | 11 | [dependencies] 12 | egui.workspace = true 13 | egui-probe = { workspace = true, features = ["derive"] } 14 | eframe = { workspace = true, features = ["serde", "persistence"] } 15 | egui_extras = { workspace = true, features = ["all_loaders"] } 16 | syn = { workspace = true, features = ["extra-traits"] } 17 | serde = { workspace = true, features = ["derive"] } 18 | serde_json.workspace = true 19 | 20 | egui-snarl = { path = "..", features = ["egui-probe", "serde"] } 21 | 22 | [target.'cfg(target_arch = "wasm32")'.dependencies] 23 | wasm-bindgen-futures.workspace = true 24 | web-sys.workspace = true 25 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::use_self)] 2 | 3 | use std::collections::HashMap; 4 | 5 | use eframe::{App, CreationContext}; 6 | use egui::{Color32, Id, Ui}; 7 | use egui_snarl::{ 8 | InPin, InPinId, NodeId, OutPin, OutPinId, Snarl, 9 | ui::{ 10 | AnyPins, NodeLayout, PinInfo, PinPlacement, SnarlStyle, SnarlViewer, SnarlWidget, 11 | WireStyle, get_selected_nodes, 12 | }, 13 | }; 14 | 15 | const STRING_COLOR: Color32 = Color32::from_rgb(0x00, 0xb0, 0x00); 16 | const NUMBER_COLOR: Color32 = Color32::from_rgb(0xb0, 0x00, 0x00); 17 | const IMAGE_COLOR: Color32 = Color32::from_rgb(0xb0, 0x00, 0xb0); 18 | const UNTYPED_COLOR: Color32 = Color32::from_rgb(0xb0, 0xb0, 0xb0); 19 | 20 | #[derive(Clone, serde::Serialize, serde::Deserialize)] 21 | enum DemoNode { 22 | /// Node with single input. 23 | /// Displays the value of the input. 24 | Sink, 25 | 26 | /// Value node with a single output. 27 | /// The value is editable in UI. 28 | Number(f64), 29 | 30 | /// Value node with a single output. 31 | String(String), 32 | 33 | /// Converts URI to Image 34 | ShowImage(String), 35 | 36 | /// Expression node with a single output. 37 | /// It has number of inputs equal to number of variables in the expression. 38 | ExprNode(ExprNode), 39 | } 40 | 41 | impl DemoNode { 42 | const fn name(&self) -> &str { 43 | match self { 44 | DemoNode::Sink => "Sink", 45 | DemoNode::Number(_) => "Number", 46 | DemoNode::String(_) => "String", 47 | DemoNode::ShowImage(_) => "ShowImage", 48 | DemoNode::ExprNode(_) => "ExprNode", 49 | } 50 | } 51 | 52 | fn number_out(&self) -> f64 { 53 | match self { 54 | DemoNode::Number(value) => *value, 55 | DemoNode::ExprNode(expr_node) => expr_node.eval(), 56 | _ => unreachable!(), 57 | } 58 | } 59 | 60 | fn number_in(&mut self, idx: usize) -> &mut f64 { 61 | match self { 62 | DemoNode::ExprNode(expr_node) => &mut expr_node.values[idx - 1], 63 | _ => unreachable!(), 64 | } 65 | } 66 | 67 | fn label_in(&mut self, idx: usize) -> &str { 68 | match self { 69 | DemoNode::ShowImage(_) if idx == 0 => "URL", 70 | DemoNode::ExprNode(expr_node) => &expr_node.bindings[idx - 1], 71 | _ => unreachable!(), 72 | } 73 | } 74 | 75 | fn string_out(&self) -> &str { 76 | match self { 77 | DemoNode::String(value) => value, 78 | _ => unreachable!(), 79 | } 80 | } 81 | 82 | fn string_in(&mut self) -> &mut String { 83 | match self { 84 | DemoNode::ShowImage(uri) => uri, 85 | DemoNode::ExprNode(expr_node) => &mut expr_node.text, 86 | _ => unreachable!(), 87 | } 88 | } 89 | 90 | fn expr_node(&mut self) -> &mut ExprNode { 91 | match self { 92 | DemoNode::ExprNode(expr_node) => expr_node, 93 | _ => unreachable!(), 94 | } 95 | } 96 | } 97 | 98 | struct DemoViewer; 99 | 100 | impl SnarlViewer for DemoViewer { 101 | #[inline] 102 | fn connect(&mut self, from: &OutPin, to: &InPin, snarl: &mut Snarl) { 103 | // Validate connection 104 | #[allow(clippy::match_same_arms)] // For match clarity 105 | match (&snarl[from.id.node], &snarl[to.id.node]) { 106 | (DemoNode::Sink, _) => { 107 | unreachable!("Sink node has no outputs") 108 | } 109 | (_, DemoNode::Sink) => {} 110 | (_, DemoNode::Number(_)) => { 111 | unreachable!("Number node has no inputs") 112 | } 113 | (_, DemoNode::String(_)) => { 114 | unreachable!("String node has no inputs") 115 | } 116 | (DemoNode::Number(_), DemoNode::ShowImage(_)) => { 117 | return; 118 | } 119 | (DemoNode::ShowImage(_), DemoNode::ShowImage(_)) => { 120 | return; 121 | } 122 | (DemoNode::String(_), DemoNode::ShowImage(_)) => {} 123 | (DemoNode::ExprNode(_), DemoNode::ExprNode(_)) if to.id.input == 0 => { 124 | return; 125 | } 126 | (DemoNode::ExprNode(_), DemoNode::ExprNode(_)) => {} 127 | (DemoNode::Number(_), DemoNode::ExprNode(_)) if to.id.input == 0 => { 128 | return; 129 | } 130 | (DemoNode::Number(_), DemoNode::ExprNode(_)) => {} 131 | (DemoNode::String(_), DemoNode::ExprNode(_)) if to.id.input == 0 => {} 132 | (DemoNode::String(_), DemoNode::ExprNode(_)) => { 133 | return; 134 | } 135 | (DemoNode::ShowImage(_), DemoNode::ExprNode(_)) => { 136 | return; 137 | } 138 | (DemoNode::ExprNode(_), DemoNode::ShowImage(_)) => { 139 | return; 140 | } 141 | } 142 | 143 | for &remote in &to.remotes { 144 | snarl.disconnect(remote, to.id); 145 | } 146 | 147 | snarl.connect(from.id, to.id); 148 | } 149 | 150 | fn title(&mut self, node: &DemoNode) -> String { 151 | match node { 152 | DemoNode::Sink => "Sink".to_owned(), 153 | DemoNode::Number(_) => "Number".to_owned(), 154 | DemoNode::String(_) => "String".to_owned(), 155 | DemoNode::ShowImage(_) => "Show image".to_owned(), 156 | DemoNode::ExprNode(_) => "Expr".to_owned(), 157 | } 158 | } 159 | 160 | fn inputs(&mut self, node: &DemoNode) -> usize { 161 | match node { 162 | DemoNode::Sink | DemoNode::ShowImage(_) => 1, 163 | DemoNode::Number(_) | DemoNode::String(_) => 0, 164 | DemoNode::ExprNode(expr_node) => 1 + expr_node.bindings.len(), 165 | } 166 | } 167 | 168 | fn outputs(&mut self, node: &DemoNode) -> usize { 169 | match node { 170 | DemoNode::Sink => 0, 171 | DemoNode::Number(_) 172 | | DemoNode::String(_) 173 | | DemoNode::ShowImage(_) 174 | | DemoNode::ExprNode(_) => 1, 175 | } 176 | } 177 | 178 | #[allow(clippy::too_many_lines)] 179 | #[allow(refining_impl_trait)] 180 | fn show_input(&mut self, pin: &InPin, ui: &mut Ui, snarl: &mut Snarl) -> PinInfo { 181 | match snarl[pin.id.node] { 182 | DemoNode::Sink => { 183 | assert_eq!(pin.id.input, 0, "Sink node has only one input"); 184 | 185 | match &*pin.remotes { 186 | [] => { 187 | ui.label("None"); 188 | PinInfo::circle().with_fill(UNTYPED_COLOR) 189 | } 190 | [remote] => match snarl[remote.node] { 191 | DemoNode::Sink => unreachable!("Sink node has no outputs"), 192 | DemoNode::Number(value) => { 193 | assert_eq!(remote.output, 0, "Number node has only one output"); 194 | ui.label(format_float(value)); 195 | PinInfo::circle().with_fill(NUMBER_COLOR) 196 | } 197 | DemoNode::String(ref value) => { 198 | assert_eq!(remote.output, 0, "String node has only one output"); 199 | ui.label(format!("{value:?}")); 200 | 201 | PinInfo::circle().with_fill(STRING_COLOR).with_wire_style( 202 | WireStyle::AxisAligned { 203 | corner_radius: 10.0, 204 | }, 205 | ) 206 | } 207 | DemoNode::ExprNode(ref expr) => { 208 | assert_eq!(remote.output, 0, "Expr node has only one output"); 209 | ui.label(format_float(expr.eval())); 210 | PinInfo::circle().with_fill(NUMBER_COLOR) 211 | } 212 | DemoNode::ShowImage(ref uri) => { 213 | assert_eq!(remote.output, 0, "ShowImage node has only one output"); 214 | 215 | let image = egui::Image::new(uri).show_loading_spinner(true); 216 | ui.add(image); 217 | 218 | PinInfo::circle().with_fill(IMAGE_COLOR) 219 | } 220 | }, 221 | _ => unreachable!("Sink input has only one wire"), 222 | } 223 | } 224 | DemoNode::Number(_) => { 225 | unreachable!("Number node has no inputs") 226 | } 227 | DemoNode::String(_) => { 228 | unreachable!("String node has no inputs") 229 | } 230 | DemoNode::ShowImage(_) => match &*pin.remotes { 231 | [] => { 232 | let input = snarl[pin.id.node].string_in(); 233 | egui::TextEdit::singleline(input) 234 | .clip_text(false) 235 | .desired_width(0.0) 236 | .margin(ui.spacing().item_spacing) 237 | .show(ui); 238 | PinInfo::circle().with_fill(STRING_COLOR).with_wire_style( 239 | WireStyle::AxisAligned { 240 | corner_radius: 10.0, 241 | }, 242 | ) 243 | } 244 | [remote] => { 245 | let new_value = snarl[remote.node].string_out().to_owned(); 246 | 247 | egui::TextEdit::singleline(&mut &*new_value) 248 | .clip_text(false) 249 | .desired_width(0.0) 250 | .margin(ui.spacing().item_spacing) 251 | .show(ui); 252 | 253 | let input = snarl[pin.id.node].string_in(); 254 | *input = new_value; 255 | 256 | PinInfo::circle().with_fill(STRING_COLOR).with_wire_style( 257 | WireStyle::AxisAligned { 258 | corner_radius: 10.0, 259 | }, 260 | ) 261 | } 262 | _ => unreachable!("Sink input has only one wire"), 263 | }, 264 | DemoNode::ExprNode(_) if pin.id.input == 0 => { 265 | let changed = match &*pin.remotes { 266 | [] => { 267 | let input = snarl[pin.id.node].string_in(); 268 | let r = egui::TextEdit::singleline(input) 269 | .clip_text(false) 270 | .desired_width(0.0) 271 | .margin(ui.spacing().item_spacing) 272 | .show(ui) 273 | .response; 274 | 275 | r.changed() 276 | } 277 | [remote] => { 278 | let new_string = snarl[remote.node].string_out().to_owned(); 279 | 280 | egui::TextEdit::singleline(&mut &*new_string) 281 | .clip_text(false) 282 | .desired_width(0.0) 283 | .margin(ui.spacing().item_spacing) 284 | .show(ui); 285 | 286 | let input = snarl[pin.id.node].string_in(); 287 | if new_string == *input { 288 | false 289 | } else { 290 | *input = new_string; 291 | true 292 | } 293 | } 294 | _ => unreachable!("Expr pins has only one wire"), 295 | }; 296 | 297 | if changed { 298 | let expr_node = snarl[pin.id.node].expr_node(); 299 | 300 | if let Ok(expr) = syn::parse_str(&expr_node.text) { 301 | expr_node.expr = expr; 302 | 303 | let values = Iterator::zip( 304 | expr_node.bindings.iter().map(String::clone), 305 | expr_node.values.iter().copied(), 306 | ) 307 | .collect::>(); 308 | 309 | let mut new_bindings = Vec::new(); 310 | expr_node.expr.extend_bindings(&mut new_bindings); 311 | 312 | let old_bindings = 313 | std::mem::replace(&mut expr_node.bindings, new_bindings.clone()); 314 | 315 | let new_values = new_bindings 316 | .iter() 317 | .map(|name| values.get(&**name).copied().unwrap_or(0.0)) 318 | .collect::>(); 319 | 320 | expr_node.values = new_values; 321 | 322 | let old_inputs = (0..old_bindings.len()) 323 | .map(|idx| { 324 | snarl.in_pin(InPinId { 325 | node: pin.id.node, 326 | input: idx + 1, 327 | }) 328 | }) 329 | .collect::>(); 330 | 331 | for (idx, name) in old_bindings.iter().enumerate() { 332 | let new_idx = 333 | new_bindings.iter().position(|new_name| *new_name == *name); 334 | 335 | match new_idx { 336 | None => { 337 | snarl.drop_inputs(old_inputs[idx].id); 338 | } 339 | Some(new_idx) if new_idx != idx => { 340 | let new_in_pin = InPinId { 341 | node: pin.id.node, 342 | input: new_idx, 343 | }; 344 | for &remote in &old_inputs[idx].remotes { 345 | snarl.disconnect(remote, old_inputs[idx].id); 346 | snarl.connect(remote, new_in_pin); 347 | } 348 | } 349 | _ => {} 350 | } 351 | } 352 | } 353 | } 354 | PinInfo::circle() 355 | .with_fill(STRING_COLOR) 356 | .with_wire_style(WireStyle::AxisAligned { 357 | corner_radius: 10.0, 358 | }) 359 | } 360 | DemoNode::ExprNode(ref expr_node) => { 361 | if pin.id.input <= expr_node.bindings.len() { 362 | match &*pin.remotes { 363 | [] => { 364 | let node = &mut snarl[pin.id.node]; 365 | ui.label(node.label_in(pin.id.input)); 366 | ui.add(egui::DragValue::new(node.number_in(pin.id.input))); 367 | PinInfo::circle().with_fill(NUMBER_COLOR) 368 | } 369 | [remote] => { 370 | let new_value = snarl[remote.node].number_out(); 371 | let node = &mut snarl[pin.id.node]; 372 | ui.label(node.label_in(pin.id.input)); 373 | ui.label(format_float(new_value)); 374 | *node.number_in(pin.id.input) = new_value; 375 | PinInfo::circle().with_fill(NUMBER_COLOR) 376 | } 377 | _ => unreachable!("Expr pins has only one wire"), 378 | } 379 | } else { 380 | ui.label("Removed"); 381 | PinInfo::circle().with_fill(Color32::BLACK) 382 | } 383 | } 384 | } 385 | } 386 | 387 | #[allow(refining_impl_trait)] 388 | fn show_output(&mut self, pin: &OutPin, ui: &mut Ui, snarl: &mut Snarl) -> PinInfo { 389 | match snarl[pin.id.node] { 390 | DemoNode::Sink => { 391 | unreachable!("Sink node has no outputs") 392 | } 393 | DemoNode::Number(ref mut value) => { 394 | assert_eq!(pin.id.output, 0, "Number node has only one output"); 395 | ui.add(egui::DragValue::new(value)); 396 | PinInfo::circle().with_fill(NUMBER_COLOR) 397 | } 398 | DemoNode::String(ref mut value) => { 399 | assert_eq!(pin.id.output, 0, "String node has only one output"); 400 | let edit = egui::TextEdit::singleline(value) 401 | .clip_text(false) 402 | .desired_width(0.0) 403 | .margin(ui.spacing().item_spacing); 404 | ui.add(edit); 405 | PinInfo::circle() 406 | .with_fill(STRING_COLOR) 407 | .with_wire_style(WireStyle::AxisAligned { 408 | corner_radius: 10.0, 409 | }) 410 | } 411 | DemoNode::ExprNode(ref expr_node) => { 412 | let value = expr_node.eval(); 413 | assert_eq!(pin.id.output, 0, "Expr node has only one output"); 414 | ui.label(format_float(value)); 415 | PinInfo::circle().with_fill(NUMBER_COLOR) 416 | } 417 | DemoNode::ShowImage(_) => { 418 | ui.allocate_at_least(egui::Vec2::ZERO, egui::Sense::hover()); 419 | PinInfo::circle().with_fill(IMAGE_COLOR) 420 | } 421 | } 422 | } 423 | 424 | fn has_graph_menu(&mut self, _pos: egui::Pos2, _snarl: &mut Snarl) -> bool { 425 | true 426 | } 427 | 428 | fn show_graph_menu(&mut self, pos: egui::Pos2, ui: &mut Ui, snarl: &mut Snarl) { 429 | ui.label("Add node"); 430 | if ui.button("Number").clicked() { 431 | snarl.insert_node(pos, DemoNode::Number(0.0)); 432 | ui.close_menu(); 433 | } 434 | if ui.button("Expr").clicked() { 435 | snarl.insert_node(pos, DemoNode::ExprNode(ExprNode::new())); 436 | ui.close_menu(); 437 | } 438 | if ui.button("String").clicked() { 439 | snarl.insert_node(pos, DemoNode::String(String::new())); 440 | ui.close_menu(); 441 | } 442 | if ui.button("Show image").clicked() { 443 | snarl.insert_node(pos, DemoNode::ShowImage(String::new())); 444 | ui.close_menu(); 445 | } 446 | if ui.button("Sink").clicked() { 447 | snarl.insert_node(pos, DemoNode::Sink); 448 | ui.close_menu(); 449 | } 450 | } 451 | 452 | fn has_dropped_wire_menu(&mut self, _src_pins: AnyPins, _snarl: &mut Snarl) -> bool { 453 | true 454 | } 455 | 456 | fn show_dropped_wire_menu( 457 | &mut self, 458 | pos: egui::Pos2, 459 | ui: &mut Ui, 460 | src_pins: AnyPins, 461 | snarl: &mut Snarl, 462 | ) { 463 | // In this demo, we create a context-aware node graph menu, and connect a wire 464 | // dropped on the fly based on user input to a new node created. 465 | // 466 | // In your implementation, you may want to define specifications for each node's 467 | // pin inputs and outputs and compatibility to make this easier. 468 | 469 | type PinCompat = usize; 470 | const PIN_NUM: PinCompat = 1; 471 | const PIN_STR: PinCompat = 2; 472 | const PIN_IMG: PinCompat = 4; 473 | const PIN_SINK: PinCompat = PIN_NUM | PIN_STR | PIN_IMG; 474 | 475 | const fn pin_out_compat(node: &DemoNode) -> PinCompat { 476 | match node { 477 | DemoNode::Sink => 0, 478 | DemoNode::String(_) => PIN_STR, 479 | DemoNode::ShowImage(_) => PIN_IMG, 480 | DemoNode::Number(_) | DemoNode::ExprNode(_) => PIN_NUM, 481 | } 482 | } 483 | 484 | const fn pin_in_compat(node: &DemoNode, pin: usize) -> PinCompat { 485 | match node { 486 | DemoNode::Sink => PIN_SINK, 487 | DemoNode::Number(_) | DemoNode::String(_) => 0, 488 | DemoNode::ShowImage(_) => PIN_STR, 489 | DemoNode::ExprNode(_) => { 490 | if pin == 0 { 491 | PIN_STR 492 | } else { 493 | PIN_NUM 494 | } 495 | } 496 | } 497 | } 498 | 499 | ui.label("Add node"); 500 | 501 | match src_pins { 502 | AnyPins::Out(src_pins) => { 503 | assert!( 504 | src_pins.len() == 1, 505 | "There's no concept of multi-input nodes in this demo" 506 | ); 507 | 508 | let src_pin = src_pins[0]; 509 | let src_out_ty = pin_out_compat(snarl.get_node(src_pin.node).unwrap()); 510 | let dst_in_candidates = [ 511 | ("Sink", (|| DemoNode::Sink) as fn() -> DemoNode, PIN_SINK), 512 | ("Show Image", || DemoNode::ShowImage(String::new()), PIN_STR), 513 | ("Expr", || DemoNode::ExprNode(ExprNode::new()), PIN_STR), 514 | ]; 515 | 516 | for (name, ctor, in_ty) in dst_in_candidates { 517 | if src_out_ty & in_ty != 0 && ui.button(name).clicked() { 518 | // Create new node. 519 | let new_node = snarl.insert_node(pos, ctor()); 520 | let dst_pin = InPinId { 521 | node: new_node, 522 | input: 0, 523 | }; 524 | 525 | // Connect the wire. 526 | snarl.connect(src_pin, dst_pin); 527 | ui.close_menu(); 528 | } 529 | } 530 | } 531 | AnyPins::In(pins) => { 532 | let all_src_types = pins.iter().fold(0, |acc, pin| { 533 | acc | pin_in_compat(snarl.get_node(pin.node).unwrap(), pin.input) 534 | }); 535 | 536 | let dst_out_candidates = [ 537 | ( 538 | "Number", 539 | (|| DemoNode::Number(0.)) as fn() -> DemoNode, 540 | PIN_NUM, 541 | ), 542 | ("String", || DemoNode::String(String::new()), PIN_STR), 543 | ("Expr", || DemoNode::ExprNode(ExprNode::new()), PIN_NUM), 544 | ("Show Image", || DemoNode::ShowImage(String::new()), PIN_IMG), 545 | ]; 546 | 547 | for (name, ctor, out_ty) in dst_out_candidates { 548 | if all_src_types & out_ty != 0 && ui.button(name).clicked() { 549 | // Create new node. 550 | let new_node = ctor(); 551 | let dst_ty = pin_out_compat(&new_node); 552 | 553 | let new_node = snarl.insert_node(pos, new_node); 554 | let dst_pin = OutPinId { 555 | node: new_node, 556 | output: 0, 557 | }; 558 | 559 | // Connect the wire. 560 | for src_pin in pins { 561 | let src_ty = 562 | pin_in_compat(snarl.get_node(src_pin.node).unwrap(), src_pin.input); 563 | if src_ty & dst_ty != 0 { 564 | // In this demo, input pin MUST be unique ... 565 | // Therefore here we drop inputs of source input pin. 566 | snarl.drop_inputs(*src_pin); 567 | snarl.connect(dst_pin, *src_pin); 568 | ui.close_menu(); 569 | } 570 | } 571 | } 572 | } 573 | } 574 | }; 575 | } 576 | 577 | fn has_node_menu(&mut self, _node: &DemoNode) -> bool { 578 | true 579 | } 580 | 581 | fn show_node_menu( 582 | &mut self, 583 | node: NodeId, 584 | _inputs: &[InPin], 585 | _outputs: &[OutPin], 586 | ui: &mut Ui, 587 | snarl: &mut Snarl, 588 | ) { 589 | ui.label("Node menu"); 590 | if ui.button("Remove").clicked() { 591 | snarl.remove_node(node); 592 | ui.close_menu(); 593 | } 594 | } 595 | 596 | fn has_on_hover_popup(&mut self, _: &DemoNode) -> bool { 597 | true 598 | } 599 | 600 | fn show_on_hover_popup( 601 | &mut self, 602 | node: NodeId, 603 | _inputs: &[InPin], 604 | _outputs: &[OutPin], 605 | ui: &mut Ui, 606 | snarl: &mut Snarl, 607 | ) { 608 | match snarl[node] { 609 | DemoNode::Sink => { 610 | ui.label("Displays anything connected to it"); 611 | } 612 | DemoNode::Number(_) => { 613 | ui.label("Outputs integer value"); 614 | } 615 | DemoNode::String(_) => { 616 | ui.label("Outputs string value"); 617 | } 618 | DemoNode::ShowImage(_) => { 619 | ui.label("Displays image from URL in input"); 620 | } 621 | DemoNode::ExprNode(_) => { 622 | ui.label("Evaluates algebraic expression with input for each unique variable name"); 623 | } 624 | } 625 | } 626 | 627 | fn header_frame( 628 | &mut self, 629 | frame: egui::Frame, 630 | node: NodeId, 631 | _inputs: &[InPin], 632 | _outputs: &[OutPin], 633 | snarl: &Snarl, 634 | ) -> egui::Frame { 635 | match snarl[node] { 636 | DemoNode::Sink => frame.fill(egui::Color32::from_rgb(70, 70, 80)), 637 | DemoNode::Number(_) => frame.fill(egui::Color32::from_rgb(70, 40, 40)), 638 | DemoNode::String(_) => frame.fill(egui::Color32::from_rgb(40, 70, 40)), 639 | DemoNode::ShowImage(_) => frame.fill(egui::Color32::from_rgb(40, 40, 70)), 640 | DemoNode::ExprNode(_) => frame.fill(egui::Color32::from_rgb(70, 66, 40)), 641 | } 642 | } 643 | } 644 | 645 | #[derive(Clone, serde::Serialize, serde::Deserialize)] 646 | struct ExprNode { 647 | text: String, 648 | bindings: Vec, 649 | values: Vec, 650 | expr: Expr, 651 | } 652 | 653 | impl ExprNode { 654 | fn new() -> Self { 655 | ExprNode { 656 | text: "0".to_string(), 657 | bindings: Vec::new(), 658 | values: Vec::new(), 659 | expr: Expr::Val(0.0), 660 | } 661 | } 662 | 663 | fn eval(&self) -> f64 { 664 | self.expr.eval(&self.bindings, &self.values) 665 | } 666 | } 667 | 668 | #[derive(Clone, Copy, serde::Serialize, serde::Deserialize)] 669 | enum UnOp { 670 | Pos, 671 | Neg, 672 | } 673 | 674 | #[derive(Clone, Copy, serde::Serialize, serde::Deserialize)] 675 | enum BinOp { 676 | Add, 677 | Sub, 678 | Mul, 679 | Div, 680 | } 681 | 682 | #[derive(Clone, serde::Serialize, serde::Deserialize)] 683 | enum Expr { 684 | Var(String), 685 | Val(f64), 686 | UnOp { 687 | op: UnOp, 688 | expr: Box, 689 | }, 690 | BinOp { 691 | lhs: Box, 692 | op: BinOp, 693 | rhs: Box, 694 | }, 695 | } 696 | 697 | impl Expr { 698 | fn eval(&self, bindings: &[String], args: &[f64]) -> f64 { 699 | let binding_index = 700 | |name: &str| bindings.iter().position(|binding| binding == name).unwrap(); 701 | 702 | match self { 703 | Expr::Var(name) => args[binding_index(name)], 704 | Expr::Val(value) => *value, 705 | Expr::UnOp { op, expr } => match op { 706 | UnOp::Pos => expr.eval(bindings, args), 707 | UnOp::Neg => -expr.eval(bindings, args), 708 | }, 709 | Expr::BinOp { lhs, op, rhs } => match op { 710 | BinOp::Add => lhs.eval(bindings, args) + rhs.eval(bindings, args), 711 | BinOp::Sub => lhs.eval(bindings, args) - rhs.eval(bindings, args), 712 | BinOp::Mul => lhs.eval(bindings, args) * rhs.eval(bindings, args), 713 | BinOp::Div => lhs.eval(bindings, args) / rhs.eval(bindings, args), 714 | }, 715 | } 716 | } 717 | 718 | fn extend_bindings(&self, bindings: &mut Vec) { 719 | match self { 720 | Expr::Var(name) => { 721 | if !bindings.contains(name) { 722 | bindings.push(name.clone()); 723 | } 724 | } 725 | Expr::Val(_) => {} 726 | Expr::UnOp { expr, .. } => { 727 | expr.extend_bindings(bindings); 728 | } 729 | Expr::BinOp { lhs, rhs, .. } => { 730 | lhs.extend_bindings(bindings); 731 | rhs.extend_bindings(bindings); 732 | } 733 | } 734 | } 735 | } 736 | 737 | impl syn::parse::Parse for UnOp { 738 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 739 | let lookahead = input.lookahead1(); 740 | if lookahead.peek(syn::Token![+]) { 741 | input.parse::()?; 742 | Ok(UnOp::Pos) 743 | } else if lookahead.peek(syn::Token![-]) { 744 | input.parse::()?; 745 | Ok(UnOp::Neg) 746 | } else { 747 | Err(lookahead.error()) 748 | } 749 | } 750 | } 751 | 752 | impl syn::parse::Parse for BinOp { 753 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 754 | let lookahead = input.lookahead1(); 755 | if lookahead.peek(syn::Token![+]) { 756 | input.parse::()?; 757 | Ok(BinOp::Add) 758 | } else if lookahead.peek(syn::Token![-]) { 759 | input.parse::()?; 760 | Ok(BinOp::Sub) 761 | } else if lookahead.peek(syn::Token![*]) { 762 | input.parse::()?; 763 | Ok(BinOp::Mul) 764 | } else if lookahead.peek(syn::Token![/]) { 765 | input.parse::()?; 766 | Ok(BinOp::Div) 767 | } else { 768 | Err(lookahead.error()) 769 | } 770 | } 771 | } 772 | 773 | impl syn::parse::Parse for Expr { 774 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 775 | let lookahead = input.lookahead1(); 776 | 777 | let lhs; 778 | if lookahead.peek(syn::token::Paren) { 779 | let content; 780 | syn::parenthesized!(content in input); 781 | let expr = content.parse::()?; 782 | if input.is_empty() { 783 | return Ok(expr); 784 | } 785 | lhs = expr; 786 | // } else if lookahead.peek(syn::LitFloat) { 787 | // let lit = input.parse::()?; 788 | // let value = lit.base10_parse::()?; 789 | // let expr = Expr::Val(value); 790 | // if input.is_empty() { 791 | // return Ok(expr); 792 | // } 793 | // lhs = expr; 794 | } else if lookahead.peek(syn::LitInt) { 795 | let lit = input.parse::()?; 796 | let value = lit.base10_parse::()?; 797 | let expr = Expr::Val(value); 798 | if input.is_empty() { 799 | return Ok(expr); 800 | } 801 | lhs = expr; 802 | } else if lookahead.peek(syn::Ident) { 803 | let ident = input.parse::()?; 804 | let expr = Expr::Var(ident.to_string()); 805 | if input.is_empty() { 806 | return Ok(expr); 807 | } 808 | lhs = expr; 809 | } else { 810 | let unop = input.parse::()?; 811 | 812 | return Self::parse_with_unop(unop, input); 813 | } 814 | 815 | let binop = input.parse::()?; 816 | 817 | Self::parse_binop(Box::new(lhs), binop, input) 818 | } 819 | } 820 | 821 | impl Expr { 822 | fn parse_with_unop(op: UnOp, input: syn::parse::ParseStream) -> syn::Result { 823 | let lookahead = input.lookahead1(); 824 | 825 | let lhs; 826 | if lookahead.peek(syn::token::Paren) { 827 | let content; 828 | syn::parenthesized!(content in input); 829 | let expr = Expr::UnOp { 830 | op, 831 | expr: Box::new(content.parse::()?), 832 | }; 833 | if input.is_empty() { 834 | return Ok(expr); 835 | } 836 | lhs = expr; 837 | } else if lookahead.peek(syn::LitFloat) { 838 | let lit = input.parse::()?; 839 | let value = lit.base10_parse::()?; 840 | let expr = Expr::UnOp { 841 | op, 842 | expr: Box::new(Expr::Val(value)), 843 | }; 844 | if input.is_empty() { 845 | return Ok(expr); 846 | } 847 | lhs = expr; 848 | } else if lookahead.peek(syn::LitInt) { 849 | let lit = input.parse::()?; 850 | let value = lit.base10_parse::()?; 851 | let expr = Expr::UnOp { 852 | op, 853 | expr: Box::new(Expr::Val(value)), 854 | }; 855 | if input.is_empty() { 856 | return Ok(expr); 857 | } 858 | lhs = expr; 859 | } else if lookahead.peek(syn::Ident) { 860 | let ident = input.parse::()?; 861 | let expr = Expr::UnOp { 862 | op, 863 | expr: Box::new(Expr::Var(ident.to_string())), 864 | }; 865 | if input.is_empty() { 866 | return Ok(expr); 867 | } 868 | lhs = expr; 869 | } else { 870 | return Err(lookahead.error()); 871 | } 872 | 873 | let op = input.parse::()?; 874 | 875 | Self::parse_binop(Box::new(lhs), op, input) 876 | } 877 | 878 | fn parse_binop(lhs: Box, op: BinOp, input: syn::parse::ParseStream) -> syn::Result { 879 | let lookahead = input.lookahead1(); 880 | 881 | let rhs; 882 | if lookahead.peek(syn::token::Paren) { 883 | let content; 884 | syn::parenthesized!(content in input); 885 | rhs = Box::new(content.parse::()?); 886 | if input.is_empty() { 887 | return Ok(Expr::BinOp { lhs, op, rhs }); 888 | } 889 | } else if lookahead.peek(syn::LitFloat) { 890 | let lit = input.parse::()?; 891 | let value = lit.base10_parse::()?; 892 | rhs = Box::new(Expr::Val(value)); 893 | if input.is_empty() { 894 | return Ok(Expr::BinOp { lhs, op, rhs }); 895 | } 896 | } else if lookahead.peek(syn::LitInt) { 897 | let lit = input.parse::()?; 898 | let value = lit.base10_parse::()?; 899 | rhs = Box::new(Expr::Val(value)); 900 | if input.is_empty() { 901 | return Ok(Expr::BinOp { lhs, op, rhs }); 902 | } 903 | } else if lookahead.peek(syn::Ident) { 904 | let ident = input.parse::()?; 905 | rhs = Box::new(Expr::Var(ident.to_string())); 906 | if input.is_empty() { 907 | return Ok(Expr::BinOp { lhs, op, rhs }); 908 | } 909 | } else { 910 | return Err(lookahead.error()); 911 | } 912 | 913 | let next_op = input.parse::()?; 914 | 915 | if let (BinOp::Add | BinOp::Sub, BinOp::Mul | BinOp::Div) = (op, next_op) { 916 | let rhs = Self::parse_binop(rhs, next_op, input)?; 917 | Ok(Self::BinOp { 918 | lhs, 919 | op, 920 | rhs: Box::new(rhs), 921 | }) 922 | } else { 923 | let lhs = Self::BinOp { lhs, op, rhs }; 924 | Self::parse_binop(Box::new(lhs), next_op, input) 925 | } 926 | } 927 | } 928 | 929 | pub struct DemoApp { 930 | snarl: Snarl, 931 | style: SnarlStyle, 932 | } 933 | 934 | const fn default_style() -> SnarlStyle { 935 | SnarlStyle { 936 | node_layout: Some(NodeLayout::FlippedSandwich), 937 | pin_placement: Some(PinPlacement::Edge), 938 | pin_size: Some(7.0), 939 | node_frame: Some(egui::Frame { 940 | inner_margin: egui::Margin::same(8), 941 | outer_margin: egui::Margin { 942 | left: 0, 943 | right: 0, 944 | top: 0, 945 | bottom: 4, 946 | }, 947 | corner_radius: egui::CornerRadius::same(8), 948 | fill: egui::Color32::from_gray(30), 949 | stroke: egui::Stroke::NONE, 950 | shadow: egui::Shadow::NONE, 951 | }), 952 | bg_frame: Some(egui::Frame { 953 | inner_margin: egui::Margin::ZERO, 954 | outer_margin: egui::Margin::same(2), 955 | corner_radius: egui::CornerRadius::ZERO, 956 | fill: egui::Color32::from_gray(40), 957 | stroke: egui::Stroke::NONE, 958 | shadow: egui::Shadow::NONE, 959 | }), 960 | ..SnarlStyle::new() 961 | } 962 | } 963 | 964 | impl DemoApp { 965 | pub fn new(cx: &CreationContext) -> Self { 966 | egui_extras::install_image_loaders(&cx.egui_ctx); 967 | 968 | cx.egui_ctx.style_mut(|style| style.animation_time *= 10.0); 969 | 970 | let snarl = cx.storage.map_or_else(Snarl::new, |storage| { 971 | storage 972 | .get_string("snarl") 973 | .and_then(|snarl| serde_json::from_str(&snarl).ok()) 974 | .unwrap_or_default() 975 | }); 976 | // let snarl = Snarl::new(); 977 | 978 | let style = cx.storage.map_or_else(default_style, |storage| { 979 | storage 980 | .get_string("style") 981 | .and_then(|style| serde_json::from_str(&style).ok()) 982 | .unwrap_or_else(default_style) 983 | }); 984 | // let style = SnarlStyle::new(); 985 | 986 | DemoApp { snarl, style } 987 | } 988 | } 989 | 990 | impl App for DemoApp { 991 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 992 | egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { 993 | // The top panel is often a good place for a menu bar: 994 | 995 | egui::menu::bar(ui, |ui| { 996 | #[cfg(not(target_arch = "wasm32"))] 997 | { 998 | ui.menu_button("File", |ui| { 999 | if ui.button("Quit").clicked() { 1000 | ctx.send_viewport_cmd(egui::ViewportCommand::Close); 1001 | } 1002 | }); 1003 | ui.add_space(16.0); 1004 | } 1005 | 1006 | egui::widgets::global_theme_preference_switch(ui); 1007 | 1008 | if ui.button("Clear All").clicked() { 1009 | self.snarl = Snarl::default(); 1010 | } 1011 | }); 1012 | }); 1013 | 1014 | egui::SidePanel::left("style").show(ctx, |ui| { 1015 | egui::ScrollArea::vertical().show(ui, |ui| { 1016 | egui_probe::Probe::new(&mut self.style).show(ui); 1017 | }); 1018 | }); 1019 | 1020 | egui::SidePanel::right("selected-list").show(ctx, |ui| { 1021 | egui::ScrollArea::vertical().show(ui, |ui| { 1022 | ui.strong("Selected nodes"); 1023 | 1024 | let selected = get_selected_nodes(Id::new("snarl-demo"), ui.ctx()); 1025 | 1026 | let mut selected = selected 1027 | .into_iter() 1028 | .map(|id| (id, &self.snarl[id])) 1029 | .collect::>(); 1030 | 1031 | selected.sort_by_key(|(id, _)| *id); 1032 | 1033 | let mut remove = None; 1034 | 1035 | for (id, node) in selected { 1036 | ui.horizontal(|ui| { 1037 | ui.label(format!("{id:?}")); 1038 | ui.label(node.name()); 1039 | ui.add_space(ui.spacing().item_spacing.x); 1040 | if ui.button("Remove").clicked() { 1041 | remove = Some(id); 1042 | } 1043 | }); 1044 | } 1045 | 1046 | if let Some(id) = remove { 1047 | self.snarl.remove_node(id); 1048 | } 1049 | }); 1050 | }); 1051 | 1052 | egui::CentralPanel::default().show(ctx, |ui| { 1053 | SnarlWidget::new() 1054 | .id(Id::new("snarl-demo")) 1055 | .style(self.style) 1056 | .show(&mut self.snarl, &mut DemoViewer, ui); 1057 | }); 1058 | } 1059 | 1060 | fn save(&mut self, storage: &mut dyn eframe::Storage) { 1061 | let snarl = serde_json::to_string(&self.snarl).unwrap(); 1062 | storage.set_string("snarl", snarl); 1063 | 1064 | let style = serde_json::to_string(&self.style).unwrap(); 1065 | storage.set_string("style", style); 1066 | } 1067 | } 1068 | 1069 | // When compiling natively: 1070 | #[cfg(not(target_arch = "wasm32"))] 1071 | fn main() -> eframe::Result<()> { 1072 | let native_options = eframe::NativeOptions { 1073 | viewport: egui::ViewportBuilder::default() 1074 | .with_inner_size([400.0, 300.0]) 1075 | .with_min_inner_size([300.0, 220.0]), 1076 | ..Default::default() 1077 | }; 1078 | 1079 | eframe::run_native( 1080 | "egui-snarl demo", 1081 | native_options, 1082 | Box::new(|cx| Ok(Box::new(DemoApp::new(cx)))), 1083 | ) 1084 | } 1085 | 1086 | #[cfg(target_arch = "wasm32")] 1087 | fn get_canvas_element() -> Option { 1088 | use eframe::wasm_bindgen::JsCast; 1089 | 1090 | let document = web_sys::window()?.document()?; 1091 | let canvas = document.get_element_by_id("egui_snarl_demo")?; 1092 | canvas.dyn_into::().ok() 1093 | } 1094 | 1095 | // When compiling to web using trunk: 1096 | #[cfg(target_arch = "wasm32")] 1097 | fn main() { 1098 | let canvas = get_canvas_element().expect("Failed to find canvas with id 'egui_snarl_demo'"); 1099 | 1100 | let web_options = eframe::WebOptions::default(); 1101 | 1102 | wasm_bindgen_futures::spawn_local(async { 1103 | eframe::WebRunner::new() 1104 | .start( 1105 | canvas, 1106 | web_options, 1107 | Box::new(|cx| Ok(Box::new(DemoApp::new(cx)))), 1108 | ) 1109 | .await 1110 | .expect("failed to start eframe"); 1111 | }); 1112 | } 1113 | 1114 | fn format_float(v: f64) -> String { 1115 | let v = (v * 1000.0).round() / 1000.0; 1116 | format!("{v}") 1117 | } 1118 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zakarumych/egui-snarl/449295aad135b9605a81c17dda42b0196b2093ca/logo.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! # egui-snarl 3 | //! 4 | //! Provides a node-graph container for egui. 5 | //! 6 | //! 7 | 8 | #![deny(missing_docs, non_ascii_idents, unsafe_code)] 9 | #![deny( 10 | clippy::correctness, 11 | clippy::complexity, 12 | clippy::perf, 13 | clippy::style, 14 | clippy::suspicious 15 | )] 16 | #![warn(clippy::pedantic, clippy::dbg_macro, clippy::must_use_candidate)] 17 | #![allow(clippy::range_plus_one, clippy::inline_always)] 18 | 19 | pub mod ui; 20 | 21 | use std::ops::{Index, IndexMut}; 22 | 23 | use egui::{ahash::HashSet, Pos2}; 24 | use slab::Slab; 25 | 26 | impl Default for Snarl { 27 | fn default() -> Self { 28 | Snarl::new() 29 | } 30 | } 31 | 32 | /// Node identifier. 33 | /// 34 | /// This is newtype wrapper around [`usize`] that implements 35 | /// necessary traits, but omits arithmetic operations. 36 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 37 | #[repr(transparent)] 38 | #[cfg_attr( 39 | feature = "serde", 40 | derive(serde::Serialize, serde::Deserialize), 41 | serde(transparent) 42 | )] 43 | pub struct NodeId(pub usize); 44 | 45 | /// Node of the graph. 46 | #[derive(Clone, Debug)] 47 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 48 | #[non_exhaustive] 49 | pub struct Node { 50 | /// Node generic value. 51 | pub value: T, 52 | 53 | /// Position of the top-left corner of the node. 54 | /// This does not include frame margin. 55 | pub pos: egui::Pos2, 56 | 57 | /// Flag indicating that the node is open - not collapsed. 58 | pub open: bool, 59 | } 60 | 61 | /// Output pin identifier. 62 | /// Cosists of node id and pin index. 63 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 64 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 65 | pub struct OutPinId { 66 | /// Node id. 67 | pub node: NodeId, 68 | 69 | /// Output pin index. 70 | pub output: usize, 71 | } 72 | 73 | /// Input pin identifier. Cosists of node id and pin index. 74 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 75 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 76 | pub struct InPinId { 77 | /// Node id. 78 | pub node: NodeId, 79 | 80 | /// Input pin index. 81 | pub input: usize, 82 | } 83 | 84 | /// Connection between two nodes. 85 | /// 86 | /// Nodes may support multiple connections to the same input or output. 87 | /// But duplicate connections between same input and the same output are not allowed. 88 | /// Attempt to insert existing connection will be ignored. 89 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 90 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 91 | struct Wire { 92 | out_pin: OutPinId, 93 | in_pin: InPinId, 94 | } 95 | 96 | #[derive(Clone, Debug)] 97 | struct Wires { 98 | wires: HashSet, 99 | } 100 | 101 | #[cfg(feature = "serde")] 102 | impl serde::Serialize for Wires { 103 | fn serialize(&self, serializer: S) -> Result 104 | where 105 | S: serde::Serializer, 106 | { 107 | use serde::ser::SerializeSeq; 108 | 109 | let mut seq = serializer.serialize_seq(Some(self.wires.len()))?; 110 | for wire in &self.wires { 111 | seq.serialize_element(&wire)?; 112 | } 113 | seq.end() 114 | } 115 | } 116 | 117 | #[cfg(feature = "serde")] 118 | impl<'de> serde::Deserialize<'de> for Wires { 119 | fn deserialize(deserializer: D) -> Result 120 | where 121 | D: serde::Deserializer<'de>, 122 | { 123 | struct Visitor; 124 | 125 | impl<'de> serde::de::Visitor<'de> for Visitor { 126 | type Value = HashSet; 127 | 128 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 129 | formatter.write_str("a sequence of wires") 130 | } 131 | 132 | fn visit_seq(self, mut seq: A) -> Result 133 | where 134 | A: serde::de::SeqAccess<'de>, 135 | { 136 | let mut wires = HashSet::with_hasher(egui::ahash::RandomState::new()); 137 | while let Some(wire) = seq.next_element()? { 138 | wires.insert(wire); 139 | } 140 | Ok(wires) 141 | } 142 | } 143 | 144 | let wires = deserializer.deserialize_seq(Visitor)?; 145 | Ok(Wires { wires }) 146 | } 147 | } 148 | 149 | impl Wires { 150 | fn new() -> Self { 151 | Wires { 152 | wires: HashSet::with_hasher(egui::ahash::RandomState::new()), 153 | } 154 | } 155 | 156 | fn insert(&mut self, wire: Wire) -> bool { 157 | self.wires.insert(wire) 158 | } 159 | 160 | fn remove(&mut self, wire: &Wire) -> bool { 161 | self.wires.remove(wire) 162 | } 163 | 164 | fn drop_node(&mut self, node: NodeId) -> usize { 165 | let count = self.wires.len(); 166 | self.wires 167 | .retain(|wire| wire.out_pin.node != node && wire.in_pin.node != node); 168 | count - self.wires.len() 169 | } 170 | 171 | fn drop_inputs(&mut self, pin: InPinId) -> usize { 172 | let count = self.wires.len(); 173 | self.wires.retain(|wire| wire.in_pin != pin); 174 | count - self.wires.len() 175 | } 176 | 177 | fn drop_outputs(&mut self, pin: OutPinId) -> usize { 178 | let count = self.wires.len(); 179 | self.wires.retain(|wire| wire.out_pin != pin); 180 | count - self.wires.len() 181 | } 182 | 183 | fn wired_inputs(&self, out_pin: OutPinId) -> impl Iterator + '_ { 184 | self.wires 185 | .iter() 186 | .filter(move |wire| wire.out_pin == out_pin) 187 | .map(|wire| (wire.in_pin)) 188 | } 189 | 190 | fn wired_outputs(&self, in_pin: InPinId) -> impl Iterator + '_ { 191 | self.wires 192 | .iter() 193 | .filter(move |wire| wire.in_pin == in_pin) 194 | .map(|wire| (wire.out_pin)) 195 | } 196 | 197 | fn iter(&self) -> impl Iterator + '_ { 198 | self.wires.iter().copied() 199 | } 200 | } 201 | 202 | /// Snarl is generic node-graph container. 203 | /// 204 | /// It holds graph state - positioned nodes and wires between their pins. 205 | /// It can be rendered using [`Snarl::show`]. 206 | #[derive(Clone, Debug)] 207 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 208 | pub struct Snarl { 209 | // #[cfg_attr(feature = "serde", serde(with = "serde_nodes"))] 210 | nodes: Slab>, 211 | wires: Wires, 212 | } 213 | 214 | impl Snarl { 215 | /// Create a new empty Snarl. 216 | /// 217 | /// # Examples 218 | /// 219 | /// ``` 220 | /// # use egui_snarl::Snarl; 221 | /// let snarl = Snarl::<()>::new(); 222 | /// ``` 223 | #[must_use] 224 | pub fn new() -> Self { 225 | Snarl { 226 | nodes: Slab::new(), 227 | wires: Wires::new(), 228 | } 229 | } 230 | 231 | /// Adds a node to the Snarl. 232 | /// Returns the index of the node. 233 | /// 234 | /// # Examples 235 | /// 236 | /// ``` 237 | /// # use egui_snarl::Snarl; 238 | /// let mut snarl = Snarl::<()>::new(); 239 | /// snarl.insert_node(egui::pos2(0.0, 0.0), ()); 240 | /// ``` 241 | pub fn insert_node(&mut self, pos: egui::Pos2, node: T) -> NodeId { 242 | let idx = self.nodes.insert(Node { 243 | value: node, 244 | pos, 245 | open: true, 246 | }); 247 | 248 | NodeId(idx) 249 | } 250 | 251 | /// Adds a node to the Snarl in collapsed state. 252 | /// Returns the index of the node. 253 | /// 254 | /// # Examples 255 | /// 256 | /// ``` 257 | /// # use egui_snarl::Snarl; 258 | /// let mut snarl = Snarl::<()>::new(); 259 | /// snarl.insert_node_collapsed(egui::pos2(0.0, 0.0), ()); 260 | /// ``` 261 | pub fn insert_node_collapsed(&mut self, pos: egui::Pos2, node: T) -> NodeId { 262 | let idx = self.nodes.insert(Node { 263 | value: node, 264 | pos, 265 | open: false, 266 | }); 267 | 268 | NodeId(idx) 269 | } 270 | 271 | /// Opens or collapses a node. 272 | /// 273 | /// # Panics 274 | /// 275 | /// Panics if the node does not exist. 276 | #[track_caller] 277 | pub fn open_node(&mut self, node: NodeId, open: bool) { 278 | self.nodes[node.0].open = open; 279 | } 280 | 281 | /// Removes a node from the Snarl. 282 | /// Returns the node if it was removed. 283 | /// 284 | /// # Panics 285 | /// 286 | /// Panics if the node does not exist. 287 | /// 288 | /// # Examples 289 | /// 290 | /// ``` 291 | /// # use egui_snarl::Snarl; 292 | /// let mut snarl = Snarl::<()>::new(); 293 | /// let node = snarl.insert_node(egui::pos2(0.0, 0.0), ()); 294 | /// snarl.remove_node(node); 295 | /// ``` 296 | #[track_caller] 297 | pub fn remove_node(&mut self, idx: NodeId) -> T { 298 | let value = self.nodes.remove(idx.0).value; 299 | self.wires.drop_node(idx); 300 | value 301 | } 302 | 303 | /// Connects two nodes. 304 | /// Returns true if the connection was successful. 305 | /// Returns false if the connection already exists. 306 | /// 307 | /// # Panics 308 | /// 309 | /// Panics if either node does not exist. 310 | #[track_caller] 311 | pub fn connect(&mut self, from: OutPinId, to: InPinId) -> bool { 312 | assert!(self.nodes.contains(from.node.0)); 313 | assert!(self.nodes.contains(to.node.0)); 314 | 315 | let wire = Wire { 316 | out_pin: from, 317 | in_pin: to, 318 | }; 319 | self.wires.insert(wire) 320 | } 321 | 322 | /// Disconnects two nodes. 323 | /// Returns true if the connection was removed. 324 | /// 325 | /// # Panics 326 | /// 327 | /// Panics if either node does not exist. 328 | #[track_caller] 329 | pub fn disconnect(&mut self, from: OutPinId, to: InPinId) -> bool { 330 | assert!(self.nodes.contains(from.node.0)); 331 | assert!(self.nodes.contains(to.node.0)); 332 | 333 | let wire = Wire { 334 | out_pin: from, 335 | in_pin: to, 336 | }; 337 | 338 | self.wires.remove(&wire) 339 | } 340 | 341 | /// Removes all connections to the node's pin. 342 | /// 343 | /// Returns number of removed connections. 344 | /// 345 | /// # Panics 346 | /// 347 | /// Panics if the node does not exist. 348 | #[track_caller] 349 | pub fn drop_inputs(&mut self, pin: InPinId) -> usize { 350 | assert!(self.nodes.contains(pin.node.0)); 351 | self.wires.drop_inputs(pin) 352 | } 353 | 354 | /// Removes all connections from the node's pin. 355 | /// Returns number of removed connections. 356 | /// 357 | /// # Panics 358 | /// 359 | /// Panics if the node does not exist. 360 | #[track_caller] 361 | pub fn drop_outputs(&mut self, pin: OutPinId) -> usize { 362 | assert!(self.nodes.contains(pin.node.0)); 363 | self.wires.drop_outputs(pin) 364 | } 365 | 366 | /// Returns reference to the node. 367 | #[must_use] 368 | pub fn get_node(&self, idx: NodeId) -> Option<&T> { 369 | self.nodes.get(idx.0).map(|node| &node.value) 370 | } 371 | 372 | /// Returns mutable reference to the node. 373 | pub fn get_node_mut(&mut self, idx: NodeId) -> Option<&mut T> { 374 | match self.nodes.get_mut(idx.0) { 375 | Some(node) => Some(&mut node.value), 376 | None => None, 377 | } 378 | } 379 | 380 | /// Returns reference to the node data. 381 | #[must_use] 382 | pub fn get_node_info(&self, idx: NodeId) -> Option<&Node> { 383 | self.nodes.get(idx.0) 384 | } 385 | 386 | /// Returns mutable reference to the node data. 387 | pub fn get_node_info_mut(&mut self, idx: NodeId) -> Option<&mut Node> { 388 | self.nodes.get_mut(idx.0) 389 | } 390 | 391 | /// Iterates over shared references to each node. 392 | pub fn nodes(&self) -> NodesIter<'_, T> { 393 | NodesIter { 394 | nodes: self.nodes.iter(), 395 | } 396 | } 397 | 398 | /// Iterates over mutable references to each node. 399 | pub fn nodes_mut(&mut self) -> NodesIterMut<'_, T> { 400 | NodesIterMut { 401 | nodes: self.nodes.iter_mut(), 402 | } 403 | } 404 | 405 | /// Iterates over shared references to each node and its position. 406 | pub fn nodes_pos(&self) -> NodesPosIter<'_, T> { 407 | NodesPosIter { 408 | nodes: self.nodes.iter(), 409 | } 410 | } 411 | 412 | /// Iterates over mutable references to each node and its position. 413 | pub fn nodes_pos_mut(&mut self) -> NodesPosIterMut<'_, T> { 414 | NodesPosIterMut { 415 | nodes: self.nodes.iter_mut(), 416 | } 417 | } 418 | 419 | /// Iterates over shared references to each node and its identifier. 420 | pub fn node_ids(&self) -> NodesIdsIter<'_, T> { 421 | NodesIdsIter { 422 | nodes: self.nodes.iter(), 423 | } 424 | } 425 | 426 | /// Iterates over mutable references to each node and its identifier. 427 | pub fn nodes_ids_mut(&mut self) -> NodesIdsIterMut<'_, T> { 428 | NodesIdsIterMut { 429 | nodes: self.nodes.iter_mut(), 430 | } 431 | } 432 | 433 | /// Iterates over shared references to each node, its position and its identifier. 434 | pub fn nodes_pos_ids(&self) -> NodesPosIdsIter<'_, T> { 435 | NodesPosIdsIter { 436 | nodes: self.nodes.iter(), 437 | } 438 | } 439 | 440 | /// Iterates over mutable references to each node, its position and its identifier. 441 | pub fn nodes_pos_ids_mut(&mut self) -> NodesPosIdsIterMut<'_, T> { 442 | NodesPosIdsIterMut { 443 | nodes: self.nodes.iter_mut(), 444 | } 445 | } 446 | 447 | /// Iterates over shared references to each node data. 448 | pub fn nodes_info(&self) -> NodeInfoIter<'_, T> { 449 | NodeInfoIter { 450 | nodes: self.nodes.iter(), 451 | } 452 | } 453 | 454 | /// Iterates over mutable references to each node data. 455 | pub fn nodes_info_mut(&mut self) -> NodeInfoIterMut<'_, T> { 456 | NodeInfoIterMut { 457 | nodes: self.nodes.iter_mut(), 458 | } 459 | } 460 | 461 | /// Iterates over shared references to each node id and data. 462 | pub fn nodes_ids_data(&self) -> NodeIdsDataIter<'_, T> { 463 | NodeIdsDataIter { 464 | nodes: self.nodes.iter(), 465 | } 466 | } 467 | 468 | /// Iterates over mutable references to each node id and data. 469 | pub fn nodes_ids_data_mut(&mut self) -> NodeIdsDataIterMut<'_, T> { 470 | NodeIdsDataIterMut { 471 | nodes: self.nodes.iter_mut(), 472 | } 473 | } 474 | 475 | /// Iterates over wires. 476 | pub fn wires(&self) -> impl Iterator + '_ { 477 | self.wires.iter().map(|wire| (wire.out_pin, wire.in_pin)) 478 | } 479 | 480 | /// Returns input pin of the node. 481 | #[must_use] 482 | pub fn in_pin(&self, pin: InPinId) -> InPin { 483 | InPin::new(self, pin) 484 | } 485 | 486 | /// Returns output pin of the node. 487 | #[must_use] 488 | pub fn out_pin(&self, pin: OutPinId) -> OutPin { 489 | OutPin::new(self, pin) 490 | } 491 | } 492 | 493 | impl Index for Snarl { 494 | type Output = T; 495 | 496 | #[inline] 497 | #[track_caller] 498 | fn index(&self, idx: NodeId) -> &Self::Output { 499 | &self.nodes[idx.0].value 500 | } 501 | } 502 | 503 | impl IndexMut for Snarl { 504 | #[inline] 505 | #[track_caller] 506 | fn index_mut(&mut self, idx: NodeId) -> &mut Self::Output { 507 | &mut self.nodes[idx.0].value 508 | } 509 | } 510 | 511 | /// Iterator over shared references to nodes. 512 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 513 | pub struct NodesIter<'a, T> { 514 | nodes: slab::Iter<'a, Node>, 515 | } 516 | 517 | impl<'a, T> Iterator for NodesIter<'a, T> { 518 | type Item = &'a T; 519 | 520 | fn size_hint(&self) -> (usize, Option) { 521 | self.nodes.size_hint() 522 | } 523 | 524 | fn next(&mut self) -> Option<&'a T> { 525 | let (_, node) = self.nodes.next()?; 526 | Some(&node.value) 527 | } 528 | 529 | fn nth(&mut self, n: usize) -> Option<&'a T> { 530 | let (_, node) = self.nodes.nth(n)?; 531 | Some(&node.value) 532 | } 533 | } 534 | 535 | /// Iterator over mutable references to nodes. 536 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 537 | pub struct NodesIterMut<'a, T> { 538 | nodes: slab::IterMut<'a, Node>, 539 | } 540 | 541 | impl<'a, T> Iterator for NodesIterMut<'a, T> { 542 | type Item = &'a mut T; 543 | 544 | fn size_hint(&self) -> (usize, Option) { 545 | self.nodes.size_hint() 546 | } 547 | 548 | fn next(&mut self) -> Option<&'a mut T> { 549 | let (_, node) = self.nodes.next()?; 550 | Some(&mut node.value) 551 | } 552 | 553 | fn nth(&mut self, n: usize) -> Option<&'a mut T> { 554 | let (_, node) = self.nodes.nth(n)?; 555 | Some(&mut node.value) 556 | } 557 | } 558 | 559 | /// Iterator over shared references to nodes and their positions. 560 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 561 | pub struct NodesPosIter<'a, T> { 562 | nodes: slab::Iter<'a, Node>, 563 | } 564 | 565 | impl<'a, T> Iterator for NodesPosIter<'a, T> { 566 | type Item = (Pos2, &'a T); 567 | 568 | fn size_hint(&self) -> (usize, Option) { 569 | self.nodes.size_hint() 570 | } 571 | 572 | fn next(&mut self) -> Option<(Pos2, &'a T)> { 573 | let (_, node) = self.nodes.next()?; 574 | Some((node.pos, &node.value)) 575 | } 576 | 577 | fn nth(&mut self, n: usize) -> Option<(Pos2, &'a T)> { 578 | let (_, node) = self.nodes.nth(n)?; 579 | Some((node.pos, &node.value)) 580 | } 581 | } 582 | 583 | /// Iterator over mutable references to nodes and their positions. 584 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 585 | pub struct NodesPosIterMut<'a, T> { 586 | nodes: slab::IterMut<'a, Node>, 587 | } 588 | 589 | impl<'a, T> Iterator for NodesPosIterMut<'a, T> { 590 | type Item = (Pos2, &'a mut T); 591 | 592 | fn size_hint(&self) -> (usize, Option) { 593 | self.nodes.size_hint() 594 | } 595 | 596 | fn next(&mut self) -> Option<(Pos2, &'a mut T)> { 597 | let (_, node) = self.nodes.next()?; 598 | Some((node.pos, &mut node.value)) 599 | } 600 | 601 | fn nth(&mut self, n: usize) -> Option<(Pos2, &'a mut T)> { 602 | let (_, node) = self.nodes.nth(n)?; 603 | Some((node.pos, &mut node.value)) 604 | } 605 | } 606 | 607 | /// Iterator over shared references to nodes and their identifiers. 608 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 609 | pub struct NodesIdsIter<'a, T> { 610 | nodes: slab::Iter<'a, Node>, 611 | } 612 | 613 | impl<'a, T> Iterator for NodesIdsIter<'a, T> { 614 | type Item = (NodeId, &'a T); 615 | 616 | fn size_hint(&self) -> (usize, Option) { 617 | self.nodes.size_hint() 618 | } 619 | 620 | fn next(&mut self) -> Option<(NodeId, &'a T)> { 621 | let (idx, node) = self.nodes.next()?; 622 | Some((NodeId(idx), &node.value)) 623 | } 624 | 625 | fn nth(&mut self, n: usize) -> Option<(NodeId, &'a T)> { 626 | let (idx, node) = self.nodes.nth(n)?; 627 | Some((NodeId(idx), &node.value)) 628 | } 629 | } 630 | 631 | /// Iterator over mutable references to nodes and their identifiers. 632 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 633 | pub struct NodesIdsIterMut<'a, T> { 634 | nodes: slab::IterMut<'a, Node>, 635 | } 636 | 637 | impl<'a, T> Iterator for NodesIdsIterMut<'a, T> { 638 | type Item = (NodeId, &'a mut T); 639 | 640 | fn size_hint(&self) -> (usize, Option) { 641 | self.nodes.size_hint() 642 | } 643 | 644 | fn next(&mut self) -> Option<(NodeId, &'a mut T)> { 645 | let (idx, node) = self.nodes.next()?; 646 | Some((NodeId(idx), &mut node.value)) 647 | } 648 | 649 | fn nth(&mut self, n: usize) -> Option<(NodeId, &'a mut T)> { 650 | let (idx, node) = self.nodes.nth(n)?; 651 | Some((NodeId(idx), &mut node.value)) 652 | } 653 | } 654 | 655 | /// Iterator over shared references to nodes, their positions and their identifiers. 656 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 657 | pub struct NodesPosIdsIter<'a, T> { 658 | nodes: slab::Iter<'a, Node>, 659 | } 660 | 661 | impl<'a, T> Iterator for NodesPosIdsIter<'a, T> { 662 | type Item = (NodeId, Pos2, &'a T); 663 | 664 | fn size_hint(&self) -> (usize, Option) { 665 | self.nodes.size_hint() 666 | } 667 | 668 | fn next(&mut self) -> Option<(NodeId, Pos2, &'a T)> { 669 | let (idx, node) = self.nodes.next()?; 670 | Some((NodeId(idx), node.pos, &node.value)) 671 | } 672 | 673 | fn nth(&mut self, n: usize) -> Option<(NodeId, Pos2, &'a T)> { 674 | let (idx, node) = self.nodes.nth(n)?; 675 | Some((NodeId(idx), node.pos, &node.value)) 676 | } 677 | } 678 | 679 | /// Iterator over mutable references to nodes, their positions and their identifiers. 680 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 681 | pub struct NodesPosIdsIterMut<'a, T> { 682 | nodes: slab::IterMut<'a, Node>, 683 | } 684 | 685 | impl<'a, T> Iterator for NodesPosIdsIterMut<'a, T> { 686 | type Item = (NodeId, Pos2, &'a mut T); 687 | 688 | fn size_hint(&self) -> (usize, Option) { 689 | self.nodes.size_hint() 690 | } 691 | 692 | fn next(&mut self) -> Option<(NodeId, Pos2, &'a mut T)> { 693 | let (idx, node) = self.nodes.next()?; 694 | Some((NodeId(idx), node.pos, &mut node.value)) 695 | } 696 | 697 | fn nth(&mut self, n: usize) -> Option<(NodeId, Pos2, &'a mut T)> { 698 | let (idx, node) = self.nodes.nth(n)?; 699 | Some((NodeId(idx), node.pos, &mut node.value)) 700 | } 701 | } 702 | 703 | /// Iterator over shared references to nodes. 704 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 705 | pub struct NodeInfoIter<'a, T> { 706 | nodes: slab::Iter<'a, Node>, 707 | } 708 | 709 | impl<'a, T> Iterator for NodeInfoIter<'a, T> { 710 | type Item = &'a Node; 711 | 712 | fn size_hint(&self) -> (usize, Option) { 713 | self.nodes.size_hint() 714 | } 715 | 716 | fn next(&mut self) -> Option<&'a Node> { 717 | let (_, node) = self.nodes.next()?; 718 | Some(node) 719 | } 720 | 721 | fn nth(&mut self, n: usize) -> Option<&'a Node> { 722 | let (_, node) = self.nodes.nth(n)?; 723 | Some(node) 724 | } 725 | } 726 | 727 | /// Iterator over mutable references to nodes. 728 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 729 | pub struct NodeInfoIterMut<'a, T> { 730 | nodes: slab::IterMut<'a, Node>, 731 | } 732 | 733 | impl<'a, T> Iterator for NodeInfoIterMut<'a, T> { 734 | type Item = &'a mut Node; 735 | 736 | fn size_hint(&self) -> (usize, Option) { 737 | self.nodes.size_hint() 738 | } 739 | 740 | fn next(&mut self) -> Option<&'a mut Node> { 741 | let (_, node) = self.nodes.next()?; 742 | Some(node) 743 | } 744 | 745 | fn nth(&mut self, n: usize) -> Option<&'a mut Node> { 746 | let (_, node) = self.nodes.nth(n)?; 747 | Some(node) 748 | } 749 | } 750 | 751 | /// Iterator over shared references to nodes. 752 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 753 | pub struct NodeIdsDataIter<'a, T> { 754 | nodes: slab::Iter<'a, Node>, 755 | } 756 | 757 | impl<'a, T> Iterator for NodeIdsDataIter<'a, T> { 758 | type Item = (NodeId, &'a Node); 759 | 760 | fn size_hint(&self) -> (usize, Option) { 761 | self.nodes.size_hint() 762 | } 763 | 764 | fn next(&mut self) -> Option<(NodeId, &'a Node)> { 765 | let (id, node) = self.nodes.next()?; 766 | Some((NodeId(id), node)) 767 | } 768 | 769 | fn nth(&mut self, n: usize) -> Option<(NodeId, &'a Node)> { 770 | let (id, node) = self.nodes.nth(n)?; 771 | Some((NodeId(id), node)) 772 | } 773 | } 774 | 775 | /// Iterator over mutable references to nodes. 776 | #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] 777 | pub struct NodeIdsDataIterMut<'a, T> { 778 | nodes: slab::IterMut<'a, Node>, 779 | } 780 | 781 | impl<'a, T> Iterator for NodeIdsDataIterMut<'a, T> { 782 | type Item = (NodeId, &'a mut Node); 783 | 784 | fn size_hint(&self) -> (usize, Option) { 785 | self.nodes.size_hint() 786 | } 787 | 788 | fn next(&mut self) -> Option<(NodeId, &'a mut Node)> { 789 | let (id, node) = self.nodes.next()?; 790 | Some((NodeId(id), node)) 791 | } 792 | 793 | fn nth(&mut self, n: usize) -> Option<(NodeId, &'a mut Node)> { 794 | let (id, node) = self.nodes.nth(n)?; 795 | Some((NodeId(id), node)) 796 | } 797 | } 798 | 799 | /// Node and its output pin. 800 | #[derive(Clone, Debug)] 801 | pub struct OutPin { 802 | /// Output pin identifier. 803 | pub id: OutPinId, 804 | 805 | /// List of input pins connected to this output pin. 806 | pub remotes: Vec, 807 | } 808 | 809 | /// Node and its output pin. 810 | #[derive(Clone, Debug)] 811 | pub struct InPin { 812 | /// Input pin identifier. 813 | pub id: InPinId, 814 | 815 | /// List of output pins connected to this input pin. 816 | pub remotes: Vec, 817 | } 818 | 819 | impl OutPin { 820 | fn new(snarl: &Snarl, pin: OutPinId) -> Self { 821 | OutPin { 822 | id: pin, 823 | remotes: snarl.wires.wired_inputs(pin).collect(), 824 | } 825 | } 826 | } 827 | 828 | impl InPin { 829 | fn new(snarl: &Snarl, pin: InPinId) -> Self { 830 | InPin { 831 | id: pin, 832 | remotes: snarl.wires.wired_outputs(pin).collect(), 833 | } 834 | } 835 | } 836 | -------------------------------------------------------------------------------- /src/ui/background_pattern.rs: -------------------------------------------------------------------------------- 1 | use egui::{emath::Rot2, vec2, Painter, Rect, Style, Vec2}; 2 | 3 | use super::SnarlStyle; 4 | 5 | ///Grid background pattern. 6 | ///Use `SnarlStyle::background_pattern_stroke` for change stroke options 7 | #[derive(Clone, Copy, Debug, PartialEq)] 8 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 9 | #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] 10 | pub struct Grid { 11 | /// Spacing between grid lines. 12 | pub spacing: Vec2, 13 | 14 | /// Angle of the grid. 15 | #[cfg_attr(feature = "egui-probe", egui_probe(as egui_probe::angle))] 16 | pub angle: f32, 17 | } 18 | 19 | const DEFAULT_GRID_SPACING: Vec2 = vec2(50.0, 50.0); 20 | macro_rules! default_grid_spacing { 21 | () => { 22 | stringify!(vec2(50.0, 50.0)) 23 | }; 24 | } 25 | 26 | const DEFAULT_GRID_ANGLE: f32 = 1.0; 27 | macro_rules! default_grid_angle { 28 | () => { 29 | stringify!(1.0) 30 | }; 31 | } 32 | 33 | impl Default for Grid { 34 | fn default() -> Self { 35 | Self { 36 | spacing: DEFAULT_GRID_SPACING, 37 | angle: DEFAULT_GRID_ANGLE, 38 | } 39 | } 40 | } 41 | 42 | impl Grid { 43 | /// Create new grid with given spacing and angle. 44 | #[must_use] 45 | pub const fn new(spacing: Vec2, angle: f32) -> Self { 46 | Self { spacing, angle } 47 | } 48 | 49 | fn draw(&self, viewport: &Rect, snarl_style: &SnarlStyle, style: &Style, painter: &Painter) { 50 | let bg_stroke = snarl_style.get_bg_pattern_stroke(style); 51 | 52 | let spacing = vec2(self.spacing.x.max(1.0), self.spacing.y.max(1.0)); 53 | 54 | let rot = Rot2::from_angle(self.angle); 55 | let rot_inv = rot.inverse(); 56 | 57 | let pattern_bounds = viewport.rotate_bb(rot_inv); 58 | 59 | let min_x = (pattern_bounds.min.x / spacing.x).ceil(); 60 | let max_x = (pattern_bounds.max.x / spacing.x).floor(); 61 | 62 | #[allow(clippy::cast_possible_truncation)] 63 | for x in 0..=f32::ceil(max_x - min_x) as i64 { 64 | #[allow(clippy::cast_precision_loss)] 65 | let x = (x as f32 + min_x) * spacing.x; 66 | 67 | let top = (rot * vec2(x, pattern_bounds.min.y)).to_pos2(); 68 | let bottom = (rot * vec2(x, pattern_bounds.max.y)).to_pos2(); 69 | 70 | painter.line_segment([top, bottom], bg_stroke); 71 | } 72 | 73 | let min_y = (pattern_bounds.min.y / spacing.y).ceil(); 74 | let max_y = (pattern_bounds.max.y / spacing.y).floor(); 75 | 76 | #[allow(clippy::cast_possible_truncation)] 77 | for y in 0..=f32::ceil(max_y - min_y) as i64 { 78 | #[allow(clippy::cast_precision_loss)] 79 | let y = (y as f32 + min_y) * spacing.y; 80 | 81 | let top = (rot * vec2(pattern_bounds.min.x, y)).to_pos2(); 82 | let bottom = (rot * vec2(pattern_bounds.max.x, y)).to_pos2(); 83 | 84 | painter.line_segment([top, bottom], bg_stroke); 85 | } 86 | } 87 | } 88 | 89 | /// Background pattern show beneath nodes and wires. 90 | #[derive(Clone, Copy, Debug, PartialEq)] 91 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 92 | #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] 93 | pub enum BackgroundPattern { 94 | /// No pattern. 95 | NoPattern, 96 | 97 | /// Linear grid. 98 | #[cfg_attr(feature = "egui-probe", egui_probe(transparent))] 99 | Grid(Grid), 100 | } 101 | 102 | impl Default for BackgroundPattern { 103 | fn default() -> Self { 104 | BackgroundPattern::new() 105 | } 106 | } 107 | 108 | impl BackgroundPattern { 109 | /// Create new background pattern with default values. 110 | /// 111 | /// Default patter is `Grid` with spacing - ` 112 | #[doc = default_grid_spacing!()] 113 | /// ` and angle - ` 114 | #[doc = default_grid_angle!()] 115 | /// ` radian. 116 | #[must_use] 117 | pub const fn new() -> Self { 118 | Self::Grid(Grid::new(DEFAULT_GRID_SPACING, DEFAULT_GRID_ANGLE)) 119 | } 120 | 121 | /// Create new grid background pattern with given spacing and angle. 122 | #[must_use] 123 | pub const fn grid(spacing: Vec2, angle: f32) -> Self { 124 | Self::Grid(Grid::new(spacing, angle)) 125 | } 126 | 127 | /// Draws background pattern. 128 | pub fn draw( 129 | &self, 130 | viewport: &Rect, 131 | snarl_style: &SnarlStyle, 132 | style: &Style, 133 | painter: &Painter, 134 | ) { 135 | match self { 136 | BackgroundPattern::Grid(g) => g.draw(viewport, snarl_style, style, painter), 137 | BackgroundPattern::NoPattern => {} 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/ui/effect.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use egui::Pos2; 4 | 5 | use crate::{wire_pins, InPinId, Node, OutPinId, Snarl}; 6 | 7 | pub enum Effect { 8 | /// Adds a new node to the Snarl. 9 | InsertNode { pos: Pos2, node: T }, 10 | 11 | /// Removes a node from snarl. 12 | RemoveNode { node: NodeId }, 13 | 14 | /// Opens/closes a node. 15 | OpenNode { node: NodeId, open: bool }, 16 | 17 | /// Adds connection between two nodes. 18 | Connect { from: OutPinId, to: InPinId }, 19 | 20 | /// Removes connection between two nodes. 21 | Disconnect { from: OutPinId, to: InPinId }, 22 | 23 | /// Removes all connections from the output pin. 24 | DropOutputs { pin: OutPinId }, 25 | 26 | /// Removes all connections to the input pin. 27 | DropInputs { pin: InPinId }, 28 | 29 | /// Executes a closure with mutable reference to the Snarl. 30 | Closure(Box)>), 31 | } 32 | 33 | /// Contained for deferred execution of effects. 34 | /// It is populated by [`SnarlViewer`] methods and then applied to the Snarl. 35 | pub struct Effects { 36 | effects: Vec>, 37 | } 38 | 39 | impl Default for Effects { 40 | #[inline] 41 | fn default() -> Self { 42 | Effects { 43 | effects: Default::default(), 44 | } 45 | } 46 | } 47 | 48 | impl Effects { 49 | #[inline(always)] 50 | #[doc(hidden)] 51 | pub fn new() -> Self { 52 | Effects { 53 | effects: Vec::new(), 54 | } 55 | } 56 | 57 | /// Returns `true` if there are no effects. 58 | /// Returns `false` otherwise. 59 | #[inline(always)] 60 | pub fn is_empty(&self) -> bool { 61 | self.effects.is_empty() 62 | } 63 | 64 | /// Inserts a new node to the Snarl. 65 | #[inline(always)] 66 | pub fn insert_node(&mut self, pos: Pos2, node: T) { 67 | self.effects.push(Effect::InsertNode { node, pos }); 68 | } 69 | 70 | /// Removes a node from the Snarl. 71 | #[inline(always)] 72 | pub fn remove_node(&mut self, node: NodeId) { 73 | self.effects.push(Effect::RemoveNode { node }); 74 | } 75 | 76 | /// Opens/closes a node. 77 | #[inline(always)] 78 | pub fn open_node(&mut self, node: NodeId, open: bool) { 79 | self.effects.push(Effect::OpenNode { node, open }); 80 | } 81 | 82 | /// Connects two nodes. 83 | #[inline(always)] 84 | pub fn connect(&mut self, from: OutPinId, to: InPinId) { 85 | self.effects.push(Effect::Connect { from, to }); 86 | } 87 | 88 | /// Disconnects two nodes. 89 | #[inline(always)] 90 | pub fn disconnect(&mut self, from: OutPinId, to: InPinId) { 91 | self.effects.push(Effect::Disconnect { from, to }); 92 | } 93 | 94 | /// Removes all connections from the output pin. 95 | #[inline(always)] 96 | pub fn drop_inputs(&mut self, pin: InPinId) { 97 | self.effects.push(Effect::DropInputs { pin }); 98 | } 99 | 100 | /// Removes all connections to the input pin. 101 | #[inline(always)] 102 | pub fn drop_outputs(&mut self, pin: OutPinId) { 103 | self.effects.push(Effect::DropOutputs { pin }); 104 | } 105 | } 106 | 107 | impl Snarl { 108 | pub fn apply_effects(&mut self, effects: Effects) { 109 | if effects.effects.is_empty() { 110 | return; 111 | } 112 | for effect in effects.effects { 113 | self.apply_effect(effect); 114 | } 115 | } 116 | 117 | pub fn apply_effect(&mut self, effect: Effect) { 118 | match effect { 119 | Effect::InsertNode { node, pos } => { 120 | let idx = self.nodes.insert(Node { 121 | value: RefCell::new(node), 122 | pos, 123 | open: true, 124 | }); 125 | self.draw_order.push(idx); 126 | } 127 | Effect::RemoveNode { node } => { 128 | if self.nodes.contains(node) { 129 | self.remove_node(node); 130 | } 131 | } 132 | Effect::OpenNode { node, open } => { 133 | if self.nodes.contains(node) { 134 | self.nodes[node].open = open; 135 | } 136 | } 137 | Effect::Connect { from, to } => { 138 | if self.nodes.contains(from.node) && self.nodes.contains(to.node) { 139 | self.wires.insert(wire_pins(from, to)); 140 | } 141 | } 142 | Effect::Disconnect { from, to } => { 143 | if self.nodes.contains(from.node) && self.nodes.contains(to.node) { 144 | self.wires.remove(&wire_pins(from, to)); 145 | } 146 | } 147 | Effect::DropOutputs { pin } => { 148 | if self.nodes.contains(pin.node) { 149 | self.wires.drop_outputs(pin); 150 | } 151 | } 152 | Effect::DropInputs { pin } => { 153 | if self.nodes.contains(pin.node) { 154 | self.wires.drop_inputs(pin); 155 | } 156 | } 157 | Effect::Closure(f) => f(self), 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/ui/pin.rs: -------------------------------------------------------------------------------- 1 | use egui::{epaint::PathShape, pos2, vec2, Color32, Painter, Rect, Shape, Stroke, Style, Vec2}; 2 | 3 | use crate::{InPinId, OutPinId}; 4 | 5 | use super::{SnarlStyle, WireStyle}; 6 | 7 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 8 | pub enum AnyPin { 9 | Out(OutPinId), 10 | In(InPinId), 11 | } 12 | 13 | /// In the current context, these are the I/O pins of the 'source' node that the newly 14 | /// created node's I/O pins will connect to. 15 | #[derive(Debug)] 16 | pub enum AnyPins<'a> { 17 | /// Output pins. 18 | Out(&'a [OutPinId]), 19 | /// Input pins 20 | In(&'a [InPinId]), 21 | } 22 | 23 | /// Contains information about a pin's wire. 24 | /// Used to draw the wire. 25 | /// When two pins are connected, the wire is drawn between them, 26 | /// using merged `PinWireInfo` from both pins. 27 | pub struct PinWireInfo { 28 | /// Desired color of the wire. 29 | pub color: Color32, 30 | 31 | /// Desired style of the wire. 32 | /// Zoomed with current scale. 33 | pub style: WireStyle, 34 | } 35 | 36 | /// Uses `Painter` to draw a pin. 37 | pub trait SnarlPin { 38 | /// Calculates pin Rect from the given parameters. 39 | fn pin_rect(&self, x: f32, y0: f32, y1: f32, size: f32) -> Rect { 40 | // Center vertically by default. 41 | let y = (y0 + y1) * 0.5; 42 | let pin_pos = pos2(x, y); 43 | Rect::from_center_size(pin_pos, vec2(size, size)) 44 | } 45 | 46 | /// Draws the pin. 47 | /// 48 | /// `rect` is the interaction rectangle of the pin. 49 | /// Pin should fit in it. 50 | /// `painter` is used to add pin's shapes to the UI. 51 | /// 52 | /// Returns the color 53 | #[must_use] 54 | fn draw( 55 | self, 56 | snarl_style: &SnarlStyle, 57 | style: &Style, 58 | rect: Rect, 59 | painter: &Painter, 60 | ) -> PinWireInfo; 61 | } 62 | 63 | /// Shape of a pin. 64 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] 65 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 66 | #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] 67 | pub enum PinShape { 68 | /// Circle shape. 69 | #[default] 70 | Circle, 71 | 72 | /// Triangle shape. 73 | Triangle, 74 | 75 | /// Square shape. 76 | Square, 77 | 78 | /// Star shape. 79 | Star, 80 | } 81 | 82 | /// Information about a pin returned by `SnarlViewer::show_input` and `SnarlViewer::show_output`. 83 | /// 84 | /// All fields are optional. 85 | /// If a field is `None`, the default value is used derived from the graph style. 86 | #[derive(Default)] 87 | pub struct PinInfo { 88 | /// Shape of the pin. 89 | pub shape: Option, 90 | 91 | /// Fill color of the pin. 92 | pub fill: Option, 93 | 94 | /// Outline stroke of the pin. 95 | pub stroke: Option, 96 | 97 | /// Color of the wire connected to the pin. 98 | /// If `None`, the pin's fill color is used. 99 | pub wire_color: Option, 100 | 101 | /// Style of the wire connected to the pin. 102 | pub wire_style: Option, 103 | 104 | /// Custom vertical position of a pin 105 | pub position: Option, 106 | } 107 | 108 | impl PinInfo { 109 | /// Sets the shape of the pin. 110 | #[must_use] 111 | pub const fn with_shape(mut self, shape: PinShape) -> Self { 112 | self.shape = Some(shape); 113 | self 114 | } 115 | 116 | /// Sets the fill color of the pin. 117 | #[must_use] 118 | pub const fn with_fill(mut self, fill: Color32) -> Self { 119 | self.fill = Some(fill); 120 | self 121 | } 122 | 123 | /// Sets the outline stroke of the pin. 124 | #[must_use] 125 | pub const fn with_stroke(mut self, stroke: Stroke) -> Self { 126 | self.stroke = Some(stroke); 127 | self 128 | } 129 | 130 | /// Sets the style of the wire connected to the pin. 131 | #[must_use] 132 | pub const fn with_wire_style(mut self, wire_style: WireStyle) -> Self { 133 | self.wire_style = Some(wire_style); 134 | self 135 | } 136 | 137 | /// Sets the color of the wire connected to the pin. 138 | #[must_use] 139 | pub const fn with_wire_color(mut self, wire_color: Color32) -> Self { 140 | self.wire_color = Some(wire_color); 141 | self 142 | } 143 | 144 | /// Creates a circle pin. 145 | #[must_use] 146 | pub fn circle() -> Self { 147 | PinInfo { 148 | shape: Some(PinShape::Circle), 149 | ..Default::default() 150 | } 151 | } 152 | 153 | /// Creates a triangle pin. 154 | #[must_use] 155 | pub fn triangle() -> Self { 156 | PinInfo { 157 | shape: Some(PinShape::Triangle), 158 | ..Default::default() 159 | } 160 | } 161 | 162 | /// Creates a square pin. 163 | #[must_use] 164 | pub fn square() -> Self { 165 | PinInfo { 166 | shape: Some(PinShape::Square), 167 | ..Default::default() 168 | } 169 | } 170 | 171 | /// Creates a star pin. 172 | #[must_use] 173 | pub fn star() -> Self { 174 | PinInfo { 175 | shape: Some(PinShape::Star), 176 | ..Default::default() 177 | } 178 | } 179 | 180 | /// Returns the shape of the pin. 181 | #[must_use] 182 | pub fn get_shape(&self, snarl_style: &SnarlStyle) -> PinShape { 183 | self.shape.unwrap_or_else(|| snarl_style.get_pin_shape()) 184 | } 185 | 186 | /// Returns fill color of the pin. 187 | #[must_use] 188 | pub fn get_fill(&self, snarl_style: &SnarlStyle, style: &Style) -> Color32 { 189 | self.fill.unwrap_or_else(|| snarl_style.get_pin_fill(style)) 190 | } 191 | 192 | /// Returns outline stroke of the pin. 193 | #[must_use] 194 | pub fn get_stroke(&self, snarl_style: &SnarlStyle, style: &Style) -> Stroke { 195 | self.stroke 196 | .unwrap_or_else(|| snarl_style.get_pin_stroke(style)) 197 | } 198 | 199 | /// Draws the pin and returns color. 200 | /// 201 | /// Wires are drawn with returned color by default. 202 | #[must_use] 203 | pub fn draw( 204 | &self, 205 | snarl_style: &SnarlStyle, 206 | style: &Style, 207 | rect: Rect, 208 | painter: &Painter, 209 | ) -> PinWireInfo { 210 | let shape = self.get_shape(snarl_style); 211 | let fill = self.get_fill(snarl_style, style); 212 | let stroke = self.get_stroke(snarl_style, style); 213 | draw_pin(painter, shape, fill, stroke, rect); 214 | 215 | PinWireInfo { 216 | color: self.wire_color.unwrap_or(fill), 217 | style: self.wire_style.unwrap_or(snarl_style.get_wire_style()), 218 | } 219 | } 220 | } 221 | 222 | impl SnarlPin for PinInfo { 223 | fn draw( 224 | self, 225 | snarl_style: &SnarlStyle, 226 | style: &Style, 227 | rect: Rect, 228 | painter: &Painter, 229 | ) -> PinWireInfo { 230 | Self::draw(&self, snarl_style, style, rect, painter) 231 | } 232 | } 233 | 234 | pub fn draw_pin(painter: &Painter, shape: PinShape, fill: Color32, stroke: Stroke, rect: Rect) { 235 | let center = rect.center(); 236 | let size = f32::min(rect.width(), rect.height()); 237 | 238 | match shape { 239 | PinShape::Circle => { 240 | painter.circle(center, size / 2.0, fill, stroke); 241 | } 242 | PinShape::Triangle => { 243 | const A: Vec2 = vec2(-0.649_519, 0.4875); 244 | const B: Vec2 = vec2(0.649_519, 0.4875); 245 | const C: Vec2 = vec2(0.0, -0.6375); 246 | 247 | let points = vec![center + A * size, center + B * size, center + C * size]; 248 | 249 | painter.add(Shape::Path(PathShape { 250 | points, 251 | closed: true, 252 | fill, 253 | stroke: stroke.into(), 254 | })); 255 | } 256 | PinShape::Square => { 257 | let points = vec![ 258 | center + vec2(-0.5, -0.5) * size, 259 | center + vec2(0.5, -0.5) * size, 260 | center + vec2(0.5, 0.5) * size, 261 | center + vec2(-0.5, 0.5) * size, 262 | ]; 263 | 264 | painter.add(Shape::Path(PathShape { 265 | points, 266 | closed: true, 267 | fill, 268 | stroke: stroke.into(), 269 | })); 270 | } 271 | 272 | PinShape::Star => { 273 | let points = vec![ 274 | center + size * 0.700_000 * vec2(0.0, -1.0), 275 | center + size * 0.267_376 * vec2(-0.587_785, -0.809_017), 276 | center + size * 0.700_000 * vec2(-0.951_057, -0.309_017), 277 | center + size * 0.267_376 * vec2(-0.951_057, 0.309_017), 278 | center + size * 0.700_000 * vec2(-0.587_785, 0.809_017), 279 | center + size * 0.267_376 * vec2(0.0, 1.0), 280 | center + size * 0.700_000 * vec2(0.587_785, 0.809_017), 281 | center + size * 0.267_376 * vec2(0.951_057, 0.309_017), 282 | center + size * 0.700_000 * vec2(0.951_057, -0.309_017), 283 | center + size * 0.267_376 * vec2(0.587_785, -0.809_017), 284 | ]; 285 | 286 | painter.add(Shape::Path(PathShape { 287 | points, 288 | closed: true, 289 | fill, 290 | stroke: stroke.into(), 291 | })); 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/ui/scale.rs: -------------------------------------------------------------------------------- 1 | use egui_scale::EguiScale; 2 | 3 | use super::{BackgroundPattern, PinPlacement, SelectionStyle, SnarlStyle, WireStyle}; 4 | 5 | impl EguiScale for WireStyle { 6 | #[inline(always)] 7 | fn scale(&mut self, scale: f32) { 8 | match self { 9 | WireStyle::Line | WireStyle::Bezier3 | WireStyle::Bezier5 => {} 10 | WireStyle::AxisAligned { corner_radius } => { 11 | corner_radius.scale(scale); 12 | } 13 | } 14 | } 15 | } 16 | 17 | impl EguiScale for SelectionStyle { 18 | #[inline(always)] 19 | fn scale(&mut self, scale: f32) { 20 | self.margin.scale(scale); 21 | self.rounding.scale(scale); 22 | self.stroke.scale(scale); 23 | } 24 | } 25 | 26 | impl EguiScale for PinPlacement { 27 | fn scale(&mut self, scale: f32) { 28 | if let PinPlacement::Outside { margin } = self { 29 | margin.scale(scale); 30 | } 31 | } 32 | } 33 | 34 | impl EguiScale for BackgroundPattern { 35 | fn scale(&mut self, scale: f32) { 36 | if let BackgroundPattern::Grid(grid) = self { 37 | grid.spacing.scale(scale); 38 | } 39 | } 40 | } 41 | 42 | impl EguiScale for SnarlStyle { 43 | fn scale(&mut self, scale: f32) { 44 | self.node_frame.scale(scale); 45 | self.header_frame.scale(scale); 46 | self.header_drag_space.scale(scale); 47 | self.pin_size.scale(scale); 48 | self.pin_stroke.scale(scale); 49 | self.pin_placement.scale(scale); 50 | self.wire_width.scale(scale); 51 | self.wire_frame_size.scale(scale); 52 | self.wire_style.scale(scale); 53 | self.bg_frame.scale(scale); 54 | self.bg_pattern.scale(scale); 55 | self.bg_pattern_stroke.scale(scale); 56 | self.min_scale.scale(scale); 57 | self.max_scale.scale(scale); 58 | self.select_stoke.scale(scale); 59 | self.select_style.scale(scale); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ui/state.rs: -------------------------------------------------------------------------------- 1 | use egui::{ 2 | Context, Id, Pos2, Rect, Ui, Vec2, 3 | ahash::HashSet, 4 | emath::{GuiRounding, TSTransform}, 5 | style::Spacing, 6 | }; 7 | 8 | use crate::{InPinId, NodeId, OutPinId, Snarl}; 9 | 10 | use super::{SnarlWidget, transform_matching_points}; 11 | 12 | /// Node UI state. 13 | pub struct NodeState { 14 | /// Node size for this frame. 15 | /// It is updated to fit content. 16 | size: Vec2, 17 | header_height: f32, 18 | 19 | id: Id, 20 | dirty: bool, 21 | } 22 | 23 | #[derive(Clone, Copy, PartialEq)] 24 | struct NodeData { 25 | size: Vec2, 26 | header_height: f32, 27 | } 28 | 29 | impl NodeState { 30 | pub fn load(cx: &Context, id: Id, spacing: &Spacing) -> Self { 31 | cx.data_mut(|d| d.get_temp::(id)).map_or_else( 32 | || { 33 | cx.request_discard("NodeState initialization"); 34 | Self::initial(id, spacing) 35 | }, 36 | |data| NodeState { 37 | size: data.size, 38 | header_height: data.header_height, 39 | id, 40 | dirty: false, 41 | }, 42 | ) 43 | } 44 | 45 | pub fn clear(self, cx: &Context) { 46 | cx.data_mut(|d| d.remove::(self.id)); 47 | } 48 | 49 | pub fn store(&self, cx: &Context) { 50 | if self.dirty { 51 | cx.data_mut(|d| { 52 | d.insert_temp( 53 | self.id, 54 | NodeData { 55 | size: self.size, 56 | header_height: self.header_height, 57 | }, 58 | ); 59 | }); 60 | } 61 | } 62 | 63 | /// Finds node rect at specific position (excluding node frame margin). 64 | pub fn node_rect(&self, pos: Pos2, openness: f32) -> Rect { 65 | Rect::from_min_size( 66 | pos, 67 | egui::vec2( 68 | self.size.x, 69 | f32::max(self.header_height, self.size.y * openness), 70 | ), 71 | ) 72 | .round_ui() 73 | } 74 | 75 | pub fn payload_offset(&self, openness: f32) -> f32 { 76 | ((self.size.y) * (1.0 - openness)).round_ui() 77 | } 78 | 79 | pub fn set_size(&mut self, size: Vec2) { 80 | if self.size != size { 81 | self.size = size; 82 | self.dirty = true; 83 | } 84 | } 85 | 86 | pub fn header_height(&self) -> f32 { 87 | self.header_height.round_ui() 88 | } 89 | 90 | pub fn set_header_height(&mut self, height: f32) { 91 | #[allow(clippy::float_cmp)] 92 | if self.header_height != height { 93 | self.header_height = height; 94 | self.dirty = true; 95 | } 96 | } 97 | 98 | const fn initial(id: Id, spacing: &Spacing) -> Self { 99 | NodeState { 100 | size: spacing.interact_size, 101 | header_height: spacing.interact_size.y, 102 | id, 103 | dirty: true, 104 | } 105 | } 106 | } 107 | 108 | #[derive(Clone)] 109 | pub enum NewWires { 110 | In(Vec), 111 | Out(Vec), 112 | } 113 | 114 | #[derive(Clone, Copy)] 115 | struct RectSelect { 116 | origin: Pos2, 117 | current: Pos2, 118 | } 119 | 120 | pub struct SnarlState { 121 | /// Snarl viewport transform to global space. 122 | to_global: TSTransform, 123 | 124 | new_wires: Option, 125 | 126 | /// Flag indicating that new wires are owned by the menu now. 127 | new_wires_menu: bool, 128 | 129 | id: Id, 130 | 131 | /// Flag indicating that the graph state is dirty must be saved. 132 | dirty: bool, 133 | 134 | /// Active rect selection. 135 | rect_selection: Option, 136 | 137 | /// Order of nodes to draw. 138 | draw_order: Vec, 139 | 140 | /// List of currently selected nodes. 141 | selected_nodes: Vec, 142 | } 143 | 144 | #[derive(Clone, Default)] 145 | struct DrawOrder(Vec); 146 | 147 | impl DrawOrder { 148 | fn save(self, cx: &Context, id: Id) { 149 | cx.data_mut(|d| { 150 | if self.0.is_empty() { 151 | d.remove_temp::(id); 152 | } else { 153 | d.insert_temp::(id, self); 154 | } 155 | }); 156 | } 157 | 158 | fn load(cx: &Context, id: Id) -> Self { 159 | cx.data(|d| d.get_temp::(id)).unwrap_or_default() 160 | } 161 | } 162 | 163 | #[derive(Clone, Default)] 164 | struct SelectedNodes(Vec); 165 | 166 | impl SelectedNodes { 167 | fn save(self, cx: &Context, id: Id) { 168 | cx.data_mut(|d| { 169 | if self.0.is_empty() { 170 | d.remove_temp::(id); 171 | } else { 172 | d.insert_temp::(id, self); 173 | } 174 | }); 175 | } 176 | 177 | fn load(cx: &Context, id: Id) -> Self { 178 | cx.data(|d| d.get_temp::(id)).unwrap_or_default() 179 | } 180 | } 181 | 182 | #[derive(Clone)] 183 | struct SnarlStateData { 184 | to_global: TSTransform, 185 | new_wires: Option, 186 | new_wires_menu: bool, 187 | rect_selection: Option, 188 | } 189 | 190 | impl SnarlStateData { 191 | fn save(self, cx: &Context, id: Id) { 192 | cx.data_mut(|d| { 193 | d.insert_temp(id, self); 194 | }); 195 | } 196 | 197 | fn load(cx: &Context, id: Id) -> Option { 198 | cx.data(|d| d.get_temp(id)) 199 | } 200 | } 201 | 202 | fn prune_selected_nodes(selected_nodes: &mut Vec, snarl: &Snarl) -> bool { 203 | let old_size = selected_nodes.len(); 204 | selected_nodes.retain(|node| snarl.nodes.contains(node.0)); 205 | old_size != selected_nodes.len() 206 | } 207 | 208 | impl SnarlState { 209 | pub fn load( 210 | cx: &Context, 211 | id: Id, 212 | snarl: &Snarl, 213 | ui_rect: Rect, 214 | min_scale: f32, 215 | max_scale: f32, 216 | ) -> Self { 217 | let Some(data) = SnarlStateData::load(cx, id) else { 218 | cx.request_discard("Initial placing"); 219 | return Self::initial(id, snarl, ui_rect, min_scale, max_scale); 220 | }; 221 | 222 | let mut selected_nodes = SelectedNodes::load(cx, id).0; 223 | let dirty = prune_selected_nodes(&mut selected_nodes, snarl); 224 | 225 | let draw_order = DrawOrder::load(cx, id).0; 226 | 227 | SnarlState { 228 | to_global: data.to_global, 229 | new_wires: data.new_wires, 230 | new_wires_menu: data.new_wires_menu, 231 | id, 232 | dirty, 233 | rect_selection: data.rect_selection, 234 | draw_order, 235 | selected_nodes, 236 | } 237 | } 238 | 239 | fn initial(id: Id, snarl: &Snarl, ui_rect: Rect, min_scale: f32, max_scale: f32) -> Self { 240 | let mut bb = Rect::NOTHING; 241 | 242 | for (_, node) in &snarl.nodes { 243 | bb.extend_with(node.pos); 244 | } 245 | 246 | if bb.is_finite() { 247 | bb = bb.expand(100.0); 248 | } else if ui_rect.is_finite() { 249 | bb = ui_rect; 250 | } else { 251 | bb = Rect::from_min_max(Pos2::new(-100.0, -100.0), Pos2::new(100.0, 100.0)); 252 | } 253 | 254 | let scaling2 = ui_rect.size() / bb.size(); 255 | let scaling = scaling2.min_elem().clamp(min_scale, max_scale); 256 | 257 | let to_global = transform_matching_points(bb.center(), ui_rect.center(), scaling); 258 | 259 | SnarlState { 260 | to_global, 261 | new_wires: None, 262 | new_wires_menu: false, 263 | id, 264 | dirty: true, 265 | draw_order: Vec::new(), 266 | rect_selection: None, 267 | selected_nodes: Vec::new(), 268 | } 269 | } 270 | 271 | #[inline(always)] 272 | pub fn store(mut self, snarl: &Snarl, cx: &Context) { 273 | self.dirty |= prune_selected_nodes(&mut self.selected_nodes, snarl); 274 | 275 | if self.dirty { 276 | let data = SnarlStateData { 277 | to_global: self.to_global, 278 | new_wires: self.new_wires, 279 | new_wires_menu: self.new_wires_menu, 280 | rect_selection: self.rect_selection, 281 | }; 282 | data.save(cx, self.id); 283 | 284 | DrawOrder(self.draw_order).save(cx, self.id); 285 | SelectedNodes(self.selected_nodes).save(cx, self.id); 286 | } 287 | } 288 | 289 | pub fn to_global(&self) -> TSTransform { 290 | self.to_global 291 | } 292 | 293 | pub fn set_to_global(&mut self, to_global: TSTransform) { 294 | if self.to_global != to_global { 295 | self.to_global = to_global; 296 | self.dirty = true; 297 | } 298 | } 299 | 300 | pub fn look_at(&mut self, view: Rect, ui_rect: Rect, min_scale: f32, max_scale: f32) { 301 | let scaling2 = ui_rect.size() / view.size(); 302 | let scaling = scaling2.min_elem().clamp(min_scale, max_scale); 303 | 304 | let to_global = transform_matching_points(view.center(), ui_rect.center(), scaling); 305 | 306 | if self.to_global != to_global { 307 | self.to_global = to_global; 308 | self.dirty = true; 309 | } 310 | } 311 | 312 | pub fn start_new_wire_in(&mut self, pin: InPinId) { 313 | self.new_wires = Some(NewWires::In(vec![pin])); 314 | self.new_wires_menu = false; 315 | self.dirty = true; 316 | } 317 | 318 | pub fn start_new_wire_out(&mut self, pin: OutPinId) { 319 | self.new_wires = Some(NewWires::Out(vec![pin])); 320 | self.new_wires_menu = false; 321 | self.dirty = true; 322 | } 323 | 324 | pub fn start_new_wires_in(&mut self, pins: &[InPinId]) { 325 | self.new_wires = Some(NewWires::In(pins.to_vec())); 326 | self.new_wires_menu = false; 327 | self.dirty = true; 328 | } 329 | 330 | pub fn start_new_wires_out(&mut self, pins: &[OutPinId]) { 331 | self.new_wires = Some(NewWires::Out(pins.to_vec())); 332 | self.new_wires_menu = false; 333 | self.dirty = true; 334 | } 335 | 336 | pub fn add_new_wire_in(&mut self, pin: InPinId) { 337 | debug_assert!(self.new_wires_menu == false); 338 | let Some(NewWires::In(pins)) = &mut self.new_wires else { 339 | unreachable!(); 340 | }; 341 | 342 | if !pins.contains(&pin) { 343 | pins.push(pin); 344 | self.dirty = true; 345 | } 346 | } 347 | 348 | pub fn add_new_wire_out(&mut self, pin: OutPinId) { 349 | debug_assert!(self.new_wires_menu == false); 350 | let Some(NewWires::Out(pins)) = &mut self.new_wires else { 351 | unreachable!(); 352 | }; 353 | 354 | if !pins.contains(&pin) { 355 | pins.push(pin); 356 | self.dirty = true; 357 | } 358 | } 359 | 360 | pub fn remove_new_wire_in(&mut self, pin: InPinId) { 361 | debug_assert!(self.new_wires_menu == false); 362 | let Some(NewWires::In(pins)) = &mut self.new_wires else { 363 | unreachable!(); 364 | }; 365 | 366 | if let Some(idx) = pins.iter().position(|p| *p == pin) { 367 | pins.swap_remove(idx); 368 | self.dirty = true; 369 | } 370 | } 371 | 372 | pub fn remove_new_wire_out(&mut self, pin: OutPinId) { 373 | debug_assert!(self.new_wires_menu == false); 374 | let Some(NewWires::Out(pins)) = &mut self.new_wires else { 375 | unreachable!(); 376 | }; 377 | 378 | if let Some(idx) = pins.iter().position(|p| *p == pin) { 379 | pins.swap_remove(idx); 380 | self.dirty = true; 381 | } 382 | } 383 | 384 | pub const fn has_new_wires(&self) -> bool { 385 | match (self.new_wires.as_ref(), self.new_wires_menu) { 386 | (Some(_), false) => true, 387 | _ => false, 388 | } 389 | } 390 | 391 | pub const fn has_new_wires_in(&self) -> bool { 392 | match (&self.new_wires, self.new_wires_menu) { 393 | (Some(NewWires::In(_)), false) => true, 394 | _ => false, 395 | } 396 | } 397 | 398 | pub const fn has_new_wires_out(&self) -> bool { 399 | match (&self.new_wires, self.new_wires_menu) { 400 | (Some(NewWires::Out(_)), false) => true, 401 | _ => false, 402 | } 403 | } 404 | 405 | pub const fn new_wires(&self) -> Option<&NewWires> { 406 | match (&self.new_wires, self.new_wires_menu) { 407 | (Some(new_wires), false) => Some(new_wires), 408 | _ => None, 409 | } 410 | } 411 | 412 | pub fn take_new_wires(&mut self) -> Option { 413 | match (&self.new_wires, self.new_wires_menu) { 414 | (Some(_), false) => { 415 | self.dirty = true; 416 | self.new_wires.take() 417 | } 418 | _ => None, 419 | } 420 | } 421 | 422 | pub(crate) fn take_new_wires_menu(&mut self) -> Option { 423 | match (&self.new_wires, self.new_wires_menu) { 424 | (Some(_), true) => { 425 | self.dirty = true; 426 | self.new_wires.take() 427 | } 428 | _ => None, 429 | } 430 | } 431 | 432 | pub(crate) fn set_new_wires_menu(&mut self, wires: NewWires) { 433 | debug_assert!(self.new_wires.is_none()); 434 | self.new_wires = Some(wires); 435 | self.new_wires_menu = true; 436 | } 437 | 438 | pub(crate) fn update_draw_order(&mut self, snarl: &Snarl) -> Vec { 439 | let mut node_ids = snarl 440 | .nodes 441 | .iter() 442 | .map(|(id, _)| NodeId(id)) 443 | .collect::>(); 444 | 445 | self.draw_order.retain(|id| { 446 | let has = node_ids.remove(id); 447 | self.dirty |= !has; 448 | has 449 | }); 450 | 451 | self.dirty |= !node_ids.is_empty(); 452 | 453 | for new_id in node_ids { 454 | self.draw_order.push(new_id); 455 | } 456 | 457 | self.draw_order.clone() 458 | } 459 | 460 | pub(crate) fn node_to_top(&mut self, node: NodeId) { 461 | if let Some(order) = self.draw_order.iter().position(|idx| *idx == node) { 462 | self.draw_order.remove(order); 463 | self.draw_order.push(node); 464 | } 465 | self.dirty = true; 466 | } 467 | 468 | pub fn selected_nodes(&self) -> &[NodeId] { 469 | &self.selected_nodes 470 | } 471 | 472 | pub fn select_one_node(&mut self, reset: bool, node: NodeId) { 473 | if reset { 474 | if self.selected_nodes[..] == [node] { 475 | return; 476 | } 477 | 478 | self.deselect_all_nodes(); 479 | } else if let Some(pos) = self.selected_nodes.iter().position(|n| *n == node) { 480 | if pos == self.selected_nodes.len() - 1 { 481 | return; 482 | } 483 | self.selected_nodes.remove(pos); 484 | } 485 | self.selected_nodes.push(node); 486 | self.dirty = true; 487 | } 488 | 489 | pub fn select_many_nodes(&mut self, reset: bool, nodes: impl Iterator) { 490 | if reset { 491 | self.deselect_all_nodes(); 492 | self.selected_nodes.extend(nodes); 493 | self.dirty = true; 494 | } else { 495 | nodes.for_each(|node| self.select_one_node(false, node)); 496 | } 497 | } 498 | 499 | pub fn deselect_one_node(&mut self, node: NodeId) { 500 | if let Some(pos) = self.selected_nodes.iter().position(|n| *n == node) { 501 | self.selected_nodes.remove(pos); 502 | self.dirty = true; 503 | } 504 | } 505 | 506 | pub fn deselect_many_nodes(&mut self, nodes: impl Iterator) { 507 | for node in nodes { 508 | if let Some(pos) = self.selected_nodes.iter().position(|n| *n == node) { 509 | self.selected_nodes.remove(pos); 510 | self.dirty = true; 511 | } 512 | } 513 | } 514 | 515 | pub fn deselect_all_nodes(&mut self) { 516 | self.dirty |= !self.selected_nodes.is_empty(); 517 | self.selected_nodes.clear(); 518 | } 519 | 520 | pub fn start_rect_selection(&mut self, pos: Pos2) { 521 | self.dirty |= self.rect_selection.is_none(); 522 | self.rect_selection = Some(RectSelect { 523 | origin: pos, 524 | current: pos, 525 | }); 526 | } 527 | 528 | pub fn stop_rect_selection(&mut self) { 529 | self.dirty |= self.rect_selection.is_some(); 530 | self.rect_selection = None; 531 | } 532 | 533 | pub const fn is_rect_selection(&self) -> bool { 534 | self.rect_selection.is_some() 535 | } 536 | 537 | pub fn update_rect_selection(&mut self, pos: Pos2) { 538 | if let Some(rect_selection) = &mut self.rect_selection { 539 | rect_selection.current = pos; 540 | self.dirty = true; 541 | } 542 | } 543 | 544 | pub fn rect_selection(&self) -> Option { 545 | let rect = self.rect_selection?; 546 | Some(Rect::from_two_pos(rect.origin, rect.current)) 547 | } 548 | } 549 | 550 | impl SnarlWidget { 551 | /// Returns list of nodes selected in the UI for the `SnarlWidget` with same id. 552 | /// 553 | /// Use same `Ui` instance that was used in [`SnarlWidget::show`]. 554 | #[must_use] 555 | #[inline] 556 | pub fn get_selected_nodes(self, ui: &Ui) -> Vec { 557 | self.get_selected_nodes_at(ui.id(), ui.ctx()) 558 | } 559 | 560 | /// Returns list of nodes selected in the UI for the `SnarlWidget` with same id. 561 | /// 562 | /// `ui_id` must be the Id of the `Ui` instance that was used in [`SnarlWidget::show`]. 563 | #[must_use] 564 | #[inline] 565 | pub fn get_selected_nodes_at(self, ui_id: Id, ctx: &Context) -> Vec { 566 | let snarl_id = self.get_id(ui_id); 567 | 568 | ctx.data(|d| d.get_temp::(snarl_id).unwrap_or_default().0) 569 | } 570 | } 571 | 572 | /// Returns nodes selected in the UI for the `SnarlWidget` with same ID. 573 | /// 574 | /// Only works if [`SnarlWidget::id`] was used. 575 | /// For other cases construct [`SnarlWidget`] and use [`SnarlWidget::get_selected_nodes`] or [`SnarlWidget::get_selected_nodes_at`]. 576 | #[must_use] 577 | #[inline] 578 | pub fn get_selected_nodes(id: Id, ctx: &Context) -> Vec { 579 | ctx.data(|d| d.get_temp::(id).unwrap_or_default().0) 580 | } 581 | -------------------------------------------------------------------------------- /src/ui/viewer.rs: -------------------------------------------------------------------------------- 1 | use egui::{emath::TSTransform, Painter, Pos2, Rect, Style, Ui}; 2 | 3 | use crate::{InPin, InPinId, NodeId, OutPin, OutPinId, Snarl}; 4 | 5 | use super::{ 6 | pin::{AnyPins, SnarlPin}, 7 | BackgroundPattern, NodeLayout, SnarlStyle, 8 | }; 9 | 10 | /// `SnarlViewer` is a trait for viewing a Snarl. 11 | /// 12 | /// It can extract necessary data from the nodes and controls their 13 | /// response to certain events. 14 | pub trait SnarlViewer { 15 | /// Returns title of the node. 16 | fn title(&mut self, node: &T) -> String; 17 | 18 | /// Returns the node's frame. 19 | /// All node's elements will be rendered inside this frame. 20 | /// Except for pins if they are configured to be rendered outside of the frame. 21 | /// 22 | /// Returns `default` by default. 23 | /// `default` frame is taken from the [`SnarlStyle::node_frame`] or constructed if it's `None`. 24 | /// 25 | /// Override this method to customize the frame for specific nodes. 26 | fn node_frame( 27 | &mut self, 28 | default: egui::Frame, 29 | node: NodeId, 30 | inputs: &[InPin], 31 | outputs: &[OutPin], 32 | snarl: &Snarl, 33 | ) -> egui::Frame { 34 | let _ = (node, inputs, outputs, snarl); 35 | default 36 | } 37 | 38 | /// Returns the node's header frame. 39 | /// 40 | /// This frame would be placed on top of the node's frame. 41 | /// And header UI (see [`show_header`]) will be placed inside this frame. 42 | /// 43 | /// Returns `default` by default. 44 | /// `default` frame is taken from the [`SnarlStyle::header_frame`], 45 | /// or [`SnarlStyle::node_frame`] with removed shadow if `None`, 46 | /// or constructed if both are `None`. 47 | fn header_frame( 48 | &mut self, 49 | default: egui::Frame, 50 | node: NodeId, 51 | inputs: &[InPin], 52 | outputs: &[OutPin], 53 | snarl: &Snarl, 54 | ) -> egui::Frame { 55 | let _ = (node, inputs, outputs, snarl); 56 | default 57 | } 58 | /// Checks if node has a custom egui style. 59 | #[inline] 60 | fn has_node_style( 61 | &mut self, 62 | node: NodeId, 63 | inputs: &[InPin], 64 | outputs: &[OutPin], 65 | snarl: &Snarl, 66 | ) -> bool { 67 | let _ = (node, inputs, outputs, snarl); 68 | false 69 | } 70 | 71 | /// Modifies the node's egui style 72 | fn apply_node_style( 73 | &mut self, 74 | style: &mut Style, 75 | node: NodeId, 76 | inputs: &[InPin], 77 | outputs: &[OutPin], 78 | snarl: &Snarl, 79 | ) { 80 | let _ = (style, node, inputs, outputs, snarl); 81 | } 82 | 83 | /// Returns elements layout for the node. 84 | /// 85 | /// Node consists of 5 parts: header, body, footer, input pins and output pins. 86 | /// See [`NodeLayout`] for available placements. 87 | /// 88 | /// Returns `default` by default. 89 | /// `default` layout is taken from the [`SnarlStyle::node_layout`] or constructed if it's `None`. 90 | /// Override this method to customize the layout for specific nodes. 91 | #[inline] 92 | fn node_layout( 93 | &mut self, 94 | default: NodeLayout, 95 | node: NodeId, 96 | inputs: &[InPin], 97 | outputs: &[OutPin], 98 | snarl: &Snarl, 99 | ) -> NodeLayout { 100 | let _ = (node, inputs, outputs, snarl); 101 | default 102 | } 103 | 104 | /// Renders elements inside the node's header frame. 105 | /// 106 | /// This is the good place to show the node's title and controls related to the whole node. 107 | /// 108 | /// By default it shows the node's title. 109 | #[inline] 110 | fn show_header( 111 | &mut self, 112 | node: NodeId, 113 | inputs: &[InPin], 114 | outputs: &[OutPin], 115 | ui: &mut Ui, 116 | snarl: &mut Snarl, 117 | ) { 118 | let _ = (inputs, outputs); 119 | ui.label(self.title(&snarl[node])); 120 | } 121 | 122 | /// Returns number of input pins of the node. 123 | /// 124 | /// [`SnarlViewer::show_input`] will be called for each input in range `0..inputs()`. 125 | fn inputs(&mut self, node: &T) -> usize; 126 | 127 | /// Renders one specified node's input element and returns drawer for the corresponding pin. 128 | fn show_input( 129 | &mut self, 130 | pin: &InPin, 131 | ui: &mut Ui, 132 | snarl: &mut Snarl, 133 | ) -> impl SnarlPin + 'static; 134 | 135 | /// Returns number of output pins of the node. 136 | /// 137 | /// [`SnarlViewer::show_output`] will be called for each output in range `0..outputs()`. 138 | fn outputs(&mut self, node: &T) -> usize; 139 | 140 | /// Renders the node's output. 141 | fn show_output( 142 | &mut self, 143 | pin: &OutPin, 144 | ui: &mut Ui, 145 | snarl: &mut Snarl, 146 | ) -> impl SnarlPin + 'static; 147 | 148 | /// Checks if node has something to show in body - between input and output pins. 149 | #[inline] 150 | fn has_body(&mut self, node: &T) -> bool { 151 | let _ = node; 152 | false 153 | } 154 | 155 | /// Renders the node's body. 156 | #[inline] 157 | fn show_body( 158 | &mut self, 159 | node: NodeId, 160 | inputs: &[InPin], 161 | outputs: &[OutPin], 162 | ui: &mut Ui, 163 | snarl: &mut Snarl, 164 | ) { 165 | let _ = (node, inputs, outputs, ui, snarl); 166 | } 167 | 168 | /// Checks if node has something to show in footer - below pins and body. 169 | #[inline] 170 | fn has_footer(&mut self, node: &T) -> bool { 171 | let _ = node; 172 | false 173 | } 174 | 175 | /// Renders the node's footer. 176 | #[inline] 177 | fn show_footer( 178 | &mut self, 179 | node: NodeId, 180 | inputs: &[InPin], 181 | outputs: &[OutPin], 182 | ui: &mut Ui, 183 | snarl: &mut Snarl, 184 | ) { 185 | let _ = (node, inputs, outputs, ui, snarl); 186 | } 187 | 188 | /// Reports the final node's rect after rendering. 189 | /// 190 | /// It aimed to be used for custom positioning of nodes that requires node dimensions for calculations. 191 | /// Node's position can be modified directly in this method. 192 | #[inline] 193 | fn final_node_rect(&mut self, node: NodeId, rect: Rect, ui: &mut Ui, snarl: &mut Snarl) { 194 | let _ = (node, rect, ui, snarl); 195 | } 196 | 197 | /// Checks if node has something to show in on-hover popup. 198 | #[inline] 199 | fn has_on_hover_popup(&mut self, node: &T) -> bool { 200 | let _ = node; 201 | false 202 | } 203 | 204 | /// Renders the node's on-hover popup. 205 | #[inline] 206 | fn show_on_hover_popup( 207 | &mut self, 208 | node: NodeId, 209 | inputs: &[InPin], 210 | outputs: &[OutPin], 211 | ui: &mut Ui, 212 | snarl: &mut Snarl, 213 | ) { 214 | let _ = (node, inputs, outputs, ui, snarl); 215 | } 216 | 217 | /// Checks if wire has something to show in widget. 218 | /// This may not be called if wire is invisible. 219 | #[inline] 220 | fn has_wire_widget(&mut self, from: &OutPinId, to: &InPinId, snarl: &Snarl) -> bool { 221 | let _ = (from, to, snarl); 222 | false 223 | } 224 | 225 | /// Renders the wire's widget. 226 | /// This may not be called if wire is invisible. 227 | #[inline] 228 | fn show_wire_widget(&mut self, from: &OutPin, to: &InPin, ui: &mut Ui, snarl: &mut Snarl) { 229 | let _ = (from, to, ui, snarl); 230 | } 231 | 232 | /// Checks if the snarl has something to show in context menu if right-clicked or long-touched on empty space at `pos`. 233 | #[inline] 234 | fn has_graph_menu(&mut self, pos: Pos2, snarl: &mut Snarl) -> bool { 235 | let _ = (pos, snarl); 236 | false 237 | } 238 | 239 | /// Show context menu for the snarl. 240 | /// 241 | /// This can be used to implement menu for adding new nodes. 242 | #[inline] 243 | fn show_graph_menu(&mut self, pos: Pos2, ui: &mut Ui, snarl: &mut Snarl) { 244 | let _ = (pos, ui, snarl); 245 | } 246 | 247 | /// Checks if the snarl has something to show in context menu if wire drag is stopped at `pos`. 248 | #[inline] 249 | fn has_dropped_wire_menu(&mut self, src_pins: AnyPins, snarl: &mut Snarl) -> bool { 250 | let _ = (src_pins, snarl); 251 | false 252 | } 253 | 254 | /// Show context menu for the snarl. This menu is opened when releasing a pin to empty 255 | /// space. It can be used to implement menu for adding new node, and directly 256 | /// connecting it to the released wire. 257 | #[inline] 258 | fn show_dropped_wire_menu( 259 | &mut self, 260 | pos: Pos2, 261 | ui: &mut Ui, 262 | src_pins: AnyPins, 263 | snarl: &mut Snarl, 264 | ) { 265 | let _ = (pos, ui, src_pins, snarl); 266 | } 267 | 268 | /// Checks if the node has something to show in context menu if right-clicked or long-touched on the node. 269 | #[inline] 270 | fn has_node_menu(&mut self, node: &T) -> bool { 271 | let _ = node; 272 | false 273 | } 274 | 275 | /// Show context menu for the snarl. 276 | /// 277 | /// This can be used to implement menu for adding new nodes. 278 | #[inline] 279 | fn show_node_menu( 280 | &mut self, 281 | node: NodeId, 282 | inputs: &[InPin], 283 | outputs: &[OutPin], 284 | ui: &mut Ui, 285 | snarl: &mut Snarl, 286 | ) { 287 | let _ = (node, inputs, outputs, ui, snarl); 288 | } 289 | 290 | /// Asks the viewer to connect two pins. 291 | /// 292 | /// This is usually happens when user drags a wire from one node's output pin to another node's input pin or vice versa. 293 | /// By default this method connects the pins and returns `Ok(())`. 294 | #[inline] 295 | fn connect(&mut self, from: &OutPin, to: &InPin, snarl: &mut Snarl) { 296 | snarl.connect(from.id, to.id); 297 | } 298 | 299 | /// Asks the viewer to disconnect two pins. 300 | #[inline] 301 | fn disconnect(&mut self, from: &OutPin, to: &InPin, snarl: &mut Snarl) { 302 | snarl.disconnect(from.id, to.id); 303 | } 304 | 305 | /// Asks the viewer to disconnect all wires from the output pin. 306 | /// 307 | /// This is usually happens when right-clicking on an output pin. 308 | /// By default this method disconnects the pins and returns `Ok(())`. 309 | #[inline] 310 | fn drop_outputs(&mut self, pin: &OutPin, snarl: &mut Snarl) { 311 | snarl.drop_outputs(pin.id); 312 | } 313 | 314 | /// Asks the viewer to disconnect all wires from the input pin. 315 | /// 316 | /// This is usually happens when right-clicking on an input pin. 317 | /// By default this method disconnects the pins and returns `Ok(())`. 318 | #[inline] 319 | fn drop_inputs(&mut self, pin: &InPin, snarl: &mut Snarl) { 320 | snarl.drop_inputs(pin.id); 321 | } 322 | 323 | /// Draws background of the snarl view. 324 | /// 325 | /// By default it draws the background pattern using [`BackgroundPattern::draw`]. 326 | /// 327 | /// If you want to draw the background yourself, you can override this method. 328 | #[inline] 329 | fn draw_background( 330 | &mut self, 331 | background: Option<&BackgroundPattern>, 332 | viewport: &Rect, 333 | snarl_style: &SnarlStyle, 334 | style: &Style, 335 | painter: &Painter, 336 | snarl: &Snarl, 337 | ) { 338 | let _ = snarl; 339 | 340 | if let Some(background) = background { 341 | background.draw(viewport, snarl_style, style, painter); 342 | } 343 | } 344 | 345 | /// Informs the viewer what is the current transform of the snarl view 346 | /// and allows viewer to override it. 347 | /// 348 | /// This method is called in the beginning of the graph rendering. 349 | /// 350 | /// By default it does nothing. 351 | #[inline] 352 | fn current_transform(&mut self, to_global: &mut TSTransform, snarl: &mut Snarl) { 353 | let _ = (to_global, snarl); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/ui/wire.rs: -------------------------------------------------------------------------------- 1 | use core::f32; 2 | 3 | use egui::{Context, Id, Pos2, Rect, Shape, Stroke, Ui, ahash::HashMap, cache::CacheTrait, pos2}; 4 | 5 | use crate::Wire; 6 | 7 | const MAX_CURVE_SAMPLES: usize = 100; 8 | 9 | /// Layer where wires are rendered. 10 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 11 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 12 | #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] 13 | #[derive(Default)] 14 | pub enum WireLayer { 15 | /// Wires are rendered behind nodes. 16 | /// This is default. 17 | #[default] 18 | BehindNodes, 19 | 20 | /// Wires are rendered above nodes. 21 | AboveNodes, 22 | } 23 | 24 | /// Controls style in which wire is rendered. 25 | /// 26 | /// Variants are given in order of precedence when two pins require different styles. 27 | #[derive(Clone, Copy, Debug, PartialEq)] 28 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 29 | #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] 30 | #[derive(Default)] 31 | pub enum WireStyle { 32 | /// Straight line from one endpoint to another. 33 | Line, 34 | 35 | /// Draw wire as straight lines with 90 degree turns. 36 | /// Corners has radius of `corner_radius`. 37 | AxisAligned { 38 | /// Radius of corners in wire. 39 | corner_radius: f32, 40 | }, 41 | 42 | /// Draw wire as 3rd degree Bezier curve. 43 | Bezier3, 44 | 45 | /// Draw wire as 5th degree Bezier curve. 46 | #[default] 47 | Bezier5, 48 | } 49 | 50 | pub fn pick_wire_style(left: WireStyle, right: WireStyle) -> WireStyle { 51 | match (left, right) { 52 | (WireStyle::Line, _) | (_, WireStyle::Line) => WireStyle::Line, 53 | ( 54 | WireStyle::AxisAligned { corner_radius: a }, 55 | WireStyle::AxisAligned { corner_radius: b }, 56 | ) => WireStyle::AxisAligned { 57 | corner_radius: f32::max(a, b), 58 | }, 59 | (WireStyle::AxisAligned { corner_radius }, _) 60 | | (_, WireStyle::AxisAligned { corner_radius }) => WireStyle::AxisAligned { corner_radius }, 61 | (WireStyle::Bezier3, _) | (_, WireStyle::Bezier3) => WireStyle::Bezier3, 62 | (WireStyle::Bezier5, WireStyle::Bezier5) => WireStyle::Bezier5, 63 | } 64 | } 65 | 66 | fn adjust_frame_size( 67 | mut frame_size: f32, 68 | upscale: bool, 69 | downscale: bool, 70 | from: Pos2, 71 | to: Pos2, 72 | ) -> f32 { 73 | let length = (from - to).length(); 74 | if upscale { 75 | frame_size = frame_size.max(length / 6.0); 76 | } 77 | if downscale { 78 | frame_size = frame_size.min(length / 6.0); 79 | } 80 | frame_size 81 | } 82 | 83 | /// Returns 5th degree bezier curve control points for the wire 84 | fn wire_bezier_5(frame_size: f32, from: Pos2, to: Pos2) -> [Pos2; 6] { 85 | let from_norm_x = frame_size; 86 | let from_2 = pos2(from.x + from_norm_x, from.y); 87 | let to_norm_x = -from_norm_x; 88 | let to_2 = pos2(to.x + to_norm_x, to.y); 89 | 90 | let between = (from_2 - to_2).length(); 91 | 92 | if from_2.x <= to_2.x && between >= frame_size * 2.0 { 93 | let middle_1 = from_2 + (to_2 - from_2).normalized() * frame_size; 94 | let middle_2 = to_2 + (from_2 - to_2).normalized() * frame_size; 95 | 96 | [from, from_2, middle_1, middle_2, to_2, to] 97 | } else if from_2.x <= to_2.x { 98 | let t = (between - (to_2.y - from_2.y).abs()) 99 | / frame_size.mul_add(2.0, -(to_2.y - from_2.y).abs()); 100 | 101 | let mut middle_1 = from_2 + (to_2 - from_2).normalized() * frame_size; 102 | let mut middle_2 = to_2 + (from_2 - to_2).normalized() * frame_size; 103 | 104 | if from_2.y >= to_2.y + frame_size { 105 | let u = (from_2.y - to_2.y - frame_size) / frame_size; 106 | 107 | let t0_middle_1 = pos2( 108 | (1.0 - u).mul_add(frame_size, from_2.x), 109 | frame_size.mul_add(-u, from_2.y), 110 | ); 111 | let t0_middle_2 = pos2(to_2.x, to_2.y + frame_size); 112 | 113 | middle_1 = t0_middle_1.lerp(middle_1, t); 114 | middle_2 = t0_middle_2.lerp(middle_2, t); 115 | } else if from_2.y >= to_2.y { 116 | let u = (from_2.y - to_2.y) / frame_size; 117 | 118 | let t0_middle_1 = pos2( 119 | u.mul_add(frame_size, from_2.x), 120 | frame_size.mul_add(1.0 - u, from_2.y), 121 | ); 122 | let t0_middle_2 = pos2(to_2.x, to_2.y + frame_size); 123 | 124 | middle_1 = t0_middle_1.lerp(middle_1, t); 125 | middle_2 = t0_middle_2.lerp(middle_2, t); 126 | } else if to_2.y >= from_2.y + frame_size { 127 | let u = (to_2.y - from_2.y - frame_size) / frame_size; 128 | 129 | let t0_middle_1 = pos2(from_2.x, from_2.y + frame_size); 130 | let t0_middle_2 = pos2( 131 | (1.0 - u).mul_add(-frame_size, to_2.x), 132 | frame_size.mul_add(-u, to_2.y), 133 | ); 134 | 135 | middle_1 = t0_middle_1.lerp(middle_1, t); 136 | middle_2 = t0_middle_2.lerp(middle_2, t); 137 | } else if to_2.y >= from_2.y { 138 | let u = (to_2.y - from_2.y) / frame_size; 139 | 140 | let t0_middle_1 = pos2(from_2.x, from_2.y + frame_size); 141 | let t0_middle_2 = pos2( 142 | u.mul_add(-frame_size, to_2.x), 143 | frame_size.mul_add(1.0 - u, to_2.y), 144 | ); 145 | 146 | middle_1 = t0_middle_1.lerp(middle_1, t); 147 | middle_2 = t0_middle_2.lerp(middle_2, t); 148 | } else { 149 | unreachable!(); 150 | } 151 | 152 | [from, from_2, middle_1, middle_2, to_2, to] 153 | } else if from_2.y >= frame_size.mul_add(2.0, to_2.y) { 154 | let middle_1 = pos2(from_2.x, from_2.y - frame_size); 155 | let middle_2 = pos2(to_2.x, to_2.y + frame_size); 156 | 157 | [from, from_2, middle_1, middle_2, to_2, to] 158 | } else if from_2.y >= to_2.y + frame_size { 159 | let t = (from_2.y - to_2.y - frame_size) / frame_size; 160 | 161 | let middle_1 = pos2( 162 | (1.0 - t).mul_add(frame_size, from_2.x), 163 | frame_size.mul_add(-t, from_2.y), 164 | ); 165 | let middle_2 = pos2(to_2.x, to_2.y + frame_size); 166 | 167 | [from, from_2, middle_1, middle_2, to_2, to] 168 | } else if from_2.y >= to_2.y { 169 | let t = (from_2.y - to_2.y) / frame_size; 170 | 171 | let middle_1 = pos2( 172 | t.mul_add(frame_size, from_2.x), 173 | frame_size.mul_add(1.0 - t, from_2.y), 174 | ); 175 | let middle_2 = pos2(to_2.x, to_2.y + frame_size); 176 | 177 | [from, from_2, middle_1, middle_2, to_2, to] 178 | } else if to_2.y >= frame_size.mul_add(2.0, from_2.y) { 179 | let middle_1 = pos2(from_2.x, from_2.y + frame_size); 180 | let middle_2 = pos2(to_2.x, to_2.y - frame_size); 181 | 182 | [from, from_2, middle_1, middle_2, to_2, to] 183 | } else if to_2.y >= from_2.y + frame_size { 184 | let t = (to_2.y - from_2.y - frame_size) / frame_size; 185 | 186 | let middle_1 = pos2(from_2.x, from_2.y + frame_size); 187 | let middle_2 = pos2( 188 | (1.0 - t).mul_add(-frame_size, to_2.x), 189 | frame_size.mul_add(-t, to_2.y), 190 | ); 191 | 192 | [from, from_2, middle_1, middle_2, to_2, to] 193 | } else if to_2.y >= from_2.y { 194 | let t = (to_2.y - from_2.y) / frame_size; 195 | 196 | let middle_1 = pos2(from_2.x, from_2.y + frame_size); 197 | let middle_2 = pos2( 198 | t.mul_add(-frame_size, to_2.x), 199 | frame_size.mul_add(1.0 - t, to_2.y), 200 | ); 201 | 202 | [from, from_2, middle_1, middle_2, to_2, to] 203 | } else { 204 | unreachable!(); 205 | } 206 | } 207 | 208 | /// Returns 3rd degree bezier curve control points for the wire 209 | fn wire_bezier_3(frame_size: f32, from: Pos2, to: Pos2) -> [Pos2; 4] { 210 | let [a, b, _, _, c, d] = wire_bezier_5(frame_size, from, to); 211 | [a, b, c, d] 212 | } 213 | 214 | #[allow(clippy::too_many_arguments)] 215 | pub fn draw_wire( 216 | ui: &Ui, 217 | snarl_id: Id, 218 | wire: Option, 219 | shapes: &mut Vec, 220 | frame_size: f32, 221 | upscale: bool, 222 | downscale: bool, 223 | from: Pos2, 224 | to: Pos2, 225 | mut stroke: Stroke, 226 | threshold: f32, 227 | style: WireStyle, 228 | ) { 229 | if !ui.is_visible() { 230 | return; 231 | } 232 | 233 | if stroke.width < 1.0 { 234 | stroke.color = stroke.color.gamma_multiply(stroke.width); 235 | stroke.width = 1.0; 236 | } 237 | 238 | let frame_size = adjust_frame_size(frame_size, upscale, downscale, from, to); 239 | match style { 240 | WireStyle::Line => { 241 | let bb = Rect::from_two_pos(from, to); 242 | if ui.is_rect_visible(bb) { 243 | shapes.push(Shape::line_segment([from, to], stroke)); 244 | } 245 | } 246 | WireStyle::Bezier3 => { 247 | draw_bezier_3( 248 | ui, snarl_id, wire, frame_size, from, to, stroke, threshold, shapes, 249 | ); 250 | } 251 | 252 | WireStyle::Bezier5 => { 253 | draw_bezier_5( 254 | ui, snarl_id, wire, frame_size, from, to, stroke, threshold, shapes, 255 | ); 256 | } 257 | 258 | WireStyle::AxisAligned { corner_radius } => { 259 | draw_axis_aligned( 260 | ui, 261 | snarl_id, 262 | wire, 263 | corner_radius, 264 | frame_size, 265 | from, 266 | to, 267 | stroke, 268 | threshold, 269 | shapes, 270 | ); 271 | } 272 | } 273 | } 274 | 275 | #[allow(clippy::too_many_arguments)] 276 | pub fn hit_wire( 277 | ctx: &Context, 278 | snarl_id: Id, 279 | wire: Wire, 280 | frame_size: f32, 281 | upscale: bool, 282 | downscale: bool, 283 | from: Pos2, 284 | to: Pos2, 285 | pos: Pos2, 286 | threshold: f32, 287 | hit_threshold: f32, 288 | style: WireStyle, 289 | ) -> bool { 290 | let frame_size = adjust_frame_size(frame_size, upscale, downscale, from, to); 291 | match style { 292 | WireStyle::Line => { 293 | let aabb = Rect::from_two_pos(from, to); 294 | let aabb_e = aabb.expand(hit_threshold); 295 | if !aabb_e.contains(pos) { 296 | return false; 297 | } 298 | 299 | let a = to - from; 300 | let b = pos - from; 301 | 302 | let dot = b.dot(a); 303 | let dist2 = b.length_sq() - dot * dot / a.length_sq(); 304 | 305 | dist2 < hit_threshold * hit_threshold 306 | } 307 | WireStyle::Bezier3 => hit_wire_bezier_3( 308 | ctx, 309 | snarl_id, 310 | wire, 311 | frame_size, 312 | from, 313 | to, 314 | pos, 315 | hit_threshold, 316 | ), 317 | WireStyle::Bezier5 => hit_wire_bezier_5( 318 | ctx, 319 | snarl_id, 320 | wire, 321 | frame_size, 322 | from, 323 | to, 324 | pos, 325 | hit_threshold, 326 | ), 327 | WireStyle::AxisAligned { corner_radius } => hit_wire_axis_aligned( 328 | ctx, 329 | snarl_id, 330 | wire, 331 | corner_radius, 332 | frame_size, 333 | from, 334 | to, 335 | pos, 336 | threshold, 337 | hit_threshold, 338 | ), 339 | } 340 | } 341 | 342 | #[inline] 343 | fn bezier_arc_length_upper_bound(points: &[Pos2]) -> f32 { 344 | let mut size = 0.0; 345 | for i in 1..points.len() { 346 | size += (points[i] - points[i - 1]).length(); 347 | } 348 | size 349 | } 350 | 351 | fn bezier_hit_samples_number(points: &[Pos2], threshold: f32) -> usize { 352 | let arc_length = bezier_arc_length_upper_bound(points); 353 | 354 | #[allow(clippy::cast_sign_loss)] 355 | #[allow(clippy::cast_possible_truncation)] 356 | ((arc_length / threshold).ceil().max(0.0) as usize) 357 | } 358 | 359 | fn bezier_derivative_3(points: &[Pos2; 4]) -> [Pos2; 3] { 360 | let [p0, p1, p2, p3] = *points; 361 | 362 | let factor = 3.0; 363 | 364 | [ 365 | (factor * (p1 - p0)).to_pos2(), 366 | (factor * (p2 - p1)).to_pos2(), 367 | (factor * (p3 - p2)).to_pos2(), 368 | ] 369 | } 370 | 371 | fn bezier_derivative_5(points: &[Pos2; 6]) -> [Pos2; 5] { 372 | let [p0, p1, p2, p3, p4, p5] = *points; 373 | 374 | let factor = 5.0; 375 | 376 | [ 377 | (factor * (p1 - p0)).to_pos2(), 378 | (factor * (p2 - p1)).to_pos2(), 379 | (factor * (p3 - p2)).to_pos2(), 380 | (factor * (p4 - p3)).to_pos2(), 381 | (factor * (p5 - p4)).to_pos2(), 382 | ] 383 | } 384 | 385 | fn bezier_draw_samples_number_3(points: &[Pos2; 4], threshold: f32) -> usize { 386 | #![allow(clippy::similar_names)] 387 | #![allow(clippy::cast_precision_loss)] 388 | 389 | let d = bezier_derivative_3(points); 390 | 391 | lower_bound(2, MAX_CURVE_SAMPLES, |n| { 392 | let mut prev = points[0]; 393 | for i in 1..n { 394 | let t = i as f32 / (n - 1) as f32; 395 | let next = sample_bezier(points, t); 396 | 397 | let m = t - 0.5 / (n - 1) as f32; 398 | 399 | // Compare absolute error of mid point 400 | let mid_line = ((prev.to_vec2() + next.to_vec2()) * 0.5).to_pos2(); 401 | let mid_curve = sample_bezier(points, m); 402 | 403 | let error_sq = (mid_curve - mid_line).length_sq(); 404 | if error_sq > threshold * threshold { 405 | return false; 406 | } 407 | 408 | // Compare angular error of mid point 409 | let mid_line_dx = next.x - prev.x; 410 | let mid_line_dy = next.y - prev.y; 411 | 412 | let line_w = f32::hypot(mid_line_dx, mid_line_dy); 413 | 414 | let d_curve = sample_bezier(&d, m); 415 | let mid_curve_dx = d_curve.x; 416 | let mid_curve_dy = d_curve.y; 417 | 418 | let curve_w = f32::hypot(mid_curve_dx, mid_curve_dy); 419 | 420 | let error = f32::max( 421 | (mid_curve_dx / curve_w * line_w - mid_line_dx).abs(), 422 | (mid_curve_dy / curve_w * line_w - mid_line_dy).abs(), 423 | ); 424 | if error > threshold * 2.0 { 425 | return false; 426 | } 427 | 428 | prev = next; 429 | } 430 | 431 | true 432 | }) 433 | } 434 | 435 | fn bezier_draw_samples_number_5(points: &[Pos2; 6], threshold: f32) -> usize { 436 | #![allow(clippy::similar_names)] 437 | #![allow(clippy::cast_precision_loss)] 438 | 439 | let d = bezier_derivative_5(points); 440 | 441 | lower_bound(2, MAX_CURVE_SAMPLES, |n| { 442 | let mut prev = points[0]; 443 | for i in 1..n { 444 | let t = i as f32 / (n - 1) as f32; 445 | let next = sample_bezier(points, t); 446 | 447 | let m = t - 0.5 / (n - 1) as f32; 448 | 449 | // Compare absolute error of mid point 450 | let mid_line = ((prev.to_vec2() + next.to_vec2()) * 0.5).to_pos2(); 451 | let mid_curve = sample_bezier(points, m); 452 | 453 | let error_sq = (mid_curve - mid_line).length_sq(); 454 | if error_sq > threshold * threshold { 455 | return false; 456 | } 457 | 458 | // Compare angular error of mid point 459 | let mid_line_dx = next.x - prev.x; 460 | let mid_line_dy = next.y - prev.y; 461 | 462 | let line_w = f32::hypot(mid_line_dx, mid_line_dy); 463 | 464 | let d_curve = sample_bezier(&d, m); 465 | let mid_curve_dx = d_curve.x; 466 | let mid_curve_dy = d_curve.y; 467 | 468 | let curve_w = f32::hypot(mid_curve_dx, mid_curve_dy); 469 | 470 | let error = f32::max( 471 | (mid_curve_dx / curve_w * line_w - mid_line_dx).abs(), 472 | (mid_curve_dy / curve_w * line_w - mid_line_dy).abs(), 473 | ); 474 | if error > threshold * 2.0 { 475 | return false; 476 | } 477 | 478 | prev = next; 479 | } 480 | 481 | true 482 | }) 483 | } 484 | 485 | #[derive(Clone, Copy, PartialEq, Eq, Hash)] 486 | struct WireId { 487 | snarl_id: Id, 488 | wire: Option, 489 | } 490 | 491 | struct WireCache3 { 492 | generation: u32, 493 | frame_size: f32, 494 | from: Pos2, 495 | to: Pos2, 496 | aabb: Rect, 497 | points: [Pos2; 4], 498 | threshold: f32, 499 | line: Vec, 500 | } 501 | 502 | impl Default for WireCache3 { 503 | fn default() -> Self { 504 | Self { 505 | generation: 0, 506 | frame_size: 0.0, 507 | from: Pos2::ZERO, 508 | to: Pos2::ZERO, 509 | aabb: Rect::NOTHING, 510 | points: [Pos2::ZERO; 4], 511 | threshold: 0.0, 512 | line: Vec::new(), 513 | } 514 | } 515 | } 516 | 517 | impl WireCache3 { 518 | fn line(&mut self, threshold: f32) -> Vec { 519 | #[allow(clippy::float_cmp)] 520 | if !self.line.is_empty() && self.threshold == threshold { 521 | return self.line.clone(); 522 | } 523 | 524 | let samples = bezier_draw_samples_number_3(&self.points, threshold); 525 | 526 | let line = (0..samples) 527 | .map(|i| { 528 | #[allow(clippy::cast_precision_loss)] 529 | let t = i as f32 / (samples - 1) as f32; 530 | sample_bezier(&self.points, t) 531 | }) 532 | .collect::>(); 533 | 534 | self.threshold = threshold; 535 | self.line.clone_from(&line); 536 | 537 | line 538 | } 539 | } 540 | 541 | struct WireCache5 { 542 | generation: u32, 543 | frame_size: f32, 544 | from: Pos2, 545 | to: Pos2, 546 | aabb: Rect, 547 | points: [Pos2; 6], 548 | threshold: f32, 549 | line: Vec, 550 | } 551 | 552 | impl Default for WireCache5 { 553 | fn default() -> Self { 554 | Self { 555 | generation: 0, 556 | frame_size: 0.0, 557 | from: Pos2::ZERO, 558 | to: Pos2::ZERO, 559 | aabb: Rect::NOTHING, 560 | points: [Pos2::ZERO; 6], 561 | threshold: 0.0, 562 | line: Vec::new(), 563 | } 564 | } 565 | } 566 | 567 | impl WireCache5 { 568 | fn line(&mut self, threshold: f32) -> Vec { 569 | #[allow(clippy::float_cmp)] 570 | if !self.line.is_empty() && self.threshold == threshold { 571 | return self.line.clone(); 572 | } 573 | 574 | let samples = bezier_draw_samples_number_5(&self.points, threshold); 575 | 576 | let line = (0..samples) 577 | .map(|i| { 578 | #[allow(clippy::cast_precision_loss)] 579 | let t = i as f32 / (samples - 1) as f32; 580 | sample_bezier(&self.points, t) 581 | }) 582 | .collect::>(); 583 | 584 | self.threshold = threshold; 585 | self.line.clone_from(&line); 586 | 587 | line 588 | } 589 | } 590 | 591 | #[derive(Default)] 592 | struct WireCacheAA { 593 | generation: u32, 594 | frame_size: f32, 595 | from: Pos2, 596 | to: Pos2, 597 | corner_radius: f32, 598 | aawire: AxisAlignedWire, 599 | threshold: f32, 600 | line: Vec, 601 | } 602 | 603 | impl WireCacheAA { 604 | fn line(&mut self) -> Vec { 605 | #[allow(clippy::float_cmp)] 606 | if !self.line.is_empty() { 607 | return self.line.clone(); 608 | } 609 | 610 | let mut line = Vec::new(); 611 | 612 | for i in 0..self.aawire.turns { 613 | // shapes.push(Shape::line_segment( 614 | // [wire.segments[i].0, wire.segments[i].1], 615 | // stroke, 616 | // )); 617 | 618 | // Draw segment first 619 | line.push(self.aawire.segments[i].0); 620 | line.push(self.aawire.segments[i].1); 621 | 622 | if self.aawire.turn_radii[i] > 0.0 { 623 | let turn = self.aawire.turn_centers[i]; 624 | let samples = turn_samples_number(self.aawire.turn_radii[i], self.threshold); 625 | 626 | let start = self.aawire.segments[i].1; 627 | let end = self.aawire.segments[i + 1].0; 628 | 629 | let sin_x = end.x - turn.x; 630 | let cos_x = start.x - turn.x; 631 | 632 | let sin_y = end.y - turn.y; 633 | let cos_y = start.y - turn.y; 634 | 635 | for j in 1..samples { 636 | #[allow(clippy::cast_precision_loss)] 637 | let a = std::f32::consts::FRAC_PI_2 * (j as f32 / samples as f32); 638 | 639 | let (sin_a, cos_a) = a.sin_cos(); 640 | 641 | let point: Pos2 = pos2( 642 | turn.x + sin_x * sin_a + cos_x * cos_a, 643 | turn.y + sin_y * sin_a + cos_y * cos_a, 644 | ); 645 | line.push(point); 646 | } 647 | } 648 | } 649 | 650 | line.push(self.aawire.segments[self.aawire.turns].0); 651 | line.push(self.aawire.segments[self.aawire.turns].1); 652 | 653 | self.line.clone_from(&line); 654 | 655 | line 656 | } 657 | } 658 | 659 | #[derive(Default)] 660 | struct WiresCache { 661 | generation: u32, 662 | bezier_3: HashMap, 663 | bezier_5: HashMap, 664 | axis_aligned: HashMap, 665 | } 666 | 667 | impl CacheTrait for WiresCache { 668 | fn update(&mut self) { 669 | self.bezier_3 670 | .retain(|_, cache| cache.generation == self.generation); 671 | self.bezier_5 672 | .retain(|_, cache| cache.generation == self.generation); 673 | self.axis_aligned 674 | .retain(|_, cache| cache.generation == self.generation); 675 | 676 | self.generation = self.generation.wrapping_add(1); 677 | } 678 | 679 | fn len(&self) -> usize { 680 | self.bezier_3.len() + self.bezier_5.len() + self.axis_aligned.len() 681 | } 682 | 683 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any { 684 | self 685 | } 686 | } 687 | 688 | impl WiresCache { 689 | pub fn get_3( 690 | &mut self, 691 | snarl_id: Id, 692 | wire: Option, 693 | frame_size: f32, 694 | from: Pos2, 695 | to: Pos2, 696 | ) -> &mut WireCache3 { 697 | let cached = self.bezier_3.entry(WireId { snarl_id, wire }).or_default(); 698 | 699 | cached.generation = self.generation; 700 | 701 | #[allow(clippy::float_cmp)] 702 | if cached.frame_size == frame_size && cached.from == from && cached.to == to { 703 | return cached; 704 | } 705 | 706 | let points = wire_bezier_3(frame_size, from, to); 707 | let aabb = Rect::from_points(&points); 708 | 709 | cached.frame_size = frame_size; 710 | cached.from = from; 711 | cached.to = to; 712 | cached.points = points; 713 | cached.aabb = aabb; 714 | cached.line.clear(); 715 | 716 | cached 717 | } 718 | 719 | pub fn get_5( 720 | &mut self, 721 | snarl_id: Id, 722 | wire: Option, 723 | frame_size: f32, 724 | from: Pos2, 725 | to: Pos2, 726 | ) -> &mut WireCache5 { 727 | let cached = self.bezier_5.entry(WireId { snarl_id, wire }).or_default(); 728 | 729 | cached.generation = self.generation; 730 | 731 | #[allow(clippy::float_cmp)] 732 | if cached.frame_size == frame_size && cached.from == from && cached.to == to { 733 | return cached; 734 | } 735 | 736 | let points = wire_bezier_5(frame_size, from, to); 737 | let aabb = Rect::from_points(&points); 738 | 739 | cached.frame_size = frame_size; 740 | cached.from = from; 741 | cached.to = to; 742 | cached.points = points; 743 | cached.aabb = aabb; 744 | cached.line.clear(); 745 | 746 | cached 747 | } 748 | 749 | pub fn get_aa( 750 | &mut self, 751 | snarl_id: Id, 752 | wire: Option, 753 | frame_size: f32, 754 | from: Pos2, 755 | to: Pos2, 756 | corner_radius: f32, 757 | threshold: f32, 758 | ) -> &mut WireCacheAA { 759 | let cached = self 760 | .axis_aligned 761 | .entry(WireId { snarl_id, wire }) 762 | .or_default(); 763 | 764 | cached.generation = self.generation; 765 | 766 | #[allow(clippy::float_cmp)] 767 | if cached.frame_size == frame_size 768 | && cached.from == from 769 | && cached.to == to 770 | && cached.corner_radius == corner_radius 771 | && cached.threshold == threshold 772 | { 773 | return cached; 774 | } 775 | 776 | let aawire = wire_axis_aligned(corner_radius, frame_size, from, to, threshold); 777 | 778 | cached.frame_size = frame_size; 779 | cached.from = from; 780 | cached.to = to; 781 | cached.corner_radius = corner_radius; 782 | cached.threshold = threshold; 783 | cached.aawire = aawire; 784 | cached.line.clear(); 785 | 786 | cached 787 | } 788 | } 789 | 790 | #[inline(never)] 791 | fn draw_bezier_3( 792 | ui: &Ui, 793 | snarl_id: Id, 794 | wire: Option, 795 | frame_size: f32, 796 | from: Pos2, 797 | to: Pos2, 798 | stroke: Stroke, 799 | threshold: f32, 800 | shapes: &mut Vec, 801 | ) { 802 | debug_assert!(ui.is_visible(), "Must be checked earlier"); 803 | 804 | let clip_rect = ui.clip_rect(); 805 | 806 | ui.memory_mut(|m| { 807 | let cached = m 808 | .caches 809 | .cache::() 810 | .get_3(snarl_id, wire, frame_size, from, to); 811 | 812 | if cached.aabb.intersects(clip_rect) { 813 | shapes.push(Shape::line(cached.line(threshold), stroke)); 814 | } 815 | }); 816 | 817 | // { 818 | // let samples = bezier_draw_samples_number_3(points, threshold); 819 | // // dbg!(samples, bezier_hit_samples_number(points, threshold)); 820 | // shapes.push(Shape::line( 821 | // points.to_vec(), 822 | // Stroke::new(1.0, Color32::PLACEHOLDER), 823 | // )); 824 | 825 | // let samples = 100; 826 | // shapes.push(Shape::line( 827 | // (0..samples) 828 | // .map(|i| { 829 | // #[allow(clippy::cast_precision_loss)] 830 | // let t = i as f32 / (samples - 1) as f32; 831 | // sample_bezier(points, t) 832 | // }) 833 | // .collect(), 834 | // Stroke::new(1.0, Color32::PLACEHOLDER), 835 | // )); 836 | // } 837 | } 838 | 839 | fn draw_bezier_5( 840 | ui: &Ui, 841 | snarl_id: Id, 842 | wire: Option, 843 | frame_size: f32, 844 | from: Pos2, 845 | to: Pos2, 846 | stroke: Stroke, 847 | threshold: f32, 848 | shapes: &mut Vec, 849 | ) { 850 | debug_assert!(ui.is_visible(), "Must be checked earlier"); 851 | 852 | let clip_rect = ui.clip_rect(); 853 | 854 | ui.memory_mut(|m| { 855 | let cached = m 856 | .caches 857 | .cache::() 858 | .get_5(snarl_id, wire, frame_size, from, to); 859 | 860 | if cached.aabb.intersects(clip_rect) { 861 | shapes.push(Shape::line(cached.line(threshold), stroke)); 862 | } 863 | }); 864 | 865 | // { 866 | // let samples = bezier_draw_samples_number_5(points, threshold); 867 | // // dbg!(samples, bezier_hit_samples_number(points, threshold)); 868 | // shapes.push(Shape::line( 869 | // points.to_vec(), 870 | // Stroke::new(1.0, Color32::PLACEHOLDER), 871 | // )); 872 | 873 | // let samples = 100; 874 | // shapes.push(Shape::line( 875 | // (0..samples) 876 | // .map(|i| { 877 | // #[allow(clippy::cast_precision_loss)] 878 | // let t = i as f32 / (samples - 1) as f32; 879 | // sample_bezier(points, t) 880 | // }) 881 | // .collect(), 882 | // Stroke::new(1.0, Color32::PLACEHOLDER), 883 | // )); 884 | // } 885 | } 886 | 887 | // #[allow(clippy::let_and_return)] 888 | fn sample_bezier(points: &[Pos2], t: f32) -> Pos2 { 889 | match *points { 890 | [] => unimplemented!(), 891 | [p0] => p0, 892 | [p0, p1] => p0.lerp(p1, t), 893 | [p0, p1, p2] => { 894 | let p0_0 = p0; 895 | let p1_0 = p1; 896 | let p2_0 = p2; 897 | 898 | let p0_1 = p0_0.lerp(p1_0, t); 899 | let p1_1 = p1_0.lerp(p2_0, t); 900 | 901 | p0_1.lerp(p1_1, t) 902 | } 903 | [p0, p1, p2, p3] => { 904 | let p0_0 = p0; 905 | let p1_0 = p1; 906 | let p2_0 = p2; 907 | let p3_0 = p3; 908 | 909 | let p0_1 = p0_0.lerp(p1_0, t); 910 | let p1_1 = p1_0.lerp(p2_0, t); 911 | let p2_1 = p2_0.lerp(p3_0, t); 912 | 913 | sample_bezier(&[p0_1, p1_1, p2_1], t) 914 | } 915 | [p0, p1, p2, p3, p4] => { 916 | let p0_0 = p0; 917 | let p1_0 = p1; 918 | let p2_0 = p2; 919 | let p3_0 = p3; 920 | let p4_0 = p4; 921 | 922 | let p0_1 = p0_0.lerp(p1_0, t); 923 | let p1_1 = p1_0.lerp(p2_0, t); 924 | let p2_1 = p2_0.lerp(p3_0, t); 925 | let p3_1 = p3_0.lerp(p4_0, t); 926 | 927 | sample_bezier(&[p0_1, p1_1, p2_1, p3_1], t) 928 | } 929 | [p0, p1, p2, p3, p4, p5] => { 930 | let p0_0 = p0; 931 | let p1_0 = p1; 932 | let p2_0 = p2; 933 | let p3_0 = p3; 934 | let p4_0 = p4; 935 | let p5_0 = p5; 936 | 937 | let p0_1 = p0_0.lerp(p1_0, t); 938 | let p1_1 = p1_0.lerp(p2_0, t); 939 | let p2_1 = p2_0.lerp(p3_0, t); 940 | let p3_1 = p3_0.lerp(p4_0, t); 941 | let p4_1 = p4_0.lerp(p5_0, t); 942 | 943 | sample_bezier(&[p0_1, p1_1, p2_1, p3_1, p4_1], t) 944 | } 945 | _ => unimplemented!(), 946 | } 947 | } 948 | 949 | fn split_bezier_3(points: &[Pos2; 4], t: f32) -> [[Pos2; 4]; 2] { 950 | let [p0, p1, p2, p3] = *points; 951 | 952 | let p0_0 = p0; 953 | let p1_0 = p1; 954 | let p2_0 = p2; 955 | let p3_0 = p3; 956 | 957 | let p0_1 = p0_0.lerp(p1_0, t); 958 | let p1_1 = p1_0.lerp(p2_0, t); 959 | let p2_1 = p2_0.lerp(p3_0, t); 960 | 961 | let p0_2 = p0_1.lerp(p1_1, t); 962 | let p1_2 = p1_1.lerp(p2_1, t); 963 | 964 | let p0_3 = p0_2.lerp(p1_2, t); 965 | 966 | [[p0_0, p0_1, p0_2, p0_3], [p0_3, p1_2, p2_1, p3_0]] 967 | } 968 | 969 | fn hit_wire_bezier_3( 970 | ctx: &Context, 971 | snarl_id: Id, 972 | wire: Wire, 973 | frame_size: f32, 974 | from: Pos2, 975 | to: Pos2, 976 | pos: Pos2, 977 | hit_threshold: f32, 978 | ) -> bool { 979 | let (aabb, points) = ctx.memory_mut(|m| { 980 | let cache = 981 | m.caches 982 | .cache::() 983 | .get_3(snarl_id, Some(wire), frame_size, from, to); 984 | 985 | (cache.aabb, cache.points) 986 | }); 987 | 988 | let aabb_e = aabb.expand(hit_threshold); 989 | if !aabb_e.contains(pos) { 990 | return false; 991 | } 992 | 993 | hit_bezier_3(&points, pos, hit_threshold) 994 | } 995 | 996 | fn hit_bezier_3(points: &[Pos2; 4], pos: Pos2, hit_threshold: f32) -> bool { 997 | let samples = bezier_hit_samples_number(points, hit_threshold); 998 | if samples > 8 { 999 | let [points1, points2] = split_bezier_3(&points, 0.5); 1000 | 1001 | let aabb_e = Rect::from_points(&points1).expand(hit_threshold); 1002 | if aabb_e.contains(pos) && hit_bezier_3(&points1, pos, hit_threshold) { 1003 | return true; 1004 | } 1005 | let aabb_e = Rect::from_points(&points2).expand(hit_threshold); 1006 | if aabb_e.contains(pos) && hit_bezier_3(&points2, pos, hit_threshold) { 1007 | return true; 1008 | } 1009 | return false; 1010 | } 1011 | 1012 | let threshold_sq = hit_threshold * hit_threshold; 1013 | 1014 | for i in 0..samples { 1015 | #[allow(clippy::cast_precision_loss)] 1016 | let t = i as f32 / (samples - 1) as f32; 1017 | let p = sample_bezier(points, t); 1018 | if p.distance_sq(pos) <= threshold_sq { 1019 | return true; 1020 | } 1021 | } 1022 | 1023 | false 1024 | } 1025 | 1026 | fn split_bezier_5(points: &[Pos2; 6], t: f32) -> [[Pos2; 6]; 2] { 1027 | let [p0, p1, p2, p3, p4, p5] = *points; 1028 | 1029 | let p0_0 = p0; 1030 | let p1_0 = p1; 1031 | let p2_0 = p2; 1032 | let p3_0 = p3; 1033 | let p4_0 = p4; 1034 | let p5_0 = p5; 1035 | 1036 | let p0_1 = p0_0.lerp(p1_0, t); 1037 | let p1_1 = p1_0.lerp(p2_0, t); 1038 | let p2_1 = p2_0.lerp(p3_0, t); 1039 | let p3_1 = p3_0.lerp(p4_0, t); 1040 | let p4_1 = p4_0.lerp(p5_0, t); 1041 | 1042 | let p0_2 = p0_1.lerp(p1_1, t); 1043 | let p1_2 = p1_1.lerp(p2_1, t); 1044 | let p2_2 = p2_1.lerp(p3_1, t); 1045 | let p3_2 = p3_1.lerp(p4_1, t); 1046 | 1047 | let p0_3 = p0_2.lerp(p1_2, t); 1048 | let p1_3 = p1_2.lerp(p2_2, t); 1049 | let p2_3 = p2_2.lerp(p3_2, t); 1050 | 1051 | let p0_4 = p0_3.lerp(p1_3, t); 1052 | let p1_4 = p1_3.lerp(p2_3, t); 1053 | 1054 | let p0_5 = p0_4.lerp(p1_4, t); 1055 | 1056 | [ 1057 | [p0_0, p0_1, p0_2, p0_3, p0_4, p0_5], 1058 | [p0_5, p1_4, p2_3, p3_2, p4_1, p5_0], 1059 | ] 1060 | } 1061 | 1062 | fn hit_wire_bezier_5( 1063 | ctx: &Context, 1064 | snarl_id: Id, 1065 | wire: Wire, 1066 | frame_size: f32, 1067 | from: Pos2, 1068 | to: Pos2, 1069 | pos: Pos2, 1070 | hit_threshold: f32, 1071 | ) -> bool { 1072 | let (aabb, points) = ctx.memory_mut(|m| { 1073 | let cache = 1074 | m.caches 1075 | .cache::() 1076 | .get_5(snarl_id, Some(wire), frame_size, from, to); 1077 | 1078 | (cache.aabb, cache.points) 1079 | }); 1080 | 1081 | let aabb_e = aabb.expand(hit_threshold); 1082 | if !aabb_e.contains(pos) { 1083 | return false; 1084 | } 1085 | 1086 | hit_bezier_5(&points, pos, hit_threshold) 1087 | } 1088 | 1089 | fn hit_bezier_5(points: &[Pos2; 6], pos: Pos2, hit_threshold: f32) -> bool { 1090 | let samples = bezier_hit_samples_number(points, hit_threshold); 1091 | if samples > 16 { 1092 | let [points1, points2] = split_bezier_5(points, 0.5); 1093 | let aabb_e = Rect::from_points(&points1).expand(hit_threshold); 1094 | if aabb_e.contains(pos) && hit_bezier_5(&points1, pos, hit_threshold) { 1095 | return true; 1096 | } 1097 | let aabb_e = Rect::from_points(&points2).expand(hit_threshold); 1098 | if aabb_e.contains(pos) && hit_bezier_5(&points2, pos, hit_threshold) { 1099 | return true; 1100 | } 1101 | return false; 1102 | } 1103 | 1104 | let threshold_sq = hit_threshold * hit_threshold; 1105 | 1106 | for i in 0..samples { 1107 | #[allow(clippy::cast_precision_loss)] 1108 | let t = i as f32 / (samples - 1) as f32; 1109 | let p = sample_bezier(points, t); 1110 | 1111 | if p.distance_sq(pos) <= threshold_sq { 1112 | return true; 1113 | } 1114 | } 1115 | 1116 | false 1117 | } 1118 | 1119 | #[derive(Clone, Copy, PartialEq)] 1120 | struct AxisAlignedWire { 1121 | aabb: Rect, 1122 | turns: usize, 1123 | segments: [(Pos2, Pos2); 5], 1124 | turn_centers: [Pos2; 4], 1125 | turn_radii: [f32; 4], 1126 | } 1127 | 1128 | impl Default for AxisAlignedWire { 1129 | #[inline] 1130 | fn default() -> Self { 1131 | Self { 1132 | aabb: Rect::NOTHING, 1133 | turns: 0, 1134 | segments: [(Pos2::ZERO, Pos2::ZERO); 5], 1135 | turn_centers: [Pos2::ZERO; 4], 1136 | turn_radii: [0.0; 4], 1137 | } 1138 | } 1139 | } 1140 | 1141 | #[allow(clippy::too_many_lines)] 1142 | fn wire_axis_aligned( 1143 | corner_radius: f32, 1144 | frame_size: f32, 1145 | from: Pos2, 1146 | to: Pos2, 1147 | threshold: f32, 1148 | ) -> AxisAlignedWire { 1149 | let corner_radius = corner_radius.max(0.0); 1150 | 1151 | let half_height = f32::abs(from.y - to.y) / 2.0; 1152 | let max_radius = (half_height / 2.0).min(corner_radius); 1153 | 1154 | let frame_size = frame_size.max(max_radius * 2.0); 1155 | 1156 | let zero_segment = (Pos2::ZERO, Pos2::ZERO); 1157 | 1158 | if from.x + frame_size <= to.x - frame_size { 1159 | if f32::abs(from.y - to.y) < threshold { 1160 | // Single segment case. 1161 | AxisAlignedWire { 1162 | aabb: Rect::from_two_pos(from, to), 1163 | segments: [ 1164 | (from, to), 1165 | zero_segment, 1166 | zero_segment, 1167 | zero_segment, 1168 | zero_segment, 1169 | ], 1170 | turns: 0, 1171 | turn_centers: [Pos2::ZERO; 4], 1172 | turn_radii: [f32::NAN; 4], 1173 | } 1174 | } else { 1175 | // Two turns case. 1176 | let mid_x = (from.x + to.x) / 2.0; 1177 | let half_width = (to.x - from.x) / 2.0; 1178 | 1179 | let turn_radius = max_radius.min(half_width); 1180 | 1181 | let turn_vert_len = if from.y < to.y { 1182 | turn_radius 1183 | } else { 1184 | -turn_radius 1185 | }; 1186 | 1187 | let segments = [ 1188 | (from, pos2(mid_x - turn_radius, from.y)), 1189 | ( 1190 | pos2(mid_x, from.y + turn_vert_len), 1191 | pos2(mid_x, to.y - turn_vert_len), 1192 | ), 1193 | (pos2(mid_x + turn_radius, to.y), to), 1194 | zero_segment, 1195 | zero_segment, 1196 | ]; 1197 | 1198 | let turn_centers = [ 1199 | pos2(mid_x - turn_radius, from.y + turn_vert_len), 1200 | pos2(mid_x + turn_radius, to.y - turn_vert_len), 1201 | Pos2::ZERO, 1202 | Pos2::ZERO, 1203 | ]; 1204 | 1205 | let turn_radii = [turn_radius, turn_radius, f32::NAN, f32::NAN]; 1206 | 1207 | AxisAlignedWire { 1208 | aabb: Rect::from_two_pos(from, to), 1209 | turns: 2, 1210 | segments, 1211 | turn_centers, 1212 | turn_radii, 1213 | } 1214 | } 1215 | } else { 1216 | // Four turns case. 1217 | let mid = (from.y + to.y) / 2.0; 1218 | 1219 | let right = from.x + frame_size; 1220 | let left = to.x - frame_size; 1221 | 1222 | let half_width = f32::abs(right - left) / 2.0; 1223 | 1224 | let ends_turn_radius = max_radius; 1225 | let middle_turn_radius = max_radius.min(half_width); 1226 | 1227 | let ends_turn_vert_len = if from.y < to.y { 1228 | ends_turn_radius 1229 | } else { 1230 | -ends_turn_radius 1231 | }; 1232 | 1233 | let middle_turn_vert_len = if from.y < to.y { 1234 | middle_turn_radius 1235 | } else { 1236 | -middle_turn_radius 1237 | }; 1238 | 1239 | let segments = [ 1240 | (from, pos2(right - ends_turn_radius, from.y)), 1241 | ( 1242 | pos2(right, from.y + ends_turn_vert_len), 1243 | pos2(right, mid - middle_turn_vert_len), 1244 | ), 1245 | ( 1246 | pos2(right - middle_turn_radius, mid), 1247 | pos2(left + middle_turn_radius, mid), 1248 | ), 1249 | ( 1250 | pos2(left, mid + middle_turn_vert_len), 1251 | pos2(left, to.y - ends_turn_vert_len), 1252 | ), 1253 | (pos2(left + ends_turn_radius, to.y), to), 1254 | ]; 1255 | 1256 | let turn_centers = [ 1257 | pos2(right - ends_turn_radius, from.y + ends_turn_vert_len), 1258 | pos2(right - middle_turn_radius, mid - middle_turn_vert_len), 1259 | pos2(left + middle_turn_radius, mid + middle_turn_vert_len), 1260 | pos2(left + ends_turn_radius, to.y - ends_turn_vert_len), 1261 | ]; 1262 | 1263 | let turn_radii = [ 1264 | ends_turn_radius, 1265 | middle_turn_radius, 1266 | middle_turn_radius, 1267 | ends_turn_radius, 1268 | ]; 1269 | 1270 | AxisAlignedWire { 1271 | aabb: Rect::from_min_max( 1272 | pos2(f32::min(left, from.x), f32::min(from.y, to.y)), 1273 | pos2(f32::max(right, to.x), f32::max(from.y, to.y)), 1274 | ), 1275 | turns: 4, 1276 | segments, 1277 | turn_centers, 1278 | turn_radii, 1279 | } 1280 | } 1281 | } 1282 | 1283 | fn hit_wire_axis_aligned( 1284 | ctx: &Context, 1285 | snarl_id: Id, 1286 | wire: Wire, 1287 | corner_radius: f32, 1288 | frame_size: f32, 1289 | from: Pos2, 1290 | to: Pos2, 1291 | pos: Pos2, 1292 | threshold: f32, 1293 | hit_threshold: f32, 1294 | ) -> bool { 1295 | let aawire = ctx.memory_mut(|m| { 1296 | let cache = m.caches.cache::().get_aa( 1297 | snarl_id, 1298 | Some(wire), 1299 | frame_size, 1300 | from, 1301 | to, 1302 | corner_radius, 1303 | threshold, 1304 | ); 1305 | 1306 | cache.aawire 1307 | }); 1308 | 1309 | // Check AABB first 1310 | if !aawire.aabb.expand(hit_threshold).contains(pos) { 1311 | return false; 1312 | } 1313 | 1314 | // Check all straight segments first 1315 | // Number of segments is number of turns + 1 1316 | for i in 0..aawire.turns + 1 { 1317 | let (start, end) = aawire.segments[i]; 1318 | 1319 | // Segments are always axis aligned 1320 | // So we can use AABB for checking 1321 | if Rect::from_two_pos(start, end) 1322 | .expand(hit_threshold) 1323 | .contains(pos) 1324 | { 1325 | return true; 1326 | } 1327 | } 1328 | 1329 | // Check all turns 1330 | for i in 0..aawire.turns { 1331 | if aawire.turn_radii[i] > 0.0 { 1332 | let turn = aawire.turn_centers[i]; 1333 | let turn_aabb = Rect::from_two_pos(aawire.segments[i].1, aawire.segments[i + 1].0); 1334 | if !turn_aabb.contains(pos) { 1335 | continue; 1336 | } 1337 | 1338 | // Avoid sqrt 1339 | let dist2 = (turn - pos).length_sq(); 1340 | let min = aawire.turn_radii[i] - hit_threshold; 1341 | let max = aawire.turn_radii[i] + hit_threshold; 1342 | 1343 | if dist2 <= max * max && dist2 >= min * min { 1344 | return true; 1345 | } 1346 | } 1347 | } 1348 | 1349 | false 1350 | } 1351 | 1352 | fn turn_samples_number(radius: f32, threshold: f32) -> usize { 1353 | #![allow(clippy::cast_sign_loss)] 1354 | #![allow(clippy::cast_possible_truncation)] 1355 | 1356 | if threshold / radius >= 1.0 { 1357 | return 2; 1358 | } 1359 | 1360 | let a: f32 = (1.0 - threshold / radius).acos(); 1361 | let samples = (std::f32::consts::PI / (4.0 * a) + 1.0) 1362 | .min(MAX_CURVE_SAMPLES as f32) 1363 | .ceil() as usize; 1364 | samples.max(2).min(MAX_CURVE_SAMPLES) 1365 | } 1366 | 1367 | #[allow(clippy::too_many_arguments)] 1368 | fn draw_axis_aligned( 1369 | ui: &Ui, 1370 | snarl_id: Id, 1371 | wire: Option, 1372 | corner_radius: f32, 1373 | frame_size: f32, 1374 | from: Pos2, 1375 | to: Pos2, 1376 | stroke: Stroke, 1377 | threshold: f32, 1378 | shapes: &mut Vec, 1379 | ) { 1380 | debug_assert!(ui.is_visible(), "Must be checked earlier"); 1381 | 1382 | let clip_rect = ui.clip_rect(); 1383 | ui.memory_mut(|m| { 1384 | let cached = m.caches.cache::().get_aa( 1385 | snarl_id, 1386 | wire, 1387 | frame_size, 1388 | from, 1389 | to, 1390 | corner_radius, 1391 | threshold, 1392 | ); 1393 | 1394 | if cached.aawire.aabb.intersects(clip_rect) { 1395 | shapes.push(Shape::line(cached.line(), stroke)); 1396 | } 1397 | }); 1398 | } 1399 | 1400 | /// Very basic lower-bound algorithm 1401 | /// Finds the smallest number in range [min, max) that satisfies the predicate 1402 | /// If no such number exists, returns max 1403 | /// 1404 | /// For the algorithm to work, the predicate must be monotonic 1405 | /// i.e. if f(i) is true, then f(j) is true for all j within (i, max) 1406 | /// and if f(i) is false, then f(j) is false for all j within [min, i) 1407 | fn lower_bound(min: usize, max: usize, f: impl Fn(usize) -> bool) -> usize { 1408 | #![allow(clippy::similar_names)] 1409 | 1410 | let mut min = min; 1411 | let mut max = max; 1412 | 1413 | while min < max { 1414 | let mid = (min + max) / 2; 1415 | if f(mid) { 1416 | max = mid; 1417 | } else { 1418 | min = mid + 1; 1419 | } 1420 | } 1421 | 1422 | max 1423 | 1424 | // for i in min..max { 1425 | // if f(i) { 1426 | // return i; 1427 | // } 1428 | // } 1429 | // max 1430 | } 1431 | --------------------------------------------------------------------------------