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
+2 -2
View File
@@ -21,8 +21,8 @@ impl Editor {
std::mem::take(&mut self.dispatcher.responses)
}
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) {
self.dispatcher.poll_node_graph_evaluation(responses);
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> Result<(), String> {
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 HIDE_HANDLE_DISTANCE: f64 = 3.;
pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.;
pub const INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE: f64 = 5.;
// Pen tool
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
+14 -9
View File
@@ -205,8 +205,8 @@ impl Dispatcher {
list
}
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) {
self.message_handlers.portfolio_message_handler.poll_node_graph_evaluation(responses);
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)
}
/// 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::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::ToolType;
use crate::test_utils::EditorTestUtils;
use graph_craft::document::NodeId;
use graphene_core::raster::color::Color;
fn init_logger() {
@@ -412,11 +409,9 @@ mod test {
assert_eq!(layers_after_copy[5], shape_id);
}
// TODO: Fix text
#[ignore]
#[test]
#[tokio::test]
/// 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::*;
let print_problem_to_terminal_on_failure = |value: &String| {
@@ -449,6 +444,16 @@ mod test {
document_name: document_name.into(),
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 {
// 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")]
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,
TriggerVisitLink {
url: String,
@@ -1236,32 +1236,27 @@ impl DocumentMessageHandler {
}
/// 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> {
self.click_xray(viewport_location).filter(|&layer| !is_artboard(layer, network)).collect::<Vec<_>>()
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(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.
pub fn click_list(&self, viewport_location: DVec2, network: &NodeNetwork) -> Vec<LayerNodeIdentifier> {
let mut node_list = self.click_list_any(viewport_location, network);
node_list.truncate(
node_list
.iter()
.position(|&layer| {
if layer != LayerNodeIdentifier::ROOT_PARENT {
!network.nodes.get(&layer.to_node()).map(|node| node.layer_has_child_layers(network)).unwrap_or_default()
} else {
log::error!("ROOT_PARENT should not exist in click_list_any");
false
}
})
.unwrap_or(0) + 1,
);
node_list
pub fn click_list<'a>(&'a self, viewport_location: DVec2, network: &'a NodeNetwork) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
self.click_xray_no_artboards(viewport_location, network)
.skip_while(|&layer| layer == LayerNodeIdentifier::ROOT_PARENT)
.scan(true, |last_had_children, layer| {
if *last_had_children {
*last_had_children = network.nodes.get(&layer.to_node()).map_or(false, |node| node.layer_has_child_layers(network));
Some(layer)
} else {
None
}
})
}
/// 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> {
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
@@ -1,5 +1,4 @@
use super::utility_types::TransformIn;
use super::utility_types::VectorDataModification;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
@@ -7,9 +6,10 @@ use bezier_rs::Subpath;
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::PointId;
use graphene_core::vector::VectorModificationType;
use graphene_core::{Artboard, Color};
use graphene_std::vector::misc::BooleanOperation;
@@ -77,11 +77,6 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
blend_mode: BlendMode,
},
UpdateBounds {
layer: LayerNodeIdentifier,
old_bounds: [DVec2; 2],
new_bounds: [DVec2; 2],
},
StrokeSet {
layer: LayerNodeIdentifier,
stroke: Stroke,
@@ -104,7 +99,7 @@ pub enum GraphOperationMessage {
},
Vector {
layer: LayerNodeIdentifier,
modification: VectorDataModification,
modification_type: VectorModificationType,
},
Brush {
layer: LayerNodeIdentifier,
@@ -129,7 +124,7 @@ pub enum GraphOperationMessage {
},
NewVectorLayer {
id: NodeId,
subpaths: Vec<Subpath<ManipulatorGroupId>>,
subpaths: Vec<Subpath<PointId>>,
parent: LayerNodeIdentifier,
insert_index: isize,
},
@@ -1,4 +1,4 @@
use super::transform_utils::{self, LayerBounds};
use super::transform_utils;
use super::utility_types::ModifyInputsContext;
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};
@@ -9,10 +9,9 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork, Previewing};
use graphene_core::renderer::Quad;
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_std::vector::convert_usvg_path;
use graphene_std::vector::style::GradientStops;
use glam::{DAffine2, DVec2, IVec2};
@@ -404,15 +403,6 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
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 } => {
if layer == LayerNodeIdentifier::ROOT_PARENT {
log::error!("Cannot run StrokeSet on ROOT_PARENT");
@@ -433,9 +423,8 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
return;
}
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) {
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 {
@@ -451,9 +440,8 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
let parent_transform = document_metadata.downstream_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) {
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 } => {
@@ -461,21 +449,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
log::error!("Cannot run TransformSetPivot on ROOT_PARENT");
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) {
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 {
log::error!("Cannot run Vector on ROOT_PARENT");
return;
}
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);
if let Some(layer) = previous_layer {
responses.add(GraphOperationMessage::DeleteLayer { layer, reconnect: true })
}
modify_inputs.vector_modify(modification_type);
}
}
GraphOperationMessage::Brush { layer, strokes } => {
@@ -698,6 +682,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
GraphOperationMessage::SetName { layer, name } => {
responses.add(DocumentMessage::StartTransaction);
responses.add(GraphOperationMessage::SetNameImpl { layer, name });
responses.add(NodeGraphMessage::RunDocumentGraph);
}
GraphOperationMessage::SetNameImpl { layer, name } => {
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();
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| {
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 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::{ManipulatorGroup, Subpath};
use bezier_rs::Subpath;
use graph_craft::document::{value::TaggedValue, NodeInput};
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use graphene_core::vector::PointId;
use glam::{DAffine2, DVec2};
use super::utility_types::VectorDataModification;
/// 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) {
let x_axis = transform.matrix2.x_axis;
@@ -53,7 +48,10 @@ pub struct LayerBounds {
impl LayerBounds {
/// 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 {
bounds: metadata.nonzero_bounding_box(layer),
bounds_transform: DAffine2::IDENTITY,
@@ -184,7 +182,7 @@ fn clamp_bounds(bounds_min: DVec2, mut bounds_max: DVec2) -> [DVec2; 2] {
[bounds_min, bounds_max]
}
/// Returns corners of all subpaths
fn subpath_bounds(subpaths: &[Subpath<ManipulatorGroupId>]) -> [DVec2; 2] {
fn subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
subpaths
.iter()
.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)
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);
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::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
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 graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Type;
use graphene_core::{Artboard, Color};
use graphene_std::vector::ManipulatorPointId;
use graphene_core::vector::{PointId, VectorModificationType};
use graphene_core::{Artboard, Color, Type};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
use interpreted_executor::node_registry::NODE_REGISTRY;
@@ -29,22 +27,6 @@ pub enum TransformIn {
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.
/// NodeGraphMessage or GraphOperationMessage cannot be added in ModifyInputsContext, since the functions are called by both messages handlers
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)
}
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 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())
@@ -453,10 +435,9 @@ impl<'a> ModifyInputsContext<'a> {
}
}
/// Changes the inputs of a specific node
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
.document_network
/// Find a node id as part of the layer
fn existing_node_id(&mut self, name: &'static str) -> Option<NodeId> {
self.document_network
.upstream_flow_back_from_nodes(
self.layer_node.map_or_else(
|| {
@@ -471,7 +452,20 @@ impl<'a> ModifyInputsContext<'a> {
graph_craft::document::FlowType::HorizontalFlow,
)
.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 {
self.modify_existing_node_inputs(node_id, update_input);
} 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) {
self.modify_inputs("Transform", skip_rerender, |inputs, node_id, metadata| {
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| {
let layer_transform = transform_utils::get_current_transform(inputs);
let upstream_transform = metadata.upstream_transform(node_id);
let to = match transform_in {
TransformIn::Local => DAffine2::IDENTITY,
TransformIn::Scope { scope } => scope * 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 = pivot.inverse() * to.inverse() * transform * to * pivot * layer_transform;
let transform = to.inverse() * transform * to * layer_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| {
let upstream_transform = metadata.upstream_transform(node_id);
@@ -649,7 +641,6 @@ impl<'a> ModifyInputsContext<'a> {
TransformIn::Scope { scope } => scope * 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
.filter(|transform| transform.matrix2.determinant() != 0. && upstream_transform.matrix2.determinant() != 0.)
@@ -657,79 +648,29 @@ impl<'a> ModifyInputsContext<'a> {
{
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);
});
}
pub fn pivot_set(&mut self, new_pivot: DVec2, bounds: LayerBounds) {
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);
pub fn pivot_set(&mut self, new_pivot: DVec2) {
self.modify_inputs("Transform", false, |inputs, _node_id, _metadata| {
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]) {
self.modify_all_node_inputs("Transform", false, |inputs, node_id, metadata| {
let upstream_transform = metadata.upstream_transform(node_id);
let layer_transform = transform_utils::get_current_transform(inputs);
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),
pub fn vector_modify(&mut self, modification_type: VectorModificationType) {
self.modify_inputs("Path", false, |inputs, _node_id, _metadata| {
let [_, NodeInput::Value {
tagged_value: TaggedValue::VectorModification(modification),
..
} = subpaths
}] = inputs.as_mut_slice()
else {
return;
};
let NodeInput::Value {
tagged_value: TaggedValue::ManipulatorGroupIds(colinear_manipulators),
..
} = colinear_manipulators
else {
return;
panic!("Path node does not have modification input");
};
[old_bounds_min, old_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths);
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);
modification.modify(&modification_type);
});
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>) {
@@ -761,20 +761,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentInputType::value("Dimensions", TaggedValue::UVec2((512, 512).into()), false),
DocumentInputType::value("Seed", TaggedValue::U32(0), 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
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),
// 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 Lacunarity", TaggedValue::F64(2.), 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 Ping Pong Strength", TaggedValue::F64(2.), false),
// Cellular
DocumentInputType::value("Cellular Distance Function", TaggedValue::CellularDistanceFunction(CellularDistanceFunction::Euclidean), false),
DocumentInputType::value("Cellular Return Type", TaggedValue::CellularReturnType(CellularReturnType::Nearest), false),
DocumentInputType::value("Cellular Distance Function", TaggedValue::CellularDistanceFunction(CellularDistanceFunction::default()), false),
DocumentInputType::value("Cellular Return Type", TaggedValue::CellularReturnType(CellularReturnType::default()), false),
DocumentInputType::value("Cellular Jitter", TaggedValue::F64(1.), false),
],
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
@@ -802,7 +802,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
inputs: vec![
DocumentInputType::value("Image", 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)],
properties: node_properties::insert_channel_properties,
@@ -935,7 +935,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
name: "Color Channel",
category: "Raster",
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)],
properties: node_properties::color_channel_properties,
..Default::default()
@@ -944,7 +944,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
name: "Blend Mode Value",
category: "Inputs",
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)],
properties: node_properties::blend_mode_value_properties,
..Default::default()
@@ -955,7 +955,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
implementation: DocumentNodeImplementation::proto("graphene_core::raster::LuminanceNode<_>"),
inputs: vec![
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)],
properties: node_properties::luminance_properties,
@@ -967,7 +967,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
implementation: DocumentNodeImplementation::proto("graphene_core::raster::ExtractChannelNode<_>"),
inputs: vec![
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)],
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) Constant", TaggedValue::F64(0.), false),
// 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)],
properties: node_properties::adjust_channel_mixer_properties,
@@ -1879,7 +1879,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
inputs: vec![
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
// Mode
DocumentInputType::value("Mode", TaggedValue::RelativeAbsolute(RelativeAbsolute::Relative), false),
DocumentInputType::value("Mode", TaggedValue::RelativeAbsolute(RelativeAbsolute::default()), false),
// Reds
DocumentInputType::value("(Reds) Cyan", 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) Black", TaggedValue::F64(0.), false),
// 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)],
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<_>"),
inputs: vec![
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)],
..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 {
name: "Sample",
category: "Structural",
@@ -2489,8 +2519,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
DocumentInputType::value("Weight", TaggedValue::F64(0.), false),
DocumentInputType::value("Dash Lengths", TaggedValue::VecF64(Vec::new()), 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 Join", TaggedValue::LineJoin(graphene_core::vector::style::LineJoin::Miter), 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::default()), false),
DocumentInputType::value("Miter Limit", TaggedValue::F64(4.), false),
],
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::{DocumentNode, DocumentNodeImplementation, FlowType, NodeId, NodeInput, NodeNetwork, Previewing, Source};
use graph_craft::proto::GraphErrors;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::*;
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
use renderer::{ClickTarget, Quad};
use vector::PointId;
use glam::{DAffine2, DVec2, IVec2, UVec2};
use renderer::{ClickTarget, Quad};
use web_sys::window;
#[derive(Debug)]
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.
pub node_metadata: HashMap<NodeId, NodeMetadata>,
/// 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,
}
@@ -1548,8 +1547,15 @@ impl NodeGraphMessageHandler {
common
}
#[cfg(not(target_arch = "wasm32"))]
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") {
Ok(div) => div,
Err(err) => {
@@ -1594,6 +1600,7 @@ impl NodeGraphMessageHandler {
Some(text_width)
}
pub fn layer_width_cells(node: &DocumentNode) -> u32 {
let half_grid_cell_offset = 24. / 2.;
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> {
let translation_assist = |widgets: &mut Vec<WidgetHolder>| {
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 translation = vec2_widget(document_node, node_id, 1, "Translation", "X", "Y", " px", None, add_blank_assist);
let rotation = {
let index = 2;
@@ -18,7 +18,7 @@ impl FrontendGraphDataType {
pub fn with_type(input: &Type) -> Self {
match TaggedValue::from_type(input) {
TaggedValue::Image(_) | TaggedValue::ImageFrame(_) => Self::Raster,
TaggedValue::Subpaths(_) | TaggedValue::RcSubpath(_) | TaggedValue::VectorData(_) => Self::VectorData,
TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::VectorData,
TaggedValue::U32(_)
| TaggedValue::U64(_)
| TaggedValue::F64(_)
@@ -37,11 +37,10 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context:
} else {
DVec2::new(secondary_pos, primary_end)
};
overlay_context.line(
overlay_context.colored_line(
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
None,
&("#".to_string() + &grid_color.rgba_hex()),
);
}
}
@@ -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 start = DVec2::new(x_pos, min_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(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
None,
&("#".to_string() + &grid_color.rgba_hex()),
);
}
@@ -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 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)));
overlay_context.line(
overlay_context.colored_line(
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
None,
&("#".to_string() + &grid_color.rgba_hex()),
);
}
}
@@ -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 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(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
@@ -1,10 +1,9 @@
use super::utility_types::OverlayContext;
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::tool_messages::tool_prelude::DocumentMessageHandler;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use graphene_core::vector::ManipulatorPointId;
use glam::DVec2;
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) {
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 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));
overlay_context.outline(subpaths.iter(), transform);
overlay_context.outline_vector(&vector_data, transform);
for manipulator_group in get_manipulator_groups(subpaths) {
let anchor = manipulator_group.anchor;
let anchor_position = transform.transform_point2(anchor);
let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) {
let handle_position = transform.transform_point2(in_handle);
overlay_context.line(handle_position, anchor_position, None, None);
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
for (segment_id, bezier, _start, _end) in vector_data.segment_bezier_iter() {
let bezier = bezier.apply_transformation(|point| transform.transform_point2(point));
let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
match bezier.handles {
bezier_rs::BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => {
overlay_context.line(handle, bezier.start);
overlay_context.line(handle, bezier.end);
overlay_context.manipulator_handle(handle, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)));
}
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);
overlay_context.line(handle_position, anchor_position, None, 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);
}
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(selected, ManipulatorPointId::Anchor(id)), None);
}
}
}
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()) {
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 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 mut manipulator_groups = get_manipulator_groups(subpaths);
if let Some(first_manipulator) = manipulator_groups.next() {
let anchor = first_manipulator.anchor;
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);
};
for point in vector_data.single_connected_points() {
let Some(position) = vector_data.point_domain.position_from_id(point) else { continue };
let position = transform.transform_point2(position);
overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), 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::messages::prelude::Message;
use bezier_rs::Subpath;
use bezier_rs::{Bezier, Subpath};
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 glam::{DAffine2, DVec2};
use wasm_bindgen::JsValue;
pub type OverlayProvider = fn(OverlayContext) -> Message;
@@ -39,7 +41,15 @@ impl OverlayContext {
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 end = end.round() - DVec2::splat(0.5);
if let Some(dash_width) = dash_width {
@@ -152,9 +162,44 @@ impl OverlayContext {
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();
for subpath in subpaths {
let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable();
let Some(first) = curves.peek() else {
@@ -1,13 +1,16 @@
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::{NodeId, NodeNetwork};
use graphene_core::renderer::ClickTarget;
use graphene_core::renderer::Quad;
use graphene_core::transform::Footprint;
use graphene_std::vector::PointId;
use graphene_std::vector::VectorData;
use glam::{DAffine2, DVec2};
use graphene_std::vector::PointId;
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
@@ -26,6 +29,7 @@ pub struct DocumentMetadata {
hidden: HashSet<NodeId>,
locked: HashSet<NodeId>,
click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
vector_modify: HashMap<NodeId, VectorData>,
/// Transform from document space to viewport space.
pub document_to_viewport: DAffine2,
}
@@ -39,6 +43,7 @@ impl Default for DocumentMetadata {
folders: HashSet::new(),
hidden: HashSet::new(),
locked: HashSet::new(),
vector_modify: HashMap::new(),
click_targets: HashMap::new(),
document_to_viewport: DAffine2::IDENTITY,
}
@@ -62,6 +67,23 @@ impl DocumentMetadata {
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.
fn get_relations(&self, node_identifier: LayerNodeIdentifier) -> Option<&NodeRelations> {
self.structure.get(&node_identifier)
@@ -225,6 +247,7 @@ impl DocumentMetadata {
self.upstream_transforms.retain(|node, _| graph.nodes.contains_key(node));
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 {
/// Update the cached click targets of the layers
pub fn update_click_targets(&mut self, new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>) {
/// Update the cached click targets and vector modify values of the layers
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.vector_modify = new_vector_modify;
}
/// 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::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::prelude::*;
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 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 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)]
pub enum OriginalTransforms {
Layer(HashMap<LayerNodeIdentifier, DAffine2>),
Path(HashMap<LayerNodeIdentifier, Vec<(ManipulatorPointId, DVec2)>>),
Path(HashMap<LayerNodeIdentifier, InitialPoints>),
}
impl Default for OriginalTransforms {
fn default() -> Self {
@@ -43,36 +65,46 @@ impl OriginalTransforms {
}
}
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 {
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) {
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;
};
let get_manipulator_point_position = |point_id: ManipulatorPointId| {
graph_modification_utils::get_manipulator_from_id(vector_data, point_id.group)
.and_then(|manipulator_group| point_id.manipulator_type.get_position(manipulator_group))
.map(|position| (point_id, position))
let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else {
continue;
};
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(
document_metadata: &DocumentMetadata,
layer: LayerNodeIdentifier,
initial_points: Option<&Vec<(ManipulatorPointId, DVec2)>>,
transformation: DAffine2,
responses: &mut VecDeque<Message>,
) {
fn transform_path(document_metadata: &DocumentMetadata, layer: LayerNodeIdentifier, initial_points: &mut InitialPoints, transformation: DAffine2, responses: &mut VecDeque<Message>) {
let viewspace = document_metadata.transform_to_viewport(layer);
let layerspace_rotation = viewspace.inverse() * transformation;
let Some(initial_points) = initial_points else {
return;
};
for (&point, anchor) in initial_points.anchors.iter_mut() {
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 {
let viewport_point = viewspace.transform_point2(*position);
let new_pos_viewport = layerspace_rotation.transform_point2(viewport_point);
let point = *point_id;
let position = new_pos_viewport;
let modification = VectorDataModification::SetManipulatorPosition { point, position };
for (&id, handle) in initial_points.handles.iter() {
let new_pos_viewport = layerspace_rotation.transform_point2(viewspace.transform_point2(handle.initial));
let relative = initial_points.anchors.get(&handle.anchor).map_or(handle.relative, |anchor| anchor.current);
let modification_type = id.set_relative_position(new_pos_viewport - relative);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
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()) {
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::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) => {
for (&layer, points) in path {
for &(point, position) in points {
self.responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorPosition { point, position },
});
for (&point, &anchor) in &points.anchors {
let delta = anchor.initial - anchor.current;
let modification_type = VectorModificationType::ApplyPointDelta { point, delta };
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)]
pub struct PortfolioMessageHandler {
menu_bar_message_handler: MenuBarMessageHandler,
documents: HashMap<DocumentId, DocumentMessageHandler>,
pub documents: HashMap<DocumentId, DocumentMessageHandler>,
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],
pub persistent_data: PersistentData,
pub executor: NodeGraphExecutor,
@@ -375,6 +375,20 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
document_is_saved,
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);
match 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")
}
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 {
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#"
<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)">
@@ -673,5 +688,6 @@ impl PortfolioMessageHandler {
.to_string();
responses.add(FrontendMessage::UpdateDocumentArtwork { svg: error });
}
result
}
}
@@ -1,5 +1,6 @@
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_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(
&self,
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::prelude::*;
use bezier_rs::{ManipulatorGroup, Subpath};
use bezier_rs::Subpath;
use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork};
use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::text::Font;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::style::Gradient;
use graphene_core::vector::PointId;
use graphene_core::Color;
use glam::DVec2;
use std::collections::VecDeque;
/// 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;
responses.add(GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index });
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)
}
/// Batch set all of the manipulator groups to set their colinear handle state on a specific layer
pub fn set_manipulator_colinear_handles_state(manipulator_groups: &[ManipulatorGroup<ManipulatorGroupId>], layer: LayerNodeIdentifier, colinear: bool, responses: &mut VecDeque<Message>) {
for manipulator_group in manipulator_groups {
responses.add(GraphOperationMessage::Vector {
layer,
modification: VectorDataModification::SetManipulatorColinearHandlesState { id: manipulator_group.id, colinear },
});
}
}
/// 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
}
pub fn new_custom(id: NodeId, nodes: HashMap<NodeId, DocumentNode>, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
responses.add(GraphOperationMessage::NewCustomLayer {
id,
nodes,
parent,
insert_index: -1,
alias: String::new(),
});
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] });
LayerNodeIdentifier::new_unchecked(id)
}
/// 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)
}
/// 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
pub fn get_gradient(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> Option<Gradient> {
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()
}
/// 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.
pub struct NodeGraphLayer<'a> {
node_graph: &'a NodeNetwork,
@@ -29,13 +29,12 @@ impl Resize {
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> {
let Some(layer) = self.layer else {
return None;
};
pub fn calculate_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key) -> Option<[DVec2; 2]> {
let layer = self.layer?;
if layer == LayerNodeIdentifier::ROOT_PARENT {
log::error!("Resize layer cannot be ROOT_PARENT");
return None;
}
if !document.network().nodes.contains_key(&layer.to_node()) {
@@ -87,9 +86,14 @@ impl Resize {
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(
GraphOperationMessage::TransformSet {
layer,
layer: self.layer?,
transform: DAffine2::from_scale_angle_translation(points_viewport[1] - points_viewport[0], 0., points_viewport[0]),
transform_in: TransformIn::Viewport,
skip_rerender,
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,20 @@
mod grid_snapper;
mod layer_snapper;
mod snap_results;
pub use {grid_snapper::*, layer_snapper::*, snap_results::*};
use crate::consts::COLOR_OVERLAY_BLUE;
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::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget};
use crate::messages::prelude::*;
use bezier_rs::{Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::PointId;
use glam::{DAffine2, DVec2};
use std::cmp::Ordering;
pub use {grid_snapper::*, layer_snapper::*, snap_results::*};
/// Handles snapping and snap overlays
#[derive(Debug, Clone, Default)]
@@ -150,7 +153,7 @@ pub struct SnapData<'a> {
pub document: &'a DocumentMessageHandler,
pub input: &'a InputPreprocessorMessageHandler,
pub ignore: &'a [LayerNodeIdentifier],
pub manipulators: Vec<(LayerNodeIdentifier, ManipulatorGroupId)>,
pub manipulators: Vec<(LayerNodeIdentifier, PointId)>,
pub candidates: Option<&'a Vec<LayerNodeIdentifier>>,
}
impl<'a> SnapData<'a> {
@@ -172,7 +175,7 @@ impl<'a> SnapData<'a> {
fn ignore_bounds(&self, layer: LayerNodeIdentifier) -> bool {
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()))
}
}
@@ -277,10 +280,8 @@ impl SnapManager {
}
}
if let Some(root) = snap_data.document.network.get_root_node() {
if snap_data.document.network.nodes.get(&root.id).expect("Root should always be a node in find_candidates").is_layer {
add_candidates(LayerNodeIdentifier::new(root.id, &snap_data.document.network), snap_data, quad, &mut candidates);
}
for layer in LayerNodeIdentifier::ROOT_PARENT.children(&document.metadata) {
add_candidates(layer, snap_data, quad, &mut candidates);
}
if candidates.len() > 10 {
@@ -333,7 +334,7 @@ impl SnapManager {
if let Some(ind) = &self.indicator {
for curve in &ind.curves {
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 {
overlay_context.quad(to_viewport * quad);
@@ -8,8 +8,7 @@ use crate::messages::prelude::*;
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_std::vector::PointId;
use graphene_core::vector::PointId;
#[derive(Clone, Debug, Default)]
pub struct LayerSnapper {
@@ -33,7 +32,7 @@ impl LayerSnapper {
self.paths_to_snap.push(SnapCandidatePath {
document_curve,
layer,
start: ManipulatorGroupId::new(),
start: PointId::new(),
target,
bounds: Some(bounds),
});
@@ -133,7 +132,7 @@ impl LayerSnapper {
let direction = constraint.direction().normalize_or_zero();
let start = 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 {
@@ -296,7 +295,7 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
struct SnapCandidatePath {
document_curve: Bezier,
layer: LayerNodeIdentifier,
start: ManipulatorGroupId,
start: PointId,
target: SnapTarget,
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<Id: bezier_rs::Identifier>(group: &bezier_rs::ManipulatorGroup<Id>, to_document: DAffine2, subpath: &Subpath<Id>, index: usize) -> bool {
pub fn are_manipulator_handles_colinear(group: &bezier_rs::ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool {
let anchor = group.anchor;
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));
@@ -3,7 +3,7 @@ use crate::messages::portfolio::document::utility_types::misc::{SnapSource, Snap
use bezier_rs::Bezier;
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
use graphene_core::vector::PointId;
#[derive(Clone, Debug, Default)]
pub struct SnapResults {
@@ -79,7 +79,7 @@ pub struct SnappedLine {
#[derive(Clone, Debug)]
pub struct SnappedCurve {
pub layer: LayerNodeIdentifier,
pub start: ManipulatorGroupId,
pub start: PointId,
pub point: SnappedPoint,
pub document_curve: Bezier,
}
@@ -126,8 +126,8 @@ impl SelectedEdges {
for point in points {
let old_position = 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 updated = normalised * (max - min) + min;
let normalized = (bounds_space - self.bounds[0]) / (self.bounds[1] - self.bounds[0]);
let updated = normalized * (max - min) + min;
point.document_point = bounds_to_doc.transform_point2(updated);
let mut snapped = if constrain {
let constraint = SnapConstraint::Line {
@@ -1,31 +1,27 @@
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_subpaths;
use graphene_std::vector::PointId;
use glam::DVec2;
/// 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_distance_squared = tolerance * tolerance;
for layer in document.selected_nodes.selected_layers(document.metadata()) {
let viewspace = document.metadata().transform_to_viewport(layer);
let subpaths = get_subpaths(layer, &document.network)?;
for (subpath_index, subpath) in subpaths.iter().enumerate() {
if subpath.closed() {
continue;
}
let vector_data = document.metadata.compute_modified_vector(layer, document.network())?;
for id in vector_data.single_connected_points() {
let Some(point) = vector_data.point_domain.position_from_id(id) else { continue };
for (manipulator_group, from_start) in [(subpath.manipulator_groups().first(), true), (subpath.manipulator_groups().last(), false)] {
let Some(manipulator_group) = manipulator_group else { break };
let distance_squared = viewspace.transform_point2(point).distance_squared(goal);
let distance_squared = viewspace.transform_point2(manipulator_group.anchor).distance_squared(pos);
if distance_squared < best_distance_squared {
best = Some((layer, subpath_index, from_start));
best_distance_squared = distance_squared;
}
if distance_squared < best_distance_squared {
best = Some((layer, id, point));
best_distance_squared = distance_squared;
}
}
}
@@ -1,5 +1,6 @@
use super::tool_prelude::*;
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::tool::common_functionality::auto_panning::AutoPanning;
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::snapping::SnapData;
use graph_craft::document::NodeId;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color;
#[derive(Default)]
@@ -201,10 +201,18 @@ impl Fsm for EllipseToolFsmState {
responses.add(DocumentMessage::StartTransaction);
// Create a new ellipse vector shape
let subpath = bezier_rs::Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE);
let manipulator_groups = subpath.manipulator_groups().to_vec();
let layer = graph_modification_utils::new_vector_layer(vec![subpath], NodeId(generate_uuid()), document.new_layer_parent(true), responses);
graph_modification_utils::set_manipulator_colinear_handles_state(&manipulator_groups, layer, true, responses);
let nodes = {
let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node does not exist");
let node = node_type.to_document_node_default_inputs(
[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);
responses.add(GraphOperationMessage::TransformSet {
@@ -214,22 +222,18 @@ impl Fsm for EllipseToolFsmState {
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, EllipseToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, input, center, lock_ratio, false) {
responses.add(message);
if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) {
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
@@ -1,5 +1,5 @@
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_types::OverlayContext;
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 graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::VectorModificationType;
use graphene_core::Color;
use graphene_std::vector::{PointId, SegmentId};
use bezier_rs::ManipulatorGroup;
use glam::DVec2;
#[derive(Default)]
@@ -176,7 +176,7 @@ impl ToolTransition for FreehandTool {
#[derive(Clone, Debug, Default)]
struct FreehandToolData {
extend_from_start: bool,
last_point: DVec2,
end_point: Option<(DVec2, PointId)>,
dragged: bool,
weight: f64,
layer: Option<LayerNodeIdentifier>,
@@ -211,64 +211,44 @@ impl Fsm for FreehandToolFsmState {
tool_data.extend_from_start = false;
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) {
let transform = document.metadata().transform_to_viewport(layer);
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;
// Extend an endpoint of the selected path
if let Some((layer, _, position)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
tool_data.layer = Some(layer);
responses.add(GraphOperationMessage::Vector { layer, modification });
} else {
responses.add(DocumentMessage::DeselectAllLayers);
extend_path_with_next_segment(tool_data, position, responses);
let parent = document.new_layer_parent(true);
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),
});
return FreehandToolFsmState::Drawing;
}
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, FreehandToolMessage::PointerMove) => {
if let Some(layer) = tool_data.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 {
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;
}
extend_path_with_next_segment(tool_data, position, responses);
}
FreehandToolFsmState::Drawing
@@ -276,6 +256,8 @@ impl Fsm for FreehandToolFsmState {
(FreehandToolFsmState::Drawing, FreehandToolMessage::DragStop) => {
if tool_data.dragged {
responses.add(DocumentMessage::CommitTransaction);
} else {
responses.add(DocumentMessage::DocumentHistoryBackward);
}
tool_data.layer = None;
@@ -312,3 +294,34 @@ impl Fsm for FreehandToolFsmState {
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 (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(end, dragging == Some(GradientDragTarget::End));
@@ -1,6 +1,7 @@
use super::tool_prelude::*;
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
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::utility_types::document_metadata::LayerNodeIdentifier;
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::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::vector::style::Stroke;
use graphene_core::Color;
#[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);
tool_data.drag_start = snapped.snapped_point_document;
let subpath = bezier_rs::Subpath::new_line(DVec2::ZERO, DVec2::X);
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 {
layer,
@@ -190,11 +203,6 @@ impl Fsm for LineToolFsmState {
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.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::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
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::snapping::{SnapData, SnapManager};
use graph_craft::document::NodeNetwork;
use graphene_core::renderer::Quad;
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use graphene_core::vector::ManipulatorPointId;
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 selection_status = &self.tool_data.selection_status;
let manipulator_angle = selection_status
.as_multiple()
.map(|multiple| multiple.manipulator_angle)
.or_else(|| selection_status.as_one().map(|point| point.manipulator_angle));
let manipulator_angle = selection_status.angle();
let x_location = NumberInput::new(x)
.unit(" px")
@@ -105,8 +101,11 @@ impl LayoutHolder for PathTool {
.min(-((1_u64 << std::f64::MANTISSA_DIGITS) as f64))
.max((1_u64 << std::f64::MANTISSA_DIGITS) as f64)
.on_update(move |number_input: &NumberInput| {
let new_x = number_input.value.unwrap_or(x.unwrap());
PathToolMessage::SelectedPointXChanged { new_x }.into()
if let Some(new_x) = number_input.value.or(x) {
PathToolMessage::SelectedPointXChanged { new_x }.into()
} else {
Message::NoOp
}
})
.widget_holder();
@@ -118,8 +117,11 @@ impl LayoutHolder for PathTool {
.min(-((1_u64 << std::f64::MANTISSA_DIGITS) as f64))
.max((1_u64 << std::f64::MANTISSA_DIGITS) as f64)
.on_update(move |number_input: &NumberInput| {
let new_y = number_input.value.unwrap_or(y.unwrap());
PathToolMessage::SelectedPointYChanged { new_y }.into()
if let Some(new_y) = number_input.value.or(y) {
PathToolMessage::SelectedPointYChanged { new_y }.into()
} else {
Message::NoOp
}
})
.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 {
if let Some(closed_segment) = &mut self.segment {
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)
} else {
PathToolFsmState::InsertPoint
@@ -318,6 +320,8 @@ impl PathToolData {
let document_network = document.network();
let document_metadata = document.metadata();
self.drag_start_pos = input.mouse.position;
// 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) = selected_points {
@@ -345,7 +349,7 @@ impl PathToolData {
}
self.drag_start_pos = 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
}
@@ -370,16 +374,12 @@ impl PathToolData {
// Do not snap against handles when anchor is selected
let mut additional_selected_points = Vec::new();
for point in selected_points.points.iter() {
if point.point_id.manipulator_type == SelectedType::Anchor {
additional_selected_points.push(ManipulatorPointInfo {
layer: point.layer,
point_id: ManipulatorPointId::new(point.point_id.group, SelectedType::InHandle),
});
additional_selected_points.push(ManipulatorPointInfo {
layer: point.layer,
point_id: ManipulatorPointId::new(point.point_id.group, SelectedType::OutHandle),
});
}
let Some(anchor) = point.point_id.as_anchor() else { continue };
let connected = selected_points.vector_data.segment_domain.all_connected(anchor).map(|handle| handle.to_manipulator_point());
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.extend(point_info);
}
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);
}
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
if alt && !self.alt_debounce {
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;
if shift {
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
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);
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);
}
}
@@ -431,7 +443,6 @@ impl Fsm for PathToolFsmState {
responses.add(OverlaysMessage::Draw);
responses.add(PathToolMessage::SelectedPointUpdated);
// This can happen in any state (which is why we return self)
self
}
(_, PathToolMessage::Overlays(mut overlay_context)) => {
@@ -503,7 +514,9 @@ impl Fsm for PathToolFsmState {
(PathToolFsmState::Dragging, PathToolMessage::PointerMove { alt, shift }) => {
let alt_state = input.keyboard.get(alt 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
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, .. }) => {
// 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);
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
@@ -575,18 +588,16 @@ impl Fsm for PathToolFsmState {
(_, PathToolMessage::DragStop { equidistant }) => {
let equidistant = input.keyboard.get(equidistant as usize);
let nearest_point = shape_editor
.find_nearest_point_indices(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD)
.map(|(_, nearest_point)| nearest_point);
let nearest_point = shape_editor.find_nearest_point_indices(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD);
shape_editor.delete_selected_handles_with_zero_length(&document.network, &document.metadata, &tool_data.opposing_handle_lengths, responses);
if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !equidistant {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == Some(point));
if clicked_selected {
shape_editor.deselect_all_points();
shape_editor.change_point_selection(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, false);
responses.add(OverlaysMessage::Draw);
if let Some((layer, nearest_point)) = nearest_point {
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);
if clicked_selected {
shape_editor.deselect_all_points();
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
responses.add(OverlaysMessage::Draw);
}
}
}
@@ -599,17 +610,17 @@ impl Fsm for PathToolFsmState {
(_, PathToolMessage::Delete) => {
// Delete the selected points and clean up overlays
responses.add(DocumentMessage::StartTransaction);
shape_editor.delete_selected_points(responses);
shape_editor.delete_selected_points(&document, responses);
responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready
}
(_, PathToolMessage::BreakPath) => {
shape_editor.break_path_at_selected_point(&document.network, responses);
shape_editor.break_path_at_selected_point(document, responses);
PathToolFsmState::Ready
}
(_, PathToolMessage::DeleteAndBreakPath) => {
shape_editor.delete_point_and_break_path(&document.network, responses);
shape_editor.delete_point_and_break_path(document, responses);
PathToolFsmState::Ready
}
(_, PathToolMessage::FlipSmoothSharp) => {
@@ -625,12 +636,12 @@ impl Fsm for PathToolFsmState {
}
(_, PathToolMessage::PointerMove { .. }) => self,
(_, 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
}
(_, 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);
PathToolFsmState::Ready
}
@@ -641,13 +652,13 @@ impl Fsm for PathToolFsmState {
}
(_, PathToolMessage::SelectedPointXChanged { new_x }) => {
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
}
(_, PathToolMessage::SelectedPointYChanged { new_y }) => {
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
}
@@ -657,14 +668,14 @@ impl Fsm for PathToolFsmState {
}
(_, PathToolMessage::ManipulatorMakeHandlesColinear) => {
responses.add(DocumentMessage::StartTransaction);
shape_editor.set_colinear_handles_state_on_selected(true, responses);
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, &document.network);
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
responses.add(DocumentMessage::CommitTransaction);
responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready
}
(_, PathToolMessage::ManipulatorMakeHandlesFree) => {
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);
PathToolFsmState::Ready
}
@@ -743,10 +754,11 @@ impl SelectionStatus {
}
}
fn as_multiple(&self) -> Option<&MultipleSelectedPoints> {
fn angle(&self) -> Option<ManipulatorAngle> {
match self {
SelectionStatus::Multiple(multiple) => Some(multiple),
_ => None,
Self::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 {
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;
};
let Some(colinear_manipulators) = get_colinear_manipulators(layer, document_network) else {
let Some(&point) = shape_state.selected_points().next() else {
return SelectionStatus::None;
};
let Some(point) = shape_state.selected_points().next() 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 {
let Some(local_position) = point.get_position(&vector_data) else {
return SelectionStatus::None;
};
let coordinates = document_metadata.transform_to_document(layer).transform_point2(local_position);
let manipulator_angle = if colinear_manipulators.contains(&point.group) {
ManipulatorAngle::Colinear
} else {
ManipulatorAngle::Free
};
let manipulator_angle = if vector_data.colinear(point) { ManipulatorAngle::Colinear } else { ManipulatorAngle::Free };
return SelectionStatus::One(SingleSelectedPoint {
coordinates,
layer,
id: *point,
id: point,
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
if total_selected_points > 1 {
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 crate::consts::HIDE_HANDLE_DISTANCE;
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_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
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::utility_functions::should_extend;
use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId;
use graphene_core::uuid::{generate_uuid, ManipulatorGroupId};
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::{ManipulatorPointId, SelectedType};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::{PointId, VectorModificationType};
use graphene_core::Color;
use graphene_std::vector::{HandleId, SegmentId};
#[derive(Default)]
pub struct PenTool {
@@ -187,310 +188,175 @@ impl ToolTransition for PenTool {
}
}
}
#[derive(Default)]
#[derive(Clone, Debug, Default)]
struct ModifierState {
snap_angle: bool,
lock_angle: bool,
break_handle: bool,
}
#[derive(Clone, Debug)]
struct LastPoint {
id: PointId,
pos: DVec2,
in_segment: Option<SegmentId>,
handle_start: DVec2,
}
#[derive(Clone, Debug, Default)]
struct PenToolData {
weight: f64,
layer: Option<LayerNodeIdentifier>,
subpath_index: usize,
snap_manager: SnapManager,
colinear_handles: bool,
// Indicates that curve extension is occurring from the first point, rather than (more commonly) the last point
from_start: bool,
latest_points: Vec<LastPoint>,
point_index: usize,
handle_end: Option<DVec2>,
next_point: DVec2,
next_handle_start: DVec2,
g1_continuous: bool,
angle: f64,
auto_panning: AutoPanning,
modifiers: ModifierState,
}
impl PenToolData {
fn extend_subpath(&mut self, layer: LayerNodeIdentifier, subpath_index: usize, from_start: bool, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.layer = Some(layer);
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 latest_point(&self) -> Option<&LastPoint> {
self.latest_points.get(self.point_index)
}
fn create_new_path(
&mut self,
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);
fn latest_point_mut(&mut self) -> Option<&mut LastPoint> {
self.latest_points.get_mut(self.point_index)
}
// Get the position and set properties
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 = self.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
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;
fn add_point(&mut self, point: LastPoint) {
self.point_index = (self.point_index + 1).min(self.latest_points.len());
self.latest_points.truncate(self.point_index);
self.latest_points.push(point);
}
/// 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>) {
(|| -> Option<()> {
// 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> {
fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2) {
self.g1_continuous = true;
let document = snap_data.document;
// Get subpath
let subpath = &get_subpaths(self.layer?, &document.network)?[self.subpath_index];
self.next_handle_start = self.next_point;
// Get the last manipulator group
let manipulator_groups = subpath.manipulator_groups();
let last_manipulator_group = if self.from_start { manipulator_groups.first()? } else { manipulator_groups.last()? };
// 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 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);
// Break the control
let Some(last_pos) = self.latest_point().map(|point| point.pos) else { return };
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);
if on_top {
if let Some(point) = self.latest_point_mut() {
point.in_segment = None;
}
self.handle_end = None;
}
}
// Update points on current segment (to show preview of new handle)
let point = ManipulatorPointId::new(last_manipulator_group.id, outwards_handle);
responses.add(GraphOperationMessage::Vector {
layer: self.layer?,
modification: VectorDataModification::SetManipulatorPosition { point, position },
fn finish_placing_handle(&mut self, snap_data: SnapData, transform: DAffine2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let document = snap_data.document;
let next_handle_start = self.next_handle_start;
let handle_start = self.latest_point()?.handle_start;
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
if colinear {
// Could also be written as `last_anchor.position * 2 - pos` but this way avoids overflow/underflow better
let position = last_anchor - (position - last_anchor);
let point = ManipulatorPointId::new(last_manipulator_group.id, inwards_handle);
let points = [start, end];
let id = SegmentId::generate();
let modification_type = VectorModificationType::InsertSegment { id, points, handles };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
// Mirror
if let Some(last_segment) = self.latest_point().and_then(|point| point.in_segment) {
responses.add(GraphOperationMessage::Vector {
layer: self.layer?,
modification: VectorDataModification::SetManipulatorPosition { point, position },
layer,
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
let id = last_manipulator_group.id;
responses.add(GraphOperationMessage::Vector {
layer: self.layer?,
modification: VectorDataModification::SetManipulatorColinearHandlesState { id, colinear },
});
fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let colinear = !self.modifiers.break_handle && self.handle_end.is_some();
self.next_handle_start = self.compute_snapped_angle(snap_data, transform, colinear, mouse, Some(self.next_point), false);
if let Some(handle_end) = self.handle_end.as_mut().filter(|_| 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)
}
fn place_anchor(&mut self, mut snap_data: SnapData, transform: DAffine2, mouse: DVec2, modifiers: ModifierState, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let document = snap_data.document;
// 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 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 },
});
fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let relative = self.latest_point().map(|point| point.pos);
self.next_point = self.compute_snapped_angle(snap_data, transform, false, mouse, relative, true);
if let Some(handle_end) = self.handle_end.as_mut() {
*handle_end = self.next_point;
self.next_handle_start = self.next_point;
}
responses.add(OverlaysMessage::Draw);
Some(PenToolFsmState::PlacingAnchor)
}
/// 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 mut document_pos = document.metadata.document_to_viewport.inverse().transform_point2(mouse);
let snap = &mut self.snap_manager;
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 angle = if lock_angle {
@@ -540,56 +406,11 @@ impl PenToolData {
if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) {
if (relative - document_pos) != DVec2::ZERO {
self.angle = -(relative - document_pos).angle_between(DVec2::X)
} else {
self.angle = 0.0;
}
}
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 {
@@ -628,9 +449,52 @@ impl Fsm for PenToolFsmState {
responses.add(OverlaysMessage::Draw);
self
}
(_, PenToolMessage::Overlays(mut overlay_context)) => {
(PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => {
path_overlays(document, shape_editor, &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
}
@@ -644,46 +508,71 @@ impl Fsm for PenToolFsmState {
(PenToolFsmState::Ready, PenToolMessage::DragStart) => {
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
if let Some((layer, subpath_index, from_start)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
tool_data.extend_subpath(layer, subpath_index, from_start, document, responses);
if let Some((layer, point, position)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE) {
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 {
tool_data.create_new_path(
document,
tool_options.line_weight,
tool_options.stroke.active_color(),
tool_options.fill.active_color(),
input,
responses,
);
// New path layer
let nodes = {
let node_type = resolve_document_node_type("Path").expect("Path node does not exist");
HashMap::from([(NodeId(0), node_type.to_document_node_default_inputs([], Default::default()))])
};
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
PenToolFsmState::DraggingHandle
}
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => {
responses.add(DocumentMessage::StartTransaction);
tool_data.bend_from_previous_point(document, transform, responses);
if tool_data.handle_end.is_some() {
responses.add(DocumentMessage::StartTransaction);
}
tool_data.bend_from_previous_point(SnapData::new(document, input), transform);
PenToolFsmState::DraggingHandle
}
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => {
tool_data.colinear_handles = true;
tool_data.finish_placing_handle(document, transform, responses).unwrap_or(PenToolFsmState::PlacingAnchor)
}
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data
.finish_placing_handle(SnapData::new(document, input), transform, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor),
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
let modifiers = ModifierState {
tool_data.modifiers = ModifierState {
snap_angle: input.keyboard.key(snap_angle),
lock_angle: input.keyboard.key(lock_angle),
break_handle: input.keyboard.key(break_handle),
};
let snap_data = SnapData::new(document, input);
let state = tool_data
.drag_handle(snap_data, transform, input.mouse.position, modifiers, responses)
.unwrap_or(PenToolFsmState::Ready);
let state = tool_data.drag_handle(snap_data, transform, input.mouse.position, responses).unwrap_or(PenToolFsmState::Ready);
// Auto-panning
let messages = [
@@ -695,13 +584,13 @@ impl Fsm for PenToolFsmState {
state
}
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
let modifiers = ModifierState {
tool_data.modifiers = ModifierState {
snap_angle: input.keyboard.key(snap_angle),
lock_angle: input.keyboard.key(lock_angle),
break_handle: input.keyboard.key(break_handle),
};
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);
// Auto-panning
@@ -741,11 +630,10 @@ impl Fsm for PenToolFsmState {
state
}
(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.handle_end = None;
tool_data.latest_points.clear();
tool_data.point_index = 0;
tool_data.snap_manager.cleanup(responses);
PenToolFsmState::Ready
@@ -755,12 +643,23 @@ impl Fsm for PenToolFsmState {
self
}
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, ModifierState::default(), responses)
.unwrap_or(PenToolFsmState::PlacingAnchor),
(_, PenToolMessage::Redo) => tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, ModifierState::default(), responses)
.unwrap_or(PenToolFsmState::PlacingAnchor),
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => {
if tool_data.point_index > 0 {
tool_data.point_index -= 1;
tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses)
.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,
}
}
@@ -800,16 +699,3 @@ impl Fsm for PenToolFsmState {
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 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::tool::common_functionality::auto_panning::AutoPanning;
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::snapping::SnapData;
use graph_craft::document::NodeId;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color;
#[derive(Default)]
@@ -244,11 +244,34 @@ impl Fsm for PolygonToolFsmState {
polygon_data.start(document, input);
responses.add(DocumentMessage::StartTransaction);
let subpath = match tool_options.polygon_type {
PolygonType::Convex => bezier_rs::Subpath::new_regular_polygon(DVec2::ZERO, tool_options.vertices as u64, 1.),
PolygonType::Star => bezier_rs::Subpath::new_star_polygon(DVec2::ZERO, tool_options.vertices as u64, 1., 0.5),
let nodes = {
let node = match tool_options.polygon_type {
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);
responses.add(GraphOperationMessage::TransformSet {
@@ -258,22 +281,19 @@ impl Fsm for PolygonToolFsmState {
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, PolygonToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(message) = polygon_data.calculate_transform(document, input, center, lock_ratio, false) {
responses.add(message);
if let Some([start, end]) = tool_data.data.calculate_points(document, input, center, lock_ratio) {
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
@@ -1,4 +1,5 @@
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::tool::common_functionality::auto_panning::AutoPanning;
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::snapping::SnapData;
use graph_craft::document::NodeId;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color;
#[derive(Default)]
@@ -205,11 +205,20 @@ impl Fsm for RectangleToolFsmState {
(RectangleToolFsmState::Ready, RectangleToolMessage::DragStart) => {
shape_data.start(document, input);
let subpath = bezier_rs::Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
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);
responses.add(GraphOperationMessage::TransformSet {
@@ -219,22 +228,19 @@ impl Fsm for RectangleToolFsmState {
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, RectangleToolMessage::PointerMove { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, input, center, lock_ratio, false) {
responses.add(message);
if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) {
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
@@ -529,7 +529,7 @@ impl Fsm for SelectToolFsmState {
.unwrap_or_default();
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);
// 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") {
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
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 });
}
}
@@ -1,15 +1,14 @@
use super::tool_prelude::*;
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::tool::common_functionality::auto_panning::AutoPanning;
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::snapping::SnapManager;
use graph_craft::document::NodeId;
use graph_craft::document::{value::TaggedValue, NodeId, NodeInput};
use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::Color;
#[derive(Default)]
@@ -217,17 +216,15 @@ impl Fsm for SplineToolFsmState {
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 {
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),
});
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);
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
}
@@ -260,7 +257,7 @@ impl Fsm for SplineToolFsmState {
let pos = transform.inverse().transform_point2(snapped_position);
tool_data.next_point = pos;
update_spline(tool_data, true, responses);
update_spline(document, tool_data, true, responses);
// Auto-panning
let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()];
@@ -283,7 +280,7 @@ impl Fsm for SplineToolFsmState {
}
(SplineToolFsmState::Drawing, SplineToolMessage::Confirm | SplineToolMessage::Abort) => {
if tool_data.points.len() >= 2 {
update_spline(tool_data, false, responses);
update_spline(document, tool_data, false, responses);
responses.add(DocumentMessage::CommitTransaction);
} else {
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();
if show_preview {
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;
};
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 });
responses.add_front(NodeGraphMessage::SetInputValue { node_id, input_index: 1, value });
}
@@ -313,25 +313,6 @@ impl TextToolData {
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> {
@@ -429,7 +410,6 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Editing
}
(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => {
tool_data.fix_text_bounds(&new_text, document, font_cache, responses);
responses.add(NodeGraphMessage::SetQualifiedInputValue {
node_id: graph_modification_utils::get_text_id(tool_data.layer, &document.network).unwrap(),
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::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing};
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::utility_types::{ToolData, ToolType};
@@ -69,16 +68,12 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
}
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();
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
let mut point_count: usize = 0;
let get_location = |point: &ManipulatorPointId| {
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 get_location = |point: &ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
let points = shape_editor.selected_points();
*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>>,
/// The current click targets for layer nodes.
click_targets: HashMap<NodeId, Vec<ClickTarget>>,
/// Vector data in Path nodes.
vector_modify: HashMap<NodeId, VectorData>,
/// The current upstream transforms for nodes.
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
}
/// Messages passed from the editor thread to the node runtime thread.
enum NodeRuntimeMessage {
pub enum NodeRuntimeMessage {
ExecutionRequest(ExecutionRequest),
FontCacheUpdate(FontCache),
ImaginatePreferencesUpdate(ImaginatePreferences),
@@ -78,24 +80,25 @@ pub struct ExportConfig {
pub size: DVec2,
}
pub(crate) struct ExecutionRequest {
pub struct ExecutionRequest {
execution_id: u64,
graph: NodeNetwork,
render_config: RenderConfig,
}
pub(crate) struct ExecutionResponse {
pub struct ExecutionResponse {
execution_id: u64,
result: Result<TaggedValue, String>,
responses: VecDeque<Message>,
new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
new_vector_modify: HashMap<NodeId, VectorData>,
new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
resolved_types: ResolvedDocumentNodeTypes,
node_graph_errors: GraphErrors,
transform: DAffine2,
}
enum NodeGraphUpdate {
pub enum NodeGraphUpdate {
ExecutionResponse(ExecutionResponse),
NodeGraphUpdateMessage(NodeGraphUpdateMessage),
}
@@ -119,7 +122,7 @@ thread_local! {
}
impl NodeRuntime {
fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<NodeGraphUpdate>) -> Self {
pub fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<NodeGraphUpdate>) -> Self {
Self {
executor: DynamicExecutor::default(),
receiver,
@@ -136,6 +139,7 @@ impl NodeRuntime {
thumbnail_renders: Default::default(),
click_targets: HashMap::new(),
vector_modify: HashMap::new(),
upstream_transforms: HashMap::new(),
}
}
@@ -166,6 +170,7 @@ impl NodeRuntime {
result,
responses,
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(),
resolved_types: self.resolved_types.clone(),
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!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()),
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 {
Ok(value) => value,
@@ -326,6 +331,9 @@ impl NodeRuntime {
});
*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:
@@ -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)]
pub struct NodeGraphExecutor {
sender: Sender<NodeRuntimeMessage>,
@@ -394,9 +409,7 @@ impl Default for NodeGraphExecutor {
fn default() -> Self {
let (request_sender, request_receiver) = std::sync::mpsc::channel();
let (response_sender, response_receiver) = std::sync::mpsc::channel();
NODE_RUNTIME.with(|runtime| {
runtime.borrow_mut().replace(NodeRuntime::new(request_receiver, response_sender));
});
replace_node_runtime(NodeRuntime::new(request_receiver, response_sender));
Self {
futures: Default::default(),
@@ -556,6 +569,7 @@ impl NodeGraphExecutor {
result,
responses: existing_responses,
new_click_targets,
new_vector_modify,
new_upstream_transforms,
resolved_types,
node_graph_errors,
@@ -567,15 +581,18 @@ impl NodeGraphExecutor {
responses.add(NodeGraphMessage::SendGraph);
responses.add(OverlaysMessage::Draw);
let Ok(node_graph_output) = result else {
// Clear the click targets while the graph is in an un-renderable state
document.metadata.update_click_targets(HashMap::new());
let node_graph_output = match result {
Ok(output) => output,
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_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())?;
if let Some(export_config) = execution_context.export_config {