mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 19:48:13 +08:00
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:
co-authored by
Keavon Chambers
Adam
Dennis Kobert
parent
fd3613018a
commit
1652c713a6
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
+9
-26
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user