├── .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 | [](https://docs.rs/egui-snarl/)
4 | [](https://crates.io/crates/egui-snarl)
5 | [](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.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 |
--------------------------------------------------------------------------------