Merge branch 'master' into refit_on_delete

This commit is contained in:
Adesh Gupta
2025-06-27 19:43:40 +05:30
committed by GitHub
148 changed files with 4497 additions and 5666 deletions
-2
View File
@@ -5,8 +5,6 @@ on:
branches: branches:
- master - master
pull_request: pull_request:
branches:
- master
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script> INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script>
@@ -2,7 +2,6 @@ name: Clippy Check
on: on:
pull_request: pull_request:
branches: [master]
types: [opened, reopened, synchronize, ready_for_review] types: [opened, reopened, synchronize, ready_for_review]
jobs: jobs:
@@ -2,7 +2,6 @@ name: Profiling Changes
on: on:
pull_request: pull_request:
branches: [master]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
-2
View File
@@ -7,8 +7,6 @@ on:
paths: paths:
- "libraries/rawkit/**" - "libraries/rawkit/**"
pull_request: pull_request:
branches:
- master
paths: paths:
- "libraries/rawkit/**" - "libraries/rawkit/**"
-2
View File
@@ -7,8 +7,6 @@ on:
paths: paths:
- website/** - website/**
pull_request: pull_request:
branches:
- master
paths: paths:
- website/** - website/**
env: env:
Generated
+48 -6
View File
@@ -2132,6 +2132,8 @@ dependencies = [
"graph-craft", "graph-craft",
"graphene-application-io", "graphene-application-io",
"graphene-core", "graphene-core",
"graphene-path-bool",
"graphene-svg-renderer",
"iai-callgrind", "iai-callgrind",
"js-sys", "js-sys",
"log", "log",
@@ -2175,6 +2177,7 @@ dependencies = [
"graphene-std", "graphene-std",
"interpreted-executor", "interpreted-executor",
"log", "log",
"preprocessor",
"tokio", "tokio",
"wgpu", "wgpu",
"wgpu-executor", "wgpu-executor",
@@ -2208,17 +2211,29 @@ dependencies = [
"specta", "specta",
"tinyvec", "tinyvec",
"tokio", "tokio",
"usvg",
"vello",
"wgpu", "wgpu",
] ]
[[package]]
name = "graphene-path-bool"
version = "0.1.0"
dependencies = [
"bezier-rs",
"dyn-any",
"glam",
"graphene-core",
"log",
"node-macro",
"path-bool",
"serde",
"specta",
]
[[package]] [[package]]
name = "graphene-std" name = "graphene-std"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bezier-rs",
"bytemuck", "bytemuck",
"dyn-any", "dyn-any",
"fastnoise-lite", "fastnoise-lite",
@@ -2227,16 +2242,16 @@ dependencies = [
"graph-craft", "graph-craft",
"graphene-application-io", "graphene-application-io",
"graphene-core", "graphene-core",
"graphene-path-bool",
"graphene-svg-renderer",
"image", "image",
"log", "log",
"ndarray", "ndarray",
"node-macro", "node-macro",
"path-bool",
"rand 0.9.0", "rand 0.9.0",
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"reqwest", "reqwest",
"tokio", "tokio",
"usvg",
"vello", "vello",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
@@ -2244,6 +2259,22 @@ dependencies = [
"wgpu-executor", "wgpu-executor",
] ]
[[package]]
name = "graphene-svg-renderer"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"bezier-rs",
"dyn-any",
"glam",
"graphene-core",
"log",
"num-traits",
"serde",
"usvg",
"vello",
]
[[package]] [[package]]
name = "graphite-desktop" name = "graphite-desktop"
version = "0.1.0" version = "0.1.0"
@@ -2282,6 +2313,7 @@ dependencies = [
"log", "log",
"num_enum", "num_enum",
"once_cell", "once_cell",
"preprocessor",
"ron", "ron",
"serde", "serde",
"serde_json", "serde_json",
@@ -2311,7 +2343,6 @@ dependencies = [
name = "graphite-wasm" name = "graphite-wasm"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"glam",
"graph-craft", "graph-craft",
"graphene-std", "graphene-std",
"graphite-editor", "graphite-editor",
@@ -2946,6 +2977,7 @@ dependencies = [
"glam", "glam",
"graph-craft", "graph-craft",
"graphene-core", "graphene-core",
"graphene-path-bool",
"graphene-std", "graphene-std",
"log", "log",
"once_cell", "once_cell",
@@ -4475,6 +4507,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "preprocessor"
version = "0.1.0"
dependencies = [
"graph-craft",
"graphene-std",
"interpreted-executor",
]
[[package]] [[package]]
name = "presser" name = "presser"
version = "0.3.1" version = "0.3.1"
@@ -7402,6 +7443,7 @@ dependencies = [
"glam", "glam",
"graphene-application-io", "graphene-application-io",
"graphene-core", "graphene-core",
"graphene-svg-renderer",
"node-macro", "node-macro",
"vello", "vello",
"web-sys", "web-sys",
+8 -1
View File
@@ -7,11 +7,13 @@ members = [
"node-graph/gapplication-io", "node-graph/gapplication-io",
"node-graph/gcore", "node-graph/gcore",
"node-graph/gstd", "node-graph/gstd",
"node-graph/gpath-bool",
"node-graph/graph-craft", "node-graph/graph-craft",
"node-graph/graphene-cli", "node-graph/graphene-cli",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor", "node-graph/interpreted-executor",
"node-graph/node-macro", "node-graph/node-macro",
"node-graph/wgpu-executor", "node-graph/preprocessor",
"libraries/dyn-any", "libraries/dyn-any",
"libraries/path-bool", "libraries/path-bool",
"libraries/bezier-rs", "libraries/bezier-rs",
@@ -23,8 +25,10 @@ default-members = [
"frontend/wasm", "frontend/wasm",
"node-graph/gcore", "node-graph/gcore",
"node-graph/gstd", "node-graph/gstd",
"node-graph/gpath-bool",
"node-graph/graph-craft", "node-graph/graph-craft",
"node-graph/graphene-cli", "node-graph/graphene-cli",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor", "node-graph/interpreted-executor",
"node-graph/node-macro", "node-graph/node-macro",
] ]
@@ -34,12 +38,15 @@ resolver = "2"
# Local dependencies # Local dependencies
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] } bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] }
dyn-any = { path = "libraries/dyn-any", features = ["derive", "glam", "reqwest", "log-bad-types", "rc"] } dyn-any = { path = "libraries/dyn-any", features = ["derive", "glam", "reqwest", "log-bad-types", "rc"] }
preprocessor = { path = "node-graph/preprocessor"}
math-parser = { path = "libraries/math-parser" } math-parser = { path = "libraries/math-parser" }
path-bool = { path = "libraries/path-bool" } path-bool = { path = "libraries/path-bool" }
graphene-application-io = { path = "node-graph/gapplication-io" } graphene-application-io = { path = "node-graph/gapplication-io" }
graphene-core = { path = "node-graph/gcore" } graphene-core = { path = "node-graph/gcore" }
graphene-path-bool = { path = "node-graph/gpath-bool" }
graph-craft = { path = "node-graph/graph-craft" } graph-craft = { path = "node-graph/graph-craft" }
graphene-std = { path = "node-graph/gstd" } graphene-std = { path = "node-graph/gstd" }
graphene-svg-renderer = { path = "node-graph/gsvg-renderer" }
interpreted-executor = { path = "node-graph/interpreted-executor" } interpreted-executor = { path = "node-graph/interpreted-executor" }
node-macro = { path = "node-graph/node-macro" } node-macro = { path = "node-graph/node-macro" }
wgpu-executor = { path = "node-graph/wgpu-executor" } wgpu-executor = { path = "node-graph/wgpu-executor" }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -4
View File
@@ -13,10 +13,7 @@ license = "Apache-2.0"
[features] [features]
default = ["wasm"] default = ["wasm"]
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"] wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
gpu = [ gpu = ["interpreted-executor/gpu", "wgpu-executor"]
"interpreted-executor/gpu",
"wgpu-executor",
]
tauri = ["ron", "decouple-execution"] tauri = ["ron", "decouple-execution"]
decouple-execution = [] decouple-execution = []
resvg = ["graphene-std/resvg"] resvg = ["graphene-std/resvg"]
@@ -29,6 +26,7 @@ graphite-proc-macros = { workspace = true }
graph-craft = { workspace = true } graph-craft = { workspace = true }
interpreted-executor = { workspace = true } interpreted-executor = { workspace = true }
graphene-std = { workspace = true } graphene-std = { workspace = true }
preprocessor = { workspace = true }
# Workspace dependencies # Workspace dependencies
js-sys = { workspace = true } js-sys = { workspace = true }
-50
View File
@@ -55,53 +55,3 @@ pub fn commit_info_localized(localized_commit_date: &str) -> String {
localized_commit_date localized_commit_date
) )
} }
// #[cfg(test)]
// mod test {
// use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds;
// use crate::messages::prelude::*;
// // TODO: Fix and reenable
// #[ignore]
// #[test]
// fn debug_ub() {
// use super::Message;
// let mut editor = super::Editor::new();
// let mut responses = Vec::new();
// let messages: Vec<Message> = vec![
// Message::Init,
// Message::Preferences(PreferencesMessage::Load {
// preferences: r#"{ "imaginate_server_hostname": "http://localhost:7860/", "imaginate_refresh_frequency": 1, "zoom_with_scroll": false }"#.to_string(),
// }),
// PortfolioMessage::OpenDocumentFileWithId {
// document_id: DocumentId(0),
// document_name: "".into(),
// document_is_auto_saved: true,
// document_is_saved: true,
// document_serialized_content: r#" [removed until test is reenabled] "#.into(),
// to_front: false,
// }
// .into(),
// InputPreprocessorMessage::BoundsOfViewports {
// bounds_of_viewports: vec![ViewportBounds::from_slice(&[0., 0., 1920., 1080.])],
// }
// .into(),
// ];
// use futures::executor::block_on;
// for message in messages {
// block_on(crate::node_graph_executor::run_node_graph());
// let mut res = VecDeque::new();
// editor.poll_node_graph_evaluation(&mut res).expect("poll_node_graph_evaluation failed");
// let res = editor.handle_message(message);
// responses.push(res);
// }
// let responses = responses.pop().unwrap();
// // let trigger_message = responses[responses.len() - 2].clone();
// println!("responses: {responses:#?}");
// }
// }
+2 -2
View File
@@ -122,8 +122,8 @@ pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
// GIZMOS // GIZMOS
pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.; pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9; pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
pub const NUMBER_OF_POINTS_HANDLE_SPOKE_EXTENSION: f64 = 1.2; pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2;
pub const NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH: f64 = 10.; pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.;
pub const GIZMO_HIDE_THRESHOLD: f64 = 20.; pub const GIZMO_HIDE_THRESHOLD: f64 = 20.;
// SCROLLBARS // SCROLLBARS
@@ -204,27 +204,6 @@ impl PreferencesDialogMessageHandler {
.widget_holder(), .widget_holder(),
]; ];
// TODO: Reenable when Imaginate is restored
// let imaginate_server_hostname = vec![
// TextLabel::new("Imaginate").min_width(60).italic(true).widget_holder(),
// TextLabel::new("Server Hostname").table_align(true).widget_holder(),
// TextInput::new(&preferences.imaginate_server_hostname)
// .min_width(200)
// .on_update(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into())
// .widget_holder(),
// ];
// let imaginate_refresh_frequency = vec![
// TextLabel::new("").min_width(60).widget_holder(),
// TextLabel::new("Refresh Frequency").table_align(true).widget_holder(),
// NumberInput::new(Some(preferences.imaginate_refresh_frequency))
// .unit(" seconds")
// .min(0.)
// .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
// .min_width(200)
// .on_update(|number_input: &NumberInput| PreferencesMessage::ImaginateRefreshFrequency { seconds: number_input.value.unwrap() }.into())
// .widget_holder(),
// ];
Layout::WidgetLayout(WidgetLayout::new(vec![ Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row { widgets: navigation_header }, LayoutGroup::Row { widgets: navigation_header },
LayoutGroup::Row { widgets: zoom_rate_label }, LayoutGroup::Row { widgets: zoom_rate_label },
@@ -238,8 +217,6 @@ impl PreferencesDialogMessageHandler {
LayoutGroup::Row { widgets: graph_wire_style }, LayoutGroup::Row { widgets: graph_wire_style },
LayoutGroup::Row { widgets: use_vello }, LayoutGroup::Row { widgets: use_vello },
LayoutGroup::Row { widgets: vector_meshes }, LayoutGroup::Row { widgets: vector_meshes },
// LayoutGroup::Row { widgets: imaginate_server_hostname },
// LayoutGroup::Row { widgets: imaginate_refresh_frequency },
])) ]))
} }
@@ -99,19 +99,6 @@ pub enum FrontendMessage {
#[serde(rename = "copyText")] #[serde(rename = "copyText")]
copy_text: String, copy_text: String,
}, },
// TODO: Eventually remove this document upgrade code
TriggerUpgradeDocumentToVectorManipulationFormat {
#[serde(rename = "documentId")]
document_id: DocumentId,
#[serde(rename = "documentName")]
document_name: String,
#[serde(rename = "documentIsAutoSaved")]
document_is_auto_saved: bool,
#[serde(rename = "documentIsSaved")]
document_is_saved: bool,
#[serde(rename = "documentSerializedContent")]
document_serialized_content: String,
},
TriggerVisitLink { TriggerVisitLink {
url: String, url: String,
}, },
@@ -206,13 +206,6 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(ArrowUp); action_dispatch=ShapeToolMessage::IncreaseSides), entry!(KeyDown(ArrowUp); action_dispatch=ShapeToolMessage::IncreaseSides),
entry!(KeyDown(ArrowDown); action_dispatch=ShapeToolMessage::DecreaseSides), entry!(KeyDown(ArrowDown); action_dispatch=ShapeToolMessage::DecreaseSides),
// //
// ImaginateToolMessage
// entry!(KeyDown(MouseLeft); action_dispatch=ImaginateToolMessage::DragStart),
// entry!(KeyUp(MouseLeft); action_dispatch=ImaginateToolMessage::DragStop),
// entry!(KeyDown(MouseRight); action_dispatch=ImaginateToolMessage::Abort),
// entry!(KeyDown(Escape); action_dispatch=ImaginateToolMessage::Abort),
// entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=ImaginateToolMessage::Resize { center: Alt, lock_ratio: Shift }),
//
// PathToolMessage // PathToolMessage
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
@@ -11,8 +11,8 @@ use graph_craft::document::NodeId;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::raster::Image; use graphene_std::raster::Image;
use graphene_std::renderer::ClickTarget;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::ClickTarget;
use graphene_std::vector::style::ViewMode; use graphene_std::vector::style::ViewMode;
#[impl_message(Message, PortfolioMessage, Document)] #[impl_message(Message, PortfolioMessage, Document)]
@@ -73,13 +73,6 @@ pub enum DocumentMessage {
GroupSelectedLayers { GroupSelectedLayers {
group_folder_type: GroupFolderType, group_folder_type: GroupFolderType,
}, },
// ImaginateGenerate {
// imaginate_node: Vec<NodeId>,
// },
// ImaginateRandom {
// imaginate_node: Vec<NodeId>,
// then_generate: bool,
// },
MoveSelectedLayersTo { MoveSelectedLayersTo {
parent: LayerNodeIdentifier, parent: LayerNodeIdentifier,
insert_index: usize, insert_index: usize,
@@ -29,11 +29,13 @@ use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_std::math::quad::Quad;
use graphene_std::path_bool::{boolean_intersect, path_bool_lib};
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{Raster, RasterDataTable}; use graphene_std::raster_types::{Raster, RasterDataTable};
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad}; use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::style::ViewMode; use graphene_std::vector::style::ViewMode;
use graphene_std::vector::{PointId, path_bool_lib};
use std::time::Duration; use std::time::Duration;
pub struct DocumentMessageData<'a> { pub struct DocumentMessageData<'a> {
@@ -613,37 +615,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_folders }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_folders });
} }
} }
// DocumentMessage::ImaginateGenerate { imaginate_node } => {
// let random_value = generate_uuid();
// responses.add(NodeGraphMessage::SetInputValue {
// node_id: *imaginate_node.last().unwrap(),
// // Needs to match the index of the seed parameter in `pub const IMAGINATE_NODE: DocumentNodeDefinition` in `document_node_type.rs`
// input_index: 17,
// value: graph_craft::document::value::TaggedValue::U64(random_value),
// });
// responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
// }
// DocumentMessage::ImaginateRandom { imaginate_node, then_generate } => {
// // Generate a random seed. We only want values between -2^53 and 2^53, because integer values
// // outside of this range can get rounded in f64
// let random_bits = generate_uuid();
// let random_value = ((random_bits >> 11) as f64).copysign(f64::from_bits(random_bits & (1 << 63)));
// responses.add(DocumentMessage::AddTransaction);
// // Set a random seed input
// responses.add(NodeGraphMessage::SetInputValue {
// node_id: *imaginate_node.last().unwrap(),
// // Needs to match the index of the seed parameter in `pub const IMAGINATE_NODE: DocumentNodeDefinition` in `document_node_type.rs`
// input_index: 3,
// value: graph_craft::document::value::TaggedValue::F64(random_value),
// });
// // Generate the image
// if then_generate {
// responses.add(DocumentMessage::ImaginateGenerate { imaginate_node });
// }
// }
DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => { DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => {
if !self.selection_network_path.is_empty() { if !self.selection_network_path.is_empty() {
log::error!("Moving selected layers is only supported for the Document Network"); log::error!("Moving selected layers is only supported for the Document Network");
@@ -1655,7 +1626,7 @@ impl DocumentMessageHandler {
subpath.is_inside_subpath(&viewport_polygon, None, None) subpath.is_inside_subpath(&viewport_polygon, None, None)
} }
ClickTargetType::FreePoint(point) => { ClickTargetType::FreePoint(point) => {
let mut point = point.clone(); let mut point = *point;
point.apply_transform(layer_transform); point.apply_transform(layer_transform);
viewport_polygon.contains_point(point.position) viewport_polygon.contains_point(point.position)
} }
@@ -2992,7 +2963,7 @@ impl<'a> ClickXRayIter<'a> {
// We do this on this using the target area to reduce computation (as the target area is usually very simple). // We do this on this using the target area to reduce computation (as the target area is usually very simple).
if clip && intersects { if clip && intersects {
let clip_path = click_targets_to_path_lib_segments(click_targets.iter().flat_map(|x| x.iter()), transform); let clip_path = click_targets_to_path_lib_segments(click_targets.iter().flat_map(|x| x.iter()), transform);
let subtracted = graphene_std::vector::boolean_intersect(path, clip_path).into_iter().flatten().collect::<Vec<_>>(); let subtracted = boolean_intersect(path, clip_path).into_iter().flatten().collect::<Vec<_>>();
if subtracted.is_empty() { if subtracted.is_empty() {
use_children = false; use_children = false;
} else { } else {
@@ -3377,9 +3348,9 @@ mod document_message_handler_tests {
let rect_bbox_after = document.metadata().bounding_box_viewport(rect_layer).unwrap(); let rect_bbox_after = document.metadata().bounding_box_viewport(rect_layer).unwrap();
// Verifing the rectangle maintains approximately the same position in viewport space // Verifing the rectangle maintains approximately the same position in viewport space
let before_center = (rect_bbox_before[0] + rect_bbox_before[1]) / 2.; // TODO: Should be: DVec2(0.0, -25.0), regression (#2688) causes it to be: DVec2(100.0, 25.0) let before_center = (rect_bbox_before[0] + rect_bbox_before[1]) / 2.; // TODO: Should be: DVec2(0., -25.), regression (#2688) causes it to be: DVec2(100., 25.)
let after_center = (rect_bbox_after[0] + rect_bbox_after[1]) / 2.; // TODO: Should be: DVec2(0.0, -25.0), regression (#2688) causes it to be: DVec2(200.0, 75.0) let after_center = (rect_bbox_after[0] + rect_bbox_after[1]) / 2.; // TODO: Should be: DVec2(0., -25.), regression (#2688) causes it to be: DVec2(200., 75.)
let distance = before_center.distance(after_center); // TODO: Should be: 0.0, regression (#2688) causes it to be: 111.80339887498948 let distance = before_center.distance(after_center); // TODO: Should be: 0., regression (#2688) causes it to be: 111.80339887498948
assert!( assert!(
distance < 1., distance < 1.,
@@ -79,7 +79,7 @@ pub enum GraphOperationMessage {
}, },
NewBooleanOperationLayer { NewBooleanOperationLayer {
id: NodeId, id: NodeId,
operation: graphene_std::vector::misc::BooleanOperation, operation: graphene_std::path_bool::BooleanOperation,
parent: LayerNodeIdentifier, parent: LayerNodeIdentifier,
insert_index: usize, insert_index: usize,
}, },
@@ -10,8 +10,8 @@ use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
use graphene_std::Color; use graphene_std::Color;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::convert_usvg_path;
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -134,7 +134,7 @@ impl<'a> ModifyInputsContext<'a> {
LayerNodeIdentifier::new(new_id, self.network_interface, &[]) LayerNodeIdentifier::new(new_id, self.network_interface, &[])
} }
pub fn insert_boolean_data(&mut self, operation: graphene_std::vector::misc::BooleanOperation, layer: LayerNodeIdentifier) { pub fn insert_boolean_data(&mut self, operation: graphene_std::path_bool::BooleanOperation, layer: LayerNodeIdentifier) {
let boolean = resolve_document_node_type("Boolean Operation").expect("Boolean node does not exist").node_template_input_override([ let boolean = resolve_document_node_type("Boolean Operation").expect("Boolean node does not exist").node_template_input_override([
Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_std::GraphicGroupTable::default()), true)), Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_std::GraphicGroupTable::default()), true)),
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)), Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
@@ -1,3 +1,5 @@
mod document_node_derive;
use super::node_properties::choice::enum_choice; use super::node_properties::choice::enum_choice;
use super::node_properties::{self, ParameterWidgetsInfo}; use super::node_properties::{self, ParameterWidgetsInfo};
use super::utility_types::FrontendNodeType; use super::utility_types::FrontendNodeType;
@@ -91,7 +93,7 @@ static DOCUMENT_NODE_TYPES: once_cell::sync::Lazy<Vec<DocumentNodeDefinition>> =
/// Defines the "signature" or "header file"-like metadata for the document nodes, but not the implementation (which is defined in the node registry). /// Defines the "signature" or "header file"-like metadata for the document nodes, but not the implementation (which is defined in the node registry).
/// The [`DocumentNode`] is the instance while these [`DocumentNodeDefinition`]s are the "classes" or "blueprints" from which the instances are built. /// The [`DocumentNode`] is the instance while these [`DocumentNodeDefinition`]s are the "classes" or "blueprints" from which the instances are built.
fn static_nodes() -> Vec<DocumentNodeDefinition> { fn static_nodes() -> Vec<DocumentNodeDefinition> {
let mut custom = vec![ let custom = vec![
// TODO: Auto-generate this from its proto node macro // TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition { DocumentNodeDefinition {
identifier: "Identity", identifier: "Identity",
@@ -164,13 +166,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)], inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)], inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
@@ -241,21 +243,21 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNode { DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 1)], inputs: vec![NodeInput::network(generic!(T), 1)],
implementation: DocumentNodeImplementation::proto("graphene_core::graphic_element::ToElementNode"), implementation: DocumentNodeImplementation::proto("graphene_core::graphic_element::ToElementNode"),
manual_composition: Some(generic!(T)), manual_composition: Some(concrete!(Context)),
..Default::default() ..Default::default()
}, },
// Primary (bottom) input type coercion // Primary (bottom) input type coercion
DocumentNode { DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 0)], inputs: vec![NodeInput::network(generic!(T), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::graphic_element::ToGroupNode"), implementation: DocumentNodeImplementation::proto("graphene_core::graphic_element::ToGroupNode"),
manual_composition: Some(generic!(T)), manual_composition: Some(concrete!(Context)),
..Default::default() ..Default::default()
}, },
// The monitor node is used to display a thumbnail in the UI // The monitor node is used to display a thumbnail in the UI
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)], inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"), implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"),
manual_composition: Some(generic!(T)), manual_composition: Some(concrete!(Context)),
skip_deduplication: true, skip_deduplication: true,
..Default::default() ..Default::default()
}, },
@@ -316,7 +318,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Layer".to_string(), display_name: "Layer".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(1, -3)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -3)),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
@@ -333,7 +335,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default() ..Default::default()
}, },
}, },
description: Cow::Borrowed("The Merge node combines graphical data through composition."), description: Cow::Borrowed("Merge attaches a layer to the stack's group."),
properties: None, properties: None,
}, },
DocumentNodeDefinition { DocumentNodeDefinition {
@@ -429,7 +431,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
display_name: "To Artboard".to_string(), display_name: "To Artboard".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-10, -3)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
@@ -437,7 +439,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Monitor".to_string(), display_name: "Monitor".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-2, -3)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, -3)),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
@@ -445,7 +447,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Append Artboards".to_string(), display_name: "Append Artboards".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(6, -4)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -4)),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
@@ -1464,7 +1466,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
// description: Cow::Borrowed("TODO"), // description: Cow::Borrowed("TODO"),
// properties: None, // properties: None,
// }, // },
// (*IMAGINATE_NODE).clone(),
DocumentNodeDefinition { DocumentNodeDefinition {
identifier: "Path", identifier: "Path",
category: "Vector", category: "Vector",
@@ -1649,7 +1650,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::network(concrete!(DVec2), 5), NodeInput::network(concrete!(DVec2), 5),
], ],
manual_composition: Some(concrete!(Context)), manual_composition: Some(concrete!(Context)),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::TransformNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::TransformNode")),
..Default::default() ..Default::default()
}, },
] ]
@@ -1733,7 +1734,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
nodes: vec![ nodes: vec![
DocumentNode { DocumentNode {
inputs: vec![NodeInput::network(concrete!(VectorDataTable), 0), NodeInput::network(concrete!(vector::style::Fill), 1)], inputs: vec![NodeInput::network(concrete!(VectorDataTable), 0), NodeInput::network(concrete!(vector::style::Fill), 1)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::vector::BooleanOperationNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_path_bool::BooleanOperationNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
@@ -1745,13 +1746,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)], inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)], inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
@@ -1764,7 +1765,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}), }),
inputs: vec![ inputs: vec![
NodeInput::value(TaggedValue::GraphicGroup(GraphicGroupTable::default()), true), NodeInput::value(TaggedValue::GraphicGroup(GraphicGroupTable::default()), true),
NodeInput::value(TaggedValue::BooleanOperation(vector::misc::BooleanOperation::Union), false), NodeInput::value(TaggedValue::BooleanOperation(path_bool::BooleanOperation::Union), false),
], ],
..Default::default() ..Default::default()
}, },
@@ -1822,12 +1823,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
properties: None, properties: None,
}, },
DocumentNodeDefinition { DocumentNodeDefinition {
identifier: "Sample Points", identifier: "Sample Polyline",
category: "Vector: Modifier", category: "Vector: Modifier",
node_template: NodeTemplate { node_template: NodeTemplate {
document_node: DocumentNode { document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork { implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(4), 0)], // Taken from output 0 of Sample Points exports: vec![NodeInput::node(NodeId(4), 0)],
nodes: [ nodes: [
DocumentNode { DocumentNode {
inputs: vec![NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0)], inputs: vec![NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0)],
@@ -1838,13 +1839,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNode { DocumentNode {
inputs: vec![ inputs: vec![
NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0), NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0),
NodeInput::network(concrete!(f64), 1), // From the document node's parameters NodeInput::network(concrete!(vector::misc::PointSpacingType), 1),
NodeInput::network(concrete!(f64), 2), // From the document node's parameters NodeInput::network(concrete!(f64), 2),
NodeInput::network(concrete!(f64), 3), // From the document node's parameters NodeInput::network(concrete!(f64), 3),
NodeInput::network(concrete!(bool), 4), // From the document node's parameters NodeInput::network(concrete!(f64), 4),
NodeInput::node(NodeId(0), 0), // From output 0 of SubpathSegmentLengthsNode NodeInput::network(concrete!(f64), 5),
NodeInput::network(concrete!(bool), 6),
NodeInput::node(NodeId(0), 0),
], ],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SamplePointsNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SamplePolylineNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
@@ -1856,13 +1859,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)], inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(3), 0)], inputs: vec![NodeInput::node(NodeId(3), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
@@ -1875,6 +1878,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}), }),
inputs: vec![ inputs: vec![
NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorDataTable::default()), true), NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorDataTable::default()), true),
NodeInput::value(TaggedValue::PointSpacingType(Default::default()), false),
NodeInput::value(TaggedValue::F64(100.), false),
NodeInput::value(TaggedValue::F64(100.), false), NodeInput::value(TaggedValue::F64(100.), false),
NodeInput::value(TaggedValue::F64(0.), false), NodeInput::value(TaggedValue::F64(0.), false),
NodeInput::value(TaggedValue::F64(0.), false), NodeInput::value(TaggedValue::F64(0.), false),
@@ -1889,14 +1894,14 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Subpath Segment Lengths".to_string(), display_name: "Subpath Segment Lengths".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 5)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 7)),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
}, },
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Sample Points".to_string(), display_name: "Sample Polyline".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
..Default::default() ..Default::default()
}, },
@@ -1937,18 +1942,28 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}), }),
input_properties: vec![ input_properties: vec![
("Vector Data", "The shape to be resampled and converted into a polyline.").into(), ("Vector Data", "The shape to be resampled and converted into a polyline.").into(),
Into::<PropertiesRow>::into(("Spacing", node_properties::SAMPLE_POLYLINE_TOOLTIP_SPACING)),
PropertiesRow::with_override( PropertiesRow::with_override(
"Spacing", "Separation",
"Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).", node_properties::SAMPLE_POLYLINE_TOOLTIP_SEPARATION,
WidgetOverride::Number(NumberInputSettings { WidgetOverride::Number(NumberInputSettings {
min: Some(1.), min: Some(0.),
unit: Some(" px".to_string()), unit: Some(" px".to_string()),
..Default::default() ..Default::default()
}), }),
), ),
PropertiesRow::with_override(
"Quantity",
node_properties::SAMPLE_POLYLINE_TOOLTIP_QUANTITY,
WidgetOverride::Number(NumberInputSettings {
min: Some(2.),
is_integer: true,
..Default::default()
}),
),
PropertiesRow::with_override( PropertiesRow::with_override(
"Start Offset", "Start Offset",
"Exclude some distance from the start of the path before the first instance.", node_properties::SAMPLE_POLYLINE_TOOLTIP_START_OFFSET,
WidgetOverride::Number(NumberInputSettings { WidgetOverride::Number(NumberInputSettings {
min: Some(0.), min: Some(0.),
unit: Some(" px".to_string()), unit: Some(" px".to_string()),
@@ -1957,21 +1972,21 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
), ),
PropertiesRow::with_override( PropertiesRow::with_override(
"Stop Offset", "Stop Offset",
"Exclude some distance from the end of the path after the last instance.", node_properties::SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET,
WidgetOverride::Number(NumberInputSettings { WidgetOverride::Number(NumberInputSettings {
min: Some(0.), min: Some(0.),
unit: Some(" px".to_string()), unit: Some(" px".to_string()),
..Default::default() ..Default::default()
}), }),
), ),
Into::<PropertiesRow>::into(("Adaptive Spacing", "Round 'Spacing' to a nearby value that divides into the path length evenly.")), Into::<PropertiesRow>::into(("Adaptive Spacing", node_properties::SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING)),
], ],
output_names: vec!["Vector".to_string()], output_names: vec!["Vector".to_string()],
..Default::default() ..Default::default()
}, },
}, },
description: Cow::Borrowed("Convert vector geometry into a polyline composed of evenly spaced points."), description: Cow::Borrowed("Convert vector geometry into a polyline composed of evenly spaced points."),
properties: None, properties: Some("sample_polyline_properties"),
}, },
DocumentNodeDefinition { DocumentNodeDefinition {
identifier: "Scatter Points", identifier: "Scatter Points",
@@ -1999,13 +2014,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)], inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)], inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")), implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
..Default::default() ..Default::default()
}, },
@@ -2100,233 +2115,9 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}, },
]; ];
// Remove struct generics document_node_derive::post_process_nodes(custom)
for DocumentNodeDefinition { node_template, .. } in custom.iter_mut() {
let NodeTemplate {
document_node: DocumentNode { implementation, .. },
..
} = node_template;
if let DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) = implementation {
if let Some((new_name, _suffix)) = name.rsplit_once("<") {
*name = Cow::Owned(new_name.to_string())
}
};
}
let node_registry = graphene_std::registry::NODE_REGISTRY.lock().unwrap();
'outer: for (id, metadata) in graphene_std::registry::NODE_METADATA.lock().unwrap().iter() {
use graphene_std::registry::*;
let id = id.clone();
for node in custom.iter() {
let DocumentNodeDefinition {
node_template: NodeTemplate {
document_node: DocumentNode { implementation, .. },
..
},
..
} = node;
match implementation {
DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) if name == &id => continue 'outer,
_ => (),
}
}
let NodeMetadata {
display_name,
category,
fields,
description,
properties,
} = metadata;
let Some(implementations) = &node_registry.get(&id) else { continue };
let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
let mut input_type = &first_node_io.call_argument;
if valid_inputs.len() > 1 {
input_type = &const { generic!(D) };
}
let output_type = &first_node_io.return_value;
let inputs = fields
.iter()
.zip(first_node_io.inputs.iter())
.enumerate()
.map(|(index, (field, node_io_ty))| {
let ty = field.default_type.as_ref().unwrap_or(node_io_ty);
let exposed = if index == 0 { *ty != fn_type_fut!(Context, ()) } else { field.exposed };
match field.value_source {
RegistryValueSource::None => {}
RegistryValueSource::Default(data) => return NodeInput::value(TaggedValue::from_primitive_string(data, ty).unwrap_or(TaggedValue::None), exposed),
RegistryValueSource::Scope(data) => return NodeInput::scope(Cow::Borrowed(data)),
};
if let Some(type_default) = TaggedValue::from_type(ty) {
return NodeInput::value(type_default, exposed);
}
NodeInput::value(TaggedValue::None, true)
})
.collect();
let node = DocumentNodeDefinition {
identifier: display_name,
node_template: NodeTemplate {
document_node: DocumentNode {
inputs,
manual_composition: Some(input_type.clone()),
implementation: DocumentNodeImplementation::ProtoNode(id.clone().into()),
visible: true,
skip_deduplication: false,
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
// TODO: Store information for input overrides in the node macro
input_properties: fields
.iter()
.map(|f| match f.widget_override {
RegistryWidgetOverride::None => (f.name, f.description).into(),
RegistryWidgetOverride::Hidden => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Hidden),
RegistryWidgetOverride::String(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::String(str.to_string())),
RegistryWidgetOverride::Custom(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
})
.collect(),
output_names: vec![output_type.to_string()],
has_primary_output: true,
locked: false,
..Default::default()
},
},
category: category.unwrap_or("UNCATEGORIZED"),
description: Cow::Borrowed(description),
properties: *properties,
};
custom.push(node);
}
custom
} }
// pub static IMAGINATE_NODE: Lazy<DocumentNodeDefinition> = Lazy::new(|| DocumentNodeDefinition {
// identifier: "Imaginate",
// category: "Raster",
// node_template: NodeTemplate {
// document_node: DocumentNode {
// implementation: DocumentNodeImplementation::Network(NodeNetwork {
// exports: vec![NodeInput::node(NodeId(1), 0)],
// nodes: [
// DocumentNode {
// inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0)],
// implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"),
// manual_composition: Some(concrete!(Context)),
// skip_deduplication: true,
// ..Default::default()
// },
// DocumentNode {
// inputs: vec![
// NodeInput::node(NodeId(0), 0),
// NodeInput::network(concrete!(&WasmEditorApi), 1),
// NodeInput::network(concrete!(ImaginateController), 2),
// NodeInput::network(concrete!(f64), 3),
// NodeInput::network(concrete!(Option<DVec2>), 4),
// NodeInput::network(concrete!(u32), 5),
// NodeInput::network(concrete!(ImaginateSamplingMethod), 6),
// NodeInput::network(concrete!(f64), 7),
// NodeInput::network(concrete!(String), 8),
// NodeInput::network(concrete!(String), 9),
// NodeInput::network(concrete!(bool), 10),
// NodeInput::network(concrete!(f64), 11),
// NodeInput::network(concrete!(bool), 12),
// NodeInput::network(concrete!(f64), 13),
// NodeInput::network(concrete!(ImaginateMaskStartingFill), 14),
// NodeInput::network(concrete!(bool), 15),
// NodeInput::network(concrete!(bool), 16),
// NodeInput::network(concrete!(u64), 17),
// ],
// implementation: DocumentNodeImplementation::proto("graphene_std::raster::ImaginateNode"),
// ..Default::default()
// },
// ]
// .into_iter()
// .enumerate()
// .map(|(id, node)| (NodeId(id as u64), node))
// .collect(),
// ..Default::default()
// }),
// inputs: vec![
// NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true),
// NodeInput::scope("editor-api"),
// NodeInput::value(TaggedValue::ImaginateController(Default::default()), false),
// NodeInput::value(TaggedValue::F64(0.), false), // Remember to keep index used in `ImaginateRandom` updated with this entry's index
// NodeInput::value(TaggedValue::OptionalDVec2(None), false),
// NodeInput::value(TaggedValue::U32(30), false),
// NodeInput::value(TaggedValue::ImaginateSamplingMethod(ImaginateSamplingMethod::EulerA), false),
// NodeInput::value(TaggedValue::F64(7.5), false),
// NodeInput::value(TaggedValue::String(String::new()), false),
// NodeInput::value(TaggedValue::String(String::new()), false),
// NodeInput::value(TaggedValue::Bool(false), false),
// NodeInput::value(TaggedValue::F64(66.), false),
// NodeInput::value(TaggedValue::Bool(true), false),
// NodeInput::value(TaggedValue::F64(4.), false),
// NodeInput::value(TaggedValue::ImaginateMaskStartingFill(ImaginateMaskStartingFill::Fill), false),
// NodeInput::value(TaggedValue::Bool(false), false),
// NodeInput::value(TaggedValue::Bool(false), false),
// NodeInput::value(TaggedValue::U64(0), false),
// ],
// ..Default::default()
// },
// persistent_node_metadata: DocumentNodePersistentMetadata {
// network_metadata: Some(NodeNetworkMetadata {
// persistent_metadata: NodeNetworkPersistentMetadata {
// node_metadata: [
// DocumentNodeMetadata {
// persistent_metadata: DocumentNodePersistentMetadata {
// display_name: "Monitor".to_string(),
// ..Default::default()
// },
// ..Default::default()
// },
// DocumentNodeMetadata {
// persistent_metadata: DocumentNodePersistentMetadata {
// display_name: "Imaginate".to_string(),
// ..Default::default()
// },
// ..Default::default()
// },
// ]
// .into_iter()
// .enumerate()
// .map(|(id, node)| (NodeId(id as u64), node))
// .collect(),
// ..Default::default()
// },
// ..Default::default()
// }),
// input_properties: vec![
// "Input Image".into(),
// "Editor Api".into(),
// "Controller".into(),
// "Seed".into(),
// "Resolution".into(),
// "Samples".into(),
// "Sampling Method".into(),
// "Prompt Guidance".into(),
// "Prompt".into(),
// "Negative Prompt".into(),
// "Adapt Input Image".into(),
// "Image Creativity".into(),
// "Inpaint".into(),
// "Mask Blur".into(),
// "Mask Starting Fill".into(),
// "Improve Faces".into(),
// "Tiling".into(),
// ],
// output_names: vec!["Image".to_string()],
// ..Default::default()
// },
// },
// description: Cow::Borrowed("TODO"),
// properties: None, // Some(&node_properties::imaginate_properties),
// });
type NodeProperties = HashMap<String, Box<dyn Fn(NodeId, &mut NodePropertiesContext) -> Vec<LayoutGroup> + Send + Sync>>; type NodeProperties = HashMap<String, Box<dyn Fn(NodeId, &mut NodePropertiesContext) -> Vec<LayoutGroup> + Send + Sync>>;
pub static NODE_OVERRIDES: once_cell::sync::Lazy<NodeProperties> = once_cell::sync::Lazy::new(static_node_properties); pub static NODE_OVERRIDES: once_cell::sync::Lazy<NodeProperties> = once_cell::sync::Lazy::new(static_node_properties);
@@ -2344,6 +2135,7 @@ fn static_node_properties() -> NodeProperties {
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties)); map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties)); map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties)); map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
map.insert( map.insert(
"identity_properties".to_string(), "identity_properties".to_string(),
Box::new(|_node_id, _context| node_properties::string_properties("The identity node simply passes its data through.")), Box::new(|_node_id, _context| node_properties::string_properties("The identity node simply passes its data through.")),
@@ -3060,19 +2852,3 @@ impl DocumentNodeDefinition {
self.node_template_input_override(self.node_template.document_node.inputs.clone().into_iter().map(Some)) self.node_template_input_override(self.node_template.document_node.inputs.clone().into_iter().map(Some))
} }
} }
// Previously used by the Imaginate node, but usage was commented out since it did nothing.
// pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetwork {
// let mut network = NodeNetwork { ..Default::default() };
// network.push_node_to_document_network(
// resolve_document_node_type("Input Frame")
// .expect("Input Frame node does not exist")
// .to_document_node_default_inputs([], DocumentNodeMetadata::position((8, 4))),
// );
// network.push_node_to_document_network(
// resolve_document_node_type("Output")
// .expect("Output node does not exist")
// .to_document_node([NodeInput::node(output_node_id, 0)], DocumentNodeMetadata::position((output_offset + 8, 4))),
// );
// network
// }
@@ -0,0 +1,94 @@
use super::DocumentNodeDefinition;
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, NodeTemplate, PropertiesRow, WidgetOverride};
use graph_craft::ProtoNodeIdentifier;
use graph_craft::document::*;
use graphene_std::registry::*;
use graphene_std::*;
use std::collections::HashSet;
pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec<DocumentNodeDefinition> {
// Remove struct generics
for DocumentNodeDefinition { node_template, .. } in custom.iter_mut() {
let NodeTemplate {
document_node: DocumentNode { implementation, .. },
..
} = node_template;
if let DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) = implementation {
if let Some((new_name, _suffix)) = name.rsplit_once("<") {
*name = Cow::Owned(new_name.to_string())
}
};
}
let node_registry = graphene_core::registry::NODE_REGISTRY.lock().unwrap();
'outer: for (id, metadata) in NODE_METADATA.lock().unwrap().iter() {
for node in custom.iter() {
let DocumentNodeDefinition {
node_template: NodeTemplate {
document_node: DocumentNode { implementation, .. },
..
},
..
} = node;
match implementation {
DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) if name == id => continue 'outer,
_ => (),
}
}
let NodeMetadata {
display_name,
category,
fields,
description,
properties,
} = metadata;
let Some(implementations) = &node_registry.get(id) else { continue };
let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
let input_type = if valid_inputs.len() > 1 { &const { generic!(D) } } else { &first_node_io.call_argument };
let output_type = &first_node_io.return_value;
let inputs = preprocessor::node_inputs(fields, first_node_io);
let node = DocumentNodeDefinition {
identifier: display_name,
node_template: NodeTemplate {
document_node: DocumentNode {
inputs,
manual_composition: Some(input_type.clone()),
implementation: DocumentNodeImplementation::ProtoNode(id.clone().into()),
visible: true,
skip_deduplication: false,
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
// TODO: Store information for input overrides in the node macro
input_properties: fields
.iter()
.map(|f| match f.widget_override {
RegistryWidgetOverride::None => (f.name, f.description).into(),
RegistryWidgetOverride::Hidden => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Hidden),
RegistryWidgetOverride::String(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::String(str.to_string())),
RegistryWidgetOverride::Custom(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
})
.collect(),
output_names: vec![output_type.to_string()],
has_primary_output: true,
locked: false,
..Default::default()
},
},
category: category.unwrap_or("UNCATEGORIZED"),
description: Cow::Borrowed(description),
properties: *properties,
};
custom.push(node);
}
custom
}
@@ -21,6 +21,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::proto::GraphErrors; use graph_craft::proto::GraphErrors;
use graphene_std::math::math_ext::QuadExt;
use graphene_std::*; use graphene_std::*;
use renderer::Quad; use renderer::Quad;
use std::cmp::Ordering; use std::cmp::Ordering;
@@ -1396,12 +1397,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
input, input,
}); });
responses.add(PropertiesPanelMessage::Refresh); responses.add(PropertiesPanelMessage::Refresh);
if (network_interface if !(network_interface.reference(&node_id, selection_network_path).is_none() || input_index == 0) && network_interface.connected_to_output(&node_id, selection_network_path) {
.reference(&node_id, selection_network_path)
.is_none_or(|reference| *reference != Some("Imaginate".to_string())) // TODO: Potentially remove the reference to Imaginate
|| input_index == 0)
&& network_interface.connected_to_output(&node_id, selection_network_path)
{
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
} }
@@ -13,6 +13,7 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graphene_std::animation::RealTimeMode; use graphene_std::animation::RealTimeMode;
use graphene_std::ops::XY; use graphene_std::ops::XY;
use graphene_std::path_bool::BooleanOperation;
use graphene_std::raster::curve::Curve; use graphene_std::raster::curve::Curve;
use graphene_std::raster::{ use graphene_std::raster::{
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
@@ -22,9 +23,9 @@ use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
use graphene_std::text::Font; use graphene_std::text::Font;
use graphene_std::transform::{Footprint, ReferencePoint}; use graphene_std::transform::{Footprint, ReferencePoint};
use graphene_std::vector::VectorDataTable; use graphene_std::vector::VectorDataTable;
use graphene_std::vector::misc::CentroidType; use graphene_std::vector::misc::GridType;
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm}; use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
use graphene_std::vector::misc::{BooleanOperation, GridType}; use graphene_std::vector::misc::{CentroidType, PointSpacingType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops};
use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{GraphicGroupTable, NodeInputDecleration}; use graphene_std::{GraphicGroupTable, NodeInputDecleration};
@@ -238,6 +239,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
@@ -1225,6 +1227,64 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
widgets widgets
} }
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SPACING: &str = "Use a point sampling density controlled by a distance between, or specific number of, points.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SEPARATION: &str = "Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_QUANTITY: &str = "Number of points to place along the path.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_START_OFFSET: &str = "Exclude some distance from the start of the path before the first instance.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET: &str = "Exclude some distance from the end of the path after the last instance.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING: &str = "Round 'Separation' to a nearby value that divides into the path length evenly.";
pub(crate) fn sample_polyline_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::vector::sample_polyline::*;
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
log::error!("Could not get document node in sample_polyline_properties: {err}");
return Vec::new();
}
};
let current_spacing = document_node.inputs.get(SpacingInput::INDEX).and_then(|input| input.as_value()).cloned();
let is_quantity = matches!(current_spacing, Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)));
let spacing = enum_choice::<PointSpacingType>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, SpacingInput::INDEX, true, context))
.property_row();
let separation = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, SeparationInput::INDEX, true, context),
NumberInput::default().min(0.).unit(" px"),
);
let quantity = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, QuantityInput::INDEX, true, context),
NumberInput::default().min(2.).int(),
);
let start_offset = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, StartOffsetInput::INDEX, true, context),
NumberInput::default().min(0.).unit(" px"),
);
let stop_offset = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, StopOffsetInput::INDEX, true, context),
NumberInput::default().min(0.).unit(" px"),
);
let adaptive_spacing = bool_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, AdaptiveSpacingInput::INDEX, true, context),
CheckboxInput::default().disabled(is_quantity),
);
vec![
spacing.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_SPACING),
match current_spacing {
Some(TaggedValue::PointSpacingType(PointSpacingType::Separation)) => LayoutGroup::Row { widgets: separation }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_SEPARATION),
Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)) => LayoutGroup::Row { widgets: quantity }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_QUANTITY),
_ => LayoutGroup::Row { widgets: vec![] },
},
LayoutGroup::Row { widgets: start_offset }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_START_OFFSET),
LayoutGroup::Row { widgets: stop_offset }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET),
LayoutGroup::Row { widgets: adaptive_spacing }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING),
]
}
pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::exposure::*; use graphene_std::raster::exposure::*;
@@ -1,547 +0,0 @@
//! This has all been copied out of node_properties.rs to avoid leaving hundreds of lines of commented out code in that file. It's left here instead for future reference.
// pub fn imaginate_sampling_method(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup {
// let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info;
// vec![
// DropdownInput::new(
// ImaginateSamplingMethod::list()
// .into_iter()
// .map(|method| {
// vec![
// MenuListEntry::new(format!("{:?}", method))
// .label(method.to_string())
// .on_update(update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, index)),
// ]
// })
// .collect(),
// )
// .widget_holder(),
// ]
// .into()
// }
// pub fn imaginate_mask_starting_fill(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup {
// let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info;
// vec![
// DropdownInput::new(
// ImaginateMaskStartingFill::list()
// .into_iter()
// .map(|fill| {
// vec![
// MenuListEntry::new(format!("{:?}", fill))
// .label(fill.to_string())
// .on_update(update_value(move |_| TaggedValue::ImaginateMaskStartingFill(fill), node_id, index)),
// ]
// })
// .collect(),
// )
// .widget_holder(),
// ]
// .into()
// }
// pub(crate) fn imaginate_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
// let imaginate_node = [context.selection_network_path, &[node_id]].concat();
// let resolve_input = |name: &str| {
// IMAGINATE_NODE
// .default_node_template()
// .persistent_node_metadata
// .input_properties
// .iter()
// .position(|row| row.input_name.as_str() == name)
// .unwrap_or_else(|| panic!("Input {name} not found"))
// };
// let seed_index = resolve_input("Seed");
// let resolution_index = resolve_input("Resolution");
// let samples_index = resolve_input("Samples");
// let sampling_method_index = resolve_input("Sampling Method");
// let text_guidance_index = resolve_input("Prompt Guidance");
// let text_index = resolve_input("Prompt");
// let neg_index = resolve_input("Negative Prompt");
// let base_img_index = resolve_input("Adapt Input Image");
// let img_creativity_index = resolve_input("Image Creativity");
// // let mask_index = resolve_input("Masking Layer");
// // let inpaint_index = resolve_input("Inpaint");
// // let mask_blur_index = resolve_input("Mask Blur");
// // let mask_fill_index = resolve_input("Mask Starting Fill");
// let faces_index = resolve_input("Improve Faces");
// let tiling_index = resolve_input("Tiling");
// let document_node = match get_document_node(node_id, context) {
// Ok(document_node) => document_node,
// Err(err) => {
// log::error!("Could not get document node in imaginate_properties: {err}");
// return Vec::new();
// }
// };
// let controller = &document_node.inputs[resolve_input("Controller")];
// let server_status = {
// let server_status = context.persistent_data.imaginate.server_status();
// let status_text = server_status.to_text();
// let mut widgets = vec![
// TextLabel::new("Server").widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// IconButton::new("Settings", 24)
// .tooltip("Preferences: Imaginate")
// .on_update(|_| DialogMessage::RequestPreferencesDialog.into())
// .widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// TextLabel::new(status_text).bold(true).widget_holder(),
// Separator::new(SeparatorType::Related).widget_holder(),
// IconButton::new("Reload", 24)
// .tooltip("Refresh connection status")
// .on_update(|_| PortfolioMessage::ImaginateCheckServerStatus.into())
// .widget_holder(),
// ];
// if let ImaginateServerStatus::Unavailable | ImaginateServerStatus::Failed(_) = server_status {
// widgets.extend([
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// TextButton::new("Server Help")
// .tooltip("Learn how to connect Imaginate to an image generation server")
// .on_update(|_| {
// FrontendMessage::TriggerVisitLink {
// url: "https://github.com/GraphiteEditor/Graphite/discussions/1089".to_string(),
// }
// .into()
// })
// .widget_holder(),
// ]);
// }
// LayoutGroup::Row { widgets }.with_tooltip("Connection status to the server that computes generated images")
// };
// let Some(TaggedValue::ImaginateController(controller)) = controller.as_value() else {
// panic!("Invalid output status input")
// };
// let imaginate_status = controller.get_status();
// let use_base_image = if let Some(&TaggedValue::Bool(use_base_image)) = &document_node.inputs[base_img_index].as_value() {
// use_base_image
// } else {
// true
// };
// let transform_not_connected = false;
// let progress = {
// let mut widgets = vec![TextLabel::new("Progress").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()];
// add_blank_assist(&mut widgets);
// let status = imaginate_status.to_text();
// widgets.push(TextLabel::new(status.as_ref()).bold(true).widget_holder());
// LayoutGroup::Row { widgets }.with_tooltip(match imaginate_status {
// ImaginateStatus::Failed(_) => status.as_ref(),
// _ => "When generating, the percentage represents how many sampling steps have so far been processed out of the target number",
// })
// };
// let image_controls = {
// let mut widgets = vec![TextLabel::new("Image").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()];
// match &imaginate_status {
// ImaginateStatus::Beginning | ImaginateStatus::Uploading => {
// add_blank_assist(&mut widgets);
// widgets.push(TextButton::new("Beginning...").tooltip("Sending image generation request to the server").disabled(true).widget_holder());
// }
// ImaginateStatus::Generating(_) => {
// add_blank_assist(&mut widgets);
// widgets.push(
// TextButton::new("Terminate")
// .tooltip("Cancel the in-progress image generation and keep the latest progress")
// .on_update({
// let controller = controller.clone();
// move |_| {
// controller.request_termination();
// Message::NoOp
// }
// })
// .widget_holder(),
// );
// }
// ImaginateStatus::Terminating => {
// add_blank_assist(&mut widgets);
// widgets.push(
// TextButton::new("Terminating...")
// .tooltip("Waiting on the final image generated after termination")
// .disabled(true)
// .widget_holder(),
// );
// }
// ImaginateStatus::Ready | ImaginateStatus::ReadyDone | ImaginateStatus::Terminated | ImaginateStatus::Failed(_) => widgets.extend_from_slice(&[
// IconButton::new("Random", 24)
// .tooltip("Generate with a new random seed")
// .on_update({
// let imaginate_node = imaginate_node.clone();
// let controller = controller.clone();
// move |_| {
// controller.trigger_regenerate();
// DocumentMessage::ImaginateRandom {
// imaginate_node: imaginate_node.clone(),
// then_generate: true,
// }
// .into()
// }
// })
// .widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// TextButton::new("Generate")
// .tooltip("Fill layer frame by generating a new image")
// .on_update({
// let controller = controller.clone();
// let imaginate_node = imaginate_node.clone();
// move |_| {
// controller.trigger_regenerate();
// DocumentMessage::ImaginateGenerate {
// imaginate_node: imaginate_node.clone(),
// }
// .into()
// }
// })
// .widget_holder(),
// Separator::new(SeparatorType::Related).widget_holder(),
// TextButton::new("Clear")
// .tooltip("Remove generated image from the layer frame")
// .disabled(!matches!(imaginate_status, ImaginateStatus::ReadyDone))
// .on_update({
// let controller = controller.clone();
// let imaginate_node = imaginate_node.clone();
// move |_| {
// controller.set_status(ImaginateStatus::Ready);
// DocumentMessage::ImaginateGenerate {
// imaginate_node: imaginate_node.clone(),
// }
// .into()
// }
// })
// .widget_holder(),
// ]),
// }
// LayoutGroup::Row { widgets }.with_tooltip("Buttons that control the image generation process")
// };
// // Requires custom layout for the regenerate button
// let seed = {
// let mut widgets = start_widgets(document_node, node_id, seed_index, "Seed", FrontendGraphDataType::Number, false);
// let Some(input) = document_node.inputs.get(seed_index) else {
// log::warn!("A widget failed to be built because its node's input index is invalid.");
// return vec![];
// };
// if let Some(&TaggedValue::F64(seed)) = &input.as_non_exposed_value() {
// widgets.extend_from_slice(&[
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// IconButton::new("Resync", 24)
// .tooltip("Set a new random seed")
// .on_update({
// let imaginate_node = imaginate_node.clone();
// move |_| {
// DocumentMessage::ImaginateRandom {
// imaginate_node: imaginate_node.clone(),
// then_generate: false,
// }
// .into()
// }
// })
// .widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// NumberInput::new(Some(seed))
// .int()
// .min(-((1_u64 << f64::MANTISSA_DIGITS) as f64))
// .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
// .on_update(update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap()), node_id, seed_index))
// .on_commit(commit_value)
// .mode(NumberInputMode::Increment)
// .widget_holder(),
// ])
// }
// // Note: Limited by f64. You cannot even have all the possible u64 values :)
// LayoutGroup::Row { widgets }.with_tooltip("Seed determines the random outcome, enabling limitless unique variations")
// };
// // let transform = context
// // .executor
// // .introspect_node_in_network(context.network, &imaginate_node, |network| network.inputs.first().copied(), |frame: &RasterData<Color>| frame.transform)
// // .unwrap_or_default();
// let image_size = context
// .executor
// .introspect_node_in_network(
// context.network_interface.document_network().unwrap(),
// &imaginate_node,
// |network| {
// network
// .nodes
// .iter()
// .find(|node| {
// node.1
// .inputs
// .iter()
// .any(|node_input| if let NodeInput::Network { import_index, .. } = node_input { *import_index == 0 } else { false })
// })
// .map(|(node_id, _)| node_id)
// .copied()
// },
// |frame: &IORecord<(), RasterData<Color>>| (frame.output.image.width, frame.output.image.height),
// )
// .unwrap_or_default();
// let document_node = match get_document_node(node_id, context) {
// Ok(document_node) => document_node,
// Err(err) => {
// log::error!("Could not get document node in imaginate_properties: {err}");
// return Vec::new();
// }
// };
// let resolution = {
// let mut widgets = start_widgets(document_node, node_id, resolution_index, "Resolution", FrontendGraphDataType::Number, false);
// let round = |size: DVec2| {
// let (x, y) = graphene_std::imaginate::pick_safe_imaginate_resolution(size.into());
// DVec2::new(x as f64, y as f64)
// };
// let Some(input) = document_node.inputs.get(resolution_index) else {
// log::warn!("A widget failed to be built because its node's input index is invalid.");
// return vec![];
// };
// if let Some(&TaggedValue::OptionalDVec2(vec2)) = &input.as_non_exposed_value() {
// let dimensions_is_auto = vec2.is_none();
// let vec2 = vec2.unwrap_or_else(|| round((image_size.0 as f64, image_size.1 as f64).into()));
// widgets.extend_from_slice(&[
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// IconButton::new("FrameAll", 24)
// .tooltip("Set the layer dimensions to this resolution")
// .on_update(move |_| DialogMessage::RequestComingSoonDialog { issue: None }.into())
// .widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// CheckboxInput::new(!dimensions_is_auto || transform_not_connected)
// .icon("Edit12px")
// .tooltip({
// let message = "Set a custom resolution instead of using the input's dimensions (rounded to the nearest 64)";
// let manual_message = "Set a custom resolution instead of using the input's dimensions (rounded to the nearest 64).\n\
// \n\
// (Resolution must be set manually while the 'Transform' input is disconnected.)";
// if transform_not_connected {
// manual_message
// } else {
// message
// }
// })
// .disabled(transform_not_connected)
// .on_update(update_value(
// move |checkbox_input: &CheckboxInput| TaggedValue::OptionalDVec2(if checkbox_input.checked { Some(vec2) } else { None }),
// node_id,
// resolution_index,
// ))
// .on_commit(commit_value)
// .for_label(checkbox_id.clone())
// .widget_holder(),
// Separator::new(SeparatorType::Related).widget_holder(),
// NumberInput::new(Some(vec2.x))
// .label("W")
// .min(64.)
// .step(64.)
// .unit(" px")
// .disabled(dimensions_is_auto && !transform_not_connected)
// .on_update(update_value(
// move |number_input: &NumberInput| TaggedValue::OptionalDVec2(Some(round(DVec2::new(number_input.value.unwrap(), vec2.y)))),
// node_id,
// resolution_index,
// ))
// .on_commit(commit_value)
// .widget_holder(),
// Separator::new(SeparatorType::Related).widget_holder(),
// NumberInput::new(Some(vec2.y))
// .label("H")
// .min(64.)
// .step(64.)
// .unit(" px")
// .disabled(dimensions_is_auto && !transform_not_connected)
// .on_update(update_value(
// move |number_input: &NumberInput| TaggedValue::OptionalDVec2(Some(round(DVec2::new(vec2.x, number_input.value.unwrap())))),
// node_id,
// resolution_index,
// ))
// .on_commit(commit_value)
// .widget_holder(),
// ])
// }
// LayoutGroup::Row { widgets }.with_tooltip(
// "Width and height of the image that will be generated. Larger resolutions take longer to compute.\n\
// \n\
// 512x512 yields optimal results because the AI is trained to understand that scale best. Larger sizes may tend to integrate the prompt's subject more than once. Small sizes are often incoherent.\n\
// \n\
// Dimensions must be a multiple of 64, so these are set by rounding the layer dimensions. A resolution exceeding 1 megapixel is reduced below that limit because larger sizes may exceed available GPU memory on the server.")
// };
// let sampling_steps = {
// let widgets = number_widget(document_node, node_id, samples_index, "Sampling Steps", NumberInput::default().min(0.).max(150.).int(), true);
// LayoutGroup::Row { widgets }.with_tooltip("Number of iterations to improve the image generation quality, with diminishing returns around 40 when using the Euler A sampling method")
// };
// let sampling_method = {
// let mut widgets = start_widgets(document_node, node_id, sampling_method_index, "Sampling Method", FrontendGraphDataType::General, true);
// let Some(input) = document_node.inputs.get(sampling_method_index) else {
// log::warn!("A widget failed to be built because its node's input index is invalid.");
// return vec![];
// };
// if let Some(&TaggedValue::ImaginateSamplingMethod(sampling_method)) = &input.as_non_exposed_value() {
// let sampling_methods = ImaginateSamplingMethod::list();
// let mut entries = Vec::with_capacity(sampling_methods.len());
// for method in sampling_methods {
// entries.push(
// MenuListEntry::new(format!("{method:?}"))
// .label(method.to_string())
// .on_update(update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, sampling_method_index))
// .on_commit(commit_value),
// );
// }
// let entries = vec![entries];
// widgets.extend_from_slice(&[
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// DropdownInput::new(entries).selected_index(Some(sampling_method as u32)).widget_holder(),
// ]);
// }
// LayoutGroup::Row { widgets }.with_tooltip("Algorithm used to generate the image during each sampling step")
// };
// let text_guidance = {
// let widgets = number_widget(document_node, node_id, text_guidance_index, "Prompt Guidance", NumberInput::default().min(0.).max(30.), true);
// LayoutGroup::Row { widgets }.with_tooltip(
// "Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\
// \n\
// Lower values are more creative and exploratory. Higher values are more literal and uninspired.\n\
// \n\
// This parameter is otherwise known as CFG (classifier-free guidance).",
// )
// };
// let text_prompt = {
// let widgets = text_area_widget(document_node, node_id, text_index, "Prompt", true);
// LayoutGroup::Row { widgets }.with_tooltip(
// "Description of the desired image subject and style.\n\
// \n\
// Include an artist name like \"Rembrandt\" or art medium like \"watercolor\" or \"photography\" to influence the look. List multiple to meld styles.\n\
// \n\
// To boost (or lessen) the importance of a word or phrase, wrap it in parentheses ending with a colon and a multiplier, for example:\n\
// \"Colorless green ideas (sleep:1.3) furiously\"",
// )
// };
// let negative_prompt = {
// let widgets = text_area_widget(document_node, node_id, neg_index, "Negative Prompt", true);
// LayoutGroup::Row { widgets }.with_tooltip("A negative text prompt can be used to list things like objects or colors to avoid")
// };
// let base_image = {
// let widgets = bool_widget(document_node, node_id, base_img_index, "Adapt Input Image", CheckboxInput::default().for_label(checkbox_id.clone()), true);
// LayoutGroup::Row { widgets }.with_tooltip("Generate an image based upon the bitmap data plugged into this node")
// };
// let image_creativity = {
// let props = NumberInput::default().percentage().disabled(!use_base_image);
// let widgets = number_widget(document_node, node_id, img_creativity_index, "Image Creativity", props, true);
// LayoutGroup::Row { widgets }.with_tooltip(
// "Strength of the artistic liberties allowing changes from the input image. The image is unchanged at 0% and completely different at 100%.\n\
// \n\
// This parameter is otherwise known as denoising strength.",
// )
// };
// let mut layout = vec![
// server_status,
// progress,
// image_controls,
// seed,
// resolution,
// sampling_steps,
// sampling_method,
// text_guidance,
// text_prompt,
// negative_prompt,
// base_image,
// image_creativity,
// // layer_mask,
// ];
// // if use_base_image && layer_reference_input_layer_is_some {
// // let in_paint = {
// // let mut widgets = start_widgets(document_node, node_id, inpaint_index, "Inpaint", FrontendGraphDataType::Boolean, true);
// // if let Some(& TaggedValue::Bool(in_paint)
// //)/ } = &document_node.inputs[inpaint_index].as_non_exposed_value()
// // {
// // widgets.extend_from_slice(&[
// // Separator::new(SeparatorType::Unrelated).widget_holder(),
// // RadioInput::new(
// // [(true, "Inpaint"), (false, "Outpaint")]
// // .into_iter()
// // .map(|(paint, name)| RadioEntryData::new(name).label(name).on_update(update_value(move |_| TaggedValue::Bool(paint), node_id, inpaint_index)))
// // .collect(),
// // )
// // .selected_index(Some(1 - in_paint as u32))
// // .widget_holder(),
// // ]);
// // }
// // LayoutGroup::Row { widgets }.with_tooltip(
// // "Constrain image generation to the interior (inpaint) or exterior (outpaint) of the mask, while referencing the other unchanged parts as context imagery.\n\
// // \n\
// // An unwanted part of an image can be replaced by drawing around it with a black shape and inpainting with that mask layer.\n\
// // \n\
// // An image can be uncropped by resizing the Imaginate layer to the target bounds and outpainting with a black rectangle mask matching the original image bounds.",
// // )
// // };
// // let blur_radius = {
// // let number_props = NumberInput::default().unit(" px").min(0.).max(25.).int();
// // let widgets = number_widget(document_node, node_id, mask_blur_index, "Mask Blur", number_props, true);
// // LayoutGroup::Row { widgets }.with_tooltip("Blur radius for the mask. Useful for softening sharp edges to blend the masked area with the rest of the image.")
// // };
// // let mask_starting_fill = {
// // let mut widgets = start_widgets(document_node, node_id, mask_fill_index, "Mask Starting Fill", FrontendGraphDataType::General, true);
// // if let Some(& TaggedValue::ImaginateMaskStartingFill(starting_fill)
// //)/ } = &document_node.inputs[mask_fill_index].as_non_exposed_value()
// // {
// // let mask_fill_content_modes = ImaginateMaskStartingFill::list();
// // let mut entries = Vec::with_capacity(mask_fill_content_modes.len());
// // for mode in mask_fill_content_modes {
// // entries.push(MenuListEntry::new(format!("{mode:?}")).label(mode.to_string()).on_update(update_value(move |_| TaggedValue::ImaginateMaskStartingFill(mode), node_id, mask_fill_index)));
// // }
// // let entries = vec![entries];
// // widgets.extend_from_slice(&[
// // Separator::new(SeparatorType::Unrelated).widget_holder(),
// // DropdownInput::new(entries).selected_index(Some(starting_fill as u32)).widget_holder(),
// // ]);
// // }
// // LayoutGroup::Row { widgets }.with_tooltip(
// // "Begin in/outpainting the masked areas using this fill content as the starting input image.\n\
// // \n\
// // Each option can be visualized by generating with 'Sampling Steps' set to 0.",
// // )
// // };
// // layout.extend_from_slice(&[in_paint, blur_radius, mask_starting_fill]);
// // }
// let improve_faces = {
// let widgets = bool_widget(document_node, node_id, faces_index, "Improve Faces", CheckboxInput::default().for_label(checkbox_id.clone()), true);
// LayoutGroup::Row { widgets }.with_tooltip(
// "Postprocess human (or human-like) faces to look subtly less distorted.\n\
// \n\
// This filter can be used on its own by enabling 'Adapt Input Image' and setting 'Sampling Steps' to 0.",
// )
// };
// let tiling = {
// let widgets = bool_widget(document_node, node_id, tiling_index, "Tiling", CheckboxInput::default().for_label(checkbox_id.clone()), true);
// LayoutGroup::Row { widgets }.with_tooltip("Generate the image so its edges loop seamlessly to make repeatable patterns or textures")
// };
// layout.extend_from_slice(&[improve_faces, tiling]);
// layout
// }
@@ -9,8 +9,8 @@ use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, TAU}; use core::f64::consts::{FRAC_PI_2, TAU};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::Color; use graphene_std::Color;
use graphene_std::renderer::ClickTargetType; use graphene_std::math::quad::Quad;
use graphene_std::renderer::Quad; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::{PointId, SegmentId, VectorData}; use graphene_std::vector::{PointId, SegmentId, VectorData};
use std::collections::HashMap; use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
@@ -3,8 +3,9 @@ use crate::messages::portfolio::document::graph_operation::transform_utils;
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad}; use graphene_std::math::quad::Quad;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, VectorData}; use graphene_std::vector::{PointId, VectorData};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64; use std::num::NonZeroU64;
@@ -693,5 +693,5 @@ impl PTZ {
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum GroupFolderType { pub enum GroupFolderType {
Layer, Layer,
BooleanOperation(graphene_std::vector::misc::BooleanOperation), BooleanOperation(graphene_std::path_bool::BooleanOperation),
} }
@@ -12,8 +12,9 @@ use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graph_craft::{Type, concrete}; use graph_craft::{Type, concrete};
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad}; use graphene_std::math::quad::Quad;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, VectorData, VectorModificationType}; use graphene_std::vector::{PointId, VectorData, VectorModificationType};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
use interpreted_executor::node_registry::NODE_REGISTRY; use interpreted_executor::node_registry::NODE_REGISTRY;
@@ -6515,6 +6516,12 @@ pub struct NodePersistentMetadata {
position: NodePosition, position: NodePosition,
} }
impl NodePersistentMetadata {
pub fn new(position: NodePosition) -> Self {
Self { position }
}
}
/// A layer can either be position as Absolute or in a Stack /// A layer can either be position as Absolute or in a Stack
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum LayerPosition { pub enum LayerPosition {
@@ -9,8 +9,8 @@ use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::vector::ManipulatorPointId;
use graphene_std::vector::VectorModificationType; use graphene_std::vector::VectorModificationType;
use graphene_std::vector::{HandleExt, ManipulatorPointId};
use graphene_std::vector::{HandleId, PointId}; use graphene_std::vector::{HandleId, PointId};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::f64::consts::PI; use std::f64::consts::PI;
@@ -0,0 +1,707 @@
// TODO: Eventually remove this document upgrade code
// This file contains lots of hacky code for upgrading old documents to the new format
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
use crate::messages::prelude::DocumentMessageHandler;
use bezier_rs::Subpath;
use glam::IVec2;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
use graphene_std::text::TypesettingConfig;
use graphene_std::uuid::NodeId;
use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAlign};
use graphene_std::vector::{VectorData, VectorDataTable};
use std::collections::HashMap;
const TEXT_REPLACEMENTS: &[(&str, &str)] = &[
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePolylineNode"),
("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"),
];
const REPLACEMENTS: &[(&str, &str)] = &[
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"),
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
("graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode"),
("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"),
("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"),
("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"),
("graphene_core::ops::ConstructVector2", "graphene_core::ops::CoordinateValueNode"),
("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"),
("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"),
("graphene_core::raster::BlendModeNode", "graphene_core::blending_nodes::BlendModeNode"),
("graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"),
("graphene_core::raster::BlendingNode", "graphene_core::blending_nodes::BlendingNode"),
("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"),
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode"),
("graphene_core::raster::ExposureNode", "graphene_core::raster::adjustments::ExposureNode"),
("graphene_core::raster::ExtractChannelNode", "graphene_core::raster::adjustments::ExtractChannelNode"),
("graphene_core::raster::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode"),
("graphene_core::raster::HueSaturationNode", "graphene_core::raster::adjustments::HueSaturationNode"),
("graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::AutoTangentsNode"),
("graphene_core::vector::RemoveHandlesNode", "graphene_core::vector::AutoTangentsNode"),
("graphene_core::raster::InvertNode", "graphene_core::raster::adjustments::InvertNode"),
("graphene_core::raster::InvertRGBNode", "graphene_core::raster::adjustments::InvertNode"),
("graphene_core::raster::LevelsNode", "graphene_core::raster::adjustments::LevelsNode"),
("graphene_core::raster::LuminanceNode", "graphene_core::raster::adjustments::LuminanceNode"),
("graphene_core::raster::ExtractOpaqueNode", "graphene_core::raster::adjustments::MakeOpaqueNode"),
("graphene_core::raster::PosterizeNode", "graphene_core::raster::adjustments::PosterizeNode"),
("graphene_core::raster::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode"),
("graphene_core::raster::VibranceNode", "graphene_core::raster::adjustments::VibranceNode"),
("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"),
("graphene_core::transform::SetTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
("graphene_core::transform::ReplaceTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
("graphene_core::transform::TransformNode", "graphene_core::transform_nodes::TransformNode"),
("graphene_core::transform::BoundlessFootprintNode", "graphene_core::transform_nodes::BoundlessFootprintNode"),
("graphene_core::transform::FreezeRealTimeNode", "graphene_core::transform_nodes::FreezeRealTimeNode"),
("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"),
("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"),
("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"),
("graphene_core::vector::generator_nodes::RectangleGenerator", "graphene_core::vector::generator_nodes::RectangleNode"),
(
"graphene_core::vector::generator_nodes::RegularPolygonGenerator",
"graphene_core::vector::generator_nodes::RegularPolygonNode",
),
("graphene_core::vector::generator_nodes::StarGenerator", "graphene_core::vector::generator_nodes::StarNode"),
("graphene_std::executor::BlendGpuImageNode", "graphene_std::gpu_nodes::BlendGpuImageNode"),
("graphene_std::raster::SampleNode", "graphene_std::raster::SampleImageNode"),
("graphene_core::transform::CullNode", "graphene_core::ops::IdentityNode"),
("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"),
("graphene_core::vector::FlattenVectorElementsNode", "graphene_core::vector::FlattenPathNode"),
("graphene_std::vector::BooleanOperationNode", "graphene_path_bool::BooleanOperationNode"),
];
pub fn document_migration_string_preprocessing(document_serialized_content: String) -> String {
TEXT_REPLACEMENTS
.iter()
.fold(document_serialized_content, |document_serialized_content, (old, new)| document_serialized_content.replace(old, new))
}
pub fn document_migration_reset_node_definition(document_serialized_content: &str) -> bool {
// Upgrade a document being opened to use fresh copies of all nodes
if document_serialized_content.contains("node_output_index") {
return true;
}
// Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946 (see also `fn fix_nodes()` in `main.rs` of Graphene CLI)
if document_serialized_content.contains("graphene_core::ConstructLayerNode") || document_serialized_content.contains("graphene_core::AddArtboardNode") {
return true;
}
false
}
pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) {
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);
// Apply string replacements to each node
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
for (node_id, path) in &node_ids {
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
if let Some(DocumentNodeImplementation::ProtoNode(protonode_id)) = document
.network_interface
.nested_network(&network_path)
.unwrap()
.nodes
.get(node_id)
.map(|node| node.implementation.clone())
{
for (old, new) in REPLACEMENTS {
let node_path_without_type_args = protonode_id.name.split('<').next();
if node_path_without_type_args == Some(old) {
document
.network_interface
.replace_implementation(node_id, &network_path, DocumentNodeImplementation::ProtoNode(new.to_string().into()));
document.network_interface.set_manual_compostion(node_id, &network_path, Some(graph_craft::Type::Generic("T".into())));
}
}
}
}
if reset_node_definitions_on_open {
// This can be used, if uncommented, to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
for node_id in &document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.keys()
.cloned()
.collect::<Vec<NodeId>>()
{
if let Some(reference) = document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.get(node_id)
.and_then(|node| node.persistent_metadata.reference.as_ref())
{
let Some(node_definition) = resolve_document_node_type(reference) else { continue };
let default_definition_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, &[], default_definition_node.document_node.implementation);
document
.network_interface
.replace_implementation_metadata(node_id, &[], default_definition_node.persistent_node_metadata);
document.network_interface.set_manual_compostion(node_id, &[], default_definition_node.document_node.manual_composition);
}
}
}
if document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.iter()
.any(|(node_id, node)| node.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Output") && *node_id == NodeId(0))
{
document.network_interface.delete_nodes(vec![NodeId(0)], true, &[]);
}
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
// Apply upgrades to each node
for (node_id, path) in &node_ids {
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
let network_path = &network_path;
let Some(node) = document.network_interface.nested_network(network_path).unwrap().nodes.get(node_id).cloned() else {
log::error!("could not get node in deserialize_document");
continue;
};
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` for manual composition
if node.manual_composition == Some(graph_craft::concrete!(())) || node.manual_composition == Some(graph_craft::concrete!(graphene_std::transform::Footprint)) {
document
.network_interface
.set_manual_compostion(node_id, network_path, graph_craft::concrete!(graphene_std::Context).into());
}
let Some(node_metadata) = document.network_interface.network_metadata(network_path).unwrap().persistent_metadata.node_metadata.get(node_id) else {
log::error!("could not get node metadata for node {node_id} in deserialize_document");
continue;
};
let Some(ref reference) = node_metadata.persistent_metadata.reference.clone() else {
// TODO: Investigate if this should be an expected case, because currently it runs hundreds of times normally.
// TODO: Either delete the commented out error below if this is normal, or fix the underlying issue if this is not expected.
// log::error!("could not get reference in deserialize_document");
continue;
};
let inputs_count = node.inputs.len();
// Upgrade Fill nodes to the format change in #1778
if reference == "Fill" && inputs_count == 8 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
let Some(fill_type) = old_inputs[1].as_value().cloned() else { continue };
let TaggedValue::FillType(fill_type) = fill_type else { continue };
let Some(solid_color) = old_inputs[2].as_value().cloned() else { continue };
let TaggedValue::OptionalColor(solid_color) = solid_color else { continue };
let Some(gradient_type) = old_inputs[3].as_value().cloned() else { continue };
let TaggedValue::GradientType(gradient_type) = gradient_type else { continue };
let Some(start) = old_inputs[4].as_value().cloned() else { continue };
let TaggedValue::DVec2(start) = start else { continue };
let Some(end) = old_inputs[5].as_value().cloned() else { continue };
let TaggedValue::DVec2(end) = end else { continue };
let Some(transform) = old_inputs[6].as_value().cloned() else { continue };
let TaggedValue::DAffine2(transform) = transform else { continue };
let Some(positions) = old_inputs[7].as_value().cloned() else { continue };
let TaggedValue::GradientStops(positions) = positions else { continue };
let fill = match (fill_type, solid_color) {
(FillType::Solid, None) => Fill::None,
(FillType::Solid, Some(color)) => Fill::Solid(color),
(FillType::Gradient, _) => Fill::Gradient(Gradient {
stops: positions,
gradient_type,
start,
end,
transform,
}),
};
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Fill(fill.clone()), false), network_path);
match fill {
Fill::None => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(None), false), network_path);
}
Fill::Solid(color) => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(Some(color)), false), network_path);
}
Fill::Gradient(gradient) => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(gradient), false), network_path);
}
}
}
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
if reference == "Stroke" && inputs_count == 8 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document.network_interface.insert_input_properties_row(node_id, 8, network_path);
document.network_interface.insert_input_properties_row(node_id, 9, network_path);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false);
let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), align_input, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
}
// Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node
if reference == "Splines from Points" {
document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string()));
}
// Upgrade the old "Spline" node to the new "Spline" node
if reference == "Spline" {
// Retrieve the proto node identifier and verify it is the old "Spline" node, otherwise skip it if this is the new "Spline" node
let identifier = document
.network_interface
.implementation(node_id, network_path)
.and_then(|implementation| implementation.get_proto_node());
if identifier.map(|identifier| &identifier.name) != Some(&"graphene_core::vector::generator_nodes::SplineNode".into()) {
continue;
}
// Obtain the document node for the given node ID, extract the vector points, and create vector data from the list of points
let node = document.network_interface.document_node(node_id, network_path).unwrap();
let Some(TaggedValue::VecDVec2(points)) = node.inputs.get(1).and_then(|tagged_value| tagged_value.as_value()) else {
log::error!("The old Spline node's input at index 1 is not a TaggedValue::VecDVec2");
continue;
};
let vector_data = VectorData::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
// Retrieve the output connectors linked to the "Spline" node's output port
let spline_outputs = document
.network_interface
.outward_wires(network_path)
.unwrap()
.get(&OutputConnector::node(*node_id, 0))
.expect("Vec of InputConnector Spline node is connected to its output port 0.")
.clone();
// Get the node's current position in the graph
let Some(node_position) = document.network_interface.position(node_id, network_path) else {
log::error!("Could not get position of spline node.");
continue;
};
// Get the "Path" node definition and fill it in with the vector data and default vector modification
let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist.");
let path_node = path_node_type.node_template_input_override([
Some(NodeInput::value(TaggedValue::VectorData(VectorDataTable::new(vector_data)), true)),
Some(NodeInput::value(TaggedValue::VectorModification(Default::default()), false)),
]);
// Get the "Spline" node definition and wire it up with the "Path" node as input
let spline_node_type = resolve_document_node_type("Spline").expect("Spline node does not exist.");
let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]);
// Create a new node group with the "Path" and "Spline" nodes and generate new node IDs for them
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::<HashMap<_, _>>();
let new_spline_id = *new_ids.get(&NodeId(0)).unwrap();
let new_path_id = *new_ids.get(&NodeId(1)).unwrap();
// Remove the old "Spline" node from the document
document.network_interface.delete_nodes(vec![*node_id], false, network_path);
// Insert the new "Path" and "Spline" nodes into the network interface with generated IDs
document.network_interface.insert_node_group(nodes.clone(), new_ids, network_path);
// Reposition the new "Spline" node to match the original "Spline" node's position
document.network_interface.shift_node(&new_spline_id, node_position, network_path);
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position
document.network_interface.shift_node(&new_path_id, node_position + IVec2::new(-7, 0), network_path);
// Redirect each output connection from the old node to the new "Spline" node's output port
for input_connector in spline_outputs {
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
}
}
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
if reference == "Text" && inputs_count != 8 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 4),
if inputs_count == 6 {
old_inputs[4].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 5),
if inputs_count == 6 {
old_inputs[5].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 6),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 7),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
network_path,
);
}
// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
if (reference == "Sine" || reference == "Cosine" || reference == "Tangent") && inputs_count == 1 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path);
}
// Upgrade the Modulo node to include a boolean input for whether the output should be always positive, which was previously not an option
if reference == "Modulo" && inputs_count == 2 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
}
// Upgrade the Mirror node to add the `keep_original` boolean input
if reference == "Mirror" && inputs_count == 3 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), network_path);
}
// Upgrade the Mirror node to add the `reference_point` input and change `offset` from `DVec2` to `f64`
if reference == "Mirror" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let Some(&TaggedValue::DVec2(old_offset)) = old_inputs[1].as_value() else { return };
let old_offset = if old_offset.x.abs() > old_offset.y.abs() { old_offset.x } else { old_offset.y };
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 1),
NodeInput::value(TaggedValue::ReferencePoint(graphene_std::transform::ReferencePoint::Center), false),
network_path,
);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(old_offset), false), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path);
}
// Upgrade artboard name being passed as hidden value input to "To Artboard"
if reference == "Artboard" && reset_node_definitions_on_open {
let label = document.network_interface.display_name(node_id, network_path);
document
.network_interface
.set_input(&InputConnector::node(NodeId(0), 1), NodeInput::value(TaggedValue::String(label), false), &[*node_id]);
}
if reference == "Image" && inputs_count == 1 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_image_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, new_image_node.document_node.implementation);
// Insert a new empty input for the image
document.network_interface.add_import(TaggedValue::None, false, 0, "Empty", "", &[*node_id]);
document.network_interface.set_reference(node_id, network_path, Some("Image".to_string()));
}
if reference == "Noise Pattern" && inputs_count == 15 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_noise_pattern_node = node_definition.default_node_template();
document
.network_interface
.replace_implementation(node_id, network_path, new_noise_pattern_node.document_node.implementation);
let old_inputs = document.network_interface.replace_inputs(node_id, new_noise_pattern_node.document_node.inputs.clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path);
for (i, input) in old_inputs.iter().enumerate() {
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), input.clone(), network_path);
}
}
if reference == "Instance on Points" && inputs_count == 2 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
}
if reference == "Morph" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
// We have removed the last input, so we don't add index 3
}
if reference == "Brush" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
// We have removed the second input ("bounds"), so we don't add index 1 and we shift the rest of the inputs down by one
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[3].clone(), network_path);
}
if reference == "Flatten Vector Elements" {
let node_definition = resolve_document_node_type("Flatten Path").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Flatten Path".to_string());
}
if reference == "Remove Handles" {
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::F64(0.), false), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
}
if reference == "Generate Handles" {
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(true), false), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
}
if reference == "Merge by Distance" && inputs_count == 2 {
let node_definition = resolve_document_node_type("Merge by Distance").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 2),
NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Topological), false),
network_path,
);
}
if reference == "Spatial Merge by Distance" {
let node_definition = resolve_document_node_type("Merge by Distance").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 2),
NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Spatial), false),
network_path,
);
document.network_interface.replace_reference_name(node_id, network_path, "Merge by Distance".to_string());
}
if reference == "Sample Points" && inputs_count == 5 {
let node_definition = resolve_document_node_type("Sample Polyline").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let new_spacing_value = NodeInput::value(TaggedValue::PointSpacingType(graphene_std::vector::misc::PointSpacingType::Separation), false);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), new_spacing_value, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[4].clone(), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Sample Polyline".to_string());
}
}
// Ensure layers are positioned as stacks if they are upstream siblings of another layer
document.network_interface.load_structure();
let all_layers = LayerNodeIdentifier::ROOT_PARENT.descendants(document.network_interface.document_metadata()).collect::<Vec<_>>();
for layer in all_layers {
let Some((downstream_node, input_index)) = document
.network_interface
.outward_wires(&[])
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(layer.to_node(), 0)))
.and_then(|outward_wires| outward_wires.first())
.and_then(|input_connector| input_connector.node_id().map(|node_id| (node_id, input_connector.input_index())))
else {
continue;
};
// If the downstream node is a layer and the input is the first input and the current layer is not in a stack
if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && !document.network_interface.is_stack(&layer.to_node(), &[]) {
// Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files
let (Some(layer_position), Some(downstream_position)) = (document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[])) else {
log::error!("Could not get position for layer {:?} or downstream node {} when opening file", layer.to_node(), downstream_node);
continue;
};
if layer_position.x == downstream_position.x {
document.network_interface.set_stack_position_calculated_offset(&layer.to_node(), &downstream_node, &[]);
}
}
}
}
@@ -4,7 +4,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene_std::vector::misc::BooleanOperation; use graphene_std::path_bool::BooleanOperation;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct MenuBarMessageHandler { pub struct MenuBarMessageHandler {
+1
View File
@@ -2,6 +2,7 @@ mod portfolio_message;
mod portfolio_message_handler; mod portfolio_message_handler;
pub mod document; pub mod document;
pub mod document_migration;
pub mod menu_bar; pub mod menu_bar;
pub mod spreadsheet; pub mod spreadsheet;
pub mod utility_types; pub mod utility_types;
@@ -54,9 +54,6 @@ pub enum PortfolioMessage {
preview_url: String, preview_url: String,
data: Vec<u8>, data: Vec<u8>,
}, },
// ImaginateCheckServerStatus,
// ImaginatePollServerStatus,
// ImaginateServerHostname,
Import, Import,
LoadDocumentResources { LoadDocumentResources {
document_id: DocumentId, document_id: DocumentId,
@@ -1,5 +1,5 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier; use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface::{self, InputConnector, OutputConnector}; use super::document::utility_types::network_interface;
use super::spreadsheet::SpreadsheetMessageHandler; use super::spreadsheet::SpreadsheetMessageHandler;
use super::utility_types::{PanelType, PersistentData}; use super::utility_types::{PanelType, PersistentData};
use crate::application::generate_uuid; use crate::application::generate_uuid;
@@ -11,21 +11,17 @@ use crate::messages::frontend::utility_types::FrontendDocumentDetails;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::DocumentMessageData; use crate::messages::portfolio::document::DocumentMessageData;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
use crate::messages::portfolio::document_migration::*;
use crate::messages::preferences::SelectionMode; use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use bezier_rs::Subpath; use glam::{DAffine2, DVec2};
use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::Font;
use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAlign};
use graphene_std::vector::{VectorData, VectorDataTable};
use std::vec; use std::vec;
pub struct PortfolioMessageData<'a> { pub struct PortfolioMessageData<'a> {
@@ -332,35 +328,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
} }
// PortfolioMessage::ImaginateCheckServerStatus => {
// let server_status = self.persistent_data.imaginate.server_status().clone();
// self.persistent_data.imaginate.poll_server_check();
// #[cfg(target_arch = "wasm32")]
// if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() {
// wasm_bindgen_futures::spawn_local(async move {
// let () = fut.await;
// use wasm_bindgen::prelude::*;
// #[wasm_bindgen(module = "/../frontend/src/editor.ts")]
// extern "C" {
// #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
// fn inject();
// }
// inject();
// })
// }
// if &server_status != self.persistent_data.imaginate.server_status() {
// responses.add(PropertiesPanelMessage::Refresh);
// }
// }
// PortfolioMessage::ImaginatePollServerStatus => {
// self.persistent_data.imaginate.poll_server_check();
// responses.add(PropertiesPanelMessage::Refresh);
// }
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()), PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
// PortfolioMessage::ImaginateServerHostname => {
// self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
// }
PortfolioMessage::Import => { PortfolioMessage::Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages // This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
responses.add(FrontendMessage::TriggerImport); responses.add(FrontendMessage::TriggerImport);
@@ -429,30 +397,18 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
document_serialized_content, document_serialized_content,
to_front, to_front,
} => { } => {
// TODO: Eventually remove this document upgrade code // Upgrade the document being opened to use fresh copies of all nodes
// This big code block contains lots of hacky code for upgrading old documents to the new format let reset_node_definitions_on_open = reset_node_definitions_on_open || document_migration_reset_node_definition(&document_serialized_content);
// Upgrade the document being opened with string replacements on the original JSON
// Upgrade a document being opened to use fresh copies of all nodes let document_serialized_content = document_migration_string_preprocessing(document_serialized_content);
let replace_implementations_from_definition = reset_node_definitions_on_open || document_serialized_content.contains("node_output_index");
// Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946 (see also `fn fix_nodes()` in `main.rs` of Graphene CLI)
let upgrade_from_before_returning_nested_click_targets =
document_serialized_content.contains("graphene_core::ConstructLayerNode") || document_serialized_content.contains("graphene_core::AddArtboardNode");
let upgrade_vector_manipulation_format = document_serialized_content.contains("ManipulatorGroupIds") && !document_name.contains("__DO_NOT_UPGRADE__");
let document_name = document_name.replace("__DO_NOT_UPGRADE__", "");
const TEXT_REPLACEMENTS: [(&str, &str); 2] = [
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePointsNode"),
("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"),
];
let document_serialized_content = TEXT_REPLACEMENTS
.iter()
.fold(document_serialized_content, |document_serialized_content, (old, new)| document_serialized_content.replace(old, new));
// Deserialize the document
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| { let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| {
document.name.clone_from(&document_name); document.name.clone_from(&document_name);
document document
}); });
// Display an error to the user if the document could not be opened
let mut document = match document { let mut document = match document {
Ok(document) => document, Ok(document) => document,
Err(e) => { Err(e) => {
@@ -467,660 +423,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
} }
}; };
const REPLACEMENTS: [(&str, &str); 40] = [ // Upgrade the document's nodes to be compatible with the latest version
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"), document_migration_upgrades(&mut document, reset_node_definitions_on_open);
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
("graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode"),
("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"),
("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"),
("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"),
("graphene_core::ops::ConstructVector2", "graphene_core::ops::CoordinateValueNode"),
("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"),
("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"),
("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"),
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode"),
("graphene_core::raster::ExposureNode", "graphene_core::raster::adjustments::ExposureNode"),
("graphene_core::raster::ExtractChannelNode", "graphene_core::raster::adjustments::ExtractChannelNode"),
("graphene_core::raster::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode"),
("graphene_core::raster::HueSaturationNode", "graphene_core::raster::adjustments::HueSaturationNode"),
("graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::AutoTangentsNode"),
("graphene_core::vector::RemoveHandlesNode", "graphene_core::vector::AutoTangentsNode"),
("graphene_core::raster::InvertNode", "graphene_core::raster::adjustments::InvertNode"),
("graphene_core::raster::InvertRGBNode", "graphene_core::raster::adjustments::InvertNode"),
("graphene_core::raster::LevelsNode", "graphene_core::raster::adjustments::LevelsNode"),
("graphene_core::raster::LuminanceNode", "graphene_core::raster::adjustments::LuminanceNode"),
("graphene_core::raster::ExtractOpaqueNode", "graphene_core::raster::adjustments::MakeOpaqueNode"),
("graphene_core::raster::PosterizeNode", "graphene_core::raster::adjustments::PosterizeNode"),
("graphene_core::raster::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode"),
("graphene_core::raster::VibranceNode", "graphene_core::raster::adjustments::VibranceNode"),
("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"),
("graphene_core::transform::SetTransformNode", "graphene_core::transform::ReplaceTransformNode"),
("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"),
("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"),
("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"),
("graphene_core::vector::generator_nodes::RectangleGenerator", "graphene_core::vector::generator_nodes::RectangleNode"),
(
"graphene_core::vector::generator_nodes::RegularPolygonGenerator",
"graphene_core::vector::generator_nodes::RegularPolygonNode",
),
("graphene_core::vector::generator_nodes::StarGenerator", "graphene_core::vector::generator_nodes::StarNode"),
("graphene_std::executor::BlendGpuImageNode", "graphene_std::gpu_nodes::BlendGpuImageNode"),
("graphene_std::raster::SampleNode", "graphene_std::raster::SampleImageNode"),
("graphene_core::transform::CullNode", "graphene_core::ops::IdentityNode"),
("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"),
("graphene_core::vector::FlattenVectorElementsNode", "graphene_core::vector::FlattenPathNode"),
];
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
// Apply upgrades to each node
for (node_id, path) in &node_ids {
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
if let Some(DocumentNodeImplementation::ProtoNode(protonode_id)) = document
.network_interface
.nested_network(&network_path)
.unwrap()
.nodes
.get(node_id)
.map(|node| node.implementation.clone())
{
for (old, new) in REPLACEMENTS {
let node_path_without_type_args = protonode_id.name.split('<').next();
if node_path_without_type_args == Some(old) {
document
.network_interface
.replace_implementation(node_id, &network_path, DocumentNodeImplementation::ProtoNode(new.to_string().into()));
document.network_interface.set_manual_compostion(node_id, &network_path, Some(graph_craft::Type::Generic("T".into())));
}
}
}
}
// Upgrade all old nodes to support editable subgraphs introduced in #1750
if replace_implementations_from_definition || upgrade_from_before_returning_nested_click_targets {
// This can be used, if uncommented, to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
for node_id in &document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.keys()
.cloned()
.collect::<Vec<NodeId>>()
{
if let Some(reference) = document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.get(node_id)
.and_then(|node| node.persistent_metadata.reference.as_ref())
{
let Some(node_definition) = resolve_document_node_type(reference) else { continue };
let default_definition_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, &[], default_definition_node.document_node.implementation);
document
.network_interface
.replace_implementation_metadata(node_id, &[], default_definition_node.persistent_node_metadata);
document.network_interface.set_manual_compostion(node_id, &[], default_definition_node.document_node.manual_composition);
}
}
}
if document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.iter()
.any(|(node_id, node)| node.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Output") && *node_id == NodeId(0))
{
document.network_interface.delete_nodes(vec![NodeId(0)], true, &[]);
}
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
// Apply upgrades to each node
for (node_id, path) in &node_ids {
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
let network_path = &network_path;
let Some(node) = document.network_interface.nested_network(network_path).unwrap().nodes.get(node_id).cloned() else {
log::error!("could not get node in deserialize_document");
continue;
};
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` for manual composition
if node.manual_composition == Some(graph_craft::concrete!(())) || node.manual_composition == Some(graph_craft::concrete!(graphene_std::transform::Footprint)) {
document
.network_interface
.set_manual_compostion(node_id, network_path, graph_craft::concrete!(graphene_std::Context).into());
}
let Some(node_metadata) = document.network_interface.network_metadata(network_path).unwrap().persistent_metadata.node_metadata.get(node_id) else {
log::error!("could not get node metadata for node {node_id} in deserialize_document");
continue;
};
let Some(ref reference) = node_metadata.persistent_metadata.reference.clone() else {
// TODO: Investigate if this should be an expected case, because currently it runs hundreds of times normally.
// TODO: Either delete the commented out error below if this is normal, or fix the underlying issue if this is not expected.
// log::error!("could not get reference in deserialize_document");
continue;
};
let inputs_count = node.inputs.len();
// Upgrade Fill nodes to the format change in #1778
if reference == "Fill" && inputs_count == 8 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
let Some(fill_type) = old_inputs[1].as_value().cloned() else { continue };
let TaggedValue::FillType(fill_type) = fill_type else { continue };
let Some(solid_color) = old_inputs[2].as_value().cloned() else { continue };
let TaggedValue::OptionalColor(solid_color) = solid_color else { continue };
let Some(gradient_type) = old_inputs[3].as_value().cloned() else { continue };
let TaggedValue::GradientType(gradient_type) = gradient_type else { continue };
let Some(start) = old_inputs[4].as_value().cloned() else { continue };
let TaggedValue::DVec2(start) = start else { continue };
let Some(end) = old_inputs[5].as_value().cloned() else { continue };
let TaggedValue::DVec2(end) = end else { continue };
let Some(transform) = old_inputs[6].as_value().cloned() else { continue };
let TaggedValue::DAffine2(transform) = transform else { continue };
let Some(positions) = old_inputs[7].as_value().cloned() else { continue };
let TaggedValue::GradientStops(positions) = positions else { continue };
let fill = match (fill_type, solid_color) {
(FillType::Solid, None) => Fill::None,
(FillType::Solid, Some(color)) => Fill::Solid(color),
(FillType::Gradient, _) => Fill::Gradient(Gradient {
stops: positions,
gradient_type,
start,
end,
transform,
}),
};
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Fill(fill.clone()), false), network_path);
match fill {
Fill::None => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(None), false), network_path);
}
Fill::Solid(color) => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(Some(color)), false), network_path);
}
Fill::Gradient(gradient) => {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(gradient), false), network_path);
}
}
}
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
if reference == "Stroke" && inputs_count == 8 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document.network_interface.insert_input_properties_row(node_id, 8, network_path);
document.network_interface.insert_input_properties_row(node_id, 9, network_path);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false);
let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), align_input, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
}
// Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node
if reference == "Splines from Points" {
document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string()));
}
// Upgrade the old "Spline" node to the new "Spline" node
if reference == "Spline" {
// Retrieve the proto node identifier and verify it is the old "Spline" node, otherwise skip it if this is the new "Spline" node
let identifier = document
.network_interface
.implementation(node_id, network_path)
.and_then(|implementation| implementation.get_proto_node());
if identifier.map(|identifier| &identifier.name) != Some(&"graphene_core::vector::generator_nodes::SplineNode".into()) {
continue;
}
// Obtain the document node for the given node ID, extract the vector points, and create vector data from the list of points
let node = document.network_interface.document_node(node_id, network_path).unwrap();
let Some(TaggedValue::VecDVec2(points)) = node.inputs.get(1).and_then(|tagged_value| tagged_value.as_value()) else {
log::error!("The old Spline node's input at index 1 is not a TaggedValue::VecDVec2");
continue;
};
let vector_data = VectorData::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
// Retrieve the output connectors linked to the "Spline" node's output port
let spline_outputs = document
.network_interface
.outward_wires(network_path)
.unwrap()
.get(&OutputConnector::node(*node_id, 0))
.expect("Vec of InputConnector Spline node is connected to its output port 0.")
.clone();
// Get the node's current position in the graph
let Some(node_position) = document.network_interface.position(node_id, network_path) else {
log::error!("Could not get position of spline node.");
continue;
};
// Get the "Path" node definition and fill it in with the vector data and default vector modification
let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist.");
let path_node = path_node_type.node_template_input_override([
Some(NodeInput::value(TaggedValue::VectorData(VectorDataTable::new(vector_data)), true)),
Some(NodeInput::value(TaggedValue::VectorModification(Default::default()), false)),
]);
// Get the "Spline" node definition and wire it up with the "Path" node as input
let spline_node_type = resolve_document_node_type("Spline").expect("Spline node does not exist.");
let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]);
// Create a new node group with the "Path" and "Spline" nodes and generate new node IDs for them
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::<HashMap<_, _>>();
let new_spline_id = *new_ids.get(&NodeId(0)).unwrap();
let new_path_id = *new_ids.get(&NodeId(1)).unwrap();
// Remove the old "Spline" node from the document
document.network_interface.delete_nodes(vec![*node_id], false, network_path);
// Insert the new "Path" and "Spline" nodes into the network interface with generated IDs
document.network_interface.insert_node_group(nodes.clone(), new_ids, network_path);
// Reposition the new "Spline" node to match the original "Spline" node's position
document.network_interface.shift_node(&new_spline_id, node_position, network_path);
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position
document.network_interface.shift_node(&new_path_id, node_position + IVec2::new(-7, 0), network_path);
// Redirect each output connection from the old node to the new "Spline" node's output port
for input_connector in spline_outputs {
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
}
}
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
if reference == "Text" && inputs_count != 8 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 4),
if inputs_count == 6 {
old_inputs[4].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 5),
if inputs_count == 6 {
old_inputs[5].clone()
} else {
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 6),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 7),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
network_path,
);
}
// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
if (reference == "Sine" || reference == "Cosine" || reference == "Tangent") && inputs_count == 1 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path);
}
// Upgrade the Modulo node to include a boolean input for whether the output should be always positive, which was previously not an option
if reference == "Modulo" && inputs_count == 2 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
}
// Upgrade the Mirror node to add the `keep_original` boolean input
if reference == "Mirror" && inputs_count == 3 {
let node_definition = resolve_document_node_type(reference).unwrap();
let document_node = node_definition.default_node_template().document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), network_path);
}
// Upgrade the Mirror node to add the `reference_point` input and change `offset` from `DVec2` to `f64`
if reference == "Mirror" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
let Some(&TaggedValue::DVec2(old_offset)) = old_inputs[1].as_value() else { return };
let old_offset = if old_offset.x.abs() > old_offset.y.abs() { old_offset.x } else { old_offset.y };
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 1),
NodeInput::value(TaggedValue::ReferencePoint(graphene_std::transform::ReferencePoint::Center), false),
network_path,
);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(old_offset), false), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path);
}
// Upgrade artboard name being passed as hidden value input to "To Artboard"
if reference == "Artboard" && upgrade_from_before_returning_nested_click_targets {
let label = document.network_interface.display_name(node_id, network_path);
document
.network_interface
.set_input(&InputConnector::node(NodeId(0), 1), NodeInput::value(TaggedValue::String(label), false), &[*node_id]);
}
if reference == "Image" && inputs_count == 1 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_image_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, new_image_node.document_node.implementation);
// Insert a new empty input for the image
document.network_interface.add_import(TaggedValue::None, false, 0, "Empty", "", &[*node_id]);
document.network_interface.set_reference(node_id, network_path, Some("Image".to_string()));
}
if reference == "Noise Pattern" && inputs_count == 15 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_noise_pattern_node = node_definition.default_node_template();
document
.network_interface
.replace_implementation(node_id, network_path, new_noise_pattern_node.document_node.implementation);
let old_inputs = document.network_interface.replace_inputs(node_id, new_noise_pattern_node.document_node.inputs.clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path);
for (i, input) in old_inputs.iter().enumerate() {
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), input.clone(), network_path);
}
}
if reference == "Instance on Points" && inputs_count == 2 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
}
if reference == "Morph" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
// We have removed the last input, so we don't add index 3
}
if reference == "Brush" && inputs_count == 4 {
let node_definition = resolve_document_node_type(reference).unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
// We have removed the second input ("bounds"), so we don't add index 1 and we shift the rest of the inputs down by one
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[3].clone(), network_path);
}
if reference == "Flatten Vector Elements" {
let node_definition = resolve_document_node_type("Flatten Path").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Flatten Path".to_string());
}
if reference == "Remove Handles" {
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::F64(0.), false), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
}
if reference == "Generate Handles" {
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(true), false), network_path);
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
}
if reference == "Merge by Distance" && inputs_count == 2 {
let node_definition = resolve_document_node_type("Merge by Distance").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 2),
NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Topological), false),
network_path,
);
}
if reference == "Spatial Merge by Distance" {
let node_definition = resolve_document_node_type("Merge by Distance").unwrap();
let new_node_template = node_definition.default_node_template();
let document_node = new_node_template.document_node;
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
document
.network_interface
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 2),
NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Spatial), false),
network_path,
);
document.network_interface.replace_reference_name(node_id, network_path, "Merge by Distance".to_string());
}
}
// TODO: Eventually remove this document upgrade code
// Upgrade document to the new vector manipulation format introduced in #1676
let document_serialized_content = document.serialize_document();
if upgrade_vector_manipulation_format && !document_serialized_content.is_empty() {
responses.add(FrontendMessage::TriggerUpgradeDocumentToVectorManipulationFormat {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
});
return;
}
// Ensure layers are positioned as stacks if they upstream siblings of another layer
document.network_interface.load_structure();
let all_layers = LayerNodeIdentifier::ROOT_PARENT.descendants(document.network_interface.document_metadata()).collect::<Vec<_>>();
for layer in all_layers {
let Some((downstream_node, input_index)) = document
.network_interface
.outward_wires(&[])
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(layer.to_node(), 0)))
.and_then(|outward_wires| outward_wires.first())
.and_then(|input_connector| input_connector.node_id().map(|node_id| (node_id, input_connector.input_index())))
else {
continue;
};
// If the downstream node is a layer and the input is the first input and the current layer is not in a stack
if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && !document.network_interface.is_stack(&layer.to_node(), &[]) {
// Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files
let (Some(layer_position), Some(downstream_position)) =
(document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[]))
else {
log::error!("Could not get position for layer {:?} or downstream node {} when opening file", layer.to_node(), downstream_node);
continue;
};
if layer_position.x == downstream_position.x {
document.network_interface.set_stack_position_calculated_offset(&layer.to_node(), &downstream_node, &[]);
}
}
}
// Set the save state of the document based on what's given to us by the caller to this message
document.set_auto_save_state(document_is_auto_saved); document.set_auto_save_state(document_is_auto_saved);
document.set_save_state(document_is_saved); document.set_save_state(document_is_saved);
// Load the document into the portfolio so it opens in the editor
self.load_document(document, document_id, responses, to_front); self.load_document(document, document_id, responses, to_front);
} }
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => { PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
@@ -4,7 +4,6 @@ use graphene_std::text::FontCache;
pub struct PersistentData { pub struct PersistentData {
pub font_cache: FontCache, pub font_cache: FontCache,
pub use_vello: bool, pub use_vello: bool,
// pub imaginate: ImaginatePersistentData,
} }
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
@@ -16,6 +16,4 @@ pub enum PreferencesMessage {
ModifyLayout { zoom_with_scroll: bool }, ModifyLayout { zoom_with_scroll: bool },
GraphWireStyle { style: GraphWireStyle }, GraphWireStyle { style: GraphWireStyle },
ViewportZoomWheelRate { rate: f64 }, ViewportZoomWheelRate { rate: f64 },
// ImaginateRefreshFrequency { seconds: f64 },
// ImaginateServerHostname { hostname: String },
} }
@@ -7,8 +7,6 @@ use graph_craft::wasm_application_io::EditorPreferences;
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct PreferencesMessageHandler { pub struct PreferencesMessageHandler {
// pub imaginate_server_hostname: String,
// pub imaginate_refresh_frequency: f64,
pub selection_mode: SelectionMode, pub selection_mode: SelectionMode,
pub zoom_with_scroll: bool, pub zoom_with_scroll: bool,
pub use_vello: bool, pub use_vello: bool,
@@ -24,7 +22,6 @@ impl PreferencesMessageHandler {
pub fn editor_preferences(&self) -> EditorPreferences { pub fn editor_preferences(&self) -> EditorPreferences {
EditorPreferences { EditorPreferences {
// imaginate_hostname: self.imaginate_server_hostname.clone(),
use_vello: self.use_vello && self.supports_wgpu(), use_vello: self.use_vello && self.supports_wgpu(),
} }
} }
@@ -37,8 +34,6 @@ impl PreferencesMessageHandler {
impl Default for PreferencesMessageHandler { impl Default for PreferencesMessageHandler {
fn default() -> Self { fn default() -> Self {
Self { Self {
// imaginate_server_hostname: EditorPreferences::default().imaginate_hostname,
// imaginate_refresh_frequency: 1.,
selection_mode: SelectionMode::Touched, selection_mode: SelectionMode::Touched,
zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll), zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll),
use_vello: EditorPreferences::default().use_vello, use_vello: EditorPreferences::default().use_vello,
@@ -57,10 +52,6 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) { if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
*self = deserialized_preferences; *self = deserialized_preferences;
// TODO: Reenable when Imaginate is restored
// responses.add(PortfolioMessage::ImaginateServerHostname);
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
responses.add(PortfolioMessage::EditorPreferences); responses.add(PortfolioMessage::EditorPreferences);
responses.add(PortfolioMessage::UpdateVelloPreference); responses.add(PortfolioMessage::UpdateVelloPreference);
responses.add(PreferencesMessage::ModifyLayout { responses.add(PreferencesMessage::ModifyLayout {
@@ -101,27 +92,6 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
self.viewport_zoom_wheel_rate = rate; self.viewport_zoom_wheel_rate = rate;
} }
} }
// TODO: Reenable when Imaginate is restored (and move back up one line since the auto-formatter doesn't like it in that block)
// PreferencesMessage::ImaginateRefreshFrequency { seconds } => {
// self.imaginate_refresh_frequency = seconds;
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
// responses.add(PortfolioMessage::EditorPreferences);
// }
// PreferencesMessage::ImaginateServerHostname { hostname } => {
// let initial = hostname.clone();
// let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://");
// let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname };
// let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" };
// if hostname != initial {
// refresh_dialog(responses);
// }
// self.imaginate_server_hostname = hostname;
// responses.add(PortfolioMessage::ImaginateServerHostname);
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
// responses.add(PortfolioMessage::EditorPreferences);
//}
responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() }); responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() });
} }
-1
View File
@@ -38,7 +38,6 @@ pub use crate::messages::tool::tool_messages::eyedropper_tool::{EyedropperToolMe
pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant};
// pub use crate::messages::tool::tool_messages::imaginate_tool::{ImaginateToolMessage, ImaginateToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant};
pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant};
@@ -0,0 +1,246 @@
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
use glam::DVec2;
use std::collections::VecDeque;
/// A unified enum wrapper around all available shape-specific gizmo handlers.
///
/// This abstraction allows `GizmoManager` to interact with different shape gizmos (like Star or Polygon)
/// using a common interface without needing to know the specific shape type at compile time.
///
/// Each variant stores a concrete handler (e.g., `StarGizmoHandler`, `PolygonGizmoHandler`) that implements
/// the shape-specific logic for rendering overlays, responding to input, and modifying shape parameters.
#[derive(Clone, Debug, Default)]
pub enum ShapeGizmoHandlers {
#[default]
None,
Star(StarGizmoHandler),
Polygon(PolygonGizmoHandler),
}
impl ShapeGizmoHandlers {
/// Returns the kind of shape the handler is managing, such as `"star"` or `"polygon"`.
/// Used for grouping logic and distinguishing between handler types at runtime.
pub fn kind(&self) -> &'static str {
match self {
Self::Star(_) => "star",
Self::Polygon(_) => "polygon",
Self::None => "none",
}
}
/// Dispatches interaction state updates to the corresponding shape-specific handler.
pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
match self {
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
Self::None => {}
}
}
/// Checks if any interactive part of the gizmo is currently hovered.
pub fn is_any_gizmo_hovered(&self) -> bool {
match self {
Self::Star(h) => h.is_any_gizmo_hovered(),
Self::Polygon(h) => h.is_any_gizmo_hovered(),
Self::None => false,
}
}
/// Passes the click interaction to the appropriate gizmo handler if one is hovered.
pub fn handle_click(&mut self) {
match self {
Self::Star(h) => h.handle_click(),
Self::Polygon(h) => h.handle_click(),
Self::None => {}
}
}
/// Updates the gizmo state while the user is dragging a handle (e.g., adjusting radius).
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
match self {
Self::Star(h) => h.handle_update(drag_start, document, input, responses),
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
Self::None => {}
}
}
/// Cleans up any state used by the gizmo handler.
pub fn cleanup(&mut self) {
match self {
Self::Star(h) => h.cleanup(),
Self::Polygon(h) => h.cleanup(),
Self::None => {}
}
}
/// Draws overlays like control points or outlines for the shape handled by this gizmo.
pub fn overlays(
&self,
document: &DocumentMessageHandler,
layer: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
match self {
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
/// Draws live-updating overlays during drag interactions for the shape handled by this gizmo.
pub fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
match self {
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
}
/// Central manager that coordinates shape gizmo handlers for interactive editing on the canvas.
///
/// The `GizmoManager` is responsible for detecting which shapes are selected, activating the appropriate
/// shape-specific gizmo, and routing user interactions (hover, click, drag) to the correct handler.
/// It allows editing multiple shapes of the same type or focusing on a single active shape when a gizmo is hovered.
///
/// ## Responsibilities:
/// - Detect which selected layers support shape gizmos (e.g., stars, polygons)
/// - Activate the correct handler and manage state between frames
/// - Route click, hover, and drag events to the proper shape gizmo
/// - Render overlays and dragging visuals
#[derive(Clone, Debug, Default)]
pub struct GizmoManager {
active_shape_handler: Option<ShapeGizmoHandlers>,
layers_handlers: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)>,
}
impl GizmoManager {
/// Detects and returns a shape gizmo handler based on the layer type (e.g., star, polygon).
///
/// Returns `None` if the given layer does not represent a shape with a registered gizmo.
pub fn detect_shape_handler(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<ShapeGizmoHandlers> {
// Star
if graph_modification_utils::get_star_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Star(StarGizmoHandler::default()));
}
// Polygon
if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
}
None
}
/// Returns `true` if a gizmo is currently active (hovered or being interacted with).
pub fn hovering_over_gizmo(&self) -> bool {
self.active_shape_handler.is_some()
}
/// Called every frame to check selected layers and update the active shape gizmo, if hovered.
///
/// Also groups all shape layers with the same kind of gizmo to support overlays for multi-shape editing.
pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let mut handlers_layer: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)> = Vec::new();
for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) {
if let Some(mut handler) = Self::detect_shape_handler(layer, document) {
handler.handle_state(layer, mouse_position, document, responses);
let is_hovered = handler.is_any_gizmo_hovered();
if is_hovered {
self.layers_handlers.clear();
self.active_shape_handler = Some(handler);
return;
}
// Try to group this handler with others of the same type
if let Some((_, layers)) = handlers_layer.iter_mut().find(|(existing_handler, _)| existing_handler.kind() == handler.kind()) {
layers.push(layer);
} else {
handlers_layer.push((handler, vec![layer]));
}
}
}
self.layers_handlers = handlers_layer;
self.active_shape_handler = None;
}
/// Handles click interactions if a gizmo is active. Returns `true` if a gizmo handled the click.
pub fn handle_click(&mut self) -> bool {
if let Some(handle) = &mut self.active_shape_handler {
handle.handle_click();
return true;
}
false
}
pub fn handle_cleanup(&mut self) {
if let Some(handle) = &mut self.active_shape_handler {
handle.cleanup();
}
}
/// Passes drag update data to the active gizmo to update shape parameters live.
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if let Some(handle) = &mut self.active_shape_handler {
handle.handle_update(drag_start, document, input, responses);
}
}
/// Draws overlays for the currently active shape gizmo during a drag interaction.
pub fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if let Some(handle) = &self.active_shape_handler {
handle.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context);
}
}
/// Draws overlays for either the active gizmo (if hovered) or all grouped selected gizmos.
///
/// If no single gizmo is active, it renders overlays for all grouped layers with associated handlers.
pub fn overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if let Some(handler) = &self.active_shape_handler {
handler.overlays(document, None, input, shape_editor, mouse_position, overlay_context);
return;
}
for (handler, selected_layers) in &self.layers_handlers {
for layer in selected_layers {
handler.overlays(document, Some(*layer), input, shape_editor, mouse_position, overlay_context);
}
}
}
}
@@ -0,0 +1,2 @@
pub mod gizmo_manager;
pub mod shape_gizmos;
@@ -0,0 +1,2 @@
pub mod number_of_points_dial;
pub mod point_radius_handle;
@@ -0,0 +1,209 @@
use crate::consts::{GIZMO_HIDE_THRESHOLD, NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION, NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::Responses;
use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_polygon_parameters, inside_polygon, inside_star, polygon_outline, polygon_vertex_position, star_outline};
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_star_parameters, star_vertex_position};
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
use std::f64::consts::TAU;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum NumberOfPointsDialState {
#[default]
Inactive,
Hover,
Dragging,
}
#[derive(Clone, Debug, Default)]
pub struct NumberOfPointsDial {
pub layer: Option<LayerNodeIdentifier>,
pub initial_points: u32,
pub handle_state: NumberOfPointsDialState,
}
impl NumberOfPointsDial {
pub fn cleanup(&mut self) {
self.handle_state = NumberOfPointsDialState::Inactive;
self.layer = None;
}
pub fn update_state(&mut self, state: NumberOfPointsDialState) {
self.handle_state = state;
}
pub fn is_hovering(&self) -> bool {
self.handle_state == NumberOfPointsDialState::Hover
}
pub fn is_dragging(&self) -> bool {
self.handle_state == NumberOfPointsDialState::Dragging
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
match &self.handle_state {
NumberOfPointsDialState::Inactive => {
// Star
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let point_on_max_radius = star_vertex_position(viewport, 0, sides, radius1, radius2);
if mouse_position.distance(center) < NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.layer = Some(layer);
self.initial_points = sides;
self.update_state(NumberOfPointsDialState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
// Polygon
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let point_on_max_radius = polygon_vertex_position(viewport, 0, sides, radius);
if mouse_position.distance(center) < NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.layer = Some(layer);
self.initial_points = sides;
self.update_state(NumberOfPointsDialState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
}
NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging => {
let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if mouse_position.distance(center) > NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && matches!(&self.handle_state, NumberOfPointsDialState::Hover) {
self.update_state(NumberOfPointsDialState::Inactive);
self.layer = None;
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
}
}
}
}
pub fn overlays(&self, document: &DocumentMessageHandler, layer: Option<LayerNodeIdentifier>, shape_editor: &mut &mut ShapeState, mouse_position: DVec2, overlay_context: &mut OverlayContext) {
match &self.handle_state {
NumberOfPointsDialState::Inactive => {
let Some(layer) = layer else { return };
// Star
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let radius = radius1.max(radius2);
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
if closest_segment.layer() == layer {
return;
}
}
let point_on_max_radius = star_vertex_position(viewport, 0, sides, radius1, radius2);
if inside_star(viewport, sides, radius1, radius2, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.draw_spokes(center, viewport, sides, radius, overlay_context);
return;
}
}
// Polygon
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
if closest_segment.layer() == layer {
return;
}
}
let point_on_max_radius = polygon_vertex_position(viewport, 0, sides, radius);
if inside_polygon(viewport, sides, radius, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.draw_spokes(center, viewport, sides, radius, overlay_context);
}
}
}
NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging => {
let Some(layer) = self.layer else {
return;
};
// Get the star's greater radius or polygon's radius, as well as the number of sides
let Some((sides, radius)) = extract_star_parameters(Some(layer), document)
.map(|(sides, r1, r2)| (sides, r1.max(r2)))
.or_else(|| extract_polygon_parameters(Some(layer), document))
else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
// Draw either the star or polygon outline
star_outline(Some(layer), document, overlay_context);
polygon_outline(Some(layer), document, overlay_context);
self.draw_spokes(center, viewport, sides, radius, overlay_context);
}
}
}
fn draw_spokes(&self, center: DVec2, viewport: DAffine2, sides: u32, radius: f64, overlay_context: &mut OverlayContext) {
for i in 0..sides {
let angle = ((i as f64) * TAU) / (sides as f64);
let point = viewport.transform_point2(DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
});
let Some(direction) = (point - center).try_normalize() else { continue };
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
let end_point = direction * NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH;
if matches!(self.handle_state, NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging) {
overlay_context.line(center, end_point * NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION + center, None, None);
} else {
overlay_context.line(center, end_point + center, None, None);
}
}
}
pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start);
let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum();
let net_delta = (delta.length() / 25.).round() * sign;
let Some(layer) = self.layer else { return };
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
return;
};
let new_point_count = ((self.initial_points as i32) + (net_delta as i32)).max(3);
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::U32(new_point_count as u32), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
@@ -1,12 +1,15 @@
use crate::consts::{COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD, POINT_RADIUS_HANDLE_SNAP_THRESHOLD}; use crate::consts::GIZMO_HIDE_THRESHOLD;
use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message; use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector};
use crate::messages::prelude::FrontendMessage;
use crate::messages::prelude::Responses; use crate::messages::prelude::Responses;
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage}; use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer}; use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
use crate::messages::tool::common_functionality::shapes::shape_utility::{draw_snapping_ticks, extract_polygon_parameters, extract_star_parameters, polygon_vertex_position, star_vertex_position}; use crate::messages::tool::common_functionality::shapes::shape_utility::{draw_snapping_ticks, extract_polygon_parameters, polygon_outline, polygon_vertex_position, star_outline};
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_star_parameters, star_vertex_position};
use glam::DVec2; use glam::DVec2;
use graph_craft::document::NodeInput; use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
@@ -39,78 +42,71 @@ impl PointRadiusHandle {
self.layer = None; self.layer = None;
} }
pub fn is_inactive(&self) -> bool {
self.handle_state == PointRadiusHandleState::Inactive
}
pub fn hovered(&self) -> bool { pub fn hovered(&self) -> bool {
self.handle_state == PointRadiusHandleState::Hover self.handle_state == PointRadiusHandleState::Hover
} }
pub fn is_dragging_or_snapped(&self) -> bool {
self.handle_state == PointRadiusHandleState::Dragging || matches!(self.handle_state, PointRadiusHandleState::Snapped(_))
}
pub fn update_state(&mut self, state: PointRadiusHandleState) { pub fn update_state(&mut self, state: PointRadiusHandleState) {
self.handle_state = state; self.handle_state = state;
} }
pub fn handle_actions(&mut self, document: &DocumentMessageHandler, mouse_position: DVec2) { pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
match &self.handle_state { match &self.handle_state {
PointRadiusHandleState::Inactive => { PointRadiusHandleState::Inactive => {
for layer in document // Draw the point handle gizmo for the star shape
.network_interface if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
.selected_nodes() let viewport = document.metadata().transform_to_viewport(layer);
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter(|layer| {
graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some()
}) {
// Draw the point handle gizmo for the star shape
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..2 * n { for i in 0..2 * sides {
let (radius, radius_index) = if i % 2 == 0 { (radius1, 2) } else { (radius2, 3) }; let (radius, radius_index) = if i % 2 == 0 { (radius1, 2) } else { (radius2, 3) };
let point = star_vertex_position(viewport, i as i32, n, radius1, radius2); let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
let center = viewport.transform_point2(DVec2::ZERO); let center = viewport.transform_point2(DVec2::ZERO);
// If the user zooms out such that shape is very small hide the gizmo // If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD { if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return; return;
} }
if point.distance(mouse_position) < 5. { if point.distance(mouse_position) < 5. {
self.radius_index = radius_index; self.radius_index = radius_index;
self.layer = Some(layer); self.layer = Some(layer);
self.point = i; self.point = i;
self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index); self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index);
self.initial_radius = radius; self.initial_radius = radius;
self.update_state(PointRadiusHandleState::Hover); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
self.update_state(PointRadiusHandleState::Hover);
return; return;
}
} }
} }
}
// Draw the point handle gizmo for the polygon shape // Draw the point handle gizmo for the polygon shape
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) { if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer); let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..n { for i in 0..sides {
let point = polygon_vertex_position(viewport, i as i32, n, radius); let point = polygon_vertex_position(viewport, i as i32, sides, radius);
let center = viewport.transform_point2(DVec2::ZERO); let center = viewport.transform_point2(DVec2::ZERO);
// If the user zooms out so the shape is very small, hide the gizmo // If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD { if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return; return;
} }
if point.distance(mouse_position) < 5. { if point.distance(mouse_position) < 5. {
self.radius_index = 2; self.radius_index = 2;
self.layer = Some(layer); self.layer = Some(layer);
self.point = i; self.point = i;
self.snap_radii.clear(); self.snap_radii.clear();
self.initial_radius = radius; self.initial_radius = radius;
self.update_state(PointRadiusHandleState::Hover); self.update_state(PointRadiusHandleState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
return; return;
}
} }
} }
} }
@@ -121,8 +117,9 @@ impl PointRadiusHandle {
let viewport = document.metadata().transform_to_viewport(layer); let viewport = document.metadata().transform_to_viewport(layer);
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) { // Star
let point = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let point = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. { if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
self.update_state(PointRadiusHandleState::Inactive); self.update_state(PointRadiusHandleState::Inactive);
@@ -131,8 +128,9 @@ impl PointRadiusHandle {
} }
} }
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) { // Polygon
let point = polygon_vertex_position(viewport, self.point as i32, n, radius); if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let point = polygon_vertex_position(viewport, self.point as i32, sides, radius);
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. { if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
self.update_state(PointRadiusHandleState::Inactive); self.update_state(PointRadiusHandleState::Inactive);
@@ -144,85 +142,109 @@ impl PointRadiusHandle {
} }
} }
pub fn overlays(&mut self, other_gizmo_active: bool, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, mouse_position: DVec2, overlay_context: &mut OverlayContext) { pub fn overlays(
&self,
selected_star_layer: Option<LayerNodeIdentifier>,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
match &self.handle_state { match &self.handle_state {
PointRadiusHandleState::Inactive => { PointRadiusHandleState::Inactive => {
let selected_nodes = document.network_interface.selected_nodes(); let Some(layer) = selected_star_layer else { return };
let layers = selected_nodes.selected_visible_and_unlocked_layers(&document.network_interface).filter(|layer| {
graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some()
});
for layer in layers {
if other_gizmo_active {
return;
}
// Draw the point handle gizmo for the star shape
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..(2 * n) { // Draw the point handle gizmo for the star shape
let point = star_vertex_position(viewport, i as i32, n, radius1, radius2); if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let center = viewport.transform_point2(DVec2::ZERO); let viewport = document.metadata().transform_to_viewport(layer);
let viewport_diagonal = input.viewport_bounds.size().length();
// If the user zooms out such that shape is very small hide the gizmo for i in 0..(2 * sides) {
if point.distance(center) < GIZMO_HIDE_THRESHOLD { let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
return; let center = viewport.transform_point2(DVec2::ZERO);
} let viewport_diagonal = input.viewport_bounds.size().length();
if point.distance(mouse_position) < 5. { // If the user zooms out such that shape is very small hide the gizmo
let Some(direction) = (point - center).try_normalize() else { continue }; if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
overlay_context.manipulator_handle(point, true, None);
let angle = ((i as f64) * PI) / (n as f64);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
return;
}
overlay_context.manipulator_handle(point, false, None);
} }
}
// Draw the point handle gizmo for the Polygon shape if point.distance(mouse_position) < 5. {
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) { let Some(direction) = (point - center).try_normalize() else { continue };
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..n { overlay_context.manipulator_handle(point, true, None);
let point = polygon_vertex_position(viewport, i as i32, n, radius); let angle = ((i as f64) * PI) / (sides as f64);
let center = viewport.transform_point2(DVec2::ZERO); overlay_context.line(center, center + direction * viewport_diagonal, None, None);
let viewport_diagonal = input.viewport_bounds.size().length();
// If the user zooms out such that shape is very small hide the gizmo draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. { return;
let Some(direction) = (point - center).try_normalize() else { continue };
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
return;
}
overlay_context.manipulator_handle(point, false, None);
} }
overlay_context.manipulator_handle(point, false, None);
}
}
// Draw the point handle gizmo for the Polygon shape
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..sides {
let point = polygon_vertex_position(viewport, i as i32, sides, radius);
let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length();
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
let Some(direction) = (point - center).try_normalize() else { continue };
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
return;
}
overlay_context.manipulator_handle(point, false, None);
} }
} }
} }
PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => { PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => {
let Some(layer) = self.layer else { return }; let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer); let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO); let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length(); let viewport_diagonal = input.viewport_bounds.size().length();
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) { // Star
let angle = ((self.point as f64) * PI) / (n as f64); if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let point = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); let angle = ((self.point as f64) * PI) / (sides as f64);
let point = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
let Some(direction) = (point - center).try_normalize() else { return };
// Draws the ray from the center to the dragging point extending till the viewport
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
star_outline(Some(layer), document, overlay_context);
// Make the ticks for snapping
// If dragging to make radius negative don't show the
if (mouse_position - center).dot(direction) < 0. {
return;
}
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
return;
}
// Polygon
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
let point = polygon_vertex_position(viewport, self.point as i32, sides, radius);
let Some(direction) = (point - center).try_normalize() else { return }; let Some(direction) = (point - center).try_normalize() else { return };
@@ -230,47 +252,33 @@ impl PointRadiusHandle {
overlay_context.manipulator_handle(point, true, None); overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None); overlay_context.line(center, center + direction * viewport_diagonal, None, None);
// Makes the tick marks for snapping polygon_outline(Some(layer), document, overlay_context);
// Only show the snapping ticks if the radius is positive
if (mouse_position - center).dot(direction) >= 0. {
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
}
return;
}
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) {
let point = polygon_vertex_position(viewport, self.point as i32, n, radius);
let Some(direction) = (point - center).try_normalize() else { return };
// Draws the ray from the center to the dragging point and extending until the viewport edge is reached
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
} }
} }
PointRadiusHandleState::Snapped(snapping_index) => { PointRadiusHandleState::Snapped(snapping_index) => {
let Some(layer) = self.layer else { return }; let Some(layer) = self.layer else { return };
let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) else { return }; let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer); let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO); let center = viewport.transform_point2(DVec2::ZERO);
match snapping_index { match snapping_index {
// Make a triangle with the previous two points // Make a triangle with previous two points
0 => { 0 => {
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 2, n, radius1, radius2); let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 2, sides, radius1, radius2);
let outer_position = star_vertex_position(viewport, (self.point as i32) - 1, n, radius1, radius2); let outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
let point_position = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let l1 = (before_outer_position - outer_position).length() * 0.2;
let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return }; let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return };
let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return }; let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return };
let Some(direction) = (center - outer_position).try_normalize() else { return }; let Some(direction) = (center - outer_position).try_normalize() else { return };
let l1 = 0.2 * (before_outer_position - outer_position).length();
let new_point = SQRT_2 * l1 * direction + outer_position; let new_point = SQRT_2 * l1 * direction + outer_position;
let before_outer_position = l1 * l1_direction + outer_position; let before_outer_position = l1 * l1_direction + outer_position;
@@ -280,18 +288,20 @@ impl PointRadiusHandle {
overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
} }
1 => { 1 => {
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, n, radius1, radius2); let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, n, radius1, radius2);
let point_position = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, sides, radius1, radius2);
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let l1 = (before_outer_position - point_position).length() * 0.2;
let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return }; let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return };
let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return }; let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return };
let Some(direction) = (center - point_position).try_normalize() else { return }; let Some(direction) = (center - point_position).try_normalize() else { return };
let l1 = 0.2 * (before_outer_position - point_position).length();
let new_point = SQRT_2 * l1 * direction + point_position; let new_point = SQRT_2 * l1 * direction + point_position;
let before_outer_position = l1 * l1_direction + point_position; let before_outer_position = l1 * l1_direction + point_position;
@@ -301,35 +311,37 @@ impl PointRadiusHandle {
overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
} }
i => { i => {
// Use `self.point` as an absolute reference, as it matches the index of the star's vertices starting from 0 // Use `self.point` as absolute reference as it matches the index of vertices of the star starting from 0
if i % 2 != 0 { if i % 2 != 0 {
// Flipped case // Flipped case
let point_position = star_vertex_position(viewport, self.point as i32, n, radius1, radius2); let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
let target_index = (1 - (*i as i32)).abs() + (self.point as i32); let target_index = (1 - (*i as i32)).abs() + (self.point as i32);
let target_point_position = star_vertex_position(viewport, target_index, n, radius1, radius2); let target_point_position = star_vertex_position(viewport, target_index, sides, radius1, radius2);
let mirrored_index = 2 * (self.point as i32) - target_index; let mirrored_index = 2 * (self.point as i32) - target_index;
let mirrored = star_vertex_position(viewport, mirrored_index, n, radius1, radius2); let mirrored = star_vertex_position(viewport, mirrored_index, sides, radius1, radius2);
overlay_context.line(point_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(point_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(point_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(point_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
} else { } else {
let outer_index = (self.point as i32) - 1; let outer_index = (self.point as i32) - 1;
let outer_position = star_vertex_position(viewport, outer_index, n, radius1, radius2); let outer_position = star_vertex_position(viewport, outer_index, sides, radius1, radius2);
// The vertex which is colinear with the point we are dragging and its previous outer vertex // The vertex which is colinear with the point we are dragging and its previous outer vertex
let target_index = (self.point as i32) + (*i as i32) - 1; let target_index = (self.point as i32) + (*i as i32) - 1;
let target_point_position = star_vertex_position(viewport, target_index, n, radius1, radius2); let target_point_position = star_vertex_position(viewport, target_index, sides, radius1, radius2);
let mirrored_index = 2 * outer_index - target_index; let mirrored_index = 2 * outer_index - target_index;
let mirrored = star_vertex_position(viewport, mirrored_index, n, radius1, radius2); let mirrored = star_vertex_position(viewport, mirrored_index, sides, radius1, radius2);
overlay_context.line(outer_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(outer_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(outer_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(outer_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
} }
} }
} }
star_outline(Some(layer), document, overlay_context);
} }
} }
} }
@@ -342,29 +354,32 @@ impl PointRadiusHandle {
}; };
let other_index = if radius_index == 3 { 2 } else { 3 }; let other_index = if radius_index == 3 { 2 } else { 3 };
let Some(&TaggedValue::F64(other_radius)) = node_inputs[other_index].as_value() else { let Some(&TaggedValue::F64(other_radius)) = node_inputs[other_index].as_value() else {
return snap_radii; return snap_radii;
}; };
let Some(&TaggedValue::U32(n)) = node_inputs[1].as_value() else { let Some(&TaggedValue::U32(sides)) = node_inputs[1].as_value() else {
return snap_radii; return snap_radii;
}; };
// Inner radius for 90° // Inner radius for 90°
let b = FRAC_PI_4 * 3. - PI / (n as f64); let b = FRAC_PI_4 * 3. - PI / (sides as f64);
let angle = b.sin(); let angle = b.sin();
let required_radius = (other_radius / angle) * FRAC_1_SQRT_2; let required_radius = (other_radius / angle) * FRAC_1_SQRT_2;
snap_radii.push(required_radius); snap_radii.push(required_radius);
// Also add the case where the radius exceeds the other radius (the "flipped" case) // Also push the case when the when it length increases more than the other
let flipped = other_radius * angle * SQRT_2; let flipped = other_radius * angle * SQRT_2;
snap_radii.push(flipped); snap_radii.push(flipped);
for i in 1..n { for i in 1..sides {
let n = n as f64; let sides = sides as f64;
let i = i as f64; let i = i as f64;
let denominator = 2. * ((PI * (i - 1.)) / n).cos() * ((PI * i) / n).sin(); let denominator = 2. * ((PI * (i - 1.)) / sides).cos() * ((PI * i) / sides).sin();
let numerator = ((2. * PI * i) / n).sin(); let numerator = ((2. * PI * i) / sides).sin();
let factor = numerator / denominator; let factor = numerator / denominator;
if factor < 0. { if factor < 0. {
@@ -392,33 +407,32 @@ impl PointRadiusHandle {
// Check if either index is 0 or 1 and prioritize them // Check if either index is 0 or 1 and prioritize them
match (*i_a == 0 || *i_a == 1, *i_b == 0 || *i_b == 1) { match (*i_a == 0 || *i_a == 1, *i_b == 0 || *i_b == 1) {
(true, false) => std::cmp::Ordering::Less, // a is priority index, b is not // `a` is priority index, `b` is not
(false, true) => std::cmp::Ordering::Greater, // b is priority index, a is not (true, false) => std::cmp::Ordering::Less,
_ => dist_a.partial_cmp(&dist_b).unwrap_or(std::cmp::Ordering::Equal), // normal comparison // `b` is priority index, `a` is not
(false, true) => std::cmp::Ordering::Greater,
// Normal comparison
_ => dist_a.partial_cmp(&dist_b).unwrap_or(std::cmp::Ordering::Equal),
} }
}) })
.map(|(i, rad)| (i, *rad - original_radius)) .map(|(i, rad)| (i, *rad - original_radius))
} }
pub fn update_inner_radius( pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
&mut self, let Some(layer) = self.layer else { return };
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
drag_start: DVec2,
) {
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else { let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
return; return;
}; };
let transform = document.network_interface.document_metadata().transform_to_viewport(layer); let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer);
let center = transform.transform_point2(DVec2::ZERO); let document_transform = document.network_interface.document_metadata().transform_to_document(layer);
let center = viewport_transform.transform_point2(DVec2::ZERO);
let radius_index = self.radius_index; let radius_index = self.radius_index;
let original_radius = self.initial_radius; let original_radius = self.initial_radius;
let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start); let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - document_transform.inverse().transform_point2(drag_start);
let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center; let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center;
let projection = delta.project_onto(radius); let projection = delta.project_onto(radius);
let sign = radius.dot(delta).signum(); let sign = radius.dot(delta).signum();
@@ -1,12 +1,12 @@
pub mod auto_panning; pub mod auto_panning;
pub mod color_selector; pub mod color_selector;
pub mod compass_rose; pub mod compass_rose;
pub mod gizmos;
pub mod graph_modification_utils; pub mod graph_modification_utils;
pub mod measure; pub mod measure;
pub mod pivot; pub mod pivot;
pub mod resize; pub mod resize;
pub mod shape_editor; pub mod shape_editor;
pub mod shape_gizmos;
pub mod shapes; pub mod shapes;
pub mod snapping; pub mod snapping;
pub mod transformation_cage; pub mod transformation_cage;
@@ -12,7 +12,7 @@ use crate::messages::tool::common_functionality::utility_functions::{find_refit_
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState}; use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
use bezier_rs::{Bezier, BezierHandles, Subpath, TValue}; use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::vector::{HandleId, SegmentId}; use graphene_std::vector::{HandleExt, HandleId, SegmentId};
use graphene_std::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType}; use graphene_std::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType};
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -1,2 +0,0 @@
pub mod number_of_points_handle;
pub mod point_radius_handle;
@@ -1,241 +0,0 @@
use crate::consts::{GIZMO_HIDE_THRESHOLD, NUMBER_OF_POINTS_HANDLE_SPOKE_EXTENSION, NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::Responses;
use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::{
extract_polygon_parameters, extract_star_parameters, inside_polygon, inside_star, polygon_vertex_position, star_vertex_position,
};
use crate::messages::tool::tool_messages::tool_prelude::Key;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
use std::f64::consts::TAU;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum NumberOfPointsHandleState {
#[default]
Inactive,
Hover,
Dragging,
}
#[derive(Clone, Debug, Default)]
pub struct NumberOfPointsHandle {
pub layer: Option<LayerNodeIdentifier>,
pub initial_points: u32,
pub handle_state: NumberOfPointsHandleState,
}
impl NumberOfPointsHandle {
pub fn cleanup(&mut self) {
self.handle_state = NumberOfPointsHandleState::Inactive;
self.layer = None;
}
pub fn update_state(&mut self, state: NumberOfPointsHandleState) {
self.handle_state = state;
}
pub fn is_hovering(&self) -> bool {
self.handle_state == NumberOfPointsHandleState::Hover
}
pub fn is_dragging(&self) -> bool {
self.handle_state == NumberOfPointsHandleState::Dragging
}
pub fn handle_actions(
&mut self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
responses: &mut VecDeque<Message>,
) {
if input.keyboard.key(Key::Control) {
return;
}
match &self.handle_state {
NumberOfPointsHandleState::Inactive => {
let selected_nodes = document.network_interface.selected_nodes();
let layers = selected_nodes.selected_visible_and_unlocked_layers(&document.network_interface).filter(|layer| {
graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some()
});
for layer in layers {
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let point_on_max_radius = star_vertex_position(viewport, 0, n, radius1, radius2);
if mouse_position.distance(center) < NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.layer = Some(layer);
self.initial_points = n;
self.update_state(NumberOfPointsHandleState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let point_on_max_radius = polygon_vertex_position(viewport, 0, n, radius);
if mouse_position.distance(center) < NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.layer = Some(layer);
self.initial_points = n;
self.update_state(NumberOfPointsHandleState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
}
}
NumberOfPointsHandleState::Hover | NumberOfPointsHandleState::Dragging => {
let Some(layer) = self.layer else { return };
let Some((n, radius)) = extract_star_parameters(Some(layer), document)
.map(|(n, r1, r2)| (n, r1.max(r2)))
.or_else(|| extract_polygon_parameters(Some(layer), document))
else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if mouse_position.distance(center) > NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH && matches!(&self.handle_state, NumberOfPointsHandleState::Hover) {
self.update_state(NumberOfPointsHandleState::Inactive);
self.layer = None;
self.draw_spokes(center, viewport, n, radius, overlay_context);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
}
}
}
}
pub fn overlays(
&mut self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if input.keyboard.key(Key::Control) {
return;
}
match &self.handle_state {
NumberOfPointsHandleState::Inactive => {
let selected_nodes = document.network_interface.selected_nodes();
let layers = selected_nodes.selected_visible_and_unlocked_layers(&document.network_interface).filter(|layer| {
graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some()
});
for layer in layers {
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let radius = radius1.max(radius2);
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
if closest_segment.layer() == layer {
return;
}
}
let point_on_max_radius = star_vertex_position(viewport, 0, n, radius1, radius2);
if inside_star(viewport, n, radius1, radius2, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.draw_spokes(center, viewport, n, radius, overlay_context);
return;
}
}
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
if closest_segment.layer() == layer {
return;
}
}
let point_on_max_radius = polygon_vertex_position(viewport, 0, n, radius);
if inside_polygon(viewport, n, radius, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.draw_spokes(center, viewport, n, radius, overlay_context);
return;
}
}
}
}
NumberOfPointsHandleState::Hover | NumberOfPointsHandleState::Dragging => {
let Some(layer) = self.layer else { return };
let Some((n, radius)) = extract_star_parameters(Some(layer), document)
.map(|(n, r1, r2)| (n, r1.max(r2)))
.or_else(|| extract_polygon_parameters(Some(layer), document))
else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
self.draw_spokes(center, viewport, n, radius, overlay_context);
}
}
}
fn draw_spokes(&self, center: DVec2, viewport: DAffine2, n: u32, radius: f64, overlay_context: &mut OverlayContext) {
for i in 0..n {
let angle = ((i as f64) * TAU) / (n as f64);
let point = viewport.transform_point2(DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
});
let Some(direction) = (point - center).try_normalize() else { continue };
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
let end_point = direction * NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH;
if matches!(self.handle_state, NumberOfPointsHandleState::Hover | NumberOfPointsHandleState::Dragging) {
overlay_context.line(center, end_point * NUMBER_OF_POINTS_HANDLE_SPOKE_EXTENSION + center, None, None);
} else {
overlay_context.line(center, end_point + center, None, None);
}
}
}
pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start);
let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum();
let net_delta = (delta.length() / 25.).round() * sign;
let Some(layer) = self.layer else { return };
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
return;
};
let new_point_count = ((self.initial_points as i32) + (net_delta as i32)).max(3);
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::U32(new_point_count as u32), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
@@ -3,15 +3,98 @@ use super::shape_utility::update_radius_sign;
use super::*; use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::NumberOfPointsDial;
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::NumberOfPointsDialState;
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandle;
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandleState;
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
use crate::messages::tool::common_functionality::shapes::shape_utility::polygon_outline;
use crate::messages::tool::tool_messages::tool_prelude::*; use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2; use glam::DAffine2;
use graph_craft::document::NodeInput; use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque; use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
pub struct PolygonGizmoHandler {
number_of_points_dial: NumberOfPointsDial,
point_radius_handle: PointRadiusHandle,
}
impl ShapeGizmoHandler for PolygonGizmoHandler {
fn is_any_gizmo_hovered(&self) -> bool {
self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered()
}
fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses);
self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses);
}
fn handle_click(&mut self) {
if self.number_of_points_dial.is_hovering() {
self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging);
return;
}
if self.point_radius_handle.hovered() {
self.point_radius_handle.update_state(PointRadiusHandleState::Dragging);
}
}
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if self.number_of_points_dial.is_dragging() {
self.number_of_points_dial.update_number_of_sides(document, input, responses, drag_start);
}
if self.point_radius_handle.is_dragging_or_snapped() {
self.point_radius_handle.update_inner_radius(document, input, responses, drag_start);
}
}
fn overlays(
&self,
document: &DocumentMessageHandler,
selected_polygon_layer: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
self.number_of_points_dial.overlays(document, selected_polygon_layer, shape_editor, mouse_position, overlay_context);
self.point_radius_handle.overlays(selected_polygon_layer, document, input, mouse_position, overlay_context);
polygon_outline(selected_polygon_layer, document, overlay_context);
}
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if self.number_of_points_dial.is_dragging() {
self.number_of_points_dial.overlays(document, None, shape_editor, mouse_position, overlay_context);
}
if self.point_radius_handle.is_dragging_or_snapped() {
self.point_radius_handle.overlays(None, document, input, mouse_position, overlay_context);
}
}
fn cleanup(&mut self) {
self.number_of_points_dial.cleanup();
self.point_radius_handle.cleanup();
}
}
#[derive(Default)] #[derive(Default)]
pub struct Polygon; pub struct Polygon;
@@ -3,8 +3,9 @@ use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::{DocumentMessageHandler, NodeGraphMessage, Responses}; use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage, Responses};
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager; use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager;
use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::*; use crate::messages::tool::utility_types::*;
@@ -12,7 +13,7 @@ use bezier_rs::Subpath;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput; use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::renderer::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::dvec2_to_point; use graphene_std::vector::misc::dvec2_to_point;
use kurbo::{BezPath, PathEl, Shape}; use kurbo::{BezPath, PathEl, Shape};
use std::collections::VecDeque; use std::collections::VecDeque;
@@ -70,9 +71,62 @@ impl ShapeType {
} }
} }
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
pub type ShapeToolModifierKey = [Key; 4]; pub type ShapeToolModifierKey = [Key; 4];
/// The `ShapeGizmoHandler` trait defines the interactive behavior and overlay logic for shape-specific tools in the editor.
/// A gizmo is a visual handle or control point used to manipulate a shape's properties (e.g., number of sides, radius, angle).
pub trait ShapeGizmoHandler {
/// Called every frame to update the gizmo's interaction state based on the mouse position and selection.
///
/// This includes detecting hover states and preparing interaction flags or visual feedback (e.g., highlighting a hovered handle).
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>);
/// Called when a mouse click occurs over the canvas and a gizmo handle is hovered.
///
/// Used to initiate drag interactions or toggle states on the handle, depending on the tool.
/// For example, a hovered "number of points" handle might enter a "Dragging" state.
fn handle_click(&mut self);
/// Called during a drag interaction to update the shape's parameters in real time.
///
/// For example, a handle might calculate the distance from the drag start to determine a new radius or update the number of points.
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>);
/// Draws the static or hover-dependent overlays associated with the gizmo.
///
/// These overlays include visual indicators like shape outlines, control points, and hover highlights.
fn overlays(
&self,
document: &DocumentMessageHandler,
selected_shape_layers: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
);
/// Draws overlays specifically during a drag operation.
///
/// Used to give real-time visual feedback based on drag progress, such as showing the updated shape preview or snapping guides.
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
);
/// Returns `true` if any handle or control point in the gizmo is currently being hovered.
fn is_any_gizmo_hovered(&self) -> bool;
/// Resets or clears any internal state maintained by the gizmo when it is no longer active.
///
/// For example, dragging states or hover flags should be cleared to avoid visual glitches when switching tools or shapes.
fn cleanup(&mut self);
}
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let sign_num = if end[1] > start[1] { 1. } else { -1. }; let sign_num = if end[1] > start[1] { 1. } else { -1. };
let new_layer = NodeGraphLayer::new(layer, &document.network_interface); let new_layer = NodeGraphLayer::new(layer, &document.network_interface);
@@ -154,19 +208,22 @@ pub fn anchor_overlays(document: &DocumentMessageHandler, overlay_context: &mut
} }
} }
/// Extract the node input values of Star /// Extract the node input values of Star.
/// Returns an option of (sides, radius1, radius2).
pub fn extract_star_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64, f64)> { pub fn extract_star_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64, f64)> {
let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Star")?; let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Star")?;
let (Some(&TaggedValue::U32(n)), Some(&TaggedValue::F64(outer)), Some(&TaggedValue::F64(inner))) = (node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value(), node_inputs.get(3)?.as_value()) let (Some(&TaggedValue::U32(sides)), Some(&TaggedValue::F64(radius_1)), Some(&TaggedValue::F64(radius_2))) =
(node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value(), node_inputs.get(3)?.as_value())
else { else {
return None; return None;
}; };
Some((n, outer, inner)) Some((sides, radius_1, radius_2))
} }
/// Extract the node input values of Polygon /// Extract the node input values of Polygon.
/// Returns an option of (sides, radius).
pub fn extract_polygon_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64)> { pub fn extract_polygon_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64)> {
let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Regular Polygon")?; let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Regular Polygon")?;
@@ -188,7 +245,7 @@ pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radiu
}) })
} }
/// Calculate the viewport position of as a polygon vertex given its index /// Calculate the viewport position of a polygon vertex given its index
pub fn polygon_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius: f64) -> DVec2 { pub fn polygon_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius: f64) -> DVec2 {
let angle = ((vertex_index as f64) * TAU) / (n as f64); let angle = ((vertex_index as f64) * TAU) / (n as f64);
@@ -198,49 +255,37 @@ pub fn polygon_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, ra
}) })
} }
/// Outlines the geometric shape made by the Star node /// Outlines the geometric shape made by star-node
pub fn star_outline(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { pub fn star_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
let mut anchors = Vec::new(); let Some(layer) = layer else { return };
let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) else { return }; let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) else {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..2 * n {
let angle = ((i as f64) * PI) / (n as f64);
let radius = if i % 2 == 0 { radius1 } else { radius2 };
let point = DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
};
anchors.push(point);
}
let subpath = [ClickTargetType::Subpath(Subpath::from_anchors_linear(anchors, true))];
overlay_context.outline(subpath.iter(), viewport, None);
}
/// Outlines the geometric shape made by the Polygon node
pub fn polygon_outline(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
let mut anchors = Vec::new();
let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) else {
return; return;
}; };
let viewport = document.metadata().transform_to_viewport(layer); let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..2 * n {
let angle = ((i as f64) * TAU) / (n as f64);
let point = DVec2 { let points = sides as u64;
x: radius * angle.sin(), let diameter: f64 = radius1 * 2.;
y: -radius * angle.cos(), let inner_diameter = radius2 * 2.;
};
anchors.push(point); let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))];
}
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::from_anchors_linear(anchors, true))]; overlay_context.outline(subpath.iter(), viewport, None);
}
/// Outlines the geometric shape made by polygon-node
pub fn polygon_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
let Some(layer) = layer else { return };
let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let points = sides as u64;
let radius: f64 = radius * 2.;
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius))];
overlay_context.outline(subpath.iter(), viewport, None); overlay_context.outline(subpath.iter(), viewport, None);
} }
@@ -2,9 +2,14 @@ use super::shape_utility::{ShapeToolModifierKey, update_radius_sign};
use super::*; use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::{NumberOfPointsDial, NumberOfPointsDialState};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::{PointRadiusHandle, PointRadiusHandleState};
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, star_outline};
use crate::messages::tool::tool_messages::tool_prelude::*; use crate::messages::tool::tool_messages::tool_prelude::*;
use core::f64; use core::f64;
use glam::DAffine2; use glam::DAffine2;
@@ -12,6 +17,81 @@ use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque; use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
pub struct StarGizmoHandler {
number_of_points_dial: NumberOfPointsDial,
point_radius_handle: PointRadiusHandle,
}
impl ShapeGizmoHandler for StarGizmoHandler {
fn is_any_gizmo_hovered(&self) -> bool {
self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered()
}
fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses);
self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses);
}
fn handle_click(&mut self) {
if self.number_of_points_dial.is_hovering() {
self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging);
return;
}
if self.point_radius_handle.hovered() {
self.point_radius_handle.update_state(PointRadiusHandleState::Dragging);
}
}
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if self.number_of_points_dial.is_dragging() {
self.number_of_points_dial.update_number_of_sides(document, input, responses, drag_start);
}
if self.point_radius_handle.is_dragging_or_snapped() {
self.point_radius_handle.update_inner_radius(document, input, responses, drag_start);
}
}
fn overlays(
&self,
document: &DocumentMessageHandler,
selected_star_layer: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
self.number_of_points_dial.overlays(document, selected_star_layer, shape_editor, mouse_position, overlay_context);
self.point_radius_handle.overlays(selected_star_layer, document, input, mouse_position, overlay_context);
star_outline(selected_star_layer, document, overlay_context);
}
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if self.number_of_points_dial.is_dragging() {
self.number_of_points_dial.overlays(document, None, shape_editor, mouse_position, overlay_context);
}
if self.point_radius_handle.is_dragging_or_snapped() {
self.point_radius_handle.overlays(None, document, input, mouse_position, overlay_context);
}
}
fn cleanup(&mut self) {
self.number_of_points_dial.cleanup();
self.point_radius_handle.cleanup();
}
}
#[derive(Default)] #[derive(Default)]
pub struct Star; pub struct Star;
@@ -5,6 +5,7 @@ use crate::messages::portfolio::document::utility_types::misc::*;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use bezier_rs::{Bezier, Identifier, Subpath, TValue}; use bezier_rs::{Bezier, Identifier, Subpath, TValue};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::math::math_ext::QuadExt;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
@@ -12,7 +12,7 @@ use bezier_rs::Bezier;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::text::{FontCache, load_face}; use graphene_std::text::{FontCache, load_face};
use graphene_std::vector::{HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType}; use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable. /// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend( pub fn should_extend(
+5 -6
View File
@@ -48,17 +48,17 @@ pub enum ToolMessage {
// Relight(RelightToolMessage), // Relight(RelightToolMessage),
// // #[child] // // #[child]
// Detail(DetailToolMessage), // Detail(DetailToolMessage),
// #[child]
// Imaginate(ImaginateToolMessage),
// Messages // Messages
// General tools
ActivateToolSelect, ActivateToolSelect,
ActivateToolArtboard, ActivateToolArtboard,
ActivateToolNavigate, ActivateToolNavigate,
ActivateToolEyedropper, ActivateToolEyedropper,
ActivateToolFill, ActivateToolFill,
ActivateToolGradient, ActivateToolGradient,
// Vector tools
ActivateToolPath, ActivateToolPath,
ActivateToolPen, ActivateToolPen,
ActivateToolFreehand, ActivateToolFreehand,
@@ -68,10 +68,9 @@ pub enum ToolMessage {
ActivateToolShapeEllipse, ActivateToolShapeEllipse,
ActivateToolShape, ActivateToolShape,
ActivateToolText, ActivateToolText,
// Raster tools
ActivateToolBrush, ActivateToolBrush,
// ActivateToolImaginate,
//
ActivateTool { ActivateTool {
tool_type: ToolType, tool_type: ToolType,
}, },
@@ -82,7 +82,6 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
responses.add(ShapeToolMessage::HideShapeTypeWidget(true)); responses.add(ShapeToolMessage::HideShapeTypeWidget(true));
responses.add(ShapeToolMessage::SetShape(shape)); responses.add(ShapeToolMessage::SetShape(shape));
} }
// ToolMessage::ActivateToolImaginate => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Imaginate }),
ToolMessage::ActivateTool { tool_type } => { ToolMessage::ActivateTool { tool_type } => {
let tool_data = &mut self.tool_state.tool_data; let tool_data = &mut self.tool_state.tool_data;
let old_tool = tool_data.active_tool_type.get_tool(); let old_tool = tool_data.active_tool_type.get_tool();
@@ -340,7 +339,6 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
ActivateToolText, ActivateToolText,
ActivateToolBrush, ActivateToolBrush,
// ActivateToolImaginate,
SelectRandomPrimaryColor, SelectRandomPrimaryColor,
ResetColors, ResetColors,
@@ -416,7 +416,7 @@ mod test_freehand {
editor editor
.handle_message(GraphOperationMessage::TransformSet { .handle_message(GraphOperationMessage::TransformSet {
layer: artboard, layer: artboard,
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10.0, -5.0)), transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10., -5.)),
transform_in: TransformIn::Local, transform_in: TransformIn::Local,
skip_rerender: false, skip_rerender: false,
}) })
@@ -424,14 +424,14 @@ mod test_freehand {
editor.select_tool(ToolType::Freehand).await; editor.select_tool(ToolType::Freehand).await;
let mouse_points = [DVec2::new(150.0, 100.0), DVec2::new(200.0, 150.0), DVec2::new(250.0, 130.0), DVec2::new(300.0, 170.0)]; let mouse_points = [DVec2::new(150., 100.), DVec2::new(200., 150.), DVec2::new(250., 130.), DVec2::new(300., 170.)];
// Expected points that will actually be captured by the tool // Expected points that will actually be captured by the tool
let expected_captured_points = &mouse_points[1..]; let expected_captured_points = &mouse_points[1..];
editor.drag_path(&mouse_points, ModifierKeys::empty()).await; editor.drag_path(&mouse_points, ModifierKeys::empty()).await;
let vector_data_list = get_vector_data(&mut editor).await; let vector_data_list = get_vector_data(&mut editor).await;
verify_path_points(&vector_data_list, expected_captured_points, 1.0).expect("Path points verification failed"); verify_path_points(&vector_data_list, expected_captured_points, 1.).expect("Path points verification failed");
} }
#[tokio::test] #[tokio::test]
@@ -439,7 +439,7 @@ mod test_freehand {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
let initial_points = [DVec2::new(100.0, 100.0), DVec2::new(200.0, 200.0), DVec2::new(300.0, 100.0)]; let initial_points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
editor.select_tool(ToolType::Freehand).await; editor.select_tool(ToolType::Freehand).await;
@@ -491,7 +491,7 @@ mod test_freehand {
assert!(endpoint_viewport_pos.is_finite(), "Endpoint position is not finite"); assert!(endpoint_viewport_pos.is_finite(), "Endpoint position is not finite");
let extension_points = [DVec2::new(400.0, 200.0), DVec2::new(500.0, 100.0)]; let extension_points = [DVec2::new(400., 200.), DVec2::new(500., 100.)];
let layer_node_id = { let layer_node_id = {
let document = editor.active_document(); let document = editor.active_document();
@@ -558,7 +558,7 @@ mod test_freehand {
editor.select_tool(ToolType::Freehand).await; editor.select_tool(ToolType::Freehand).await;
let initial_points = [DVec2::new(100.0, 100.0), DVec2::new(200.0, 200.0), DVec2::new(300.0, 100.0)]; let initial_points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
let first_point = initial_points[0]; let first_point = initial_points[0];
editor.move_mouse(first_point.x, first_point.y, ModifierKeys::empty(), MouseKeys::empty()).await; editor.move_mouse(first_point.x, first_point.y, ModifierKeys::empty(), MouseKeys::empty()).await;
@@ -599,7 +599,7 @@ mod test_freehand {
}) })
.await; .await;
let second_path_points = [DVec2::new(400.0, 100.0), DVec2::new(500.0, 200.0), DVec2::new(600.0, 100.0)]; let second_path_points = [DVec2::new(400., 100.), DVec2::new(500., 200.), DVec2::new(600., 100.)];
let first_second_point = second_path_points[0]; let first_second_point = second_path_points[0];
editor.move_mouse(first_second_point.x, first_second_point.y, ModifierKeys::SHIFT, MouseKeys::empty()).await; editor.move_mouse(first_second_point.x, first_second_point.y, ModifierKeys::SHIFT, MouseKeys::empty()).await;
@@ -677,12 +677,12 @@ mod test_freehand {
editor.select_tool(ToolType::Freehand).await; editor.select_tool(ToolType::Freehand).await;
let custom_line_weight = 5.0; let custom_line_weight = 5.;
editor editor
.handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::LineWeight(custom_line_weight)))) .handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::LineWeight(custom_line_weight))))
.await; .await;
let points = [DVec2::new(100.0, 100.0), DVec2::new(200.0, 200.0), DVec2::new(300.0, 100.0)]; let points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
let first_point = points[0]; let first_point = points[0];
editor.move_mouse(first_point.x, first_point.y, ModifierKeys::empty(), MouseKeys::empty()).await; editor.move_mouse(first_point.x, first_point.y, ModifierKeys::empty(), MouseKeys::empty()).await;
@@ -718,7 +718,7 @@ mod test_gradient {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2.0 }).await; editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
editor.drag_tool(ToolType::Rectangle, -5., -3., 100., 100., ModifierKeys::empty()).await; editor.drag_tool(ToolType::Rectangle, -5., -3., 100., 100., ModifierKeys::empty()).await;
@@ -727,7 +727,7 @@ mod test_gradient {
editor editor
.handle_message(GraphOperationMessage::TransformSet { .handle_message(GraphOperationMessage::TransformSet {
layer: selected_layer, layer: selected_layer,
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10.0, -5.0)), transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10., -5.)),
transform_in: TransformIn::Local, transform_in: TransformIn::Local,
skip_rerender: false, skip_rerender: false,
}) })
@@ -803,7 +803,7 @@ mod test_gradient {
stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let positions: Vec<f64> = stops.iter().map(|(pos, _)| *pos).collect(); let positions: Vec<f64> = stops.iter().map(|(pos, _)| *pos).collect();
assert_stops_at_positions(&positions, &[0.0, 0.5, 1.0], 0.1); assert_stops_at_positions(&positions, &[0., 0.5, 1.], 0.1);
let middle_color = stops[1].1.to_rgba8_srgb(); let middle_color = stops[1].1.to_rgba8_srgb();
@@ -843,7 +843,7 @@ mod test_gradient {
// Check positions are now correctly ordered // Check positions are now correctly ordered
let updated_positions: Vec<f64> = updated_stops.iter().map(|(pos, _)| *pos).collect(); let updated_positions: Vec<f64> = updated_stops.iter().map(|(pos, _)| *pos).collect();
assert_stops_at_positions(&updated_positions, &[0.0, 0.8, 1.0], 0.1); assert_stops_at_positions(&updated_positions, &[0., 0.8, 1.], 0.1);
// Colors should maintain their associations with the stop points // Colors should maintain their associations with the stop points
assert_eq!(updated_stops[0].1.to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb()); assert_eq!(updated_stops[0].1.to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb());
@@ -877,7 +877,7 @@ mod test_gradient {
let positions: Vec<f64> = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect(); let positions: Vec<f64> = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect();
// Use helper function to verify positions // Use helper function to verify positions
assert_stops_at_positions(&positions, &[0.0, 0.25, 0.75, 1.0], 0.05); assert_stops_at_positions(&positions, &[0., 0.25, 0.75, 1.], 0.05);
// Select the stop at position 0.75 and delete it // Select the stop at position 0.75 and delete it
let position2 = DVec2::new(75., 0.); let position2 = DVec2::new(75., 0.);
@@ -903,7 +903,7 @@ mod test_gradient {
let final_positions: Vec<f64> = final_gradient.stops.iter().map(|(pos, _)| *pos).collect(); let final_positions: Vec<f64> = final_gradient.stops.iter().map(|(pos, _)| *pos).collect();
// Verify final positions with helper function // Verify final positions with helper function
assert_stops_at_positions(&final_positions, &[0.0, 0.25, 1.0], 0.05); assert_stops_at_positions(&final_positions, &[0., 0.25, 1.], 0.05);
// Additional verification that 0.75 stop is gone // Additional verification that 0.75 stop is gone
assert!(!final_positions.iter().any(|pos| (pos - 0.75).abs() < 0.05), "Stop at position 0.75 should have been deleted"); assert!(!final_positions.iter().any(|pos| (pos - 0.75).abs() < 0.05), "Stop at position 0.75 should have been deleted");
@@ -1,184 +0,0 @@
use super::tool_prelude::*;
use crate::messages::tool::common_functionality::resize::Resize;
#[derive(Default)]
pub struct ImaginateTool {
fsm_state: ImaginateToolFsmState,
tool_data: ImaginateToolData,
}
#[impl_message(Message, ToolMessage, Imaginate)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ImaginateToolMessage {
// Standard messages
Abort,
// Tool-specific messages
DragStart,
DragStop,
Resize { center: Key, lock_ratio: Key },
}
impl LayoutHolder for ImaginateTool {
fn layout(&self) -> Layout {
Layout::WidgetLayout(WidgetLayout::default())
}
}
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for ImaginateTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true);
}
fn actions(&self) -> ActionList {
match self.fsm_state {
ImaginateToolFsmState::Ready => actions!(ImaginateToolMessageDiscriminant;
DragStart,
),
ImaginateToolFsmState::Drawing => actions!(ImaginateToolMessageDiscriminant;
DragStop,
Abort,
Resize,
),
}
}
}
impl ToolMetadata for ImaginateTool {
fn icon_name(&self) -> String {
"RasterImaginateTool".into()
}
fn tooltip(&self) -> String {
"Imaginate Tool".into()
}
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
ToolType::Imaginate
}
}
impl ToolTransition for ImaginateTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
tool_abort: Some(ImaginateToolMessage::Abort.into()),
..Default::default()
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum ImaginateToolFsmState {
#[default]
Ready,
Drawing,
}
#[derive(Clone, Debug, Default)]
struct ImaginateToolData {
data: Resize,
}
impl Fsm for ImaginateToolFsmState {
type ToolData = ImaginateToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
ToolActionHandlerData { document, input, .. }: &mut ToolActionHandlerData,
_tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
let shape_data = &mut tool_data.data;
let ToolMessage::Imaginate(event) = event else { return self };
match (self, event) {
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
shape_data.start(document, input);
// responses.add(DocumentMessage::AddTransaction);
//shape_data.layer = Some(LayerNodeIdentifier::new(NodeId::new(), &document.network_interface));
responses.add(DocumentMessage::DeselectAllLayers);
// // Utility function to offset the position of each consecutive node
// let mut pos = 8;
// let mut next_pos = || {
// pos += 8;
// DocumentNodeMetadata::position((pos, 4))
// };
// // Get the node type for the Transform and Imaginate nodes
// let Some(transform_node_type) = resolve_document_node_type("Transform") else {
// warn!("Transform node should be in registry");
// return ImaginateToolFsmState::Drawing;
// };
// let imaginate_node_type = &*IMAGINATE_NODE;
// // Give them a unique ID
// let transform_node_id = NodeId(100);
//let imaginate_node_id = NodeId(101);
// Create the network based on the Input -> Output passthrough default network
// let mut network = new_image_network(16, imaginate_node_id);
// // Insert the nodes into the default network
// network.insert_node(
// transform_node_id,
// transform_node_type.to_document_node_default_inputs([Some(NodeInput::node(NodeId(0), 0))], next_pos()),
// );
// network.insert_node(
// imaginate_node_id,
// imaginate_node_type.to_document_node_default_inputs([Some(NodeInput::node(transform_node_id, 0))], next_pos()),
// );
// responses.add(NodeGraphMessage::ShiftNode { node_id: imaginate_node_id });
// // Add a layer with a frame to the document
// responses.add(Operation::AddFrame {
// path: shape_data.layer.unwrap().to_path(),
// insert_index: -1,
// transform: DAffine2::ZERO.to_cols_array(),
// network,
// });
ImaginateToolFsmState::Drawing
}
(state, ImaginateToolMessage::Resize { center, lock_ratio }) => {
let message = shape_data.calculate_transform(document, input, center, lock_ratio, true);
responses.try_add(message);
state
}
(ImaginateToolFsmState::Drawing, ImaginateToolMessage::DragStop) => {
input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses);
shape_data.cleanup(responses);
ImaginateToolFsmState::Ready
}
(ImaginateToolFsmState::Drawing, ImaginateToolMessage::Abort) => {
responses.add(DocumentMessage::AbortTransaction);
shape_data.cleanup(responses);
ImaginateToolFsmState::Ready
}
(_, ImaginateToolMessage::Abort) => ImaginateToolFsmState::Ready,
_ => self,
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
ImaginateToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Repaint Frame"),
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])]),
ImaginateToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")])]),
};
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
}
}
@@ -4,7 +4,6 @@ pub mod eyedropper_tool;
pub mod fill_tool; pub mod fill_tool;
pub mod freehand_tool; pub mod freehand_tool;
pub mod gradient_tool; pub mod gradient_tool;
// pub mod imaginate_tool;
pub mod navigate_tool; pub mod navigate_tool;
pub mod path_tool; pub mod path_tool;
pub mod pen_tool; pub mod pen_tool;
@@ -18,7 +18,7 @@ use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandi
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate}; use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate};
use bezier_rs::{Bezier, TValue}; use bezier_rs::{Bezier, TValue};
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData}; use graphene_std::vector::{HandleExt, HandleId, NoHashBuilder, SegmentId, VectorData};
use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType}; use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType};
use std::vec; use std::vec;
@@ -22,10 +22,10 @@ use crate::messages::tool::common_functionality::utility_functions::{resize_boun
use bezier_rs::Subpath; use bezier_rs::Subpath;
use glam::DMat2; use glam::DMat2;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::path_bool::BooleanOperation;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect; use graphene_std::renderer::Rect;
use graphene_std::transform::ReferencePoint; use graphene_std::transform::ReferencePoint;
use graphene_std::vector::misc::BooleanOperation;
use std::fmt; use std::fmt;
#[derive(Default)] #[derive(Default)]
@@ -6,13 +6,13 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer}; use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoManager;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::shape_gizmos::number_of_points_handle::{NumberOfPointsHandle, NumberOfPointsHandleState};
use crate::messages::tool::common_functionality::shape_gizmos::point_radius_handle::{PointRadiusHandle, PointRadiusHandleState};
use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints}; use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints};
use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon; use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon;
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, polygon_outline, star_outline, transform_cage_overlays}; use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays};
use crate::messages::tool::common_functionality::shapes::star_shape::Star; use crate::messages::tool::common_functionality::shapes::star_shape::Star;
use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle}; use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle};
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration};
@@ -22,6 +22,8 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput}; use graph_craft::document::{NodeId, NodeInput};
use graphene_std::Color; use graphene_std::Color;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::vector::misc::ArcType;
use std::vec;
#[derive(Default)] #[derive(Default)]
pub struct ShapeTool { pub struct ShapeTool {
@@ -36,6 +38,7 @@ pub struct ShapeToolOptions {
stroke: ToolColorOptions, stroke: ToolColorOptions,
vertices: u32, vertices: u32,
shape_type: ShapeType, shape_type: ShapeType,
arc_type: ArcType,
} }
impl Default for ShapeToolOptions { impl Default for ShapeToolOptions {
@@ -44,8 +47,9 @@ impl Default for ShapeToolOptions {
line_weight: DEFAULT_STROKE_WIDTH, line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(), fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(), stroke: ToolColorOptions::new_primary(),
shape_type: ShapeType::Polygon,
vertices: 5, vertices: 5,
shape_type: ShapeType::Polygon,
arc_type: ArcType::Open,
} }
} }
} }
@@ -60,6 +64,7 @@ pub enum ShapeOptionsUpdate {
WorkingColors(Option<Color>, Option<Color>), WorkingColors(Option<Color>, Option<Color>),
Vertices(u32), Vertices(u32),
ShapeType(ShapeType), ShapeType(ShapeType),
ArcType(ArcType),
} }
#[impl_message(Message, ToolMessage, Shape)] #[impl_message(Message, ToolMessage, Shape)]
@@ -195,6 +200,9 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for ShapeTo
ShapeOptionsUpdate::Vertices(vertices) => { ShapeOptionsUpdate::Vertices(vertices) => {
self.options.vertices = vertices; self.options.vertices = vertices;
} }
ShapeOptionsUpdate::ArcType(arc_type) => {
self.options.arc_type = arc_type;
}
} }
self.fsm_state.update_hints(responses); self.fsm_state.update_hints(responses);
@@ -217,8 +225,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for ShapeTo
| ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::ResizingBounds
| ShapeToolFsmState::DraggingLineEndpoints | ShapeToolFsmState::DraggingLineEndpoints
| ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::RotatingBounds
| ShapeToolFsmState::DraggingStarInnerRadius | ShapeToolFsmState::ModifyingGizmo
| ShapeToolFsmState::DraggingStarNumberPointHandle
| ShapeToolFsmState::SkewingBounds { .. } => { | ShapeToolFsmState::SkewingBounds { .. } => {
actions!(ShapeToolMessageDiscriminant; actions!(ShapeToolMessageDiscriminant;
DragStop, DragStop,
@@ -263,12 +270,9 @@ pub enum ShapeToolFsmState {
Ready(ShapeType), Ready(ShapeType),
Drawing(ShapeType), Drawing(ShapeType),
// Line shape-specific // Gizmos
DraggingLineEndpoints, DraggingLineEndpoints,
ModifyingGizmo,
// Star shape-specific
DraggingStarInnerRadius,
DraggingStarNumberPointHandle,
// Transform cage // Transform cage
ResizingBounds, ResizingBounds,
@@ -306,9 +310,8 @@ pub struct ShapeToolData {
// Current shape which is being drawn // Current shape which is being drawn
current_shape: ShapeType, current_shape: ShapeType,
// Gizmo data // Gizmos
pub point_radius_handle: PointRadiusHandle, gizmo_manger: GizmoManager,
pub number_of_points_handle: NumberOfPointsHandle,
} }
impl ShapeToolData { impl ShapeToolData {
@@ -324,26 +327,6 @@ impl ShapeToolData {
} }
} }
} }
fn outlines(&self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
if let Some(layer) = self.number_of_points_handle.layer.or(self.point_radius_handle.layer) {
star_outline(layer, document, overlay_context);
polygon_outline(layer, document, overlay_context);
return;
}
// Fallback: apply to all selected visible & unlocked star layers
for layer in document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter(|layer| {
graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some()
}) {
star_outline(layer, document, overlay_context);
polygon_outline(layer, document, overlay_context);
}
}
} }
impl Fsm for ShapeToolFsmState { impl Fsm for ShapeToolFsmState {
@@ -382,30 +365,24 @@ impl Fsm for ShapeToolFsmState {
.map(|pos| document.metadata().document_to_viewport.transform_point2(pos)) .map(|pos| document.metadata().document_to_viewport.transform_point2(pos))
.unwrap_or(input.mouse.position); .unwrap_or(input.mouse.position);
let is_resizing_or_rotating = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::RotatingBounds); let is_resizing_or_rotating = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::RotatingBounds);
let dragging_start_gizmos = matches!(self, Self::DraggingStarInnerRadius);
if matches!(self, ShapeToolFsmState::DraggingStarInnerRadius | Self::DraggingStarNumberPointHandle | Self::Ready(_)) && !input.keyboard.key(Key::Control) { if matches!(self, Self::Ready(_)) && !input.keyboard.key(Key::Control) {
// Manage state handling of the number of point gizmos tool_data.gizmo_manger.handle_actions(mouse_position, document, responses);
tool_data.number_of_points_handle.handle_actions(document, input, mouse_position, &mut overlay_context, responses); tool_data.gizmo_manger.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
// Manage state handling of point radius handle gizmo
tool_data.point_radius_handle.handle_actions(document, mouse_position);
tool_data.number_of_points_handle.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
tool_data
.point_radius_handle
.overlays(tool_data.number_of_points_handle.layer.is_some(), document, input, mouse_position, &mut overlay_context);
tool_data.outlines(document, &mut overlay_context);
} }
let hovered = tool_data.number_of_points_handle.is_hovering() || tool_data.number_of_points_handle.is_dragging() || !tool_data.point_radius_handle.is_inactive(); if matches!(self, ShapeToolFsmState::ModifyingGizmo) && !input.keyboard.key(Key::Control) {
let modifying_transform_cage = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::SkewingBounds { .. }); tool_data.gizmo_manger.dragging_overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
}
if !is_resizing_or_rotating && !dragging_start_gizmos && !hovered && !modifying_transform_cage { let modifying_transform_cage = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::SkewingBounds { .. });
let hovering_over_gizmo = tool_data.gizmo_manger.hovering_over_gizmo();
if !is_resizing_or_rotating && !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo {
tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
} }
if modifying_transform_cage { if modifying_transform_cage && !matches!(self, ShapeToolFsmState::ModifyingGizmo) {
transform_cage_overlays(document, tool_data, &mut overlay_context); transform_cage_overlays(document, tool_data, &mut overlay_context);
} }
@@ -418,7 +395,9 @@ impl Fsm for ShapeToolFsmState {
return self; return self;
} }
transform_cage_overlays(document, tool_data, &mut overlay_context); if !hovering_over_gizmo {
transform_cage_overlays(document, tool_data, &mut overlay_context);
}
let dragging_bounds = tool_data let dragging_bounds = tool_data
.bounding_box_manager .bounding_box_manager
@@ -430,10 +409,10 @@ impl Fsm for ShapeToolFsmState {
let edges = bounds.check_selected_edges(input.mouse.position); let edges = bounds.check_selected_edges(input.mouse.position);
let is_skewing = matches!(self, ShapeToolFsmState::SkewingBounds { .. }); let is_skewing = matches!(self, ShapeToolFsmState::SkewingBounds { .. });
let is_near_square = edges.is_some_and(|hover_edge| bounds.over_extended_edge_midpoint(input.mouse.position, hover_edge)); let is_near_square = edges.is_some_and(|hover_edge| bounds.over_extended_edge_midpoint(input.mouse.position, hover_edge));
if is_skewing || (dragging_bounds && is_near_square && !is_resizing_or_rotating) { if is_skewing || (dragging_bounds && is_near_square && !is_resizing_or_rotating && !hovering_over_gizmo) {
bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge); bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge);
} }
if !is_skewing && dragging_bounds { if !is_skewing && dragging_bounds && !hovering_over_gizmo {
if let Some(edges) = edges { if let Some(edges) = edges {
tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position); tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position);
} }
@@ -559,28 +538,9 @@ impl Fsm for ShapeToolFsmState {
tool_data.line_data.drag_current = mouse_pos; tool_data.line_data.drag_current = mouse_pos;
// Check if dragging the inner vertices of a star if tool_data.gizmo_manger.handle_click() {
if tool_data.point_radius_handle.hovered() {
tool_data.last_mouse_position = mouse_pos;
tool_data.point_radius_handle.update_state(PointRadiusHandleState::Dragging);
// Always store it in document space
tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos); tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos);
return ShapeToolFsmState::ModifyingGizmo;
responses.add(DocumentMessage::StartTransaction);
return ShapeToolFsmState::DraggingStarInnerRadius;
}
// Check if dragging the number of points handle of a star or polygon
if tool_data.number_of_points_handle.is_hovering() {
tool_data.last_mouse_position = mouse_pos;
tool_data.number_of_points_handle.update_state(NumberOfPointsHandleState::Dragging);
// Always store it in document space
tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos);
responses.add(DocumentMessage::StartTransaction);
return ShapeToolFsmState::DraggingStarNumberPointHandle;
} }
// If clicked on endpoints of a selected line, drag its endpoints // If clicked on endpoints of a selected line, drag its endpoints
@@ -653,13 +613,13 @@ impl Fsm for ShapeToolFsmState {
tool_options.fill.apply_fill(layer, responses); tool_options.fill.apply_fill(layer, responses);
} }
ShapeType::Line => { ShapeType::Line => {
tool_data.line_data.angle = 0.;
tool_data.line_data.weight = tool_options.line_weight; tool_data.line_data.weight = tool_options.line_weight;
tool_data.line_data.editing_layer = Some(layer); tool_data.line_data.editing_layer = Some(layer);
} }
} }
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
tool_data.data.layer = Some(layer); tool_data.data.layer = Some(layer);
ShapeToolFsmState::Drawing(tool_data.current_shape) ShapeToolFsmState::Drawing(tool_data.current_shape)
@@ -695,23 +655,13 @@ impl Fsm for ShapeToolFsmState {
self self
} }
(ShapeToolFsmState::DraggingStarInnerRadius, ShapeToolMessage::PointerMove(..)) => { (ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => {
if let Some(layer) = tool_data.point_radius_handle.layer { responses.add(DocumentMessage::StartTransaction);
tool_data.point_radius_handle.update_inner_radius(document, input, layer, responses, tool_data.data.drag_start); tool_data.gizmo_manger.handle_update(tool_data.data.drag_start, document, input, responses);
tool_data.last_mouse_position = input.mouse.position;
}
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
ShapeToolFsmState::DraggingStarInnerRadius ShapeToolFsmState::ModifyingGizmo
}
(ShapeToolFsmState::DraggingStarNumberPointHandle, ShapeToolMessage::PointerMove(..)) => {
tool_data.number_of_points_handle.update_number_of_sides(document, input, responses, tool_data.data.drag_start);
tool_data.last_mouse_position = input.mouse.position;
responses.add(OverlaysMessage::Draw);
ShapeToolFsmState::DraggingStarNumberPointHandle
} }
(ShapeToolFsmState::ResizingBounds, ShapeToolMessage::PointerMove(modifier)) => { (ShapeToolFsmState::ResizingBounds, ShapeToolMessage::PointerMove(modifier)) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
@@ -773,12 +723,12 @@ impl Fsm for ShapeToolFsmState {
.and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position)) .and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position))
.is_some(); .is_some();
let cursor = tool_data let cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Crosshair, |bounds| {
.bounding_box_manager let cursor = bounds.get_cursor(input, true, dragging_bounds, Some(tool_data.skew_edge));
.as_ref() if cursor == MouseCursorIcon::Default { MouseCursorIcon::Crosshair } else { cursor }
.map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true, dragging_bounds, Some(tool_data.skew_edge))); });
if tool_data.cursor != cursor && !input.keyboard.key(Key::Control) && tool_data.point_radius_handle.is_inactive() && !all_selected_layers_line { if tool_data.cursor != cursor && !input.keyboard.key(Key::Control) && !all_selected_layers_line {
tool_data.cursor = cursor; tool_data.cursor = cursor;
responses.add(FrontendMessage::UpdateMouseCursor { cursor }); responses.add(FrontendMessage::UpdateMouseCursor { cursor });
} }
@@ -811,15 +761,13 @@ impl Fsm for ShapeToolFsmState {
| ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::ResizingBounds
| ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::RotatingBounds
| ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::SkewingBounds { .. }
| ShapeToolFsmState::DraggingStarInnerRadius | ShapeToolFsmState::ModifyingGizmo,
| ShapeToolFsmState::DraggingStarNumberPointHandle,
ShapeToolMessage::DragStop, ShapeToolMessage::DragStop,
) => { ) => {
input.mouse.finish_transaction(tool_data.data.drag_start, responses); input.mouse.finish_transaction(tool_data.data.drag_start, responses);
tool_data.data.cleanup(responses); tool_data.data.cleanup(responses);
tool_data.number_of_points_handle.cleanup(); tool_data.gizmo_manger.handle_cleanup();
tool_data.point_radius_handle.cleanup();
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear(); bounds.original_transforms.clear();
@@ -837,17 +785,14 @@ impl Fsm for ShapeToolFsmState {
| ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::ResizingBounds
| ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::RotatingBounds
| ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::SkewingBounds { .. }
| ShapeToolFsmState::DraggingStarInnerRadius | ShapeToolFsmState::ModifyingGizmo,
| ShapeToolFsmState::DraggingStarNumberPointHandle,
ShapeToolMessage::Abort, ShapeToolMessage::Abort,
) => { ) => {
responses.add(DocumentMessage::AbortTransaction); responses.add(DocumentMessage::AbortTransaction);
tool_data.data.cleanup(responses); tool_data.data.cleanup(responses);
tool_data.line_data.dragging_endpoint = None; tool_data.line_data.dragging_endpoint = None;
// Reset gizmo state tool_data.gizmo_manger.handle_cleanup();
tool_data.number_of_points_handle.cleanup();
tool_data.point_radius_handle.cleanup();
if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear(); bounds.original_transforms.clear();
@@ -952,9 +897,7 @@ impl Fsm for ShapeToolFsmState {
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]), HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]),
]), ]),
ShapeToolFsmState::DraggingStarInnerRadius | ShapeToolFsmState::DraggingStarNumberPointHandle => { ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])])
}
}; };
responses.add(FrontendMessage::UpdateInputHints { hint_data }); responses.add(FrontendMessage::UpdateInputHints { hint_data });
@@ -656,15 +656,15 @@ mod test_spline_tool {
editor.new_document().await; editor.new_document().await;
// Zooming the viewport // Zooming the viewport
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2.0 }).await; editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
// Selecting the spline tool // Selecting the spline tool
editor.select_tool(ToolType::Spline).await; editor.select_tool(ToolType::Spline).await;
// Adding points by clicking at different positions // Adding points by clicking at different positions
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50.0, 50.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50., 50.), ModifierKeys::empty()).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100.0, 50.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100., 50.), ModifierKeys::empty()).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150.0, 100.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150., 100.), ModifierKeys::empty()).await;
// Finish the spline // Finish the spline
editor.handle_message(SplineToolMessage::Confirm).await; editor.handle_message(SplineToolMessage::Confirm).await;
@@ -686,7 +686,7 @@ mod test_spline_tool {
let layer_to_viewport = document.metadata().transform_to_viewport(layer); let layer_to_viewport = document.metadata().transform_to_viewport(layer);
// Expected points in viewport coordinates // Expected points in viewport coordinates
let expected_points = vec![DVec2::new(50.0, 50.0), DVec2::new(100.0, 50.0), DVec2::new(150.0, 100.0)]; let expected_points = vec![DVec2::new(50., 50.), DVec2::new(100., 50.), DVec2::new(150., 100.)];
// Assert all points are correctly positioned // Assert all points are correctly positioned
assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10); assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10);
@@ -697,15 +697,15 @@ mod test_spline_tool {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
editor.new_document().await; editor.new_document().await;
let pan_amount = DVec2::new(200.0, 150.0); let pan_amount = DVec2::new(200., 150.);
editor.handle_message(NavigationMessage::CanvasPan { delta: pan_amount }).await; editor.handle_message(NavigationMessage::CanvasPan { delta: pan_amount }).await;
editor.select_tool(ToolType::Spline).await; editor.select_tool(ToolType::Spline).await;
// Add points by clicking at different positions // Add points by clicking at different positions
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50.0, 50.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50., 50.), ModifierKeys::empty()).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100.0, 50.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100., 50.), ModifierKeys::empty()).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150.0, 100.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150., 100.), ModifierKeys::empty()).await;
editor.handle_message(SplineToolMessage::Confirm).await; editor.handle_message(SplineToolMessage::Confirm).await;
@@ -726,7 +726,7 @@ mod test_spline_tool {
let layer_to_viewport = document.metadata().transform_to_viewport(layer); let layer_to_viewport = document.metadata().transform_to_viewport(layer);
// Expected points in viewport coordinates // Expected points in viewport coordinates
let expected_points = vec![DVec2::new(50.0, 50.0), DVec2::new(100.0, 50.0), DVec2::new(150.0, 100.0)]; let expected_points = vec![DVec2::new(50., 50.), DVec2::new(100., 50.), DVec2::new(150., 100.)];
// Assert all points are correctly positioned // Assert all points are correctly positioned
assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10); assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10);
@@ -738,12 +738,12 @@ mod test_spline_tool {
editor.new_document().await; editor.new_document().await;
// Tilt/rotate the viewport (45 degrees) // Tilt/rotate the viewport (45 degrees)
editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 45.0_f64.to_radians() }).await; editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 45_f64.to_radians() }).await;
editor.select_tool(ToolType::Spline).await; editor.select_tool(ToolType::Spline).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50.0, 50.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50., 50.), ModifierKeys::empty()).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100.0, 50.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100., 50.), ModifierKeys::empty()).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150.0, 100.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150., 100.), ModifierKeys::empty()).await;
editor.handle_message(SplineToolMessage::Confirm).await; editor.handle_message(SplineToolMessage::Confirm).await;
@@ -764,7 +764,7 @@ mod test_spline_tool {
let layer_to_viewport = document.metadata().transform_to_viewport(layer); let layer_to_viewport = document.metadata().transform_to_viewport(layer);
// Expected points in viewport coordinates // Expected points in viewport coordinates
let expected_points = vec![DVec2::new(50.0, 50.0), DVec2::new(100.0, 50.0), DVec2::new(150.0, 100.0)]; let expected_points = vec![DVec2::new(50., 50.), DVec2::new(100., 50.), DVec2::new(150., 100.)];
// Assert all points are correctly positioned // Assert all points are correctly positioned
assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10); assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10);
@@ -777,14 +777,14 @@ mod test_spline_tool {
// Applying multiple transformations // Applying multiple transformations
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 1.5 }).await; editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 1.5 }).await;
editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100.0, 75.0) }).await; editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 75.) }).await;
editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 30.0_f64.to_radians() }).await; editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 30_f64.to_radians() }).await;
editor.select_tool(ToolType::Spline).await; editor.select_tool(ToolType::Spline).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50.0, 50.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50., 50.), ModifierKeys::empty()).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100.0, 50.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100., 50.), ModifierKeys::empty()).await;
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150.0, 100.0), ModifierKeys::empty()).await; editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150., 100.), ModifierKeys::empty()).await;
editor.handle_message(SplineToolMessage::Confirm).await; editor.handle_message(SplineToolMessage::Confirm).await;
if let Err(e) = editor.eval_graph().await { if let Err(e) = editor.eval_graph().await {
@@ -803,7 +803,7 @@ mod test_spline_tool {
let layer_to_viewport = document.metadata().transform_to_viewport(layer); let layer_to_viewport = document.metadata().transform_to_viewport(layer);
// Expected points in viewport coordinates // Expected points in viewport coordinates
let expected_points = vec![DVec2::new(50.0, 50.0), DVec2::new(100.0, 50.0), DVec2::new(150.0, 100.0)]; let expected_points = vec![DVec2::new(50., 50.), DVec2::new(100., 50.), DVec2::new(150., 100.)];
// Assert all points are correctly positioned // Assert all points are correctly positioned
assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10); assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10);
@@ -1021,8 +1021,8 @@ mod test_transform_layer {
let scale_x = final_transform.matrix2.x_axis.length() / original_transform.matrix2.x_axis.length(); let scale_x = final_transform.matrix2.x_axis.length() / original_transform.matrix2.x_axis.length();
let scale_y = final_transform.matrix2.y_axis.length() / original_transform.matrix2.y_axis.length(); let scale_y = final_transform.matrix2.y_axis.length() / original_transform.matrix2.y_axis.length();
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2.0, got: {}", scale_x); assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2, got: {}", scale_x);
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2.0, got: {}", scale_y); assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2, got: {}", scale_y);
} }
#[tokio::test] #[tokio::test]
@@ -1047,8 +1047,8 @@ mod test_transform_layer {
let scale_x = final_transform.matrix2.x_axis.length() / original_transform.matrix2.x_axis.length(); let scale_x = final_transform.matrix2.x_axis.length() / original_transform.matrix2.x_axis.length();
let scale_y = final_transform.matrix2.y_axis.length() / original_transform.matrix2.y_axis.length(); let scale_y = final_transform.matrix2.y_axis.length() / original_transform.matrix2.y_axis.length();
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2.0, got: {}", scale_x); assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2, got: {}", scale_x);
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2.0, got: {}", scale_y); assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2, got: {}", scale_y);
} }
#[tokio::test] #[tokio::test]
@@ -363,7 +363,6 @@ pub enum ToolType {
Patch, Patch,
Detail, Detail,
Relight, Relight,
Imaginate,
Frame, Frame,
} }
@@ -418,9 +417,6 @@ fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Patch, "RasterPatchTool").tooltip("Coming Soon: Patch Tool")), ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Patch, "RasterPatchTool").tooltip("Coming Soon: Patch Tool")),
ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Detail, "RasterDetailTool").tooltip("Coming Soon: Detail Tool (D)")), ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Detail, "RasterDetailTool").tooltip("Coming Soon: Detail Tool (D)")),
ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Relight, "RasterRelightTool").tooltip("Coming Soon: Relight Tool (O)")), ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Relight, "RasterRelightTool").tooltip("Coming Soon: Relight Tool (O)")),
// TODO: Fix and reenable Imaginate tool
// ToolAvailability::Available(Box::<imaginate_tool::ImaginateTool>::default()),
ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Heal, "RasterImaginateTool").tooltip("Coming Soon: Imaginate Tool")),
], ],
] ]
} }
@@ -450,7 +446,6 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
// ToolMessage::Patch(_) => ToolType::Patch, // ToolMessage::Patch(_) => ToolType::Patch,
// ToolMessage::Detail(_) => ToolType::Detail, // ToolMessage::Detail(_) => ToolType::Detail,
// ToolMessage::Relight(_) => ToolType::Relight, // ToolMessage::Relight(_) => ToolType::Relight,
// ToolMessage::Imaginate(_) => ToolType::Imaginate,
_ => panic!("Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {tool_message:?}"), _ => panic!("Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {tool_message:?}"),
} }
} }
@@ -483,7 +478,6 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
// ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch, // ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch,
// ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail, // ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail,
// ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight, // ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight,
// ToolType::Imaginate => ToolMessageDiscriminant::ActivateToolImaginate,
_ => panic!("Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {tool_type:?}"), _ => panic!("Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {tool_type:?}"),
} }
} }
-3
View File
@@ -299,9 +299,6 @@ impl NodeGraphExecutor {
} }
} }
} }
// NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => {
// responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::Refresh));
// }
NodeGraphUpdate::CompilationResponse(execution_response) => { NodeGraphUpdate::CompilationResponse(execution_response) => {
let CompilationResponse { node_graph_errors, result } = execution_response; let CompilationResponse { node_graph_errors, result } = execution_response;
let type_delta = match result { let type_delta = match result {
+11 -1
View File
@@ -45,6 +45,9 @@ pub struct NodeRuntime {
/// Which node is inspected and which monitor node is used (if any) for the current execution /// Which node is inspected and which monitor node is used (if any) for the current execution
inspect_state: Option<InspectState>, inspect_state: Option<InspectState>,
/// Mapping of the fully-qualified node paths to their preprocessor substitutions.
substitutions: HashMap<String, DocumentNode>,
// TODO: Remove, it doesn't need to be persisted anymore // TODO: Remove, it doesn't need to be persisted anymore
/// The current renders of the thumbnails for layer nodes. /// The current renders of the thumbnails for layer nodes.
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>, thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
@@ -120,6 +123,8 @@ impl NodeRuntime {
node_graph_errors: Vec::new(), node_graph_errors: Vec::new(),
monitor_nodes: Vec::new(), monitor_nodes: Vec::new(),
substitutions: preprocessor::generate_node_substitutions(),
thumbnail_renders: Default::default(), thumbnail_renders: Default::default(),
vector_modify: Default::default(), vector_modify: Default::default(),
inspect_state: None, inspect_state: None,
@@ -221,11 +226,16 @@ impl NodeRuntime {
} }
} }
async fn update_network(&mut self, graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> { async fn update_network(&mut self, mut graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> {
if cfg!(not(test)) {
preprocessor::expand_network(&mut graph, &self.substitutions);
}
let scoped_network = wrap_network_in_scope(graph, self.editor_api.clone()); let scoped_network = wrap_network_in_scope(graph, self.editor_api.clone());
// We assume only one output // We assume only one output
assert_eq!(scoped_network.exports.len(), 1, "Graph with multiple outputs not yet handled"); assert_eq!(scoped_network.exports.len(), 1, "Graph with multiple outputs not yet handled");
let c = Compiler {}; let c = Compiler {};
let proto_network = match c.compile_single(scoped_network) { let proto_network = match c.compile_single(scoped_network) {
Ok(network) => network, Ok(network) => network,
-12
View File
@@ -47,9 +47,6 @@ export function createEditor(): Editor {
subscriptions.handleJsMessage(messageType, messageData, raw, handle); subscriptions.handleJsMessage(messageType, messageData, raw, handle);
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorHandle = handle;
// Subscriptions: allows subscribing to messages in JS that are sent from the WASM backend // Subscriptions: allows subscribing to messages in JS that are sent from the WASM backend
const subscriptions: SubscriptionRouter = createSubscriptionRouter(); const subscriptions: SubscriptionRouter = createSubscriptionRouter();
@@ -76,12 +73,3 @@ export function createEditor(): Editor {
return { raw, handle, subscriptions }; return { raw, handle, subscriptions };
} }
// TODO: Find a better way to do this, since no other code takes this approach.
// TODO: Then, delete the `(window as any).editorHandle = handle;` line above.
// This function is called by an FFI binding within the Rust code directly, rather than using the FrontendMessage system.
// Then, this directly calls the `injectImaginatePollServerStatus` function on the `EditorHandle` object which is a JS binding generated by wasm-bindgen, going straight back into the Rust code.
// export function injectImaginatePollServerStatus() {
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// (window as any).editorHandle?.injectImaginatePollServerStatus();
// }
-10
View File
@@ -940,15 +940,6 @@ export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
readonly commitDate!: string; readonly commitDate!: string;
} }
// TODO: Eventually remove this document upgrade code
export class TriggerUpgradeDocumentToVectorManipulationFormat extends JsMessage {
readonly documentId!: bigint;
readonly documentName!: string;
readonly documentIsAutoSaved!: boolean;
readonly documentIsSaved!: boolean;
readonly documentSerializedContent!: string;
}
// WIDGET PROPS // WIDGET PROPS
export abstract class WidgetProps { export abstract class WidgetProps {
@@ -1679,7 +1670,6 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerSavePreferences, TriggerSavePreferences,
TriggerTextCommit, TriggerTextCommit,
TriggerTextCopy, TriggerTextCopy,
TriggerUpgradeDocumentToVectorManipulationFormat,
TriggerVisitLink, TriggerVisitLink,
UpdateActiveDocument, UpdateActiveDocument,
UpdateBox, UpdateBox,
@@ -10,7 +10,6 @@ import {
TriggerDownloadTextFile, TriggerDownloadTextFile,
TriggerImport, TriggerImport,
TriggerOpenDocument, TriggerOpenDocument,
TriggerUpgradeDocumentToVectorManipulationFormat,
UpdateActiveDocument, UpdateActiveDocument,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
UpdateSpreadsheetState, UpdateSpreadsheetState,
@@ -104,11 +103,6 @@ export function createPortfolioState(editor: Editor) {
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image // Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
} }
}); });
editor.subscriptions.subscribeJsMessage(TriggerUpgradeDocumentToVectorManipulationFormat, async (triggerUpgradeDocumentToVectorManipulationFormat) => {
// TODO: Eventually remove this document upgrade code
const { documentId, documentName, documentIsAutoSaved, documentIsSaved, documentSerializedContent } = triggerUpgradeDocumentToVectorManipulationFormat;
editor.handle.triggerUpgradeDocumentToVectorManipulationFormat(documentId, documentName, documentIsAutoSaved, documentIsSaved, documentSerializedContent);
});
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => { editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => {
update((state) => { update((state) => {
-1
View File
@@ -35,7 +35,6 @@ wasm-bindgen = { workspace = true }
serde-wasm-bindgen = { workspace = true } serde-wasm-bindgen = { workspace = true }
js-sys = { workspace = true } js-sys = { workspace = true }
wasm-bindgen-futures = { workspace = true } wasm-bindgen-futures = { workspace = true }
glam = { workspace = true }
math-parser = { workspace = true } math-parser = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
web-sys = { workspace = true } web-sys = { workspace = true }
+1 -222
View File
@@ -11,7 +11,7 @@ use editor::consts::FILE_SAVE_SUFFIX;
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use editor::messages::portfolio::document::utility_types::network_interface::{ImportOrExport, NodeTemplate}; use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
use editor::messages::portfolio::utility_types::Platform; use editor::messages::portfolio::utility_types::Platform;
use editor::messages::prelude::*; use editor::messages::prelude::*;
use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
@@ -734,227 +734,6 @@ impl EditorHandle {
}; };
self.dispatch(message); self.dispatch(message);
} }
// #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
// pub fn inject_imaginate_poll_server_status(&self) {
// self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
// }
// TODO: Eventually remove this document upgrade code
#[wasm_bindgen(js_name = triggerUpgradeDocumentToVectorManipulationFormat)]
pub async fn upgrade_document_to_vector_manipulation_format(
&self,
document_id: u64,
document_name: String,
document_is_auto_saved: bool,
document_is_saved: bool,
document_serialized_content: String,
) {
use editor::messages::portfolio::document::graph_operation::transform_utils::*;
use editor::messages::portfolio::document::graph_operation::utility_types::*;
use editor::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use editor::node_graph_executor::NodeRuntime;
use editor::node_graph_executor::replace_node_runtime;
use graph_craft::document::DocumentNodeImplementation;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::vector::*;
let (_, request_receiver) = std::sync::mpsc::channel();
let (response_sender, _) = std::sync::mpsc::channel();
let old_runtime = replace_node_runtime(NodeRuntime::new(request_receiver, response_sender)).await;
let mut editor = Editor::new();
let document_id = DocumentId(document_id);
editor.handle_message(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name: document_name.clone(),
document_is_auto_saved,
document_is_saved,
document_serialized_content: document_serialized_content.clone(),
to_front: false,
});
let Some(document) = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut() else {
warn!("Document wasn't loaded");
return;
};
for node in document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.iter()
.filter(|(_, d)| d.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Artboard"))
.map(|(id, _)| *id)
.collect::<Vec<_>>()
{
let Some(document_node) = document.network_interface.document_network().nodes.get(&node) else {
log::error!("Could not get document node in document network");
return;
};
if let Some(network) = document_node.implementation.get_network() {
let mut nodes_to_upgrade = Vec::new();
for (node_id, _) in network.nodes.iter().collect::<Vec<_>>() {
if document
.network_interface
.reference(node_id, &[])
.is_some_and(|reference| *reference == Some("To Artboard".to_string()))
&& document
.network_interface
.document_network()
.nodes
.get(node_id)
.is_some_and(|document_node| document_node.inputs.len() != 6)
{
nodes_to_upgrade.push(*node_id);
}
}
for node_id in nodes_to_upgrade {
document
.network_interface
.replace_implementation(&node_id, &[], DocumentNodeImplementation::proto("graphene_core::ToArtboardNode"));
document.network_interface.add_import(TaggedValue::IVec2(glam::IVec2::default()), false, 2, "", "", &[node_id]);
}
}
}
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
portfolio
.executor
.submit_node_graph_evaluation(
portfolio.documents.get_mut(&portfolio.active_document_id().unwrap()).unwrap(),
glam::UVec2::ONE,
Default::default(),
None,
true,
)
.unwrap();
editor::node_graph_executor::run_node_graph().await;
let mut messages = VecDeque::new();
if let Err(err) = editor.poll_node_graph_evaluation(&mut messages) {
log::warn!(
"While attempting to upgrade the old document format, the graph evaluation failed which is necessary for the upgrade process:\n{:#?}",
err
);
replace_node_runtime(old_runtime.unwrap()).await;
let document_name = document_name.clone() + "__DO_NOT_UPGRADE__";
self.dispatch(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
to_front: false,
});
return;
}
let mut updated_nodes = HashSet::new();
let document = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap();
document.network_interface.load_structure();
for node in document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.iter()
.filter(|(_, d)| d.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Merge"))
.map(|(id, _)| *id)
.collect::<Vec<_>>()
{
let layer = LayerNodeIdentifier::new(node, &document.network_interface, &[]);
if layer.has_children(document.metadata()) {
continue;
}
let bounds = LayerBounds::new(document.metadata(), layer);
let mut responses = VecDeque::new();
let mut shape = None;
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, &mut document.network_interface, &mut responses) {
let Some(transform_node_id) = modify_inputs.existing_node_id("Transform", true) else {
return;
};
if !updated_nodes.insert(transform_node_id) {
return;
}
let Some(inputs) = modify_inputs.network_interface.document_network().nodes.get(&transform_node_id).map(|node| &node.inputs) else {
log::error!("Could not get transform node in document network");
return;
};
let transform = get_current_transform(inputs);
let upstream_transform = modify_inputs.network_interface.document_metadata().upstream_transform(transform_node_id);
let pivot_transform = glam::DAffine2::from_translation(upstream_transform.transform_point2(bounds.local_pivot(get_current_normalized_pivot(inputs))));
update_transform(&mut document.network_interface, &transform_node_id, pivot_transform * transform * pivot_transform.inverse());
}
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, &mut document.network_interface, &mut responses) {
let Some(shape_node_id) = modify_inputs.existing_node_id("Shape", true) else {
return;
};
if !updated_nodes.insert(shape_node_id) {
return;
}
let Some(shape_node) = modify_inputs.network_interface.document_network().nodes.get(&shape_node_id) else {
log::error!("Could not get shape node in document network");
return;
};
let path_data = match &shape_node.inputs[0].as_value() {
Some(TaggedValue::Subpaths(translation)) => translation,
_ => &Vec::new(),
};
let colinear_manipulators = match &shape_node.inputs[1].as_value() {
Some(TaggedValue::PointIds(translation)) => translation,
_ => &Vec::new(),
};
let mut vector_data = VectorData::from_subpaths(path_data, false);
vector_data.colinear_manipulators = colinear_manipulators
.iter()
.filter_map(|&point| ManipulatorPointId::Anchor(point).get_handle_pair(&vector_data))
.collect();
shape = Some((shape_node_id, VectorModification::create_from_vector(&vector_data)));
}
if let Some((node_id, modification)) = shape {
let node_type = resolve_document_node_type("Path").unwrap();
let document_node = node_type
.node_template_input_override([None, Some(NodeInput::value(TaggedValue::VectorModification(Box::new(modification)), false))])
.document_node;
let node_metadata = document.network_interface.node_metadata(&node_id, &[]).cloned().unwrap_or_default();
document.network_interface.insert_node(
node_id,
NodeTemplate {
document_node,
persistent_node_metadata: node_metadata.persistent_metadata,
},
&[],
);
}
}
let document_serialized_content = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap().serialize_document();
replace_node_runtime(old_runtime.unwrap()).await;
self.dispatch(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
to_front: false,
});
}
} }
// ============================================================================ // ============================================================================
+1 -1
View File
@@ -157,7 +157,7 @@ raster_node!(graphene_core::raster::OpacityNode<_>, params: [f64]),
There is also the more general `register_node!` for nodes that do not need to run per pixel. There is also the more general `register_node!` for nodes that do not need to run per pixel.
```rs ```rs
register_node!(graphene_core::transform::SetTransformNode<_>, input: VectorData, params: [DAffine2]), register_node!(graphene_core::transform_nodes::SetTransformNode<_>, input: VectorData, params: [DAffine2]),
``` ```
## Debugging ## Debugging
+1 -8
View File
@@ -193,9 +193,7 @@ pub enum ApplicationError {
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum NodeGraphUpdateMessage { pub enum NodeGraphUpdateMessage {}
// ImaginateStatusUpdate,
}
pub trait NodeGraphUpdateSender { pub trait NodeGraphUpdateSender {
fn send(&self, message: NodeGraphUpdateMessage); fn send(&self, message: NodeGraphUpdateMessage);
@@ -208,7 +206,6 @@ impl<T: NodeGraphUpdateSender> NodeGraphUpdateSender for std::sync::Mutex<T> {
} }
pub trait GetEditorPreferences { pub trait GetEditorPreferences {
// fn hostname(&self) -> &str;
fn use_vello(&self) -> bool; fn use_vello(&self) -> bool;
} }
@@ -250,10 +247,6 @@ impl NodeGraphUpdateSender for Logger {
struct DummyPreferences; struct DummyPreferences;
impl GetEditorPreferences for DummyPreferences { impl GetEditorPreferences for DummyPreferences {
// fn hostname(&self) -> &str {
// "dummy_endpoint"
// }
fn use_vello(&self) -> bool { fn use_vello(&self) -> bool {
false false
} }
-3
View File
@@ -11,7 +11,6 @@ default = ["serde"]
nightly = [] nightly = []
type_id_logging = [] type_id_logging = []
wgpu = ["dep:wgpu"] wgpu = ["dep:wgpu"]
vello = ["dep:vello", "bezier-rs/kurbo", "wgpu"]
dealloc_nodes = [] dealloc_nodes = []
[dependencies] [dependencies]
@@ -23,7 +22,6 @@ bytemuck = { workspace = true }
node-macro = { workspace = true } node-macro = { workspace = true }
num-derive = { workspace = true } num-derive = { workspace = true }
num-traits = { workspace = true } num-traits = { workspace = true }
usvg = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
glam = { workspace = true } glam = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
@@ -44,7 +42,6 @@ base64 = { workspace = true }
# Optional workspace dependencies # Optional workspace dependencies
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
vello = { workspace = true, optional = true }
wgpu = { workspace = true, optional = true } wgpu = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
+240
View File
@@ -0,0 +1,240 @@
use dyn_any::DynAny;
use std::hash::Hash;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct AlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
impl Default for AlphaBlending {
fn default() -> Self {
Self::new()
}
}
impl Hash for AlphaBlending {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.opacity.to_bits().hash(state);
self.fill.to_bits().hash(state);
self.blend_mode.hash(state);
self.clip.hash(state);
}
}
impl std::fmt::Display for AlphaBlending {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f32| (x * 1e3).round() / 1e3;
write!(
f,
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
self.blend_mode,
round(self.opacity * 100.),
round(self.fill * 100.),
if self.clip { "Yes" } else { "No" }
)
}
}
impl AlphaBlending {
pub const fn new() -> Self {
Self {
opacity: 1.,
fill: 1.,
blend_mode: BlendMode::Normal,
clip: false,
}
}
pub fn lerp(&self, other: &Self, t: f32) -> Self {
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
AlphaBlending {
opacity: lerp(self.opacity, other.opacity, t),
fill: lerp(self.fill, other.fill, t),
blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode },
clip: if t < 0.5 { self.clip } else { other.clip },
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type)]
#[repr(i32)]
pub enum BlendMode {
// Basic group
#[default]
Normal,
// Darken group
Darken,
Multiply,
ColorBurn,
LinearBurn,
DarkerColor,
// Lighten group
Lighten,
Screen,
ColorDodge,
LinearDodge,
LighterColor,
// Contrast group
Overlay,
SoftLight,
HardLight,
VividLight,
LinearLight,
PinLight,
HardMix,
// Inversion group
Difference,
Exclusion,
Subtract,
Divide,
// Component group
Hue,
Saturation,
Color,
Luminosity,
// Other stuff
Erase,
Restore,
MultiplyAlpha,
}
impl BlendMode {
/// All standard blend modes ordered by group.
pub fn list() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
// Lighten group
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
// Contrast group
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
// Inversion group
&[Difference, Exclusion, Subtract, Divide],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
/// The subset of [`BlendMode::list()`] that is supported by SVG.
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn],
// Lighten group
&[Lighten, Screen, ColorDodge],
// Contrast group
&[Overlay, SoftLight, HardLight],
// Inversion group
&[Difference, Exclusion],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
pub fn index_in_list(&self) -> Option<usize> {
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
/// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
pub fn to_svg_style_name(&self) -> Option<&'static str> {
match self {
// Normal group
BlendMode::Normal => Some("normal"),
// Darken group
BlendMode::Darken => Some("darken"),
BlendMode::Multiply => Some("multiply"),
BlendMode::ColorBurn => Some("color-burn"),
// Lighten group
BlendMode::Lighten => Some("lighten"),
BlendMode::Screen => Some("screen"),
BlendMode::ColorDodge => Some("color-dodge"),
// Contrast group
BlendMode::Overlay => Some("overlay"),
BlendMode::SoftLight => Some("soft-light"),
BlendMode::HardLight => Some("hard-light"),
// Inversion group
BlendMode::Difference => Some("difference"),
BlendMode::Exclusion => Some("exclusion"),
// Component group
BlendMode::Hue => Some("hue"),
BlendMode::Saturation => Some("saturation"),
BlendMode::Color => Some("color"),
BlendMode::Luminosity => Some("luminosity"),
_ => None,
}
}
/// Renders the blend mode CSS style declaration.
pub fn render(&self) -> String {
format!(
r#" mix-blend-mode: {};"#,
self.to_svg_style_name().unwrap_or_else(|| {
warn!("Unsupported blend mode {self:?}");
"normal"
})
)
}
}
impl std::fmt::Display for BlendMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"),
// Darken group
BlendMode::Darken => write!(f, "Darken"),
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::ColorBurn => write!(f, "Color Burn"),
BlendMode::LinearBurn => write!(f, "Linear Burn"),
BlendMode::DarkerColor => write!(f, "Darker Color"),
// Lighten group
BlendMode::Lighten => write!(f, "Lighten"),
BlendMode::Screen => write!(f, "Screen"),
BlendMode::ColorDodge => write!(f, "Color Dodge"),
BlendMode::LinearDodge => write!(f, "Linear Dodge"),
BlendMode::LighterColor => write!(f, "Lighter Color"),
// Contrast group
BlendMode::Overlay => write!(f, "Overlay"),
BlendMode::SoftLight => write!(f, "Soft Light"),
BlendMode::HardLight => write!(f, "Hard Light"),
BlendMode::VividLight => write!(f, "Vivid Light"),
BlendMode::LinearLight => write!(f, "Linear Light"),
BlendMode::PinLight => write!(f, "Pin Light"),
BlendMode::HardMix => write!(f, "Hard Mix"),
// Inversion group
BlendMode::Difference => write!(f, "Difference"),
BlendMode::Exclusion => write!(f, "Exclusion"),
BlendMode::Subtract => write!(f, "Subtract"),
BlendMode::Divide => write!(f, "Divide"),
// Component group
BlendMode::Hue => write!(f, "Hue"),
BlendMode::Saturation => write!(f, "Saturation"),
BlendMode::Color => write!(f, "Color"),
BlendMode::Luminosity => write!(f, "Luminosity"),
// Other utility blend modes (hidden from the normal list)
BlendMode::Erase => write!(f, "Erase"),
BlendMode::Restore => write!(f, "Restore"),
BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"),
}
}
}
+175
View File
@@ -0,0 +1,175 @@
use crate::raster::Image;
use crate::raster_types::{CPU, RasterDataTable};
use crate::registry::types::Percentage;
use crate::vector::VectorDataTable;
use crate::{BlendMode, Color, Ctx, GraphicElement, GraphicGroupTable};
pub(super) trait MultiplyAlpha {
fn multiply_alpha(&mut self, factor: f64);
}
impl MultiplyAlpha for Color {
fn multiply_alpha(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyAlpha for VectorDataTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for GraphicGroupTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for RasterDataTable<CPU>
where
GraphicElement: From<Image<Color>>,
{
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
pub(super) trait MultiplyFill {
fn multiply_fill(&mut self, factor: f64);
}
impl MultiplyFill for Color {
fn multiply_fill(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyFill for VectorDataTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for GraphicGroupTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for RasterDataTable<CPU> {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
trait SetBlendMode {
fn set_blend_mode(&mut self, blend_mode: BlendMode);
}
impl SetBlendMode for VectorDataTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for GraphicGroupTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for RasterDataTable<CPU> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
trait SetClip {
fn set_clip(&mut self, clip: bool);
}
impl SetClip for VectorDataTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for GraphicGroupTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for RasterDataTable<CPU> {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
#[node_macro::node(category("Style"))]
fn blend_mode<T: SetBlendMode>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value
}
#[node_macro::node(category("Style"))]
fn opacity<T: MultiplyAlpha>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
#[default(100.)] opacity: Percentage,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.multiply_alpha(opacity / 100.);
value
}
#[node_macro::node(category("Style"))]
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
#[default(100.)] opacity: Percentage,
#[default(100.)] fill: Percentage,
#[default(false)] clip: bool,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value.multiply_alpha(opacity / 100.);
value.multiply_fill(fill / 100.);
value.set_clip(clip);
value
}
+24
View File
@@ -0,0 +1,24 @@
use crate::Color;
use glam::{DAffine2, DVec2};
pub trait BoundingBox {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]>;
}
macro_rules! none_impl {
($t:path) => {
impl BoundingBox for $t {
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
None
}
}
};
}
none_impl!(String);
none_impl!(bool);
none_impl!(f32);
none_impl!(f64);
none_impl!(DVec2);
none_impl!(Option<Color>);
none_impl!(Vec<Color>);
@@ -1,5 +1,5 @@
use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float}; use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
use super::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use dyn_any::DynAny; use dyn_any::DynAny;
use half::f16; use half::f16;
@@ -345,7 +345,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.3, 0.14, 0.15, 0.92).unwrap(); /// let color = Color::from_rgbaf32(0.3, 0.14, 0.15, 0.92).unwrap();
/// assert!(color.components() == (0.3, 0.14, 0.15, 0.92)); /// assert!(color.components() == (0.3, 0.14, 0.15, 0.92));
/// ///
@@ -383,7 +383,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgb8_srgb(0x72, 0x67, 0x62); /// let color = Color::from_rgb8_srgb(0x72, 0x67, 0x62);
/// let color2 = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0xFF); /// let color2 = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0xFF);
/// assert_eq!(color, color2) /// assert_eq!(color, color2)
@@ -398,7 +398,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0x61); /// let color = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0x61);
/// ``` /// ```
#[inline(always)] #[inline(always)]
@@ -416,7 +416,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.); /// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.);
/// ``` /// ```
pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color { pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color {
@@ -458,7 +458,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.r() == 0.114); /// assert!(color.r() == 0.114);
/// ``` /// ```
@@ -471,7 +471,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.g() == 0.103); /// assert!(color.g() == 0.103);
/// ``` /// ```
@@ -484,7 +484,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.b() == 0.98); /// assert!(color.b() == 0.98);
/// ``` /// ```
@@ -497,7 +497,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.a() == 0.97); /// assert!(color.a() == 0.97);
/// ``` /// ```
@@ -773,7 +773,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert_eq!(color.components(), (0.114, 0.103, 0.98, 0.97)); /// assert_eq!(color.components(), (0.114, 0.103, 0.98, 0.97));
/// ``` /// ```
@@ -786,7 +786,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha /// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ``` /// ```
@@ -803,7 +803,7 @@ impl Color {
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in linear space. /// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in linear space.
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha /// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ``` /// ```
@@ -813,7 +813,7 @@ impl Color {
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in gamma space. /// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in gamma space.
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha /// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ``` /// ```
@@ -825,7 +825,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// // TODO: Add test /// // TODO: Add test
/// ``` /// ```
@@ -840,7 +840,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.).to_hsla(); /// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.).to_hsla();
/// ``` /// ```
pub fn to_hsla(&self) -> [f32; 4] { pub fn to_hsla(&self) -> [f32; 4] {
@@ -876,7 +876,7 @@ impl Color {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgba_str("7C67FA61").unwrap(); /// let color = Color::from_rgba_str("7C67FA61").unwrap();
/// ``` /// ```
pub fn from_rgba_str(color_str: &str) -> Option<Color> { pub fn from_rgba_str(color_str: &str) -> Option<Color> {
@@ -894,7 +894,7 @@ impl Color {
/// Creates a color from a 6-character RGB hex string (without a # prefix). /// Creates a color from a 6-character RGB hex string (without a # prefix).
/// ///
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::color::Color;
/// let color = Color::from_rgb_str("7C67FA").unwrap(); /// let color = Color::from_rgb_str("7C67FA").unwrap();
/// ``` /// ```
pub fn from_rgb_str(color_str: &str) -> Option<Color> { pub fn from_rgb_str(color_str: &str) -> Option<Color> {
+205
View File
@@ -0,0 +1,205 @@
use bytemuck::{Pod, Zeroable};
use glam::DVec2;
use std::fmt::Debug;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
pub use crate::blending::*;
pub trait Linear {
fn from_f32(x: f32) -> Self;
fn to_f32(self) -> f32;
fn from_f64(x: f64) -> Self;
fn to_f64(self) -> f64;
fn lerp(self, other: Self, value: Self) -> Self
where
Self: Sized + Copy,
Self: std::ops::Sub<Self, Output = Self>,
Self: std::ops::Mul<Self, Output = Self>,
Self: std::ops::Add<Self, Output = Self>,
{
self + (other - self) * value
}
}
#[rustfmt::skip]
impl Linear for f32 {
#[inline(always)] fn from_f32(x: f32) -> Self { x }
#[inline(always)] fn to_f32(self) -> f32 { self }
#[inline(always)] fn from_f64(x: f64) -> Self { x as f32 }
#[inline(always)] fn to_f64(self) -> f64 { self as f64 }
}
#[rustfmt::skip]
impl Linear for f64 {
#[inline(always)] fn from_f32(x: f32) -> Self { x as f64 }
#[inline(always)] fn to_f32(self) -> f32 { self as f32 }
#[inline(always)] fn from_f64(x: f64) -> Self { x }
#[inline(always)] fn to_f64(self) -> f64 { self }
}
pub trait Channel: Copy + Debug {
fn to_linear<Out: Linear>(self) -> Out;
fn from_linear<In: Linear>(linear: In) -> Self;
}
pub trait LinearChannel: Channel {
fn cast_linear_channel<Out: LinearChannel>(self) -> Out {
Out::from_linear(self.to_linear::<f64>())
}
}
impl<T: Linear + Debug + Copy> Channel for T {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
Out::from_f64(self.to_f64())
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
Self::from_f64(linear.to_f64())
}
}
impl<T: Linear + Debug + Copy> LinearChannel for T {}
use num_derive::*;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
pub struct SRGBGammaFloat(f32);
impl Channel for SRGBGammaFloat {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
let x = self.0;
Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) })
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
let x = linear.to_f32();
if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) }
}
}
pub trait RGBPrimaries {
const RED: DVec2;
const GREEN: DVec2;
const BLUE: DVec2;
const WHITE: DVec2;
}
pub trait Rec709Primaries {}
impl<T: Rec709Primaries> RGBPrimaries for T {
const RED: DVec2 = DVec2::new(0.64, 0.33);
const GREEN: DVec2 = DVec2::new(0.3, 0.6);
const BLUE: DVec2 = DVec2::new(0.15, 0.06);
const WHITE: DVec2 = DVec2::new(0.3127, 0.329);
}
pub trait SRGB: Rec709Primaries {}
pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {}
#[cfg(not(feature = "serde"))]
pub trait Serde {}
impl<T: serde::Serialize + for<'a> serde::Deserialize<'a>> Serde for T {}
#[cfg(not(feature = "serde"))]
impl<T> Serde for T {}
// TODO: Come up with a better name for this trait
pub trait Pixel: Clone + Pod + Zeroable + Default {
#[cfg(not(target_arch = "spirv"))]
fn to_bytes(&self) -> Vec<u8> {
bytemuck::bytes_of(self).to_vec()
}
// TODO: use u8 for Color
fn from_bytes(bytes: &[u8]) -> Self {
*bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel")
}
fn byte_size() -> usize {
size_of::<Self>()
}
}
pub trait RGB: Pixel {
type ColorChannel: Channel;
fn red(&self) -> Self::ColorChannel;
fn r(&self) -> Self::ColorChannel {
self.red()
}
fn green(&self) -> Self::ColorChannel;
fn g(&self) -> Self::ColorChannel {
self.green()
}
fn blue(&self) -> Self::ColorChannel;
fn b(&self) -> Self::ColorChannel {
self.blue()
}
}
pub trait RGBMut: RGB {
fn set_red(&mut self, red: Self::ColorChannel);
fn set_green(&mut self, green: Self::ColorChannel);
fn set_blue(&mut self, blue: Self::ColorChannel);
}
pub trait AssociatedAlpha: RGB + Alpha {
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
}
pub trait UnassociatedAlpha: RGB + Alpha {
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
}
pub trait Alpha {
type AlphaChannel: LinearChannel;
const TRANSPARENT: Self;
fn alpha(&self) -> Self::AlphaChannel;
fn a(&self) -> Self::AlphaChannel {
self.alpha()
}
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
}
pub trait AlphaMut: Alpha {
fn set_alpha(&mut self, value: Self::AlphaChannel);
}
pub trait Depth {
type DepthChannel: Channel;
fn depth(&self) -> Self::DepthChannel;
fn d(&self) -> Self::DepthChannel {
self.depth()
}
}
pub trait ExtraChannels<const NUM: usize> {
type ChannelType: Channel;
fn extra_channels(&self) -> [Self::ChannelType; NUM];
}
pub trait Luminance {
type LuminanceChannel: LinearChannel;
fn luminance(&self) -> Self::LuminanceChannel;
fn l(&self) -> Self::LuminanceChannel {
self.luminance()
}
}
pub trait LuminanceMut: Luminance {
fn set_luminance(&mut self, luminance: Self::LuminanceChannel);
}
// TODO: We might rename this to Raster at some point
pub trait Sample {
type Pixel: Pixel;
// TODO: Add an area parameter
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
}
impl<T: Sample> Sample for &T {
type Pixel = T::Pixel;
#[inline(always)]
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
(**self).sample(pos, area)
}
}
+7
View File
@@ -0,0 +1,7 @@
mod color;
mod color_traits;
mod discrete_srgb;
pub use color::*;
pub use color_traits::*;
pub use discrete_srgb::*;
+248
View File
@@ -0,0 +1,248 @@
use crate::Color;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum GradientType {
#[default]
Linear,
Radial,
}
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
// TODO: Use linear not gamma colors
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct GradientStops(pub Vec<(f64, Color)>);
impl std::hash::Hash for GradientStops {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.len().hash(state);
self.0.iter().for_each(|(position, color)| {
position.to_bits().hash(state);
color.hash(state);
});
}
}
impl Default for GradientStops {
fn default() -> Self {
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
}
}
impl IntoIterator for GradientStops {
type Item = (f64, Color);
type IntoIter = std::vec::IntoIter<(f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a GradientStops {
type Item = &'a (f64, Color);
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl std::ops::Index<usize> for GradientStops {
type Output = (f64, Color);
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl std::ops::Deref for GradientStops {
type Target = Vec<(f64, Color)>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for GradientStops {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl GradientStops {
pub fn new(stops: Vec<(f64, Color)>) -> Self {
let mut stops = Self(stops);
stops.sort();
stops
}
pub fn evaluate(&self, t: f64) -> Color {
if self.0.is_empty() {
return Color::BLACK;
}
if t <= self.0[0].0 {
return self.0[0].1;
}
if t >= self.0[self.0.len() - 1].0 {
return self.0[self.0.len() - 1].1;
}
for i in 0..self.0.len() - 1 {
let (t1, c1) = self.0[i];
let (t2, c2) = self.0[i + 1];
if t >= t1 && t <= t2 {
let normalized_t = (t - t1) / (t2 - t1);
return c1.lerp(&c2, normalized_t as f32);
}
}
Color::BLACK
}
pub fn sort(&mut self) {
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
}
pub fn reversed(&self) -> Self {
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
}
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
}
}
/// A gradient fill.
///
/// Contains the start and end points, along with the colors at varying points along the length.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct Gradient {
pub stops: GradientStops,
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
pub transform: DAffine2,
}
impl Default for Gradient {
fn default() -> Self {
Self {
stops: GradientStops::default(),
gradient_type: GradientType::Linear,
start: DVec2::new(0., 0.5),
end: DVec2::new(1., 0.5),
transform: DAffine2::IDENTITY,
}
}
}
impl std::hash::Hash for Gradient {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.stops.0.len().hash(state);
[].iter()
.chain(self.start.to_array().iter())
.chain(self.end.to_array().iter())
.chain(self.transform.to_cols_array().iter())
.chain(self.stops.0.iter().map(|(position, _)| position))
.for_each(|x| x.to_bits().hash(state));
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
self.gradient_type.hash(state);
}
}
impl std::fmt::Display for Gradient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f64| (x * 1e3).round() / 1e3;
let stops = self
.stops
.0
.iter()
.map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb()))
.collect::<Vec<_>>()
.join(", ");
write!(f, "{} Gradient: {stops}", self.gradient_type)
}
}
impl Gradient {
/// Constructs a new gradient with the colors at 0 and 1 specified.
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self {
Gradient {
start,
end,
stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
transform,
gradient_type,
}
}
pub fn lerp(&self, other: &Self, time: f64) -> Self {
let start = self.start + (other.start - self.start) * time;
let end = self.end + (other.end - self.end) * time;
let transform = self.transform;
let stops = self
.stops
.0
.iter()
.zip(other.stops.0.iter())
.map(|((a_pos, a_color), (b_pos, b_color))| {
let position = a_pos + (b_pos - a_pos) * time;
let color = a_color.lerp(b_color, time as f32);
(position, color)
})
.collect::<Vec<_>>();
let stops = GradientStops::new(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
Self {
start,
end,
transform,
stops,
gradient_type,
}
}
/// Insert a stop into the gradient, the index if successful
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
// Transform the start and end positions to the same coordinate space as the mouse.
let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end));
// Calculate the new position by finding the closest point on the line
let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
// Don't insert point past end of line
if !(0. ..=1.).contains(&new_position) {
return None;
}
// Compute the color of the inserted stop
let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) {
// Lerp between the nearest colors if applicable
(a, Some(b)) => a.lerp(
&b,
((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32,
),
// Use the start or the end color if applicable
(v, _) => v,
};
// Compute the correct index to keep the positions in order
let mut index = 0;
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
index += 1;
}
let new_color = get_color(index - 1, new_position);
// Insert the new stop
self.stops.0.insert(index, (new_position, new_color));
Some(index)
}
}
+55 -62
View File
@@ -1,5 +1,7 @@
use crate::blending::AlphaBlending;
use crate::bounds::BoundingBox;
use crate::instances::{Instance, Instances}; use crate::instances::{Instance, Instances};
use crate::raster::BlendMode; use crate::math::quad::Quad;
use crate::raster::image::Image; use crate::raster::image::Image;
use crate::raster_types::{CPU, GPU, Raster, RasterDataTable}; use crate::raster_types::{CPU, GPU, Raster, RasterDataTable};
use crate::transform::TransformMut; use crate::transform::TransformMut;
@@ -7,68 +9,9 @@ use crate::uuid::NodeId;
use crate::vector::{VectorData, VectorDataTable}; use crate::vector::{VectorData, VectorDataTable};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use std::hash::Hash; use std::hash::Hash;
pub mod renderer;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct AlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
impl Default for AlphaBlending {
fn default() -> Self {
Self::new()
}
}
impl Hash for AlphaBlending {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.opacity.to_bits().hash(state);
self.fill.to_bits().hash(state);
self.blend_mode.hash(state);
self.clip.hash(state);
}
}
impl std::fmt::Display for AlphaBlending {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f32| (x * 1e3).round() / 1e3;
write!(
f,
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
self.blend_mode,
round(self.opacity * 100.),
round(self.fill * 100.),
if self.clip { "Yes" } else { "No" }
)
}
}
impl AlphaBlending {
pub const fn new() -> Self {
Self {
opacity: 1.,
fill: 1.,
blend_mode: BlendMode::Normal,
clip: false,
}
}
pub fn lerp(&self, other: &Self, t: f32) -> Self {
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
AlphaBlending {
opacity: lerp(self.opacity, other.opacity, t),
fill: lerp(self.fill, other.fill, t),
blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode },
clip: if t < 0.5 { self.clip } else { other.clip },
}
}
}
// TODO: Eventually remove this migration document upgrade code // TODO: Eventually remove this migration document upgrade code
pub fn migrate_graphic_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GraphicGroupTable, D::Error> { pub fn migrate_graphic_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GraphicGroupTable, D::Error> {
use serde::Deserialize; use serde::Deserialize;
@@ -239,6 +182,25 @@ impl GraphicElement {
} }
} }
impl BoundingBox for GraphicElement {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
match self {
GraphicElement::VectorData(vector_data) => vector_data.bounding_box(transform, include_stroke),
GraphicElement::RasterDataCPU(raster) => raster.bounding_box(transform, include_stroke),
GraphicElement::RasterDataGPU(raster) => raster.bounding_box(transform, include_stroke),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform, include_stroke),
}
}
}
impl BoundingBox for GraphicGroupTable {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.filter_map(|element| element.instance.bounding_box(transform * *element.transform, include_stroke))
.reduce(Quad::combine_bounds)
}
}
impl<'de> serde::Deserialize<'de> for Raster<CPU> { impl<'de> serde::Deserialize<'de> for Raster<CPU> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
@@ -304,6 +266,20 @@ impl Artboard {
} }
} }
impl BoundingBox for Artboard {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
if self.clip {
Some(artboard_bounds)
} else {
[self.graphic_group.bounding_box(transform, include_stroke), Some(artboard_bounds)]
.into_iter()
.flatten()
.reduce(Quad::combine_bounds)
}
}
}
// TODO: Eventually remove this migration document upgrade code // TODO: Eventually remove this migration document upgrade code
pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<ArtboardGroupTable, D::Error> { pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<ArtboardGroupTable, D::Error> {
use serde::Deserialize; use serde::Deserialize;
@@ -339,8 +315,21 @@ pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D)
pub type ArtboardGroupTable = Instances<Artboard>; pub type ArtboardGroupTable = Instances<Artboard>;
impl BoundingBox for ArtboardGroupTable {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.filter_map(|instance| instance.instance.bounding_box(transform, include_stroke))
.reduce(Quad::combine_bounds)
}
}
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn layer(_: impl Ctx, mut stack: GraphicGroupTable, element: GraphicElement, node_path: Vec<NodeId>) -> GraphicGroupTable { async fn layer<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] mut stack: Instances<I>,
#[implementations(GraphicElement, VectorData, Raster<CPU>, Raster<GPU>)] element: I,
node_path: Vec<NodeId>,
) -> Instances<I> {
// Get the penultimate element of the node path, or None if the path is too short // Get the penultimate element of the node path, or None if the path is too short
let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied(); let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
@@ -558,3 +547,7 @@ impl From<GraphicGroupTable> for GraphicElement {
GraphicElement::GraphicGroup(graphic_group) GraphicElement::GraphicGroup(graphic_group)
} }
} }
pub trait ToGraphicElement {
fn to_graphic_element(&self) -> GraphicElement;
}
+26 -14
View File
@@ -1,38 +1,41 @@
#[macro_use] #[macro_use]
extern crate log; extern crate log;
pub use crate as graphene_core;
pub use ctor;
pub use num_traits;
pub mod animation; pub mod animation;
pub mod blending;
pub mod blending_nodes;
pub mod bounds;
pub mod color;
pub mod consts; pub mod consts;
pub mod context; pub mod context;
pub mod generic; pub mod generic;
pub mod gradient;
mod graphic_element;
pub mod instances; pub mod instances;
pub mod logic; pub mod logic;
pub mod math;
pub mod memo;
pub mod misc; pub mod misc;
pub mod ops; pub mod ops;
pub mod raster;
pub mod raster_types; pub mod raster_types;
pub mod registry;
pub mod structural; pub mod structural;
pub mod text; pub mod text;
pub mod transform;
pub mod transform_nodes;
pub mod uuid; pub mod uuid;
pub mod value; pub mod value;
pub mod memo;
pub mod raster;
pub mod transform;
mod graphic_element;
pub use graphic_element::*;
pub mod vector; pub mod vector;
pub mod registry; pub use crate as graphene_core;
pub use blending::*;
pub use context::*; pub use context::*;
pub use ctor;
pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync}; pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
pub use graphic_element::*;
pub use memo::MemoHash; pub use memo::MemoHash;
pub use num_traits;
pub use raster::Color; pub use raster::Color;
use std::any::TypeId; use std::any::TypeId;
use std::future::Future; use std::future::Future;
@@ -159,3 +162,12 @@ pub trait NodeInputDecleration {
fn identifier() -> &'static str; fn identifier() -> &'static str;
type Result; type Result;
} }
pub trait AsU32 {
fn as_u32(&self) -> u32;
}
impl AsU32 for u32 {
fn as_u32(&self) -> u32 {
*self
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ use crate::vector::VectorDataTable;
use crate::{Color, Context, Ctx}; use crate::{Color, Context, Ctx};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"), name("Log to Console"))]
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T { fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself // KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("{:#?}", value); log::debug!("{:#?}", value);
+25
View File
@@ -0,0 +1,25 @@
use crate::math::quad::Quad;
use crate::math::rect::Rect;
use bezier_rs::Bezier;
pub trait QuadExt {
/// Get all the edges in the rect as linear bezier curves
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
}
impl QuadExt for Quad {
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
self.all_edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
}
}
pub trait RectExt {
/// Get all the edges in the quad as linear bezier curves
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
}
impl RectExt for Rect {
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
self.edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod bbox;
pub mod math_ext;
pub mod quad;
pub mod rect;
@@ -58,11 +58,6 @@ impl Quad {
self.edges().into_iter().all(|[a, b]| (a - b).length_squared() >= width.powi(2)) self.edges().into_iter().all(|[a, b]| (a - b).length_squared() >= width.powi(2))
} }
/// Get all the edges in the quad as linear bezier curves
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
self.all_edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
}
/// Generates the axis aligned bounding box of the quad /// Generates the axis aligned bounding box of the quad
pub fn bounding_box(&self) -> [DVec2; 2] { pub fn bounding_box(&self) -> [DVec2; 2] {
[ [
@@ -1,4 +1,4 @@
use super::Quad; use crate::math::quad::Quad;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
#[derive(Debug, Clone, Default, Copy, PartialEq)] #[derive(Debug, Clone, Default, Copy, PartialEq)]
@@ -43,11 +43,6 @@ impl Rect {
[[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]]] [[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]]]
} }
/// Get all the edges in the rect as linear bezier curves
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
self.edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
}
/// Gets the center of a rect /// Gets the center of a rect
#[must_use] #[must_use]
pub fn center(&self) -> DVec2 { pub fn center(&self) -> DVec2 {
+70
View File
@@ -571,6 +571,76 @@ where
} }
} }
/// The [`Convert`] trait allows for conversion between Rust primitive numeric types.
/// Because number casting is lossy, we cannot use the normal [`Into`] trait like we do for other types.
pub trait Convert<T>: Sized {
/// Converts this type into the (usually inferred) output type.
#[must_use]
fn convert(self) -> T;
}
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
macro_rules! impl_convert {
($from:ty,$to:ty) => {
impl Convert<$to> for $from {
fn convert(self) -> $to {
self as $to
}
}
};
($to:ty) => {
impl_convert!(f32, $to);
impl_convert!(f64, $to);
impl_convert!(i8, $to);
impl_convert!(u8, $to);
impl_convert!(u16, $to);
impl_convert!(i16, $to);
impl_convert!(i32, $to);
impl_convert!(u32, $to);
impl_convert!(i64, $to);
impl_convert!(u64, $to);
impl_convert!(i128, $to);
impl_convert!(u128, $to);
impl_convert!(isize, $to);
impl_convert!(usize, $to);
};
}
impl_convert!(f32);
impl_convert!(f64);
impl_convert!(i8);
impl_convert!(u8);
impl_convert!(u16);
impl_convert!(i16);
impl_convert!(i32);
impl_convert!(u32);
impl_convert!(i64);
impl_convert!(u64);
impl_convert!(i128);
impl_convert!(u128);
impl_convert!(isize);
impl_convert!(usize);
// Convert
pub struct ConvertNode<O>(PhantomData<O>);
impl<_O> ConvertNode<_O> {
pub const fn new() -> Self {
Self(core::marker::PhantomData)
}
}
impl<_O> Default for ConvertNode<_O> {
fn default() -> Self {
Self::new()
}
}
impl<'input, I: 'input + Convert<_O> + Sync + Send, _O: 'input> Node<'input, I> for ConvertNode<_O> {
type Output = ::dyn_any::DynFuture<'input, _O>;
#[inline]
fn eval(&'input self, input: I) -> Self::Output {
Box::pin(async move { input.convert() })
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
+8 -314
View File
@@ -1,222 +1,25 @@
pub use self::color::{Color, Luma, SRGBA8};
use crate::Ctx;
use crate::GraphicGroupTable; use crate::GraphicGroupTable;
pub use crate::color::*;
use crate::raster_types::{CPU, RasterDataTable}; use crate::raster_types::{CPU, RasterDataTable};
use crate::registry::types::Percentage;
use crate::vector::VectorDataTable; use crate::vector::VectorDataTable;
use bytemuck::{Pod, Zeroable};
use glam::DVec2;
use std::fmt::Debug; use std::fmt::Debug;
#[cfg(target_arch = "spirv")] #[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float; use spirv_std::num_traits::float::Float;
/// as to not yet rename all references
pub mod color {
pub use super::*;
}
pub mod adjustments; pub mod adjustments;
pub mod bbox;
pub mod brush_cache; pub mod brush_cache;
pub mod color;
pub mod curve; pub mod curve;
pub mod discrete_srgb; pub mod image;
pub use self::image::Image;
pub use adjustments::*; pub use adjustments::*;
pub trait Linear {
fn from_f32(x: f32) -> Self;
fn to_f32(self) -> f32;
fn from_f64(x: f64) -> Self;
fn to_f64(self) -> f64;
fn lerp(self, other: Self, value: Self) -> Self
where
Self: Sized + Copy,
Self: std::ops::Sub<Self, Output = Self>,
Self: std::ops::Mul<Self, Output = Self>,
Self: std::ops::Add<Self, Output = Self>,
{
self + (other - self) * value
}
}
#[rustfmt::skip]
impl Linear for f32 {
#[inline(always)] fn from_f32(x: f32) -> Self { x }
#[inline(always)] fn to_f32(self) -> f32 { self }
#[inline(always)] fn from_f64(x: f64) -> Self { x as f32 }
#[inline(always)] fn to_f64(self) -> f64 { self as f64 }
}
#[rustfmt::skip]
impl Linear for f64 {
#[inline(always)] fn from_f32(x: f32) -> Self { x as f64 }
#[inline(always)] fn to_f32(self) -> f32 { self as f32 }
#[inline(always)] fn from_f64(x: f64) -> Self { x }
#[inline(always)] fn to_f64(self) -> f64 { self }
}
pub trait Channel: Copy + Debug {
fn to_linear<Out: Linear>(self) -> Out;
fn from_linear<In: Linear>(linear: In) -> Self;
}
pub trait LinearChannel: Channel {
fn cast_linear_channel<Out: LinearChannel>(self) -> Out {
Out::from_linear(self.to_linear::<f64>())
}
}
impl<T: Linear + Debug + Copy> Channel for T {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
Out::from_f64(self.to_f64())
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
Self::from_f64(linear.to_f64())
}
}
impl<T: Linear + Debug + Copy> LinearChannel for T {}
use num_derive::*;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
pub struct SRGBGammaFloat(f32);
impl Channel for SRGBGammaFloat {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
let x = self.0;
Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) })
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
let x = linear.to_f32();
if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) }
}
}
pub trait RGBPrimaries {
const RED: DVec2;
const GREEN: DVec2;
const BLUE: DVec2;
const WHITE: DVec2;
}
pub trait Rec709Primaries {}
impl<T: Rec709Primaries> RGBPrimaries for T {
const RED: DVec2 = DVec2::new(0.64, 0.33);
const GREEN: DVec2 = DVec2::new(0.3, 0.6);
const BLUE: DVec2 = DVec2::new(0.15, 0.06);
const WHITE: DVec2 = DVec2::new(0.3127, 0.329);
}
pub trait SRGB: Rec709Primaries {}
pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {}
#[cfg(not(feature = "serde"))]
pub trait Serde {}
impl<T: serde::Serialize + for<'a> serde::Deserialize<'a>> Serde for T {}
#[cfg(not(feature = "serde"))]
impl<T> Serde for T {}
// TODO: Come up with a better name for this trait
pub trait Pixel: Clone + Pod + Zeroable + Default {
#[cfg(not(target_arch = "spirv"))]
fn to_bytes(&self) -> Vec<u8> {
bytemuck::bytes_of(self).to_vec()
}
// TODO: use u8 for Color
fn from_bytes(bytes: &[u8]) -> Self {
*bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel")
}
fn byte_size() -> usize {
size_of::<Self>()
}
}
pub trait RGB: Pixel {
type ColorChannel: Channel;
fn red(&self) -> Self::ColorChannel;
fn r(&self) -> Self::ColorChannel {
self.red()
}
fn green(&self) -> Self::ColorChannel;
fn g(&self) -> Self::ColorChannel {
self.green()
}
fn blue(&self) -> Self::ColorChannel;
fn b(&self) -> Self::ColorChannel {
self.blue()
}
}
pub trait RGBMut: RGB {
fn set_red(&mut self, red: Self::ColorChannel);
fn set_green(&mut self, green: Self::ColorChannel);
fn set_blue(&mut self, blue: Self::ColorChannel);
}
pub trait AssociatedAlpha: RGB + Alpha {
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
}
pub trait UnassociatedAlpha: RGB + Alpha {
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
}
pub trait Alpha {
type AlphaChannel: LinearChannel;
const TRANSPARENT: Self;
fn alpha(&self) -> Self::AlphaChannel;
fn a(&self) -> Self::AlphaChannel {
self.alpha()
}
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
}
pub trait AlphaMut: Alpha {
fn set_alpha(&mut self, value: Self::AlphaChannel);
}
pub trait Depth {
type DepthChannel: Channel;
fn depth(&self) -> Self::DepthChannel;
fn d(&self) -> Self::DepthChannel {
self.depth()
}
}
pub trait ExtraChannels<const NUM: usize> {
type ChannelType: Channel;
fn extra_channels(&self) -> [Self::ChannelType; NUM];
}
pub trait Luminance {
type LuminanceChannel: LinearChannel;
fn luminance(&self) -> Self::LuminanceChannel;
fn l(&self) -> Self::LuminanceChannel {
self.luminance()
}
}
pub trait LuminanceMut: Luminance {
fn set_luminance(&mut self, luminance: Self::LuminanceChannel);
}
// TODO: We might rename this to Raster at some point
pub trait Sample {
type Pixel: Pixel;
// TODO: Add an area parameter
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
}
impl<T: Sample> Sample for &T {
type Pixel = T::Pixel;
#[inline(always)]
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
(**self).sample(pos, area)
}
}
pub trait Bitmap { pub trait Bitmap {
type Pixel: Pixel; type Pixel: Pixel;
fn width(&self) -> u32; fn width(&self) -> u32;
@@ -282,112 +85,3 @@ impl<T: BitmapMut + Bitmap> BitmapMut for &mut T {
(*self).get_pixel_mut(x, y) (*self).get_pixel_mut(x, y)
} }
} }
pub use self::image::Image;
pub mod image;
trait SetBlendMode {
fn set_blend_mode(&mut self, blend_mode: BlendMode);
}
impl SetBlendMode for VectorDataTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for GraphicGroupTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for RasterDataTable<CPU> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
trait SetClip {
fn set_clip(&mut self, clip: bool);
}
impl SetClip for VectorDataTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for GraphicGroupTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for RasterDataTable<CPU> {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
#[node_macro::node(category("Style"))]
fn blend_mode<T: SetBlendMode>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value
}
#[node_macro::node(category("Style"))]
fn opacity<T: MultiplyAlpha>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
#[default(100.)] opacity: Percentage,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.multiply_alpha(opacity / 100.);
value
}
#[node_macro::node(category("Style"))]
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
#[default(100.)] opacity: Percentage,
#[default(100.)] fill: Percentage,
#[default(false)] clip: bool,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value.multiply_alpha(opacity / 100.);
value.multiply_fill(fill / 100.);
value.set_clip(clip);
value
}
+4 -291
View File
@@ -1,15 +1,15 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
use crate::GraphicElement;
use crate::blending::BlendMode;
use crate::raster::curve::{CubicSplines, CurveManipulatorGroup}; use crate::raster::curve::{CubicSplines, CurveManipulatorGroup};
use crate::raster::curve::{Curve, ValueMapperNode}; use crate::raster::curve::{Curve, ValueMapperNode};
use crate::raster::image::Image; use crate::raster::image::Image;
use crate::raster::{Channel, Color, Pixel}; use crate::raster::{Channel, Color, Pixel};
use crate::raster_types::{CPU, Raster, RasterDataTable}; use crate::raster_types::{CPU, Raster, RasterDataTable};
use crate::registry::types::{Angle, Percentage, SignedPercentage}; use crate::registry::types::{Angle, Percentage, SignedPercentage};
use crate::vector::VectorDataTable;
use crate::vector::style::GradientStops; use crate::vector::style::GradientStops;
use crate::{Ctx, Node}; use crate::{Ctx, Node};
use crate::{GraphicElement, GraphicGroupTable};
use dyn_any::DynAny; use dyn_any::DynAny;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt::Debug; use std::fmt::Debug;
@@ -41,217 +41,6 @@ pub enum LuminanceCalculation {
MaximumChannels, MaximumChannels,
} }
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type, serde::Serialize, serde::Deserialize)]
#[repr(i32)] // TODO: Enable Int8 capability for SPIR-V so that we don't need this?
pub enum BlendMode {
// Basic group
#[default]
Normal,
// Darken group
Darken,
Multiply,
ColorBurn,
LinearBurn,
DarkerColor,
// Lighten group
Lighten,
Screen,
ColorDodge,
LinearDodge,
LighterColor,
// Contrast group
Overlay,
SoftLight,
HardLight,
VividLight,
LinearLight,
PinLight,
HardMix,
// Inversion group
Difference,
Exclusion,
Subtract,
Divide,
// Component group
Hue,
Saturation,
Color,
Luminosity,
// Other stuff
Erase,
Restore,
MultiplyAlpha,
}
impl BlendMode {
/// All standard blend modes ordered by group.
pub fn list() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
// Lighten group
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
// Contrast group
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
// Inversion group
&[Difference, Exclusion, Subtract, Divide],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
/// The subset of [`BlendMode::list()`] that is supported by SVG.
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn],
// Lighten group
&[Lighten, Screen, ColorDodge],
// Contrast group
&[Overlay, SoftLight, HardLight],
// Inversion group
&[Difference, Exclusion],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
pub fn index_in_list(&self) -> Option<usize> {
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
/// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
pub fn to_svg_style_name(&self) -> Option<&'static str> {
match self {
// Normal group
BlendMode::Normal => Some("normal"),
// Darken group
BlendMode::Darken => Some("darken"),
BlendMode::Multiply => Some("multiply"),
BlendMode::ColorBurn => Some("color-burn"),
// Lighten group
BlendMode::Lighten => Some("lighten"),
BlendMode::Screen => Some("screen"),
BlendMode::ColorDodge => Some("color-dodge"),
// Contrast group
BlendMode::Overlay => Some("overlay"),
BlendMode::SoftLight => Some("soft-light"),
BlendMode::HardLight => Some("hard-light"),
// Inversion group
BlendMode::Difference => Some("difference"),
BlendMode::Exclusion => Some("exclusion"),
// Component group
BlendMode::Hue => Some("hue"),
BlendMode::Saturation => Some("saturation"),
BlendMode::Color => Some("color"),
BlendMode::Luminosity => Some("luminosity"),
_ => None,
}
}
/// Renders the blend mode CSS style declaration.
pub fn render(&self) -> String {
format!(
r#" mix-blend-mode: {};"#,
self.to_svg_style_name().unwrap_or_else(|| {
warn!("Unsupported blend mode {self:?}");
"normal"
})
)
}
}
impl std::fmt::Display for BlendMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"),
// Darken group
BlendMode::Darken => write!(f, "Darken"),
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::ColorBurn => write!(f, "Color Burn"),
BlendMode::LinearBurn => write!(f, "Linear Burn"),
BlendMode::DarkerColor => write!(f, "Darker Color"),
// Lighten group
BlendMode::Lighten => write!(f, "Lighten"),
BlendMode::Screen => write!(f, "Screen"),
BlendMode::ColorDodge => write!(f, "Color Dodge"),
BlendMode::LinearDodge => write!(f, "Linear Dodge"),
BlendMode::LighterColor => write!(f, "Lighter Color"),
// Contrast group
BlendMode::Overlay => write!(f, "Overlay"),
BlendMode::SoftLight => write!(f, "Soft Light"),
BlendMode::HardLight => write!(f, "Hard Light"),
BlendMode::VividLight => write!(f, "Vivid Light"),
BlendMode::LinearLight => write!(f, "Linear Light"),
BlendMode::PinLight => write!(f, "Pin Light"),
BlendMode::HardMix => write!(f, "Hard Mix"),
// Inversion group
BlendMode::Difference => write!(f, "Difference"),
BlendMode::Exclusion => write!(f, "Exclusion"),
BlendMode::Subtract => write!(f, "Subtract"),
BlendMode::Divide => write!(f, "Divide"),
// Component group
BlendMode::Hue => write!(f, "Hue"),
BlendMode::Saturation => write!(f, "Saturation"),
BlendMode::Color => write!(f, "Color"),
BlendMode::Luminosity => write!(f, "Luminosity"),
// Other utility blend modes (hidden from the normal list)
BlendMode::Erase => write!(f, "Erase"),
BlendMode::Restore => write!(f, "Restore"),
BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"),
}
}
}
#[cfg(feature = "vello")]
impl From<BlendMode> for vello::peniko::Mix {
fn from(val: BlendMode) -> Self {
match val {
// Normal group
BlendMode::Normal => vello::peniko::Mix::Normal,
// Darken group
BlendMode::Darken => vello::peniko::Mix::Darken,
BlendMode::Multiply => vello::peniko::Mix::Multiply,
BlendMode::ColorBurn => vello::peniko::Mix::ColorBurn,
// Lighten group
BlendMode::Lighten => vello::peniko::Mix::Lighten,
BlendMode::Screen => vello::peniko::Mix::Screen,
BlendMode::ColorDodge => vello::peniko::Mix::ColorDodge,
// Contrast group
BlendMode::Overlay => vello::peniko::Mix::Overlay,
BlendMode::SoftLight => vello::peniko::Mix::SoftLight,
BlendMode::HardLight => vello::peniko::Mix::HardLight,
// Inversion group
BlendMode::Difference => vello::peniko::Mix::Difference,
BlendMode::Exclusion => vello::peniko::Mix::Exclusion,
// Component group
BlendMode::Hue => vello::peniko::Mix::Hue,
BlendMode::Saturation => vello::peniko::Mix::Saturation,
BlendMode::Color => vello::peniko::Mix::Color,
BlendMode::Luminosity => vello::peniko::Mix::Luminosity,
_ => todo!(),
}
}
}
#[node_macro::node(category("Raster: Adjustment"))] #[node_macro::node(category("Raster: Adjustment"))]
fn luminance<T: Adjust<Color>>( fn luminance<T: Adjust<Color>>(
_: impl Ctx, _: impl Ctx,
@@ -1272,70 +1061,6 @@ async fn selective_color<T: Adjust<Color>>(
image image
} }
pub(super) trait MultiplyAlpha {
fn multiply_alpha(&mut self, factor: f64);
}
impl MultiplyAlpha for Color {
fn multiply_alpha(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyAlpha for VectorDataTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for GraphicGroupTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for RasterDataTable<CPU>
where
GraphicElement: From<Image<Color>>,
{
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
pub(super) trait MultiplyFill {
fn multiply_fill(&mut self, factor: f64);
}
impl MultiplyFill for Color {
fn multiply_fill(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyFill for VectorDataTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for GraphicGroupTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for RasterDataTable<CPU> {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
// Aims for interoperable compatibility with: // Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=nvrt%27%20%3D%20Invert-,%27post%27%20%3D%20Posterize,-%27thrs%27%20%3D%20Threshold // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=nvrt%27%20%3D%20Invert-,%27post%27%20%3D%20Posterize,-%27thrs%27%20%3D%20Threshold
// //
@@ -1499,22 +1224,10 @@ fn color_overlay<T: Adjust<Color>>(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::raster::adjustments::BlendMode; use crate::Color;
use crate::blending::BlendMode;
use crate::raster::image::Image; use crate::raster::image::Image;
use crate::raster_types::{Raster, RasterDataTable}; use crate::raster_types::{Raster, RasterDataTable};
use crate::{Color, Node};
use std::pin::Pin;
#[derive(Clone)]
pub struct FutureWrapperNode<T: Clone>(T);
impl<'i, T: 'i + Clone + Send> Node<'i, ()> for FutureWrapperNode<T> {
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
fn eval(&'i self, _input: ()) -> Self::Output {
let value = self.0.clone();
Box::pin(async move { value })
}
}
#[tokio::test] #[tokio::test]
async fn color_overlay_multiply() { async fn color_overlay_multiply() {
+1 -1
View File
@@ -1,6 +1,6 @@
use super::Color; use super::Color;
use super::discrete_srgb::float_to_srgb_u8;
use crate::AlphaBlending; use crate::AlphaBlending;
use crate::color::float_to_srgb_u8;
use crate::instances::{Instance, Instances}; use crate::instances::{Instance, Instances};
use crate::raster_types::Raster; use crate::raster_types::Raster;
use core::hash::{Hash, Hasher}; use core::hash::{Hash, Hasher};
+17 -3
View File
@@ -1,8 +1,11 @@
use crate::Color; use crate::Color;
use crate::bounds::BoundingBox;
use crate::instances::Instances; use crate::instances::Instances;
use crate::math::quad::Quad;
use crate::raster::Image; use crate::raster::Image;
use core::ops::Deref; use core::ops::Deref;
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
use std::sync::Arc; use std::sync::Arc;
@@ -11,18 +14,18 @@ pub struct CPU;
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)] #[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)]
pub struct GPU; pub struct GPU;
trait Storage {} trait Storage: 'static {}
impl Storage for CPU {} impl Storage for CPU {}
impl Storage for GPU {} impl Storage for GPU {}
#[derive(Clone, Debug, Hash, PartialEq)] #[derive(Clone, Debug, Hash, PartialEq)]
#[allow(private_bounds)] #[allow(private_bounds)]
pub struct Raster<T: 'static + Storage> { pub struct Raster<T: Storage> {
data: RasterStorage, data: RasterStorage,
storage: T, storage: T,
} }
unsafe impl<T: 'static + Storage> dyn_any::StaticType for Raster<T> { unsafe impl<T: Storage> dyn_any::StaticType for Raster<T> {
type Static = Raster<T>; type Static = Raster<T>;
} }
#[derive(Clone, Debug, Hash, PartialEq, DynAny)] #[derive(Clone, Debug, Hash, PartialEq, DynAny)]
@@ -100,3 +103,14 @@ impl Deref for Raster<GPU> {
} }
} }
pub type RasterDataTable<Storage> = Instances<Raster<Storage>>; pub type RasterDataTable<Storage> = Instances<Raster<Storage>>;
impl<S: Storage> BoundingBox for RasterDataTable<S> {
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.flat_map(|instance| {
let transform = transform * *instance.transform;
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
})
.reduce(Quad::combine_bounds)
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ pub struct FieldMetadata {
pub unit: Option<&'static str>, pub unit: Option<&'static str>,
} }
pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync { pub trait ChoiceTypeStatic: Sized + Copy + crate::AsU32 + Send + Sync {
const WIDGET_HINT: ChoiceWidgetHint; const WIDGET_HINT: ChoiceWidgetHint;
const DESCRIPTION: Option<&'static str>; const DESCRIPTION: Option<&'static str>;
fn list() -> &'static [&'static [(Self, VariantMetadata)]]; fn list() -> &'static [&'static [(Self, VariantMetadata)]];
+3 -188
View File
@@ -1,8 +1,6 @@
use crate::instances::Instances; use crate::Artboard;
use crate::raster::bbox::AxisAlignedBbox; use crate::math::bbox::AxisAlignedBbox;
use crate::raster_types::{CPU, GPU, RasterDataTable}; pub use crate::vector::ReferencePoint;
use crate::vector::VectorDataTable;
use crate::{Artboard, CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl};
use core::f64; use core::f64;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
@@ -152,186 +150,3 @@ impl<T: TransformMut> ApplyTransform for T {
impl ApplyTransform for () { impl ApplyTransform for () {
fn apply_transform(&mut self, &_modification: &DAffine2) {} fn apply_transform(&mut self, &_modification: &DAffine2) {}
} }
#[node_macro::node(category(""))]
async fn transform<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
)]
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
translate: DVec2,
rotate: f64,
scale: DVec2,
shear: DVec2,
_pivot: DVec2,
) -> Instances<T> {
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied();
let mut ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint {
footprint.apply_transform(&matrix);
ctx = ctx.with_footprint(footprint);
}
let mut transform_target = transform_target.eval(ctx.into_context()).await;
for data_transform in transform_target.instance_mut_iter() {
*data_transform.transform = matrix * *data_transform.transform;
}
transform_target
}
#[node_macro::node(category(""))]
fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx,
#[implementations(VectorDataTable, RasterDataTable<CPU>, GraphicGroupTable)] mut data: Instances<Data>,
#[implementations(DAffine2)] transform: TransformInput,
) -> Instances<Data> {
for data_transform in data.instance_mut_iter() {
*data_transform.transform = transform.transform();
}
data
}
#[node_macro::node(category("Debug"))]
async fn boundless_footprint<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS);
transform_target.eval(ctx.into_context()).await
}
#[node_macro::node(category("Debug"))]
async fn freeze_real_time<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_real_time(0.);
transform_target.eval(ctx.into_context()).await
}
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ReferencePoint {
#[default]
None,
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight,
}
impl ReferencePoint {
pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option<DVec2> {
let size = bounding_box.size();
let offset = match self {
ReferencePoint::None => return None,
ReferencePoint::TopLeft => DVec2::ZERO,
ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.),
ReferencePoint::TopRight => DVec2::new(size.x, 0.),
ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.),
ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.),
ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.),
ReferencePoint::BottomLeft => DVec2::new(0., size.y),
ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y),
ReferencePoint::BottomRight => DVec2::new(size.x, size.y),
};
Some(bounding_box.start + offset)
}
}
impl From<&str> for ReferencePoint {
fn from(input: &str) -> Self {
match input {
"None" => ReferencePoint::None,
"TopLeft" => ReferencePoint::TopLeft,
"TopCenter" => ReferencePoint::TopCenter,
"TopRight" => ReferencePoint::TopRight,
"CenterLeft" => ReferencePoint::CenterLeft,
"Center" => ReferencePoint::Center,
"CenterRight" => ReferencePoint::CenterRight,
"BottomLeft" => ReferencePoint::BottomLeft,
"BottomCenter" => ReferencePoint::BottomCenter,
"BottomRight" => ReferencePoint::BottomRight,
_ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"),
}
}
}
impl From<ReferencePoint> for Option<DVec2> {
fn from(input: ReferencePoint) -> Self {
match input {
ReferencePoint::None => None,
ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)),
ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)),
ReferencePoint::TopRight => Some(DVec2::new(1., 0.)),
ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)),
ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)),
ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)),
ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)),
ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)),
ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)),
}
}
}
impl From<DVec2> for ReferencePoint {
fn from(input: DVec2) -> Self {
const TOLERANCE: f64 = 1e-5_f64;
if input.y.abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::TopLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::TopCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::TopRight;
}
} else if (input.y - 0.5).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::CenterLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::Center;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::CenterRight;
}
} else if (input.y - 1.).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::BottomLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::BottomCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::BottomRight;
}
}
ReferencePoint::None
}
}

Some files were not shown because too many files have changed in this diff Show More