Add nondestructive vector editing (#1676)

* Initial vector modify node

* Initial extraction of data from monitor nodes

* Migrate to point id

* Start converting to modify node

* Non destructive spline tool (tout le reste est cassé)

* Fix unconnected modify node

* Fix freehand tool

* Pen tool

* Migrate demo art

* Select points

* Fix the demo artwork

* Fix the X and Y inputs for path tool

* G1 continous toggle

* Delete points

* Fix test

* Insert point

* Improve robustness of handles

* Fix GRS shortcuts on path

* Dragging points

* Fix build

* Preserve opposing handle lengths

* Update demo art and snapping

* Fix polygon tool

* Double click end anchor

* Improve dragging

* Fix text shifting

* Select only connected verts

* Colinear alt

* Cleanup

* Fix imports

* Improve pen tool avoiding handle placement

* Improve disolve

* Remove pivot widget from Transform node properties

* Fix demo art

* Fix bugs

* Re-save demo artwork

* Code review

* Serialize hashmap as tuple vec to enable deserialize_inputs

* Fix migrate

* Add document upgrade function to editor_api.rs

* Finalize document upgrading

* Rename to the Path node

* Remove smoothing from Freehand tool

* Upgrade demo artwork

* Propertly disable raw-rs tests

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Adam <adamgerhant@gmail.com>
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
James Lindsay
2024-07-05 13:42:40 -07:00
committed by GitHub
co-authored by Keavon Chambers Adam Dennis Kobert
parent fd3613018a
commit 1652c713a6
96 changed files with 3343 additions and 2622 deletions
Generated
+2
View File
@@ -2392,6 +2392,7 @@ dependencies = [
"serde_json", "serde_json",
"specta", "specta",
"thiserror", "thiserror",
"tokio",
"usvg", "usvg",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
@@ -2415,6 +2416,7 @@ name = "graphite-wasm"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"bezier-rs", "bezier-rs",
"glam",
"graph-craft", "graph-craft",
"graphene-core", "graphene-core",
"graphite-editor", "graphite-editor",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
View File
@@ -64,3 +64,10 @@ web-sys = { workspace = true, features = [
[dev-dependencies] [dev-dependencies]
env_logger = "0.10" env_logger = "0.10"
futures = { workspace = true } futures = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }
[lints.rust]
# TODO: figure out why we check these features when they do not exist
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(feature, values("resvg", "vello"))',
] }
+2 -2
View File
@@ -21,8 +21,8 @@ impl Editor {
std::mem::take(&mut self.dispatcher.responses) std::mem::take(&mut self.dispatcher.responses)
} }
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) { pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> Result<(), String> {
self.dispatcher.poll_node_graph_evaluation(responses); self.dispatcher.poll_node_graph_evaluation(responses)
} }
} }
-1
View File
@@ -49,7 +49,6 @@ pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
pub const SELECTION_THRESHOLD: f64 = 10.; pub const SELECTION_THRESHOLD: f64 = 10.;
pub const HIDE_HANDLE_DISTANCE: f64 = 3.; pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.; pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.;
pub const INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE: f64 = 5.;
// Pen tool // Pen tool
pub const CREATE_CURVE_THRESHOLD: f64 = 5.; pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
+14 -9
View File
@@ -205,8 +205,8 @@ impl Dispatcher {
list list
} }
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) { pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> Result<(), String> {
self.message_handlers.portfolio_message_handler.poll_node_graph_evaluation(responses); self.message_handlers.portfolio_message_handler.poll_node_graph_evaluation(responses)
} }
/// Create the tree structure for logging the messages as a tree /// Create the tree structure for logging the messages as a tree
@@ -262,10 +262,7 @@ mod test {
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::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::ToolType;
use crate::test_utils::EditorTestUtils; use crate::test_utils::EditorTestUtils;
use graph_craft::document::NodeId;
use graphene_core::raster::color::Color; use graphene_core::raster::color::Color;
fn init_logger() { fn init_logger() {
@@ -412,11 +409,9 @@ mod test {
assert_eq!(layers_after_copy[5], shape_id); assert_eq!(layers_after_copy[5], shape_id);
} }
// TODO: Fix text #[tokio::test]
#[ignore]
#[test]
/// This test will fail when you make changes to the underlying serialization format for a document. /// This test will fail when you make changes to the underlying serialization format for a document.
fn check_if_demo_art_opens() { async fn check_if_demo_art_opens() {
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
let print_problem_to_terminal_on_failure = |value: &String| { let print_problem_to_terminal_on_failure = |value: &String| {
@@ -449,6 +444,16 @@ mod test {
document_name: document_name.into(), document_name: document_name.into(),
document_serialized_content, document_serialized_content,
}); });
println!("Responses:\n{responses:#?}");
// Check if the graph renders
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);
crate::node_graph_executor::run_node_graph().await;
let mut messages = VecDeque::new();
editor.poll_node_graph_evaluation(&mut messages).expect("Graph should render");
for response in responses { for response in responses {
// Check for the existence of the file format incompatibility warning dialog after opening the test file // Check for the existence of the file format incompatibility warning dialog after opening the test file
@@ -99,6 +99,19 @@ pub enum FrontendMessage {
#[serde(rename = "copyText")] #[serde(rename = "copyText")]
copy_text: String, copy_text: String,
}, },
// TODO: Eventually remove this (probably starting late 2024)
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,
},
TriggerViewportResize, TriggerViewportResize,
TriggerVisitLink { TriggerVisitLink {
url: String, url: String,
@@ -1236,32 +1236,27 @@ impl DocumentMessageHandler {
} }
/// Find any layers sorted by index that are under the given location in viewport space. /// Find any layers sorted by index that are under the given location in viewport space.
pub fn click_list_any(&self, viewport_location: DVec2, network: &NodeNetwork) -> Vec<LayerNodeIdentifier> { pub fn click_xray_no_artboards<'a>(&'a self, viewport_location: DVec2, network: &'a NodeNetwork) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
self.click_xray(viewport_location).filter(|&layer| !is_artboard(layer, network)).collect::<Vec<_>>() self.click_xray(viewport_location).filter(move |&layer| !is_artboard(layer, network))
} }
/// Find layers under the location in viewport space that was clicked, listed by their depth in the layer tree hierarchy. /// Find layers under the location in viewport space that was clicked, listed by their depth in the layer tree hierarchy.
pub fn click_list(&self, viewport_location: DVec2, network: &NodeNetwork) -> Vec<LayerNodeIdentifier> { pub fn click_list<'a>(&'a self, viewport_location: DVec2, network: &'a NodeNetwork) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
let mut node_list = self.click_list_any(viewport_location, network); self.click_xray_no_artboards(viewport_location, network)
node_list.truncate( .skip_while(|&layer| layer == LayerNodeIdentifier::ROOT_PARENT)
node_list .scan(true, |last_had_children, layer| {
.iter() if *last_had_children {
.position(|&layer| { *last_had_children = network.nodes.get(&layer.to_node()).map_or(false, |node| node.layer_has_child_layers(network));
if layer != LayerNodeIdentifier::ROOT_PARENT { Some(layer)
!network.nodes.get(&layer.to_node()).map(|node| node.layer_has_child_layers(network)).unwrap_or_default() } else {
} else { None
log::error!("ROOT_PARENT should not exist in click_list_any"); }
false })
}
})
.unwrap_or(0) + 1,
);
node_list
} }
/// Find the deepest layer that has been clicked on from a location in viewport space. /// Find the deepest layer that has been clicked on from a location in viewport space.
pub fn click(&self, viewport_location: DVec2, network: &NodeNetwork) -> Option<LayerNodeIdentifier> { pub fn click(&self, viewport_location: DVec2, network: &NodeNetwork) -> Option<LayerNodeIdentifier> {
self.click_list(viewport_location, network).last().copied() self.click_list(viewport_location, network).last()
} }
/// Get the combined bounding box of the click targets of the selected visible layers in viewport space /// Get the combined bounding box of the click targets of the selected visible layers in viewport space
@@ -1,5 +1,4 @@
use super::utility_types::TransformIn; use super::utility_types::TransformIn;
use super::utility_types::VectorDataModification;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*; use crate::messages::prelude::*;
@@ -7,9 +6,10 @@ use bezier_rs::Subpath;
use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font; use graphene_core::text::Font;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::PointId;
use graphene_core::vector::VectorModificationType;
use graphene_core::{Artboard, Color}; use graphene_core::{Artboard, Color};
use graphene_std::vector::misc::BooleanOperation; use graphene_std::vector::misc::BooleanOperation;
@@ -77,11 +77,6 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
blend_mode: BlendMode, blend_mode: BlendMode,
}, },
UpdateBounds {
layer: LayerNodeIdentifier,
old_bounds: [DVec2; 2],
new_bounds: [DVec2; 2],
},
StrokeSet { StrokeSet {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
stroke: Stroke, stroke: Stroke,
@@ -104,7 +99,7 @@ pub enum GraphOperationMessage {
}, },
Vector { Vector {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
modification: VectorDataModification, modification_type: VectorModificationType,
}, },
Brush { Brush {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
@@ -129,7 +124,7 @@ pub enum GraphOperationMessage {
}, },
NewVectorLayer { NewVectorLayer {
id: NodeId, id: NodeId,
subpaths: Vec<Subpath<ManipulatorGroupId>>, subpaths: Vec<Subpath<PointId>>,
parent: LayerNodeIdentifier, parent: LayerNodeIdentifier,
insert_index: isize, insert_index: isize,
}, },
@@ -1,4 +1,4 @@
use super::transform_utils::{self, LayerBounds}; use super::transform_utils;
use super::utility_types::ModifyInputsContext; use super::utility_types::ModifyInputsContext;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type; use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
@@ -9,10 +9,9 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork, Previewing}; use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork, Previewing};
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::text::Font; use graphene_core::text::Font;
use graphene_core::vector::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke}; use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, LineCap, LineJoin, Stroke};
use graphene_core::Color; use graphene_core::Color;
use graphene_std::vector::convert_usvg_path; use graphene_std::vector::convert_usvg_path;
use graphene_std::vector::style::GradientStops;
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
@@ -404,15 +403,6 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
modify_inputs.blend_mode_set(blend_mode); modify_inputs.blend_mode_set(blend_mode);
} }
} }
GraphOperationMessage::UpdateBounds { layer, old_bounds, new_bounds } => {
if layer == LayerNodeIdentifier::ROOT_PARENT {
log::error!("Cannot run UpdateBounds on ROOT_PARENT");
return;
}
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), document_network, document_metadata, node_graph, responses) {
modify_inputs.update_bounds(old_bounds, new_bounds);
}
}
GraphOperationMessage::StrokeSet { layer, stroke } => { GraphOperationMessage::StrokeSet { layer, stroke } => {
if layer == LayerNodeIdentifier::ROOT_PARENT { if layer == LayerNodeIdentifier::ROOT_PARENT {
log::error!("Cannot run StrokeSet on ROOT_PARENT"); log::error!("Cannot run StrokeSet on ROOT_PARENT");
@@ -433,9 +423,8 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
return; return;
} }
let parent_transform = document_metadata.downstream_transform_to_viewport(layer); let parent_transform = document_metadata.downstream_transform_to_viewport(layer);
let bounds = LayerBounds::new(document_metadata, layer);
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), document_network, document_metadata, node_graph, responses) { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), document_network, document_metadata, node_graph, responses) {
modify_inputs.transform_change(transform, transform_in, parent_transform, bounds, skip_rerender); modify_inputs.transform_change(transform, transform_in, parent_transform, skip_rerender);
} }
} }
GraphOperationMessage::TransformSet { GraphOperationMessage::TransformSet {
@@ -451,9 +440,8 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
let parent_transform = document_metadata.downstream_transform_to_viewport(layer); let parent_transform = document_metadata.downstream_transform_to_viewport(layer);
let current_transform = Some(document_metadata.transform_to_viewport(layer)); let current_transform = Some(document_metadata.transform_to_viewport(layer));
let bounds = LayerBounds::new(document_metadata, layer);
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), document_network, document_metadata, node_graph, responses) { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), document_network, document_metadata, node_graph, responses) {
modify_inputs.transform_set(transform, transform_in, parent_transform, current_transform, bounds, skip_rerender); modify_inputs.transform_set(transform, transform_in, parent_transform, current_transform, skip_rerender);
} }
} }
GraphOperationMessage::TransformSetPivot { layer, pivot } => { GraphOperationMessage::TransformSetPivot { layer, pivot } => {
@@ -461,21 +449,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
log::error!("Cannot run TransformSetPivot on ROOT_PARENT"); log::error!("Cannot run TransformSetPivot on ROOT_PARENT");
return; return;
} }
let bounds = LayerBounds::new(document_metadata, layer);
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), document_network, document_metadata, node_graph, responses) { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), document_network, document_metadata, node_graph, responses) {
modify_inputs.pivot_set(pivot, bounds); modify_inputs.pivot_set(pivot);
} }
} }
GraphOperationMessage::Vector { layer, modification } => { GraphOperationMessage::Vector { layer, modification_type } => {
if layer == LayerNodeIdentifier::ROOT_PARENT { if layer == LayerNodeIdentifier::ROOT_PARENT {
log::error!("Cannot run Vector on ROOT_PARENT"); log::error!("Cannot run Vector on ROOT_PARENT");
return; return;
} }
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), document_network, document_metadata, node_graph, responses) { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer.to_node(), document_network, document_metadata, node_graph, responses) {
let previous_layer = modify_inputs.vector_modify(modification); modify_inputs.vector_modify(modification_type);
if let Some(layer) = previous_layer {
responses.add(GraphOperationMessage::DeleteLayer { layer, reconnect: true })
}
} }
} }
GraphOperationMessage::Brush { layer, strokes } => { GraphOperationMessage::Brush { layer, strokes } => {
@@ -698,6 +682,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
GraphOperationMessage::SetName { layer, name } => { GraphOperationMessage::SetName { layer, name } => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
responses.add(GraphOperationMessage::SetNameImpl { layer, name }); responses.add(GraphOperationMessage::SetNameImpl { layer, name });
responses.add(NodeGraphMessage::RunDocumentGraph);
} }
GraphOperationMessage::SetNameImpl { layer, name } => { GraphOperationMessage::SetNameImpl { layer, name } => {
if let Some(node) = document_network.nodes.get_mut(&layer.to_node()) { if let Some(node) = document_network.nodes.get_mut(&layer.to_node()) {
@@ -829,10 +814,8 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
.unwrap_or_default(); .unwrap_or_default();
modify_inputs.insert_vector_data(subpaths, layer); modify_inputs.insert_vector_data(subpaths, layer);
let center = DAffine2::from_translation((bounds[0] + bounds[1]) / 2.);
modify_inputs.modify_inputs("Transform", true, |inputs, _node_id, _metadata| { modify_inputs.modify_inputs("Transform", true, |inputs, _node_id, _metadata| {
transform_utils::update_transform(inputs, center.inverse() * transform * usvg_transform(node.abs_transform()) * center); transform_utils::update_transform(inputs, transform * usvg_transform(node.abs_transform()));
}); });
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]); let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
@@ -1,14 +1,9 @@
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use bezier_rs::Subpath;
use bezier_rs::{ManipulatorGroup, Subpath};
use graph_craft::document::{value::TaggedValue, NodeInput}; use graph_craft::document::{value::TaggedValue, NodeInput};
use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::PointId;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use super::utility_types::VectorDataModification;
/// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`. /// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`.
pub fn compute_scale_angle_translation_shear(transform: DAffine2) -> (DVec2, f64, DVec2, DVec2) { pub fn compute_scale_angle_translation_shear(transform: DAffine2) -> (DVec2, f64, DVec2, DVec2) {
let x_axis = transform.matrix2.x_axis; let x_axis = transform.matrix2.x_axis;
@@ -53,7 +48,10 @@ pub struct LayerBounds {
impl LayerBounds { impl LayerBounds {
/// Extract the layer bounds and their transform for a layer. /// Extract the layer bounds and their transform for a layer.
pub fn new(metadata: &DocumentMetadata, layer: LayerNodeIdentifier) -> Self { pub fn new(
metadata: &crate::messages::portfolio::document::utility_types::document_metadata::DocumentMetadata,
layer: crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier,
) -> Self {
Self { Self {
bounds: metadata.nonzero_bounding_box(layer), bounds: metadata.nonzero_bounding_box(layer),
bounds_transform: DAffine2::IDENTITY, bounds_transform: DAffine2::IDENTITY,
@@ -184,7 +182,7 @@ fn clamp_bounds(bounds_min: DVec2, mut bounds_max: DVec2) -> [DVec2; 2] {
[bounds_min, bounds_max] [bounds_min, bounds_max]
} }
/// Returns corners of all subpaths /// Returns corners of all subpaths
fn subpath_bounds(subpaths: &[Subpath<ManipulatorGroupId>]) -> [DVec2; 2] { fn subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
subpaths subpaths
.iter() .iter()
.filter_map(|subpath| subpath.bounding_box()) .filter_map(|subpath| subpath.bounding_box())
@@ -193,116 +191,7 @@ fn subpath_bounds(subpaths: &[Subpath<ManipulatorGroupId>]) -> [DVec2; 2] {
} }
/// Returns corners of all subpaths (but expanded to avoid division-by-zero errors) /// Returns corners of all subpaths (but expanded to avoid division-by-zero errors)
pub fn nonzero_subpath_bounds(subpaths: &[Subpath<ManipulatorGroupId>]) -> [DVec2; 2] { pub fn nonzero_subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
let [bounds_min, bounds_max] = subpath_bounds(subpaths); let [bounds_min, bounds_max] = subpath_bounds(subpaths);
clamp_bounds(bounds_min, bounds_max) clamp_bounds(bounds_min, bounds_max)
} }
pub struct VectorModificationState<'a> {
pub subpaths: &'a mut Vec<Subpath<ManipulatorGroupId>>,
pub colinear_manipulators: &'a mut Vec<ManipulatorGroupId>,
}
impl<'a> VectorModificationState<'a> {
fn insert_start(&mut self, subpath_index: usize, manipulator_group: ManipulatorGroup<ManipulatorGroupId>) {
self.subpaths[subpath_index].insert_manipulator_group(0, manipulator_group)
}
fn insert_end(&mut self, subpath_index: usize, manipulator_group: ManipulatorGroup<ManipulatorGroupId>) {
let subpath = &mut self.subpaths[subpath_index];
subpath.insert_manipulator_group(subpath.len(), manipulator_group)
}
fn insert(&mut self, manipulator_group: ManipulatorGroup<ManipulatorGroupId>, after_id: ManipulatorGroupId) {
for subpath in self.subpaths.iter_mut() {
if let Some(index) = subpath.manipulator_index_from_id(after_id) {
subpath.insert_manipulator_group(index + 1, manipulator_group);
break;
}
}
}
fn remove_group(&mut self, id: ManipulatorGroupId) {
for subpath in self.subpaths.iter_mut() {
if let Some(index) = subpath.manipulator_index_from_id(id) {
subpath.remove_manipulator_group(index);
break;
}
}
}
fn remove_point(&mut self, point: ManipulatorPointId) {
for subpath in self.subpaths.iter_mut() {
if point.manipulator_type == SelectedType::Anchor {
if let Some(index) = subpath.manipulator_index_from_id(point.group) {
subpath.remove_manipulator_group(index);
break;
}
} else if let Some(group) = subpath.manipulator_mut_from_id(point.group) {
if point.manipulator_type == SelectedType::InHandle {
group.in_handle = None;
} else if point.manipulator_type == SelectedType::OutHandle {
group.out_handle = None;
}
}
}
}
fn set_manipulator_colinear_handles_state(&mut self, id: ManipulatorGroupId, colinear: bool) {
if !colinear {
self.colinear_manipulators.retain(|&manipulator_group_id| manipulator_group_id != id);
} else if !self.colinear_manipulators.contains(&id) {
self.colinear_manipulators.push(id);
}
}
fn toggle_manipulator_colinear_handles_state(&mut self, id: ManipulatorGroupId) {
if self.colinear_manipulators.contains(&id) {
self.colinear_manipulators.retain(|&manipulator_group_id| manipulator_group_id != id);
} else {
self.colinear_manipulators.push(id);
}
}
fn set_position(&mut self, point: ManipulatorPointId, position: DVec2) {
assert!(position.is_finite(), "Point position should be finite");
for subpath in self.subpaths.iter_mut() {
if let Some(manipulator) = subpath.manipulator_mut_from_id(point.group) {
match point.manipulator_type {
SelectedType::Anchor => manipulator.anchor = position,
SelectedType::InHandle => manipulator.in_handle = Some(position),
SelectedType::OutHandle => manipulator.out_handle = Some(position),
}
if point.manipulator_type != SelectedType::Anchor && self.colinear_manipulators.contains(&point.group) {
let reflect = |opposite: DVec2| {
(manipulator.anchor - position)
.try_normalize()
.map(|direction| direction * (opposite - manipulator.anchor).length() + manipulator.anchor)
.unwrap_or(opposite)
};
match point.manipulator_type {
SelectedType::InHandle => manipulator.out_handle = manipulator.out_handle.map(reflect),
SelectedType::OutHandle => manipulator.in_handle = manipulator.in_handle.map(reflect),
_ => {}
}
}
break;
}
}
}
pub fn modify(&mut self, modification: VectorDataModification) {
match modification {
VectorDataModification::AddEndManipulatorGroup { subpath_index, manipulator_group } => self.insert_end(subpath_index, manipulator_group),
VectorDataModification::AddStartManipulatorGroup { subpath_index, manipulator_group } => self.insert_start(subpath_index, manipulator_group),
VectorDataModification::AddManipulatorGroup { manipulator_group, after_id } => self.insert(manipulator_group, after_id),
VectorDataModification::RemoveManipulatorGroup { id } => self.remove_group(id),
VectorDataModification::RemoveManipulatorPoint { point } => self.remove_point(point),
VectorDataModification::SetClosed { index, closed } => self.subpaths[index].set_closed(closed),
VectorDataModification::SetManipulatorColinearHandlesState { id, colinear } => self.set_manipulator_colinear_handles_state(id, colinear),
VectorDataModification::SetManipulatorPosition { point, position } => self.set_position(point, position),
VectorDataModification::ToggleManipulatorColinearHandlesState { id } => self.toggle_manipulator_colinear_handles_state(id),
VectorDataModification::UpdateSubpaths { subpaths } => *self.subpaths = subpaths,
}
}
}
@@ -1,4 +1,4 @@
use super::transform_utils::{self, LayerBounds}; use super::transform_utils;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type; use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
@@ -10,12 +10,10 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{generate_uuid, DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, Previewing}; use graph_craft::document::{generate_uuid, DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, Previewing};
use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font; use graphene_core::text::Font;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Type; use graphene_core::vector::{PointId, VectorModificationType};
use graphene_core::{Artboard, Color}; use graphene_core::{Artboard, Color, Type};
use graphene_std::vector::ManipulatorPointId;
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;
@@ -29,22 +27,6 @@ pub enum TransformIn {
Viewport, Viewport,
} }
type ManipulatorGroup = bezier_rs::ManipulatorGroup<ManipulatorGroupId>;
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum VectorDataModification {
AddEndManipulatorGroup { subpath_index: usize, manipulator_group: ManipulatorGroup },
AddManipulatorGroup { manipulator_group: ManipulatorGroup, after_id: ManipulatorGroupId },
AddStartManipulatorGroup { subpath_index: usize, manipulator_group: ManipulatorGroup },
RemoveManipulatorGroup { id: ManipulatorGroupId },
RemoveManipulatorPoint { point: ManipulatorPointId },
SetClosed { index: usize, closed: bool },
SetManipulatorColinearHandlesState { id: ManipulatorGroupId, colinear: bool },
SetManipulatorPosition { point: ManipulatorPointId, position: DVec2 },
ToggleManipulatorColinearHandlesState { id: ManipulatorGroupId },
UpdateSubpaths { subpaths: Vec<Subpath<ManipulatorGroupId>> },
}
// TODO: This is helpful to prevent passing the same arguments to multiple functions, but is currently inefficient due to the collect_outwards_wires. Move it into a function and use only when needed. // TODO: This is helpful to prevent passing the same arguments to multiple functions, but is currently inefficient due to the collect_outwards_wires. Move it into a function and use only when needed.
/// NodeGraphMessage or GraphOperationMessage cannot be added in ModifyInputsContext, since the functions are called by both messages handlers /// NodeGraphMessage or GraphOperationMessage cannot be added in ModifyInputsContext, since the functions are called by both messages handlers
pub struct ModifyInputsContext<'a> { pub struct ModifyInputsContext<'a> {
@@ -315,7 +297,7 @@ impl<'a> ModifyInputsContext<'a> {
ModifyInputsContext::insert_node_as_primary_export(node_graph, document_network, new_id, artboard_node) ModifyInputsContext::insert_node_as_primary_export(node_graph, document_network, new_id, artboard_node)
} }
pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<ManipulatorGroupId>>, layer: NodeId) { pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<PointId>>, layer: NodeId) {
let shape = { let shape = {
let node_type: &crate::messages::portfolio::document::node_graph::document_node_types::DocumentNodeDefinition = resolve_document_node_type("Shape").expect("Shape node does not exist"); let node_type: &crate::messages::portfolio::document::node_graph::document_node_types::DocumentNodeDefinition = resolve_document_node_type("Shape").expect("Shape node does not exist");
node_type.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::Subpaths(subpaths), false))], Default::default()) node_type.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::Subpaths(subpaths), false))], Default::default())
@@ -453,10 +435,9 @@ impl<'a> ModifyInputsContext<'a> {
} }
} }
/// Changes the inputs of a specific node /// Find a node id as part of the layer
pub fn modify_inputs(&mut self, name: &'static str, skip_rerender: bool, update_input: impl FnOnce(&mut Vec<NodeInput>, NodeId, &DocumentMetadata)) { fn existing_node_id(&mut self, name: &'static str) -> Option<NodeId> {
let existing_node_id = self self.document_network
.document_network
.upstream_flow_back_from_nodes( .upstream_flow_back_from_nodes(
self.layer_node.map_or_else( self.layer_node.map_or_else(
|| { || {
@@ -471,7 +452,20 @@ impl<'a> ModifyInputsContext<'a> {
graph_craft::document::FlowType::HorizontalFlow, graph_craft::document::FlowType::HorizontalFlow,
) )
.find(|(node, _)| node.name == name) .find(|(node, _)| node.name == name)
.map(|(_, id)| id); .map(|(_, id)| id)
}
/// Changes the input of a specific node; skipping if it doesn't exist
pub fn modify_existing_inputs(&mut self, name: &'static str, update_input: impl FnOnce(&mut Vec<NodeInput>, NodeId, &DocumentMetadata)) {
let existing_node_id = self.existing_node_id(name);
if let Some(node_id) = existing_node_id {
self.modify_existing_node_inputs(node_id, update_input);
}
}
/// Changes the inputs of a specific node; creating it if it doesn't exist
pub fn modify_inputs(&mut self, name: &'static str, skip_rerender: bool, update_input: impl FnOnce(&mut Vec<NodeInput>, NodeId, &DocumentMetadata)) {
let existing_node_id = self.existing_node_id(name);
if let Some(node_id) = existing_node_id { if let Some(node_id) = existing_node_id {
self.modify_existing_node_inputs(node_id, update_input); self.modify_existing_node_inputs(node_id, update_input);
} else { } else {
@@ -625,22 +619,20 @@ impl<'a> ModifyInputsContext<'a> {
}); });
} }
pub fn transform_change(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, bounds: LayerBounds, skip_rerender: bool) { pub fn transform_change(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, skip_rerender: bool) {
self.modify_inputs("Transform", skip_rerender, |inputs, node_id, metadata| { self.modify_inputs("Transform", skip_rerender, |inputs, _node_id, _metadata| {
let layer_transform = transform_utils::get_current_transform(inputs); let layer_transform = transform_utils::get_current_transform(inputs);
let upstream_transform = metadata.upstream_transform(node_id);
let to = match transform_in { let to = match transform_in {
TransformIn::Local => DAffine2::IDENTITY, TransformIn::Local => DAffine2::IDENTITY,
TransformIn::Scope { scope } => scope * parent_transform, TransformIn::Scope { scope } => scope * parent_transform,
TransformIn::Viewport => parent_transform, TransformIn::Viewport => parent_transform,
}; };
let pivot = DAffine2::from_translation(upstream_transform.transform_point2(bounds.layerspace_pivot(transform_utils::get_current_normalized_pivot(inputs)))); let transform = to.inverse() * transform * to * layer_transform;
let transform = pivot.inverse() * to.inverse() * transform * to * pivot * layer_transform;
transform_utils::update_transform(inputs, transform); transform_utils::update_transform(inputs, transform);
}); });
} }
pub fn transform_set(&mut self, mut transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, current_transform: Option<DAffine2>, bounds: LayerBounds, skip_rerender: bool) { pub fn transform_set(&mut self, mut transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, current_transform: Option<DAffine2>, skip_rerender: bool) {
self.modify_inputs("Transform", skip_rerender, |inputs, node_id, metadata| { self.modify_inputs("Transform", skip_rerender, |inputs, node_id, metadata| {
let upstream_transform = metadata.upstream_transform(node_id); let upstream_transform = metadata.upstream_transform(node_id);
@@ -649,7 +641,6 @@ impl<'a> ModifyInputsContext<'a> {
TransformIn::Scope { scope } => scope * parent_transform, TransformIn::Scope { scope } => scope * parent_transform,
TransformIn::Viewport => parent_transform, TransformIn::Viewport => parent_transform,
}; };
let pivot = DAffine2::from_translation(upstream_transform.transform_point2(bounds.layerspace_pivot(transform_utils::get_current_normalized_pivot(inputs))));
if current_transform if current_transform
.filter(|transform| transform.matrix2.determinant() != 0. && upstream_transform.matrix2.determinant() != 0.) .filter(|transform| transform.matrix2.determinant() != 0. && upstream_transform.matrix2.determinant() != 0.)
@@ -657,79 +648,29 @@ impl<'a> ModifyInputsContext<'a> {
{ {
transform *= upstream_transform.inverse(); transform *= upstream_transform.inverse();
} }
let final_transform = pivot.inverse() * to.inverse() * transform * pivot; let final_transform = to.inverse() * transform;
transform_utils::update_transform(inputs, final_transform); transform_utils::update_transform(inputs, final_transform);
}); });
} }
pub fn pivot_set(&mut self, new_pivot: DVec2, bounds: LayerBounds) { pub fn pivot_set(&mut self, new_pivot: DVec2) {
self.modify_inputs("Transform", false, |inputs, node_id, metadata| { self.modify_inputs("Transform", false, |inputs, _node_id, _metadata| {
let layer_transform = transform_utils::get_current_transform(inputs);
let upstream_transform = metadata.upstream_transform(node_id);
let old_pivot_transform = DAffine2::from_translation(upstream_transform.transform_point2(bounds.local_pivot(transform_utils::get_current_normalized_pivot(inputs))));
let new_pivot_transform = DAffine2::from_translation(upstream_transform.transform_point2(bounds.local_pivot(new_pivot)));
let transform = new_pivot_transform.inverse() * old_pivot_transform * layer_transform * old_pivot_transform.inverse() * new_pivot_transform;
transform_utils::update_transform(inputs, transform);
inputs[5] = NodeInput::value(TaggedValue::DVec2(new_pivot), false); inputs[5] = NodeInput::value(TaggedValue::DVec2(new_pivot), false);
}); });
} }
pub fn update_bounds(&mut self, [old_bounds_min, old_bounds_max]: [DVec2; 2], [new_bounds_min, new_bounds_max]: [DVec2; 2]) { pub fn vector_modify(&mut self, modification_type: VectorModificationType) {
self.modify_all_node_inputs("Transform", false, |inputs, node_id, metadata| { self.modify_inputs("Path", false, |inputs, _node_id, _metadata| {
let upstream_transform = metadata.upstream_transform(node_id); let [_, NodeInput::Value {
let layer_transform = transform_utils::get_current_transform(inputs); tagged_value: TaggedValue::VectorModification(modification),
let normalized_pivot = transform_utils::get_current_normalized_pivot(inputs);
let old_layerspace_pivot = (old_bounds_max - old_bounds_min) * normalized_pivot + old_bounds_min;
let new_layerspace_pivot = (new_bounds_max - new_bounds_min) * normalized_pivot + new_bounds_min;
let new_pivot_transform = DAffine2::from_translation(upstream_transform.transform_point2(new_layerspace_pivot));
let old_pivot_transform = DAffine2::from_translation(upstream_transform.transform_point2(old_layerspace_pivot));
let transform = new_pivot_transform.inverse() * old_pivot_transform * layer_transform * old_pivot_transform.inverse() * new_pivot_transform;
transform_utils::update_transform(inputs, transform);
});
}
pub fn vector_modify(&mut self, modification: VectorDataModification) -> Option<LayerNodeIdentifier> {
let [mut old_bounds_min, mut old_bounds_max] = [DVec2::ZERO, DVec2::ONE];
let [mut new_bounds_min, mut new_bounds_max] = [DVec2::ZERO, DVec2::ONE];
let mut empty = false;
self.modify_inputs("Shape", false, |inputs, _node_id, _metadata| {
let [subpaths, colinear_manipulators] = inputs.as_mut_slice() else {
panic!("Shape does not have both `subpath` and `colinear_manipulators` inputs");
};
let NodeInput::Value {
tagged_value: TaggedValue::Subpaths(subpaths),
.. ..
} = subpaths }] = inputs.as_mut_slice()
else { else {
return; panic!("Path node does not have modification input");
};
let NodeInput::Value {
tagged_value: TaggedValue::ManipulatorGroupIds(colinear_manipulators),
..
} = colinear_manipulators
else {
return;
}; };
[old_bounds_min, old_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths); modification.modify(&modification_type);
transform_utils::VectorModificationState { subpaths, colinear_manipulators }.modify(modification);
empty = !subpaths.iter().any(|subpath| !subpath.is_empty());
[new_bounds_min, new_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths);
}); });
self.update_bounds([old_bounds_min, old_bounds_max], [new_bounds_min, new_bounds_max]);
if empty {
self.layer_node.map(|layer_id| LayerNodeIdentifier::new(layer_id, &self.document_network))
} else {
None
}
} }
pub fn brush_modify(&mut self, strokes: Vec<BrushStroke>) { pub fn brush_modify(&mut self, strokes: Vec<BrushStroke>) {
@@ -761,20 +761,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentInputType::value("Dimensions", TaggedValue::UVec2((512, 512).into()), false), DocumentInputType::value("Dimensions", TaggedValue::UVec2((512, 512).into()), false),
DocumentInputType::value("Seed", TaggedValue::U32(0), false), DocumentInputType::value("Seed", TaggedValue::U32(0), false),
DocumentInputType::value("Scale", TaggedValue::F64(10.), false), DocumentInputType::value("Scale", TaggedValue::F64(10.), false),
DocumentInputType::value("Noise Type", TaggedValue::NoiseType(NoiseType::Perlin), false), DocumentInputType::value("Noise Type", TaggedValue::NoiseType(NoiseType::default()), false),
// Domain Warp // Domain Warp
DocumentInputType::value("Domain Warp Type", TaggedValue::DomainWarpType(DomainWarpType::None), false), DocumentInputType::value("Domain Warp Type", TaggedValue::DomainWarpType(DomainWarpType::default()), false),
DocumentInputType::value("Domain Warp Amplitude", TaggedValue::F64(100.), false), DocumentInputType::value("Domain Warp Amplitude", TaggedValue::F64(100.), false),
// Fractal // Fractal
DocumentInputType::value("Fractal Type", TaggedValue::FractalType(FractalType::None), false), DocumentInputType::value("Fractal Type", TaggedValue::FractalType(FractalType::default()), false),
DocumentInputType::value("Fractal Octaves", TaggedValue::U32(3), false), DocumentInputType::value("Fractal Octaves", TaggedValue::U32(3), false),
DocumentInputType::value("Fractal Lacunarity", TaggedValue::F64(2.), false), DocumentInputType::value("Fractal Lacunarity", TaggedValue::F64(2.), false),
DocumentInputType::value("Fractal Gain", TaggedValue::F64(0.5), false), DocumentInputType::value("Fractal Gain", TaggedValue::F64(0.5), false),
DocumentInputType::value("Fractal Weighted Strength", TaggedValue::F64(0.), false), // 0-1 range DocumentInputType::value("Fractal Weighted Strength", TaggedValue::F64(0.), false), // 0-1 range
DocumentInputType::value("Fractal Ping Pong Strength", TaggedValue::F64(2.), false), DocumentInputType::value("Fractal Ping Pong Strength", TaggedValue::F64(2.), false),
// Cellular // Cellular
DocumentInputType::value("Cellular Distance Function", TaggedValue::CellularDistanceFunction(CellularDistanceFunction::Euclidean), false), DocumentInputType::value("Cellular Distance Function", TaggedValue::CellularDistanceFunction(CellularDistanceFunction::default()), false),
DocumentInputType::value("Cellular Return Type", TaggedValue::CellularReturnType(CellularReturnType::Nearest), false), DocumentInputType::value("Cellular Return Type", TaggedValue::CellularReturnType(CellularReturnType::default()), false),
DocumentInputType::value("Cellular Jitter", TaggedValue::F64(1.), false), DocumentInputType::value("Cellular Jitter", TaggedValue::F64(1.), false),
], ],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
@@ -802,7 +802,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
inputs: vec![ inputs: vec![
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
DocumentInputType::value("Insertion", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Insertion", TaggedValue::ImageFrame(ImageFrame::empty()), true),
DocumentInputType::value("Replace", TaggedValue::RedGreenBlue(RedGreenBlue::Red), false), DocumentInputType::value("Replace", TaggedValue::RedGreenBlue(RedGreenBlue::default()), false),
], ],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
properties: node_properties::insert_channel_properties, properties: node_properties::insert_channel_properties,
@@ -935,7 +935,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
name: "Color Channel", name: "Color Channel",
category: "Raster", category: "Raster",
implementation: DocumentNodeImplementation::proto("graphene_core::ops::IdentityNode"), implementation: DocumentNodeImplementation::proto("graphene_core::ops::IdentityNode"),
inputs: vec![DocumentInputType::value("Channel", TaggedValue::RedGreenBlue(RedGreenBlue::Red), false)], inputs: vec![DocumentInputType::value("Channel", TaggedValue::RedGreenBlue(RedGreenBlue::default()), false)],
outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::General)], outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::General)],
properties: node_properties::color_channel_properties, properties: node_properties::color_channel_properties,
..Default::default() ..Default::default()
@@ -944,7 +944,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
name: "Blend Mode Value", name: "Blend Mode Value",
category: "Inputs", category: "Inputs",
implementation: DocumentNodeImplementation::proto("graphene_core::ops::IdentityNode"), implementation: DocumentNodeImplementation::proto("graphene_core::ops::IdentityNode"),
inputs: vec![DocumentInputType::value("Blend Mode", TaggedValue::BlendMode(BlendMode::Normal), false)], inputs: vec![DocumentInputType::value("Blend Mode", TaggedValue::BlendMode(BlendMode::default()), false)],
outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::General)], outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::General)],
properties: node_properties::blend_mode_value_properties, properties: node_properties::blend_mode_value_properties,
..Default::default() ..Default::default()
@@ -955,7 +955,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
implementation: DocumentNodeImplementation::proto("graphene_core::raster::LuminanceNode<_>"), implementation: DocumentNodeImplementation::proto("graphene_core::raster::LuminanceNode<_>"),
inputs: vec![ inputs: vec![
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
DocumentInputType::value("Luminance Calc", TaggedValue::LuminanceCalculation(LuminanceCalculation::SRGB), false), DocumentInputType::value("Luminance Calc", TaggedValue::LuminanceCalculation(LuminanceCalculation::default()), false),
], ],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
properties: node_properties::luminance_properties, properties: node_properties::luminance_properties,
@@ -967,7 +967,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
implementation: DocumentNodeImplementation::proto("graphene_core::raster::ExtractChannelNode<_>"), implementation: DocumentNodeImplementation::proto("graphene_core::raster::ExtractChannelNode<_>"),
inputs: vec![ inputs: vec![
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
DocumentInputType::value("From", TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Red), false), DocumentInputType::value("From", TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::default()), false),
], ],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
properties: node_properties::extract_channel_properties, properties: node_properties::extract_channel_properties,
@@ -1864,7 +1864,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentInputType::value("(Blue) Blue", TaggedValue::F64(100.), false), DocumentInputType::value("(Blue) Blue", TaggedValue::F64(100.), false),
DocumentInputType::value("(Blue) Constant", TaggedValue::F64(0.), false), DocumentInputType::value("(Blue) Constant", TaggedValue::F64(0.), false),
// Display-only properties (not used within the node) // Display-only properties (not used within the node)
DocumentInputType::value("Output Channel", TaggedValue::RedGreenBlue(RedGreenBlue::Red), false), DocumentInputType::value("Output Channel", TaggedValue::RedGreenBlue(RedGreenBlue::default()), false),
], ],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
properties: node_properties::adjust_channel_mixer_properties, properties: node_properties::adjust_channel_mixer_properties,
@@ -1879,7 +1879,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
inputs: vec![ inputs: vec![
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
// Mode // Mode
DocumentInputType::value("Mode", TaggedValue::RelativeAbsolute(RelativeAbsolute::Relative), false), DocumentInputType::value("Mode", TaggedValue::RelativeAbsolute(RelativeAbsolute::default()), false),
// Reds // Reds
DocumentInputType::value("(Reds) Cyan", TaggedValue::F64(0.), false), DocumentInputType::value("(Reds) Cyan", TaggedValue::F64(0.), false),
DocumentInputType::value("(Reds) Magenta", TaggedValue::F64(0.), false), DocumentInputType::value("(Reds) Magenta", TaggedValue::F64(0.), false),
@@ -1926,7 +1926,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentInputType::value("(Blacks) Yellow", TaggedValue::F64(0.), false), DocumentInputType::value("(Blacks) Yellow", TaggedValue::F64(0.), false),
DocumentInputType::value("(Blacks) Black", TaggedValue::F64(0.), false), DocumentInputType::value("(Blacks) Black", TaggedValue::F64(0.), false),
// Display-only properties (not used within the node) // Display-only properties (not used within the node)
DocumentInputType::value("Colors", TaggedValue::SelectiveColorChoice(SelectiveColorChoice::Reds), false), DocumentInputType::value("Colors", TaggedValue::SelectiveColorChoice(SelectiveColorChoice::default()), false),
], ],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
properties: node_properties::adjust_selective_color_properties, properties: node_properties::adjust_selective_color_properties,
@@ -2347,11 +2347,41 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
implementation: DocumentNodeImplementation::proto("graphene_core::vector::generator_nodes::PathGenerator<_>"), implementation: DocumentNodeImplementation::proto("graphene_core::vector::generator_nodes::PathGenerator<_>"),
inputs: vec![ inputs: vec![
DocumentInputType::value("Path Data", TaggedValue::Subpaths(vec![]), false), DocumentInputType::value("Path Data", TaggedValue::Subpaths(vec![]), false),
DocumentInputType::value("Colinear Manipulators", TaggedValue::ManipulatorGroupIds(vec![]), false), DocumentInputType::value("Colinear Manipulators", TaggedValue::PointIds(vec![]), false),
], ],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)],
..Default::default() ..Default::default()
}, },
DocumentNodeDefinition {
name: "Path",
category: "Vector",
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: vec![
DocumentNode {
inputs: vec![NodeInput::network(concrete!(VectorData), 0)],
..monitor_node()
},
DocumentNode {
name: "Path Modify".to_string(),
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::network(concrete!(graphene_core::vector::VectorModification), 1)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::PathModify<_>")),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![
DocumentInputType::value("Vector Data", TaggedValue::VectorData(VectorData::empty()), true),
DocumentInputType::value("Modification", TaggedValue::VectorModification(Default::default()), false),
],
outputs: vec![DocumentOutputType::new("Vector Data", FrontendGraphDataType::VectorData)],
..Default::default()
},
DocumentNodeDefinition { DocumentNodeDefinition {
name: "Sample", name: "Sample",
category: "Structural", category: "Structural",
@@ -2489,8 +2519,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentInputType::value("Weight", TaggedValue::F64(0.), false), DocumentInputType::value("Weight", TaggedValue::F64(0.), false),
DocumentInputType::value("Dash Lengths", TaggedValue::VecF64(Vec::new()), false), DocumentInputType::value("Dash Lengths", TaggedValue::VecF64(Vec::new()), false),
DocumentInputType::value("Dash Offset", TaggedValue::F64(0.), false), DocumentInputType::value("Dash Offset", TaggedValue::F64(0.), false),
DocumentInputType::value("Line Cap", TaggedValue::LineCap(graphene_core::vector::style::LineCap::Butt), false), DocumentInputType::value("Line Cap", TaggedValue::LineCap(graphene_core::vector::style::LineCap::default()), false),
DocumentInputType::value("Line Join", TaggedValue::LineJoin(graphene_core::vector::style::LineJoin::Miter), false), DocumentInputType::value("Line Join", TaggedValue::LineJoin(graphene_core::vector::style::LineJoin::default()), false),
DocumentInputType::value("Miter Limit", TaggedValue::F64(4.), false), DocumentInputType::value("Miter Limit", TaggedValue::F64(4.), false),
], ],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)],
@@ -16,13 +16,12 @@ use bezier_rs::Subpath;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, FlowType, NodeId, NodeInput, NodeNetwork, Previewing, Source}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, FlowType, NodeId, NodeInput, NodeNetwork, Previewing, Source};
use graph_craft::proto::GraphErrors; use graph_craft::proto::GraphErrors;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::*; use graphene_core::*;
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
use renderer::{ClickTarget, Quad};
use vector::PointId;
use glam::{DAffine2, DVec2, IVec2, UVec2}; use glam::{DAffine2, DVec2, IVec2, UVec2};
use renderer::{ClickTarget, Quad};
use web_sys::window;
#[derive(Debug)] #[derive(Debug)]
pub struct NodeGraphHandlerData<'a> { pub struct NodeGraphHandlerData<'a> {
@@ -62,7 +61,7 @@ pub struct NodeGraphMessageHandler {
/// Click targets for every node in the network by using the path to that node. /// Click targets for every node in the network by using the path to that node.
pub node_metadata: HashMap<NodeId, NodeMetadata>, pub node_metadata: HashMap<NodeId, NodeMetadata>,
/// Cache for the bounding box around all nodes in node graph space. /// Cache for the bounding box around all nodes in node graph space.
pub bounding_box_subpath: Option<Subpath<ManipulatorGroupId>>, pub bounding_box_subpath: Option<Subpath<PointId>>,
auto_panning: AutoPanning, auto_panning: AutoPanning,
} }
@@ -1548,8 +1547,15 @@ impl NodeGraphMessageHandler {
common common
} }
#[cfg(not(target_arch = "wasm32"))]
fn get_text_width(node: &DocumentNode) -> Option<f64> { fn get_text_width(node: &DocumentNode) -> Option<f64> {
let document = window().unwrap().document().unwrap(); warn!("Failed to find width of {node:#?} due to non-wasm arch");
None
}
#[cfg(target_arch = "wasm32")]
fn get_text_width(node: &DocumentNode) -> Option<f64> {
let document = web_sys::window().unwrap().document().unwrap();
let div = match document.create_element("div") { let div = match document.create_element("div") {
Ok(div) => div, Ok(div) => div,
Err(err) => { Err(err) => {
@@ -1594,6 +1600,7 @@ impl NodeGraphMessageHandler {
Some(text_width) Some(text_width)
} }
pub fn layer_width_cells(node: &DocumentNode) -> u32 { pub fn layer_width_cells(node: &DocumentNode) -> u32 {
let half_grid_cell_offset = 24. / 2.; let half_grid_cell_offset = 24. / 2.;
let thumbnail_width = 3. * 24.; let thumbnail_width = 3. * 24.;
@@ -1733,25 +1733,7 @@ pub fn logic_operator_properties(document_node: &DocumentNode, node_id: NodeId,
} }
pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let translation_assist = |widgets: &mut Vec<WidgetHolder>| { let translation = vec2_widget(document_node, node_id, 1, "Translation", "X", "Y", " px", None, add_blank_assist);
let pivot_index = 5;
if let NodeInput::Value {
tagged_value: TaggedValue::DVec2(pivot),
exposed: false,
} = document_node.inputs[pivot_index]
{
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(
PivotInput::new(pivot.into())
.on_update(update_value(|pivot: &PivotInput| TaggedValue::DVec2(Into::<Option<DVec2>>::into(pivot.position).unwrap()), node_id, 5))
.on_commit(commit_value)
.widget_holder(),
);
} else {
add_blank_assist(widgets);
}
};
let translation = vec2_widget(document_node, node_id, 1, "Translation", "X", "Y", " px", None, translation_assist);
let rotation = { let rotation = {
let index = 2; let index = 2;
@@ -18,7 +18,7 @@ impl FrontendGraphDataType {
pub fn with_type(input: &Type) -> Self { pub fn with_type(input: &Type) -> Self {
match TaggedValue::from_type(input) { match TaggedValue::from_type(input) {
TaggedValue::Image(_) | TaggedValue::ImageFrame(_) => Self::Raster, TaggedValue::Image(_) | TaggedValue::ImageFrame(_) => Self::Raster,
TaggedValue::Subpaths(_) | TaggedValue::RcSubpath(_) | TaggedValue::VectorData(_) => Self::VectorData, TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::VectorData,
TaggedValue::U32(_) TaggedValue::U32(_)
| TaggedValue::U64(_) | TaggedValue::U64(_)
| TaggedValue::F64(_) | TaggedValue::F64(_)
@@ -37,11 +37,10 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context:
} else { } else {
DVec2::new(secondary_pos, primary_end) DVec2::new(secondary_pos, primary_end)
}; };
overlay_context.line( overlay_context.colored_line(
document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end), document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())), &("#".to_string() + &grid_color.rgba_hex()),
None,
); );
} }
} }
@@ -112,11 +111,10 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x; let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x;
let start = DVec2::new(x_pos, min_y); let start = DVec2::new(x_pos, min_y);
let end = DVec2::new(x_pos, max_y); let end = DVec2::new(x_pos, max_y);
overlay_context.line( overlay_context.colored_line(
document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end), document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())), &("#".to_string() + &grid_color.rgba_hex()),
None,
); );
} }
@@ -131,11 +129,10 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let y_pos = (((inverse_project(&min_y) - origin.y) / spacing).ceil() + line_index as f64) * spacing + origin.y; let y_pos = (((inverse_project(&min_y) - origin.y) / spacing).ceil() + line_index as f64) * spacing + origin.y;
let start = DVec2::new(min_x, project(&DVec2::new(min_x, y_pos))); let start = DVec2::new(min_x, project(&DVec2::new(min_x, y_pos)));
let end = DVec2::new(max_x, project(&DVec2::new(max_x, y_pos))); let end = DVec2::new(max_x, project(&DVec2::new(max_x, y_pos)));
overlay_context.line( overlay_context.colored_line(
document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end), document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())), &("#".to_string() + &grid_color.rgba_hex()),
None,
); );
} }
} }
@@ -178,7 +175,7 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
let start = DVec2::new(min_x + x_offset, project(&DVec2::new(min_x + x_offset, y_pos))); let start = DVec2::new(min_x + x_offset, project(&DVec2::new(min_x + x_offset, y_pos)));
let end = DVec2::new(max_x + x_offset, project(&DVec2::new(max_x + x_offset, y_pos))); let end = DVec2::new(max_x + x_offset, project(&DVec2::new(max_x + x_offset, y_pos)));
overlay_context.line( overlay_context.dashed_line(
document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end), document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())), Some(&("#".to_string() + &grid_color.rgba_hex())),
@@ -1,10 +1,9 @@
use super::utility_types::OverlayContext; use super::utility_types::OverlayContext;
use crate::consts::HIDE_HANDLE_DISTANCE; use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_groups, get_subpaths};
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState}; use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler; use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
use graphene_core::vector::{ManipulatorPointId, SelectedType}; use graphene_core::vector::ManipulatorPointId;
use glam::DVec2; use glam::DVec2;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
@@ -26,54 +25,55 @@ pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d {
pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) { pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) {
for layer in document.selected_nodes.selected_layers(document.metadata()) { for layer in document.selected_nodes.selected_layers(document.metadata()) {
let Some(subpaths) = get_subpaths(layer, &document.network) else { continue }; let Some(vector_data) = document.metadata.compute_modified_vector(layer, &document.network) else {
continue;
};
let transform = document.metadata().transform_to_viewport(layer); let transform = document.metadata().transform_to_viewport(layer);
let selected = shape_editor.selected_shape_state.get(&layer); let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point)); let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point));
overlay_context.outline(subpaths.iter(), transform); overlay_context.outline_vector(&vector_data, transform);
for manipulator_group in get_manipulator_groups(subpaths) { for (segment_id, bezier, _start, _end) in vector_data.segment_bezier_iter() {
let anchor = manipulator_group.anchor; let bezier = bezier.apply_transformation(|point| transform.transform_point2(point));
let anchor_position = transform.transform_point2(anchor); let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
match bezier.handles {
let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE; bezier_rs::BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => {
if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) { overlay_context.line(handle, bezier.start);
let handle_position = transform.transform_point2(in_handle); overlay_context.line(handle, bezier.end);
overlay_context.line(handle_position, anchor_position, None, None); overlay_context.manipulator_handle(handle, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)));
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle))); }
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
if not_under_anchor(handle_start, bezier.start) {
overlay_context.line(handle_start, bezier.start);
overlay_context.manipulator_handle(handle_start, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)));
}
if not_under_anchor(handle_end, bezier.end) {
overlay_context.line(handle_end, bezier.end);
overlay_context.manipulator_handle(handle_end, is_selected(selected, ManipulatorPointId::EndHandle(segment_id)));
}
}
_ => {}
} }
if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) { }
let handle_position = transform.transform_point2(out_handle); for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
overlay_context.line(handle_position, anchor_position, None, None); overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(selected, ManipulatorPointId::Anchor(id)), None);
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
}
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None);
} }
} }
} }
pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) { pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) {
for layer in document.selected_nodes.selected_layers(document.metadata()) { for layer in document.selected_nodes.selected_layers(document.metadata()) {
let Some(subpaths) = get_subpaths(layer, &document.network) else { continue }; let Some(vector_data) = document.metadata.compute_modified_vector(layer, &document.network) else {
continue;
};
let transform = document.metadata().transform_to_viewport(layer); let transform = document.metadata().transform_to_viewport(layer);
let selected = shape_editor.selected_shape_state.get(&layer); let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point)); let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point));
let mut manipulator_groups = get_manipulator_groups(subpaths); for point in vector_data.single_connected_points() {
let Some(position) = vector_data.point_domain.position_from_id(point) else { continue };
if let Some(first_manipulator) = manipulator_groups.next() { let position = transform.transform_point2(position);
let anchor = first_manipulator.anchor; overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), None);
let anchor_position = transform.transform_point2(anchor); }
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None);
};
if let Some(last_manipulator) = manipulator_groups.last() {
let anchor = last_manipulator.anchor;
let anchor_position = transform.transform_point2(anchor);
overlay_context.manipulator_anchor(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None);
};
} }
} }
@@ -2,12 +2,14 @@ use super::utility_functions::overlay_canvas_context;
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER}; use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER};
use crate::messages::prelude::Message; use crate::messages::prelude::Message;
use bezier_rs::Subpath; use bezier_rs::{Bezier, Subpath};
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use wasm_bindgen::JsValue; use graphene_std::vector::{PointId, VectorData};
use core::borrow::Borrow;
use core::f64::consts::TAU; use core::f64::consts::TAU;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use wasm_bindgen::JsValue;
pub type OverlayProvider = fn(OverlayContext) -> Message; pub type OverlayProvider = fn(OverlayContext) -> Message;
@@ -39,7 +41,15 @@ impl OverlayContext {
self.render_context.stroke(); self.render_context.stroke();
} }
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>) { pub fn line(&mut self, start: DVec2, end: DVec2) {
self.dashed_line(start, end, None, None)
}
pub fn colored_line(&mut self, start: DVec2, end: DVec2, color: &str) {
self.dashed_line(start, end, Some(color), None)
}
pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>) {
let start = start.round() - DVec2::splat(0.5); let start = start.round() - DVec2::splat(0.5);
let end = end.round() - DVec2::splat(0.5); let end = end.round() - DVec2::splat(0.5);
if let Some(dash_width) = dash_width { if let Some(dash_width) = dash_width {
@@ -152,9 +162,44 @@ impl OverlayContext {
self.render_context.stroke(); self.render_context.stroke();
} }
pub fn outline<'a, Id: bezier_rs::Identifier>(&mut self, subpaths: impl Iterator<Item = &'a Subpath<Id>>, transform: DAffine2) { pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
self.render_context.begin_path();
let mut last_point = None;
for (_, bezier, start_id, end_id) in vector_data.segment_bezier_iter() {
let move_to = last_point != Some(start_id);
last_point = Some(end_id);
self.bezier_command(bezier, transform, move_to);
}
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
self.render_context.stroke();
}
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
self.render_context.begin_path();
self.bezier_command(bezier, transform, true);
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
self.render_context.stroke();
}
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) {
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
if move_to {
self.render_context.move_to(start.x, start.y);
}
match handles {
bezier_rs::BezierHandles::Linear => self.render_context.line_to(end.x, end.y),
bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.y),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(handle_start.x, handle_start.y, handle_end.x, handle_end.y, end.x, end.y),
}
}
pub fn outline<'a>(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
self.render_context.begin_path(); self.render_context.begin_path();
for subpath in subpaths { for subpath in subpaths {
let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable(); let mut curves = subpath.iter().peekable();
let Some(first) = curves.peek() else { let Some(first) = curves.peek() else {
@@ -1,13 +1,16 @@
use super::nodes::SelectedNodes; use super::nodes::SelectedNodes;
use crate::messages::tool::common_functionality::graph_modification_utils;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::FlowType; use graph_craft::document::FlowType;
use graph_craft::document::{NodeId, NodeNetwork}; use graph_craft::document::{NodeId, NodeNetwork};
use graphene_core::renderer::ClickTarget; use graphene_core::renderer::ClickTarget;
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::transform::Footprint; use graphene_core::transform::Footprint;
use graphene_std::vector::PointId;
use graphene_std::vector::VectorData;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::vector::PointId;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64; use std::num::NonZeroU64;
@@ -26,6 +29,7 @@ pub struct DocumentMetadata {
hidden: HashSet<NodeId>, hidden: HashSet<NodeId>,
locked: HashSet<NodeId>, locked: HashSet<NodeId>,
click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>, click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
vector_modify: HashMap<NodeId, VectorData>,
/// Transform from document space to viewport space. /// Transform from document space to viewport space.
pub document_to_viewport: DAffine2, pub document_to_viewport: DAffine2,
} }
@@ -39,6 +43,7 @@ impl Default for DocumentMetadata {
folders: HashSet::new(), folders: HashSet::new(),
hidden: HashSet::new(), hidden: HashSet::new(),
locked: HashSet::new(), locked: HashSet::new(),
vector_modify: HashMap::new(),
click_targets: HashMap::new(), click_targets: HashMap::new(),
document_to_viewport: DAffine2::IDENTITY, document_to_viewport: DAffine2::IDENTITY,
} }
@@ -62,6 +67,23 @@ impl DocumentMetadata {
self.click_targets.get(&layer) self.click_targets.get(&layer)
} }
/// Get vector data after the modification is appled
pub fn compute_modified_vector(&self, layer: LayerNodeIdentifier, network: &NodeNetwork) -> Option<VectorData> {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network);
if let Some(vector_data) = graph_layer.upstream_node_id_from_name("Path").and_then(|node| self.vector_modify.get(&node)) {
let mut modified = vector_data.clone();
if let Some(TaggedValue::VectorModification(modification)) = graph_layer.find_input("Path", 1) {
modification.apply(&mut modified);
}
return Some(modified);
}
self.click_targets
.get(&layer)
.map(|click| click.iter().map(|click| &click.subpath))
.map(|subpaths| VectorData::from_subpaths(subpaths, true))
}
/// Access the [`NodeRelations`] of a layer. /// Access the [`NodeRelations`] of a layer.
fn get_relations(&self, node_identifier: LayerNodeIdentifier) -> Option<&NodeRelations> { fn get_relations(&self, node_identifier: LayerNodeIdentifier) -> Option<&NodeRelations> {
self.structure.get(&node_identifier) self.structure.get(&node_identifier)
@@ -225,6 +247,7 @@ impl DocumentMetadata {
self.upstream_transforms.retain(|node, _| graph.nodes.contains_key(node)); self.upstream_transforms.retain(|node, _| graph.nodes.contains_key(node));
self.click_targets.retain(|layer, _| self.structure.contains_key(layer)); self.click_targets.retain(|layer, _| self.structure.contains_key(layer));
self.vector_modify.retain(|node, _| graph.nodes.contains_key(node));
} }
} }
@@ -281,9 +304,10 @@ impl DocumentMetadata {
// =============================== // ===============================
impl DocumentMetadata { impl DocumentMetadata {
/// Update the cached click targets of the layers /// Update the cached click targets and vector modify values of the layers
pub fn update_click_targets(&mut self, new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>) { pub fn update_from_monitor(&mut self, new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>, new_vector_modify: HashMap<NodeId, VectorData>) {
self.click_targets = new_click_targets; self.click_targets = new_click_targets;
self.vector_modify = new_vector_modify;
} }
/// Get the bounding box of the click target of the specified layer in the specified transform space /// Get the bounding box of the click target of the specified layer in the specified transform space
@@ -1,5 +1,5 @@
use crate::consts::{ROTATE_SNAP_ANGLE, SCALE_SNAP_INTERVAL}; use crate::consts::{ROTATE_SNAP_ANGLE, SCALE_SNAP_INTERVAL};
use crate::messages::portfolio::document::graph_operation::utility_types::{TransformIn, VectorDataModification}; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
@@ -8,15 +8,37 @@ use crate::messages::tool::utility_types::ToolType;
use graph_craft::document::NodeNetwork; use graph_craft::document::NodeNetwork;
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::vector::{ManipulatorPointId, SelectedType}; use graphene_core::vector::ManipulatorPointId;
use graphene_core::vector::VectorModificationType;
use graphene_std::vector::{HandleId, PointId};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
#[derive(Debug, PartialEq, Clone, Copy)]
struct AnchorPoint {
initial: DVec2,
current: DVec2,
}
#[derive(Debug, PartialEq, Clone, Copy)]
struct HandlePoint {
initial: DVec2,
relative: DVec2,
anchor: PointId,
mirror: Option<(HandleId, DVec2)>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct InitialPoints {
anchors: HashMap<PointId, AnchorPoint>,
handles: HashMap<HandleId, HandlePoint>,
}
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum OriginalTransforms { pub enum OriginalTransforms {
Layer(HashMap<LayerNodeIdentifier, DAffine2>), Layer(HashMap<LayerNodeIdentifier, DAffine2>),
Path(HashMap<LayerNodeIdentifier, Vec<(ManipulatorPointId, DVec2)>>), Path(HashMap<LayerNodeIdentifier, InitialPoints>),
} }
impl Default for OriginalTransforms { impl Default for OriginalTransforms {
fn default() -> Self { fn default() -> Self {
@@ -43,36 +65,46 @@ impl OriginalTransforms {
} }
} }
OriginalTransforms::Path(path_map) => { OriginalTransforms::Path(path_map) => {
let Some(shape_editor) = shape_editor else {
warn!("No shape editor structure found, which only happens in select tool, which cannot reach this point as we check for ToolType");
return;
};
for &layer in selected { for &layer in selected {
let Some(shape_editor) = shape_editor else {
warn!("No shape editor structure found, which only happens in select tool, which cannot reach this point as we check for ToolType");
continue;
};
// Anchors also move their handles
let expand_anchors = |&point: &ManipulatorPointId| {
if point.manipulator_type.is_handle() {
[Some(point), None, None]
} else {
[
Some(point),
Some(ManipulatorPointId::new(point.group, SelectedType::InHandle)),
Some(ManipulatorPointId::new(point.group, SelectedType::OutHandle)),
]
}
};
let points = shape_editor.selected_points().flat_map(expand_anchors).flatten();
if path_map.contains_key(&layer) { if path_map.contains_key(&layer) {
continue; continue;
} }
let Some(vector_data) = graph_modification_utils::get_subpaths(layer, document_network) else { let Some(vector_data) = document_metadata.compute_modified_vector(layer, document_network) else {
continue; continue;
}; };
let get_manipulator_point_position = |point_id: ManipulatorPointId| { let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else {
graph_modification_utils::get_manipulator_from_id(vector_data, point_id.group) continue;
.and_then(|manipulator_group| point_id.manipulator_type.get_position(manipulator_group))
.map(|position| (point_id, position))
}; };
path_map.insert(layer, points.filter_map(get_manipulator_point_position).collect());
// Anchors also move their handles
let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor());
let anchors = anchor_ids.filter_map(|id| vector_data.point_domain.position_from_id(id).map(|pos| (id, AnchorPoint { initial: pos, current: pos })));
let anchors = anchors.collect();
let selected_handles = selected_points.iter().filter_map(|point| point.as_handle());
let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor());
let connected_handles = anchor_ids.flat_map(|point| vector_data.segment_domain.all_connected(point));
let all_handles = selected_handles.chain(connected_handles);
let handles = all_handles
.filter_map(|id| {
let anchor = id.to_manipulator_point().get_anchor(&vector_data)?;
let initial = id.to_manipulator_point().get_position(&vector_data)?;
let relative = vector_data.point_domain.position_from_id(anchor)?;
let other_handle = vector_data
.other_colinear_handle(id)
.filter(|other| !selected_points.contains(&other.to_manipulator_point()) && !selected_points.contains(&ManipulatorPointId::Anchor(anchor)));
let mirror = other_handle.and_then(|id| Some((id, id.to_manipulator_point().get_position(&vector_data)?)));
Some((id, HandlePoint { initial, relative, anchor, mirror }))
})
.collect();
path_map.insert(layer, InitialPoints { anchors, handles });
} }
} }
} }
@@ -382,28 +414,31 @@ impl<'a> Selected<'a> {
}); });
} }
fn transform_path( fn transform_path(document_metadata: &DocumentMetadata, layer: LayerNodeIdentifier, initial_points: &mut InitialPoints, transformation: DAffine2, responses: &mut VecDeque<Message>) {
document_metadata: &DocumentMetadata,
layer: LayerNodeIdentifier,
initial_points: Option<&Vec<(ManipulatorPointId, DVec2)>>,
transformation: DAffine2,
responses: &mut VecDeque<Message>,
) {
let viewspace = document_metadata.transform_to_viewport(layer); let viewspace = document_metadata.transform_to_viewport(layer);
let layerspace_rotation = viewspace.inverse() * transformation; let layerspace_rotation = viewspace.inverse() * transformation;
let Some(initial_points) = initial_points else { for (&point, anchor) in initial_points.anchors.iter_mut() {
return; let new_pos_viewport = layerspace_rotation.transform_point2(viewspace.transform_point2(anchor.initial));
}; let delta = new_pos_viewport - anchor.current;
anchor.current += delta;
let modification_type = VectorModificationType::ApplyPointDelta { point, delta };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
for (point_id, position) in initial_points { for (&id, handle) in initial_points.handles.iter() {
let viewport_point = viewspace.transform_point2(*position); let new_pos_viewport = layerspace_rotation.transform_point2(viewspace.transform_point2(handle.initial));
let new_pos_viewport = layerspace_rotation.transform_point2(viewport_point); let relative = initial_points.anchors.get(&handle.anchor).map_or(handle.relative, |anchor| anchor.current);
let point = *point_id; let modification_type = id.set_relative_position(new_pos_viewport - relative);
let position = new_pos_viewport; responses.add(GraphOperationMessage::Vector { layer, modification_type });
let modification = VectorDataModification::SetManipulatorPosition { point, position };
responses.add(GraphOperationMessage::Vector { layer, modification }); if let Some((id, initial)) = handle.mirror {
let direction = viewspace.transform_vector2(new_pos_viewport - relative).try_normalize();
let length = viewspace.transform_vector2(initial - relative).length();
let new_relative = direction.map_or(initial - relative, |direction| viewspace.inverse().transform_vector2(-direction * length));
let modification_type = id.set_relative_position(new_relative);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
} }
} }
@@ -413,9 +448,13 @@ impl<'a> Selected<'a> {
for layer_ancestors in self.document_metadata.shallowest_unique_layers(self.selected.iter().copied()) { for layer_ancestors in self.document_metadata.shallowest_unique_layers(self.selected.iter().copied()) {
let layer = *layer_ancestors.last().unwrap(); let layer = *layer_ancestors.last().unwrap();
match &self.original_transforms { match &mut self.original_transforms {
OriginalTransforms::Layer(layer_transforms) => Self::transform_layer(self.document_metadata, layer, layer_transforms.get(&layer), transformation, self.responses), OriginalTransforms::Layer(layer_transforms) => Self::transform_layer(self.document_metadata, layer, layer_transforms.get(&layer), transformation, self.responses),
OriginalTransforms::Path(path_transforms) => Self::transform_path(self.document_metadata, layer, path_transforms.get(&layer), transformation, self.responses), OriginalTransforms::Path(path_transforms) => {
if let Some(initial_points) = path_transforms.get_mut(&layer) {
Self::transform_path(self.document_metadata, layer, initial_points, transformation, self.responses)
}
}
} }
} }
} }
@@ -442,11 +481,20 @@ impl<'a> Selected<'a> {
} }
OriginalTransforms::Path(path) => { OriginalTransforms::Path(path) => {
for (&layer, points) in path { for (&layer, points) in path {
for &(point, position) in points { for (&point, &anchor) in &points.anchors {
self.responses.add(GraphOperationMessage::Vector { let delta = anchor.initial - anchor.current;
layer, let modification_type = VectorModificationType::ApplyPointDelta { point, delta };
modification: VectorDataModification::SetManipulatorPosition { point, position }, self.responses.add(GraphOperationMessage::Vector { layer, modification_type });
}); }
for (&point, &handle) in &points.handles {
let modification_type = point.set_relative_position(handle.initial - handle.relative);
self.responses.add(GraphOperationMessage::Vector { layer, modification_type });
if let Some((id, initial)) = handle.mirror {
let modification_type = id.set_relative_position(initial - handle.relative);
self.responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
} }
} }
} }
@@ -23,9 +23,9 @@ pub struct PortfolioMessageData<'a> {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct PortfolioMessageHandler { pub struct PortfolioMessageHandler {
menu_bar_message_handler: MenuBarMessageHandler, menu_bar_message_handler: MenuBarMessageHandler,
documents: HashMap<DocumentId, DocumentMessageHandler>, pub documents: HashMap<DocumentId, DocumentMessageHandler>,
document_ids: Vec<DocumentId>, document_ids: Vec<DocumentId>,
active_document_id: Option<DocumentId>, pub(crate) active_document_id: Option<DocumentId>,
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize], copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
pub persistent_data: PersistentData, pub persistent_data: PersistentData,
pub executor: NodeGraphExecutor, pub executor: NodeGraphExecutor,
@@ -375,6 +375,20 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
document_is_saved, document_is_saved,
document_serialized_content, document_serialized_content,
} => { } => {
// TODO: Eventually remove this (probably starting late 2024)
let do_not_upgrade = document_name.contains("__DO_NOT_UPGRADE__");
let document_name = document_name.replace("__DO_NOT_UPGRADE__", "");
if document_serialized_content.contains("ManipulatorGroupIds") && !do_not_upgrade {
responses.add(FrontendMessage::TriggerUpgradeDocumentToVectorManipulationFormat {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
});
return;
}
let document = DocumentMessageHandler::with_name_and_content(document_name, document_serialized_content); let document = DocumentMessageHandler::with_name_and_content(document_name, document_serialized_content);
match document { match document {
Ok(mut document) => { Ok(mut document) => {
@@ -656,12 +670,13 @@ impl PortfolioMessageHandler {
self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids") self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids")
} }
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) { pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> Result<(), String> {
let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) else { let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) else {
return; return Err("No active document".to_string());
}; };
if self.executor.poll_node_graph_evaluation(active_document, responses).is_err() { let result = self.executor.poll_node_graph_evaluation(active_document, responses);
if result.is_err() {
let error = r#" let error = r#"
<rect x="50%" y="50%" width="480" height="100" transform="translate(-240 -50)" rx="4" fill="var(--color-error-red)" /> <rect x="50%" y="50%" width="480" height="100" transform="translate(-240 -50)" rx="4" fill="var(--color-error-red)" />
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="18" fill="var(--color-2-mildblack)"> <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="18" fill="var(--color-2-mildblack)">
@@ -673,5 +688,6 @@ impl PortfolioMessageHandler {
.to_string(); .to_string();
responses.add(FrontendMessage::UpdateDocumentArtwork { svg: error }); responses.add(FrontendMessage::UpdateDocumentArtwork { svg: error });
} }
result
} }
} }
@@ -1,5 +1,6 @@
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::Message; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use graphene_core::Color; use graphene_core::Color;
use graphene_std::vector::style::FillChoice; use graphene_std::vector::style::FillChoice;
@@ -58,6 +59,20 @@ impl ToolColorOptions {
} }
} }
pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(color) = self.active_color() {
let fill = graphene_core::vector::style::Fill::solid(color);
responses.add(GraphOperationMessage::FillSet { layer, fill });
}
}
pub fn apply_stroke(&self, weight: f64, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(color) = self.active_color() {
let stroke = graphene_core::vector::style::Stroke::new(Some(color), weight);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
}
}
pub fn create_widgets( pub fn create_widgets(
&self, &self,
label_text: impl Into<String>, label_text: impl Into<String>,
@@ -1,20 +1,18 @@
use crate::messages::portfolio::document::graph_operation::utility_types::VectorDataModification;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use bezier_rs::Subpath;
use bezier_rs::{ManipulatorGroup, Subpath};
use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork}; use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork};
use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font; use graphene_core::text::Font;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::style::Gradient; use graphene_core::vector::style::Gradient;
use graphene_core::vector::PointId;
use graphene_core::Color; use graphene_core::Color;
use glam::DVec2; use glam::DVec2;
use std::collections::VecDeque; use std::collections::VecDeque;
/// Create a new vector layer from a vector of [`bezier_rs::Subpath`]. /// Create a new vector layer from a vector of [`bezier_rs::Subpath`].
pub fn new_vector_layer(subpaths: Vec<Subpath<ManipulatorGroupId>>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier { pub fn new_vector_layer(subpaths: Vec<Subpath<PointId>>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
let insert_index = -1; let insert_index = -1;
responses.add(GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index }); responses.add(GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index });
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
@@ -46,25 +44,16 @@ pub fn new_svg_layer(svg: String, transform: glam::DAffine2, id: NodeId, parent:
}); });
LayerNodeIdentifier::new_unchecked(id) LayerNodeIdentifier::new_unchecked(id)
} }
pub fn new_custom(id: NodeId, nodes: HashMap<NodeId, DocumentNode>, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
/// Batch set all of the manipulator groups to set their colinear handle state on a specific layer responses.add(GraphOperationMessage::NewCustomLayer {
pub fn set_manipulator_colinear_handles_state(manipulator_groups: &[ManipulatorGroup<ManipulatorGroupId>], layer: LayerNodeIdentifier, colinear: bool, responses: &mut VecDeque<Message>) { id,
for manipulator_group in manipulator_groups { nodes,
responses.add(GraphOperationMessage::Vector { parent,
layer, insert_index: -1,
modification: VectorDataModification::SetManipulatorColinearHandlesState { id: manipulator_group.id, colinear }, alias: String::new(),
}); });
} responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
} LayerNodeIdentifier::new_unchecked(id)
/// Locate the subpaths from the shape nodes of a particular layer
pub fn get_subpaths(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<&Vec<Subpath<ManipulatorGroupId>>> {
let path_data_node_input_index = 0;
if let TaggedValue::Subpaths(subpaths) = NodeGraphLayer::new(layer, document_network).find_input("Shape", path_data_node_input_index)? {
Some(subpaths)
} else {
None
}
} }
/// Locate the final pivot from the transform (TODO: decide how the pivot should actually work) /// Locate the final pivot from the transform (TODO: decide how the pivot should actually work)
@@ -83,16 +72,6 @@ pub fn get_viewport_pivot(layer: LayerNodeIdentifier, document_network: &NodeNet
document_metadata.transform_to_viewport(layer).transform_point2(min + (max - min) * pivot) document_metadata.transform_to_viewport(layer).transform_point2(min + (max - min) * pivot)
} }
/// Get the manipulator groups that currently have colinear handles for a particular layer from the shape node
pub fn get_colinear_manipulators(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<&Vec<ManipulatorGroupId>> {
let colinear_manipulators_node_input_index = 1;
if let TaggedValue::ManipulatorGroupIds(manipulator_groups) = NodeGraphLayer::new(layer, document_network).find_input("Shape", colinear_manipulators_node_input_index)? {
Some(manipulator_groups)
} else {
None
}
}
/// Get the current gradient of a layer from the closest Fill node /// Get the current gradient of a layer from the closest Fill node
pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Gradient> { pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Gradient> {
let fill_index = 1; let fill_index = 1;
@@ -191,16 +170,6 @@ pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, document_network
NodeGraphLayer::new(layer, document_network).find_node_inputs(node_name).is_some() NodeGraphLayer::new(layer, document_network).find_node_inputs(node_name).is_some()
} }
/// Convert subpaths to an iterator of manipulator groups
pub fn get_manipulator_groups(subpaths: &[Subpath<ManipulatorGroupId>]) -> impl Iterator<Item = &bezier_rs::ManipulatorGroup<ManipulatorGroupId>> + DoubleEndedIterator {
subpaths.iter().flat_map(|subpath| subpath.manipulator_groups())
}
/// Find a manipulator group with a specific id from several subpaths
pub fn get_manipulator_from_id(subpaths: &[Subpath<ManipulatorGroupId>], id: ManipulatorGroupId) -> Option<&bezier_rs::ManipulatorGroup<ManipulatorGroupId>> {
subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id))
}
/// An immutable reference to a layer within the document node graph for easy access. /// An immutable reference to a layer within the document node graph for easy access.
pub struct NodeGraphLayer<'a> { pub struct NodeGraphLayer<'a> {
node_graph: &'a NodeNetwork, node_graph: &'a NodeNetwork,
@@ -29,13 +29,12 @@ impl Resize {
root_transform.transform_point2(self.drag_start) root_transform.transform_point2(self.drag_start)
} }
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> { pub fn calculate_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key) -> Option<[DVec2; 2]> {
let Some(layer) = self.layer else { let layer = self.layer?;
return None;
};
if layer == LayerNodeIdentifier::ROOT_PARENT { if layer == LayerNodeIdentifier::ROOT_PARENT {
log::error!("Resize layer cannot be ROOT_PARENT"); log::error!("Resize layer cannot be ROOT_PARENT");
return None;
} }
if !document.network().nodes.contains_key(&layer.to_node()) { if !document.network().nodes.contains_key(&layer.to_node()) {
@@ -87,9 +86,14 @@ impl Resize {
self.snap_manager.update_indicator(snapped); self.snap_manager.update_indicator(snapped);
} }
Some(points_viewport)
}
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
let points_viewport = self.calculate_points(document, input, center, lock_ratio)?;
Some( Some(
GraphOperationMessage::TransformSet { GraphOperationMessage::TransformSet {
layer, layer: self.layer?,
transform: DAffine2::from_scale_angle_translation(points_viewport[1] - points_viewport[0], 0., points_viewport[0]), transform: DAffine2::from_scale_angle_translation(points_viewport[1] - points_viewport[0], 0., points_viewport[0]),
transform_in: TransformIn::Viewport, transform_in: TransformIn::Viewport,
skip_rerender, skip_rerender,
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,20 @@
mod grid_snapper; mod grid_snapper;
mod layer_snapper; mod layer_snapper;
mod snap_results; mod snap_results;
pub use {grid_snapper::*, layer_snapper::*, snap_results::*};
use crate::consts::COLOR_OVERLAY_BLUE; use crate::consts::COLOR_OVERLAY_BLUE;
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::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget}; use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use bezier_rs::{Subpath, TValue}; use bezier_rs::{Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::PointId;
use glam::{DAffine2, DVec2};
use std::cmp::Ordering; use std::cmp::Ordering;
pub use {grid_snapper::*, layer_snapper::*, snap_results::*};
/// Handles snapping and snap overlays /// Handles snapping and snap overlays
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@@ -150,7 +153,7 @@ pub struct SnapData<'a> {
pub document: &'a DocumentMessageHandler, pub document: &'a DocumentMessageHandler,
pub input: &'a InputPreprocessorMessageHandler, pub input: &'a InputPreprocessorMessageHandler,
pub ignore: &'a [LayerNodeIdentifier], pub ignore: &'a [LayerNodeIdentifier],
pub manipulators: Vec<(LayerNodeIdentifier, ManipulatorGroupId)>, pub manipulators: Vec<(LayerNodeIdentifier, PointId)>,
pub candidates: Option<&'a Vec<LayerNodeIdentifier>>, pub candidates: Option<&'a Vec<LayerNodeIdentifier>>,
} }
impl<'a> SnapData<'a> { impl<'a> SnapData<'a> {
@@ -172,7 +175,7 @@ impl<'a> SnapData<'a> {
fn ignore_bounds(&self, layer: LayerNodeIdentifier) -> bool { fn ignore_bounds(&self, layer: LayerNodeIdentifier) -> bool {
self.manipulators.iter().any(|&(ignore, _)| ignore == layer) self.manipulators.iter().any(|&(ignore, _)| ignore == layer)
} }
fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: impl Into<ManipulatorGroupId>) -> bool { fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: impl Into<PointId>) -> bool {
self.manipulators.contains(&(layer, manipulator.into())) self.manipulators.contains(&(layer, manipulator.into()))
} }
} }
@@ -277,10 +280,8 @@ impl SnapManager {
} }
} }
if let Some(root) = snap_data.document.network.get_root_node() { for layer in LayerNodeIdentifier::ROOT_PARENT.children(&document.metadata) {
if snap_data.document.network.nodes.get(&root.id).expect("Root should always be a node in find_candidates").is_layer { add_candidates(layer, snap_data, quad, &mut candidates);
add_candidates(LayerNodeIdentifier::new(root.id, &snap_data.document.network), snap_data, quad, &mut candidates);
}
} }
if candidates.len() > 10 { if candidates.len() > 10 {
@@ -333,7 +334,7 @@ impl SnapManager {
if let Some(ind) = &self.indicator { if let Some(ind) = &self.indicator {
for curve in &ind.curves { for curve in &ind.curves {
let Some(curve) = curve else { continue }; let Some(curve) = curve else { continue };
overlay_context.outline::<ManipulatorGroupId>([Subpath::from_bezier(curve)].iter(), to_viewport); overlay_context.outline([Subpath::from_bezier(curve)].iter(), to_viewport);
} }
if let Some(quad) = ind.target_bounds { if let Some(quad) = ind.target_bounds {
overlay_context.quad(to_viewport * quad); overlay_context.quad(to_viewport * quad);
@@ -8,8 +8,7 @@ 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_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::PointId;
use graphene_std::vector::PointId;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct LayerSnapper { pub struct LayerSnapper {
@@ -33,7 +32,7 @@ impl LayerSnapper {
self.paths_to_snap.push(SnapCandidatePath { self.paths_to_snap.push(SnapCandidatePath {
document_curve, document_curve,
layer, layer,
start: ManipulatorGroupId::new(), start: PointId::new(),
target, target,
bounds: Some(bounds), bounds: Some(bounds),
}); });
@@ -133,7 +132,7 @@ impl LayerSnapper {
let direction = constraint.direction().normalize_or_zero(); let direction = constraint.direction().normalize_or_zero();
let start = constrained_point - tolerance * direction; let start = constrained_point - tolerance * direction;
let end = constrained_point + tolerance * direction; let end = constrained_point + tolerance * direction;
Subpath::<ManipulatorGroupId>::new_line(start, end) Subpath::<PointId>::new_line(start, end)
}; };
for path in &self.paths_to_snap { for path in &self.paths_to_snap {
@@ -296,7 +295,7 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
struct SnapCandidatePath { struct SnapCandidatePath {
document_curve: Bezier, document_curve: Bezier,
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
start: ManipulatorGroupId, start: PointId,
target: SnapTarget, target: SnapTarget,
bounds: Option<Quad>, bounds: Option<Quad>,
} }
@@ -418,8 +417,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
} }
} }
/// Returns true if both handles in a manipulator group are colinear, unless the anchor is an endpoint. Endpoint anchors are never considered colinear. pub fn are_manipulator_handles_colinear(group: &bezier_rs::ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool {
pub fn are_manipulator_handles_colinear<Id: bezier_rs::Identifier>(group: &bezier_rs::ManipulatorGroup<Id>, to_document: DAffine2, subpath: &Subpath<Id>, index: usize) -> bool {
let anchor = group.anchor; let anchor = group.anchor;
let handle_in = group.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document)); let handle_in = group.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
let handle_out = group.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document)); let handle_out = group.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document));
@@ -3,7 +3,7 @@ use crate::messages::portfolio::document::utility_types::misc::{SnapSource, Snap
use bezier_rs::Bezier; use bezier_rs::Bezier;
use glam::DVec2; use glam::DVec2;
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::PointId;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct SnapResults { pub struct SnapResults {
@@ -79,7 +79,7 @@ pub struct SnappedLine {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SnappedCurve { pub struct SnappedCurve {
pub layer: LayerNodeIdentifier, pub layer: LayerNodeIdentifier,
pub start: ManipulatorGroupId, pub start: PointId,
pub point: SnappedPoint, pub point: SnappedPoint,
pub document_curve: Bezier, pub document_curve: Bezier,
} }
@@ -126,8 +126,8 @@ impl SelectedEdges {
for point in points { for point in points {
let old_position = point.document_point; let old_position = point.document_point;
let bounds_space = bounds_to_doc.inverse().transform_point2(point.document_point); let bounds_space = bounds_to_doc.inverse().transform_point2(point.document_point);
let normalised = (bounds_space - self.bounds[0]) / (self.bounds[1] - self.bounds[0]); let normalized = (bounds_space - self.bounds[0]) / (self.bounds[1] - self.bounds[0]);
let updated = normalised * (max - min) + min; let updated = normalized * (max - min) + min;
point.document_point = bounds_to_doc.transform_point2(updated); point.document_point = bounds_to_doc.transform_point2(updated);
let mut snapped = if constrain { let mut snapped = if constrain {
let constraint = SnapConstraint::Line { let constraint = SnapConstraint::Line {
@@ -1,31 +1,27 @@
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_subpaths;
use graphene_std::vector::PointId;
use glam::DVec2; use glam::DVec2;
/// Determines if a path should be extended. Returns the path and if it is extending from the start, if applicable. /// Determines if a path should be extended. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend(document: &DocumentMessageHandler, pos: DVec2, tolerance: f64) -> Option<(LayerNodeIdentifier, usize, bool)> { pub fn should_extend(document: &DocumentMessageHandler, goal: DVec2, tolerance: f64) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
let mut best = None; let mut best = None;
let mut best_distance_squared = tolerance * tolerance; let mut best_distance_squared = tolerance * tolerance;
for layer in document.selected_nodes.selected_layers(document.metadata()) { for layer in document.selected_nodes.selected_layers(document.metadata()) {
let viewspace = document.metadata().transform_to_viewport(layer); let viewspace = document.metadata().transform_to_viewport(layer);
let subpaths = get_subpaths(layer, &document.network)?; let vector_data = document.metadata.compute_modified_vector(layer, document.network())?;
for (subpath_index, subpath) in subpaths.iter().enumerate() { for id in vector_data.single_connected_points() {
if subpath.closed() { let Some(point) = vector_data.point_domain.position_from_id(id) else { continue };
continue;
}
for (manipulator_group, from_start) in [(subpath.manipulator_groups().first(), true), (subpath.manipulator_groups().last(), false)] { let distance_squared = viewspace.transform_point2(point).distance_squared(goal);
let Some(manipulator_group) = manipulator_group else { break };
let distance_squared = viewspace.transform_point2(manipulator_group.anchor).distance_squared(pos); if distance_squared < best_distance_squared {
best = Some((layer, id, point));
if distance_squared < best_distance_squared { best_distance_squared = distance_squared;
best = Some((layer, subpath_index, from_start));
best_distance_squared = distance_squared;
}
} }
} }
} }
@@ -1,5 +1,6 @@
use super::tool_prelude::*; use super::tool_prelude::*;
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_types::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
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};
@@ -7,9 +8,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping::SnapData; use crate::messages::tool::common_functionality::snapping::SnapData;
use graph_craft::document::NodeId; use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid; use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color; use graphene_core::Color;
#[derive(Default)] #[derive(Default)]
@@ -201,10 +201,18 @@ impl Fsm for EllipseToolFsmState {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
// Create a new ellipse vector shape // Create a new ellipse vector shape
let subpath = bezier_rs::Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE); let nodes = {
let manipulator_groups = subpath.manipulator_groups().to_vec(); let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node does not exist");
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), document.new_layer_parent(true), responses); let node = node_type.to_document_node_default_inputs(
graph_modification_utils::set_manipulator_colinear_handles_state(&manipulator_groups, layer, true, responses); [None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))],
Default::default(),
);
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
shape_data.layer = Some(layer); shape_data.layer = Some(layer);
responses.add(GraphOperationMessage::TransformSet { responses.add(GraphOperationMessage::TransformSet {
@@ -214,22 +222,18 @@ impl Fsm for EllipseToolFsmState {
skip_rerender: false, skip_rerender: false,
}); });
let fill_color = tool_options.fill.active_color();
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = fill_color { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight),
});
EllipseToolFsmState::Drawing EllipseToolFsmState::Drawing
} }
(EllipseToolFsmState::Drawing, EllipseToolMessage::PointerMove { center, lock_ratio }) => { (EllipseToolFsmState::Drawing, EllipseToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, input, center, lock_ratio, false) { if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) {
responses.add(message); if let Some(layer) = shape_data.layer {
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(end - start, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
} }
// Auto-panning // Auto-panning
@@ -1,5 +1,5 @@
use super::tool_prelude::*; use super::tool_prelude::*;
use crate::messages::portfolio::document::graph_operation::utility_types::VectorDataModification; use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays; use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays;
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;
@@ -9,10 +9,10 @@ use crate::messages::tool::common_functionality::utility_functions::should_exten
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_core::uuid::generate_uuid; use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::VectorModificationType;
use graphene_core::Color; use graphene_core::Color;
use graphene_std::vector::{PointId, SegmentId};
use bezier_rs::ManipulatorGroup;
use glam::DVec2; use glam::DVec2;
#[derive(Default)] #[derive(Default)]
@@ -176,7 +176,7 @@ impl ToolTransition for FreehandTool {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
struct FreehandToolData { struct FreehandToolData {
extend_from_start: bool, extend_from_start: bool,
last_point: DVec2, end_point: Option<(DVec2, PointId)>,
dragged: bool, dragged: bool,
weight: f64, weight: f64,
layer: Option<LayerNodeIdentifier>, layer: Option<LayerNodeIdentifier>,
@@ -211,64 +211,44 @@ impl Fsm for FreehandToolFsmState {
tool_data.extend_from_start = false; tool_data.extend_from_start = false;
tool_data.weight = tool_options.line_weight; tool_data.weight = tool_options.line_weight;
if let Some((layer, subpath_index, from_start)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) { // Extend an endpoint of the selected path
let transform = document.metadata().transform_to_viewport(layer); if let Some((layer, _, position)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
let pos = transform.inverse().transform_point2(input.mouse.position);
let manipulator_group = ManipulatorGroup::new_anchor(pos);
let modification = if from_start {
tool_data.extend_from_start = true;
VectorDataModification::AddStartManipulatorGroup { subpath_index, manipulator_group }
} else {
VectorDataModification::AddEndManipulatorGroup { subpath_index, manipulator_group }
};
tool_data.dragged = true;
tool_data.last_point = pos;
tool_data.layer = Some(layer); tool_data.layer = Some(layer);
responses.add(GraphOperationMessage::Vector { layer, modification }); extend_path_with_next_segment(tool_data, position, responses);
} else {
responses.add(DocumentMessage::DeselectAllLayers);
let parent = document.new_layer_parent(true); return FreehandToolFsmState::Drawing;
let transform = document.metadata().transform_to_viewport(parent);
let pos = transform.inverse().transform_point2(input.mouse.position);
let subpath = bezier_rs::Subpath::from_anchors([pos], false);
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), parent, responses);
tool_data.last_point = pos;
tool_data.layer = Some(layer);
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = tool_options.fill.active_color() { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_data.weight),
});
} }
responses.add(DocumentMessage::DeselectAllLayers);
let parent = document.new_layer_parent(true);
let nodes = {
let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
let node = node_type.to_document_node_default_inputs([], Default::default());
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, parent, responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
tool_data.layer = Some(layer);
let transform = document.metadata().transform_to_viewport(layer);
let position = transform.inverse().transform_point2(input.mouse.position);
extend_path_with_next_segment(tool_data, position, responses);
FreehandToolFsmState::Drawing FreehandToolFsmState::Drawing
} }
(FreehandToolFsmState::Drawing, FreehandToolMessage::PointerMove) => { (FreehandToolFsmState::Drawing, FreehandToolMessage::PointerMove) => {
if let Some(layer) = tool_data.layer { if let Some(layer) = tool_data.layer {
let transform = document.metadata().transform_to_viewport(layer); let transform = document.metadata().transform_to_viewport(layer);
let pos = transform.inverse().transform_point2(input.mouse.position); let position = transform.inverse().transform_point2(input.mouse.position);
if tool_data.last_point != pos { extend_path_with_next_segment(tool_data, position, responses);
let manipulator_group = ManipulatorGroup::new_anchor(pos);
let modification = if tool_data.extend_from_start {
VectorDataModification::AddStartManipulatorGroup { subpath_index: 0, manipulator_group }
} else {
VectorDataModification::AddEndManipulatorGroup { subpath_index: 0, manipulator_group }
};
responses.add(GraphOperationMessage::Vector { layer, modification });
tool_data.dragged = true;
tool_data.last_point = pos;
}
} }
FreehandToolFsmState::Drawing FreehandToolFsmState::Drawing
@@ -276,6 +256,8 @@ impl Fsm for FreehandToolFsmState {
(FreehandToolFsmState::Drawing, FreehandToolMessage::DragStop) => { (FreehandToolFsmState::Drawing, FreehandToolMessage::DragStop) => {
if tool_data.dragged { if tool_data.dragged {
responses.add(DocumentMessage::CommitTransaction); responses.add(DocumentMessage::CommitTransaction);
} else {
responses.add(DocumentMessage::DocumentHistoryBackward);
} }
tool_data.layer = None; tool_data.layer = None;
@@ -312,3 +294,34 @@ impl Fsm for FreehandToolFsmState {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
} }
} }
fn extend_path_with_next_segment(tool_data: &mut FreehandToolData, position: DVec2, responses: &mut VecDeque<Message>) {
if !tool_data.end_point.map_or(true, |(last_pos, _)| position != last_pos) || !position.is_finite() {
return;
}
let Some(layer) = tool_data.layer else { return };
let id = PointId::generate();
responses.add(GraphOperationMessage::Vector {
layer,
modification_type: VectorModificationType::InsertPoint { id, position },
});
if let Some((_, previous_position)) = tool_data.end_point {
let next_id = SegmentId::generate();
let points = [previous_position, id];
responses.add(GraphOperationMessage::Vector {
layer,
modification_type: VectorModificationType::InsertSegment {
id: next_id,
points,
handles: [None, None],
},
});
}
tool_data.dragged = true;
tool_data.end_point = Some((position, id));
}
@@ -259,7 +259,7 @@ impl Fsm for GradientToolFsmState {
let Gradient { start, end, stops, .. } = gradient; let Gradient { start, end, stops, .. } = gradient;
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end)); let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
overlay_context.line(start, end, None, None); overlay_context.line(start, end);
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start)); overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start));
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End)); overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End));
@@ -1,6 +1,7 @@
use super::tool_prelude::*; use super::tool_prelude::*;
use crate::consts::LINE_ROTATE_SNAP_ANGLE; use crate::consts::LINE_ROTATE_SNAP_ANGLE;
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_types::resolve_document_node_type;
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::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
@@ -8,9 +9,8 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use graph_craft::document::NodeId; use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid; use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::Stroke;
use graphene_core::Color; use graphene_core::Color;
#[derive(Default)] #[derive(Default)]
@@ -177,11 +177,24 @@ impl Fsm for LineToolFsmState {
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false); let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
tool_data.drag_start = snapped.snapped_point_document; tool_data.drag_start = snapped.snapped_point_document;
let subpath = bezier_rs::Subpath::new_line(DVec2::ZERO, DVec2::X);
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), document.new_layer_parent(true), responses); let nodes = {
let node_type = resolve_document_node_type("Line").expect("Line node does not exist");
let node = node_type.to_document_node_default_inputs(
[
None,
Some(NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false)),
Some(NodeInput::value(TaggedValue::DVec2(DVec2::X), false)),
],
Default::default(),
);
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
tool_data.layer = Some(layer);
responses.add(GraphOperationMessage::TransformSet { responses.add(GraphOperationMessage::TransformSet {
layer, layer,
@@ -190,11 +203,6 @@ impl Fsm for LineToolFsmState {
skip_rerender: false, skip_rerender: false,
}); });
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight),
});
tool_data.layer = Some(layer); tool_data.layer = Some(layer);
tool_data.weight = tool_options.line_weight; tool_data.weight = tool_options.line_weight;
@@ -4,13 +4,12 @@ use crate::messages::portfolio::document::overlays::utility_functions::path_over
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::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_colinear_manipulators, get_manipulator_from_id, get_subpaths};
use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState}; use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState};
use crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager}; use crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager};
use graph_craft::document::NodeNetwork; use graph_craft::document::NodeNetwork;
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::vector::{ManipulatorPointId, SelectedType}; use graphene_core::vector::ManipulatorPointId;
use std::vec; use std::vec;
@@ -92,10 +91,7 @@ impl LayoutHolder for PathTool {
let (x, y) = coordinates.map(|point| (Some(point.x), Some(point.y))).unwrap_or((None, None)); let (x, y) = coordinates.map(|point| (Some(point.x), Some(point.y))).unwrap_or((None, None));
let selection_status = &self.tool_data.selection_status; let selection_status = &self.tool_data.selection_status;
let manipulator_angle = selection_status let manipulator_angle = selection_status.angle();
.as_multiple()
.map(|multiple| multiple.manipulator_angle)
.or_else(|| selection_status.as_one().map(|point| point.manipulator_angle));
let x_location = NumberInput::new(x) let x_location = NumberInput::new(x)
.unit(" px") .unit(" px")
@@ -105,8 +101,11 @@ impl LayoutHolder for PathTool {
.min(-((1_u64 << std::f64::MANTISSA_DIGITS) as f64)) .min(-((1_u64 << std::f64::MANTISSA_DIGITS) as f64))
.max((1_u64 << std::f64::MANTISSA_DIGITS) as f64) .max((1_u64 << std::f64::MANTISSA_DIGITS) as f64)
.on_update(move |number_input: &NumberInput| { .on_update(move |number_input: &NumberInput| {
let new_x = number_input.value.unwrap_or(x.unwrap()); if let Some(new_x) = number_input.value.or(x) {
PathToolMessage::SelectedPointXChanged { new_x }.into() PathToolMessage::SelectedPointXChanged { new_x }.into()
} else {
Message::NoOp
}
}) })
.widget_holder(); .widget_holder();
@@ -118,8 +117,11 @@ impl LayoutHolder for PathTool {
.min(-((1_u64 << std::f64::MANTISSA_DIGITS) as f64)) .min(-((1_u64 << std::f64::MANTISSA_DIGITS) as f64))
.max((1_u64 << std::f64::MANTISSA_DIGITS) as f64) .max((1_u64 << std::f64::MANTISSA_DIGITS) as f64)
.on_update(move |number_input: &NumberInput| { .on_update(move |number_input: &NumberInput| {
let new_y = number_input.value.unwrap_or(y.unwrap()); if let Some(new_y) = number_input.value.or(y) {
PathToolMessage::SelectedPointYChanged { new_y }.into() PathToolMessage::SelectedPointYChanged { new_y }.into()
} else {
Message::NoOp
}
}) })
.widget_holder(); .widget_holder();
@@ -273,7 +275,7 @@ impl PathToolData {
fn update_insertion(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, mouse_position: DVec2) -> PathToolFsmState { fn update_insertion(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, mouse_position: DVec2) -> PathToolFsmState {
if let Some(closed_segment) = &mut self.segment { if let Some(closed_segment) = &mut self.segment {
closed_segment.update_closest_point(&document.metadata, mouse_position); closed_segment.update_closest_point(&document.metadata, mouse_position);
if closed_segment.too_far(mouse_position, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE) { if closed_segment.too_far(mouse_position, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, &document.metadata) {
self.end_insertion(shape_editor, responses, InsertEndKind::Abort) self.end_insertion(shape_editor, responses, InsertEndKind::Abort)
} else { } else {
PathToolFsmState::InsertPoint PathToolFsmState::InsertPoint
@@ -318,6 +320,8 @@ impl PathToolData {
let document_network = document.network(); let document_network = document.network();
let document_metadata = document.metadata(); let document_metadata = document.metadata();
self.drag_start_pos = input.mouse.position;
// Select the first point within the threshold (in pixels) // Select the first point within the threshold (in pixels)
if let Some(selected_points) = shape_editor.change_point_selection(document_network, document_metadata, input.mouse.position, SELECTION_THRESHOLD, add_to_selection) { if let Some(selected_points) = shape_editor.change_point_selection(document_network, document_metadata, input.mouse.position, SELECTION_THRESHOLD, add_to_selection) {
if let Some(selected_points) = selected_points { if let Some(selected_points) = selected_points {
@@ -345,7 +349,7 @@ impl PathToolData {
} }
self.drag_start_pos = input.mouse.position; self.drag_start_pos = input.mouse.position;
self.previous_mouse_position = document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position); self.previous_mouse_position = document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position);
shape_editor.select_all_anchors_in_layer(&document.network, layer); shape_editor.select_connected_anchors(document, layer, input.mouse.position);
PathToolFsmState::Dragging PathToolFsmState::Dragging
} }
@@ -370,16 +374,12 @@ impl PathToolData {
// Do not snap against handles when anchor is selected // Do not snap against handles when anchor is selected
let mut additional_selected_points = Vec::new(); let mut additional_selected_points = Vec::new();
for point in selected_points.points.iter() { for point in selected_points.points.iter() {
if point.point_id.manipulator_type == SelectedType::Anchor { let Some(anchor) = point.point_id.as_anchor() else { continue };
additional_selected_points.push(ManipulatorPointInfo {
layer: point.layer, let connected = selected_points.vector_data.segment_domain.all_connected(anchor).map(|handle| handle.to_manipulator_point());
point_id: ManipulatorPointId::new(point.point_id.group, SelectedType::InHandle), let filtered = connected.filter(|point| point.get_position(&selected_points.vector_data).is_some());
}); let point_info = filtered.map(|point_id| ManipulatorPointInfo { layer: point.layer, point_id });
additional_selected_points.push(ManipulatorPointInfo { additional_selected_points.extend(point_info);
layer: point.layer,
point_id: ManipulatorPointId::new(point.point_id.group, SelectedType::OutHandle),
});
}
} }
selected_points.points.extend(additional_selected_points); selected_points.points.extend(additional_selected_points);
@@ -387,27 +387,39 @@ impl PathToolData {
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position - selected_points.offset); self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position - selected_points.offset);
} }
fn drag(&mut self, shift: bool, alt: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { fn update_colinear(&mut self, shift: bool, alt: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> bool {
// Check if the alt key has just been pressed // Check if the alt key has just been pressed
if alt && !self.alt_debounce { if alt && !self.alt_debounce {
self.opposing_handle_lengths = None; self.opposing_handle_lengths = None;
shape_editor.toggle_colinear_handles_state_on_selected(responses); let colinear = self.selection_status.angle().map_or(false, |angle| match angle {
ManipulatorAngle::Colinear => true,
ManipulatorAngle::Free => false,
ManipulatorAngle::Mixed => false,
});
if colinear {
shape_editor.disable_colinear_handles_state_on_selected(&document.metadata, &document.network, responses);
} else {
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
}
self.alt_debounce = true;
return true;
} }
self.alt_debounce = alt; self.alt_debounce = alt;
if shift { if shift {
if self.opposing_handle_lengths.is_none() { if self.opposing_handle_lengths.is_none() {
self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(&document.network)); self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document));
} }
} else if let Some(opposing_handle_lengths) = &self.opposing_handle_lengths {
shape_editor.reset_opposing_handle_lengths(&document.network, opposing_handle_lengths, responses);
self.opposing_handle_lengths = None;
} }
false
}
fn drag(&mut self, equidistant: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
// Move the selected points with the mouse // Move the selected points with the mouse
let previous_mouse = document.metadata.document_to_viewport.transform_point2(self.previous_mouse_position); let previous_mouse = document.metadata.document_to_viewport.transform_point2(self.previous_mouse_position);
let snapped_delta = shape_editor.snap(&mut self.snap_manager, document, input, previous_mouse); let snapped_delta = shape_editor.snap(&mut self.snap_manager, document, input, previous_mouse);
shape_editor.move_selected_points(&document.network, &document.metadata, snapped_delta, shift, responses); let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() };
shape_editor.move_selected_points(handle_lengths, &document, snapped_delta, equidistant, responses);
self.previous_mouse_position += document.metadata.document_to_viewport.inverse().transform_vector2(snapped_delta); self.previous_mouse_position += document.metadata.document_to_viewport.inverse().transform_vector2(snapped_delta);
} }
} }
@@ -431,7 +443,6 @@ impl Fsm for PathToolFsmState {
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
responses.add(PathToolMessage::SelectedPointUpdated); responses.add(PathToolMessage::SelectedPointUpdated);
// This can happen in any state (which is why we return self)
self self
} }
(_, PathToolMessage::Overlays(mut overlay_context)) => { (_, PathToolMessage::Overlays(mut overlay_context)) => {
@@ -503,7 +514,9 @@ impl Fsm for PathToolFsmState {
(PathToolFsmState::Dragging, PathToolMessage::PointerMove { alt, shift }) => { (PathToolFsmState::Dragging, PathToolMessage::PointerMove { alt, shift }) => {
let alt_state = input.keyboard.get(alt as usize); let alt_state = input.keyboard.get(alt as usize);
let shift_state = input.keyboard.get(shift as usize); let shift_state = input.keyboard.get(shift as usize);
tool_data.drag(shift_state, alt_state, shape_editor, document, input, responses); if !tool_data.update_colinear(shift_state, alt_state, shape_editor, document, responses) {
tool_data.drag(shift_state, shape_editor, document, input, responses);
}
// Auto-panning // Auto-panning
let messages = [PathToolMessage::PointerOutsideViewport { alt, shift }.into(), PathToolMessage::PointerMove { alt, shift }.into()]; let messages = [PathToolMessage::PointerOutsideViewport { alt, shift }.into(), PathToolMessage::PointerMove { alt, shift }.into()];
@@ -521,9 +534,9 @@ impl Fsm for PathToolFsmState {
} }
(PathToolFsmState::Dragging, PathToolMessage::PointerOutsideViewport { shift, .. }) => { (PathToolFsmState::Dragging, PathToolMessage::PointerOutsideViewport { shift, .. }) => {
// Auto-panning // Auto-panning
if let Some(delta) = tool_data.auto_panning.shift_viewport(input, responses) { if tool_data.auto_panning.shift_viewport(input, responses).is_some() {
let shift_state = input.keyboard.get(shift as usize); let shift_state = input.keyboard.get(shift as usize);
shape_editor.move_selected_points(&document.network, &document.metadata, -delta, shift_state, responses); tool_data.drag(shift_state, shape_editor, document, input, responses);
} }
PathToolFsmState::Dragging PathToolFsmState::Dragging
@@ -575,18 +588,16 @@ impl Fsm for PathToolFsmState {
(_, PathToolMessage::DragStop { equidistant }) => { (_, PathToolMessage::DragStop { equidistant }) => {
let equidistant = input.keyboard.get(equidistant as usize); let equidistant = input.keyboard.get(equidistant as usize);
let nearest_point = shape_editor let nearest_point = shape_editor.find_nearest_point_indices(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD);
.find_nearest_point_indices(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD)
.map(|(_, nearest_point)| nearest_point);
shape_editor.delete_selected_handles_with_zero_length(&document.network, &document.metadata, &tool_data.opposing_handle_lengths, responses); if let Some((layer, nearest_point)) = nearest_point {
if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !equidistant {
if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !equidistant { let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == Some(point)); if clicked_selected {
if clicked_selected { shape_editor.deselect_all_points();
shape_editor.deselect_all_points(); shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
shape_editor.change_point_selection(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, false); responses.add(OverlaysMessage::Draw);
responses.add(OverlaysMessage::Draw); }
} }
} }
@@ -599,17 +610,17 @@ impl Fsm for PathToolFsmState {
(_, PathToolMessage::Delete) => { (_, PathToolMessage::Delete) => {
// Delete the selected points and clean up overlays // Delete the selected points and clean up overlays
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
shape_editor.delete_selected_points(responses); shape_editor.delete_selected_points(&document, responses);
responses.add(PathToolMessage::SelectionChanged); responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::BreakPath) => { (_, PathToolMessage::BreakPath) => {
shape_editor.break_path_at_selected_point(&document.network, responses); shape_editor.break_path_at_selected_point(document, responses);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::DeleteAndBreakPath) => { (_, PathToolMessage::DeleteAndBreakPath) => {
shape_editor.delete_point_and_break_path(&document.network, responses); shape_editor.delete_point_and_break_path(document, responses);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::FlipSmoothSharp) => { (_, PathToolMessage::FlipSmoothSharp) => {
@@ -625,12 +636,12 @@ impl Fsm for PathToolFsmState {
} }
(_, PathToolMessage::PointerMove { .. }) => self, (_, PathToolMessage::PointerMove { .. }) => self,
(_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => { (_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => {
shape_editor.move_selected_points(&document.network, &document.metadata, (delta_x, delta_y).into(), true, responses); shape_editor.move_selected_points(tool_data.opposing_handle_lengths.take(), &document, (delta_x, delta_y).into(), true, responses);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::SelectAllAnchors) => { (_, PathToolMessage::SelectAllAnchors) => {
shape_editor.select_all_anchors_in_selected_layers(&document.network); shape_editor.select_all_anchors_in_selected_layers(document);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
@@ -641,13 +652,13 @@ impl Fsm for PathToolFsmState {
} }
(_, PathToolMessage::SelectedPointXChanged { new_x }) => { (_, PathToolMessage::SelectedPointXChanged { new_x }) => {
if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() { if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() {
shape_editor.reposition_control_point(&id, responses, &document.network, &document.metadata, DVec2::new(new_x, coordinates.y), layer); shape_editor.reposition_control_point(&id, &document.network, &document.metadata, DVec2::new(new_x, coordinates.y), layer, responses);
} }
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::SelectedPointYChanged { new_y }) => { (_, PathToolMessage::SelectedPointYChanged { new_y }) => {
if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() { if let Some(&SingleSelectedPoint { coordinates, id, layer, .. }) = tool_data.selection_status.as_one() {
shape_editor.reposition_control_point(&id, responses, &document.network, &document.metadata, DVec2::new(coordinates.x, new_y), layer); shape_editor.reposition_control_point(&id, &document.network, &document.metadata, DVec2::new(coordinates.x, new_y), layer, responses);
} }
PathToolFsmState::Ready PathToolFsmState::Ready
} }
@@ -657,14 +668,14 @@ impl Fsm for PathToolFsmState {
} }
(_, PathToolMessage::ManipulatorMakeHandlesColinear) => { (_, PathToolMessage::ManipulatorMakeHandlesColinear) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
shape_editor.set_colinear_handles_state_on_selected(true, responses); shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, &document.network);
responses.add(DocumentMessage::CommitTransaction); responses.add(DocumentMessage::CommitTransaction);
responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
(_, PathToolMessage::ManipulatorMakeHandlesFree) => { (_, PathToolMessage::ManipulatorMakeHandlesFree) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
shape_editor.set_colinear_handles_state_on_selected(false, responses); shape_editor.disable_colinear_handles_state_on_selected(&document.metadata, &document.network, responses);
responses.add(DocumentMessage::CommitTransaction); responses.add(DocumentMessage::CommitTransaction);
PathToolFsmState::Ready PathToolFsmState::Ready
} }
@@ -743,10 +754,11 @@ impl SelectionStatus {
} }
} }
fn as_multiple(&self) -> Option<&MultipleSelectedPoints> { fn angle(&self) -> Option<ManipulatorAngle> {
match self { match self {
SelectionStatus::Multiple(multiple) => Some(multiple), Self::None => None,
_ => None, Self::One(one) => Some(one.manipulator_angle),
Self::Multiple(one) => Some(one.manipulator_angle),
} }
} }
} }
@@ -775,33 +787,23 @@ fn get_selection_status(document_network: &NodeNetwork, document_metadata: &Docu
let Some(layer) = selection_layers.find(|(_, v)| *v > 0).map(|(k, _)| k) else { let Some(layer) = selection_layers.find(|(_, v)| *v > 0).map(|(k, _)| k) else {
return SelectionStatus::None; return SelectionStatus::None;
}; };
let Some(subpaths) = get_subpaths(layer, document_network) else { let Some(vector_data) = document_metadata.compute_modified_vector(layer, document_network) else {
return SelectionStatus::None; return SelectionStatus::None;
}; };
let Some(colinear_manipulators) = get_colinear_manipulators(layer, document_network) else { let Some(&point) = shape_state.selected_points().next() else {
return SelectionStatus::None; return SelectionStatus::None;
}; };
let Some(point) = shape_state.selected_points().next() else { let Some(local_position) = point.get_position(&vector_data) else {
return SelectionStatus::None;
};
let Some(manipulator) = get_manipulator_from_id(subpaths, point.group) else {
return SelectionStatus::None;
};
let Some(local_position) = point.manipulator_type.get_position(manipulator) else {
return SelectionStatus::None; return SelectionStatus::None;
}; };
let coordinates = document_metadata.transform_to_document(layer).transform_point2(local_position); let coordinates = document_metadata.transform_to_document(layer).transform_point2(local_position);
let manipulator_angle = if colinear_manipulators.contains(&point.group) { let manipulator_angle = if vector_data.colinear(point) { ManipulatorAngle::Colinear } else { ManipulatorAngle::Free };
ManipulatorAngle::Colinear
} else {
ManipulatorAngle::Free
};
return SelectionStatus::One(SingleSelectedPoint { return SelectionStatus::One(SingleSelectedPoint {
coordinates, coordinates,
layer, layer,
id: *point, id: point,
manipulator_angle, manipulator_angle,
}); });
}; };
@@ -809,7 +811,7 @@ fn get_selection_status(document_network: &NodeNetwork, document_metadata: &Docu
// Check to see if multiple manipulator groups are selected // Check to see if multiple manipulator groups are selected
if total_selected_points > 1 { if total_selected_points > 1 {
return SelectionStatus::Multiple(MultipleSelectedPoints { return SelectionStatus::Multiple(MultipleSelectedPoints {
manipulator_angle: shape_state.selected_manipulator_angles(document_network), manipulator_angle: shape_state.selected_manipulator_angles(document_network, document_metadata),
}); });
} }
+244 -358
View File
@@ -1,21 +1,22 @@
use super::tool_prelude::*; use super::tool_prelude::*;
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::consts::LINE_ROTATE_SNAP_ANGLE; use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::messages::portfolio::document::graph_operation::utility_types::VectorDataModification; use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays; use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
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::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; use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::graph_modification_utils::get_subpaths;
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::utility_functions::should_extend; use crate::messages::tool::common_functionality::utility_functions::should_extend;
use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_core::uuid::{generate_uuid, ManipulatorGroupId}; use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::{PointId, VectorModificationType};
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use graphene_core::Color; use graphene_core::Color;
use graphene_std::vector::{HandleId, SegmentId};
#[derive(Default)] #[derive(Default)]
pub struct PenTool { pub struct PenTool {
@@ -187,310 +188,175 @@ impl ToolTransition for PenTool {
} }
} }
} }
#[derive(Default)] #[derive(Clone, Debug, Default)]
struct ModifierState { struct ModifierState {
snap_angle: bool, snap_angle: bool,
lock_angle: bool, lock_angle: bool,
break_handle: bool, break_handle: bool,
} }
#[derive(Clone, Debug)]
struct LastPoint {
id: PointId,
pos: DVec2,
in_segment: Option<SegmentId>,
handle_start: DVec2,
}
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
struct PenToolData { struct PenToolData {
weight: f64,
layer: Option<LayerNodeIdentifier>, layer: Option<LayerNodeIdentifier>,
subpath_index: usize,
snap_manager: SnapManager, snap_manager: SnapManager,
colinear_handles: bool, latest_points: Vec<LastPoint>,
// Indicates that curve extension is occurring from the first point, rather than (more commonly) the last point point_index: usize,
from_start: bool, handle_end: Option<DVec2>,
next_point: DVec2,
next_handle_start: DVec2,
g1_continuous: bool,
angle: f64, angle: f64,
auto_panning: AutoPanning, auto_panning: AutoPanning,
modifiers: ModifierState,
} }
impl PenToolData { impl PenToolData {
fn extend_subpath(&mut self, layer: LayerNodeIdentifier, subpath_index: usize, from_start: bool, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { fn latest_point(&self) -> Option<&LastPoint> {
self.layer = Some(layer); self.latest_points.get(self.point_index)
self.from_start = from_start;
self.subpath_index = subpath_index;
let Some(subpaths) = get_subpaths(layer, &document.network) else { return };
let manipulator_groups = subpaths[subpath_index].manipulator_groups();
let first_or_last = if from_start { manipulator_groups.first() } else { manipulator_groups.last() };
let Some(last_handle) = first_or_last else { return };
let id = last_handle.id;
let modification = VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false };
// Stop the handles on the first point from being colinear
responses.add(GraphOperationMessage::Vector { layer, modification });
} }
fn create_new_path( fn latest_point_mut(&mut self) -> Option<&mut LastPoint> {
&mut self, self.latest_points.get_mut(self.point_index)
document: &DocumentMessageHandler, }
line_weight: f64,
stroke_color: Option<Color>,
fill_color: Option<Color>,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) {
let parent = document.new_layer_parent(true);
// Deselect layers because we are now creating a new layer
responses.add(DocumentMessage::DeselectAllLayers);
// Get the position and set properties fn add_point(&mut self, point: LastPoint) {
let transform = document.metadata().transform_to_document(parent); self.point_index = (self.point_index + 1).min(self.latest_points.len());
let point = SnapCandidatePoint::handle(document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position)); self.latest_points.truncate(self.point_index);
let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false); self.latest_points.push(point);
let start_position = transform.inverse().transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
self.weight = line_weight;
// Create the initial shape with a `bez_path` (only contains a moveto initially)
let subpath = bezier_rs::Subpath::new(vec![bezier_rs::ManipulatorGroup::new(start_position, Some(start_position), Some(start_position))], false);
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), parent, responses);
self.layer = Some(layer);
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = fill_color { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(stroke_color, line_weight),
});
self.from_start = false;
self.subpath_index = 0;
} }
/// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged. /// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged.
fn bend_from_previous_point(&mut self, document: &DocumentMessageHandler, transform: DAffine2, responses: &mut VecDeque<Message>) { fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2) {
(|| -> Option<()> { self.g1_continuous = true;
// Get subpath
let layer = self.layer?;
let subpath = &get_subpaths(layer, &document.network)?[self.subpath_index];
// Get the last manipulator group and the one previous to that
let mut manipulator_groups = subpath.manipulator_groups().iter();
let last_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? };
let previous_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? };
// Get correct handle types
let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle };
// Get manipulator points
let last_anchor = last_manipulator_group.anchor;
let previous_anchor = previous_manipulator_group.anchor;
// Break the control
let transform = document.metadata.document_to_viewport * transform;
let on_top = transform.transform_point2(last_anchor).distance_squared(transform.transform_point2(previous_anchor)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2);
if !on_top {
return None;
}
// Remove the point that has just been placed
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::RemoveManipulatorGroup { id: last_manipulator_group.id },
});
// Move the in handle of the previous anchor to on top of the previous position
let point = ManipulatorPointId::new(previous_manipulator_group.id, outwards_handle);
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorPosition { point, position: previous_anchor },
});
// Stop the handles on the last point from being colinear
let id = previous_manipulator_group.id;
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false },
});
self.colinear_handles = false;
None
})();
}
fn finish_placing_handle(&mut self, document: &DocumentMessageHandler, transform: DAffine2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
// Get subpath
let layer = self.layer?;
let subpath = &get_subpaths(layer, &document.network)?[self.subpath_index];
// Get the last manipulator group and the one previous to that
let mut manipulator_groups = subpath.manipulator_groups().iter();
let last_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? };
let previous_manipulator_group = if self.from_start { manipulator_groups.next() } else { manipulator_groups.next_back() };
// Get the first manipulator group
let first_manipulator_group = if self.from_start {
subpath.manipulator_groups().last()?
} else {
subpath.manipulator_groups().first()?
};
// Get correct handle types
let inwards_handle = if self.from_start { SelectedType::OutHandle } else { SelectedType::InHandle };
let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle };
// Get manipulator points
let last_anchor = last_manipulator_group.anchor;
let first_anchor = first_manipulator_group.anchor;
let last_in = inwards_handle.get_position(last_manipulator_group)?;
let transform = document.metadata.document_to_viewport * transform;
let transformed_distance_between_squared = transform.transform_point2(last_anchor).distance_squared(transform.transform_point2(first_anchor));
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2);
let should_close_path = transformed_distance_between_squared < snap_point_tolerance_squared && previous_manipulator_group.is_some();
if should_close_path {
// Move the in handle of the first point to where the user has placed it
let point = ManipulatorPointId::new(first_manipulator_group.id, inwards_handle);
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorPosition { point, position: last_in },
});
// Stop the handles on the first point from being colinear
let id = first_manipulator_group.id;
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear: false },
});
// Remove the point that has just been placed
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::RemoveManipulatorGroup { id: last_manipulator_group.id },
});
// Push a close path node
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetClosed { index: 0, closed: true },
});
responses.add(DocumentMessage::CommitTransaction);
// Clean up tool data
self.layer = None;
self.snap_manager.cleanup(responses);
// Return to ready state
return Some(PenToolFsmState::Ready);
}
// Add a new manipulator for the next anchor that we will place
if let Some(out_handle) = outwards_handle.get_position(last_manipulator_group) {
responses.add(add_manipulator_group(self.layer, self.from_start, bezier_rs::ManipulatorGroup::new_anchor(out_handle)));
}
Some(PenToolFsmState::PlacingAnchor)
}
fn drag_handle(&mut self, mut snap_data: SnapData, transform: DAffine2, mouse: DVec2, modifiers: ModifierState, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let document = snap_data.document; let document = snap_data.document;
// Get subpath self.next_handle_start = self.next_point;
let subpath = &get_subpaths(self.layer?, &document.network)?[self.subpath_index];
// Get the last manipulator group // Break the control
let manipulator_groups = subpath.manipulator_groups(); let Some(last_pos) = self.latest_point().map(|point| point.pos) else { return };
let last_manipulator_group = if self.from_start { manipulator_groups.first()? } else { manipulator_groups.last()? }; let transform = document.metadata.document_to_viewport * transform;
let on_top = transform.transform_point2(self.next_point).distance_squared(transform.transform_point2(last_pos)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2);
// Get correct handle types if on_top {
let inwards_handle = if self.from_start { SelectedType::OutHandle } else { SelectedType::InHandle }; if let Some(point) = self.latest_point_mut() {
let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle }; point.in_segment = None;
}
// Get manipulator points self.handle_end = None;
let last_anchor = last_manipulator_group.anchor;
let colinear = !modifiers.break_handle && self.colinear_handles;
snap_data.manipulators = vec![(self.layer?, last_manipulator_group.id)];
let position = self.compute_snapped_angle(snap_data, transform, modifiers.lock_angle, modifiers.snap_angle, colinear, mouse, Some(last_anchor), false);
if !position.is_finite() {
return Some(PenToolFsmState::DraggingHandle);
} }
}
// Update points on current segment (to show preview of new handle) fn finish_placing_handle(&mut self, snap_data: SnapData, transform: DAffine2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let point = ManipulatorPointId::new(last_manipulator_group.id, outwards_handle); let document = snap_data.document;
responses.add(GraphOperationMessage::Vector { let next_handle_start = self.next_handle_start;
layer: self.layer?, let handle_start = self.latest_point()?.handle_start;
modification: VectorDataModification::SetManipulatorPosition { point, position }, let mouse = snap_data.input.mouse.position;
let Some(handle_end) = self.handle_end else {
self.handle_end = Some(next_handle_start);
self.place_anchor(snap_data, transform, mouse, responses);
self.latest_point_mut()?.handle_start = next_handle_start;
return None;
};
let next_point = self.next_point;
self.place_anchor(snap_data, transform, mouse, responses);
let handles = [handle_start - self.latest_point()?.pos, handle_end - next_point].map(Some);
// Get close path
let mut end = None;
let layer = self.layer?;
let vector_data = document.metadata.compute_modified_vector(layer, &document.network)?;
let start = self.latest_point()?.id;
let transform = document.metadata.document_to_viewport * transform;
for id in vector_data.single_connected_points().filter(|&point| point != start) {
let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue };
let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point));
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2);
if transformed_distance_between_squared < snap_point_tolerance_squared {
end = Some(id);
}
}
let close_subpath = end.is_some();
// Generate new point if not closing
let end = end.unwrap_or_else(|| {
let end = PointId::generate();
let modification_type = VectorModificationType::InsertPoint { id: end, position: next_point };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
end
}); });
// Place the previous anchor's in handle at the opposing position let points = [start, end];
if colinear { let id = SegmentId::generate();
// Could also be written as `last_anchor.position * 2 - pos` but this way avoids overflow/underflow better let modification_type = VectorModificationType::InsertSegment { id, points, handles };
let position = last_anchor - (position - last_anchor); responses.add(GraphOperationMessage::Vector { layer, modification_type });
let point = ManipulatorPointId::new(last_manipulator_group.id, inwards_handle);
// Mirror
if let Some(last_segment) = self.latest_point().and_then(|point| point.in_segment) {
responses.add(GraphOperationMessage::Vector { responses.add(GraphOperationMessage::Vector {
layer: self.layer?, layer,
modification: VectorDataModification::SetManipulatorPosition { point, position }, modification_type: VectorModificationType::SetG1Continuous {
handles: [HandleId::end(last_segment), HandleId::primary(id)],
enabled: true,
},
}); });
} }
if !close_subpath {
self.add_point(LastPoint {
id: end,
pos: next_point,
in_segment: self.g1_continuous.then_some(id),
handle_start: next_handle_start,
});
}
Some(if close_subpath { PenToolFsmState::Ready } else { PenToolFsmState::PlacingAnchor })
}
// Update the colinear handles status of the currently modifying point fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let id = last_manipulator_group.id; let colinear = !self.modifiers.break_handle && self.handle_end.is_some();
responses.add(GraphOperationMessage::Vector { self.next_handle_start = self.compute_snapped_angle(snap_data, transform, colinear, mouse, Some(self.next_point), false);
layer: self.layer?, if let Some(handle_end) = self.handle_end.as_mut().filter(|_| colinear) {
modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear }, *handle_end = self.next_point * 2. - self.next_handle_start;
}); self.g1_continuous = true;
} else {
self.g1_continuous = false;
}
responses.add(OverlaysMessage::Draw);
Some(PenToolFsmState::DraggingHandle) Some(PenToolFsmState::DraggingHandle)
} }
fn place_anchor(&mut self, mut snap_data: SnapData, transform: DAffine2, mouse: DVec2, modifiers: ModifierState, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> { fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let document = snap_data.document; let relative = self.latest_point().map(|point| point.pos);
// Get subpath self.next_point = self.compute_snapped_angle(snap_data, transform, false, mouse, relative, true);
let layer = self.layer?; if let Some(handle_end) = self.handle_end.as_mut() {
let subpath = &get_subpaths(layer, &document.network)?[self.subpath_index]; *handle_end = self.next_point;
self.next_handle_start = self.next_point;
// Get the last manipulator group and the one previous to that
let mut manipulator_groups = subpath.manipulator_groups().iter();
let last_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? };
let previous_manipulator_group = if self.from_start { manipulator_groups.next() } else { manipulator_groups.next_back() };
// Get the first manipulator group
let manipulator_groups = subpath.manipulator_groups();
let first_manipulator_group = if self.from_start { manipulator_groups.last()? } else { manipulator_groups.first()? };
// Get manipulator points
let first_anchor = first_manipulator_group.anchor;
let previous_anchor = previous_manipulator_group.map(|group| group.anchor);
let pos = if let Some(last_anchor) = previous_anchor.filter(|&a| mouse.distance_squared(transform.transform_point2(a)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2)) {
// Snap to the previously placed point (to show break control)
last_anchor
} else if mouse.distance_squared(transform.transform_point2(first_anchor)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2) {
// Snap to the first point (to show close path)
first_anchor
} else {
snap_data.manipulators = vec![(self.layer?, last_manipulator_group.id)];
self.compute_snapped_angle(snap_data, transform, modifiers.lock_angle, modifiers.snap_angle, false, mouse, previous_anchor, true)
};
for manipulator_type in [SelectedType::Anchor, SelectedType::InHandle, SelectedType::OutHandle] {
let point = ManipulatorPointId::new(last_manipulator_group.id, manipulator_type);
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorPosition { point, position: pos },
});
} }
responses.add(OverlaysMessage::Draw);
Some(PenToolFsmState::PlacingAnchor) Some(PenToolFsmState::PlacingAnchor)
} }
/// Snap the angle of the line from relative to position if the key is pressed. /// Snap the angle of the line from relative to position if the key is pressed.
fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, lock_angle: bool, snap_angle: bool, colinear: bool, mouse: DVec2, relative: Option<DVec2>, neighbor: bool) -> DVec2 { fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, colinear: bool, mouse: DVec2, relative: Option<DVec2>, neighbor: bool) -> DVec2 {
let ModifierState { snap_angle, lock_angle, .. } = self.modifiers;
let document = snap_data.document; let document = snap_data.document;
let mut document_pos = document.metadata.document_to_viewport.inverse().transform_point2(mouse); let mut document_pos = document.metadata.document_to_viewport.inverse().transform_point2(mouse);
let snap = &mut self.snap_manager; let snap = &mut self.snap_manager;
let neighbors = relative.filter(|_| neighbor).map_or(Vec::new(), |neighbor| vec![neighbor]); let neighbors = relative.filter(|_| neighbor).map_or(Vec::new(), |neighbor| vec![neighbor]);
if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| snap_angle || lock_angle) { if let Some(relative) = relative
.map(|layer| transform.transform_point2(layer))
.filter(|&relative| (snap_angle || lock_angle) && (relative - document_pos).length_squared() > f64::EPSILON * 100.)
{
let resolution = LINE_ROTATE_SNAP_ANGLE.to_radians(); let resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
let angle = if lock_angle { let angle = if lock_angle {
@@ -540,56 +406,11 @@ impl PenToolData {
if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) { if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) {
if (relative - document_pos) != DVec2::ZERO { if (relative - document_pos) != DVec2::ZERO {
self.angle = -(relative - document_pos).angle_between(DVec2::X) self.angle = -(relative - document_pos).angle_between(DVec2::X)
} else {
self.angle = 0.0;
} }
} }
transform.inverse().transform_point2(document_pos) transform.inverse().transform_point2(document_pos)
} }
fn finish_transaction(&mut self, fsm: PenToolFsmState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<DocumentMessage> {
// Get subpath
let subpath = &get_subpaths(self.layer?, &document.network)?[self.subpath_index];
// Abort if only one manipulator group has been placed
if fsm == PenToolFsmState::PlacingAnchor && subpath.len() < 3 {
return None;
}
// Get the last manipulator group and the one previous to that
let mut manipulator_groups = subpath.manipulator_groups().iter();
let mut last_manipulator_group = if self.from_start { manipulator_groups.next()? } else { manipulator_groups.next_back()? };
let previous_manipulator_group = if self.from_start { manipulator_groups.next() } else { manipulator_groups.next_back() };
// Get correct handle types
let outwards_handle = if self.from_start { SelectedType::InHandle } else { SelectedType::OutHandle };
// If placing anchor we should abort if there are less than three manipulators (as the last one gets deleted)
let Some(previous_manipulator_group) = previous_manipulator_group else {
return Some(DocumentMessage::AbortTransaction);
};
// Clean up if there are two or more manipulators
// Remove the unplaced anchor if in anchor placing mode
if fsm == PenToolFsmState::PlacingAnchor {
responses.add(GraphOperationMessage::Vector {
layer: self.layer?,
modification: VectorDataModification::RemoveManipulatorGroup { id: last_manipulator_group.id },
});
last_manipulator_group = previous_manipulator_group;
}
// Remove the out handle
let point = ManipulatorPointId::new(last_manipulator_group.id, outwards_handle);
let position = last_manipulator_group.anchor;
responses.add(GraphOperationMessage::Vector {
layer: self.layer?,
modification: VectorDataModification::SetManipulatorPosition { point, position },
});
Some(DocumentMessage::CommitTransaction)
}
} }
impl Fsm for PenToolFsmState { impl Fsm for PenToolFsmState {
@@ -628,9 +449,52 @@ impl Fsm for PenToolFsmState {
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
self self
} }
(_, PenToolMessage::Overlays(mut overlay_context)) => { (PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => {
path_overlays(document, shape_editor, &mut overlay_context); path_overlays(document, shape_editor, &mut overlay_context);
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self
}
(_, PenToolMessage::Overlays(mut overlay_context)) => {
let transform = document.metadata.document_to_viewport * transform;
if let (Some((start, handle_start)), Some(handle_end)) = (tool_data.latest_point().map(|point| (point.pos, point.handle_start)), tool_data.handle_end) {
let handles = BezierHandles::Cubic { handle_start, handle_end };
let bezier = Bezier {
start,
handles,
end: tool_data.next_point,
};
overlay_context.outline_bezier(bezier, transform);
}
let valid = |point: DVec2, handle: DVec2| point.distance_squared(handle) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
let next_point = transform.transform_point2(tool_data.next_point);
let next_handle_start = transform.transform_point2(tool_data.next_handle_start);
overlay_context.line(next_point, next_handle_start);
let start = tool_data.latest_point().map(|point| transform.transform_point2(point.pos));
let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start));
let handle_end = tool_data.handle_end.map(|point| transform.transform_point2(point));
if let (Some(start), Some(handle_start), Some(handle_end)) = (start, handle_start, handle_end) {
overlay_context.line(start, handle_start);
overlay_context.line(next_point, handle_end);
path_overlays(document, shape_editor, &mut overlay_context);
if self == PenToolFsmState::DraggingHandle && valid(next_point, handle_end) {
overlay_context.manipulator_handle(handle_end, false);
}
if valid(start, handle_start) {
overlay_context.manipulator_handle(handle_start, false);
}
} else {
path_overlays(document, shape_editor, &mut overlay_context);
}
if self == PenToolFsmState::DraggingHandle && valid(next_point, next_handle_start) {
overlay_context.manipulator_handle(next_handle_start, false);
}
overlay_context.manipulator_anchor(next_point, false, None);
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self self
} }
@@ -644,46 +508,71 @@ impl Fsm for PenToolFsmState {
(PenToolFsmState::Ready, PenToolMessage::DragStart) => { (PenToolFsmState::Ready, PenToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
// Prevent the initial point from having a colinear in handle while dragging the out handle
tool_data.colinear_handles = false;
// Perform extension of an existing path // Perform extension of an existing path
if let Some((layer, subpath_index, from_start)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) { if let Some((layer, point, position)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
tool_data.extend_subpath(layer, subpath_index, from_start, document, responses); tool_data.add_point(LastPoint {
id: point,
pos: position,
in_segment: None,
handle_start: position,
});
tool_data.layer = Some(layer);
tool_data.next_point = position;
tool_data.next_handle_start = position;
} else { } else {
tool_data.create_new_path( // New path layer
document, let nodes = {
tool_options.line_weight, let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
tool_options.stroke.active_color(), HashMap::from([(NodeId(0), node_type.to_document_node_default_inputs([], Default::default()))])
tool_options.fill.active_color(), };
input,
responses, let parent = document.new_layer_parent(true);
); let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, parent, responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
tool_data.layer = Some(layer);
// Generate first point
let id = PointId::generate();
let transform = document.metadata().transform_to_document(parent);
let point = SnapCandidatePoint::handle(document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
let pos = transform.inverse().transform_point2(snapped.snapped_point_document);
let modification_type = VectorModificationType::InsertPoint { id, position: pos };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
tool_data.add_point(LastPoint {
id,
pos,
in_segment: None,
handle_start: pos,
});
tool_data.next_point = pos;
tool_data.next_handle_start = pos;
} }
tool_data.handle_end = None;
// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle // Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle
PenToolFsmState::DraggingHandle PenToolFsmState::DraggingHandle
} }
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => { (PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction); if tool_data.handle_end.is_some() {
tool_data.bend_from_previous_point(document, transform, responses); responses.add(DocumentMessage::StartTransaction);
}
tool_data.bend_from_previous_point(SnapData::new(document, input), transform);
PenToolFsmState::DraggingHandle PenToolFsmState::DraggingHandle
} }
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => { (PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data
tool_data.colinear_handles = true; .finish_placing_handle(SnapData::new(document, input), transform, responses)
tool_data.finish_placing_handle(document, transform, responses).unwrap_or(PenToolFsmState::PlacingAnchor) .unwrap_or(PenToolFsmState::PlacingAnchor),
}
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => { (PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
let modifiers = ModifierState { tool_data.modifiers = ModifierState {
snap_angle: input.keyboard.key(snap_angle), snap_angle: input.keyboard.key(snap_angle),
lock_angle: input.keyboard.key(lock_angle), lock_angle: input.keyboard.key(lock_angle),
break_handle: input.keyboard.key(break_handle), break_handle: input.keyboard.key(break_handle),
}; };
let snap_data = SnapData::new(document, input); let snap_data = SnapData::new(document, input);
let state = tool_data let state = tool_data.drag_handle(snap_data, transform, input.mouse.position, responses).unwrap_or(PenToolFsmState::Ready);
.drag_handle(snap_data, transform, input.mouse.position, modifiers, responses)
.unwrap_or(PenToolFsmState::Ready);
// Auto-panning // Auto-panning
let messages = [ let messages = [
@@ -695,13 +584,13 @@ impl Fsm for PenToolFsmState {
state state
} }
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => { (PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
let modifiers = ModifierState { tool_data.modifiers = ModifierState {
snap_angle: input.keyboard.key(snap_angle), snap_angle: input.keyboard.key(snap_angle),
lock_angle: input.keyboard.key(lock_angle), lock_angle: input.keyboard.key(lock_angle),
break_handle: input.keyboard.key(break_handle), break_handle: input.keyboard.key(break_handle),
}; };
let state = tool_data let state = tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, modifiers, responses) .place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses)
.unwrap_or(PenToolFsmState::Ready); .unwrap_or(PenToolFsmState::Ready);
// Auto-panning // Auto-panning
@@ -741,11 +630,10 @@ impl Fsm for PenToolFsmState {
state state
} }
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Abort | PenToolMessage::Confirm) => { (PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Abort | PenToolMessage::Confirm) => {
// Abort or commit the transaction to the undo history
let message = tool_data.finish_transaction(self, document, responses).unwrap_or(DocumentMessage::AbortTransaction);
responses.add(message);
tool_data.layer = None; tool_data.layer = None;
tool_data.handle_end = None;
tool_data.latest_points.clear();
tool_data.point_index = 0;
tool_data.snap_manager.cleanup(responses); tool_data.snap_manager.cleanup(responses);
PenToolFsmState::Ready PenToolFsmState::Ready
@@ -755,12 +643,23 @@ impl Fsm for PenToolFsmState {
self self
} }
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => tool_data (PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => {
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, ModifierState::default(), responses) if tool_data.point_index > 0 {
.unwrap_or(PenToolFsmState::PlacingAnchor), tool_data.point_index -= 1;
(_, PenToolMessage::Redo) => tool_data tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, ModifierState::default(), responses) .place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor), .unwrap_or(PenToolFsmState::PlacingAnchor)
} else {
responses.add(PenToolMessage::Abort);
self
}
}
(_, PenToolMessage::Redo) => {
tool_data.point_index = (tool_data.point_index + 1).min(tool_data.latest_points.len().saturating_sub(1));
tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor)
}
_ => self, _ => self,
} }
} }
@@ -800,16 +699,3 @@ impl Fsm for PenToolFsmState {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
} }
} }
/// Pushes a [ManipulatorGroup] to the current layer via a [GraphOperationMessage].
fn add_manipulator_group(layer: Option<LayerNodeIdentifier>, from_start: bool, manipulator_group: bezier_rs::ManipulatorGroup<ManipulatorGroupId>) -> Message {
let Some(layer) = layer else {
return Message::NoOp;
};
let modification = if from_start {
VectorDataModification::AddStartManipulatorGroup { subpath_index: 0, manipulator_group }
} else {
VectorDataModification::AddEndManipulatorGroup { subpath_index: 0, manipulator_group }
};
GraphOperationMessage::Vector { layer, modification }.into()
}
@@ -1,5 +1,6 @@
use super::tool_prelude::*; use super::tool_prelude::*;
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_types::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
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};
@@ -7,9 +8,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping::SnapData; use crate::messages::tool::common_functionality::snapping::SnapData;
use graph_craft::document::NodeId; use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid; use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color; use graphene_core::Color;
#[derive(Default)] #[derive(Default)]
@@ -244,11 +244,34 @@ impl Fsm for PolygonToolFsmState {
polygon_data.start(document, input); polygon_data.start(document, input);
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
let subpath = match tool_options.polygon_type { let nodes = {
PolygonType::Convex => bezier_rs::Subpath::new_regular_polygon(DVec2::ZERO, tool_options.vertices as u64, 1.), let node = match tool_options.polygon_type {
PolygonType::Star => bezier_rs::Subpath::new_star_polygon(DVec2::ZERO, tool_options.vertices as u64, 1., 0.5), PolygonType::Convex => resolve_document_node_type("Regular Polygon")
.expect("Regular Polygon node does not exist")
.to_document_node_default_inputs(
[
None,
Some(NodeInput::value(TaggedValue::U32(tool_options.vertices as u32), false)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
],
Default::default(),
),
PolygonType::Star => resolve_document_node_type("Star").expect("Star node does not exist").to_document_node_default_inputs(
[
None,
Some(NodeInput::value(TaggedValue::U32(tool_options.vertices as u32), false)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
Some(NodeInput::value(TaggedValue::F64(0.25), false)),
],
Default::default(),
),
};
HashMap::from([(NodeId(0), node)])
}; };
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), document.new_layer_parent(true), responses); let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
polygon_data.layer = Some(layer); polygon_data.layer = Some(layer);
responses.add(GraphOperationMessage::TransformSet { responses.add(GraphOperationMessage::TransformSet {
@@ -258,22 +281,19 @@ impl Fsm for PolygonToolFsmState {
skip_rerender: false, skip_rerender: false,
}); });
let fill_color = tool_options.fill.active_color();
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = fill_color { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight),
});
PolygonToolFsmState::Drawing PolygonToolFsmState::Drawing
} }
(PolygonToolFsmState::Drawing, PolygonToolMessage::PointerMove { center, lock_ratio }) => { (PolygonToolFsmState::Drawing, PolygonToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(message) = polygon_data.calculate_transform(document, input, center, lock_ratio, false) { if let Some([start, end]) = tool_data.data.calculate_points(document, input, center, lock_ratio) {
responses.add(message); if let Some(layer) = tool_data.data.layer {
// TODO: make the scale impact the polygon/star node - we need to determine how to allow the polygon node to make irregular shapes
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(end - start, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
} }
// Auto-panning // Auto-panning
@@ -1,4 +1,5 @@
use super::tool_prelude::*; use super::tool_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::{graph_operation::utility_types::TransformIn, overlays::utility_types::OverlayContext}; use crate::messages::portfolio::document::{graph_operation::utility_types::TransformIn, overlays::utility_types::OverlayContext};
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};
@@ -6,9 +7,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping::SnapData; use crate::messages::tool::common_functionality::snapping::SnapData;
use graph_craft::document::NodeId; use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid; use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color; use graphene_core::Color;
#[derive(Default)] #[derive(Default)]
@@ -205,11 +205,20 @@ impl Fsm for RectangleToolFsmState {
(RectangleToolFsmState::Ready, RectangleToolMessage::DragStart) => { (RectangleToolFsmState::Ready, RectangleToolMessage::DragStart) => {
shape_data.start(document, input); shape_data.start(document, input);
let subpath = bezier_rs::Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), document.new_layer_parent(true), responses); let nodes = {
let node_type = resolve_document_node_type("Rectangle").expect("Rectangle node does not exist");
let node = node_type.to_document_node_default_inputs(
[None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::F64(1.), false))],
Default::default(),
);
HashMap::from([(NodeId(0), node)])
};
let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses);
tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
shape_data.layer = Some(layer); shape_data.layer = Some(layer);
responses.add(GraphOperationMessage::TransformSet { responses.add(GraphOperationMessage::TransformSet {
@@ -219,22 +228,19 @@ impl Fsm for RectangleToolFsmState {
skip_rerender: false, skip_rerender: false,
}); });
let fill_color = tool_options.fill.active_color();
responses.add(GraphOperationMessage::FillSet {
layer,
fill: if let Some(color) = fill_color { Fill::Solid(color) } else { Fill::None },
});
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight),
});
RectangleToolFsmState::Drawing RectangleToolFsmState::Drawing
} }
(RectangleToolFsmState::Drawing, RectangleToolMessage::PointerMove { center, lock_ratio }) => { (RectangleToolFsmState::Drawing, RectangleToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, input, center, lock_ratio, false) { if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) {
responses.add(message); if let Some(layer) = shape_data.layer {
// TODO: make the scale impact the rect node
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(end - start, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
} }
// Auto-panning // Auto-panning
@@ -529,7 +529,7 @@ impl Fsm for SelectToolFsmState {
.unwrap_or_default(); .unwrap_or_default();
let mut selected: Vec<_> = document.selected_nodes.selected_visible_and_unlocked_layers(document.metadata()).collect(); let mut selected: Vec<_> = document.selected_nodes.selected_visible_and_unlocked_layers(document.metadata()).collect();
let intersection_list = document.click_list(input.mouse.position, &document.network); let intersection_list = document.click_list(input.mouse.position, &document.network).collect::<Vec<_>>();
let intersection = document.find_deepest(&intersection_list, &document.network); let intersection = document.find_deepest(&intersection_list, &document.network);
// If the user is dragging the bounding box bounds, go into ResizingBounds mode. // If the user is dragging the bounding box bounds, go into ResizingBounds mode.
@@ -1264,7 +1264,7 @@ fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, document_network:
if is_layer_fed_by_node_of_name(layer, document_network, "Text") { if is_layer_fed_by_node_of_name(layer, document_network, "Text") {
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }); responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
responses.add(TextToolMessage::EditSelected); responses.add(TextToolMessage::EditSelected);
} else if is_layer_fed_by_node_of_name(layer, document_network, "Shape") { } else if is_layer_fed_by_node_of_name(layer, document_network, "Path") {
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }); responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path });
} }
} }
@@ -1,15 +1,14 @@
use super::tool_prelude::*; use super::tool_prelude::*;
use crate::consts::DRAG_THRESHOLD; use crate::consts::DRAG_THRESHOLD;
use crate::messages::portfolio::document::graph_operation::utility_types::VectorDataModification; use crate::messages::portfolio::document::node_graph::document_node_types::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::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; use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::snapping::SnapManager; use crate::messages::tool::common_functionality::snapping::SnapManager;
use graph_craft::document::NodeId; use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid; use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color; use graphene_core::Color;
#[derive(Default)] #[derive(Default)]
@@ -217,17 +216,15 @@ impl Fsm for SplineToolFsmState {
tool_data.weight = tool_options.line_weight; tool_data.weight = tool_options.line_weight;
let layer = graph_modification_utils::new_vector_layer(vec![], NodeId(generate_uuid()), parent, responses); let nodes = {
let node_type = resolve_document_node_type("Spline").expect("Spline node does not exist");
let node = node_type.to_document_node_default_inputs([None, Some(NodeInput::value(TaggedValue::VecDVec2(Vec::new()), false))], Default::default());
responses.add(GraphOperationMessage::FillSet { HashMap::from([(NodeId(0), node)])
layer, };
fill: if let Some(color) = tool_options.fill.active_color() { Fill::Solid(color) } else { Fill::None }, let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, parent, responses);
}); tool_options.fill.apply_fill(layer, responses);
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
responses.add(GraphOperationMessage::StrokeSet {
layer,
stroke: Stroke::new(tool_options.stroke.active_color(), tool_data.weight),
});
tool_data.layer = Some(layer); tool_data.layer = Some(layer);
SplineToolFsmState::Drawing SplineToolFsmState::Drawing
@@ -247,7 +244,7 @@ impl Fsm for SplineToolFsmState {
} }
} }
update_spline(tool_data, true, responses); update_spline(document, tool_data, true, responses);
SplineToolFsmState::Drawing SplineToolFsmState::Drawing
} }
@@ -260,7 +257,7 @@ impl Fsm for SplineToolFsmState {
let pos = transform.inverse().transform_point2(snapped_position); let pos = transform.inverse().transform_point2(snapped_position);
tool_data.next_point = pos; tool_data.next_point = pos;
update_spline(tool_data, true, responses); update_spline(document, tool_data, true, responses);
// Auto-panning // Auto-panning
let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()]; let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()];
@@ -283,7 +280,7 @@ impl Fsm for SplineToolFsmState {
} }
(SplineToolFsmState::Drawing, SplineToolMessage::Confirm | SplineToolMessage::Abort) => { (SplineToolFsmState::Drawing, SplineToolMessage::Confirm | SplineToolMessage::Abort) => {
if tool_data.points.len() >= 2 { if tool_data.points.len() >= 2 {
update_spline(tool_data, false, responses); update_spline(document, tool_data, false, responses);
responses.add(DocumentMessage::CommitTransaction); responses.add(DocumentMessage::CommitTransaction);
} else { } else {
responses.add(DocumentMessage::AbortTransaction); responses.add(DocumentMessage::AbortTransaction);
@@ -324,20 +321,17 @@ impl Fsm for SplineToolFsmState {
} }
} }
fn update_spline(tool_data: &SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) { fn update_spline(document: &DocumentMessageHandler, tool_data: &SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) {
let mut points = tool_data.points.clone(); let mut points = tool_data.points.clone();
if show_preview { if show_preview {
points.push(tool_data.next_point) points.push(tool_data.next_point)
} }
let value = TaggedValue::VecDVec2(points);
let subpath = bezier_rs::Subpath::new_cubic_spline(points); let Some(layer) = tool_data.layer else { return };
let Some(layer) = tool_data.layer else { let Some(node_id) = graph_modification_utils::NodeGraphLayer::new(layer, document.network()).upstream_node_id_from_name("Spline") else {
return; return;
}; };
responses.add_front(NodeGraphMessage::SetInputValue { node_id, input_index: 1, value });
graph_modification_utils::set_manipulator_colinear_handles_state(subpath.manipulator_groups(), layer, true, responses);
let subpaths = vec![subpath];
let modification = VectorDataModification::UpdateSubpaths { subpaths };
responses.add_front(GraphOperationMessage::Vector { layer, modification });
} }
@@ -313,25 +313,6 @@ impl TextToolData {
TextToolFsmState::Ready TextToolFsmState::Ready
} }
} }
fn get_bounds(&self, text: &str, font_cache: &FontCache) -> Option<[DVec2; 2]> {
let editing_text = self.editing_text.as_ref()?;
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data));
let subpaths = graphene_core::text::to_path(text, buzz_face, editing_text.font_size, None);
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box());
let combined_bounds = bounds.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]).unwrap_or_default();
Some(combined_bounds)
}
fn fix_text_bounds(&self, new_text: &str, _document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) -> Option<()> {
responses.add(GraphOperationMessage::UpdateBounds {
layer: self.layer,
old_bounds: self.get_bounds(&self.editing_text.as_ref()?.text, font_cache)?,
new_bounds: self.get_bounds(new_text, font_cache)?,
});
Some(())
}
} }
fn can_edit_selected(document: &DocumentMessageHandler) -> Option<LayerNodeIdentifier> { fn can_edit_selected(document: &DocumentMessageHandler) -> Option<LayerNodeIdentifier> {
@@ -429,7 +410,6 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Editing TextToolFsmState::Editing
} }
(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => { (TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => {
tool_data.fix_text_bounds(&new_text, document, font_cache, responses);
responses.add(NodeGraphMessage::SetQualifiedInputValue { responses.add(NodeGraphMessage::SetQualifiedInputValue {
node_id: graph_modification_utils::get_text_id(tool_data.layer, &document.network).unwrap(), node_id: graph_modification_utils::get_text_id(tool_data.layer, &document.network).unwrap(),
input_index: 1, input_index: 1,
@@ -2,7 +2,6 @@ use crate::consts::SLOWING_DIVISOR;
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing}; use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::utility_types::{ToolData, ToolType}; use crate::messages::tool::utility_types::{ToolData, ToolType};
@@ -69,16 +68,12 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
} }
if using_path_tool { if using_path_tool {
if let Some(subpaths) = selected_layers.first().and_then(|&layer| graph_modification_utils::get_subpaths(layer, &document.network)) { if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.metadata.compute_modified_vector(layer, &document.network)) {
*selected.original_transforms = OriginalTransforms::default(); *selected.original_transforms = OriginalTransforms::default();
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]); let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
let mut point_count: usize = 0; let mut point_count: usize = 0;
let get_location = |point: &ManipulatorPointId| { let get_location = |point: &ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
graph_modification_utils::get_manipulator_from_id(subpaths, point.group)
.and_then(|manipulator_group| point.manipulator_type.get_position(manipulator_group))
.map(|position| viewspace.transform_point2(position))
};
let points = shape_editor.selected_points(); let points = shape_editor.selected_points();
*selected.pivot = points.filter_map(get_location).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64; *selected.pivot = points.filter_map(get_location).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64;
+31 -14
View File
@@ -57,12 +57,14 @@ pub struct NodeRuntime {
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>, thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
/// The current click targets for layer nodes. /// The current click targets for layer nodes.
click_targets: HashMap<NodeId, Vec<ClickTarget>>, click_targets: HashMap<NodeId, Vec<ClickTarget>>,
/// Vector data in Path nodes.
vector_modify: HashMap<NodeId, VectorData>,
/// The current upstream transforms for nodes. /// The current upstream transforms for nodes.
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>, upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
} }
/// Messages passed from the editor thread to the node runtime thread. /// Messages passed from the editor thread to the node runtime thread.
enum NodeRuntimeMessage { pub enum NodeRuntimeMessage {
ExecutionRequest(ExecutionRequest), ExecutionRequest(ExecutionRequest),
FontCacheUpdate(FontCache), FontCacheUpdate(FontCache),
ImaginatePreferencesUpdate(ImaginatePreferences), ImaginatePreferencesUpdate(ImaginatePreferences),
@@ -78,24 +80,25 @@ pub struct ExportConfig {
pub size: DVec2, pub size: DVec2,
} }
pub(crate) struct ExecutionRequest { pub struct ExecutionRequest {
execution_id: u64, execution_id: u64,
graph: NodeNetwork, graph: NodeNetwork,
render_config: RenderConfig, render_config: RenderConfig,
} }
pub(crate) struct ExecutionResponse { pub struct ExecutionResponse {
execution_id: u64, execution_id: u64,
result: Result<TaggedValue, String>, result: Result<TaggedValue, String>,
responses: VecDeque<Message>, responses: VecDeque<Message>,
new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>, new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
new_vector_modify: HashMap<NodeId, VectorData>,
new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>, new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
resolved_types: ResolvedDocumentNodeTypes, resolved_types: ResolvedDocumentNodeTypes,
node_graph_errors: GraphErrors, node_graph_errors: GraphErrors,
transform: DAffine2, transform: DAffine2,
} }
enum NodeGraphUpdate { pub enum NodeGraphUpdate {
ExecutionResponse(ExecutionResponse), ExecutionResponse(ExecutionResponse),
NodeGraphUpdateMessage(NodeGraphUpdateMessage), NodeGraphUpdateMessage(NodeGraphUpdateMessage),
} }
@@ -119,7 +122,7 @@ thread_local! {
} }
impl NodeRuntime { impl NodeRuntime {
fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<NodeGraphUpdate>) -> Self { pub fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<NodeGraphUpdate>) -> Self {
Self { Self {
executor: DynamicExecutor::default(), executor: DynamicExecutor::default(),
receiver, receiver,
@@ -136,6 +139,7 @@ impl NodeRuntime {
thumbnail_renders: Default::default(), thumbnail_renders: Default::default(),
click_targets: HashMap::new(), click_targets: HashMap::new(),
vector_modify: HashMap::new(),
upstream_transforms: HashMap::new(), upstream_transforms: HashMap::new(),
} }
} }
@@ -166,6 +170,7 @@ impl NodeRuntime {
result, result,
responses, responses,
new_click_targets: self.click_targets.clone().into_iter().map(|(id, targets)| (LayerNodeIdentifier::new_unchecked(id), targets)).collect(), new_click_targets: self.click_targets.clone().into_iter().map(|(id, targets)| (LayerNodeIdentifier::new_unchecked(id), targets)).collect(),
new_vector_modify: self.vector_modify.clone(),
new_upstream_transforms: self.upstream_transforms.clone(), new_upstream_transforms: self.upstream_transforms.clone(),
resolved_types: self.resolved_types.clone(), resolved_types: self.resolved_types.clone(),
node_graph_errors: core::mem::take(&mut self.node_graph_errors), node_graph_errors: core::mem::take(&mut self.node_graph_errors),
@@ -233,7 +238,7 @@ impl NodeRuntime {
Some(t) if t == concrete!(WasmEditorApi) => (&self.executor).execute(editor_api).await.map_err(|e| e.to_string()), Some(t) if t == concrete!(WasmEditorApi) => (&self.executor).execute(editor_api).await.map_err(|e| e.to_string()),
Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()), Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()),
Some(t) => Err(format!("Invalid input type {t:?}")), Some(t) => Err(format!("Invalid input type {t:?}")),
_ => Err("No input type".to_string()), _ => Err(format!("No input type:\n{:?}", self.node_graph_errors)),
}; };
let result = match result { let result = match result {
Ok(value) => value, Ok(value) => value,
@@ -326,6 +331,9 @@ impl NodeRuntime {
}); });
*old_thumbnail_svg = new_thumbnail_svg; *old_thumbnail_svg = new_thumbnail_svg;
} }
} else if let Some(record) = introspected_data.downcast_ref::<IORecord<Footprint, VectorData>>() {
// Insert the vector modify if we are dealing with vector data
self.vector_modify.insert(parent_network_node_id, record.output.clone());
} }
// If this is `VectorData`, `ImageFrame`, or `GraphicElement` data: // If this is `VectorData`, `ImageFrame`, or `GraphicElement` data:
@@ -378,6 +386,13 @@ pub async fn run_node_graph() {
} }
} }
pub fn replace_node_runtime(runtime: NodeRuntime) -> Option<NodeRuntime> {
NODE_RUNTIME.with(|node_runtime| {
let mut node_runtime = node_runtime.borrow_mut();
node_runtime.replace(runtime)
})
}
#[derive(Debug)] #[derive(Debug)]
pub struct NodeGraphExecutor { pub struct NodeGraphExecutor {
sender: Sender<NodeRuntimeMessage>, sender: Sender<NodeRuntimeMessage>,
@@ -394,9 +409,7 @@ impl Default for NodeGraphExecutor {
fn default() -> Self { fn default() -> Self {
let (request_sender, request_receiver) = std::sync::mpsc::channel(); let (request_sender, request_receiver) = std::sync::mpsc::channel();
let (response_sender, response_receiver) = std::sync::mpsc::channel(); let (response_sender, response_receiver) = std::sync::mpsc::channel();
NODE_RUNTIME.with(|runtime| { replace_node_runtime(NodeRuntime::new(request_receiver, response_sender));
runtime.borrow_mut().replace(NodeRuntime::new(request_receiver, response_sender));
});
Self { Self {
futures: Default::default(), futures: Default::default(),
@@ -556,6 +569,7 @@ impl NodeGraphExecutor {
result, result,
responses: existing_responses, responses: existing_responses,
new_click_targets, new_click_targets,
new_vector_modify,
new_upstream_transforms, new_upstream_transforms,
resolved_types, resolved_types,
node_graph_errors, node_graph_errors,
@@ -567,15 +581,18 @@ impl NodeGraphExecutor {
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
let Ok(node_graph_output) = result else { let node_graph_output = match result {
// Clear the click targets while the graph is in an un-renderable state Ok(output) => output,
document.metadata.update_click_targets(HashMap::new()); Err(e) => {
// Clear the click targets while the graph is in an un-renderable state
document.metadata.update_from_monitor(HashMap::new(), HashMap::new());
return Err("Node graph evaluation failed".to_string()); return Err(format!("Node graph evaluation failed:\n{e}"));
}
}; };
document.metadata.update_transforms(new_upstream_transforms); document.metadata.update_transforms(new_upstream_transforms);
document.metadata.update_click_targets(new_click_targets); document.metadata.update_from_monitor(new_click_targets, new_vector_modify);
let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?; let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?;
if let Some(export_config) = execution_context.export_config { if let Some(export_config) = execution_context.export_config {
@@ -47,9 +47,9 @@
// Gradient color stops // Gradient color stops
$: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined; $: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined;
let activeIndex = 0 as number | undefined; let activeIndex = 0 as number | undefined;
$: selectedGradientColour = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color); $: selectedGradientColor = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color);
// Currently viewed color // Currently viewed color
$: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColour; $: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColor;
// New color components // New color components
let hue = hsva.h; let hue = hsva.h;
let saturation = hsva.s; let saturation = hsva.s;
@@ -38,7 +38,7 @@
<input <input
type="checkbox" type="checkbox"
id={`checkbox-input-${id}`} id={`checkbox-input-${id}`}
{checked} bind:checked
on:change={(_) => dispatch("checked", inputElement?.checked || false)} on:change={(_) => dispatch("checked", inputElement?.checked || false)}
{disabled} {disabled}
tabindex={disabled ? -1 : 0} tabindex={disabled ? -1 : 0}
@@ -331,7 +331,8 @@
} }
// If no buttons are down, we are stuck in the drag state after having released the mouse, so we should exit. // If no buttons are down, we are stuck in the drag state after having released the mouse, so we should exit.
if (e.buttons === 0) { // For some reason on firefox in wayland the button is -1 and the buttons is 0.
if (e.buttons === 0 && e.button !== -1) {
document.exitPointerLock(); document.exitPointerLock();
return; return;
} }
@@ -16,6 +16,7 @@ import {
TriggerImport, TriggerImport,
TriggerOpenDocument, TriggerOpenDocument,
TriggerRevokeBlobUrl, TriggerRevokeBlobUrl,
TriggerUpgradeDocumentToVectorManipulationFormat,
UpdateActiveDocument, UpdateActiveDocument,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
} from "@graphite/wasm-communication/messages"; } from "@graphite/wasm-communication/messages";
@@ -110,6 +111,11 @@ export function createPortfolioState(editor: Editor) {
editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => { editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => {
URL.revokeObjectURL(triggerRevokeBlobUrl.url); URL.revokeObjectURL(triggerRevokeBlobUrl.url);
}); });
editor.subscriptions.subscribeJsMessage(TriggerUpgradeDocumentToVectorManipulationFormat, async (triggerUpgradeDocumentToVectorManipulationFormat) => {
// TODO: Eventually remove this (probably starting late 2024)
const { documentId, documentName, documentIsAutoSaved, documentIsSaved, documentSerializedContent } = triggerUpgradeDocumentToVectorManipulationFormat;
editor.handle.triggerUpgradeDocumentToVectorManipulationFormat(documentId, documentName, documentIsAutoSaved, documentIsSaved, documentSerializedContent);
});
return { return {
subscribe, subscribe,
@@ -788,6 +788,15 @@ export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
export class TriggerViewportResize extends JsMessage {} export class TriggerViewportResize extends JsMessage {}
// TODO: Eventually remove this (probably starting late 2024)
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 {
@@ -1439,6 +1448,7 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerSavePreferences, TriggerSavePreferences,
TriggerTextCommit, TriggerTextCommit,
TriggerTextCopy, TriggerTextCopy,
TriggerUpgradeDocumentToVectorManipulationFormat,
TriggerViewportResize, TriggerViewportResize,
TriggerVisitLink, TriggerVisitLink,
UpdateActiveDocument, UpdateActiveDocument,
+1
View File
@@ -30,6 +30,7 @@ js-sys = { workspace = true }
wasm-bindgen-futures = { workspace = true } wasm-bindgen-futures = { workspace = true }
ron = { workspace = true, optional = true } ron = { workspace = true, optional = true }
bezier-rs = { workspace = true } bezier-rs = { workspace = true }
glam = { workspace = true }
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm # We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
wgpu = { workspace = true, features = ["fragile-send-sync-non-atomic-wasm"] } wgpu = { workspace = true, features = ["fragile-send-sync-non-atomic-wasm"] }
meval = "0.2.0" meval = "0.2.0"
+166 -1
View File
@@ -636,6 +636,166 @@ impl EditorHandle {
pub fn inject_imaginate_poll_server_status(&self) { pub fn inject_imaginate_poll_server_status(&self) {
self.dispatch(PortfolioMessage::ImaginatePollServerStatus); self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
} }
// TODO: Eventually remove this (probably starting late 2024)
#[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_types::resolve_document_node_type;
use editor::node_graph_executor::replace_node_runtime;
use editor::node_graph_executor::NodeRuntime;
use graph_craft::document::NodeInput;
use graph_craft::document::{value::TaggedValue, DocumentNodeImplementation};
use graphene_core::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));
let document_serialized_content = document_serialized_content.replace("\"ManipulatorGroupIds\"", "\"PointIds\"");
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(),
});
let document = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap();
for node in document.network.nodes.values_mut().filter(|d| d.name == "Artboard") {
if let Some(network) = node.implementation.get_network_mut() {
for node in network.nodes.values_mut() {
if node.name == "To Artboard" {
node.implementation = DocumentNodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _, _, _>");
if node.inputs.len() != 6 {
node.inputs.insert(2, NodeInput::value(TaggedValue::IVec2(glam::IVec2::default()), false));
}
}
}
}
}
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)
.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());
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,
});
return;
}
let mut updated_nodes = HashSet::new();
let document = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap();
document.metadata.load_structure(&document.network);
for node in document.network.nodes.iter().filter(|(_, d)| d.name == "Merge").map(|(id, _)| *id).collect::<Vec<_>>() {
let layer = LayerNodeIdentifier::new(node, &document.network);
if document.metadata.is_folder(layer) {
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.to_node(), &mut document.network, &mut document.metadata, &mut document.node_graph_handler, &mut responses) {
modify_inputs.modify_existing_inputs("Transform", |inputs, node_id, metadata| {
if !updated_nodes.insert(node_id) {
return;
}
let transform = get_current_transform(&inputs);
let upstream_transform = metadata.upstream_transform(node_id);
let pivot_transform = glam::DAffine2::from_translation(upstream_transform.transform_point2(bounds.local_pivot(get_current_normalized_pivot(&inputs))));
update_transform(inputs, pivot_transform * transform * pivot_transform.inverse());
});
modify_inputs.modify_existing_inputs("Shape", |inputs, node_id, _metadata| {
if !updated_nodes.insert(node_id) {
return;
}
let empty_vec = Vec::new();
let path_data = if let NodeInput::Value {
tagged_value: TaggedValue::Subpaths(translation),
..
} = &inputs[0]
{
translation
} else {
&empty_vec
};
let empty_vec = Vec::new();
let colinear_manipulators = if let NodeInput::Value {
tagged_value: TaggedValue::PointIds(translation),
..
} = &inputs[1]
{
translation
} else {
&empty_vec
};
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((node_id, VectorModification::create_from_vector(&vector_data)));
});
}
if let Some((id, modification)) = shape {
let metadata = document.network.nodes.remove(&id).map(|node| node.metadata).unwrap_or_default();
let node_type = resolve_document_node_type("Path").unwrap();
let document_node = node_type.to_document_node_default_inputs([None, Some(NodeInput::value(TaggedValue::VectorModification(modification), false))], metadata);
document.network.nodes.insert(id, document_node);
}
}
let document_serialized_content = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap().serialize_document();
replace_node_runtime(old_runtime.unwrap());
self.dispatch(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
});
}
} }
// ============================================================================ // ============================================================================
@@ -765,7 +925,12 @@ async fn poll_node_graph_evaluation() {
editor_and_handle(|editor, handle| { editor_and_handle(|editor, handle| {
let mut messages = VecDeque::new(); let mut messages = VecDeque::new();
editor.poll_node_graph_evaluation(&mut messages); if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) {
// TODO: This is a hacky way to suppress the error, but it shouldn't be generated in the first place
if e != "No active document" {
error!("Error evaluating node graph:\n{e}");
}
}
// Send each `FrontendMessage` to the JavaScript frontend // Send each `FrontendMessage` to the JavaScript frontend
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) { for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {
+3
View File
@@ -356,5 +356,8 @@ mod tests {
let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.); let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.);
assert_eq!(bezier2.project(DVec2::new(100., 0.)), 0.); assert_eq!(bezier2.project(DVec2::new(100., 0.)), 0.);
let bezier3 = Bezier::from_cubic_coordinates(-50.0, -50.0, -50.0, -50.0, 50.0, -50.0, 50.0, -50.0);
assert_eq!(DVec2::new(0., -50.), bezier3.evaluate(TValue::Parametric(bezier3.project(DVec2::new(0., -50.)))));
} }
} }
+31
View File
@@ -48,6 +48,14 @@ impl BezierHandles {
matches!(self, Self::Cubic { .. }) matches!(self, Self::Cubic { .. })
} }
pub fn is_finite(&self) -> bool {
match self {
BezierHandles::Linear => true,
BezierHandles::Quadratic { handle } => handle.is_finite(),
BezierHandles::Cubic { handle_start, handle_end } => handle_start.is_finite() && handle_end.is_finite(),
}
}
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment. /// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
pub fn start(&self) -> Option<DVec2> { pub fn start(&self) -> Option<DVec2> {
match *self { match *self {
@@ -64,6 +72,18 @@ impl BezierHandles {
} }
} }
pub fn move_start(&mut self, delta: DVec2) {
if let BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } = self {
*handle_start += delta
}
}
pub fn move_end(&mut self, delta: DVec2) {
if let BezierHandles::Cubic { handle_end, .. } = self {
*handle_end += delta
}
}
/// Returns a Bezier curve that results from applying the transformation function to each handle point in the Bezier. /// Returns a Bezier curve that results from applying the transformation function to each handle point in the Bezier.
#[must_use] #[must_use]
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Self { pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Self {
@@ -80,6 +100,17 @@ impl BezierHandles {
} }
} }
} }
#[must_use]
pub fn flipped(self) -> Self {
match self {
BezierHandles::Cubic { handle_start, handle_end } => Self::Cubic {
handle_start: handle_end,
handle_end: handle_start,
},
_ => self,
}
}
} }
#[cfg(feature = "dyn-any")] #[cfg(feature = "dyn-any")]
+6 -6
View File
@@ -347,7 +347,7 @@ impl Bezier {
/// - `distance` - The offset's distance from the curve. Positive values will offset the curve in the same direction as the endpoint normals, /// - `distance` - The offset's distance from the curve. Positive values will offset the curve in the same direction as the endpoint normals,
/// while negative values will offset in the opposite direction. /// while negative values will offset in the opposite direction.
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/libraries/bezier-rs#bezier/offset/solo" title="Offset Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/libraries/bezier-rs#bezier/offset/solo" title="Offset Demo"></iframe>
pub fn offset<ManipulatorGroupId: crate::Identifier>(&self, distance: f64) -> Subpath<ManipulatorGroupId> { pub fn offset<PointId: crate::Identifier>(&self, distance: f64) -> Subpath<PointId> {
if self.is_point() { if self.is_point() {
return Subpath::from_bezier(self); return Subpath::from_bezier(self);
} }
@@ -375,7 +375,7 @@ impl Bezier {
/// Version of the `offset` function which scales the offset such that the start of the offset is `start_distance` from the original curve, while the end of /// Version of the `offset` function which scales the offset such that the start of the offset is `start_distance` from the original curve, while the end of
/// of the offset is `end_distance` from the original curve. The curve transitions from `start_distance` to `end_distance` gradually, proportional to the /// of the offset is `end_distance` from the original curve. The curve transitions from `start_distance` to `end_distance` gradually, proportional to the
/// distance along the equation (`t`-value) of the curve. Similarly to the `offset` function, the returned result is an approximation. /// distance along the equation (`t`-value) of the curve. Similarly to the `offset` function, the returned result is an approximation.
pub fn graduated_offset<ManipulatorGroupId: crate::Identifier>(&self, start_distance: f64, end_distance: f64) -> Subpath<ManipulatorGroupId> { pub fn graduated_offset<PointId: crate::Identifier>(&self, start_distance: f64, end_distance: f64) -> Subpath<PointId> {
let reduced = self.reduce(None); let reduced = self.reduce(None);
let mut next_start_distance = start_distance; let mut next_start_distance = start_distance;
let distance_difference = end_distance - start_distance; let distance_difference = end_distance - start_distance;
@@ -414,7 +414,7 @@ impl Bezier {
/// Outline takes the following parameter: /// Outline takes the following parameter:
/// - `distance` - The outline's distance from the curve. /// - `distance` - The outline's distance from the curve.
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#bezier/outline/solo" title="Outline Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#bezier/outline/solo" title="Outline Demo"></iframe>
pub fn outline<ManipulatorGroupId: crate::Identifier>(&self, distance: f64, cap: Cap) -> Subpath<ManipulatorGroupId> { pub fn outline<PointId: crate::Identifier>(&self, distance: f64, cap: Cap) -> Subpath<PointId> {
let (pos_offset, neg_offset) = if self.is_point() { let (pos_offset, neg_offset) = if self.is_point() {
( (
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance)], false), Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance)], false),
@@ -434,13 +434,13 @@ impl Bezier {
/// Version of the `outline` function which draws the outline at the specified distances away from the curve. /// Version of the `outline` function which draws the outline at the specified distances away from the curve.
/// The outline begins `start_distance` away, and gradually move to being `end_distance` away. /// The outline begins `start_distance` away, and gradually move to being `end_distance` away.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/libraries/bezier-rs#bezier/graduated-outline/solo" title="Graduated Outline Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/libraries/bezier-rs#bezier/graduated-outline/solo" title="Graduated Outline Demo"></iframe>
pub fn graduated_outline<ManipulatorGroupId: crate::Identifier>(&self, start_distance: f64, end_distance: f64, cap: Cap) -> Subpath<ManipulatorGroupId> { pub fn graduated_outline<PointId: crate::Identifier>(&self, start_distance: f64, end_distance: f64, cap: Cap) -> Subpath<PointId> {
self.skewed_outline(start_distance, end_distance, end_distance, start_distance, cap) self.skewed_outline(start_distance, end_distance, end_distance, start_distance, cap)
} }
/// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve. /// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve.
/// <iframe frameBorder="0" width="100%" height="475px" src="https://graphite.rs/libraries/bezier-rs#bezier/skewed-outline/solo" title="Skewed Outline Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="475px" src="https://graphite.rs/libraries/bezier-rs#bezier/skewed-outline/solo" title="Skewed Outline Demo"></iframe>
pub fn skewed_outline<ManipulatorGroupId: crate::Identifier>(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath<ManipulatorGroupId> { pub fn skewed_outline<PointId: crate::Identifier>(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath<PointId> {
let (pos_offset, neg_offset) = if self.is_point() { let (pos_offset, neg_offset) = if self.is_point() {
( (
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance1)], false), Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance1)], false),
@@ -776,7 +776,7 @@ mod tests {
.all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE))) .all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE)))
} }
fn assert_valid_offset<ManipulatorGroupId: crate::Identifier>(bezier: &Bezier, offset: &Subpath<ManipulatorGroupId>, expected_distance: f64) { fn assert_valid_offset<PointId: crate::Identifier>(bezier: &Bezier, offset: &Subpath<PointId>, expected_distance: f64) {
// Verify that the offset is smooth // Verify that the offset is smooth
if offset.len() > 1 { if offset.len() > 1 {
offset.iter().take(offset.len() - 2).zip(offset.iter().skip(1)).for_each(|beziers_pair| { offset.iter().take(offset.len() - 2).zip(offset.iter().skip(1)).for_each(|beziers_pair| {
+1 -1
View File
@@ -37,6 +37,6 @@ pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool {
/// Compare Subpath by verifying that their bezier segments match. /// Compare Subpath by verifying that their bezier segments match.
/// In this way, matching quadratic segments where the handles are on opposite manipulator groups will be considered equal. /// In this way, matching quadratic segments where the handles are on opposite manipulator groups will be considered equal.
#[cfg(test)] #[cfg(test)]
pub fn compare_subpaths<ManipulatorGroupId: crate::Identifier>(subpath1: &Subpath<ManipulatorGroupId>, subpath2: &Subpath<ManipulatorGroupId>) -> bool { pub fn compare_subpaths<PointId: crate::Identifier>(subpath1: &Subpath<PointId>, subpath2: &Subpath<PointId>) -> bool {
subpath1.len() == subpath2.len() && subpath1.closed() == subpath2.closed() && subpath1.iter().eq(subpath2.iter()) subpath1.len() == subpath2.len() && subpath1.closed() == subpath2.closed() && subpath1.iter().eq(subpath2.iter())
} }
+1
View File
@@ -1,4 +1,5 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![allow(dead_code, unused_imports, unused_import_braces)]
pub(crate) mod compare; pub(crate) mod compare;
+18 -16
View File
@@ -5,11 +5,11 @@ use glam::DVec2;
use std::fmt::Write; use std::fmt::Write;
/// Functionality relating to core `Subpath` operations, such as constructors and `iter`. /// Functionality relating to core `Subpath` operations, such as constructors and `iter`.
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> { impl<PointId: crate::Identifier> Subpath<PointId> {
/// Create a new `Subpath` using a list of [ManipulatorGroup]s. /// Create a new `Subpath` using a list of [ManipulatorGroup]s.
/// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed. /// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed.
#[track_caller] #[track_caller]
pub fn new(manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>>, closed: bool) -> Self { pub fn new(manipulator_groups: Vec<ManipulatorGroup<PointId>>, closed: bool) -> Self {
assert!(!closed || manipulator_groups.len() > 1, "A closed Subpath must contain more than 1 ManipulatorGroup."); assert!(!closed || manipulator_groups.len() > 1, "A closed Subpath must contain more than 1 ManipulatorGroup.");
Self { manipulator_groups, closed } Self { manipulator_groups, closed }
} }
@@ -22,13 +22,13 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: bezier.start(), anchor: bezier.start(),
in_handle: None, in_handle: None,
out_handle: bezier.handle_start(), out_handle: bezier.handle_start(),
id: ManipulatorGroupId::new(), id: PointId::new(),
}, },
ManipulatorGroup { ManipulatorGroup {
anchor: bezier.end(), anchor: bezier.end(),
in_handle: bezier.handle_end(), in_handle: bezier.handle_end(),
out_handle: None, out_handle: None,
id: ManipulatorGroupId::new(), id: PointId::new(),
}, },
], ],
false, false,
@@ -48,17 +48,17 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: first.start(), anchor: first.start(),
in_handle: None, in_handle: None,
out_handle: first.handle_start(), out_handle: first.handle_start(),
id: ManipulatorGroupId::new(), id: PointId::new(),
}]; }];
let mut inner_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = beziers let mut inner_groups: Vec<ManipulatorGroup<PointId>> = beziers
.windows(2) .windows(2)
.map(|bezier_pair| ManipulatorGroup { .map(|bezier_pair| ManipulatorGroup {
anchor: bezier_pair[1].start(), anchor: bezier_pair[1].start(),
in_handle: bezier_pair[0].handle_end(), in_handle: bezier_pair[0].handle_end(),
out_handle: bezier_pair[1].handle_start(), out_handle: bezier_pair[1].handle_start(),
id: ManipulatorGroupId::new(), id: PointId::new(),
}) })
.collect::<Vec<ManipulatorGroup<ManipulatorGroupId>>>(); .collect::<Vec<ManipulatorGroup<PointId>>>();
manipulator_groups.append(&mut inner_groups); manipulator_groups.append(&mut inner_groups);
let last = beziers.last().unwrap(); let last = beziers.last().unwrap();
@@ -67,7 +67,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: last.end(), anchor: last.end(),
in_handle: last.handle_end(), in_handle: last.handle_end(),
out_handle: None, out_handle: None,
id: ManipulatorGroupId::new(), id: PointId::new(),
}); });
return Subpath::new(manipulator_groups, false); return Subpath::new(manipulator_groups, false);
} }
@@ -104,7 +104,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
} }
/// Returns an iterator of the [Bezier]s along the `Subpath`. /// Returns an iterator of the [Bezier]s along the `Subpath`.
pub fn iter(&self) -> SubpathIter<ManipulatorGroupId> { pub fn iter(&self) -> SubpathIter<PointId> {
SubpathIter { SubpathIter {
subpath: self, subpath: self,
index: 0, index: 0,
@@ -113,7 +113,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
} }
/// Returns an iterator of the [Bezier]s along the `Subpath` always considering it as a closed subpath. /// Returns an iterator of the [Bezier]s along the `Subpath` always considering it as a closed subpath.
pub fn iter_closed(&self) -> SubpathIter<ManipulatorGroupId> { pub fn iter_closed(&self) -> SubpathIter<PointId> {
SubpathIter { SubpathIter {
subpath: self, subpath: self,
index: 0, index: 0,
@@ -122,12 +122,12 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
} }
/// Returns a slice of the [ManipulatorGroup]s in the `Subpath`. /// Returns a slice of the [ManipulatorGroup]s in the `Subpath`.
pub fn manipulator_groups(&self) -> &[ManipulatorGroup<ManipulatorGroupId>] { pub fn manipulator_groups(&self) -> &[ManipulatorGroup<PointId>] {
&self.manipulator_groups &self.manipulator_groups
} }
/// Returns a mutable reference to the [ManipulatorGroup]s in the `Subpath`. /// Returns a mutable reference to the [ManipulatorGroup]s in the `Subpath`.
pub fn manipulator_groups_mut(&mut self) -> &mut Vec<ManipulatorGroup<ManipulatorGroupId>> { pub fn manipulator_groups_mut(&mut self) -> &mut Vec<ManipulatorGroup<PointId>> {
&mut self.manipulator_groups &mut self.manipulator_groups
} }
@@ -232,9 +232,13 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Constructs a rounded rectangle with `corner1` and `corner2` as the two corners and `corner_radii` as the radii of the corners: `[top_left, top_right, bottom_right, bottom_left]`. /// Constructs a rounded rectangle with `corner1` and `corner2` as the two corners and `corner_radii` as the radii of the corners: `[top_left, top_right, bottom_right, bottom_left]`.
pub fn new_rounded_rect(corner1: DVec2, corner2: DVec2, corner_radii: [f64; 4]) -> Self { pub fn new_rounded_rect(corner1: DVec2, corner2: DVec2, corner_radii: [f64; 4]) -> Self {
if corner_radii.iter().all(|radii| radii.abs() < f64::EPSILON * 100.) {
return Self::new_rect(corner1, corner2);
}
use std::f64::consts::{FRAC_1_SQRT_2, PI}; use std::f64::consts::{FRAC_1_SQRT_2, PI};
let new_arc = |center: DVec2, corner: DVec2, radius: f64| -> Vec<ManipulatorGroup<ManipulatorGroupId>> { let new_arc = |center: DVec2, corner: DVec2, radius: f64| -> Vec<ManipulatorGroup<PointId>> {
let point1 = center + DVec2::from_angle(-PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2; let point1 = center + DVec2::from_angle(-PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2;
let point2 = center + DVec2::from_angle(PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2; let point2 = center + DVec2::from_angle(PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2;
if radius == 0. { if radius == 0. {
@@ -245,10 +249,8 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014;
let handle_offset = radius * HANDLE_OFFSET_FACTOR; let handle_offset = radius * HANDLE_OFFSET_FACTOR;
vec![ vec![
ManipulatorGroup::new_anchor(point1),
ManipulatorGroup::new(point1, None, Some(point1 + handle_offset * (corner - point1).normalize())), ManipulatorGroup::new(point1, None, Some(point1 + handle_offset * (corner - point1).normalize())),
ManipulatorGroup::new(point2, Some(point2 + handle_offset * (corner - point2).normalize()), None), ManipulatorGroup::new(point2, Some(point2 + handle_offset * (corner - point2).normalize()), None),
ManipulatorGroup::new_anchor(point2),
] ]
}; };
Self::new( Self::new(
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::utils::{SubpathTValue, TValue, TValueType};
use glam::DVec2; use glam::DVec2;
/// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`. /// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`.
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> { impl<PointId: crate::Identifier> Subpath<PointId> {
/// Return a selection of equidistant points on the bezier curve. /// Return a selection of equidistant points on the bezier curve.
/// If no value is provided for `steps`, then the function will default `steps` to be 10. /// If no value is provided for `steps`, then the function will default `steps` to be 10.
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/lookup-table/solo" title="Lookup-Table Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/lookup-table/solo" title="Lookup-Table Demo"></iframe>
+14 -14
View File
@@ -3,7 +3,7 @@ use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
use crate::utils::f64_compare; use crate::utils::f64_compare;
use crate::{SubpathTValue, TValue}; use crate::{SubpathTValue, TValue};
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> { impl<PointId: crate::Identifier> Subpath<PointId> {
/// Get whether the subpath is closed. /// Get whether the subpath is closed.
pub fn closed(&self) -> bool { pub fn closed(&self) -> bool {
self.closed self.closed
@@ -14,40 +14,40 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
self.closed = new_closed; self.closed = new_closed;
} }
/// Access a [ManipulatorGroup] from a ManipulatorGroupId. /// Access a [ManipulatorGroup] from a PointId.
pub fn manipulator_from_id(&self, id: ManipulatorGroupId) -> Option<&ManipulatorGroup<ManipulatorGroupId>> { pub fn manipulator_from_id(&self, id: PointId) -> Option<&ManipulatorGroup<PointId>> {
self.manipulator_groups.iter().find(|manipulator_group| manipulator_group.id == id) self.manipulator_groups.iter().find(|manipulator_group| manipulator_group.id == id)
} }
/// Access a mutable [ManipulatorGroup] from a ManipulatorGroupId. /// Access a mutable [ManipulatorGroup] from a PointId.
pub fn manipulator_mut_from_id(&mut self, id: ManipulatorGroupId) -> Option<&mut ManipulatorGroup<ManipulatorGroupId>> { pub fn manipulator_mut_from_id(&mut self, id: PointId) -> Option<&mut ManipulatorGroup<PointId>> {
self.manipulator_groups.iter_mut().find(|manipulator_group| manipulator_group.id == id) self.manipulator_groups.iter_mut().find(|manipulator_group| manipulator_group.id == id)
} }
/// Access the index of a [ManipulatorGroup] from a ManipulatorGroupId. /// Access the index of a [ManipulatorGroup] from a PointId.
pub fn manipulator_index_from_id(&self, id: ManipulatorGroupId) -> Option<usize> { pub fn manipulator_index_from_id(&self, id: PointId) -> Option<usize> {
self.manipulator_groups.iter().position(|manipulator_group| manipulator_group.id == id) self.manipulator_groups.iter().position(|manipulator_group| manipulator_group.id == id)
} }
/// Insert a manipulator group at an index. /// Insert a manipulator group at an index.
pub fn insert_manipulator_group(&mut self, index: usize, group: ManipulatorGroup<ManipulatorGroupId>) { pub fn insert_manipulator_group(&mut self, index: usize, group: ManipulatorGroup<PointId>) {
assert!(group.is_finite(), "Inserting non finite manipulator group"); assert!(group.is_finite(), "Inserting non finite manipulator group");
self.manipulator_groups.insert(index, group) self.manipulator_groups.insert(index, group)
} }
/// Push a manipulator group to the end. /// Push a manipulator group to the end.
pub fn push_manipulator_group(&mut self, group: ManipulatorGroup<ManipulatorGroupId>) { pub fn push_manipulator_group(&mut self, group: ManipulatorGroup<PointId>) {
assert!(group.is_finite(), "Pushing non finite manipulator group"); assert!(group.is_finite(), "Pushing non finite manipulator group");
self.manipulator_groups.push(group) self.manipulator_groups.push(group)
} }
/// Get a mutable reference to the last manipulator /// Get a mutable reference to the last manipulator
pub fn last_manipulator_group_mut(&mut self) -> Option<&mut ManipulatorGroup<ManipulatorGroupId>> { pub fn last_manipulator_group_mut(&mut self) -> Option<&mut ManipulatorGroup<PointId>> {
self.manipulator_groups.last_mut() self.manipulator_groups.last_mut()
} }
/// Remove a manipulator group at an index. /// Remove a manipulator group at an index.
pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup<ManipulatorGroupId> { pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup<PointId> {
self.manipulator_groups.remove(index) self.manipulator_groups.remove(index)
} }
@@ -69,7 +69,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: first.end(), anchor: first.end(),
in_handle: first.handle_end(), in_handle: first.handle_end(),
out_handle: second.handle_start(), out_handle: second.handle_start(),
id: ManipulatorGroupId::new(), id: PointId::new(),
}; };
let number_of_groups = self.manipulator_groups.len() + 1; let number_of_groups = self.manipulator_groups.len() + 1;
self.manipulator_groups.insert((segment_index) + 1, new_group); self.manipulator_groups.insert((segment_index) + 1, new_group);
@@ -89,7 +89,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: bezier.start(), anchor: bezier.start(),
in_handle: None, in_handle: None,
out_handle: None, out_handle: None,
id: ManipulatorGroupId::new(), id: PointId::new(),
}]; }];
} }
let mut last_index = self.manipulator_groups.len() - 1; let mut last_index = self.manipulator_groups.len() - 1;
@@ -114,7 +114,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: bezier.end(), anchor: bezier.end(),
in_handle: bezier.handle_end(), in_handle: bezier.handle_end(),
out_handle: None, out_handle: None,
id: ManipulatorGroupId::new(), id: PointId::new(),
}); });
} }
} }
+11 -11
View File
@@ -15,25 +15,25 @@ use std::ops::{Index, IndexMut};
/// Structure used to represent a path composed of [Bezier] curves. /// Structure used to represent a path composed of [Bezier] curves.
#[derive(Clone, PartialEq, Hash)] #[derive(Clone, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Subpath<ManipulatorGroupId: crate::Identifier> { pub struct Subpath<PointId: crate::Identifier> {
manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>>, manipulator_groups: Vec<ManipulatorGroup<PointId>>,
pub closed: bool, pub closed: bool,
} }
#[cfg(feature = "dyn-any")] #[cfg(feature = "dyn-any")]
unsafe impl<ManipulatorGroupId: crate::Identifier> dyn_any::StaticType for Subpath<ManipulatorGroupId> { unsafe impl<PointId: crate::Identifier> dyn_any::StaticType for Subpath<PointId> {
type Static = Subpath<ManipulatorGroupId>; type Static = Subpath<PointId>;
} }
/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation. /// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation.
pub struct SubpathIter<'a, ManipulatorGroupId: crate::Identifier> { pub struct SubpathIter<'a, PointId: crate::Identifier> {
index: usize, index: usize,
subpath: &'a Subpath<ManipulatorGroupId>, subpath: &'a Subpath<PointId>,
is_always_closed: bool, is_always_closed: bool,
} }
impl<ManipulatorGroupId: crate::Identifier> Index<usize> for Subpath<ManipulatorGroupId> { impl<PointId: crate::Identifier> Index<usize> for Subpath<PointId> {
type Output = ManipulatorGroup<ManipulatorGroupId>; type Output = ManipulatorGroup<PointId>;
fn index(&self, index: usize) -> &Self::Output { fn index(&self, index: usize) -> &Self::Output {
assert!(index < self.len(), "Index out of bounds in trait Index of SubPath."); assert!(index < self.len(), "Index out of bounds in trait Index of SubPath.");
@@ -41,14 +41,14 @@ impl<ManipulatorGroupId: crate::Identifier> Index<usize> for Subpath<Manipulator
} }
} }
impl<ManipulatorGroupId: crate::Identifier> IndexMut<usize> for Subpath<ManipulatorGroupId> { impl<PointId: crate::Identifier> IndexMut<usize> for Subpath<PointId> {
fn index_mut(&mut self, index: usize) -> &mut Self::Output { fn index_mut(&mut self, index: usize) -> &mut Self::Output {
assert!(index < self.len(), "Index out of bounds in trait IndexMut of SubPath."); assert!(index < self.len(), "Index out of bounds in trait IndexMut of SubPath.");
&mut self.manipulator_groups[index] &mut self.manipulator_groups[index]
} }
} }
impl<ManipulatorGroupId: crate::Identifier> Iterator for SubpathIter<'_, ManipulatorGroupId> { impl<PointId: crate::Identifier> Iterator for SubpathIter<'_, PointId> {
type Item = Bezier; type Item = Bezier;
// Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points. // Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points.
@@ -73,7 +73,7 @@ impl<ManipulatorGroupId: crate::Identifier> Iterator for SubpathIter<'_, Manipul
} }
} }
impl<ManipulatorGroupId: crate::Identifier> Debug for Subpath<ManipulatorGroupId> { impl<PointId: crate::Identifier> Debug for Subpath<PointId> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result { fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish() f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish()
} }
+19 -7
View File
@@ -6,7 +6,7 @@ use crate::TValue;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use std::f64::consts::PI; use std::f64::consts::PI;
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> { impl<PointId: crate::Identifier> Subpath<PointId> {
/// Calculate the point on the subpath based on the parametric `t`-value provided. /// Calculate the point on the subpath based on the parametric `t`-value provided.
/// Expects `t` to be within the inclusive range `[0, 1]`. /// Expects `t` to be within the inclusive range `[0, 1]`.
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/evaluate/solo" title="Evaluate Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/evaluate/solo" title="Evaluate Demo"></iframe>
@@ -39,7 +39,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// This function expects the following: /// This function expects the following:
/// - other: a [Bezier] curve to check intersections against /// - other: a [Bezier] curve to check intersections against
/// - error: an optional f64 value to provide an error bound /// - error: an optional f64 value to provide an error bound
pub fn subpath_intersections(&self, other: &Subpath<ManipulatorGroupId>, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> { pub fn subpath_intersections(&self, other: &Subpath<PointId>, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
let mut intersection_t_values: Vec<(usize, f64)> = other.iter().flat_map(|bezier| self.intersections(&bezier, error, minimum_separation)).collect(); let mut intersection_t_values: Vec<(usize, f64)> = other.iter().flat_map(|bezier| self.intersections(&bezier, error, minimum_separation)).collect();
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
intersection_t_values intersection_t_values
@@ -302,6 +302,18 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
self.iter().map(|bezier| bezier.winding(target_point)).sum::<i32>() != 0 self.iter().map(|bezier| bezier.winding(target_point)).sum::<i32>() != 0
} }
/// Does a path contain a point? Based on the non zero winding. Automatically adds a linear segment if the subpath is not closed.
pub fn contains_point_autoclose(&self, target_point: DVec2) -> bool {
let mut winding = self.iter().map(|bezier| bezier.winding(target_point)).sum::<i32>();
if !self.closed {
if let [Some(first), Some(last)] = [self.manipulator_groups.first(), self.manipulator_groups.last()] {
winding += Bezier::from_linear_dvec2(first.anchor, last.anchor).winding(target_point);
}
}
winding != 0
}
/// Randomly places points across the filled surface of this subpath (which is assumed to be closed). /// Randomly places points across the filled surface of this subpath (which is assumed to be closed).
/// The `separation_disk_diameter` determines the minimum distance between all points from one another. /// The `separation_disk_diameter` determines the minimum distance between all points from one another.
/// Conceptually, this works by "throwing a dart" at the subpath's bounding box and keeping the dart only if: /// Conceptually, this works by "throwing a dart" at the subpath's bounding box and keeping the dart only if:
@@ -342,7 +354,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Alternatively, this can be interpreted as limiting the angle that the miter can form. /// Alternatively, this can be interpreted as limiting the angle that the miter can form.
/// When the limit is exceeded, no manipulator group will be returned. /// When the limit is exceeded, no manipulator group will be returned.
/// This value should be at least 1. If not, the default of 4 will be used. /// This value should be at least 1. If not, the default of 4 will be used.
pub(crate) fn miter_line_join(&self, other: &Subpath<ManipulatorGroupId>, miter_limit: Option<f64>) -> Option<ManipulatorGroup<ManipulatorGroupId>> { pub(crate) fn miter_line_join(&self, other: &Subpath<PointId>, miter_limit: Option<f64>) -> Option<ManipulatorGroup<PointId>> {
let miter_limit = match miter_limit { let miter_limit = match miter_limit {
Some(miter_limit) if miter_limit >= 1. => miter_limit, Some(miter_limit) if miter_limit >= 1. => miter_limit,
_ => 4., _ => 4.,
@@ -371,7 +383,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: intersection, anchor: intersection,
in_handle: None, in_handle: None,
out_handle: None, out_handle: None,
id: ManipulatorGroupId::new(), id: PointId::new(),
}); });
} }
} }
@@ -384,7 +396,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// - The `out_handle` for the last manipulator group of `self` /// - The `out_handle` for the last manipulator group of `self`
/// - The new manipulator group to be added /// - The new manipulator group to be added
/// - The `in_handle` for the first manipulator group of `other` /// - The `in_handle` for the first manipulator group of `other`
pub(crate) fn round_line_join(&self, other: &Subpath<ManipulatorGroupId>, center: DVec2) -> (DVec2, ManipulatorGroup<ManipulatorGroupId>, DVec2) { pub(crate) fn round_line_join(&self, other: &Subpath<PointId>, center: DVec2) -> (DVec2, ManipulatorGroup<PointId>, DVec2) {
let left = self.manipulator_groups[self.len() - 1].anchor; let left = self.manipulator_groups[self.len() - 1].anchor;
let right = other.manipulator_groups[0].anchor; let right = other.manipulator_groups[0].anchor;
@@ -410,7 +422,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// - The `out_handle` for the last manipulator group of `self` /// - The `out_handle` for the last manipulator group of `self`
/// - The new manipulator group to be added /// - The new manipulator group to be added
/// - The `in_handle` for the first manipulator group of `other` /// - The `in_handle` for the first manipulator group of `other`
pub(crate) fn round_cap(&self, other: &Subpath<ManipulatorGroupId>) -> (DVec2, ManipulatorGroup<ManipulatorGroupId>, DVec2) { pub(crate) fn round_cap(&self, other: &Subpath<PointId>) -> (DVec2, ManipulatorGroup<PointId>, DVec2) {
let left = self.manipulator_groups[self.len() - 1].anchor; let left = self.manipulator_groups[self.len() - 1].anchor;
let right = other.manipulator_groups[0].anchor; let right = other.manipulator_groups[0].anchor;
@@ -423,7 +435,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
} }
/// Returns the two manipulator groups that create a square cap between the end of `self` and the beginning of `other`. /// Returns the two manipulator groups that create a square cap between the end of `self` and the beginning of `other`.
pub(crate) fn square_cap(&self, other: &Subpath<ManipulatorGroupId>) -> [ManipulatorGroup<ManipulatorGroupId>; 2] { pub(crate) fn square_cap(&self, other: &Subpath<PointId>) -> [ManipulatorGroup<PointId>; 2] {
let left = self.manipulator_groups[self.len() - 1].anchor; let left = self.manipulator_groups[self.len() - 1].anchor;
let right = other.manipulator_groups[0].anchor; let right = other.manipulator_groups[0].anchor;
+11 -11
View File
@@ -26,15 +26,15 @@ impl Identifier for EmptyId {
/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath` /// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath`
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ManipulatorGroup<ManipulatorGroupId: crate::Identifier> { pub struct ManipulatorGroup<PointId: crate::Identifier> {
pub anchor: DVec2, pub anchor: DVec2,
pub in_handle: Option<DVec2>, pub in_handle: Option<DVec2>,
pub out_handle: Option<DVec2>, pub out_handle: Option<DVec2>,
pub id: ManipulatorGroupId, pub id: PointId,
} }
// TODO: Remove once we no longer need to hash floats in Graphite // TODO: Remove once we no longer need to hash floats in Graphite
impl<ManipulatorGroupId: crate::Identifier> Hash for ManipulatorGroup<ManipulatorGroupId> { impl<PointId: crate::Identifier> Hash for ManipulatorGroup<PointId> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) { fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.anchor.to_array().iter().for_each(|x| x.to_bits().hash(state)); self.anchor.to_array().iter().for_each(|x| x.to_bits().hash(state));
self.in_handle.is_some().hash(state); self.in_handle.is_some().hash(state);
@@ -50,11 +50,11 @@ impl<ManipulatorGroupId: crate::Identifier> Hash for ManipulatorGroup<Manipulato
} }
#[cfg(feature = "dyn-any")] #[cfg(feature = "dyn-any")]
unsafe impl<ManipulatorGroupId: crate::Identifier> dyn_any::StaticType for ManipulatorGroup<ManipulatorGroupId> { unsafe impl<PointId: crate::Identifier> dyn_any::StaticType for ManipulatorGroup<PointId> {
type Static = ManipulatorGroup<ManipulatorGroupId>; type Static = ManipulatorGroup<PointId>;
} }
impl<ManipulatorGroupId: crate::Identifier> Debug for ManipulatorGroup<ManipulatorGroupId> { impl<PointId: crate::Identifier> Debug for ManipulatorGroup<PointId> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result { fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("ManipulatorGroup") f.debug_struct("ManipulatorGroup")
.field("anchor", &self.anchor) .field("anchor", &self.anchor)
@@ -64,10 +64,10 @@ impl<ManipulatorGroupId: crate::Identifier> Debug for ManipulatorGroup<Manipulat
} }
} }
impl<ManipulatorGroupId: crate::Identifier> ManipulatorGroup<ManipulatorGroupId> { impl<PointId: crate::Identifier> ManipulatorGroup<PointId> {
/// Construct a new manipulator group from an anchor, in handle and out handle /// Construct a new manipulator group from an anchor, in handle and out handle
pub fn new(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>) -> Self { pub fn new(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>) -> Self {
let id = ManipulatorGroupId::new(); let id = PointId::new();
Self { anchor, in_handle, out_handle, id } Self { anchor, in_handle, out_handle, id }
} }
@@ -77,17 +77,17 @@ impl<ManipulatorGroupId: crate::Identifier> ManipulatorGroup<ManipulatorGroupId>
} }
/// Construct a new manipulator group from an anchor, in handle, out handle and an id /// Construct a new manipulator group from an anchor, in handle, out handle and an id
pub fn new_with_id(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>, id: ManipulatorGroupId) -> Self { pub fn new_with_id(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>, id: PointId) -> Self {
Self { anchor, in_handle, out_handle, id } Self { anchor, in_handle, out_handle, id }
} }
/// Construct a new manipulator point with just an anchor position and an id /// Construct a new manipulator point with just an anchor position and an id
pub fn new_anchor_with_id(anchor: DVec2, id: ManipulatorGroupId) -> Self { pub fn new_anchor_with_id(anchor: DVec2, id: PointId) -> Self {
Self::new_with_id(anchor, Some(anchor), Some(anchor), id) Self::new_with_id(anchor, Some(anchor), Some(anchor), id)
} }
/// Create a bezier curve that starts at the current manipulator group and finishes in the `end_group` manipulator group. /// Create a bezier curve that starts at the current manipulator group and finishes in the `end_group` manipulator group.
pub fn to_bezier(&self, end_group: &ManipulatorGroup<ManipulatorGroupId>) -> Bezier { pub fn to_bezier(&self, end_group: &ManipulatorGroup<PointId>) -> Bezier {
let start = self.anchor; let start = self.anchor;
let end = end_group.anchor; let end = end_group.anchor;
let out_handle = self.out_handle; let out_handle = self.out_handle;
+21 -25
View File
@@ -18,12 +18,12 @@ fn map_index_within_range(index: usize, t: f64, max_size: usize) -> (usize, f64)
} }
/// Functionality that transforms Subpaths, such as split, reduce, offset, etc. /// Functionality that transforms Subpaths, such as split, reduce, offset, etc.
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> { impl<PointId: crate::Identifier> Subpath<PointId> {
/// Returns either one or two Subpaths that result from splitting the original Subpath at the point corresponding to `t`. /// Returns either one or two Subpaths that result from splitting the original Subpath at the point corresponding to `t`.
/// If the original Subpath was closed, a single open Subpath will be returned. /// If the original Subpath was closed, a single open Subpath will be returned.
/// If the original Subpath was open, two open Subpaths will be returned. /// If the original Subpath was open, two open Subpaths will be returned.
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/split/solo" title="Split Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/split/solo" title="Split Demo"></iframe>
pub fn split(&self, t: SubpathTValue) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) { pub fn split(&self, t: SubpathTValue) -> (Subpath<PointId>, Option<Subpath<PointId>>) {
let (segment_index, t) = self.t_value_to_parametric(t); let (segment_index, t) = self.t_value_to_parametric(t);
let curve = self.get_segment(segment_index).unwrap(); let curve = self.get_segment(segment_index).unwrap();
@@ -48,7 +48,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: first_bezier.end(), anchor: first_bezier.end(),
in_handle: last_curve.handle_end(), in_handle: last_curve.handle_end(),
out_handle: None, out_handle: None,
id: ManipulatorGroupId::new(), id: PointId::new(),
}); });
} else { } else {
if !first_split.is_empty() { if !first_split.is_empty() {
@@ -68,7 +68,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: first_bezier.end(), anchor: first_bezier.end(),
in_handle: first_bezier.handle_end(), in_handle: first_bezier.handle_end(),
out_handle: None, out_handle: None,
id: ManipulatorGroupId::new(), id: PointId::new(),
}); });
} }
@@ -79,7 +79,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: second_bezier.start(), anchor: second_bezier.start(),
in_handle: None, in_handle: None,
out_handle: second_bezier.handle_start(), out_handle: second_bezier.handle_start(),
id: ManipulatorGroupId::new(), id: PointId::new(),
}, },
); );
} }
@@ -95,7 +95,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
} }
/// Returns [ManipulatorGroup]s with a reversed winding order. /// Returns [ManipulatorGroup]s with a reversed winding order.
fn reverse_manipulator_groups(manipulator_groups: &[ManipulatorGroup<ManipulatorGroupId>]) -> Vec<ManipulatorGroup<ManipulatorGroupId>> { fn reverse_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>]) -> Vec<ManipulatorGroup<PointId>> {
manipulator_groups manipulator_groups
.iter() .iter()
.rev() .rev()
@@ -103,14 +103,14 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: group.anchor, anchor: group.anchor,
in_handle: group.out_handle, in_handle: group.out_handle,
out_handle: group.in_handle, out_handle: group.in_handle,
id: ManipulatorGroupId::new(), id: PointId::new(),
}) })
.collect::<Vec<ManipulatorGroup<ManipulatorGroupId>>>() .collect::<Vec<ManipulatorGroup<PointId>>>()
} }
/// Returns a [Subpath] with a reversed winding order. /// Returns a [Subpath] with a reversed winding order.
/// Note that a reversed closed subpath will start on the same manipulator group and simply wind the other direction /// Note that a reversed closed subpath will start on the same manipulator group and simply wind the other direction
pub fn reverse(&self) -> Subpath<ManipulatorGroupId> { pub fn reverse(&self) -> Subpath<PointId> {
let mut reversed = Subpath::reverse_manipulator_groups(self.manipulator_groups()); let mut reversed = Subpath::reverse_manipulator_groups(self.manipulator_groups());
if self.closed { if self.closed {
reversed.rotate_right(1); reversed.rotate_right(1);
@@ -127,7 +127,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// That means, if the value of `t1` > `t2`, it will cross the break between endpoints from `t1` to `t = 1 = 0` to `t2`. /// That means, if the value of `t1` > `t2`, it will cross the break between endpoints from `t1` to `t = 1 = 0` to `t2`.
/// If a path winding in the reverse direction is desired, call `trim` on the `Subpath` returned from `Subpath::reverse`. /// If a path winding in the reverse direction is desired, call `trim` on the `Subpath` returned from `Subpath::reverse`.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/libraries/bezier-rs#subpath/trim/solo" title="Trim Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/libraries/bezier-rs#subpath/trim/solo" title="Trim Demo"></iframe>
pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath<ManipulatorGroupId> { pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath<PointId> {
// Return a clone of the Subpath if it is not long enough to be a valid Bezier // Return a clone of the Subpath if it is not long enough to be a valid Bezier
if self.manipulator_groups.is_empty() { if self.manipulator_groups.is_empty() {
return Subpath { return Subpath {
@@ -196,7 +196,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
cloned_manipulator_groups cloned_manipulator_groups
.drain(range_start..range_end.min(cloned_manipulator_groups.len())) .drain(range_start..range_end.min(cloned_manipulator_groups.len()))
.collect::<Vec<ManipulatorGroup<ManipulatorGroupId>>>() .collect::<Vec<ManipulatorGroup<PointId>>>()
}; };
// Adjust curve indices to match the cloned list // Adjust curve indices to match the cloned list
@@ -255,7 +255,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: front_split.start(), anchor: front_split.start(),
in_handle: None, in_handle: None,
out_handle: front_split.handle_start(), out_handle: front_split.handle_start(),
id: ManipulatorGroupId::new(), id: PointId::new(),
}; };
// Update the last two manipulator groups to match the back_split // Update the last two manipulator groups to match the back_split
@@ -264,7 +264,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
anchor: back_split.end(), anchor: back_split.end(),
in_handle: back_split.handle_end(), in_handle: back_split.handle_end(),
out_handle: None, out_handle: None,
id: ManipulatorGroupId::new(), id: PointId::new(),
}; };
Subpath { Subpath {
@@ -313,7 +313,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
// at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks. // at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
/// Helper function to clip overlap of two intersecting open Subpaths. Returns an optional, as intersections may not exist for certain arrangements and distances. /// Helper function to clip overlap of two intersecting open Subpaths. Returns an optional, as intersections may not exist for certain arrangements and distances.
/// Assumes that the Subpaths represents simple Bezier segments, and clips the Subpaths at the last intersection of the first Subpath, and first intersection of the last Subpath. /// Assumes that the Subpaths represents simple Bezier segments, and clips the Subpaths at the last intersection of the first Subpath, and first intersection of the last Subpath.
fn clip_simple_subpaths(subpath1: &Subpath<ManipulatorGroupId>, subpath2: &Subpath<ManipulatorGroupId>) -> Option<(Subpath<ManipulatorGroupId>, Subpath<ManipulatorGroupId>)> { fn clip_simple_subpaths(subpath1: &Subpath<PointId>, subpath2: &Subpath<PointId>) -> Option<(Subpath<PointId>, Subpath<PointId>)> {
// Split the first subpath at its last intersection // Split the first subpath at its last intersection
let intersections1 = subpath1.subpath_intersections(subpath2, None, None); let intersections1 = subpath1.subpath_intersections(subpath2, None, None);
if intersections1.is_empty() { if intersections1.is_empty() {
@@ -335,7 +335,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Returns a subpath that results from rotating this subpath around the origin by the given angle (in radians). /// Returns a subpath that results from rotating this subpath around the origin by the given angle (in radians).
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/libraries/bezier-rs#subpath/rotate/solo" title="Rotate Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/libraries/bezier-rs#subpath/rotate/solo" title="Rotate Demo"></iframe>
pub fn rotate(&self, angle: f64) -> Subpath<ManipulatorGroupId> { pub fn rotate(&self, angle: f64) -> Subpath<PointId> {
let mut rotated_subpath = self.clone(); let mut rotated_subpath = self.clone();
let affine_transform: DAffine2 = DAffine2::from_angle(angle); let affine_transform: DAffine2 = DAffine2::from_angle(angle);
@@ -345,7 +345,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
} }
/// Returns a subpath that results from rotating this subpath around the provided point by the given angle (in radians). /// Returns a subpath that results from rotating this subpath around the provided point by the given angle (in radians).
pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Subpath<ManipulatorGroupId> { pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Subpath<PointId> {
// Translate before and after the rotation to account for the pivot // Translate before and after the rotation to account for the pivot
let translate: DAffine2 = DAffine2::from_translation(pivot); let translate: DAffine2 = DAffine2::from_translation(pivot);
let rotate: DAffine2 = DAffine2::from_angle(angle); let rotate: DAffine2 = DAffine2::from_angle(angle);
@@ -359,7 +359,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away. /// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away.
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument. /// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/libraries/bezier-rs#subpath/offset/solo" title="Offset Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/libraries/bezier-rs#subpath/offset/solo" title="Offset Demo"></iframe>
pub fn offset(&self, distance: f64, join: Join) -> Subpath<ManipulatorGroupId> { pub fn offset(&self, distance: f64, join: Join) -> Subpath<PointId> {
assert!(self.len_segments() > 1, "Cannot offset an empty Subpath."); assert!(self.len_segments() > 1, "Cannot offset an empty Subpath.");
// An offset at a distance 0 from the curve is simply the same curve // An offset at a distance 0 from the curve is simply the same curve
@@ -368,11 +368,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
return self.clone(); return self.clone();
} }
let mut subpaths = self let mut subpaths = self.iter().filter(|bezier| !bezier.is_point()).map(|bezier| bezier.offset(distance)).collect::<Vec<Subpath<PointId>>>();
.iter()
.filter(|bezier| !bezier.is_point())
.map(|bezier| bezier.offset(distance))
.collect::<Vec<Subpath<ManipulatorGroupId>>>();
let mut drop_common_point = vec![true; self.len()]; let mut drop_common_point = vec![true; self.len()];
// Clip or join consecutive Subpaths // Clip or join consecutive Subpaths
@@ -489,8 +485,8 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
} }
/// Helper function to combine the two offsets that make up an outline. /// Helper function to combine the two offsets that make up an outline.
pub(crate) fn combine_outline(&self, other: &Subpath<ManipulatorGroupId>, cap: Cap) -> Subpath<ManipulatorGroupId> { pub(crate) fn combine_outline(&self, other: &Subpath<PointId>, cap: Cap) -> Subpath<PointId> {
let mut result_manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = vec![]; let mut result_manipulator_groups: Vec<ManipulatorGroup<PointId>> = vec![];
result_manipulator_groups.extend_from_slice(self.manipulator_groups()); result_manipulator_groups.extend_from_slice(self.manipulator_groups());
match cap { match cap {
Cap::Butt => { Cap::Butt => {
@@ -527,7 +523,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// - `distance` - The outline's distance from the curve. /// - `distance` - The outline's distance from the curve.
/// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments. /// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments.
/// <iframe frameBorder="0" width="100%" height="425px" src="https://graphite.rs/libraries/bezier-rs#subpath/outline/solo" title="Outline Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="425px" src="https://graphite.rs/libraries/bezier-rs#subpath/outline/solo" title="Outline Demo"></iframe>
pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) { pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath<PointId>, Option<Subpath<PointId>>) {
let is_point = self.is_point(); let is_point = self.is_point();
let (pos_offset, neg_offset) = if is_point { let (pos_offset, neg_offset) = if is_point {
let point = self.manipulator_groups[0].anchor; let point = self.manipulator_groups[0].anchor;
+1 -7
View File
@@ -266,13 +266,7 @@ pub fn scale_point_from_origin(point: DVec2, origin: DVec2, should_flip_directio
/// Computes the necessary details to form a circular join from `left` to `right`, along a circle around `center`. /// Computes the necessary details to form a circular join from `left` to `right`, along a circle around `center`.
/// By default, the angle is assumed to be 180 degrees. /// By default, the angle is assumed to be 180 degrees.
pub fn compute_circular_subpath_details<ManipulatorGroupId: crate::Identifier>( pub fn compute_circular_subpath_details<PointId: crate::Identifier>(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option<f64>) -> (DVec2, ManipulatorGroup<PointId>, DVec2) {
left: DVec2,
arc_point: DVec2,
right: DVec2,
center: DVec2,
angle: Option<f64>,
) -> (DVec2, ManipulatorGroup<ManipulatorGroupId>, DVec2) {
let center_to_arc_point = arc_point - center; let center_to_arc_point = arc_point - center;
// Based on https://pomax.github.io/bezierinfo/#circles_cubic // Based on https://pomax.github.io/bezierinfo/#circles_cubic
-2
View File
@@ -1,12 +1,10 @@
#![doc(html_root_url = "http://docs.rs/const-default/1.0.0")] #![doc(html_root_url = "http://docs.rs/const-default/1.0.0")]
#![cfg_attr(feature = "unstable-docs", feature(doc_cfg))]
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::missing_safety_doc)] #![allow(clippy::missing_safety_doc)]
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
extern crate alloc; extern crate alloc;
#[cfg(feature = "derive")] #[cfg(feature = "derive")]
#[cfg_attr(feature = "unstable-docs", doc(cfg(feature = "derive")))]
pub use dyn_any_derive::DynAny; pub use dyn_any_derive::DynAny;
/// Implement this trait for your `dyn Trait` types for all `T: Trait` /// Implement this trait for your `dyn Trait` types for all `T: Trait`
+4 -4
View File
@@ -12,15 +12,15 @@ homepage = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/raw
repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/raw-rs" repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/raw-rs"
documentation = "https://docs.rs/raw-rs" documentation = "https://docs.rs/raw-rs"
[features]
raw-rs-tests = []
[dependencies] [dependencies]
bitstream-io = "2.3.0" bitstream-io = "2.3.0"
num_enum = "0.7.2" num_enum = "0.7.2"
thiserror = { workspace = true } thiserror = { workspace = true }
tag-derive = { path = "tag-derive" } tag-derive = { path = "tag-derive" }
libraw-rs = { version = "0.0.4", optional = true }
[dev-dependencies] [dev-dependencies]
libraw-rs = "0.0.4"
downloader = "0.2.7" downloader = "0.2.7"
[features]
raw-rs-tests = ["libraw-rs"]
+7 -4
View File
@@ -7,13 +7,13 @@ use std::path::Path;
use raw_rs::RawImage; use raw_rs::RawImage;
use downloader::{Download, Downloader}; use downloader::{Download, Downloader};
use libraw::Processor;
const TEST_FILES: [&str; 3] = ["ILCE-7M3-ARW2.3.5-blossoms.arw", "ILCE-7RM4-ARW2.3.5-kestrel.arw", "ILCE-6000-ARW2.3.1-windsock.arw"]; const TEST_FILES: [&str; 3] = ["ILCE-7M3-ARW2.3.5-blossoms.arw", "ILCE-7RM4-ARW2.3.5-kestrel.arw", "ILCE-6000-ARW2.3.1-windsock.arw"];
const BASE_URL: &str = "https://static.graphite.rs/test-data/libraries/raw-rs/"; const BASE_URL: &str = "https://static.graphite.rs/test-data/libraries/raw-rs/";
const BASE_PATH: &str = "./tests/images"; const BASE_PATH: &str = "./tests/images";
#[cfg_attr(feature = "raw-rs-tests", test)] #[test]
#[cfg(feature = "raw-rs-tests")]
fn test_images_match_with_libraw() { fn test_images_match_with_libraw() {
download_images(); download_images();
@@ -54,6 +54,7 @@ fn test_images_match_with_libraw() {
} }
} }
#[cfg(feature = "raw-rs-tests")]
fn download_images() { fn download_images() {
let mut path = Path::new(BASE_PATH).to_owned(); let mut path = Path::new(BASE_PATH).to_owned();
let mut downloads: Vec<Download> = Vec::new(); let mut downloads: Vec<Download> = Vec::new();
@@ -74,8 +75,9 @@ fn download_images() {
} }
} }
#[cfg(feature = "raw-rs-tests")]
fn test_raw_data(content: &[u8]) -> Result<RawImage, String> { fn test_raw_data(content: &[u8]) -> Result<RawImage, String> {
let processor = Processor::new(); let processor = libraw::Processor::new();
let libraw_raw_image = processor.decode(content).unwrap(); let libraw_raw_image = processor.decode(content).unwrap();
let mut content = Cursor::new(content); let mut content = Cursor::new(content);
@@ -147,8 +149,9 @@ fn test_raw_data(content: &[u8]) -> Result<RawImage, String> {
Ok(raw_image) Ok(raw_image)
} }
#[cfg(feature = "raw-rs-tests")]
fn test_final_image(content: &[u8], raw_image: RawImage) -> Result<(), String> { fn test_final_image(content: &[u8], raw_image: RawImage) -> Result<(), String> {
let processor = Processor::new(); let processor = libraw::Processor::new();
let libraw_image = processor.process_8bit(content).unwrap(); let libraw_image = processor.process_8bit(content).unwrap();
let image = raw_rs::process_8bit(raw_image); let image = raw_rs::process_8bit(raw_image);
+4
View File
@@ -65,3 +65,7 @@ rand = { workspace = true, default-features = false, features = ["std_rng"] }
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["rt", "macros"] } tokio = { workspace = true, features = ["rt", "macros"] }
[lints.rust]
# the spirv target is not in the list of common cfgs so must be added manually
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_arch, values("spirv"))'] }
@@ -334,8 +334,14 @@ impl GraphicElementRendered for VectorData {
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight); let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight);
click_targets.extend(self.region_bezier_paths().map(|(_, subpath)| ClickTarget { stroke_width, subpath })); let filled = self.style.fill() != &crate::vector::style::Fill::None;
click_targets.extend(self.stroke_bezier_paths().map(|subpath| ClickTarget { stroke_width, subpath })); let fill = |mut subpath: bezier_rs::Subpath<_>| {
if filled {
subpath.set_closed(true);
}
subpath
};
click_targets.extend(self.stroke_bezier_paths().map(fill).map(|subpath| ClickTarget { stroke_width, subpath }));
} }
fn to_usvg_node(&self) -> usvg::Node { fn to_usvg_node(&self) -> usvg::Node {
@@ -475,7 +481,7 @@ impl GraphicElementRendered for crate::ArtboardGroup {
} }
fn contains_artboard(&self) -> bool { fn contains_artboard(&self) -> bool {
self.artboards.len() > 0 !self.artboards.is_empty()
} }
} }
+18 -9
View File
@@ -581,8 +581,9 @@ fn vibrance_node(color: Color, vibrance: f64) -> Color {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
pub enum RedGreenBlue { pub enum RedGreenBlue {
#[default]
Red, Red,
Green, Green,
Blue, Blue,
@@ -600,8 +601,9 @@ impl core::fmt::Display for RedGreenBlue {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
pub enum RedGreenBlueAlpha { pub enum RedGreenBlueAlpha {
#[default]
Red, Red,
Green, Green,
Blue, Blue,
@@ -621,8 +623,9 @@ impl core::fmt::Display for RedGreenBlueAlpha {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
pub enum NoiseType { pub enum NoiseType {
#[default]
Perlin, Perlin,
OpenSimplex2, OpenSimplex2,
OpenSimplex2S, OpenSimplex2S,
@@ -662,8 +665,9 @@ impl NoiseType {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
pub enum FractalType { pub enum FractalType {
#[default]
None, None,
FBm, FBm,
Ridged, Ridged,
@@ -700,8 +704,9 @@ impl FractalType {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
pub enum CellularDistanceFunction { pub enum CellularDistanceFunction {
#[default]
Euclidean, Euclidean,
EuclideanSq, EuclideanSq,
Manhattan, Manhattan,
@@ -732,9 +737,10 @@ impl CellularDistanceFunction {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
pub enum CellularReturnType { pub enum CellularReturnType {
CellValue, CellValue,
#[default]
Nearest, Nearest,
NextNearest, NextNearest,
Average, Average,
@@ -773,8 +779,9 @@ impl CellularReturnType {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
pub enum DomainWarpType { pub enum DomainWarpType {
#[default]
None, None,
OpenSimplex2, OpenSimplex2,
OpenSimplex2Reduced, OpenSimplex2Reduced,
@@ -867,8 +874,9 @@ fn channel_mixer_node(
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
pub enum RelativeAbsolute { pub enum RelativeAbsolute {
#[default]
Relative, Relative,
Absolute, Absolute,
} }
@@ -885,8 +893,9 @@ impl core::fmt::Display for RelativeAbsolute {
#[repr(C)] #[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
pub enum SelectiveColorChoice { pub enum SelectiveColorChoice {
#[default]
Reds, Reds,
Yellows, Yellows,
Greens, Greens,
+1 -1
View File
@@ -774,7 +774,7 @@ impl Color {
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn to_rgba8_srgb(&self) -> [u8; 4] { pub fn to_rgba8_srgb(&self) -> [u8; 4] {
let gamma = self.to_gamma_srgb(); let gamma = self.to_gamma_srgb().to_gamma_srgb();
[(gamma.red * 255.) as u8, (gamma.green * 255.) as u8, (gamma.blue * 255.) as u8, (gamma.alpha * 255.) as u8] [(gamma.red * 255.) as u8, (gamma.green * 255.) as u8, (gamma.blue * 255.) as u8, (gamma.alpha * 255.) as u8]
} }
+1 -1
View File
@@ -17,5 +17,5 @@ pub struct TextGeneratorNode<Text, FontName, Size> {
#[node_fn(TextGeneratorNode)] #[node_fn(TextGeneratorNode)]
fn generate_text<'a: 'input, T>(editor: EditorApi<'a, T>, text: String, font_name: Font, font_size: f64) -> crate::vector::VectorData { fn generate_text<'a: 'input, T>(editor: EditorApi<'a, T>, text: String, font_name: Font, font_size: f64) -> crate::vector::VectorData {
let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data)); let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data));
crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, font_size, None)) crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, font_size, None), false)
} }
+5 -1
View File
@@ -15,7 +15,11 @@ impl Font {
Self { font_family, font_style } Self { font_family, font_style }
} }
} }
impl Default for Font {
fn default() -> Self {
Self::new(crate::consts::DEFAULT_FONT_FAMILY.into(), crate::consts::DEFAULT_FONT_STYLE.into())
}
}
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`) /// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
pub struct FontCache { pub struct FontCache {
+7 -7
View File
@@ -1,4 +1,4 @@
use crate::uuid::ManipulatorGroupId; use crate::vector::PointId;
use bezier_rs::{ManipulatorGroup, Subpath}; use bezier_rs::{ManipulatorGroup, Subpath};
@@ -7,13 +7,13 @@ use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
use rustybuzz::{GlyphBuffer, UnicodeBuffer}; use rustybuzz::{GlyphBuffer, UnicodeBuffer};
struct Builder { struct Builder {
current_subpath: Subpath<ManipulatorGroupId>, current_subpath: Subpath<PointId>,
other_subpaths: Vec<Subpath<ManipulatorGroupId>>, other_subpaths: Vec<Subpath<PointId>>,
pos: DVec2, pos: DVec2,
offset: DVec2, offset: DVec2,
ascender: f64, ascender: f64,
scale: f64, scale: f64,
id: ManipulatorGroupId, id: PointId,
} }
impl Builder { impl Builder {
@@ -37,7 +37,7 @@ impl OutlineBuilder for Builder {
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) { fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)]; let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle); self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle);
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(anchor, self.id.next_id())); self.current_subpath.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, None, None, self.id.next_id()));
} }
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) { fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
@@ -80,7 +80,7 @@ fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_
false false
} }
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Vec<Subpath<ManipulatorGroupId>> { pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Vec<Subpath<PointId>> {
let buzz_face = match buzz_face { let buzz_face = match buzz_face {
Some(face) => face, Some(face) => face,
// Show blank layer if font has not loaded // Show blank layer if font has not loaded
@@ -96,7 +96,7 @@ pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, li
offset: DVec2::ZERO, offset: DVec2::ZERO,
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * font_size / scale, ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * font_size / scale,
scale, scale,
id: ManipulatorGroupId::ZERO, id: PointId::ZERO,
}; };
for line in str.split('\n') { for line in str.split('\n') {
+4 -9
View File
@@ -152,7 +152,7 @@ pub struct TransformNode<TransformTarget, Translation, Rotation, Scale, Shear, P
pub(crate) rotate: Rotation, pub(crate) rotate: Rotation,
pub(crate) scale: Scale, pub(crate) scale: Scale,
pub(crate) shear: Shear, pub(crate) shear: Shear,
pub(crate) pivot: Pivot, pub(crate) _pivot: Pivot,
} }
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)] #[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)]
@@ -165,7 +165,7 @@ pub enum RenderQuality {
Scale(f32), Scale(f32),
/// Flip a coin to decide if the render should be available with the current quality or done at full quality /// Flip a coin to decide if the render should be available with the current quality or done at full quality
/// This should be used to gradually update the render quality of a cached node /// This should be used to gradually update the render quality of a cached node
Probabilty(f32), Probability(f32),
/// Render at full quality /// Render at full quality
Full, Full,
} }
@@ -249,23 +249,18 @@ pub(crate) async fn transform_vector_data<Fut: Future>(
rotate: f64, rotate: f64,
scale: DVec2, scale: DVec2,
shear: DVec2, shear: DVec2,
pivot: DVec2, _pivot: DVec2,
) -> Fut::Output ) -> Fut::Output
where where
Fut::Output: TransformMut, Fut::Output: TransformMut,
{ {
// TODO: This is hack and might break for Vector data because the pivot may be incorrect let modification = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
let transform = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
if !footprint.ignore_modifications { if !footprint.ignore_modifications {
let pivot_transform = DAffine2::from_translation(pivot);
let modification = pivot_transform * transform * pivot_transform.inverse();
*footprint.transform_mut() = footprint.transform() * modification; *footprint.transform_mut() = footprint.transform() * modification;
} }
let mut data = self.transform_target.eval(footprint).await; let mut data = self.transform_target.eval(footprint).await;
let pivot_transform = DAffine2::from_translation(data.local_pivot(pivot));
let modification = pivot_transform * transform * pivot_transform.inverse();
let data_transform = data.transform_mut(); let data_transform = data.transform_mut();
*data_transform = modification * (*data_transform); *data_transform = modification * (*data_transform);
-32
View File
@@ -1,5 +1,3 @@
use dyn_any::{DynAny, StaticType};
#[derive(Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct Uuid( pub struct Uuid(
#[serde(with = "u64_string")] #[serde(with = "u64_string")]
@@ -69,33 +67,3 @@ mod uuid_generation {
} }
pub use uuid_generation::*; pub use uuid_generation::*;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ManipulatorGroupId(u64);
impl bezier_rs::Identifier for ManipulatorGroupId {
fn new() -> Self {
Self(generate_uuid())
}
}
impl ManipulatorGroupId {
pub const ZERO: ManipulatorGroupId = ManipulatorGroupId(0);
pub fn next_id(&mut self) -> Self {
let old = self.0;
self.0 += 1;
Self(old)
}
pub(crate) fn inner(self) -> u64 {
self.0
}
}
impl From<crate::vector::PointId> for ManipulatorGroupId {
fn from(value: crate::vector::PointId) -> Self {
Self(value.inner())
}
}
+23 -9
View File
@@ -1,5 +1,5 @@
use crate::uuid::ManipulatorGroupId; use super::HandleId;
use crate::vector::VectorData; use crate::vector::{PointId, VectorData};
use crate::Node; use crate::Node;
use bezier_rs::Subpath; use bezier_rs::Subpath;
@@ -28,7 +28,14 @@ fn ellipse_generator(_input: (), radius_x: f64, radius_y: f64) -> VectorData {
let radius = DVec2::new(radius_x, radius_y); let radius = DVec2::new(radius_x, radius_y);
let corner1 = -radius; let corner1 = -radius;
let corner2 = radius; let corner2 = radius;
super::VectorData::from_subpath(Subpath::new_ellipse(corner1, corner2)) let mut ellipse = super::VectorData::from_subpath(Subpath::new_ellipse(corner1, corner2));
let len = ellipse.segment_domain.ids().len();
for i in 0..len {
ellipse
.colinear_manipulators
.push([HandleId::end(ellipse.segment_domain.ids()[i]), HandleId::primary(ellipse.segment_domain.ids()[(i + 1) % len])]);
}
ellipse
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -46,7 +53,7 @@ trait CornerRadius {
impl CornerRadius for f64 { impl CornerRadius for f64 {
fn generate(self, size: DVec2, clamped: bool) -> super::VectorData { fn generate(self, size: DVec2, clamped: bool) -> super::VectorData {
let clamped_radius = if clamped { self.clamp(0., size.x.min(size.y).max(0.) / 2.) } else { self }; let clamped_radius = if clamped { self.clamp(0., size.x.min(size.y).max(0.) / 2.) } else { self };
super::VectorData::from_subpaths(vec![Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4])]) super::VectorData::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4]))
} }
} }
impl CornerRadius for [f64; 4] { impl CornerRadius for [f64; 4] {
@@ -66,7 +73,7 @@ impl CornerRadius for [f64; 4] {
} else { } else {
self self
}; };
super::VectorData::from_subpaths(vec![Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius)]) super::VectorData::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius))
} }
} }
@@ -122,7 +129,11 @@ pub struct SplineGenerator<Positions> {
#[node_macro::node_fn(SplineGenerator)] #[node_macro::node_fn(SplineGenerator)]
fn spline_generator(_input: (), positions: Vec<DVec2>) -> VectorData { fn spline_generator(_input: (), positions: Vec<DVec2>) -> VectorData {
super::VectorData::from_subpath(Subpath::new_cubic_spline(positions)) let mut spline = super::VectorData::from_subpath(Subpath::new_cubic_spline(positions));
for pair in spline.segment_domain.ids().windows(2) {
spline.colinear_manipulators.push([HandleId::end(pair[0]), HandleId::primary(pair[1])]);
}
spline
} }
// TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node // TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node
@@ -132,9 +143,12 @@ pub struct PathGenerator<ColinearManipulators> {
} }
#[node_macro::node_fn(PathGenerator)] #[node_macro::node_fn(PathGenerator)]
fn generate_path(path_data: Vec<Subpath<ManipulatorGroupId>>, colinear_manipulators: Vec<ManipulatorGroupId>) -> super::VectorData { fn generate_path(path_data: Vec<Subpath<PointId>>, colinear_manipulators: Vec<PointId>) -> super::VectorData {
let mut vector_data = super::VectorData::from_subpaths(path_data); let mut vector_data = super::VectorData::from_subpaths(path_data, false);
vector_data.colinear_manipulators = colinear_manipulators; vector_data.colinear_manipulators = colinear_manipulators
.iter()
.filter_map(|&point| super::ManipulatorPointId::Anchor(point).get_handle_pair(&vector_data))
.collect();
vector_data vector_data
} }
+6 -3
View File
@@ -371,16 +371,18 @@ impl From<Fill> for FillChoice {
/// Enum describing the type of [Fill]. /// Enum describing the type of [Fill].
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)] #[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
pub enum FillType { pub enum FillType {
#[default]
Solid, Solid,
Gradient, Gradient,
} }
/// The stroke (outline) style of an SVG element. /// The stroke (outline) style of an SVG element.
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
pub enum LineCap { pub enum LineCap {
#[default]
Butt, Butt,
Round, Round,
Square, Square,
@@ -397,8 +399,9 @@ impl Display for LineCap {
} }
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
pub enum LineJoin { pub enum LineJoin {
#[default]
Miter, Miter,
Bevel, Bevel,
Round, Round,
+243 -62
View File
@@ -1,13 +1,15 @@
mod attributes; mod attributes;
mod modification;
pub use attributes::*;
pub use modification::*;
use super::style::{PathStyle, Stroke}; use super::style::{PathStyle, Stroke};
use crate::Color; use crate::{AlphaBlending, Color};
use crate::{uuid::ManipulatorGroupId, AlphaBlending};
pub use attributes::*;
use bezier_rs::ManipulatorGroup; use bezier_rs::ManipulatorGroup;
use dyn_any::{DynAny, StaticType}; use dyn_any::{DynAny, StaticType};
use core::borrow::Borrow;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
/// [VectorData] is passed between nodes. /// [VectorData] is passed between nodes.
@@ -20,7 +22,7 @@ pub struct VectorData {
pub alpha_blending: AlphaBlending, pub alpha_blending: AlphaBlending,
/// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 180° angles from one another). /// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 180° angles from one another).
/// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have both `subpath` and `colinear_manipulators` inputs"` to find it). /// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have both `subpath` and `colinear_manipulators` inputs"` to find it).
pub colinear_manipulators: Vec<ManipulatorGroupId>, pub colinear_manipulators: Vec<[HandleId; 2]>,
pub point_domain: PointDomain, pub point_domain: PointDomain,
pub segment_domain: SegmentDomain, pub segment_domain: SegmentDomain,
@@ -54,15 +56,15 @@ impl VectorData {
} }
/// Construct some new vector data from a single subpath with an identity transform and black fill. /// Construct some new vector data from a single subpath with an identity transform and black fill.
pub fn from_subpath(subpath: bezier_rs::Subpath<ManipulatorGroupId>) -> Self { pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
Self::from_subpaths([subpath]) Self::from_subpaths([subpath], false)
} }
/// Push a subpath to the vector data /// Push a subpath to the vector data
pub fn append_subpath<Id: bezier_rs::Identifier + Into<PointId> + Copy>(&mut self, subpath: bezier_rs::Subpath<Id>) { pub fn append_subpath(&mut self, subpath: impl Borrow<bezier_rs::Subpath<PointId>>, preserve_id: bool) {
for point in subpath.manipulator_groups() { let subpath: &bezier_rs::Subpath<PointId> = subpath.borrow();
self.point_domain.push(point.id.into(), point.anchor); let stroke_id = StrokeId::ZERO;
} let mut point_id = self.point_domain.next_id();
let handles = |a: &ManipulatorGroup<_>, b: &ManipulatorGroup<_>| match (a.out_handle, b.in_handle) { let handles = |a: &ManipulatorGroup<_>, b: &ManipulatorGroup<_>| match (a.out_handle, b.in_handle) {
(None, None) => bezier_rs::BezierHandles::Linear, (None, None) => bezier_rs::BezierHandles::Linear,
@@ -70,33 +72,57 @@ impl VectorData {
(Some(handle_start), Some(handle_end)) => bezier_rs::BezierHandles::Cubic { handle_start, handle_end }, (Some(handle_start), Some(handle_end)) => bezier_rs::BezierHandles::Cubic { handle_start, handle_end },
}; };
let [mut first_seg, mut last_seg] = [None, None]; let [mut first_seg, mut last_seg] = [None, None];
let mut segment_id = self.segment_domain.next_id();
let mut last_point = None;
let mut first_point = None;
for pair in subpath.manipulator_groups().windows(2) { for pair in subpath.manipulator_groups().windows(2) {
let id = SegmentId::generate(); let start = last_point.unwrap_or_else(|| {
let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) {
pair[0].id
} else {
point_id.next_id()
};
self.point_domain.push(id, pair[0].anchor);
id
});
first_point = Some(first_point.unwrap_or(start));
let end = if preserve_id && !self.point_domain.ids().contains(&pair[1].id) {
pair[1].id
} else {
point_id.next_id()
};
self.point_domain.push(end, pair[1].anchor);
let id = segment_id.next_id();
first_seg = Some(first_seg.unwrap_or(id)); first_seg = Some(first_seg.unwrap_or(id));
last_seg = Some(id); last_seg = Some(id);
self.segment_domain.push(id, pair[0].id.into(), pair[1].id.into(), handles(&pair[0], &pair[1]), StrokeId::generate()); self.segment_domain.push(id, start, end, handles(&pair[0], &pair[1]), stroke_id);
last_point = Some(end);
} }
let fill_id = FillId::ZERO;
if subpath.closed() { if subpath.closed() {
if let (Some(last), Some(first)) = (subpath.manipulator_groups().last(), subpath.manipulator_groups().first()) { if let (Some(last), Some(first), Some(first_id), Some(last_id)) = (subpath.manipulator_groups().last(), subpath.manipulator_groups().first(), first_point, last_point) {
let id = SegmentId::generate(); let id = segment_id.next_id();
first_seg = Some(first_seg.unwrap_or(id)); first_seg = Some(first_seg.unwrap_or(id));
last_seg = Some(id); last_seg = Some(id);
self.segment_domain.push(id, last.id.into(), first.id.into(), handles(last, first), StrokeId::generate()); self.segment_domain.push(id, last_id, first_id, handles(last, first), stroke_id);
} }
if let [Some(first_seg), Some(last_seg)] = [first_seg, last_seg] { if let [Some(first_seg), Some(last_seg)] = [first_seg, last_seg] {
self.region_domain.push(RegionId::generate(), first_seg..=last_seg, FillId::generate()); self.region_domain.push(self.region_domain.next_id(), first_seg..=last_seg, fill_id);
} }
} }
} }
/// Construct some new vector data from subpaths with an identity transform and black fill. /// Construct some new vector data from subpaths with an identity transform and black fill.
pub fn from_subpaths(subpaths: impl IntoIterator<Item = bezier_rs::Subpath<ManipulatorGroupId>>) -> Self { pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
let mut vector_data = Self::empty(); let mut vector_data = Self::empty();
for subpath in subpaths.into_iter() { for subpath in subpaths.into_iter() {
vector_data.append_subpath(subpath); vector_data.append_subpath(subpath, preserve_id);
} }
vector_data vector_data
@@ -142,6 +168,33 @@ impl VectorData {
pub fn local_pivot(&self, normalized_pivot: DVec2) -> DVec2 { pub fn local_pivot(&self, normalized_pivot: DVec2) -> DVec2 {
self.transform.transform_point2(self.layerspace_pivot(normalized_pivot)) self.transform.transform_point2(self.layerspace_pivot(normalized_pivot))
} }
/// Points connected to a single segment
pub fn single_connected_points(&self) -> impl Iterator<Item = PointId> + '_ {
self.point_domain.ids().iter().copied().filter(|&point| self.segment_domain.connected_count(point) == 1)
}
/// Computes if all the connected handles are colinear for an anchor, or if that handle is colinear for a handle.
pub fn colinear(&self, point: ManipulatorPointId) -> bool {
let has_handle = |target| self.colinear_manipulators.iter().flatten().any(|&handle| handle == target);
match point {
ManipulatorPointId::Anchor(id) => {
self.segment_domain.start_connected(id).all(|segment| has_handle(HandleId::primary(segment))) && self.segment_domain.end_connected(id).all(|segment| has_handle(HandleId::end(segment)))
}
ManipulatorPointId::PrimaryHandle(segment) => has_handle(HandleId::primary(segment)),
ManipulatorPointId::EndHandle(segment) => has_handle(HandleId::end(segment)),
}
}
pub fn other_colinear_handle(&self, handle: HandleId) -> Option<HandleId> {
let pair = self.colinear_manipulators.iter().find(|pair| pair.iter().any(|&val| val == handle))?;
let other = pair.iter().copied().find(|&val| val != handle)?;
if handle.to_manipulator_point().get_anchor(self) == other.to_manipulator_point().get_anchor(self) {
Some(other)
} else {
None
}
}
} }
impl Default for VectorData { impl Default for VectorData {
@@ -150,64 +203,192 @@ impl Default for VectorData {
} }
} }
/// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curviture).
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ManipulatorPointId { pub enum ManipulatorPointId {
pub group: ManipulatorGroupId, /// A control anchor - the start or end point of a bézier.
pub manipulator_type: SelectedType, Anchor(PointId),
/// The handle for a bézier - the first handle on a cubic and the only handle on a quadratic.
PrimaryHandle(SegmentId),
/// The end handle on a cubic bézier.
EndHandle(SegmentId),
} }
impl ManipulatorPointId { impl ManipulatorPointId {
pub fn new(group: ManipulatorGroupId, manipulator_type: SelectedType) -> Self { /// Attempt to retrieve the manipulator position in layer space (no transformation applied).
Self { group, manipulator_type } #[must_use]
pub fn get_position(&self, vector_data: &VectorData) -> Option<DVec2> {
match self {
ManipulatorPointId::Anchor(id) => vector_data.point_domain.position_from_id(*id),
ManipulatorPointId::PrimaryHandle(id) => vector_data.segment_from_id(*id).and_then(|bezier| bezier.handle_start()),
ManipulatorPointId::EndHandle(id) => vector_data.segment_from_id(*id).and_then(|bezier| bezier.handle_end()),
}
}
/// Attempt to get a pair of handles. For an anchor this is the first to handles connected. For a handle it is self and the first opposing handle.
#[must_use]
pub fn get_handle_pair(self, vector_data: &VectorData) -> Option<[HandleId; 2]> {
match self {
ManipulatorPointId::Anchor(point) => vector_data.segment_domain.all_connected(point).take(2).collect::<Vec<_>>().try_into().ok(),
ManipulatorPointId::PrimaryHandle(segment) => {
let point = vector_data.segment_domain.segment_start_from_id(segment)?;
let current = HandleId::primary(segment);
let other = vector_data.segment_domain.all_connected(point).find(|&value| value != current);
other.map(|other| [current, other])
}
ManipulatorPointId::EndHandle(segment) => {
let point = vector_data.segment_domain.segment_end_from_id(segment)?;
let current = HandleId::end(segment);
let other = vector_data.segment_domain.all_connected(point).find(|&value| value != current);
other.map(|other| [current, other])
}
}
}
/// Attempt to find the closest anchor. If self is already an anchor then it is just self. If it is a start or end handle, then the start or end point is chosen.
#[must_use]
pub fn get_anchor(self, vector_data: &VectorData) -> Option<PointId> {
match self {
ManipulatorPointId::Anchor(point) => Some(point),
ManipulatorPointId::PrimaryHandle(segment) => vector_data.segment_domain.segment_start_from_id(segment),
ManipulatorPointId::EndHandle(segment) => vector_data.segment_domain.segment_end_from_id(segment),
}
}
/// Attempt to convert self to a [`HandleId`], returning none for an anchor.
#[must_use]
pub fn as_handle(self) -> Option<HandleId> {
match self {
ManipulatorPointId::PrimaryHandle(segment) => Some(HandleId::primary(segment)),
ManipulatorPointId::EndHandle(segment) => Some(HandleId::end(segment)),
ManipulatorPointId::Anchor(_) => None,
}
}
/// Attempt to convert self to an anchor, returning None for a handle.
#[must_use]
pub fn as_anchor(self) -> Option<PointId> {
match self {
ManipulatorPointId::Anchor(point) => Some(point),
_ => None,
}
} }
} }
/// The type of handle found on a bézier curve.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SelectedType { pub enum HandleType {
Anchor = 1 << 0, /// The first handle on a cubic bézier or the only handle on a quadratic bézier.
InHandle = 1 << 1, Primary,
OutHandle = 1 << 2, /// The second handle on a cubic bézier.
End,
} }
impl SelectedType {
/// Get the location of the [SelectedType] in the [ManipulatorGroup] /// Represents a primary or end handle found in a particular segment.
pub fn get_position(&self, manipulator_group: &ManipulatorGroup<ManipulatorGroupId>) -> Option<DVec2> { #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
match self { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Self::Anchor => Some(manipulator_group.anchor), pub struct HandleId {
Self::InHandle => manipulator_group.in_handle, pub ty: HandleType,
Self::OutHandle => manipulator_group.out_handle, pub segment: SegmentId,
}
impl HandleId {
/// Construct a handle for the first handle on a cubic bézier or the only handle on a quadratic bézier.
#[must_use]
pub const fn primary(segment: SegmentId) -> Self {
Self { ty: HandleType::Primary, segment }
}
/// Construct a handle for the end handle on a cubic bézier.
#[must_use]
pub const fn end(segment: SegmentId) -> Self {
Self { ty: HandleType::End, segment }
}
/// Convert to [`ManipulatorPointId`].
#[must_use]
pub fn to_manipulator_point(self) -> ManipulatorPointId {
match self.ty {
HandleType::Primary => ManipulatorPointId::PrimaryHandle(self.segment),
HandleType::End => ManipulatorPointId::EndHandle(self.segment),
} }
} }
/// Get the closest [SelectedType] in the [ManipulatorGroup]. /// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle.
pub fn closest_widget(manipulator_group: &ManipulatorGroup<ManipulatorGroupId>, transform_space: DAffine2, target: DVec2, hide_handle_distance: f64) -> (Self, f64) { #[must_use]
let anchor = transform_space.transform_point2(manipulator_group.anchor); pub fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType {
// Skip handles under the anchor let Self { ty, segment } = self;
let not_under_anchor = |&(selected_type, position): &(SelectedType, DVec2)| selected_type == Self::Anchor || position.distance_squared(anchor) > hide_handle_distance.powi(2); match ty {
let compute_distance = |selected_type: Self| { HandleType::Primary => VectorModificationType::SetPrimaryHandle { segment, relative_position },
selected_type.get_position(manipulator_group).and_then(|position| { HandleType::End => VectorModificationType::SetEndHandle { segment, relative_position },
Some((selected_type, transform_space.transform_point2(position)))
.filter(not_under_anchor)
.map(|(selected_type, pos)| (selected_type, pos.distance_squared(target)))
})
};
[Self::Anchor, Self::InHandle, Self::OutHandle]
.into_iter()
.filter_map(compute_distance)
.min_by(|a, b| a.1.total_cmp(&b.1))
.unwrap_or((Self::Anchor, manipulator_group.anchor.distance_squared(target)))
}
/// Opposite handle
pub fn opposite(&self) -> Self {
match self {
SelectedType::Anchor => SelectedType::Anchor,
SelectedType::InHandle => SelectedType::OutHandle,
SelectedType::OutHandle => SelectedType::InHandle,
} }
} }
/// Check if handle /// Convert an end handle to the primary handle and a primary handle to an end handle. Note that the new handle may not exist (e.g. for a quadratic bézier).
pub fn is_handle(self) -> bool { #[must_use]
self != SelectedType::Anchor pub fn opposite(self) -> Self {
match self.ty {
HandleType::Primary => Self::end(self.segment),
HandleType::End => Self::primary(self.segment),
}
} }
} }
#[cfg(test)]
fn assert_subpath_eq(generated: &Vec<bezier_rs::Subpath<PointId>>, expected: &[bezier_rs::Subpath<PointId>]) {
assert_eq!(generated.len(), expected.len());
for (generated, expected) in generated.iter().zip(expected) {
assert_eq!(generated.manipulator_groups().len(), expected.manipulator_groups().len());
assert_eq!(generated.closed(), expected.closed());
for (generated, expected) in generated.manipulator_groups().iter().zip(expected.manipulator_groups()) {
assert_eq!(generated.in_handle, expected.in_handle);
assert_eq!(generated.out_handle, expected.out_handle);
assert_eq!(generated.anchor, expected.anchor);
}
}
}
#[test]
fn construct_closed_subpath() {
let circle = bezier_rs::Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
let vector_data = VectorData::from_subpath(&circle);
assert_eq!(vector_data.point_domain.ids().len(), 4);
let bézier_paths = vector_data.segment_bezier_iter().map(|(_, bézier, _, _)| bézier).collect::<Vec<_>>();
assert_eq!(bézier_paths.len(), 4);
assert!(bézier_paths.iter().all(|&bézier| circle.iter().any(|original_bézier| original_bézier == bézier)));
let generated = vector_data.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[circle]);
}
#[test]
fn construct_open_subpath() {
let bézier = bezier_rs::Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::NEG_ONE, DVec2::ONE, DVec2::X);
let subpath = bezier_rs::Subpath::from_bezier(&bézier);
let vector_data = VectorData::from_subpath(&subpath);
assert_eq!(vector_data.point_domain.ids().len(), 2);
let bézier_paths = vector_data.segment_bezier_iter().map(|(_, bézier, _, _)| bézier).collect::<Vec<_>>();
assert_eq!(bézier_paths, vec![bézier]);
let generated = vector_data.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[subpath]);
}
#[test]
fn construct_many_subpath() {
let curve = bezier_rs::Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::NEG_ONE, DVec2::ONE, DVec2::X);
let curve = bezier_rs::Subpath::from_bezier(&curve);
let circle = bezier_rs::Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
let vector_data = VectorData::from_subpaths([&curve, &circle], false);
assert_eq!(vector_data.point_domain.ids().len(), 6);
let bézier_paths = vector_data.segment_bezier_iter().map(|(_, bézier, _, _)| bézier).collect::<Vec<_>>();
assert_eq!(bézier_paths.len(), 5);
assert!(bézier_paths.iter().all(|&bézier| circle.iter().chain(curve.iter()).any(|original_bézier| original_bézier == bézier)));
let generated = vector_data.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[curve, circle]);
}
@@ -1,8 +1,11 @@
use super::HandleId;
use dyn_any::{DynAny, StaticType}; use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use std::collections::HashMap; use std::collections::HashMap;
/// A simple macro for creating strongly typed ids (to avoid confusion when passing around ids).
macro_rules! create_ids { macro_rules! create_ids {
($($id:ident),*) => { ($($id:ident),*) => {
$( $(
@@ -12,14 +15,23 @@ macro_rules! create_ids {
pub struct $id(u64); pub struct $id(u64);
impl $id { impl $id {
pub const ZERO: $id = $id(0);
/// Generate a new random id /// Generate a new random id
pub fn generate() -> Self { pub fn generate() -> Self {
Self(crate::uuid::generate_uuid()) Self(crate::uuid::generate_uuid())
} }
/// Gets the inner raw value.
pub fn inner(self) -> u64 { pub fn inner(self) -> u64 {
self.0 self.0
} }
/// Adds one to the current value and returns the old value. Note that the ids are not going to be unique unless you use the largest id.
pub fn next_id(&mut self) -> Self {
self.0 += 1;
*self
}
} }
)* )*
}; };
@@ -55,7 +67,17 @@ impl PointDomain {
self.positions.clear(); self.positions.clear();
} }
pub fn retain(&mut self, f: impl Fn(&PointId) -> bool) {
let mut keep = self.id.iter().map(&f);
self.positions.retain(|_| keep.next().unwrap_or_default());
self.id.retain(f);
}
pub fn push(&mut self, id: PointId, position: DVec2) { pub fn push(&mut self, id: PointId, position: DVec2) {
if self.id.contains(&id) {
warn!("Duplicate point");
return;
}
self.id.push(id); self.id.push(id);
self.positions.push(position); self.positions.push(position);
} }
@@ -64,11 +86,19 @@ impl PointDomain {
&self.positions &self.positions
} }
pub fn positions_mut(&mut self) -> impl Iterator<Item = (PointId, &mut DVec2)> {
self.id.iter().copied().zip(self.positions.iter_mut())
}
pub fn ids(&self) -> &[PointId] { pub fn ids(&self) -> &[PointId] {
&self.id &self.id
} }
pub fn pos_from_id(&self, id: PointId) -> Option<DVec2> { pub fn next_id(&self) -> PointId {
self.ids().iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(PointId::ZERO)
}
pub fn position_from_id(&self, id: PointId) -> Option<DVec2> {
let pos = self.resolve_id(id).map(|index| self.positions[index]); let pos = self.resolve_id(id).map(|index| self.positions[index]);
if pos.is_none() { if pos.is_none() {
warn!("Resolving pos of invalid id"); warn!("Resolving pos of invalid id");
@@ -99,7 +129,6 @@ pub struct SegmentDomain {
ids: Vec<SegmentId>, ids: Vec<SegmentId>,
start_point: Vec<PointId>, start_point: Vec<PointId>,
end_point: Vec<PointId>, end_point: Vec<PointId>,
// TODO: Also store handle points as `PointId`s rather than Bezier-rs's internal `DVec2`s
handles: Vec<bezier_rs::BezierHandles>, handles: Vec<bezier_rs::BezierHandles>,
stroke: Vec<StrokeId>, stroke: Vec<StrokeId>,
} }
@@ -123,21 +152,116 @@ impl SegmentDomain {
self.stroke.clear(); self.stroke.clear();
} }
pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: bezier_rs::BezierHandles, stroke: StrokeId) { pub fn retain(&mut self, f: impl Fn(&SegmentId) -> bool) {
self.ids.push(id); let mut keep = self.ids.iter().map(&f);
self.start_point.push(start); self.start_point.retain(|_| keep.next().unwrap_or_default());
self.end_point.push(end); let mut keep = self.ids.iter().map(&f);
self.handles.push(handles); self.end_point.retain(|_| keep.next().unwrap_or_default());
self.stroke.push(stroke); let mut keep = self.ids.iter().map(&f);
self.handles.retain(|_| keep.next().unwrap_or_default());
let mut keep = self.ids.iter().map(&f);
self.stroke.retain(|_| keep.next().unwrap_or_default());
self.ids.retain(f);
} }
fn resolve_id(&self, id: SegmentId) -> Option<usize> { pub fn ids(&self) -> &[SegmentId] {
&self.ids
}
pub fn next_id(&self) -> SegmentId {
self.ids().iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(SegmentId::ZERO)
}
pub fn start_point(&self) -> &[PointId] {
&self.start_point
}
pub fn end_point(&self) -> &[PointId] {
&self.end_point
}
pub fn handles(&self) -> &[bezier_rs::BezierHandles] {
&self.handles
}
pub fn stroke(&self) -> &[StrokeId] {
&self.stroke
}
pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: bezier_rs::BezierHandles, stroke: StrokeId) {
if self.ids.contains(&id) {
warn!("Duplicate segment");
return;
}
// Attempt to keep line joins?
let after = self.end_point.iter().copied().position(|other_end| other_end == start || other_end == end);
let before = self.start_point.iter().copied().position(|other_start| other_start == start || other_start == end);
let (index, flip) = match (before, after) {
(_, Some(after)) => (after + 1, self.end_point[after] == end),
(Some(before), _) => (before, self.start_point[before] == start),
(None, None) => (self.ids.len(), false),
};
self.ids.insert(index, id);
self.start_point.insert(index, if flip { end } else { start });
self.end_point.insert(index, if flip { start } else { end });
self.handles.insert(index, if flip { handles.flipped() } else { handles });
self.stroke.insert(index, stroke);
}
pub fn start_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut PointId)> {
self.ids.iter().copied().zip(self.start_point.iter_mut())
}
pub fn end_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut PointId)> {
self.ids.iter().copied().zip(self.end_point.iter_mut())
}
pub fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut bezier_rs::BezierHandles, PointId, PointId)> {
let nested = self.ids.iter().zip(&mut self.handles).zip(&self.start_point).zip(&self.end_point);
nested.map(|(((&a, b), &c), &d)| (a, b, c, d))
}
pub fn stroke_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut StrokeId)> {
self.ids.iter().copied().zip(self.stroke.iter_mut())
}
pub fn segment_start_from_id(&self, segment: SegmentId) -> Option<PointId> {
self.id_to_index(segment).and_then(|index| self.start_point.get(index)).copied()
}
pub fn segment_end_from_id(&self, segment: SegmentId) -> Option<PointId> {
self.id_to_index(segment).and_then(|index| self.end_point.get(index)).copied()
}
/// Returns an array for the start and end points of a segment.
pub fn points_from_id(&self, segment: SegmentId) -> Option<[PointId; 2]> {
self.segment_start_from_id(segment).and_then(|start| self.segment_end_from_id(segment).map(|end| [start, end]))
}
/// Attempts to find another point in the segment that is not the one passed in.
pub fn other_point(&self, segment: SegmentId, current: PointId) -> Option<PointId> {
self.points_from_id(segment).and_then(|points| points.into_iter().find(|&point| point != current))
}
/// Gets all points connected to the current one but not including the current one.
pub fn connected_points(&self, current: PointId) -> impl Iterator<Item = PointId> + '_ {
self.start_point.iter().zip(&self.end_point).filter_map(move |(&a, &b)| match (a == current, b == current) {
(true, false) => Some(b),
(false, true) => Some(a),
_ => None,
})
}
fn id_to_index(&self, id: SegmentId) -> Option<usize> {
debug_assert_eq!(self.ids.len(), self.handles.len());
debug_assert_eq!(self.ids.len(), self.start_point.len());
debug_assert_eq!(self.ids.len(), self.end_point.len());
self.ids.iter().position(|&check_id| check_id == id) self.ids.iter().position(|&check_id| check_id == id)
} }
fn resolve_range(&self, range: &core::ops::RangeInclusive<SegmentId>) -> Option<core::ops::RangeInclusive<usize>> { fn resolve_range(&self, range: &core::ops::RangeInclusive<SegmentId>) -> Option<core::ops::RangeInclusive<usize>> {
match (self.resolve_id(*range.start()), self.resolve_id(*range.end())) { match (self.id_to_index(*range.start()), self.id_to_index(*range.end())) {
(Some(start), Some(end)) => Some(start..=end), (Some(start), Some(end)) if start.max(end) < self.handles.len().min(self.ids.len()).min(self.start_point.len()).min(self.end_point.len()) => Some(start..=end),
_ => { _ => {
warn!("Resolving range with invalid id"); warn!("Resolving range with invalid id");
None None
@@ -158,6 +282,26 @@ impl SegmentDomain {
*handles = handles.apply_transformation(|p| transform.transform_point2(p)); *handles = handles.apply_transformation(|p| transform.transform_point2(p));
} }
} }
/// Enumerate all segments that start at the point.
pub fn start_connected(&self, point: PointId) -> impl Iterator<Item = SegmentId> + '_ {
self.start_point.iter().zip(&self.ids).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
}
/// Enumerate all segments that end at the point.
pub fn end_connected(&self, point: PointId) -> impl Iterator<Item = SegmentId> + '_ {
self.end_point.iter().zip(&self.ids).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
}
/// Enumerate all segments that start or end at a point, converting them to [`HandleId`s]. Note that the handles may not exist e.g. for a linear segment.
pub fn all_connected(&self, point: PointId) -> impl Iterator<Item = HandleId> + '_ {
self.start_connected(point).map(HandleId::primary).chain(self.end_connected(point).map(HandleId::end))
}
/// Enumerate the number of segments connected to a point. If a segment starts and ends at a point then it is counted twice.
pub fn connected_count(&self, point: PointId) -> usize {
self.all_connected(point).count()
}
} }
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)] #[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
@@ -184,7 +328,19 @@ impl RegionDomain {
self.fill.clear(); self.fill.clear();
} }
pub fn retain(&mut self, f: impl Fn(&RegionId) -> bool) {
let mut keep = self.ids.iter().map(&f);
self.segment_range.retain(|_| keep.next().unwrap_or_default());
let mut keep = self.ids.iter().map(&f);
self.fill.retain(|_| keep.next().unwrap_or_default());
self.ids.retain(&f);
}
pub fn push(&mut self, id: RegionId, segment_range: core::ops::RangeInclusive<SegmentId>, fill: FillId) { pub fn push(&mut self, id: RegionId, segment_range: core::ops::RangeInclusive<SegmentId>, fill: FillId) {
if self.ids.contains(&id) {
warn!("Duplicate region");
return;
}
self.ids.push(id); self.ids.push(id);
self.segment_range.push(segment_range); self.segment_range.push(segment_range);
self.fill.push(fill); self.fill.push(fill);
@@ -194,6 +350,30 @@ impl RegionDomain {
self.ids.iter().position(|&check_id| check_id == id) self.ids.iter().position(|&check_id| check_id == id)
} }
pub fn next_id(&self) -> RegionId {
self.ids.iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(RegionId::ZERO)
}
pub fn segment_range_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut core::ops::RangeInclusive<SegmentId>)> {
self.ids.iter().copied().zip(self.segment_range.iter_mut())
}
pub fn fill_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut FillId)> {
self.ids.iter().copied().zip(self.fill.iter_mut())
}
pub fn ids(&self) -> &[RegionId] {
&self.ids
}
pub fn segment_range(&self) -> &[core::ops::RangeInclusive<SegmentId>] {
&self.segment_range
}
pub fn fill(&self) -> &[FillId] {
&self.fill
}
fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) { fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
self.ids.extend(other.ids.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id))); self.ids.extend(other.ids.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id)));
self.segment_range.extend( self.segment_range.extend(
@@ -209,15 +389,22 @@ impl RegionDomain {
impl super::VectorData { impl super::VectorData {
/// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles. Returns [`None`] if either ID is invalid. /// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles. Returns [`None`] if either ID is invalid.
fn segment_to_bezier(&self, start: PointId, end: PointId, handles: bezier_rs::BezierHandles) -> Option<bezier_rs::Bezier> { fn segment_to_bezier(&self, start: PointId, end: PointId, handles: bezier_rs::BezierHandles) -> Option<bezier_rs::Bezier> {
let start = self.point_domain.pos_from_id(start)?; let start = self.point_domain.position_from_id(start)?;
let end = self.point_domain.pos_from_id(end)?; let end = self.point_domain.position_from_id(end)?;
Some(bezier_rs::Bezier { start, end, handles }) Some(bezier_rs::Bezier { start, end, handles })
} }
/// Tries to convert a segment with the specified id to a [`bezier_rs::Bezier`], returning None if the id is invalid. /// Tries to convert a segment with the specified id to a [`bezier_rs::Bezier`], returning None if the id is invalid.
pub fn segment_from_id(&self, id: SegmentId) -> Option<bezier_rs::Bezier> { pub fn segment_from_id(&self, id: SegmentId) -> Option<bezier_rs::Bezier> {
let index = self.segment_domain.resolve_id(id)?; self.segment_points_from_id(id).map(|(_, _, bezier)| bezier)
self.segment_to_bezier(self.segment_domain.start_point[index], self.segment_domain.end_point[index], self.segment_domain.handles[index]) }
/// Tries to convert a segment with the specified id to the start and end points and a [`bezier_rs::Bezier`], returning None if the id is invalid.
pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, bezier_rs::Bezier)> {
let index: usize = self.segment_domain.id_to_index(id)?;
let start = self.segment_domain.start_point[index];
let end = self.segment_domain.end_point[index];
Some((start, end, self.segment_to_bezier(start, end, self.segment_domain.handles[index])?))
} }
/// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments. /// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
@@ -237,17 +424,6 @@ impl super::VectorData {
let mut first_point = None; let mut first_point = None;
let mut groups = Vec::new(); let mut groups = Vec::new();
let mut last: Option<(PointId, bezier_rs::BezierHandles)> = None; let mut last: Option<(PointId, bezier_rs::BezierHandles)> = None;
let end_point = |last: Option<(PointId, bezier_rs::BezierHandles)>, next: Option<PointId>, groups: &mut Vec<_>| {
if let Some((disconnected_previous, previous_handle)) = last.filter(|(end, _)| !next.is_some_and(|next| next == *end)) {
groups.push(bezier_rs::ManipulatorGroup {
anchor: self.point_domain.pos_from_id(disconnected_previous)?,
in_handle: previous_handle.end(),
out_handle: None,
id: disconnected_previous,
});
}
Some(())
};
for (handle, start, end) in segments { for (handle, start, end) in segments {
if last.is_some_and(|(previous_end, _)| previous_end != start) { if last.is_some_and(|(previous_end, _)| previous_end != start) {
@@ -255,10 +431,9 @@ impl super::VectorData {
return None; return None;
} }
first_point = Some(first_point.unwrap_or(start)); first_point = Some(first_point.unwrap_or(start));
end_point(last, Some(start), &mut groups)?;
groups.push(bezier_rs::ManipulatorGroup { groups.push(bezier_rs::ManipulatorGroup {
anchor: self.point_domain.pos_from_id(start)?, anchor: self.point_domain.position_from_id(start)?,
in_handle: last.and_then(|(_, handle)| handle.end()), in_handle: last.and_then(|(_, handle)| handle.end()),
out_handle: handle.start(), out_handle: handle.start(),
id: start, id: start,
@@ -266,8 +441,21 @@ impl super::VectorData {
last = Some((end, handle)); last = Some((end, handle));
} }
end_point(last, None, &mut groups)?;
let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point; let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point;
if let Some((end, last_handle)) = last {
if closed {
groups[0].in_handle = last_handle.end();
} else {
groups.push(bezier_rs::ManipulatorGroup {
anchor: self.point_domain.position_from_id(end)?,
in_handle: last_handle.end(),
out_handle: None,
id: end,
});
}
}
Some(bezier_rs::Subpath::new(groups, closed)) Some(bezier_rs::Subpath::new(groups, closed))
} }
@@ -279,10 +467,13 @@ impl super::VectorData {
.zip(&self.region_domain.segment_range) .zip(&self.region_domain.segment_range)
.filter_map(|(&id, segment_range)| self.segment_domain.resolve_range(segment_range).map(|range| (id, range))) .filter_map(|(&id, segment_range)| self.segment_domain.resolve_range(segment_range).map(|range| (id, range)))
.filter_map(|(id, range)| { .filter_map(|(id, range)| {
let segments_iter = self.segment_domain.handles[range.clone()] let segments_iter = self
.segment_domain
.handles
.get(range.clone())?
.iter() .iter()
.zip(&self.segment_domain.start_point[range.clone()]) .zip(self.segment_domain.start_point.get(range.clone())?)
.zip(&self.segment_domain.end_point[range]) .zip(self.segment_domain.end_point.get(range)?)
.map(|((&handles, &start), &end)| (handles, start, end)); .map(|((&handles, &start), &end)| (handles, start, end));
self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath)) self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath))
@@ -294,6 +485,17 @@ impl super::VectorData {
StrokePathIter { vector_data: self, segment_index: 0 } StrokePathIter { vector_data: self, segment_index: 0 }
} }
/// Construct an iterator [`bezier_rs::ManipulatorGroup`] for stroke.
pub fn manipulator_groups(&self) -> impl Iterator<Item = bezier_rs::ManipulatorGroup<PointId>> + '_ {
self.stroke_bezier_paths().flat_map(|mut path| std::mem::take(path.manipulator_groups_mut()))
}
/// Get manipulator by id
pub fn manipulator_group_id(&self, id: impl Into<PointId>) -> Option<bezier_rs::ManipulatorGroup<PointId>> {
let id = id.into();
self.manipulator_groups().find(|group| group.id == id)
}
/// Transforms this vector data /// Transforms this vector data
pub fn transform(&mut self, transform: DAffine2) { pub fn transform(&mut self, transform: DAffine2) {
self.point_domain.transform(transform); self.point_domain.transform(transform);
@@ -301,6 +503,7 @@ impl super::VectorData {
} }
} }
#[derive(Clone)]
pub struct StrokePathIter<'a> { pub struct StrokePathIter<'a> {
vector_data: &'a super::VectorData, vector_data: &'a super::VectorData,
segment_index: usize, segment_index: usize,
@@ -339,11 +542,6 @@ impl bezier_rs::Identifier for PointId {
Self::generate() Self::generate()
} }
} }
impl From<crate::uuid::ManipulatorGroupId> for PointId {
fn from(value: crate::uuid::ManipulatorGroupId) -> Self {
Self(value.inner())
}
}
impl crate::vector::ConcatElement for super::VectorData { impl crate::vector::ConcatElement for super::VectorData {
fn concat(&mut self, other: &Self, transform: glam::DAffine2) { fn concat(&mut self, other: &Self, transform: glam::DAffine2) {
@@ -369,6 +567,7 @@ impl crate::vector::ConcatElement for super::VectorData {
} }
} }
/// Represents the conversion of ids used when concatenating vector data with conflicting ids.
struct IdMap { struct IdMap {
point_map: HashMap<PointId, PointId>, point_map: HashMap<PointId, PointId>,
segment_map: HashMap<SegmentId, SegmentId>, segment_map: HashMap<SegmentId, SegmentId>,
@@ -0,0 +1,530 @@
use super::*;
use crate::Node;
use bezier_rs::BezierHandles;
use dyn_any::{DynAny, StaticType};
use std::collections::{HashMap, HashSet};
/// Represents a procedural change to the [`PointDomain`] in [`VectorData`].
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PointModification {
add: Vec<PointId>,
remove: HashSet<PointId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
delta: HashMap<PointId, DVec2>,
}
impl PointModification {
/// Apply this modification to the specified [`PointDomain`].
pub fn apply(&self, point_domain: &mut PointDomain, segment_domain: &mut SegmentDomain) {
point_domain.retain(|id| !self.remove.contains(id));
for (id, position) in point_domain.positions_mut() {
let Some(&delta) = self.delta.get(&id) else { continue };
if !delta.is_finite() {
warn!("Invalid delta when applying a point modification");
continue;
}
*position += delta;
for (_, handles, start, end) in segment_domain.handles_mut() {
if start == id {
handles.move_start(delta);
}
if end == id {
handles.move_end(delta);
}
}
}
for &add_id in &self.add {
let Some(&position) = self.delta.get(&add_id) else { continue };
if !position.is_finite() {
warn!("Invalid position when applying a point modification");
continue;
}
point_domain.push(add_id, position);
}
}
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
pub fn create_from_vector(vector_data: &VectorData) -> Self {
Self {
add: vector_data.point_domain.ids().to_vec(),
remove: HashSet::new(),
delta: vector_data.point_domain.ids().iter().copied().zip(vector_data.point_domain.positions().iter().cloned()).collect(),
}
}
fn push(&mut self, id: PointId, position: DVec2) {
self.add.push(id);
self.delta.insert(id, position);
}
fn remove(&mut self, id: PointId) {
self.remove.insert(id);
self.add.retain(|&add| add != id);
self.delta.remove(&id);
}
}
/// Represents a procedural change to the [`SegmentDomain`] in [`VectorData`].
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SegmentModification {
add: Vec<SegmentId>,
remove: HashSet<SegmentId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
start_point: HashMap<SegmentId, PointId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
end_point: HashMap<SegmentId, PointId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
handle_primary: HashMap<SegmentId, Option<DVec2>>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
handle_end: HashMap<SegmentId, Option<DVec2>>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
stroke: HashMap<SegmentId, StrokeId>,
}
impl SegmentModification {
/// Apply this modification to the specified [`SegmentDomain`].
pub fn apply(&self, segment_domain: &mut SegmentDomain, point_domain: &PointDomain) {
segment_domain.retain(|id| !self.remove.contains(id));
for (id, point) in segment_domain.start_point_mut() {
let Some(&new) = self.start_point.get(&id) else { continue };
if !point_domain.ids().contains(&new) {
warn!("Invalid start ID when applying a segment modification");
continue;
}
*point = new;
}
for (id, point) in segment_domain.end_point_mut() {
let Some(&new) = self.end_point.get(&id) else { continue };
if !point_domain.ids().contains(&new) {
warn!("Invalid end ID when applying a segment modification");
continue;
}
*point = new;
}
for (id, handles, start, end) in segment_domain.handles_mut() {
let Some(start) = point_domain.position_from_id(start) else { continue };
let Some(end) = point_domain.position_from_id(end) else { continue };
// Compute the actual start and end position based on the offset from the anchor
let start = self.handle_primary.get(&id).copied().map(|handle| handle.map(|handle| handle + start));
let end = self.handle_end.get(&id).copied().map(|handle| handle.map(|handle| handle + end));
if !start.unwrap_or_default().map_or(true, |start| start.is_finite()) || !end.unwrap_or_default().map_or(true, |end| end.is_finite()) {
warn!("Invalid handles when applying a segment modification");
continue;
}
match (start, end) {
// The new handles are fully specified by the modification
(Some(Some(handle_start)), Some(Some(handle_end))) => *handles = BezierHandles::Cubic { handle_start, handle_end },
(Some(Some(handle)), Some(None)) | (Some(None), Some(Some(handle))) => *handles = BezierHandles::Quadratic { handle },
(Some(None), Some(None)) => *handles = BezierHandles::Linear,
// Remove the end handle
(None, Some(None)) => {
if let BezierHandles::Cubic { handle_start, .. } = *handles {
*handles = BezierHandles::Quadratic { handle: handle_start }
}
}
// Change the end handle
(None, Some(Some(handle_end))) => match *handles {
BezierHandles::Linear => *handles = BezierHandles::Quadratic { handle: handle_end },
BezierHandles::Quadratic { handle: handle_start } => *handles = BezierHandles::Cubic { handle_start, handle_end },
BezierHandles::Cubic { handle_start, .. } => *handles = BezierHandles::Cubic { handle_start, handle_end },
},
// Remove the start handle
(Some(None), None) => *handles = BezierHandles::Linear,
// Change the start handle
(Some(Some(handle_start)), None) => match *handles {
BezierHandles::Linear => *handles = BezierHandles::Quadratic { handle: handle_start },
BezierHandles::Quadratic { .. } => *handles = BezierHandles::Quadratic { handle: handle_start },
BezierHandles::Cubic { handle_end, .. } => *handles = BezierHandles::Cubic { handle_start, handle_end },
},
// No change
(None, None) => {}
};
}
for (id, stroke) in segment_domain.stroke_mut() {
let Some(&new) = self.stroke.get(&id) else { continue };
*stroke = new;
}
for &add_id in &self.add {
let Some(&start) = self.start_point.get(&add_id) else { continue };
let Some(&end) = self.end_point.get(&add_id) else { continue };
let Some(&handle_start) = self.handle_primary.get(&add_id) else { continue };
let Some(&handle_end) = self.handle_end.get(&add_id) else { continue };
let Some(&stroke) = self.stroke.get(&add_id) else { continue };
if !point_domain.ids().contains(&start) {
warn!("invalid start id");
continue;
}
if !point_domain.ids().contains(&end) {
warn!("invalid end id");
continue;
}
let Some(start_position) = point_domain.position_from_id(start) else { continue };
let Some(end_position) = point_domain.position_from_id(end) else { continue };
let handles = match (handle_start, handle_end) {
(Some(handle_start), Some(handle_end)) => BezierHandles::Cubic {
handle_start: handle_start + start_position,
handle_end: handle_end + end_position,
},
(Some(handle), None) | (None, Some(handle)) => BezierHandles::Quadratic { handle: handle + start_position },
(None, None) => BezierHandles::Linear,
};
if !handles.is_finite() {
warn!("invalid handles");
continue;
}
segment_domain.push(add_id, start, end, handles, stroke);
}
}
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
pub fn create_from_vector(vector_data: &VectorData) -> Self {
Self {
add: vector_data.segment_domain.ids().to_vec(),
remove: HashSet::new(),
start_point: vector_data.segment_domain.ids().iter().copied().zip(vector_data.segment_domain.start_point().iter().cloned()).collect(),
end_point: vector_data.segment_domain.ids().iter().copied().zip(vector_data.segment_domain.end_point().iter().cloned()).collect(),
handle_primary: vector_data.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_start().map(|handle| handle - b.start))).collect(),
handle_end: vector_data.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_end().map(|handle| handle - b.end))).collect(),
stroke: vector_data.segment_domain.ids().iter().copied().zip(vector_data.segment_domain.stroke().iter().cloned()).collect(),
}
}
fn push(&mut self, id: SegmentId, points: [PointId; 2], handles: [Option<DVec2>; 2], stroke: StrokeId) {
self.remove.remove(&id);
self.add.push(id);
self.start_point.insert(id, points[0]);
self.end_point.insert(id, points[1]);
self.handle_primary.insert(id, handles[0]);
self.handle_end.insert(id, handles[1]);
self.stroke.insert(id, stroke);
}
fn remove(&mut self, id: SegmentId) {
self.remove.insert(id);
self.add.retain(|&add| add != id);
self.start_point.remove(&id);
self.end_point.remove(&id);
self.handle_primary.remove(&id);
self.handle_end.remove(&id);
self.stroke.remove(&id);
}
}
/// Represents a procedural change to the [`RegionDomain`] in [`VectorData`].
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RegionModification {
add: Vec<RegionId>,
remove: HashSet<RegionId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
segment_range: HashMap<RegionId, core::ops::RangeInclusive<SegmentId>>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
fill: HashMap<RegionId, FillId>,
}
impl RegionModification {
/// Apply this modification to the specified [`RegionDomain`].
pub fn apply(&self, region_domain: &mut RegionDomain) {
region_domain.retain(|id| !self.remove.contains(id));
for (id, segment_range) in region_domain.segment_range_mut() {
let Some(new) = self.segment_range.get(&id) else { continue };
*segment_range = new.clone(); // Range inclusive is not copy
}
for (id, fill) in region_domain.fill_mut() {
let Some(&new) = self.fill.get(&id) else { continue };
*fill = new;
}
for &add_id in &self.add {
let Some(segment_range) = self.segment_range.get(&add_id) else { continue };
let Some(&fill) = self.fill.get(&add_id) else { continue };
region_domain.push(add_id, segment_range.clone(), fill);
}
}
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
pub fn create_from_vector(vector_data: &VectorData) -> Self {
Self {
add: vector_data.region_domain.ids().to_vec(),
remove: HashSet::new(),
segment_range: vector_data.region_domain.ids().iter().copied().zip(vector_data.region_domain.segment_range().iter().cloned()).collect(),
fill: vector_data.region_domain.ids().iter().copied().zip(vector_data.region_domain.fill().iter().cloned()).collect(),
}
}
}
/// Represents a procedural change to the [`VectorData`].
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VectorModification {
points: PointModification,
segments: SegmentModification,
regions: RegionModification,
add_g1_continuous: HashSet<[HandleId; 2]>,
remove_g1_continuous: HashSet<[HandleId; 2]>,
}
/// A modification type that can be added to a [`VectorModification`].
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum VectorModificationType {
InsertSegment { id: SegmentId, points: [PointId; 2], handles: [Option<DVec2>; 2] },
InsertPoint { id: PointId, position: DVec2 },
RemoveSegment { id: SegmentId },
RemovePoint { id: PointId },
SetG1Continuous { handles: [HandleId; 2], enabled: bool },
SetHandles { segment: SegmentId, handles: [Option<DVec2>; 2] },
SetPrimaryHandle { segment: SegmentId, relative_position: DVec2 },
SetEndHandle { segment: SegmentId, relative_position: DVec2 },
SetStartPoint { segment: SegmentId, id: PointId },
SetEndPoint { segment: SegmentId, id: PointId },
ApplyPointDelta { point: PointId, delta: DVec2 },
ApplyPrimaryDelta { segment: SegmentId, delta: DVec2 },
ApplyEndDelta { segment: SegmentId, delta: DVec2 },
}
impl VectorModification {
/// Apply this modification to the specified [`VectorData`].
pub fn apply(&self, vector_data: &mut VectorData) {
self.points.apply(&mut vector_data.point_domain, &mut vector_data.segment_domain);
self.segments.apply(&mut vector_data.segment_domain, &vector_data.point_domain);
self.regions.apply(&mut vector_data.region_domain);
let valid = |val: &[HandleId; 2]| vector_data.segment_domain.ids().contains(&val[0].segment) && vector_data.segment_domain.ids().contains(&val[1].segment);
vector_data
.colinear_manipulators
.retain(|val| !self.remove_g1_continuous.contains(val) && !self.remove_g1_continuous.contains(&[val[1], val[0]]) && valid(val));
for handles in &self.add_g1_continuous {
if !vector_data.colinear_manipulators.iter().any(|test| test == handles || test == &[handles[1], handles[0]]) && valid(handles) {
vector_data.colinear_manipulators.push(*handles);
}
}
}
/// Add a [`VectorModificationType`] to this modification.
pub fn modify(&mut self, vector_data_modification: &VectorModificationType) {
match vector_data_modification {
VectorModificationType::InsertSegment { id, points, handles } => self.segments.push(*id, *points, *handles, StrokeId::ZERO),
VectorModificationType::InsertPoint { id, position } => self.points.push(*id, *position),
VectorModificationType::RemoveSegment { id } => self.segments.remove(*id),
VectorModificationType::RemovePoint { id } => self.points.remove(*id),
VectorModificationType::SetG1Continuous { handles, enabled } => {
if *enabled {
if !self.add_g1_continuous.contains(&[handles[1], handles[0]]) {
self.add_g1_continuous.insert(*handles);
}
self.remove_g1_continuous.remove(handles);
self.remove_g1_continuous.remove(&[handles[1], handles[0]]);
} else {
if !self.remove_g1_continuous.contains(&[handles[1], handles[0]]) {
self.remove_g1_continuous.insert(*handles);
}
self.add_g1_continuous.remove(handles);
self.add_g1_continuous.remove(&[handles[1], handles[0]]);
}
}
VectorModificationType::SetHandles { segment, handles } => {
self.segments.handle_primary.insert(*segment, handles[0]);
self.segments.handle_end.insert(*segment, handles[1]);
}
VectorModificationType::SetPrimaryHandle { segment, relative_position } => {
self.segments.handle_primary.insert(*segment, Some(*relative_position));
}
VectorModificationType::SetEndHandle { segment, relative_position } => {
self.segments.handle_end.insert(*segment, Some(*relative_position));
}
VectorModificationType::SetStartPoint { segment, id } => {
self.segments.start_point.insert(*segment, *id);
}
VectorModificationType::SetEndPoint { segment, id } => {
self.segments.end_point.insert(*segment, *id);
}
VectorModificationType::ApplyPointDelta { point, delta } => {
*self.points.delta.entry(*point).or_default() += *delta;
}
VectorModificationType::ApplyPrimaryDelta { segment, delta } => {
let position = self.segments.handle_primary.entry(*segment).or_default();
*position = Some(position.unwrap_or_default() + *delta);
}
VectorModificationType::ApplyEndDelta { segment, delta } => {
let position = self.segments.handle_end.entry(*segment).or_default();
*position = Some(position.unwrap_or_default() + *delta);
}
}
}
/// Create a new modification that will convert an empty [`VectorData`] into the target [`VectorData`].
pub fn create_from_vector(vector_data: &VectorData) -> Self {
Self {
points: PointModification::create_from_vector(vector_data),
segments: SegmentModification::create_from_vector(vector_data),
regions: RegionModification::create_from_vector(vector_data),
add_g1_continuous: vector_data.colinear_manipulators.iter().copied().collect(),
remove_g1_continuous: HashSet::new(),
}
}
}
impl core::hash::Hash for VectorModification {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
// TODO: properly implement (hashing a hashset is difficult because ordering is unstable)
PointId::generate().hash(state);
}
}
/// A node that applies a procedural modification to some [`VectorData`].
#[derive(Debug, Clone, Copy)]
pub struct PathModify<VectorModificationNode> {
modification: VectorModificationNode,
}
#[node_macro::node_fn(PathModify)]
fn path_modify(mut vector_data: VectorData, modification: VectorModification) -> VectorData {
modification.apply(&mut vector_data);
vector_data
}
#[test]
fn modify_new() {
let vector_data = VectorData::from_subpaths(
[bezier_rs::Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), bezier_rs::Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO)],
false,
);
let modify = VectorModification::create_from_vector(&vector_data);
let mut new = VectorData::empty();
modify.apply(&mut new);
assert_eq!(vector_data, new);
}
#[test]
fn modify_existing() {
use bezier_rs::{Bezier, Subpath};
let subpaths = [
Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE),
Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO),
Subpath::from_beziers(
&[
Bezier::from_quadratic_dvec2(DVec2::new(0., 0.), DVec2::new(5., 10.), DVec2::new(10., 0.)),
Bezier::from_quadratic_dvec2(DVec2::new(10., 0.), DVec2::new(15., 10.), DVec2::new(20., 0.)),
],
false,
),
];
let mut vector_data = VectorData::from_subpaths(&subpaths, false);
let mut modify_new = VectorModification::create_from_vector(&vector_data);
let mut modify_original = VectorModification::default();
for modification in [&mut modify_new, &mut modify_original] {
let point = vector_data.point_domain.ids()[0];
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X * 0.5 });
let point = vector_data.point_domain.ids()[9];
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X });
}
let mut new = VectorData::empty();
modify_new.apply(&mut new);
modify_original.apply(&mut vector_data);
assert_eq!(vector_data, new);
assert_eq!(vector_data.point_domain.positions()[0], DVec2::X);
assert_eq!(vector_data.point_domain.positions()[9], DVec2::new(11., 0.));
assert_eq!(
vector_data.segment_bezier_iter().nth(8).unwrap().1,
Bezier::from_quadratic_dvec2(DVec2::new(0., 0.), DVec2::new(5., 10.), DVec2::new(11., 0.))
);
assert_eq!(
vector_data.segment_bezier_iter().nth(9).unwrap().1,
Bezier::from_quadratic_dvec2(DVec2::new(11., 0.), DVec2::new(16., 10.), DVec2::new(20., 0.))
);
}
// TODO: Eventually remove this (probably starting late 2024)
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::hash::Hash;
fn serialize_hashmap<K, V, S>(hashmap: &HashMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
where
K: Serialize + Eq + Hash,
V: Serialize,
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(hashmap.len()))?;
for (key, value) in hashmap {
seq.serialize_element(&(key, value))?;
}
seq.end()
}
fn deserialize_hashmap<'de, K, V, D>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
where
K: Deserialize<'de> + Eq + Hash,
V: Deserialize<'de>,
D: Deserializer<'de>,
{
struct HashMapVisitor<K, V> {
marker: std::marker::PhantomData<fn() -> HashMap<K, V>>,
}
impl<'de, K, V> Visitor<'de> for HashMapVisitor<K, V>
where
K: Deserialize<'de> + Eq + Hash,
V: Deserialize<'de>,
{
type Value = HashMap<K, V>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a sequence of tuples")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut hashmap = HashMap::new();
while let Some((key, value)) = seq.next_element()? {
hashmap.insert(key, value);
}
Ok(hashmap)
}
}
let visitor = HashMapVisitor { marker: std::marker::PhantomData };
deserializer.deserialize_seq(visitor)
}
+8 -6
View File
@@ -170,11 +170,11 @@ fn solidify_stroke(vector_data: VectorData) -> VectorData {
// This is where we determine whether we have a closed or open path. Ex: Oval vs line segment. // This is where we determine whether we have a closed or open path. Ex: Oval vs line segment.
if subpath_out.1.is_some() { if subpath_out.1.is_some() {
// Two closed subpaths, closed shape. Add both subpaths. // Two closed subpaths, closed shape. Add both subpaths.
result.append_subpath(subpath_out.0); result.append_subpath(subpath_out.0, false);
result.append_subpath(subpath_out.1.unwrap()); result.append_subpath(subpath_out.1.unwrap(), false);
} else { } else {
// One closed subpath, open path. // One closed subpath, open path.
result.append_subpath(subpath_out.0); result.append_subpath(subpath_out.0, false);
} }
} }
@@ -363,7 +363,7 @@ pub struct PoissonDiskPoints<SeparationDiskDiameter> {
fn poisson_disk_points(vector_data: VectorData, separation_disk_diameter: f64) -> VectorData { fn poisson_disk_points(vector_data: VectorData, separation_disk_diameter: f64) -> VectorData {
let mut rng = rand::rngs::StdRng::seed_from_u64(0); let mut rng = rand::rngs::StdRng::seed_from_u64(0);
let mut result = VectorData::empty(); let mut result = VectorData::empty();
for (_, mut subpath) in vector_data.region_bezier_paths() { for mut subpath in vector_data.stroke_bezier_paths() {
if subpath.manipulator_groups().len() < 3 { if subpath.manipulator_groups().len() < 3 {
continue; continue;
} }
@@ -400,6 +400,8 @@ fn splines_from_points(mut vector_data: VectorData) -> VectorData {
let first_handles = bezier_rs::solve_spline_first_handle(points.positions()); let first_handles = bezier_rs::solve_spline_first_handle(points.positions());
let stroke_id = StrokeId::ZERO;
for (start_index, end_index) in (0..(points.positions().len())).zip(1..(points.positions().len())) { for (start_index, end_index) in (0..(points.positions().len())).zip(1..(points.positions().len())) {
let handle_start = first_handles[start_index]; let handle_start = first_handles[start_index];
let handle_end = points.positions()[end_index] * 2. - first_handles[end_index]; let handle_end = points.positions()[end_index] * 2. - first_handles[end_index];
@@ -407,7 +409,7 @@ fn splines_from_points(mut vector_data: VectorData) -> VectorData {
vector_data vector_data
.segment_domain .segment_domain
.push(SegmentId::generate(), points.ids()[start_index], points.ids()[end_index], handles, StrokeId::generate()) .push(SegmentId::generate(), points.ids()[start_index], points.ids()[end_index], handles, stroke_id)
} }
vector_data vector_data
@@ -484,7 +486,7 @@ async fn morph<SourceFuture: Future<Output = VectorData>, TargetFuture: Future<O
manipulator.anchor = manipulator.anchor.lerp(target.anchor, time); manipulator.anchor = manipulator.anchor.lerp(target.anchor, time);
} }
result.append_subpath(source_path); result.append_subpath(source_path, true);
} }
// Mismatched subpath count // Mismatched subpath count
for mut source_path in source_paths { for mut source_path in source_paths {
+6 -5
View File
@@ -65,6 +65,7 @@ fn migrate_layer_to_merge<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
} }
Ok(s) Ok(s)
} }
// TODO: Eventually remove this (probably starting late 2024) // TODO: Eventually remove this (probably starting late 2024)
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
#[serde(untagged)] #[serde(untagged)]
@@ -98,7 +99,6 @@ where
{ {
let input_versions = Vec::<NodeInputVersions>::deserialize(deserializer)?; let input_versions = Vec::<NodeInputVersions>::deserialize(deserializer)?;
// Convert Vec<NodeOutput> to Vec<NodeInput>
let inputs = input_versions let inputs = input_versions
.into_iter() .into_iter()
.map(|old_input| { .map(|old_input| {
@@ -136,8 +136,9 @@ pub struct DocumentNode {
/// - From other nodes within this graph [`NodeInput::Node`], /// - From other nodes within this graph [`NodeInput::Node`],
/// - A constant value [`NodeInput::Value`], /// - A constant value [`NodeInput::Value`],
/// - A [`NodeInput::Network`] which specifies that this input is from outside the graph, which is resolved in the graph flattening step in the case of nested networks. /// - A [`NodeInput::Network`] which specifies that this input is from outside the graph, which is resolved in the graph flattening step in the case of nested networks.
/// In the root network, it is resolved when evaluating the borrow tree. ///
/// Ensure the click target in the encapsulating network is updated when the inputs cause the node shape to change (currently only when exposing/hiding an input) by using network.update_click_target(node_id). /// In the root network, it is resolved when evaluating the borrow tree.
/// Ensure the click target in the encapsulating network is updated when the inputs cause the node shape to change (currently only when exposing/hiding an input) by using network.update_click_target(node_id).
#[serde(deserialize_with = "deserialize_inputs")] #[serde(deserialize_with = "deserialize_inputs")]
pub inputs: Vec<NodeInput>, pub inputs: Vec<NodeInput>,
/// Manual composition is a way to override the default composition flow of one node into another. /// Manual composition is a way to override the default composition flow of one node into another.
@@ -1214,7 +1215,7 @@ impl NodeNetwork {
for (nested_input_index, nested_input) in nested_node.clone().inputs.iter().enumerate() { for (nested_input_index, nested_input) in nested_node.clone().inputs.iter().enumerate() {
if let NodeInput::Network { import_index, .. } = nested_input { if let NodeInput::Network { import_index, .. } = nested_input {
let parent_input = node.inputs.get(*import_index).expect("Import index should always exist"); let parent_input = node.inputs.get(*import_index).expect(&format!("Import index {} should always exist", import_index));
match *parent_input { match *parent_input {
// If the input to self is a node, connect the corresponding output of the inner network to it // If the input to self is a node, connect the corresponding output of the inner network to it
NodeInput::Node { node_id, output_index, lambda } => { NodeInput::Node { node_id, output_index, lambda } => {
@@ -1240,7 +1241,7 @@ impl NodeNetwork {
// Match the document node input and the exports of the inner network if the export is a NodeInput::Network // Match the document node input and the exports of the inner network if the export is a NodeInput::Network
// for (i, export) in inner_network.exports.iter().enumerate() { // for (i, export) in inner_network.exports.iter().enumerate() {
// if let NodeInput::Network { import_index, .. } = export { // if let NodeInput::Network { import_index, .. } = export {
// let parent_input = node.inputs.get(*import_index).expect("Import index should always exist"); // let parent_input = node.inputs.get(*import_index).expect(&format!("Import index {} should always exist", import_index));
// match *parent_input { // match *parent_input {
// // If the input to self is a node, connect the corresponding output of the inner network to it // // If the input to self is a node, connect the corresponding output of the inner network to it
// NodeInput::Node { node_id, output_index, lambda } => { // NodeInput::Node { node_id, output_index, lambda } => {
+148 -391
View File
@@ -13,11 +13,101 @@ pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use std::hash::Hash; use std::hash::Hash;
pub use std::sync::Arc; pub use std::sync::Arc;
/// A type that is known, allowing serialization (serde::Deserialize is not object safe) /// Macro to generate the tagged value enum.
#[derive(Clone, Debug, PartialEq)] macro_rules! tagged_value {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] ($ ($( #[$meta:meta] )* $identifier:ident ($ty:ty) ),* $(,)?) => {
pub enum TaggedValue { /// A type that is known, allowing serialization (serde::Deserialize is not object safe)
None, #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TaggedValue {
None,
$( $(#[$meta] ) *$identifier( $ty ), )*
RenderOutput(RenderOutput),
SurfaceFrame(graphene_core::SurfaceFrame),
}
// We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below)
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for TaggedValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
Self::None => {}
$( Self::$identifier(x) => {x.hash(state)}),*
Self::RenderOutput(x) => x.hash(state),
Self::SurfaceFrame(x) => x.hash(state),
}
}
}
impl<'a> TaggedValue {
/// Converts to a Box<dyn DynAny> - this isn't very neat but I'm not sure of a better approach
pub fn to_any(self) -> Any<'a> {
match self {
Self::None => Box::new(()),
$( Self::$identifier(x) => Box::new(x), )*
Self::RenderOutput(x) => Box::new(x),
Self::SurfaceFrame(x) => Box::new(x),
}
}
/// Creates a graphene_core::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value
pub fn ty(&self) -> Type {
match self {
Self::None => concrete!(()),
$( Self::$identifier(_) => concrete!($ty), )*
Self::RenderOutput(_) => concrete!(RenderOutput),
Self::SurfaceFrame(_) => concrete!(graphene_core::SurfaceFrame),
}
}
/// Attempts to downcast the dynamic type to a tagged value
pub fn try_from_any(input: Box<dyn DynAny<'a> + 'a>) -> Result<Self, String> {
use dyn_any::downcast;
use std::any::TypeId;
match DynAny::type_id(input.as_ref()) {
x if x == TypeId::of::<()>() => Ok(TaggedValue::None),
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )*
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::SurfaceFrame>() => Ok(TaggedValue::SurfaceFrame(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::WasmSurfaceHandleFrame>() => {
let frame = *downcast::<graphene_core::WasmSurfaceHandleFrame>(input).unwrap();
Ok(TaggedValue::SurfaceFrame(frame.into()))
}
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
}
}
pub fn from_type(input: &Type) -> Self {
match input {
Type::Generic(_) => {
log::warn!("Generic type should be resolved");
TaggedValue::None
}
Type::Concrete(concrete_type) => {
let Some(internal_id) = concrete_type.id else {
return TaggedValue::None;
};
use std::any::TypeId;
// TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types
// Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned.
match internal_id {
x if x == TypeId::of::<()>() => TaggedValue::None,
$( x if x == TypeId::of::<$ty>() => TaggedValue::$identifier(Default::default()), )*
_ => TaggedValue::None,
}
}
Type::Fn(_, output) => TaggedValue::from_type(output),
Type::Future(_) => {
log::warn!("Future type not used");
TaggedValue::None
}
}
}
}
};
}
tagged_value! {
String(String), String(String),
U32(u32), U32(u32),
U64(u64), U64(u64),
@@ -33,8 +123,7 @@ pub enum TaggedValue {
ImaginateCache(ImaginateCache), ImaginateCache(ImaginateCache),
ImageFrame(graphene_core::raster::ImageFrame<Color>), ImageFrame(graphene_core::raster::ImageFrame<Color>),
Color(graphene_core::raster::color::Color), Color(graphene_core::raster::color::Color),
Subpaths(Vec<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>), Subpaths(Vec<bezier_rs::Subpath<graphene_core::vector::PointId>>),
RcSubpath(Arc<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>),
BlendMode(BlendMode), BlendMode(BlendMode),
LuminanceCalculation(LuminanceCalculation), LuminanceCalculation(LuminanceCalculation),
ImaginateSamplingMethod(ImaginateSamplingMethod), ImaginateSamplingMethod(ImaginateSamplingMethod),
@@ -66,7 +155,7 @@ pub enum TaggedValue {
GradientStops(graphene_core::vector::style::GradientStops), GradientStops(graphene_core::vector::style::GradientStops),
Quantization(graphene_core::quantization::QuantizationChannels), Quantization(graphene_core::quantization::QuantizationChannels),
OptionalColor(Option<graphene_core::raster::color::Color>), OptionalColor(Option<graphene_core::raster::color::Color>),
ManipulatorGroupIds(Vec<graphene_core::uuid::ManipulatorGroupId>), PointIds(Vec<graphene_core::vector::PointId>),
Font(graphene_core::text::Font), Font(graphene_core::text::Font),
BrushStrokes(Vec<graphene_core::vector::brush_stroke::BrushStroke>), BrushStrokes(Vec<graphene_core::vector::brush_stroke::BrushStroke>),
BrushCache(BrushCache), BrushCache(BrushCache),
@@ -76,168 +165,14 @@ pub enum TaggedValue {
GraphicElement(graphene_core::GraphicElement), GraphicElement(graphene_core::GraphicElement),
ArtboardGroup(graphene_core::ArtboardGroup), ArtboardGroup(graphene_core::ArtboardGroup),
Curve(graphene_core::raster::curve::Curve), Curve(graphene_core::raster::curve::Curve),
SurfaceFrame(graphene_core::SurfaceFrame),
Footprint(graphene_core::transform::Footprint), Footprint(graphene_core::transform::Footprint),
RenderOutput(RenderOutput),
Palette(Vec<Color>), Palette(Vec<Color>),
VectorModification(graphene_core::vector::VectorModification),
CentroidType(graphene_core::vector::misc::CentroidType), CentroidType(graphene_core::vector::misc::CentroidType),
BooleanOperation(graphene_core::vector::misc::BooleanOperation), BooleanOperation(graphene_core::vector::misc::BooleanOperation),
} }
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for TaggedValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
Self::None => {}
Self::String(x) => x.hash(state),
Self::U32(x) => x.hash(state),
Self::U64(x) => x.hash(state),
Self::F64(x) => x.to_bits().hash(state),
Self::Bool(x) => x.hash(state),
Self::UVec2(x) => x.to_array().iter().for_each(|x| x.hash(state)),
Self::IVec2(x) => x.hash(state),
Self::DVec2(x) => x.to_array().iter().for_each(|x| x.to_bits().hash(state)),
Self::OptionalDVec2(None) => 0.hash(state),
Self::OptionalDVec2(Some(x)) => {
1.hash(state);
Self::DVec2(*x).hash(state)
}
Self::DAffine2(x) => x.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)),
Self::Image(x) => x.hash(state),
Self::ImaginateCache(x) => x.hash(state),
Self::Color(x) => x.hash(state),
Self::Subpaths(x) => x.iter().for_each(|subpath| subpath.hash(state)),
Self::RcSubpath(x) => x.hash(state),
Self::BlendMode(x) => x.hash(state),
Self::LuminanceCalculation(x) => x.hash(state),
Self::ImaginateSamplingMethod(x) => x.hash(state),
Self::ImaginateMaskStartingFill(x) => x.hash(state),
Self::ImaginateController(x) => x.hash(state),
Self::ImageFrame(x) => x.hash(state),
Self::VectorData(x) => x.hash(state),
Self::Fill(x) => x.hash(state),
Self::Stroke(x) => x.hash(state),
Self::F64Array4(x) => x.iter().for_each(|x| x.to_bits().hash(state)),
Self::VecF64(x) => x.iter().for_each(|val| val.to_bits().hash(state)),
Self::VecDVec2(x) => x.iter().for_each(|val| val.to_array().iter().for_each(|x| x.to_bits().hash(state))),
Self::RedGreenBlue(x) => x.hash(state),
Self::RedGreenBlueAlpha(x) => x.hash(state),
Self::NoiseType(x) => x.hash(state),
Self::FractalType(x) => x.hash(state),
Self::CellularDistanceFunction(x) => x.hash(state),
Self::CellularReturnType(x) => x.hash(state),
Self::DomainWarpType(x) => x.hash(state),
Self::RelativeAbsolute(x) => x.hash(state),
Self::SelectiveColorChoice(x) => x.hash(state),
Self::LineCap(x) => x.hash(state),
Self::LineJoin(x) => x.hash(state),
Self::FillType(x) => x.hash(state),
Self::FillChoice(x) => x.hash(state),
Self::Gradient(x) => x.hash(state),
Self::GradientType(x) => x.hash(state),
Self::GradientStops(x) => {
x.0.len().hash(state);
for (position, color) in &x.0 {
position.to_bits().hash(state);
color.hash(state);
}
}
Self::Quantization(x) => x.hash(state),
Self::OptionalColor(x) => x.hash(state),
Self::ManipulatorGroupIds(x) => x.hash(state),
Self::Font(x) => x.hash(state),
Self::BrushStrokes(x) => x.hash(state),
Self::BrushCache(x) => x.hash(state),
Self::Segments(x) => {
for segment in x {
segment.hash(state)
}
}
Self::DocumentNode(x) => x.hash(state),
Self::GraphicGroup(x) => x.hash(state),
Self::GraphicElement(x) => x.hash(state),
Self::ArtboardGroup(x) => x.hash(state),
Self::Curve(x) => x.hash(state),
Self::SurfaceFrame(x) => x.hash(state),
Self::Footprint(x) => x.hash(state),
Self::RenderOutput(x) => x.hash(state),
Self::Palette(x) => x.hash(state),
Self::CentroidType(x) => x.hash(state),
Self::BooleanOperation(x) => x.hash(state),
}
}
}
impl<'a> TaggedValue { impl<'a> TaggedValue {
/// Converts to a Box<dyn DynAny> - this isn't very neat but I'm not sure of a better approach
pub fn to_any(self) -> Any<'a> {
match self {
TaggedValue::None => Box::new(()),
TaggedValue::String(x) => Box::new(x),
TaggedValue::U32(x) => Box::new(x),
TaggedValue::U64(x) => Box::new(x),
TaggedValue::F64(x) => Box::new(x),
TaggedValue::Bool(x) => Box::new(x),
TaggedValue::UVec2(x) => Box::new(x),
TaggedValue::IVec2(x) => Box::new(x),
TaggedValue::DVec2(x) => Box::new(x),
TaggedValue::OptionalDVec2(x) => Box::new(x),
TaggedValue::DAffine2(x) => Box::new(x),
TaggedValue::Image(x) => Box::new(x),
TaggedValue::ImaginateCache(x) => Box::new(x),
TaggedValue::ImageFrame(x) => Box::new(x),
TaggedValue::Color(x) => Box::new(x),
TaggedValue::Subpaths(x) => Box::new(x),
TaggedValue::RcSubpath(x) => Box::new(x),
TaggedValue::BlendMode(x) => Box::new(x),
TaggedValue::LuminanceCalculation(x) => Box::new(x),
TaggedValue::ImaginateSamplingMethod(x) => Box::new(x),
TaggedValue::ImaginateMaskStartingFill(x) => Box::new(x),
TaggedValue::ImaginateController(x) => Box::new(x),
TaggedValue::VectorData(x) => Box::new(x),
TaggedValue::Fill(x) => Box::new(x),
TaggedValue::Stroke(x) => Box::new(x),
TaggedValue::F64Array4(x) => Box::new(x),
TaggedValue::VecF64(x) => Box::new(x),
TaggedValue::VecDVec2(x) => Box::new(x),
TaggedValue::RedGreenBlue(x) => Box::new(x),
TaggedValue::RedGreenBlueAlpha(x) => Box::new(x),
TaggedValue::NoiseType(x) => Box::new(x),
TaggedValue::FractalType(x) => Box::new(x),
TaggedValue::CellularDistanceFunction(x) => Box::new(x),
TaggedValue::CellularReturnType(x) => Box::new(x),
TaggedValue::DomainWarpType(x) => Box::new(x),
TaggedValue::RelativeAbsolute(x) => Box::new(x),
TaggedValue::SelectiveColorChoice(x) => Box::new(x),
TaggedValue::LineCap(x) => Box::new(x),
TaggedValue::LineJoin(x) => Box::new(x),
TaggedValue::FillType(x) => Box::new(x),
TaggedValue::FillChoice(x) => Box::new(x),
TaggedValue::Gradient(x) => Box::new(x),
TaggedValue::GradientType(x) => Box::new(x),
TaggedValue::GradientStops(x) => Box::new(x),
TaggedValue::Quantization(x) => Box::new(x),
TaggedValue::OptionalColor(x) => Box::new(x),
TaggedValue::ManipulatorGroupIds(x) => Box::new(x),
TaggedValue::Font(x) => Box::new(x),
TaggedValue::BrushStrokes(x) => Box::new(x),
TaggedValue::BrushCache(x) => Box::new(x),
TaggedValue::Segments(x) => Box::new(x),
TaggedValue::DocumentNode(x) => Box::new(x),
TaggedValue::GraphicGroup(x) => Box::new(x),
TaggedValue::GraphicElement(x) => Box::new(x),
TaggedValue::ArtboardGroup(x) => Box::new(x),
TaggedValue::Curve(x) => Box::new(x),
TaggedValue::SurfaceFrame(x) => Box::new(x),
TaggedValue::Footprint(x) => Box::new(x),
TaggedValue::RenderOutput(x) => Box::new(x),
TaggedValue::Palette(x) => Box::new(x),
TaggedValue::CentroidType(x) => Box::new(x),
TaggedValue::BooleanOperation(x) => Box::new(x),
}
}
pub fn to_string(&self) -> String { pub fn to_string(&self) -> String {
match self { match self {
TaggedValue::String(x) => x.to_string(), TaggedValue::String(x) => x.to_string(),
@@ -248,7 +183,6 @@ impl<'a> TaggedValue {
_ => panic!("Cannot convert to string"), _ => panic!("Cannot convert to string"),
} }
} }
pub fn to_primitive_string(&self) -> String { pub fn to_primitive_string(&self) -> String {
match self { match self {
TaggedValue::None => "()".to_string(), TaggedValue::None => "()".to_string(),
@@ -262,233 +196,6 @@ impl<'a> TaggedValue {
_ => panic!("Cannot convert to primitive string"), _ => panic!("Cannot convert to primitive string"),
} }
} }
pub fn ty(&self) -> Type {
match self {
TaggedValue::None => concrete!(()),
TaggedValue::String(_) => concrete!(String),
TaggedValue::U32(_) => concrete!(u32),
TaggedValue::U64(_) => concrete!(u64),
TaggedValue::F64(_) => concrete!(f64),
TaggedValue::Bool(_) => concrete!(bool),
TaggedValue::UVec2(_) => concrete!(UVec2),
TaggedValue::IVec2(_) => concrete!(IVec2),
TaggedValue::DVec2(_) => concrete!(DVec2),
TaggedValue::OptionalDVec2(_) => concrete!(Option<DVec2>),
TaggedValue::Image(_) => concrete!(graphene_core::raster::Image<Color>),
TaggedValue::ImaginateCache(_) => concrete!(ImaginateCache),
TaggedValue::ImageFrame(_) => concrete!(graphene_core::raster::ImageFrame<Color>),
TaggedValue::Color(_) => concrete!(graphene_core::raster::Color),
TaggedValue::Subpaths(_) => concrete!(Vec<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>),
TaggedValue::RcSubpath(_) => concrete!(Arc<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>),
TaggedValue::BlendMode(_) => concrete!(BlendMode),
TaggedValue::ImaginateSamplingMethod(_) => concrete!(ImaginateSamplingMethod),
TaggedValue::ImaginateMaskStartingFill(_) => concrete!(ImaginateMaskStartingFill),
TaggedValue::ImaginateController(_) => concrete!(ImaginateController),
TaggedValue::DAffine2(_) => concrete!(DAffine2),
TaggedValue::LuminanceCalculation(_) => concrete!(LuminanceCalculation),
TaggedValue::VectorData(_) => concrete!(graphene_core::vector::VectorData),
TaggedValue::Fill(_) => concrete!(graphene_core::vector::style::Fill),
TaggedValue::Stroke(_) => concrete!(graphene_core::vector::style::Stroke),
TaggedValue::F64Array4(_) => concrete!([f64; 4]),
TaggedValue::VecF64(_) => concrete!(Vec<f64>),
TaggedValue::VecDVec2(_) => concrete!(Vec<DVec2>),
TaggedValue::RedGreenBlue(_) => concrete!(graphene_core::raster::RedGreenBlue),
TaggedValue::RedGreenBlueAlpha(_) => concrete!(graphene_core::raster::RedGreenBlueAlpha),
TaggedValue::NoiseType(_) => concrete!(graphene_core::raster::NoiseType),
TaggedValue::FractalType(_) => concrete!(graphene_core::raster::FractalType),
TaggedValue::CellularDistanceFunction(_) => concrete!(graphene_core::raster::CellularDistanceFunction),
TaggedValue::CellularReturnType(_) => concrete!(graphene_core::raster::CellularReturnType),
TaggedValue::DomainWarpType(_) => concrete!(graphene_core::raster::DomainWarpType),
TaggedValue::RelativeAbsolute(_) => concrete!(graphene_core::raster::RelativeAbsolute),
TaggedValue::SelectiveColorChoice(_) => concrete!(graphene_core::raster::SelectiveColorChoice),
TaggedValue::LineCap(_) => concrete!(graphene_core::vector::style::LineCap),
TaggedValue::LineJoin(_) => concrete!(graphene_core::vector::style::LineJoin),
TaggedValue::FillType(_) => concrete!(graphene_core::vector::style::FillType),
TaggedValue::FillChoice(_) => concrete!(graphene_core::vector::style::FillChoice),
TaggedValue::Gradient(_) => concrete!(graphene_core::vector::style::Gradient),
TaggedValue::GradientType(_) => concrete!(graphene_core::vector::style::GradientType),
TaggedValue::GradientStops(_) => concrete!(graphene_core::vector::style::GradientStops),
TaggedValue::Quantization(_) => concrete!(graphene_core::quantization::QuantizationChannels),
TaggedValue::OptionalColor(_) => concrete!(Option<graphene_core::Color>),
TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec<graphene_core::uuid::ManipulatorGroupId>),
TaggedValue::Font(_) => concrete!(graphene_core::text::Font),
TaggedValue::BrushStrokes(_) => concrete!(Vec<graphene_core::vector::brush_stroke::BrushStroke>),
TaggedValue::BrushCache(_) => concrete!(BrushCache),
TaggedValue::Segments(_) => concrete!(graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>),
TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode),
TaggedValue::GraphicGroup(_) => concrete!(graphene_core::GraphicGroup),
TaggedValue::GraphicElement(_) => concrete!(graphene_core::GraphicElement),
TaggedValue::ArtboardGroup(_) => concrete!(graphene_core::ArtboardGroup),
TaggedValue::Curve(_) => concrete!(graphene_core::raster::curve::Curve),
TaggedValue::SurfaceFrame(_) => concrete!(graphene_core::SurfaceFrame),
TaggedValue::Footprint(_) => concrete!(graphene_core::transform::Footprint),
TaggedValue::RenderOutput(_) => concrete!(RenderOutput),
TaggedValue::Palette(_) => concrete!(Vec<Color>),
TaggedValue::CentroidType(_) => concrete!(graphene_core::vector::misc::CentroidType),
TaggedValue::BooleanOperation(_) => concrete!(graphene_core::vector::misc::BooleanOperation),
}
}
pub fn try_from_any(input: Box<dyn DynAny<'a> + 'a>) -> Result<Self, String> {
use dyn_any::downcast;
use std::any::TypeId;
match DynAny::type_id(input.as_ref()) {
x if x == TypeId::of::<()>() => Ok(TaggedValue::None),
x if x == TypeId::of::<String>() => Ok(TaggedValue::String(*downcast(input).unwrap())),
x if x == TypeId::of::<u32>() => Ok(TaggedValue::U32(*downcast(input).unwrap())),
x if x == TypeId::of::<u64>() => Ok(TaggedValue::U64(*downcast(input).unwrap())),
x if x == TypeId::of::<f64>() => Ok(TaggedValue::F64(*downcast(input).unwrap())),
x if x == TypeId::of::<bool>() => Ok(TaggedValue::Bool(*downcast(input).unwrap())),
x if x == TypeId::of::<UVec2>() => Ok(TaggedValue::UVec2(*downcast(input).unwrap())),
x if x == TypeId::of::<IVec2>() => Ok(TaggedValue::IVec2(*downcast(input).unwrap())),
x if x == TypeId::of::<DVec2>() => Ok(TaggedValue::DVec2(*downcast(input).unwrap())),
x if x == TypeId::of::<Option<DVec2>>() => Ok(TaggedValue::OptionalDVec2(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::Image<Color>>() => Ok(TaggedValue::Image(*downcast(input).unwrap())),
x if x == TypeId::of::<ImaginateCache>() => Ok(TaggedValue::ImaginateCache(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::ImageFrame<Color>>() => Ok(TaggedValue::ImageFrame(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::Color>() => Ok(TaggedValue::Color(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>>() => Ok(TaggedValue::Subpaths(*downcast(input).unwrap())),
x if x == TypeId::of::<Arc<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>>() => Ok(TaggedValue::RcSubpath(*downcast(input).unwrap())),
x if x == TypeId::of::<BlendMode>() => Ok(TaggedValue::BlendMode(*downcast(input).unwrap())),
x if x == TypeId::of::<ImaginateSamplingMethod>() => Ok(TaggedValue::ImaginateSamplingMethod(*downcast(input).unwrap())),
x if x == TypeId::of::<ImaginateMaskStartingFill>() => Ok(TaggedValue::ImaginateMaskStartingFill(*downcast(input).unwrap())),
x if x == TypeId::of::<ImaginateController>() => Ok(TaggedValue::ImaginateController(*downcast(input).unwrap())),
x if x == TypeId::of::<DAffine2>() => Ok(TaggedValue::DAffine2(*downcast(input).unwrap())),
x if x == TypeId::of::<LuminanceCalculation>() => Ok(TaggedValue::LuminanceCalculation(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::VectorData>() => Ok(TaggedValue::VectorData(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::style::Fill>() => Ok(TaggedValue::Fill(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::style::Stroke>() => Ok(TaggedValue::Stroke(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<f64>>() => Ok(TaggedValue::VecF64(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<DVec2>>() => Ok(TaggedValue::VecDVec2(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::RedGreenBlue>() => Ok(TaggedValue::RedGreenBlue(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::RedGreenBlueAlpha>() => Ok(TaggedValue::RedGreenBlueAlpha(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::NoiseType>() => Ok(TaggedValue::NoiseType(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::FractalType>() => Ok(TaggedValue::FractalType(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::CellularDistanceFunction>() => Ok(TaggedValue::CellularDistanceFunction(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::CellularReturnType>() => Ok(TaggedValue::CellularReturnType(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::DomainWarpType>() => Ok(TaggedValue::DomainWarpType(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::RelativeAbsolute>() => Ok(TaggedValue::RelativeAbsolute(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::SelectiveColorChoice>() => Ok(TaggedValue::SelectiveColorChoice(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::style::LineCap>() => Ok(TaggedValue::LineCap(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::style::LineJoin>() => Ok(TaggedValue::LineJoin(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::style::FillType>() => Ok(TaggedValue::FillType(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::style::FillChoice>() => Ok(TaggedValue::FillChoice(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::style::Gradient>() => Ok(TaggedValue::Gradient(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::style::GradientType>() => Ok(TaggedValue::GradientType(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::style::GradientStops>() => Ok(TaggedValue::GradientStops(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::quantization::QuantizationChannels>() => Ok(TaggedValue::Quantization(*downcast(input).unwrap())),
x if x == TypeId::of::<Option<graphene_core::Color>>() => Ok(TaggedValue::OptionalColor(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<graphene_core::uuid::ManipulatorGroupId>>() => Ok(TaggedValue::ManipulatorGroupIds(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::text::Font>() => Ok(TaggedValue::Font(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<graphene_core::vector::brush_stroke::BrushStroke>>() => Ok(TaggedValue::BrushStrokes(*downcast(input).unwrap())),
x if x == TypeId::of::<BrushCache>() => Ok(TaggedValue::BrushCache(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>>() => Ok(TaggedValue::Segments(*downcast(input).unwrap())),
x if x == TypeId::of::<crate::document::DocumentNode>() => Ok(TaggedValue::DocumentNode(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::GraphicGroup>() => Ok(TaggedValue::GraphicGroup(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::GraphicElement>() => Ok(TaggedValue::GraphicElement(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::ArtboardGroup>() => Ok(TaggedValue::ArtboardGroup(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::SurfaceFrame>() => Ok(TaggedValue::SurfaceFrame(*downcast(input).unwrap())),
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::WasmSurfaceHandleFrame>() => {
let frame = *downcast::<graphene_core::WasmSurfaceHandleFrame>(input).unwrap();
Ok(TaggedValue::SurfaceFrame(frame.into()))
}
x if x == TypeId::of::<graphene_core::transform::Footprint>() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<Color>>() => Ok(TaggedValue::Palette(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::misc::CentroidType>() => Ok(TaggedValue::CentroidType(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::misc::BooleanOperation>() => Ok(TaggedValue::BooleanOperation(*downcast(input).unwrap())),
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
}
}
pub fn from_type(input: &Type) -> Self {
match input {
Type::Generic(_) => {
log::warn!("Generic type should be resolved");
TaggedValue::None
}
Type::Concrete(concrete_type) => {
let Some(internal_id) = concrete_type.id else {
return TaggedValue::None;
};
use std::any::TypeId;
// TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types
// Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned.
match internal_id {
x if x == TypeId::of::<()>() => TaggedValue::None,
x if x == TypeId::of::<String>() => TaggedValue::String(Default::default()),
x if x == TypeId::of::<u32>() => TaggedValue::U32(Default::default()),
x if x == TypeId::of::<u64>() => TaggedValue::U64(Default::default()),
x if x == TypeId::of::<f64>() => TaggedValue::F64(Default::default()),
x if x == TypeId::of::<bool>() => TaggedValue::Bool(Default::default()),
x if x == TypeId::of::<UVec2>() => TaggedValue::UVec2(Default::default()),
x if x == TypeId::of::<IVec2>() => TaggedValue::IVec2(Default::default()),
x if x == TypeId::of::<DVec2>() => TaggedValue::DVec2(Default::default()),
x if x == TypeId::of::<Option<DVec2>>() => TaggedValue::OptionalDVec2(Default::default()),
x if x == TypeId::of::<graphene_core::raster::Image<Color>>() => TaggedValue::Image(Default::default()),
x if x == TypeId::of::<ImaginateCache>() => TaggedValue::ImaginateCache(Default::default()),
x if x == TypeId::of::<graphene_core::raster::ImageFrame<Color>>() => TaggedValue::ImageFrame(Default::default()),
x if x == TypeId::of::<graphene_core::raster::Color>() => TaggedValue::Color(Default::default()),
x if x == TypeId::of::<Vec<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>>() => TaggedValue::Subpaths(vec![]),
x if x == TypeId::of::<Arc<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>>() => TaggedValue::None,
x if x == TypeId::of::<BlendMode>() => TaggedValue::BlendMode(Default::default()),
x if x == TypeId::of::<ImaginateSamplingMethod>() => TaggedValue::ImaginateSamplingMethod(Default::default()),
x if x == TypeId::of::<ImaginateMaskStartingFill>() => TaggedValue::ImaginateMaskStartingFill(Default::default()),
x if x == TypeId::of::<ImaginateController>() => TaggedValue::ImaginateController(Default::default()),
x if x == TypeId::of::<DAffine2>() => TaggedValue::DAffine2(Default::default()),
x if x == TypeId::of::<LuminanceCalculation>() => TaggedValue::LuminanceCalculation(Default::default()),
x if x == TypeId::of::<graphene_core::vector::VectorData>() => TaggedValue::VectorData(Default::default()),
x if x == TypeId::of::<graphene_core::vector::style::Fill>() => TaggedValue::Fill(Default::default()),
x if x == TypeId::of::<graphene_core::vector::style::Stroke>() => TaggedValue::Stroke(Default::default()),
x if x == TypeId::of::<Vec<f64>>() => TaggedValue::VecF64(Default::default()),
x if x == TypeId::of::<Vec<DVec2>>() => TaggedValue::VecDVec2(Default::default()),
x if x == TypeId::of::<graphene_core::raster::RedGreenBlue>() => TaggedValue::RedGreenBlue(graphene_core::raster::RedGreenBlue::Red),
x if x == TypeId::of::<graphene_core::raster::RedGreenBlueAlpha>() => TaggedValue::RedGreenBlueAlpha(graphene_core::raster::RedGreenBlueAlpha::Red),
x if x == TypeId::of::<graphene_core::raster::NoiseType>() => TaggedValue::NoiseType(graphene_core::raster::NoiseType::Perlin),
x if x == TypeId::of::<graphene_core::raster::FractalType>() => TaggedValue::FractalType(graphene_core::raster::FractalType::None),
x if x == TypeId::of::<graphene_core::raster::CellularDistanceFunction>() => TaggedValue::CellularDistanceFunction(graphene_core::raster::CellularDistanceFunction::Euclidean),
x if x == TypeId::of::<graphene_core::raster::CellularReturnType>() => TaggedValue::CellularReturnType(graphene_core::raster::CellularReturnType::Nearest),
x if x == TypeId::of::<graphene_core::raster::DomainWarpType>() => TaggedValue::DomainWarpType(graphene_core::raster::DomainWarpType::None),
x if x == TypeId::of::<graphene_core::raster::RelativeAbsolute>() => TaggedValue::RelativeAbsolute(graphene_core::raster::RelativeAbsolute::Relative),
x if x == TypeId::of::<graphene_core::raster::SelectiveColorChoice>() => TaggedValue::SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice::Reds),
x if x == TypeId::of::<graphene_core::vector::style::LineCap>() => TaggedValue::LineCap(graphene_core::vector::style::LineCap::Butt),
x if x == TypeId::of::<graphene_core::vector::style::LineJoin>() => TaggedValue::LineJoin(graphene_core::vector::style::LineJoin::Miter),
x if x == TypeId::of::<graphene_core::vector::style::FillType>() => TaggedValue::FillType(graphene_core::vector::style::FillType::Solid),
x if x == TypeId::of::<graphene_core::vector::style::GradientType>() => TaggedValue::GradientType(Default::default()),
x if x == TypeId::of::<graphene_core::vector::style::GradientStops>() => TaggedValue::GradientStops(Default::default()),
x if x == TypeId::of::<graphene_core::quantization::QuantizationChannels>() => TaggedValue::Quantization(Default::default()),
x if x == TypeId::of::<Option<graphene_core::Color>>() => TaggedValue::OptionalColor(Default::default()),
x if x == TypeId::of::<Vec<graphene_core::uuid::ManipulatorGroupId>>() => TaggedValue::ManipulatorGroupIds(Default::default()),
x if x == TypeId::of::<graphene_core::text::Font>() => TaggedValue::Font(graphene_core::text::Font::new(
graphene_core::consts::DEFAULT_FONT_FAMILY.into(),
graphene_core::consts::DEFAULT_FONT_STYLE.into(),
)),
x if x == TypeId::of::<Vec<graphene_core::vector::brush_stroke::BrushStroke>>() => TaggedValue::BrushStrokes(Default::default()),
x if x == TypeId::of::<BrushCache>() => TaggedValue::BrushCache(Default::default()),
x if x == TypeId::of::<graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>>() => TaggedValue::Segments(Default::default()),
x if x == TypeId::of::<crate::document::DocumentNode>() => TaggedValue::DocumentNode(Default::default()),
x if x == TypeId::of::<graphene_core::GraphicGroup>() => TaggedValue::GraphicGroup(Default::default()),
x if x == TypeId::of::<graphene_core::GraphicElement>() => TaggedValue::GraphicElement(Default::default()),
x if x == TypeId::of::<graphene_core::Artboard>() => TaggedValue::ArtboardGroup(graphene_core::ArtboardGroup::EMPTY),
x if x == TypeId::of::<graphene_core::ArtboardGroup>() => TaggedValue::ArtboardGroup(graphene_core::ArtboardGroup::EMPTY),
x if x == TypeId::of::<graphene_core::SurfaceFrame>() => TaggedValue::None,
x if x == TypeId::of::<RenderOutput>() => TaggedValue::None,
x if x == TypeId::of::<graphene_core::WasmSurfaceHandleFrame>() => TaggedValue::None,
x if x == TypeId::of::<graphene_core::transform::Footprint>() => TaggedValue::Footprint(Default::default()),
x if x == TypeId::of::<Vec<Color>>() => TaggedValue::Palette(Default::default()),
x if x == TypeId::of::<graphene_core::vector::misc::CentroidType>() => TaggedValue::CentroidType(Default::default()),
x if x == TypeId::of::<graphene_core::vector::misc::BooleanOperation>() => TaggedValue::BooleanOperation(Default::default()),
_ => TaggedValue::None,
}
}
Type::Fn(_, output) => TaggedValue::from_type(output),
Type::Future(_) => {
log::warn!("Future type not used");
TaggedValue::None
}
}
}
} }
pub struct UpcastNode { pub struct UpcastNode {
@@ -514,3 +221,53 @@ pub enum RenderOutput {
Svg(String), Svg(String),
Image(Vec<u8>), Image(Vec<u8>),
} }
/// We hash the floats and so-forth despite it not being reproducible because all inputs to the node graph must be hashed otherwise the graph execution breaks (so sorry about this hack)
trait FakeHash {
fn hash<H: core::hash::Hasher>(&self, state: &mut H);
}
mod fake_hash {
use super::*;
impl FakeHash for f64 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_bits().hash(state)
}
}
impl FakeHash for DVec2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl FakeHash for DAffine2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl<X: FakeHash> FakeHash for Option<X> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
if let Some(x) = self {
1.hash(state);
x.hash(state);
} else {
0.hash(state);
}
}
}
impl<X: FakeHash> FakeHash for Vec<X> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.len().hash(state);
self.iter().for_each(|x| x.hash(state))
}
}
impl<T: FakeHash, const N: usize> FakeHash for [T; N] {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.iter().for_each(|x| x.hash(state))
}
}
impl FakeHash for (f64, Color) {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
self.1.hash(state)
}
}
}
@@ -9,7 +9,6 @@ pub struct Compiler {}
impl Compiler { impl Compiler {
pub fn compile(&self, mut network: NodeNetwork) -> Result<impl Iterator<Item = ProtoNetwork>, String> { pub fn compile(&self, mut network: NodeNetwork) -> Result<impl Iterator<Item = ProtoNetwork>, String> {
println!("flattening");
let node_ids = network.nodes.keys().copied().collect::<Vec<_>>(); let node_ids = network.nodes.keys().copied().collect::<Vec<_>>();
for id in node_ids { for id in node_ids {
network.flatten(id); network.flatten(id);
+2 -2
View File
@@ -605,7 +605,8 @@ impl core::fmt::Debug for GraphErrorType {
"Node graph type error! If this just appeared while editing the graph,\n\ "Node graph type error! If this just appeared while editing the graph,\n\
consider using undo to go back and try another way to connect the nodes.\n\ consider using undo to go back and try another way to connect the nodes.\n\
\n\ \n\
No node implementation exists for type ({parameters}).\n\ No node implementation exists for type:\n\
({parameters})\n\
\n\ \n\
Caused by{}:\n\ Caused by{}:\n\
{}", {}",
@@ -788,7 +789,6 @@ impl TypingContext {
match valid_impls.as_slice() { match valid_impls.as_slice() {
[] => { [] => {
dbg!(&self.inferred);
let mut best_errors = usize::MAX; let mut best_errors = usize::MAX;
let mut error_inputs = Vec::new(); let mut error_inputs = Vec::new();
for node_io in impls.keys() { for node_io in impls.keys() {
+2 -3
View File
@@ -2,7 +2,6 @@ use crate::Node;
use bezier_rs::{ManipulatorGroup, Subpath}; use bezier_rs::{ManipulatorGroup, Subpath};
use graphene_core::transform::Footprint; use graphene_core::transform::Footprint;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::misc::BooleanOperation; use graphene_core::vector::misc::BooleanOperation;
pub use graphene_core::vector::*; pub use graphene_core::vector::*;
@@ -69,10 +68,10 @@ fn from_svg_string(svg_string: &str) -> VectorData {
return VectorData::empty(); return VectorData::empty();
}; };
VectorData::from_subpaths(convert_usvg_path(path)) VectorData::from_subpaths(convert_usvg_path(path), false)
} }
pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<ManipulatorGroupId>> { pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
let mut subpaths = Vec::new(); let mut subpaths = Vec::new();
let mut groups = Vec::new(); let mut groups = Vec::new();
+9 -9
View File
@@ -243,7 +243,7 @@ pub struct DrawImageFrameNode<Surface> {
} }
#[node_macro::node_fn(DrawImageFrameNode)] #[node_macro::node_fn(DrawImageFrameNode)]
async fn draw_image_frame_node<'a: 'input>(image: ImageFrame<SRGBA8>, surface_handle: Arc<SurfaceHandle<HtmlCanvasElement>>) -> SurfaceHandleFrame<HtmlCanvasElement> { async fn draw_image_frame_node<'a: 'input>(image: ImageFrame<SRGBA8>, surface_handle: Arc<WasmSurfaceHandle>) -> SurfaceHandleFrame<HtmlCanvasElement> {
let image_data = image.image.data; let image_data = image.image.data;
let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice())); let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice()));
if image.image.width > 0 && image.image.height > 0 { if image.image.width > 0 && image.image.height > 0 {
@@ -290,9 +290,9 @@ fn decode_image_node<'a: 'input>(data: Arc<[u8]>) -> ImageFrame<Color> {
pub use graph_craft::document::value::RenderOutput; pub use graph_craft::document::value::RenderOutput;
pub struct RenderNode<Data, Surface, Parameter> { pub struct RenderNode<Data, Surface, Parameter> {
data: Data, data: Data,
#[cfg(any(feature = "resvg", feature = "vello"))] #[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
surface_handle: Surface, surface_handle: Surface,
#[cfg(not(any(feature = "resvg", feature = "vello")))] #[cfg(not(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32")))]
surface_handle: PhantomData<Surface>, surface_handle: PhantomData<Surface>,
parameter: PhantomData<Parameter>, parameter: PhantomData<Parameter>,
} }
@@ -426,7 +426,7 @@ impl<'input, 'a: 'input, T: 'input + GraphicElementRendered, F: 'input + Future<
where where
Data: Node<'input, Footprint, Output = F>, Data: Node<'input, Footprint, Output = F>,
Surface: Node<'input, (), Output = SurfaceFuture>, Surface: Node<'input, (), Output = SurfaceFuture>,
SurfaceFuture: core::future::Future<Output = Arc<SurfaceHandle<HtmlCanvasElement>>>, SurfaceFuture: core::future::Future<Output = Arc<SurfaceHandle<<crate::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>>,
{ {
type Output = core::pin::Pin<Box<dyn core::future::Future<Output = RenderOutput> + 'input>>; type Output = core::pin::Pin<Box<dyn core::future::Future<Output = RenderOutput> + 'input>>;
@@ -441,7 +441,7 @@ where
let output_format = editor.render_config.export_format; let output_format = editor.render_config.export_format;
match output_format { match output_format {
ExportFormat::Svg => render_svg(self.data.eval(footprint).await, SvgRender::new(), render_params, footprint), ExportFormat::Svg => render_svg(self.data.eval(footprint).await, SvgRender::new(), render_params, footprint),
#[cfg(any(feature = "resvg", feature = "vello"))] #[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
ExportFormat::Canvas => render_canvas(self.data.eval(footprint).await, SvgRender::new(), render_params, footprint, editor, self.surface_handle.eval(()).await), ExportFormat::Canvas => render_canvas(self.data.eval(footprint).await, SvgRender::new(), render_params, footprint, editor, self.surface_handle.eval(()).await),
_ => todo!("Non-SVG render output for {output_format:?}"), _ => todo!("Non-SVG render output for {output_format:?}"),
} }
@@ -455,7 +455,7 @@ impl<'input, 'a: 'input, T: 'input + GraphicElementRendered, F: 'input + Future<
where where
Data: Node<'input, (), Output = F>, Data: Node<'input, (), Output = F>,
Surface: Node<'input, (), Output = SurfaceFuture>, Surface: Node<'input, (), Output = SurfaceFuture>,
SurfaceFuture: core::future::Future<Output = Arc<SurfaceHandle<HtmlCanvasElement>>>, SurfaceFuture: core::future::Future<Output = Arc<SurfaceHandle<<crate::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>>,
{ {
type Output = core::pin::Pin<Box<dyn core::future::Future<Output = RenderOutput> + 'input>>; type Output = core::pin::Pin<Box<dyn core::future::Future<Output = RenderOutput> + 'input>>;
#[inline] #[inline]
@@ -469,7 +469,7 @@ where
let output_format = editor.render_config.export_format; let output_format = editor.render_config.export_format;
match output_format { match output_format {
ExportFormat::Svg => render_svg(self.data.eval(()).await, SvgRender::new(), render_params, footprint), ExportFormat::Svg => render_svg(self.data.eval(()).await, SvgRender::new(), render_params, footprint),
#[cfg(any(feature = "resvg", feature = "vello"))] #[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
ExportFormat::Canvas => render_canvas(self.data.eval(()).await, SvgRender::new(), render_params, footprint, editor, self.surface_handle.eval(()).await), ExportFormat::Canvas => render_canvas(self.data.eval(()).await, SvgRender::new(), render_params, footprint, editor, self.surface_handle.eval(()).await),
_ => todo!("Non-SVG render output for {output_format:?}"), _ => todo!("Non-SVG render output for {output_format:?}"),
} }
@@ -481,9 +481,9 @@ impl<Data, Surface, Parameter> RenderNode<Data, Surface, Parameter> {
pub fn new(data: Data, surface_handle: Surface) -> Self { pub fn new(data: Data, surface_handle: Surface) -> Self {
Self { Self {
data, data,
#[cfg(any(feature = "resvg", feature = "vello"))] #[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
surface_handle, surface_handle,
#[cfg(not(any(feature = "resvg", feature = "vello")))] #[cfg(not(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32")))]
surface_handle: PhantomData, surface_handle: PhantomData,
parameter: PhantomData, parameter: PhantomData,
} }
@@ -347,7 +347,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicGroup, fn_params: [Footprint => graphene_core::GraphicGroup]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicGroup, fn_params: [Footprint => graphene_core::GraphicGroup]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicElement, fn_params: [Footprint => graphene_core::GraphicElement]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicElement, fn_params: [Footprint => graphene_core::GraphicElement]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: Artboard, fn_params: [Footprint => graphene_core::Artboard]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: Artboard, fn_params: [Footprint => Artboard]),
async_node!(graphene_std::wasm_application_io::LoadResourceNode<_>, input: WasmEditorApi, output: Arc<[u8]>, params: [String]), async_node!(graphene_std::wasm_application_io::LoadResourceNode<_>, input: WasmEditorApi, output: Arc<[u8]>, params: [String]),
register_node!(graphene_std::wasm_application_io::DecodeImageNode, input: Arc<[u8]>, params: []), register_node!(graphene_std::wasm_application_io::DecodeImageNode, input: Arc<[u8]>, params: []),
async_node!(graphene_std::wasm_application_io::CreateSurfaceNode, input: WasmEditorApi, output: Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>, params: []), async_node!(graphene_std::wasm_application_io::CreateSurfaceNode, input: WasmEditorApi, output: Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>, params: []),
@@ -522,7 +522,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
} }
}) })
}, },
NodeIOTypes::new(concrete!(ImageFrame<Color>), concrete!(ImageFrame<Color>), vec![fn_type!(f32), fn_type!(f32), fn_type!(bool)]), NodeIOTypes::new(concrete!(ImageFrame<Color>), concrete!(ImageFrame<Color>), vec![fn_type!(f64), fn_type!(f64), fn_type!(bool)]),
)], )],
vec![ vec![
( (
@@ -666,7 +666,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: ImageFrame<Color>, params: [ImageFrame<Color>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: ImageFrame<Color>, params: [ImageFrame<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: QuantizationChannels, params: [QuantizationChannels]), async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: QuantizationChannels, params: [QuantizationChannels]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: Vec<DVec2>, params: [Vec<DVec2>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: Vec<DVec2>, params: [Vec<DVec2>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: Arc<WasmSurfaceHandle>, params: [Arc<WasmSurfaceHandle>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>, params: [Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: ShaderInputFrame<WgpuExecutor>, params: [ShaderInputFrame<WgpuExecutor>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: ShaderInputFrame<WgpuExecutor>, params: [ShaderInputFrame<WgpuExecutor>]),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
@@ -683,23 +683,23 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::quantization::QuantizeNode<_>, input: Color, params: [QuantizationChannels]), register_node!(graphene_core::quantization::QuantizeNode<_>, input: Color, params: [QuantizationChannels]),
register_node!(graphene_core::quantization::DeQuantizeNode<_>, input: PackedPixel, params: [QuantizationChannels]), register_node!(graphene_core::quantization::DeQuantizeNode<_>, input: PackedPixel, params: [QuantizationChannels]),
register_node!(graphene_core::ops::CloneNode<_>, input: &QuantizationChannels, params: []), register_node!(graphene_core::ops::CloneNode<_>, input: &QuantizationChannels, params: []),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ImageFrame<Color>, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ImageFrame<Color>, () => Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => VectorData, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => VectorData, () => Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => GraphicGroup, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => GraphicGroup, () => Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Artboard, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Artboard, () => Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ArtboardGroup, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => ArtboardGroup, () => Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option<Color>, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Option<Color>, () => Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec<Color>, () => Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, fn_params: [Footprint => Vec<Color>, () => Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [ImageFrame<Color>, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [ImageFrame<Color>, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [VectorData, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [VectorData, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [GraphicGroup, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [GraphicGroup, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [Artboard, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [Artboard, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [bool, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [bool, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [f32, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [f32, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [f64, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [f64, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [String, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [String, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [Option<Color>, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [Option<Color>, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [Vec<Color>, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RenderNode<_, _, _>, input: WasmEditorApi, output: RenderOutput, params: [Vec<Color>, Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>]),
async_node!(graphene_std::wasm_application_io::RasterizeNode<_, _>, input: VectorData, output: ImageFrame<Color>, params: [Footprint, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RasterizeNode<_, _>, input: VectorData, output: ImageFrame<Color>, params: [Footprint, Arc<WasmSurfaceHandle>]),
async_node!(graphene_std::wasm_application_io::RasterizeNode<_, _>, input: GraphicGroup, output: ImageFrame<Color>, params: [Footprint, Arc<WasmSurfaceHandle>]), async_node!(graphene_std::wasm_application_io::RasterizeNode<_, _>, input: GraphicGroup, output: ImageFrame<Color>, params: [Footprint, Arc<WasmSurfaceHandle>]),
async_node!(graphene_core::transform::TransformNode<_, _, _, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, () => DVec2, () => f64, () => DVec2, () => DVec2, () => DVec2]), async_node!(graphene_core::transform::TransformNode<_, _, _, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, () => DVec2, () => f64, () => DVec2, () => DVec2, () => DVec2]),
@@ -800,9 +800,10 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::vector::generator_nodes::SplineGenerator<_>, input: (), params: [Vec<DVec2>]), register_node!(graphene_core::vector::generator_nodes::SplineGenerator<_>, input: (), params: [Vec<DVec2>]),
register_node!( register_node!(
graphene_core::vector::generator_nodes::PathGenerator<_>, graphene_core::vector::generator_nodes::PathGenerator<_>,
input: Vec<graphene_core::vector::bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>, input: Vec<graphene_core::vector::bezier_rs::Subpath<graphene_core::vector::PointId>>,
params: [Vec<graphene_core::uuid::ManipulatorGroupId>] params: [Vec<graphene_core::vector::PointId>]
), ),
register_node!(graphene_core::vector::PathModify<_>, input: VectorData, params: [graphene_core::vector::VectorModification]),
register_node!(graphene_core::text::TextGeneratorNode<_, _, _>, input: WasmEditorApi, params: [String, graphene_core::text::Font, f64]), register_node!(graphene_core::text::TextGeneratorNode<_, _, _>, input: WasmEditorApi, params: [String, graphene_core::text::Font, f64]),
register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []), register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []),
register_node!(graphene_core::ExtractImageFrame, input: WasmEditorApi, params: []), register_node!(graphene_core::ExtractImageFrame, input: WasmEditorApi, params: []),
+4 -4
View File
@@ -33,14 +33,14 @@ use syn::{
/// ///
/// - Mapping the type of the function's first argument (the node's primary input) to the impl'd `Node`'s generic type, e.g.: /// - Mapping the type of the function's first argument (the node's primary input) to the impl'd `Node`'s generic type, e.g.:
/// ///
/// ``` /// ```ignore
/// Node<'input, Color> /// Node<'input, Color>
/// ``` /// ```
/// ///
/// for a `Color` primary input type. /// for a `Color` primary input type.
/// - Mapping the type of the function's remaining arguments (the node's secondary inputs) to the given struct fields' generic types, e.g.: /// - Mapping the type of the function's remaining arguments (the node's secondary inputs) to the given struct fields' generic types, e.g.:
/// ///
/// ``` /// ```ignore
/// TheGivenStruct<S0, S1> /// TheGivenStruct<S0, S1>
/// where S0: Node<'input, (), Output = f64>, /// where S0: Node<'input, (), Output = f64>,
/// where S1: Node<'input, (), Output = f64>, /// where S1: Node<'input, (), Output = f64>,
@@ -49,7 +49,7 @@ use syn::{
/// for two `f64` parameter (secondary input) types. Since Graphene works by having each function evaluate its upstream node as a lambda that returns output data, these secondary inputs are not directly `f64` values but rather `Node`s that output `f64` values when evaluated (in this case, with an empty input of `()`). /// for two `f64` parameter (secondary input) types. Since Graphene works by having each function evaluate its upstream node as a lambda that returns output data, these secondary inputs are not directly `f64` values but rather `Node`s that output `f64` values when evaluated (in this case, with an empty input of `()`).
/// - Mapping the function's return type to the impl'd `Node` trait's associated type, e.g.: /// - Mapping the function's return type to the impl'd `Node` trait's associated type, e.g.:
/// ///
/// ``` /// ```ignore
/// Output = Color /// Output = Color
/// ``` /// ```
/// ///
@@ -67,7 +67,7 @@ use syn::{
/// ///
/// The function body runs with the actual primary input value from the `eval` method's argument and the secondary input values from the `eval` method's `let` declarations. The result looks like this: /// The function body runs with the actual primary input value from the `eval` method's argument and the secondary input values from the `eval` method's `let` declarations. The result looks like this:
/// ///
/// ``` /// ```ignore
/// fn eval(&'input self, color: Color) -> Self::Output { /// fn eval(&'input self, color: Color) -> Self::Output {
/// let secondaryA = self.secondaryA.eval(()); /// let secondaryA = self.secondaryA.eval(());
/// let secondaryB = self.secondaryB.eval(()); /// let secondaryB = self.secondaryB.eval(());
+2
View File
@@ -1,3 +1,5 @@
#![allow(unused)]
mod as_message; mod as_message;
mod combined_message_attrs; mod combined_message_attrs;
mod discriminant; mod discriminant;
+25 -25
View File
@@ -125,62 +125,58 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 8" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 8" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Editable nested node subgraphs</span> <span>Editable nested node subgraphs</span>
</div> </div>
<div class="feature-icon complete" title="Development Complete">
<img class="atlas" style="--atlas-index: 10" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Procedurally alterable vector data</span>
</div>
<div class="feature-icon ongoing" title="Development Ongoing"> <div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 0" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 0" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Imaginate (Stable Diffusion node/tool)</span> <span>Imaginate (Stable Diffusion node/tool)</span>
</div> </div>
<div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 13" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>New vector 2D renderer (with <a target="_blank" href="https://github.com/linebender/vello">Vello</a>)</span>
</div>
<div class="feature-icon ongoing" title="Development Ongoing"> <div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 12" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 12" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>GPU-accelerated raster rendering</span> <span>GPU-accelerated raster rendering</span>
</div> </div>
<div class="feature-icon ongoing" title="Development Ongoing"> <div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 41" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 19" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Timeline with animation channels</span> <span>Imported RAW photo processing</span>
</div> </div>
<div class="feature-icon ongoing" title="Development Ongoing"> <div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 26" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 26" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Interactive graph auto-layout</span> <span>Interactive graph auto-layout</span>
</div> </div>
<div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 19" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Imported RAW photo processing</span>
</div>
<div class="feature-icon ongoing" title="Development Ongoing"> <div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 14" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 14" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Adaptive resolution system</span> <span>Adaptive resolution system</span>
</div> </div>
<div class="feature-icon ongoing" title="Development Ongoing"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 49" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 41" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>AI nodes and tools (e.g. magic wand)</span> <span>Timeline with animation channels</span>
</div>
<div class="feature-icon ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 10" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Procedurally alterable vector data</span>
</div> </div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 7" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 7" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Native desktop app (with <a target="_blank" href="https://tauri.app/">Tauri</a>)</span> <span>Native desktop app (with <a target="_blank" href="https://tauri.app/">Tauri</a>)</span>
</div> </div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 13" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 54" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>New vector 2D renderer (with <a target="_blank" href="https://github.com/linebender/vello">Vello</a>)</span> <span>Local file browser for saving/loading</span>
</div> </div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 5" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 5" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Fully-supported brush tool</span> <span>Fully-supported brush tool</span>
</div> </div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Select mode (marquee masking)</span>
</div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 9" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 9" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Graph data attribute spreadsheet</span> <span>Graph data attribute spreadsheet</span>
</div> </div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 54" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Local file browser for saving/loading</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 53" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Local fonts access</span>
</div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 17" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 17" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Stable document format</span> <span>Stable document format</span>
@@ -190,8 +186,12 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<h3>— Alpha 4 —</h3> <h3>— Alpha 4 —</h3>
</div> </div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 49" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Select mode (marquee masking)</span> <span>AI nodes and tools (e.g. magic wand)</span>
</div>
<div class="feature-icon">
<img class="atlas" style="--atlas-index: 53" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Local fonts access</span>
</div> </div>
<div class="feature-icon"> <div class="feature-icon">
<img class="atlas" style="--atlas-index: 52" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" /> <img class="atlas" style="--atlas-index: 52" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />