diff --git a/editor/src/messages/clipboard/clipboard_message_handler.rs b/editor/src/messages/clipboard/clipboard_message_handler.rs index 77d0da3c563..fd977c2f49e 100644 --- a/editor/src/messages/clipboard/clipboard_message_handler.rs +++ b/editor/src/messages/clipboard/clipboard_message_handler.rs @@ -454,6 +454,7 @@ impl MessageHandler> for Clipboard // Create new segment IDs and add the segments into the existing Vector path let mut segments_map = HashMap::new(); + let path_beginnings = pasted_vector.path_beginnings(&pasted_paths); for (segment_id, segment, start, end) in pasted_vector.segment_iter(&pasted_paths) { let (Some(&start_point), Some(&end_point)) = (points_map.get(&start), points_map.get(&end)) else { warn!("Skipping pasted vector segment with an unknown endpoint"); @@ -474,6 +475,15 @@ impl MessageHandler> for Clipboard let points = [start_point, end_point]; let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + // Every pasted path stays a path of its own, even where it sets out from the end of another + if path_beginnings.contains(&segment_id) { + let modification_type = VectorModificationType::SetPathBreak { + segment: new_segment_id, + breaks: true, + }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } } // Set G1 continuity diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 6511f4d7d4e..668dc412e78 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -21,7 +21,7 @@ use graphene_std::transform::ScaleType; use graphene_std::uuid::NodeId; use graphene_std::vector::graphic_types; use graphene_std::vector::style::{GradientRamp, GradientSpread, PaintOrder, StrokeAlign}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::f64::consts::PI; use std::ops::Range; @@ -1634,6 +1634,7 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_ migrate_node(node_id, node, network_path, document, reset_node_definitions_on_open); } create_layer_stacks(document); + follow_segments_turned_on_load(document); // The old geometry-producing "Text" node was split into the current "Text" (`String[]`) -> converter pair, which reuses the same proto // identifier. Runs after `migrate_node` normalizes old text nodes to the legacy 13-input layout, distinguished from the current 12-input @@ -3347,6 +3348,136 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], Some(()) } +// TODO: Eventually remove this document upgrade code +/// Older stored edits are turned around where they chain into paths on load. Edits naming those segments by side (a handle, +/// an endpoint, or a G1 pair) in the Path nodes downstream of the one that turned them are turned with them, so each stays on +/// the side it was made on. Segment IDs are only unique within one shape, so nodes not downstream are left alone. +fn follow_segments_turned_on_load(document: &mut DocumentMessageHandler) { + for (network_path, node_id, index, input) in modifications_following_turned_segments(document.network_interface.document_network()) { + document.network_interface.set_input(&InputConnector::node_at_index(node_id, index), input, &network_path); + } +} + +// TODO: Eventually remove this document upgrade code +/// The stored edits to rewrite after loading, each with its network path, node, and input index: those that turned segments, +/// with their record taken, and those downstream of them, turned to follow. Costs O(nodes + wires) per node that turned any. +fn modifications_following_turned_segments(network: &graph_craft::document::NodeNetwork) -> Vec<(Vec, NodeId, usize, NodeInput)> { + type Location = (Vec, NodeId); + + // The nodes reading each node's data, wired straight to the node producing it through nested networks' imports and exports + let mut readers: HashMap> = HashMap::new(); + let mut turned_by: Vec<(Location, HashSet)> = Vec::new(); + for (node_id, node, network_path) in network.recursive_nodes() { + let here = (network_path.clone(), *node_id); + + for input in &node.inputs { + if let NodeInput::Value { tagged_value, .. } = input + && let TaggedValue::VectorModification(modification) = &**tagged_value + && !modification.segments_turned_on_load().is_empty() + { + turned_by.push((here.clone(), modification.segments_turned_on_load().clone())); + } + } + + // A network node's inputs only pass through to the nodes inside it, which are wired as readers themselves + if matches!(node.implementation, DocumentNodeImplementation::Network(_)) { + continue; + } + for input in &node.inputs { + if let Some(producer) = producing_node(network, &network_path, input, 0) { + readers.entry(producer).or_default().push(here.clone()); + } + } + } + if turned_by.is_empty() { + return Vec::new(); + } + + // Each node's turned segments reach the nodes reading its output, then their readers, and so on + let mut following: HashMap> = HashMap::new(); + for (origin, turned) in &turned_by { + let (origin_path, origin_id) = origin; + let Some(origin_node) = network.nested_network(origin_path).and_then(|nested| nested.nodes.get(origin_id)) else { + continue; + }; + + // Where the output comes from: the node itself, or the nodes a network node exports + let sources: Vec = match &origin_node.implementation { + DocumentNodeImplementation::Network(nested) => (0..nested.exports.len()) + .filter_map(|output| producing_node(network, origin_path, &NodeInput::node(*origin_id, output), 0)) + .collect(), + _ => vec![origin.clone()], + }; + + let mut reached = HashSet::new(); + let mut pending: Vec = sources.iter().flat_map(|source| readers.get(source).into_iter().flatten().cloned()).collect(); + while let Some(location) = pending.pop() { + if reached.insert(location.clone()) { + pending.extend(readers.get(&location).into_iter().flatten().cloned()); + } + } + + // An edit follows when the node holding it, or a node inside it, reads the turned data + for (path, node_id) in reached { + let enclosing = (0..path.len()).map(|depth| (path[..depth].to_vec(), path[depth])); + for holder in enclosing.chain([(path.clone(), node_id)]).filter(|holder| holder != origin) { + following.entry(holder).or_default().extend(turned.iter().copied()); + } + } + } + + let mut rewritten = Vec::new(); + for (node_id, node, network_path) in network.recursive_nodes() { + let location = (network_path.clone(), *node_id); + let follows = following.get(&location); + + for (index, input) in node.inputs.iter().enumerate() { + let NodeInput::Value { tagged_value, exposed } = input else { continue }; + let TaggedValue::VectorModification(modification) = &**tagged_value else { continue }; + let had_record = !modification.segments_turned_on_load().is_empty(); + if !had_record && follows.is_none() { + continue; + } + + let mut modification = modification.clone(); + modification.take_segments_turned_on_load(); + let changed = follows.is_some_and(|turned| modification.follow_segments_turned_elsewhere(turned)); + + if had_record || changed { + let input = NodeInput::value(TaggedValue::VectorModification(modification), *exposed); + rewritten.push((network_path.clone(), *node_id, index, input)); + } + } + } + + rewritten +} + +// TODO: Eventually remove this document upgrade code +/// The node, not itself a network, producing the data an input reads, looking through the imports and exports of nested networks. +fn producing_node(network: &graph_craft::document::NodeNetwork, network_path: &[NodeId], input: &NodeInput, depth: usize) -> Option<(Vec, NodeId)> { + // Nesting is shallow in practice, so this only guards against a malformed document wiring a network back into itself + if depth > 64 { + return None; + } + + match input { + NodeInput::Node { node_id, output_index } => { + let node = network.nested_network(network_path)?.nodes.get(node_id)?; + match &node.implementation { + DocumentNodeImplementation::Network(nested) => producing_node(network, &[network_path, &[*node_id]].concat(), nested.exports.get(*output_index)?, depth + 1), + _ => Some((network_path.to_vec(), *node_id)), + } + } + NodeInput::Import { import_index, .. } => { + let (parent, parent_path) = network_path.split_last()?; + let parent_node = network.nested_network(parent_path)?.nodes.get(parent)?; + producing_node(network, parent_path, parent_node.inputs.get(*import_index)?, depth + 1) + } + _ => None, + } +} + /// Ensure layers are positioned as stacks if they are upstream siblings of another layer fn create_layer_stacks(document: &mut DocumentMessageHandler) { document.network_interface.load_structure(); @@ -4081,4 +4212,115 @@ mod tests { panic!("Duplicate entries in `NODE_REPLACEMENTS`: {duplicates:?}"); } } + + // Segment IDs repeat across shapes, so a segment turned on load is followed only by the stored edits downstream of the node that turned it + #[test] + fn turned_segments_are_followed_only_downstream() { + use graph_craft::document::NodeNetwork; + use graphene_std::vector::misc::HandleId; + use graphene_std::vector::{SegmentId, VectorModification, VectorModificationType}; + + let segment = SegmentId::generate(); + let turned = VectorModification::default().with_segments_turned_on_load(HashSet::from([segment])); + let edited_on = |handle: HandleId| { + let mut modification = VectorModification::default(); + modification.modify(&VectorModificationType::SetHandlePosition { handle, relative_position: DVec2::X }); + modification + }; + let value = |modification: VectorModification| NodeInput::value(TaggedValue::VectorModification(Box::new(modification)), false); + let node = |inputs: Vec| DocumentNode { inputs, ..Default::default() }; + + // One turned Path node feeds another directly, and a second sits inside a nested network whose export feeds a third + let [turning, following, unrelated, group, following_group] = [1, 2, 3, 4, 5].map(NodeId); + let nested = NodeNetwork { + exports: vec![NodeInput::node(turning, 0)], + nodes: [(turning, node(vec![value(turned.clone())]))].into_iter().collect(), + ..Default::default() + }; + let network = NodeNetwork { + nodes: [ + (turning, node(vec![value(turned)])), + (following, node(vec![NodeInput::node(turning, 0), value(edited_on(HandleId::primary(segment)))])), + (unrelated, node(vec![value(edited_on(HandleId::primary(segment)))])), + ( + group, + DocumentNode { + implementation: DocumentNodeImplementation::Network(nested), + ..Default::default() + }, + ), + (following_group, node(vec![NodeInput::node(group, 0), value(edited_on(HandleId::primary(segment)))])), + ] + .into_iter() + .collect(), + ..Default::default() + }; + + let rewritten = modifications_following_turned_segments(&network); + let rewritten_at = |location: (&[NodeId], NodeId)| rewritten.iter().find(|(path, node_id, ..)| (path.as_slice(), *node_id) == location); + + assert!(rewritten_at((&[], turning)).is_some(), "the record of what turned is taken"); + assert!(rewritten_at((&[group], turning)).is_some(), "the nested record of what turned is taken"); + assert!(rewritten_at((&[], unrelated)).is_none(), "an edit not downstream stays as it was"); + + let expected = value(edited_on(HandleId::end(segment))); + for downstream in [following, following_group] { + let (.., index, input) = rewritten_at((&[], downstream)).expect("an edit downstream follows the turn"); + assert_eq!(*index, 1); + assert_eq!(*input, expected); + } + } + + // Inside a network fed by a turned node on one input and something else on another, only the edit reading the turned input follows + #[test] + fn turned_segments_follow_only_the_network_input_they_enter() { + use graph_craft::document::NodeNetwork; + use graphene_std::vector::misc::HandleId; + use graphene_std::vector::{SegmentId, VectorModification, VectorModificationType}; + + let segment = SegmentId::generate(); + let turned = VectorModification::default().with_segments_turned_on_load(HashSet::from([segment])); + let mut edited = VectorModification::default(); + edited.modify(&VectorModificationType::SetHandlePosition { + handle: HandleId::primary(segment), + relative_position: DVec2::X, + }); + let value = |modification: VectorModification| NodeInput::value(TaggedValue::VectorModification(Box::new(modification)), false); + let import = |import_index: usize| NodeInput::Import { + import_type: concrete!(()), + import_index, + }; + let node = |inputs: Vec| DocumentNode { inputs, ..Default::default() }; + + let [turning, other, group, reads_turned, reads_other] = [1, 2, 3, 4, 5].map(NodeId); + let nested = NodeNetwork { + exports: vec![NodeInput::node(reads_turned, 0)], + nodes: [(reads_turned, node(vec![import(0), value(edited.clone())])), (reads_other, node(vec![import(1), value(edited)]))] + .into_iter() + .collect(), + ..Default::default() + }; + let network = NodeNetwork { + nodes: [ + (turning, node(vec![value(turned)])), + (other, node(Vec::new())), + ( + group, + DocumentNode { + inputs: vec![NodeInput::node(turning, 0), NodeInput::node(other, 0)], + implementation: DocumentNodeImplementation::Network(nested), + ..Default::default() + }, + ), + ] + .into_iter() + .collect(), + ..Default::default() + }; + + let rewritten = modifications_following_turned_segments(&network); + let rewritten_at = |node_id: NodeId| rewritten.iter().any(|(path, rewritten_id, ..)| path.as_slice() == [group] && *rewritten_id == node_id); + assert!(rewritten_at(reads_turned)); + assert!(!rewritten_at(reads_other)); + } } diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index d3b73dd8576..4f790f9727a 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -164,11 +164,11 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde } /// Merge the `first_endpoint` with `second_endpoint`. -pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, first_endpoint: PointId, second_endpont: PointId, responses: &mut VecDeque) { +pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, first_endpoint: PointId, second_endpoint: PointId, responses: &mut VecDeque) { let transform = document.metadata().transform_to_document(layer); let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return }; - let segment = vector.segment_iter().find(|(_, _, start, end)| *end == second_endpont || *start == second_endpont); + let segment = vector.segment_iter().find(|(_, _, start, end)| *end == second_endpoint || *start == second_endpoint); let Some((segment, _, mut segment_start_point, mut segment_end_point)) = segment else { log::error!("Could not get the segment for second_endpoint."); return; @@ -190,20 +190,28 @@ pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifie handles[1] = Some(anchor_to_handle); } - if segment_start_point == second_endpont { - core::mem::swap(&mut segment_start_point, &mut segment_end_point); - handles.reverse(); + // The segment keeps its direction, with whichever end sat on the merged-away point moving to the point it merges into + if segment_start_point == second_endpoint { + segment_start_point = first_endpoint; + } else { + segment_end_point = first_endpoint; } - let modification_type = VectorModificationType::RemovePoint { id: second_endpont }; + let modification_type = VectorModificationType::RemovePoint { id: second_endpoint }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); let modification_type = VectorModificationType::RemoveSegment { id: segment }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); - let points = [segment_start_point, first_endpoint]; + let points = [segment_start_point, segment_end_point]; let id = SegmentId::generate(); let modification_type = VectorModificationType::InsertSegment { id, points, handles }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + // The replacement takes over as the beginning of the path that the removed segment began + if vector.path_beginnings().contains(&segment) { + let modification_type = VectorModificationType::SetPathBreak { segment: id, breaks: true }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } } /// Create a new bitmap layer. diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index be21f9e96b6..99d09532f88 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -11,7 +11,9 @@ use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSele use glam::{DAffine2, DVec2}; use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table; use graphene_std::vector::algorithms::shapes::polyline_bezpath; -use graphene_std::vector::misc::{BezierHandles, HandleId, ManipulatorPointId, PathSegPoints, bezpath_from_manipulator_groups, dvec2_to_point, pathseg_points, point_to_dvec2, segment_to_handles}; +use graphene_std::vector::misc::{ + BezierHandles, HandleId, HandleType, ManipulatorPointId, PathSegPoints, bezpath_from_manipulator_groups, dvec2_to_point, pathseg_points, point_to_dvec2, segment_to_handles, +}; use graphene_std::vector::{HandleExt, PointId, SegmentId, VectorGeometry, VectorModificationType}; use kurbo::{Affine, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveNearest, PathSeg, Rect, Shape}; use std::f64::consts::TAU; @@ -187,6 +189,7 @@ pub struct ClosestSegment { bezier: PathSeg, points: [PointId; 2], colinear: [Option; 2], + begins_path: bool, t: f64, bezier_point_to_viewport: DVec2, } @@ -277,6 +280,15 @@ impl ClosestSegment { }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); + // The first half takes over as the beginning of the path that the split segment began + if self.begins_path { + let modification_type = VectorModificationType::SetPathBreak { + segment: segment_ids[0], + breaks: true, + }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + // Last segment let modification_type = VectorModificationType::InsertSegment { id: segment_ids[1], @@ -1360,9 +1372,11 @@ impl ShapeState { } } - // Add in new segment if possible + // Add in new segment if possible, running on from the segment that arrives at the anchor into the one that leaves it let mut handles = vector.handle_pair_of(ManipulatorPointId::Anchor(anchor))?; - handles.reverse(); + if handles[0].ty != HandleType::End || handles[1].ty != HandleType::Primary { + handles.reverse(); + } let opposites = handles.map(|handle| handle.opposite()); let [Some(start), Some(end)] = opposites.map(|opposite| vector.anchor_of(opposite.to_manipulator_point())) else { @@ -1380,6 +1394,7 @@ impl ShapeState { let mut deleted_anchors = HashSet::new(); let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; let colinear_pairs = vector.colinear_pairs(); + let path_beginnings = vector.path_beginnings(); let selected_segments = &state.selected_segments; @@ -1460,6 +1475,12 @@ impl ShapeState { responses.add(GraphOperationMessage::Vector { layer, modification_type }); + // Removing the segment dropped its mark as the beginning of a path, which the reconnection takes over + if path_beginnings.contains(&segment) { + let modification_type = VectorModificationType::SetPathBreak { segment, breaks: true }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + for &handles in &colinear_pairs { if !handles.iter().any(|&handle| handle == start.0.opposite() || handle == end.0.opposite()) { continue; @@ -1784,6 +1805,7 @@ impl ShapeState { bezier: segment, points: [start, end], colinear: [primary_handle, end_handle], + begins_path: false, t, bezier_point_to_viewport: screenspace, layer, @@ -1791,6 +1813,10 @@ impl ShapeState { } } + if let Some(closest) = &mut closest { + closest.begins_path = vector.path_beginnings().contains(&closest.segment); + } + closest } diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index 8192407eced..754b3defd1f 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -14,7 +14,7 @@ use graph_craft::document::value::TaggedValue; use graphene_std::renderer::Quad; use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table; use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point}; -use graphene_std::vector::{HandleExt, PointId, SegmentId, VectorGeometry, VectorModification, VectorModificationType}; +use graphene_std::vector::{HandleExt, PointId, SegmentId, VectorGeometry, VectorModificationType}; use kurbo::{CubicBez, DEFAULT_ACCURACY, ParamCurve, PathSeg, Point, Shape}; /// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable. @@ -618,7 +618,7 @@ pub fn make_path_editable_is_allowed(network_interface: &mut NodeNetworkInterfac // Must not already have an existing Path node, in the right-most part of the layer chain, which has an empty set of modifications // (otherwise users could repeatedly keep running this command and stacking up empty Path nodes) if let Some(TaggedValue::VectorModification(modifications)) = NodeGraphLayer::new(first_layer, network_interface).find_input(&DefinitionIdentifier::Network("Path".into()), 1) - && modifications.as_ref() == &VectorModification::default() + && modifications.is_empty() { return None; } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 7378fe3aae8..ab10565fda0 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -2799,6 +2799,7 @@ impl Fsm for PathToolFsmState { } let find_index = |new_vector: &WorkingVector, id: PointId| new_vector.point_ids.iter().position(|&point_id| point_id == id); + let path_beginnings = old_vector.path_beginnings(); // Add segments which have selected ends for (segment_id, segment, start, end) in old_vector.segment_iter() { @@ -2815,6 +2816,7 @@ impl Fsm for PathToolFsmState { new_vector.segment_start.push(start_index); new_vector.segment_end.push(end_index); new_vector.segment_handles.push(segment_to_handles(&segment)); + new_vector.segment_begins_path.push(path_beginnings.contains(&segment_id)); } } @@ -2899,6 +2901,7 @@ impl Fsm for PathToolFsmState { // Create new segment IDs and add the segments into the existing vector content let mut segments_map = HashMap::new(); + let path_beginnings = pasted.path_beginnings(); for (segment_id, bezier, start, end) in pasted.segment_iter() { let new_segment_id = SegmentId::generate(); @@ -2911,6 +2914,15 @@ impl Fsm for PathToolFsmState { let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + // Every pasted path stays a path of its own, even where it sets out from the end of another + if path_beginnings.contains(&segment_id) { + let modification_type = VectorModificationType::SetPathBreak { + segment: new_segment_id, + breaks: true, + }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } } // Set G1 continuity @@ -2999,6 +3011,7 @@ impl Fsm for PathToolFsmState { } let mut segments_map = HashMap::new(); + let path_beginnings = old_vector.path_beginnings(); for (segment_id, bezier, start, end) in old_vector.segment_iter() { let both_ends_selected = layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(end)); @@ -3016,6 +3029,12 @@ impl Fsm for PathToolFsmState { let modification_type = VectorModificationType::InsertSegment { id: new_id, points, handles }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + // Every duplicated path stays a path of its own, even where it sets out from the end of another + if path_beginnings.contains(&segment_id) { + let modification_type = VectorModificationType::SetPathBreak { segment: new_id, breaks: true }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } } } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 086d632bd1b..729dcebbaea 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -779,8 +779,8 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize } "Vector" | "VectorData" => { let vector = graphic_types::migrations::migrate_to_optional_vector(content.clone()).map_err(serde::de::Error::custom)?; - if let Some((vector, paths)) = vector { - let modification = Box::new(VectorModification::create_from_vector(&vector, &paths)); + if let Some((vector, paths, turned)) = vector { + let modification = Box::new(VectorModification::create_from_vector(&vector, &paths).with_segments_turned_on_load(turned)); return Ok(MemoHash::new(TaggedValue::VectorModification(modification))); } return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Vector)))); diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 1903291f510..82a375bdbdb 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -135,7 +135,7 @@ pub mod migrations { } impl LegacyVector { - pub(super) fn into_parts(self) -> (Vector, List) { + pub(super) fn into_parts(self) -> (Vector, List, std::collections::HashSet) { vector_from_legacy_parts( self.point_domain.id, self.point_domain.position, @@ -168,7 +168,10 @@ pub mod migrations { // TODO: Eventually remove this document upgrade code /// Returns the first vector recovered from any of the legacy on-disk shapes (the legacy `VectorData` flat struct, a legacy `Vector` struct, or any of the historical list variants). - pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result)>, D::Error> { + #[allow(clippy::type_complexity)] + pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result, std::collections::HashSet)>, D::Error> { use serde::Deserialize; #[derive(serde::Deserialize)] @@ -257,7 +260,7 @@ pub mod migrations { .unwrap() .insert("fill".into(), serde_json::to_value(legacy::LegacyFill::default()).unwrap()); - let (migrated, _paths) = migrate_to_optional_vector(value).unwrap().expect("the legacy shape should parse into a vector"); + let (migrated, _paths, _) = migrate_to_optional_vector(value).unwrap().expect("the legacy shape should parse into a vector"); assert_eq!( migrated.positions().collect::>(), @@ -277,7 +280,7 @@ pub mod migrations { }; let value = serde_json::to_value(&vector).unwrap(); - let (migrated, _paths) = migrate_to_optional_vector(value).unwrap().unwrap(); + let (migrated, _paths, _) = migrate_to_optional_vector(value).unwrap().unwrap(); assert_eq!(migrated.positions().collect::>(), [glam::DVec2::new(3., 4.)]); } @@ -291,7 +294,7 @@ pub mod migrations { "segment_domain": { "ids": [7], "start_point": [0], "end_point": [1], "handles": ["Linear"] }, }); - let (migrated, paths) = migrate_to_optional_vector(value).unwrap().expect("the plural field names should parse"); + let (migrated, paths, _) = migrate_to_optional_vector(value).unwrap().expect("the plural field names should parse"); assert_eq!(migrated.positions().collect::>(), [glam::DVec2::new(3., 4.), glam::DVec2::new(5., 6.)]); assert_eq!(paths.len(), 1); @@ -316,7 +319,7 @@ pub mod migrations { }, }); - let (vector, paths) = migrate_to_optional_vector(value).unwrap().expect("the legacy vector should parse"); + let (vector, paths, _) = migrate_to_optional_vector(value).unwrap().expect("the legacy vector should parse"); // Looked up by ID, since storage order is free to differ from the legacy order let mut anchors: Vec<(u64, DVec2)> = vector diff --git a/node-graph/libraries/vector-types/src/vector/flatten.rs b/node-graph/libraries/vector-types/src/vector/flatten.rs index 3296a5807fa..da3cb3a8ff8 100644 --- a/node-graph/libraries/vector-types/src/vector/flatten.rs +++ b/node-graph/libraries/vector-types/src/vector/flatten.rs @@ -1,10 +1,11 @@ use super::misc::{BezierHandles, HandleId, HandleType, segment_to_handles}; -use super::path::{CurveType, Path, curve_type_at, push_path}; +use super::path::{CurveType, Path, continues_previous_path, curve_type_at, push_path}; use super::vector_attributes::{PointId, SegmentId}; use super::vector_types::Vector; use core_types::list::{ATTR_ID, List}; use glam::DVec2; use rustc_hash::{FxHashMap, FxHashSet}; +use std::collections::HashSet; /// One path under construction: its curve type, the pool references it weaves, and the IDs of the spans between them. type Run = (CurveType, Vec, Vec); @@ -48,7 +49,8 @@ impl WorkingVector { let stride = curve_type.stride(); let span_ids = path.0.iter_attribute_values::(ATTR_ID).map(|ids| ids.as_slice()).unwrap_or_default(); - let mut begins_path = true; + // A path that only carries on the one before it in another curve type is no beginning, so the two rejoin if their types ever match + let mut begins_path = !continues_previous_path(paths, path_index); for span in 0..path.segment_count(curve_type) { let Some(segment) = path.segment(&vector.0, curve_type, span) else { continue }; let (Some(start), Some(end)) = (path.reference(span * stride), path.reference(span * stride + stride)) else { @@ -70,6 +72,14 @@ impl WorkingVector { working } + /// The number of segments that every parallel segment array holds. + fn segment_count(&self) -> usize { + [self.segment_ids.len(), self.segment_start.len(), self.segment_end.len(), self.segment_handles.len()] + .into_iter() + .min() + .unwrap_or(0) + } + /// Hands each removed segment's mark as the beginning of a path to the next kept segment, which begins what is left of that path. pub(crate) fn carry_path_beginnings(&mut self, keep: &[bool]) { let mut carried = false; @@ -83,11 +93,14 @@ impl WorkingVector { } } - /// Weaves the working form back into a pool and paths, gathering consecutive segments that meet head to tail into one path - /// until it closes or a segment begins a path of its own. + /// Weaves the working form back into a pool and paths in O(points + segments). Segments chain head to tail into paths: + /// each runs on into the segment stored after it when that one sets out from its end, and segments left unlinked pair + /// up where they are the only two loose ends at an anchor. A segment marked as beginning a path joins nothing before it. pub fn reweave(self, vector: &mut Vector, paths: &mut List) { *paths = List::new(); + let segment_count = self.segment_count(); + // The pool is built as plain arrays and handed to the list whole, anchors first and each segment's handles after let anchor_count = self.point_ids.len().min(self.point_positions.len()); let mut positions = self.point_positions; @@ -95,46 +108,113 @@ impl WorkingVector { let mut point_ids = self.point_ids; point_ids.truncate(anchor_count); - let mut runs: Vec = Vec::new(); + let drawable = |index: usize| self.segment_start[index] < anchor_count && self.segment_end[index] < anchor_count; + let begins_path = |index: usize| self.segment_begins_path.get(index).copied().unwrap_or(false); + + // Each segment's successor and predecessor in its chain, at most one of each. A segment carries on into the one + // stored after it when that one sets out from its end. + const NONE: u32 = u32::MAX; + let (mut successor, mut predecessor) = (vec![NONE; segment_count], vec![NONE; segment_count]); + for index in (1..segment_count).filter(|&index| drawable(index) && drawable(index - 1)) { + if self.segment_start[index] == self.segment_end[index - 1] && !begins_path(index) { + successor[index - 1] = index as u32; + predecessor[index] = (index - 1) as u32; + } + } + + // Segments left unlinked pair up where one arriving and one setting out are the only such two at an anchor, which + // rejoins a path around segments that were replaced and so are stored out of order + let (mut loose_arrivals, mut loose_departures) = (vec![(0_u8, NONE); anchor_count], vec![(0_u8, NONE); anchor_count]); + for index in (0..segment_count).filter(|&index| drawable(index)) { + if successor[index] == NONE { + let (count, arrival) = &mut loose_arrivals[self.segment_end[index]]; + (*count, *arrival) = (count.saturating_add(1), index as u32); + } + if predecessor[index] == NONE && !begins_path(index) { + let (count, departure) = &mut loose_departures[self.segment_start[index]]; + (*count, *departure) = (count.saturating_add(1), index as u32); + } + } + for (&(arrivals, arrival), &(departures, departure)) in loose_arrivals.iter().zip(&loose_departures) { + if arrivals == 1 && departures == 1 && arrival != departure { + successor[arrival as usize] = departure; + predecessor[departure as usize] = arrival; + } + } // Only segments a G1 pair names need their anchors recorded, to narrow the pairs to smooth flags below let paired: FxHashSet = self.colinear.iter().flat_map(|pair| pair.map(|handle| handle.segment)).collect(); let mut segment_anchors = FxHashMap::default(); - let ends = self.segment_start.iter().zip(&self.segment_end); - for (index, ((&id, (&start, &end)), handles)) in self.segment_ids.iter().zip(ends).zip(&self.segment_handles).enumerate() { - if start >= anchor_count || end >= anchor_count { + let curve_type_of = |index: usize| match self.segment_handles[index] { + BezierHandles::Linear => CurveType::Linear, + BezierHandles::Quadratic { .. } => CurveType::Quadratic, + BezierHandles::Cubic { .. } => CurveType::Cubic, + }; + + // Each chain is woven where its earliest stored segment sits, so paths keep their order. A path holds one curve + // type, so a chain changing type starts another path right after, as does one arriving back where it set out. + let mut runs: Vec = Vec::new(); + let mut woven = vec![false; segment_count]; + let mut chain_start = usize::MAX; + for seed in (0..segment_count).filter(|&seed| drawable(seed)) { + if woven[seed] { continue; } - let (curve_type, handle_positions) = match *handles { - BezierHandles::Linear => (CurveType::Linear, Vec::new()), - BezierHandles::Quadratic { handle } => (CurveType::Quadratic, vec![handle]), - BezierHandles::Cubic { handle_start, handle_end } => (CurveType::Cubic, vec![handle_start, handle_end]), - }; - - // Runs stay in segment order, which is the order the paths come out in. A run carries on through a - // junction and gives way once it has closed, or when the next segment opens a path of its own. - let begins_path = self.segment_begins_path.get(index).copied().unwrap_or(false); - let continues_run = !begins_path - && runs - .last() - .is_some_and(|(run_curve_type, references, _)| *run_curve_type == curve_type && references.last() == Some(&(start as u32)) && references.first() != references.last()); - if !continues_run { - runs.push((curve_type, vec![start as u32], Vec::new())); + // Back to where the chain begins, which for a loop is the segment the search set out from + let mut index = seed; + loop { + let previous = predecessor[index] as usize; + if previous >= segment_count || woven[previous] { + break; + } + index = previous; + if index == seed { + break; + } } - let Some((_, references, ids)) = runs.last_mut() else { continue }; - for position in handle_positions { - references.push(positions.len() as u32); - positions.push(position); - point_ids.push(PointId::UNNAMED); + // Setting out in another curve type from where the last path ended reads back as carrying it on, so it is woven that way + let carries_on = runs + .last() + .is_some_and(|(curve_type, references, _)| *curve_type != curve_type_of(index) && references.last() == Some(&(self.segment_start[index] as u32))); + if !carries_on { + chain_start = self.segment_start[index]; } - references.push(end as u32); - ids.push(id); + let mut continues_run = false; + loop { + woven[index] = true; + let (id, start, end) = (self.segment_ids[index], self.segment_start[index], self.segment_end[index]); + let (curve_type, handle_positions) = match self.segment_handles[index] { + BezierHandles::Linear => (CurveType::Linear, [None, None]), + BezierHandles::Quadratic { handle } => (CurveType::Quadratic, [Some(handle), None]), + BezierHandles::Cubic { handle_start, handle_end } => (CurveType::Cubic, [Some(handle_start), Some(handle_end)]), + }; - if paired.contains(&id) { - segment_anchors.insert(id, (start as u32, end as u32)); + if !continues_run || runs.last().is_none_or(|(run_curve_type, ..)| *run_curve_type != curve_type) { + runs.push((curve_type, vec![start as u32], Vec::new())); + } + continues_run = end != chain_start; + + let Some((_, references, ids)) = runs.last_mut() else { break }; + for position in handle_positions.into_iter().flatten() { + references.push(positions.len() as u32); + positions.push(position); + point_ids.push(PointId::UNNAMED); + } + references.push(end as u32); + ids.push(id); + + if paired.contains(&id) { + segment_anchors.insert(id, (start as u32, end as u32)); + } + + let next = successor[index] as usize; + if next >= segment_count || woven[next] { + break; + } + index = next; } } @@ -163,126 +243,32 @@ impl WorkingVector { } /// Reorders and reorients the segments so each continues the one it meets head to tail, taking the straightest - /// route through every junction, and marks where each chain begins. This bakes path structure into a segment soup once. - pub fn chain_by_straightest_route(&mut self) { - let segment_count = [self.segment_ids.len(), self.segment_start.len(), self.segment_end.len(), self.segment_handles.len()] - .into_iter() - .min() - .unwrap_or(0); + /// route through every junction, and marks where each chain begins. This bakes path structure into a segment soup once, + /// returning the IDs of the segments it turns around. + pub fn chain_by_straightest_route(&mut self) -> HashSet { + let segment_count = self.segment_count(); let point_count = self.point_positions.len(); + let endpoints: Vec<(usize, usize)> = (0..segment_count).map(|index| (self.segment_start[index], self.segment_end[index])).collect(); - // Every segment end at each point, named by twice the segment's index plus one for its arriving end - let mut ends_at: Vec> = vec![Vec::new(); point_count]; - for index in 0..segment_count { - let (start, end) = (self.segment_start[index], self.segment_end[index]); - if start < point_count && end < point_count { - ends_at[start].push(index * 2); - ends_at[end].push(index * 2 + 1); - } - } - - // The direction a segment leaves its anchor in from the given end, toward the nearest control point not sitting on the anchor - let direction = |end: usize| { - let (index, arriving) = (end / 2, end % 2 == 1); - let (start, finish) = (self.point_positions[self.segment_start[index]], self.point_positions[self.segment_end[index]]); - let handles = self.segment_handles[index]; + // The direction a segment leaves its anchor in from either end, toward the nearest control point not sitting on the anchor + let direction = |segment: usize, arriving: bool| { + let (start, finish) = (self.point_positions[self.segment_start[segment]], self.point_positions[self.segment_end[segment]]); + let handles = self.segment_handles[segment]; let (anchor, toward) = if arriving { (finish, [handles.end(), handles.start(), Some(start)]) } else { (start, [handles.start(), handles.end(), Some(finish)]) }; - let along = toward.into_iter().flatten().map(|point| point - anchor).find(|along| along.length_squared() > 1e-12); - along.unwrap_or_default().normalize_or_zero() + toward.into_iter().flatten().map(|point| point - anchor).find(|along| along.length_squared() > 1e-12) }; - - // Pair off the ends meeting at each point, the straightest pair through a junction first, in O(k² log k) for k ends there - let mut partner = vec![usize::MAX; segment_count * 2]; - for ends in ends_at.iter().filter(|ends| ends.len() >= 2) { - let directions: Vec = ends.iter().map(|&end| direction(end)).collect(); - - // Straightness is rounded so pairs differing only by float error are equally straight, leaving the earlier stored ends first - let mut pairs: Vec<(i64, usize, usize)> = (0..ends.len()) - .flat_map(|i| (i + 1..ends.len()).map(move |j| (i, j))) - .map(|(i, j)| ((-directions[i].dot(directions[j]) * 1e9).round() as i64, i, j)) - .collect(); - pairs.sort_by(|a, b| b.0.cmp(&a.0).then((a.1, a.2).cmp(&(b.1, b.2)))); - - let mut paired = vec![false; ends.len()]; - for (_, i, j) in pairs { - if paired[i] || paired[j] { - continue; - } - paired[i] = true; - paired[j] = true; - partner[ends[i]] = ends[j]; - partner[ends[j]] = ends[i]; - } - } - - // Follow the pairings into chains, each seeded by the earliest unvisited segment and keeping its direction - let mut visited = vec![false; segment_count]; - let mut order: Vec<(usize, bool)> = Vec::with_capacity(segment_count); - let mut begins_path = Vec::with_capacity(segment_count); - for seed in 0..segment_count { - if std::mem::replace(&mut visited[seed], true) { - continue; - } - - let mut chain = vec![(seed, false)]; - let mut closed = false; - let mut end = seed * 2 + 1; - loop { - let next = partner[end]; - if next == usize::MAX { - break; - } - let segment = next / 2; - if segment == seed { - closed = true; - break; - } - if std::mem::replace(&mut visited[segment], true) { - break; - } - - // Entering at the segment's arriving end crosses it backwards - let reversed = next % 2 == 1; - chain.push((segment, reversed)); - end = if reversed { segment * 2 } else { segment * 2 + 1 }; - } - - // An open chain also grows backwards from the seed's start, which is then where it begins - let mut before = Vec::new(); - let mut end = seed * 2; - if !closed { - loop { - let next = partner[end]; - if next == usize::MAX { - break; - } - let segment = next / 2; - if std::mem::replace(&mut visited[segment], true) { - break; - } - - // Joining at the segment's arriving end means it runs forward into the chain - let reversed = next % 2 == 0; - before.push((segment, reversed)); - end = if reversed { segment * 2 + 1 } else { segment * 2 }; - } - } - before.reverse(); - - begins_path.extend((0..before.len() + chain.len()).map(|position| position == 0)); - order.append(&mut before); - order.append(&mut chain); - } + // A quadratic keeps its direction, since an edit elsewhere to its lone handle could not be re-based onto its other end + let chained = chain_order(point_count, &endpoints, direction, |segment| !matches!(self.segment_handles[segment], BezierHandles::Quadratic { .. })); // Rebuild the segment arrays in chain order, turning around the segments that run against their chain let mut reversed_ids = FxHashSet::default(); let (mut ids, mut starts, mut ends, mut handles) = (Vec::new(), Vec::new(), Vec::new(), Vec::new()); - for &(segment, reversed) in &order { + for &(segment, reversed) in &chained.order { ids.push(self.segment_ids[segment]); let (start, end) = (self.segment_start[segment], self.segment_end[segment]); if reversed { @@ -300,24 +286,199 @@ impl WorkingVector { self.segment_start = starts; self.segment_end = ends; self.segment_handles = handles; - self.segment_begins_path = begins_path; + self.segment_begins_path = chained.begins_path; // A turned segment's handles swap sides, so the G1 pairs naming them follow for handle in self.colinear.iter_mut().flatten() { if reversed_ids.contains(&handle.segment) { - handle.ty = match handle.ty { - HandleType::Primary => HandleType::End, - HandleType::End => HandleType::Primary, - }; + *handle = handle.opposite(); } } + + reversed_ids.into_iter().collect() } } +/// A segment soup put in the order and orientation that chains it into paths. +pub(crate) struct ChainOrder { + /// Each segment's index in the order it should be stored, with whether it is turned around. + pub order: Vec<(usize, bool)>, + /// Whether the segment at the same place in `order` begins a chain. + pub begins_path: Vec, +} + +/// Chains a legacy segment soup into paths the way it used to be drawn: each segment continues the one it meets end to end, +/// the straightest pair going through each junction first, and the chains come out open ones first from a free end (the +/// lower one when both are), then loops from their lowest points along their earlier stored segment there. +/// +/// `direction` gives the way a segment leaves the anchor at its start or its end, or `None` when unknown, which pairs last. +/// A chain stays as it was found where turning it around would cross a segment that is not `reversible`. +pub(crate) fn chain_order(point_count: usize, endpoints: &[(usize, usize)], direction: impl Fn(usize, bool) -> Option, reversible: impl Fn(usize) -> bool) -> ChainOrder { + let segment_count = endpoints.len(); + + // Every segment end at each point, named by twice the segment's index plus one for its arriving end + let mut ends_at: Vec> = vec![Vec::new(); point_count]; + for (index, &(start, end)) in endpoints.iter().enumerate() { + if start < point_count && end < point_count { + ends_at[start].push(index * 2); + ends_at[end].push(index * 2 + 1); + } + } + + // Pair off the ends meeting at each point, the straightest pair through a junction first, in O(k² log k) for k ends there. + // Beyond a few dozen ends that grows too costly for any route to stand out, so a crowded point pairs them in stored order. + const MOST_ENDS_PAIRED_BY_STRAIGHTNESS: usize = 64; + let mut partner = vec![usize::MAX; segment_count * 2]; + for ends in ends_at.iter().filter(|ends| ends.len() >= 2) { + if ends.len() > MOST_ENDS_PAIRED_BY_STRAIGHTNESS { + for pair in ends.chunks_exact(2) { + partner[pair[0]] = pair[1]; + partner[pair[1]] = pair[0]; + } + continue; + } + + let directions: Vec> = ends.iter().map(|&end| direction(end / 2, end % 2 == 1).map(DVec2::normalize_or_zero)).collect(); + + // A pair with an unknown direction ranks below every known one. Straightness is rounded so pairs differing only by + // float error are equally straight, leaving the earlier stored ends first. + let straightness = |i: usize, j: usize| directions[i].zip(directions[j]).map_or(-2., |(first, second): (DVec2, DVec2)| -first.dot(second)); + let mut pairs: Vec<(i64, usize, usize)> = (0..ends.len()) + .flat_map(|i| (i + 1..ends.len()).map(move |j| (i, j))) + .map(|(i, j)| ((straightness(i, j) * 1e9).round() as i64, i, j)) + .collect(); + pairs.sort_by(|a, b| b.0.cmp(&a.0).then((a.1, a.2).cmp(&(b.1, b.2)))); + + let mut paired = vec![false; ends.len()]; + for (_, i, j) in pairs { + if paired[i] || paired[j] { + continue; + } + paired[i] = true; + paired[j] = true; + partner[ends[i]] = ends[j]; + partner[ends[j]] = ends[i]; + } + } + + // Follow the pairings into chains, each seeded by the earliest unvisited segment + let mut visited = vec![false; segment_count]; + let mut chains: Vec<(Vec<(usize, bool)>, bool)> = Vec::new(); + for seed in 0..segment_count { + if std::mem::replace(&mut visited[seed], true) { + continue; + } + + let mut chain = vec![(seed, false)]; + let mut closed = false; + let mut end = seed * 2 + 1; + loop { + let next = partner[end]; + if next == usize::MAX { + break; + } + let segment = next / 2; + if segment == seed { + closed = true; + break; + } + + // Entering at the segment's arriving end crosses it against its stored direction + let reversed = next % 2 == 1; + if visited[segment] || (reversed && !reversible(segment)) { + break; + } + visited[segment] = true; + chain.push((segment, reversed)); + end = if reversed { segment * 2 } else { segment * 2 + 1 }; + } + + // An open chain also grows from the seed's start, which is then where it begins + let mut before = Vec::new(); + let mut end = seed * 2; + if !closed { + loop { + let next = partner[end]; + if next == usize::MAX { + break; + } + let segment = next / 2; + + // Joining at the segment's arriving end means it runs with its stored direction into the chain + let reversed = next % 2 == 0; + if visited[segment] || (reversed && !reversible(segment)) { + break; + } + visited[segment] = true; + before.push((segment, reversed)); + end = if reversed { segment * 2 + 1 } else { segment * 2 }; + } + } + before.reverse(); + before.append(&mut chain); + + chains.push((before, closed)); + } + + // The point a chain member is crossed from, and the chain crossed the other way when every member allows it + let from = |&(segment, reversed): &(usize, bool)| if reversed { endpoints[segment].1 } else { endpoints[segment].0 }; + let to = |&(segment, reversed): &(usize, bool)| if reversed { endpoints[segment].0 } else { endpoints[segment].1 }; + let turned = |chain: &[(usize, bool)]| -> Option> { + chain + .iter() + .rev() + .map(|&(segment, reversed)| (reversed || reversible(segment)).then_some((segment, !reversed))) + .collect() + }; + let spokes = |point: usize| ends_at.get(point).map_or(0, Vec::len); + + // Start each chain where it used to be drawn from, and note what it is ordered by + let mut keyed = Vec::with_capacity(chains.len()); + for (mut chain, closed) in chains { + if closed { + // A loop sets out from its lowest point along whichever of its two segments there is stored first + let Some(lowest) = (0..chain.len()).min_by_key(|&place| from(&chain[place])) else { continue }; + chain.rotate_left(lowest); + let (leaving, arriving) = (chain[0].0, chain[chain.len() - 1].0); + if arriving < leaving + && let Some(turned) = turned(&chain) + { + chain = turned; + } + } else { + // An open chain sets out from its free end, or from its lower point when both ends or neither are free + let (Some(first), Some(last)) = (chain.first(), chain.last()) else { continue }; + let (start, end) = (from(first), to(last)); + let from_the_far_end = match (spokes(start) == 1, spokes(end) == 1) { + (true, false) => false, + (false, true) => true, + _ => end < start, + }; + if from_the_far_end && let Some(turned) = turned(&chain) { + chain = turned; + } + } + + let Some(first) = chain.first() else { continue }; + keyed.push(((closed, from(first)), chain)); + } + keyed.sort_by_key(|&(key, _)| key); + + let mut order = Vec::with_capacity(segment_count); + let mut begins_path = Vec::with_capacity(segment_count); + for (_, mut chain) in keyed { + begins_path.extend((0..chain.len()).map(|place| place == 0)); + order.append(&mut chain); + } + + ChainOrder { order, begins_path } +} + // TODO: Eventually remove this document upgrade code /// Builds storage from the legacy flat arrays: anchors with IDs and positions, segments with IDs, endpoint indices, /// and absolute handles, plus G1 pairs. Legacy segments carry no path structure, so they are chained by the straightest -/// route through each junction before weaving. Used by document migration. +/// route through each junction before weaving, and the IDs of any segments turned around come back alongside. +/// Used by document migration. pub fn vector_from_legacy_parts( point_ids: Vec, point_positions: Vec, @@ -326,7 +487,7 @@ pub fn vector_from_legacy_parts( segment_end: Vec, segment_handles: Vec, colinear: Vec<[HandleId; 2]>, -) -> (Vector, List) { +) -> (Vector, List, HashSet) { let mut working = WorkingVector { point_ids, point_positions, @@ -337,12 +498,12 @@ pub fn vector_from_legacy_parts( segment_begins_path: Vec::new(), colinear, }; - working.chain_by_straightest_route(); + let turned = working.chain_by_straightest_route(); let (mut vector, mut paths) = (Vector::default(), List::new()); working.reweave(&mut vector, &mut paths); - (vector, paths) + (vector, paths, turned) } #[cfg(test)] @@ -456,22 +617,130 @@ mod tests { assert_eq!(paths.element(0).expect("the run became a path").segment_count(CurveType::Cubic), 2); } - // Beveling appends its corner segments after the sides, which keep that order and still walk as one closed contour + // Beveling appends its corner segments after the sides, and each still finds the one side setting out from its end #[test] - fn reweave_keeps_segments_appended_out_of_order_in_order() { + fn reweave_chains_segments_stored_out_of_order() { let sides = [(0, 1), (2, 3), (4, 5), (6, 7)]; let corners = [(1, 2), (3, 4), (5, 6), (7, 0)]; let segments: Vec<_> = sides.into_iter().chain(corners).map(|(start, end)| (start, end, BezierHandles::Linear)).collect(); - let (vector, paths) = woven(working(8, &segments)); + let (_, paths) = woven(working(8, &segments)); + + assert_eq!(paths.len(), 1); + assert_eq!(references(&paths, 0), vec![0, 1, 2, 3, 4, 5, 6, 7, 0]); + } + + // A point inserted into a path stores its two halves last, and they still weave back into the one path + #[test] + fn reweave_keeps_a_path_whole_around_a_replaced_segment() { + let soup = [(0, 1), (2, 3), (1, 4), (4, 2)].map(|(start, end)| (start, end, BezierHandles::Linear)); + let (_, paths) = woven(working(5, &soup)); + + assert_eq!(paths.len(), 1); + assert_eq!(references(&paths, 0), vec![0, 1, 4, 2, 3]); + } + + // Where several segments meet, only the one stored next carries a chain on, so a spur ending at a junction stays a spur + #[test] + fn reweave_follows_stored_order_through_a_junction() { + let soup = [(3, 1), (0, 1), (1, 2)].map(|(start, end)| (start, end, BezierHandles::Linear)); + let (_, paths) = woven(working(4, &soup)); + + assert_eq!(paths.len(), 2); + assert_eq!(references(&paths, 0), vec![3, 1]); + assert_eq!(references(&paths, 1), vec![0, 1, 2]); + } + + // A chain that closes partway keeps weaving, so the loops and the tail after them all come out + #[test] + fn reweave_weaves_all_of_a_chain_that_closes_midway() { + let soup = [(2, 0), (0, 3), (0, 1), (1, 0), (0, 2)].map(|(start, end)| (start, end, BezierHandles::Linear)); + let (_, paths) = woven(working(4, &soup)); + + assert_eq!(paths.len(), 3); + assert_eq!(references(&paths, 0), vec![0, 1, 0]); + assert_eq!(references(&paths, 1), vec![0, 2, 0]); + assert_eq!(references(&paths, 2), vec![0, 3]); + } + + // A point inserted next to a junction stores its halves last, and the path still runs through where another path begins + #[test] + fn reweave_rejoins_a_replaced_segment_beside_a_junction() { + let mut soup = working(5, &[(0, 1), (1, 3), (1, 4), (4, 2)].map(|(start, end)| (start, end, BezierHandles::Linear))); + soup.segment_begins_path = vec![true, true, false, false]; + let (_, paths) = woven(soup); + + assert_eq!(paths.len(), 2); + assert_eq!(references(&paths, 0), vec![0, 1, 4, 2]); + assert_eq!(references(&paths, 1), vec![1, 3]); + } + + // No soup loses a segment to the weave, and weaving what was just woven changes nothing + #[test] + fn reweave_keeps_every_segment_of_any_soup() { + let mut state = 0x9E37_79B9_7F4A_7C15_u64; + let mut below = |limit: usize| { + state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); + (state >> 33) as usize % limit + }; + + for _ in 0..2000 { + let points = 1 + below(6); + let segments: Vec<_> = (0..below(10)) + .map(|_| { + let handles = match below(3) { + 0 => BezierHandles::Linear, + 1 => BezierHandles::Quadratic { + handle: DVec2::new(below(5) as f64, 1.), + }, + _ => BezierHandles::Cubic { + handle_start: DVec2::new(below(5) as f64, 1.), + handle_end: DVec2::new(below(5) as f64, -1.), + }, + }; + (below(points), below(points), handles) + }) + .collect(); + let mut soup = working(points, &segments); + soup.segment_begins_path = (0..segments.len()).map(|_| below(4) == 0).collect(); + + let (vector, paths) = woven(soup); + let spans: usize = paths.iter_element_values().enumerate().map(|(index, path)| path.segment_count(curve_type_at(&paths, index))).sum(); + assert_eq!(spans, segments.len(), "{segments:?}"); + + let (_, again) = woven(WorkingVector::flatten(&vector, &paths)); + assert_eq!(again, paths, "{segments:?}"); + } + } + + // A path that exists only because the curve type changed rejoins the one before it once their types match + #[test] + fn flatten_lets_a_type_change_rejoin() { + let curve = BezierHandles::Cubic { + handle_start: DVec2::new(1., 1.), + handle_end: DVec2::new(2., 1.), + }; + let (vector, paths) = woven(working(4, &[(0, 1, BezierHandles::Linear), (1, 2, curve), (2, 3, BezierHandles::Linear)])); + assert_eq!(paths.len(), 3); - assert_eq!(paths.len(), 8); - assert_eq!(references(&paths, 3), vec![6, 7]); - assert_eq!(references(&paths, 4), vec![1, 2]); + let mut flattened = WorkingVector::flatten(&vector, &paths); + assert_eq!(flattened.segment_begins_path, vec![true, false, false]); - let contours: Vec<_> = vector.stroke_bezpath_iter(&paths).collect(); - assert_eq!(contours.len(), 1); - assert_eq!(contours[0].elements().len(), 10); + flattened.segment_handles[1] = BezierHandles::Linear; + let (_, straightened) = woven(flattened); + assert_eq!(straightened.len(), 1); + assert_eq!(references(&straightened, 0), vec![0, 1, 2, 3]); + } + + // Segments meeting tail to tail are never turned around to chain them + #[test] + fn reweave_never_reverses_a_segment() { + let soup = [(0, 1), (2, 1)].map(|(start, end)| (start, end, BezierHandles::Linear)); + let (_, paths) = woven(working(3, &soup)); + + assert_eq!(paths.len(), 2); + assert_eq!(references(&paths, 0), vec![0, 1]); + assert_eq!(references(&paths, 1), vec![2, 1]); } // Separate shapes stay where they were, so nothing downstream sees geometry shuffle @@ -553,12 +822,35 @@ mod tests { let mut working = working_at(&positions, &soup); working.chain_by_straightest_route(); - assert_eq!(working.segment_begins_path, vec![true, true, false]); + assert_eq!(working.segment_begins_path, vec![true, false, true]); let (_, paths) = woven(working); assert_eq!(paths.len(), 2); - assert_eq!(references(&paths, 0), [1, 3]); - assert_eq!(references(&paths, 1), [0, 1, 2]); + assert_eq!(references(&paths, 0), [0, 1, 2]); + assert_eq!(references(&paths, 1), [3, 1]); + } + + // An open chain comes out from its lower end and a loop from its lowest point along its earlier stored segment, as both used to be drawn + #[test] + fn chains_come_out_in_the_order_they_used_to_be_drawn() { + let positions = [ + DVec2::new(0., 0.), + DVec2::new(1., 0.), + DVec2::new(2., 0.), + DVec2::new(3., 0.), + DVec2::new(5., 0.), + DVec2::new(6., 0.), + DVec2::new(5., 1.), + ]; + let soup = [(5, 6), (6, 4), (4, 5), (2, 1), (3, 2), (1, 0)].map(|(start, end)| (start, end, BezierHandles::Linear)); + let mut working = working_at(&positions, &soup); + + working.chain_by_straightest_route(); + let (_, paths) = woven(working); + + assert_eq!(paths.len(), 2); + assert_eq!(references(&paths, 0), [0, 1, 2, 3]); + assert_eq!(references(&paths, 1), [4, 6, 5, 4]); } // A curve whose end handle is retracted onto the junction arrives along its other handle, so it carries on downward rather than along its chord @@ -613,7 +905,7 @@ mod tests { let ids = [SegmentId::generate(), SegmentId::generate()]; // The second segment runs back toward the shared anchor, so both handles facing it, one on each side, are stored as end handles - let (_, paths) = vector_from_legacy_parts( + let (_, paths, _) = vector_from_legacy_parts( (0..3).map(|_| PointId::generate()).collect(), positions.to_vec(), ids.to_vec(), diff --git a/node-graph/libraries/vector-types/src/vector/geometry.rs b/node-graph/libraries/vector-types/src/vector/geometry.rs index 7685be903db..9672efee695 100644 --- a/node-graph/libraries/vector-types/src/vector/geometry.rs +++ b/node-graph/libraries/vector-types/src/vector/geometry.rs @@ -93,6 +93,10 @@ impl VectorGeometry { self.vector.segment_ids(&self.paths) } + pub fn path_beginnings(&self) -> rustc_hash::FxHashSet { + self.vector.path_beginnings(&self.paths) + } + pub fn segment_from_id(&self, id: SegmentId) -> Option { self.vector.segment_from_id(&self.paths, id) } diff --git a/node-graph/libraries/vector-types/src/vector/path.rs b/node-graph/libraries/vector-types/src/vector/path.rs index 328d3c950a7..22ecd44da35 100644 --- a/node-graph/libraries/vector-types/src/vector/path.rs +++ b/node-graph/libraries/vector-types/src/vector/path.rs @@ -182,6 +182,18 @@ pub fn curve_type_at(paths: &List, index: usize) -> CurveType { paths.attribute_cloned_or_default(ATTR_CURVE_TYPE, index) } +/// Whether the path at `index` only carries on the one stored before it in another curve type, setting out from +/// that path's last anchor. Such a path begins nothing of its own. +pub fn continues_previous_path(paths: &List, index: usize) -> bool { + let Some(previous_index) = index.checked_sub(1) else { return false }; + let (Some(path), Some(previous)) = (paths.element(index), paths.element(previous_index)) else { + return false; + }; + let previous_last = previous.0.len().saturating_sub(1); + + previous_last > 0 && path.0.element(0) == previous.0.element(previous_last) && curve_type_at(paths, index) != curve_type_at(paths, previous_index) +} + /// Weaves bezpath contours into pool points and paths, starting a new path at each verb change so every /// path keeps one stride. The runs of a split subpath share the anchor they meet at, welding them. struct PathWeaver { diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index b4b8adf545d..42ecbb7a0d5 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -1,13 +1,14 @@ use crate::vector::addressing::{self, Span}; use crate::vector::contour::contours; use crate::vector::misc::{ManipulatorGroup, Tangent, point_to_dvec2, segment_to_handles}; -use crate::vector::path::{CurveType, Path, append_segment, curve_type_at}; +use crate::vector::path::{CurveType, Path, append_segment, continues_previous_path, curve_type_at}; use crate::vector::vector_types::Vector; use core_types::list::{ATTR_ID, Item, List}; use dyn_any::DynAny; use fixedbitset::FixedBitSet; use glam::DVec2; use kurbo::{ParamCurve, PathSeg}; +use rustc_hash::FxHashSet; use std::collections::HashSet; use std::hash::{Hash, Hasher}; @@ -629,6 +630,14 @@ impl Vector { addressing::spans(paths).map(|span| addressing::segment_id_of(paths, span).unwrap_or_default()).collect() } + /// The ID of the first segment of every path that begins one, leaving out the paths that only carry on the one before them in another curve type. + pub fn path_beginnings(&self, paths: &List) -> FxHashSet { + addressing::spans(paths) + .filter(|span| span.index == 0 && !continues_previous_path(paths, span.path as usize)) + .filter_map(|span| addressing::segment_id_of(paths, span)) + .collect() + } + pub fn auto_join_paths(&self, paths: &List) -> Vec { let graph = SegmentGraph::new(self, paths); let segments = (0..graph.len()).map(|index| HalfEdge::new(graph.ids[index], graph.start_point[index], graph.end_point[index], false)); diff --git a/node-graph/libraries/vector-types/src/vector/vector_modification.rs b/node-graph/libraries/vector-types/src/vector/vector_modification.rs index fe8ce4f27e2..3a483e96521 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_modification.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_modification.rs @@ -202,14 +202,43 @@ pub(crate) struct SegmentModification { } impl SegmentModification { - /// Apply this modification to the working form's segments. - fn apply(&self, working: &mut WorkingVector) { + /// The kept segment after removed ones begins a path when it carried on from the last of them or one of them began a path, + /// unless an added segment arrives where it sets out and so carries the path on into it. Runs in O(segments + adds). + fn mark_path_beginnings_after_removals(&self, working: &mut WorkingVector, keep: &[bool]) { + if !keep.contains(&false) { + return; + } + + let arrivals: FxHashSet = self.add.iter().filter_map(|id| self.end_point.get(id).copied()).collect(); + let segment_count = keep.len(); + working.segment_begins_path.resize(segment_count, false); + + let (mut removed_began_path, mut removed_end) = (false, None); + for index in 0..segment_count { + if !keep[index] { + removed_began_path |= std::mem::take(&mut working.segment_begins_path[index]); + removed_end = Some(working.segment_end[index]); + continue; + } + let Some(end) = removed_end.take() else { continue }; + + let start = working.segment_start[index]; + let carried_on = start == end; + let arrived_at = working.point_ids.get(start).is_some_and(|point| arrivals.contains(point)); + if (std::mem::take(&mut removed_began_path) || carried_on) && !arrived_at { + working.segment_begins_path[index] = true; + } + } + } + + /// Apply this modification to the working form's segments, where the added ones named by `path_breaks` each begin a path. + fn apply(&self, working: &mut WorkingVector, path_breaks: &HashSet) { // Remove the deleted segments, plus any segment left referencing a removed point let point_count = working.point_ids.len(); let keep: Vec = (0..working.segment_ids.len()) .map(|index| !self.remove.contains(&working.segment_ids[index]) && working.segment_start[index] < point_count && working.segment_end[index] < point_count) .collect(); - working.carry_path_beginnings(&keep); + self.mark_path_beginnings_after_removals(working, &keep); let mut keep_iter = keep.iter(); working.segment_start.retain(|_| *keep_iter.next().unwrap_or(&false)); let mut keep_iter = keep.iter(); @@ -290,6 +319,8 @@ impl SegmentModification { }; } + working.segment_begins_path.resize(working.segment_ids.len(), false); + let mut named: FxHashSet = if self.add.is_empty() { FxHashSet::default() } else { @@ -335,6 +366,7 @@ impl SegmentModification { working.segment_start.push(start_index); working.segment_end.push(end_index); working.segment_handles.push(handles); + working.segment_begins_path.push(path_breaks.contains(&add_id)); } assert!(working.segment_start.iter().all(|&index| index < working.point_ids.len()), "index should be in range"); @@ -493,7 +525,7 @@ impl SegmentModification { /// Represents a procedural change to the [`Vector`]. #[derive(Clone, Debug, Default, PartialEq, DynAny)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct VectorModification { points: PointModification, segments: SegmentModification, @@ -501,6 +533,47 @@ pub struct VectorModification { add_g1_continuous: HashSet<[HandleId; 2]>, #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashset"))] remove_g1_continuous: HashSet<[HandleId; 2]>, + /// The added segments that each begin a path of their own, even where one sets out from the end of another segment. + // TODO: Stop always writing this as a marker when switching to the new document format and Ctrl-C node serialization format + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashset_as_some"))] + path_breaks: HashSet, + /// The added segments turned around when this modification was loaded as a soup, which other modifications naming them by side follow. + // TODO: Eventually remove this document upgrade code + #[cfg_attr(feature = "serde", serde(skip))] + turned_on_load: HashSet, +} + +// TODO: Eventually remove this document upgrade code +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for VectorModification { + fn deserialize>(deserializer: D) -> Result { + // Modifications stored before paths recorded where they begin lack `path_breaks` and hold their segments as an unordered soup, which is chained into paths on load + #[derive(serde::Deserialize)] + struct StoredVectorModification { + points: PointModification, + segments: SegmentModification, + add_g1_continuous: HashSet<[HandleId; 2]>, + remove_g1_continuous: HashSet<[HandleId; 2]>, + #[serde(default)] + path_breaks: Option>, + } + + let stored = StoredVectorModification::deserialize(deserializer)?; + let legacy = stored.path_breaks.is_none(); + let mut modification = Self { + points: stored.points, + segments: stored.segments, + add_g1_continuous: stored.add_g1_continuous, + remove_g1_continuous: stored.remove_g1_continuous, + path_breaks: stored.path_breaks.unwrap_or_default(), + turned_on_load: HashSet::new(), + }; + if legacy { + modification.chain_legacy_segments(); + } + + Ok(modification) + } } /// A modification type that can be added to a [`VectorModification`]. @@ -541,6 +614,11 @@ pub enum VectorModificationType { handle: HandleId, id: PointId, }, + /// Marks or unmarks a segment this modification adds as beginning a path of its own. Segments from upstream are unaffected. + SetPathBreak { + segment: SegmentId, + breaks: bool, + }, ApplyPointDelta { point: PointId, @@ -554,7 +632,7 @@ impl VectorModificationType { match self { Self::InsertSegment { handles, .. } | Self::SetHandles { handles, .. } => handles.iter().flatten().all(|handle| handle.is_finite()), Self::InsertPoint { position: value, .. } | Self::SetHandlePosition { relative_position: value, .. } | Self::ApplyPointDelta { delta: value, .. } => value.is_finite(), - Self::RemoveSegment { .. } | Self::RemovePoint { .. } | Self::SetG1Continuous { .. } | Self::SetEndpoint { .. } => true, + Self::RemoveSegment { .. } | Self::RemovePoint { .. } | Self::SetG1Continuous { .. } | Self::SetEndpoint { .. } | Self::SetPathBreak { .. } => true, } } } @@ -668,6 +746,9 @@ impl VectorModification { segments, add_g1_continuous, remove_g1_continuous, + // Path beginnings only concern added segments, and any of those already rules this out + path_breaks: _, + turned_on_load: _, } = self; let SegmentModification { add: added_segments, @@ -713,7 +794,7 @@ impl VectorModification { fn apply_through_working_form(&self, vector: &mut Vector, paths: &mut List) { let mut working = WorkingVector::flatten(vector, paths); self.points.apply(&mut working); - self.segments.apply(&mut working); + self.segments.apply(&mut working, &self.path_breaks); let surviving_segments: FxHashSet = working.segment_ids.iter().copied().collect(); let valid = |val: &[HandleId; 2]| surviving_segments.contains(&val[0].segment) && surviving_segments.contains(&val[1].segment); @@ -745,7 +826,10 @@ impl VectorModification { VectorModificationType::InsertSegment { id, points, handles } => self.segments.push(*id, *points, *handles), VectorModificationType::InsertPoint { id, position } => self.points.push(*id, *position), - VectorModificationType::RemoveSegment { id } => self.segments.remove(*id), + VectorModificationType::RemoveSegment { id } => { + self.segments.remove(*id); + self.path_breaks.remove(id); + } VectorModificationType::RemovePoint { id } => self.points.remove(*id), VectorModificationType::SetG1Continuous { handles, enabled } => { @@ -773,6 +857,14 @@ impl VectorModification { VectorModificationType::SetEndpoint { handle, id } => { self.segments.endpoint_side_of(*handle).insert(handle.segment, *id); } + VectorModificationType::SetPathBreak { segment, breaks } => { + // Only a segment this modification adds can begin a path of its own + if *breaks && self.segments.add.contains(segment) { + self.path_breaks.insert(*segment); + } else { + self.path_breaks.remove(segment); + } + } VectorModificationType::ApplyPointDelta { point, delta } => { *self.points.delta.entry(*point).or_default() += *delta; @@ -783,12 +875,208 @@ impl VectorModification { /// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`]. pub fn create_from_vector(vector: &Vector, paths: &List) -> Self { let working = WorkingVector::flatten(vector, paths); + let begins_path = working.segment_ids.iter().zip(&working.segment_begins_path).filter(|&(_, &begins)| begins).map(|(&id, _)| id); Self { points: PointModification::create_from_working(&working), segments: SegmentModification::create_from_working(&working), add_g1_continuous: working.colinear.iter().copied().collect(), remove_g1_continuous: HashSet::new(), + path_breaks: begins_path.collect(), + turned_on_load: HashSet::new(), + } + } + + // TODO: Eventually remove this document upgrade code + /// Chains the added segments of a modification stored as an unordered soup into paths: reorders them, turns around + /// the ones running against their chain along with their handles and G1 pairs, and records where each chain begins. + fn chain_legacy_segments(&mut self) { + // Segments missing any of their parts are never applied, so they sit out of the chaining and follow the chained ones + let segments = &self.segments; + let mut seen = FxHashSet::default(); + let (mut complete, mut unchainable) = (Vec::new(), Vec::new()); + for &id in &segments.add { + if !seen.insert(id) { + continue; + } + let parts = ( + segments.start_point.get(&id), + segments.end_point.get(&id), + segments.handle_primary.get(&id), + segments.handle_end.get(&id), + ); + match parts { + (Some(&start), Some(&end), Some(&primary), Some(&end_handle)) => complete.push((id, start, end, primary, end_handle)), + _ => unchainable.push(id), + } + } + + // Points are numbered the way they used to be stored, upstream ones first and then the added ones in the order they + // were added. Only the added ones have positions here. What else meets an upstream point is unknown, so each + // segment end there is numbered apart and pairs with nothing, leaving it to be joined once upstream is known. + let added: FxHashMap = self.points.add.iter().enumerate().filter_map(|(place, id)| Some((*id, (place, *self.points.delta.get(id)?)))).collect(); + let upstream_ends = complete.iter().flat_map(|&(_, start, end, ..)| [start, end]).filter(|point| !added.contains_key(point)).count(); + let mut next_upstream_end = 0; + let mut index_of = |point: PointId| match added.get(&point) { + Some(&(place, _)) => upstream_ends + place, + None => { + next_upstream_end += 1; + next_upstream_end - 1 + } + }; + + let mut positions = vec![None; upstream_ends + self.points.add.len()]; + for &(place, position) in added.values() { + positions[upstream_ends + place] = Some(position); + } + let chainable: Vec<_> = complete + .iter() + .map(|&(id, start, end, primary, end_handle)| (id, index_of(start), index_of(end), primary, end_handle)) + .collect(); + let endpoints: Vec<(usize, usize)> = chainable.iter().map(|&(_, start, end, ..)| (start, end)).collect(); + + // A cubic's handles are stored relative to their own anchors, and a quadratic's one handle relative to its start. A segment + // leaves an anchor toward its nearest control point not sitting on that anchor, as legacy vectors are chained too. + let nonzero = |along: DVec2| (along.length_squared() > 1e-12).then_some(along); + let direction = |segment: usize, arriving: bool| { + let (_, start, end, primary, end_handle) = chainable[segment]; + let (start, end) = (positions[start], positions[end]); + let across = |from: Option, offset: DVec2, to: Option| from.zip(to).map(|(from, to)| from + offset - to); + + // Each handle as an offset from this end's anchor, nearest first, then the other anchor + let toward = match (primary, end_handle) { + (Some(primary), Some(end_handle)) if arriving => [Some(end_handle), across(start, primary, end)], + (Some(primary), Some(end_handle)) => [Some(primary), across(end, end_handle, start)], + (Some(handle), None) | (None, Some(handle)) if arriving => [across(start, handle, end), None], + (Some(handle), None) | (None, Some(handle)) => [Some(handle), None], + (None, None) => [None, None], + }; + let chord = if arriving { across(start, DVec2::ZERO, end) } else { across(end, DVec2::ZERO, start) }; + + toward.into_iter().chain([chord]).flatten().find_map(nonzero) + }; + + // A segment touching an upstream point keeps its direction, since what upstream carries on into it is unknown, and so + // does a quadratic, since another modification's offset for its lone handle could not be re-based onto the other end + let reversible = |segment: usize| { + let (_, start, end, primary, end_handle) = chainable[segment]; + positions[start].is_some() && positions[end].is_some() && primary.is_some() == end_handle.is_some() + }; + let chained = crate::vector::flatten::chain_order(positions.len(), &endpoints, direction, reversible); + + let mut reversed_ids = FxHashSet::default(); + let mut path_breaks = HashSet::new(); + let mut add = Vec::with_capacity(segments.add.len()); + let mut turned = Vec::new(); + for (&(segment, reversed), &begins) in chained.order.iter().zip(&chained.begins_path) { + let (id, start, end, primary, end_handle) = chainable[segment]; + add.push(id); + + // A chain setting out from an upstream point may be carrying on a path that ends there, so only the rest begin paths + let sets_out_from = if reversed { end } else { start }; + if begins && positions[sets_out_from].is_some() { + path_breaks.insert(id); + } + + if reversed { + reversed_ids.insert(id); + turned.push((id, (end_handle, primary))); + } + } + add.extend(unchainable); + + self.segments.add = add; + for (id, (primary, end_handle)) in turned { + let (start, end) = (self.segments.start_point.remove(&id), self.segments.end_point.remove(&id)); + if let (Some(start), Some(end)) = (start, end) { + self.segments.start_point.insert(id, end); + self.segments.end_point.insert(id, start); + } + self.segments.handle_primary.insert(id, primary); + self.segments.handle_end.insert(id, end_handle); } + self.path_breaks = path_breaks; + + // A turned segment's handles swap sides, so the G1 pairs naming them follow + let follow = |pairs: &mut HashSet<[HandleId; 2]>| { + *pairs = pairs + .drain() + .map(|pair| pair.map(|handle| if reversed_ids.contains(&handle.segment) { handle.opposite() } else { handle })) + .collect(); + }; + follow(&mut self.add_g1_continuous); + follow(&mut self.remove_g1_continuous); + + self.turned_on_load = reversed_ids.into_iter().collect(); + } + + // TODO: Eventually remove this document upgrade code + /// Records the segments a legacy conversion turned around before this modification was captured from its result. + pub fn with_segments_turned_on_load(mut self, turned: HashSet) -> Self { + self.turned_on_load = turned; + self + } + + // TODO: Eventually remove this document upgrade code + /// Takes the record of the segments this modification turned around when it was loaded. + pub fn take_segments_turned_on_load(&mut self) -> HashSet { + std::mem::take(&mut self.turned_on_load) + } + + // TODO: Eventually remove this document upgrade code + /// The segments this modification turned around when it was loaded. + pub fn segments_turned_on_load(&self) -> &HashSet { + &self.turned_on_load + } + + // TODO: Eventually remove this document upgrade code + /// Turns this modification's edits around to match segments another modification turned when it was loaded, so each edit + /// stays on the side of its segment it was made on. Returns whether anything changed. + pub fn follow_segments_turned_elsewhere(&mut self, turned: &HashSet) -> bool { + let own: HashSet = self.segments.add.iter().copied().collect(); + let named_elsewhere = |segment: &SegmentId| turned.contains(segment) && !own.contains(segment); + let mut changed = false; + + // Only the segments this modification names by side can need turning, so this is linear in its own edits + let segments = &self.segments; + let named: HashSet = [&segments.handle_primary, &segments.handle_end] + .into_iter() + .flat_map(|handles| handles.keys()) + .chain([&segments.start_point, &segments.end_point].into_iter().flat_map(|points| points.keys())) + .copied() + .filter(named_elsewhere) + .collect(); + + for segment in named { + let (primary, end_handle) = (self.segments.handle_primary.remove(&segment), self.segments.handle_end.remove(&segment)); + changed |= primary.is_some() || end_handle.is_some(); + if let Some(primary) = primary { + self.segments.handle_end.insert(segment, primary); + } + if let Some(end_handle) = end_handle { + self.segments.handle_primary.insert(segment, end_handle); + } + + let (start, end) = (self.segments.start_point.remove(&segment), self.segments.end_point.remove(&segment)); + changed |= start.is_some() || end.is_some(); + if let Some(start) = start { + self.segments.end_point.insert(segment, start); + } + if let Some(end) = end { + self.segments.start_point.insert(segment, end); + } + } + + for pairs in [&mut self.add_g1_continuous, &mut self.remove_g1_continuous] { + if pairs.iter().flatten().any(|handle| named_elsewhere(&handle.segment)) { + changed = true; + *pairs = pairs + .drain() + .map(|pair| pair.map(|handle| if named_elsewhere(&handle.segment) { handle.opposite() } else { handle })) + .collect(); + } + } + + changed } } @@ -861,6 +1149,19 @@ where seq.end() } +/// Serializes sorted `[value, ...]` as a present optional, matching the field being read back as an `Option` in formats that tag one. +pub(crate) fn serialize_hashset_as_some(set: &HashSet, serializer: S) -> Result +where + T: Serialize + Eq + Hash + Ord, + S: Serializer, + H: BuildHasher, +{ + let mut entries: Vec<_> = set.iter().collect(); + entries.sort(); + + serializer.serialize_some(&entries) +} + pub fn deserialize_hashmap<'de, K, V, D, H>(deserializer: D) -> Result, D::Error> where K: Deserialize<'de> + Eq + Hash, @@ -1347,4 +1648,251 @@ mod tests { assert_eq!(modification, VectorModification::default()); } + + /// A modification placing the points in a row and drawing a straight segment between each pair of them given. + fn drawn(points: &[PointId], segments: &[(SegmentId, usize, usize)]) -> VectorModification { + let mut modification = VectorModification::default(); + for (index, &id) in points.iter().enumerate() { + let position = DVec2::new(index as f64 * 10., 0.); + modification.modify(&VectorModificationType::InsertPoint { id, position }); + } + for &(id, start, end) in segments { + let points = [points[start], points[end]]; + modification.modify(&VectorModificationType::InsertSegment { id, points, handles: [None, None] }); + } + + modification + } + + /// The anchors each path visits once the modification is applied to nothing, as places in the given points. + fn woven_paths(modification: &VectorModification, points: &[PointId]) -> Vec> { + let (mut vector, mut paths) = (Vector::default(), List::new()); + modification.apply(&mut vector, &mut paths); + + let place = |point: u32| vector.point_id_at(point).and_then(|id| points.iter().position(|&candidate| candidate == id)).expect("a known anchor"); + paths.iter_element_values().map(|path| path.references().map(place).collect()).collect() + } + + fn generated(count: usize, generate: fn() -> T) -> Vec { + (0..count).map(|_| generate()).collect() + } + + // A modification stored before paths recorded where they begin is a soup, which loading chains into one path and turns around where needed + #[test] + fn a_stored_soup_is_chained_into_paths_on_load() { + let (points, ids) = (generated(5, PointId::generate), generated(4, SegmentId::generate)); + let soup = drawn(&points, &[(ids[0], 1, 2), (ids[1], 1, 0), (ids[2], 3, 2), (ids[3], 3, 4)]); + assert_eq!(woven_paths(&soup, &points).len(), 4); + + let mut stored = serde_json::to_value(&soup).unwrap(); + stored.as_object_mut().unwrap().remove("path_breaks"); + let loaded: VectorModification = serde_json::from_value(stored).unwrap(); + + assert_eq!(woven_paths(&loaded, &points), [[0, 1, 2, 3, 4]]); + assert_eq!(loaded.path_breaks, HashSet::from([ids[1]])); + } + + // A modification stored with its record of path beginnings loads exactly as it was saved, without being chained again + #[test] + fn the_path_record_survives_a_round_trip() { + let (points, ids) = (generated(3, PointId::generate), generated(2, SegmentId::generate)); + let mut modification = drawn(&points, &[(ids[0], 1, 2), (ids[1], 1, 0)]); + modification.modify(&VectorModificationType::SetPathBreak { segment: ids[1], breaks: true }); + + let stored = serde_json::to_string(&modification).unwrap(); + let loaded: VectorModification = serde_json::from_str(&stored).unwrap(); + + assert_eq!(loaded, modification); + assert_eq!(woven_paths(&loaded, &points), [vec![1, 2], vec![1, 0]]); + } + + // Splitting the first side of an upstream closed path, as the Path tool inserts a point, keeps one closed path beginning where it did + #[test] + fn splitting_an_upstream_path_beginning_keeps_the_path_whole() { + use kurbo::{Rect, Shape}; + + let (mut vector, mut paths) = Vector::from_bezpath(&Rect::new(0., 0., 10., 10.).to_path(0.)); + let (first, _, start, end) = vector.segment_iter(&paths).next().unwrap(); + let middle = PointId::generate(); + let halves = [SegmentId::generate(), SegmentId::generate()]; + + let mut modification = VectorModification::default(); + modification.modify(&VectorModificationType::RemoveSegment { id: first }); + modification.modify(&VectorModificationType::InsertPoint { + id: middle, + position: DVec2::new(5., 0.), + }); + for (id, points) in [(halves[0], [start, middle]), (halves[1], [middle, end])] { + modification.modify(&VectorModificationType::InsertSegment { id, points, handles: [None, None] }); + } + modification.modify(&VectorModificationType::SetPathBreak { segment: halves[0], breaks: true }); + modification.apply(&mut vector, &mut paths); + + assert_eq!(paths.len(), 1); + assert!(paths.element(0).unwrap().is_closed()); + assert_eq!(vector.segment_iter(&paths).count(), 5); + assert_eq!(vector.segment_iter(&paths).next().map(|(id, ..)| id), Some(halves[0])); + } + + // Deleting a segment cuts its path, so what is left past it stays apart from another path ending at the same junction + #[test] + fn deleting_a_segment_beside_a_junction_keeps_the_paths_apart() { + use crate::vector::path::push_path; + use core_types::list::Item; + + let mut vector = Vector::default(); + for index in 0..8 { + vector.0.push(Item::new_from_element(DVec2::new(index as f64, 0.)).with_attribute(ATTR_ID, PointId::generate())); + } + let mut paths = List::new(); + for references in [vec![0, 1, 2, 3], vec![5, 2], vec![6, 2, 7]] { + let ids: Vec = (1..references.len()).map(|_| SegmentId::generate()).collect(); + push_path(&mut paths, CurveType::Linear, references, ids); + } + let removed = vector.segment_iter(&paths).nth(1).map(|(id, ..)| id).unwrap(); + + let mut modification = VectorModification::default(); + modification.modify(&VectorModificationType::RemoveSegment { id: removed }); + modification.apply(&mut vector, &mut paths); + + // Anchors are named by their x positions, which were their original pool indices + let drawn: Vec> = paths + .iter_element_values() + .map(|path| path.references().map(|point| vector.position(point as usize).unwrap().x as i32).collect()) + .collect(); + assert!(drawn.contains(&vec![2, 3]), "{drawn:?}"); + assert!(drawn.contains(&vec![5, 2]), "{drawn:?}"); + } + + // A segment marked as beginning a path stays apart from the one it sets out from the end of + #[test] + fn a_path_break_keeps_a_stroke_apart() { + let (points, ids) = (generated(3, PointId::generate), generated(2, SegmentId::generate)); + let mut modification = drawn(&points, &[(ids[0], 0, 1), (ids[1], 1, 2)]); + assert_eq!(woven_paths(&modification, &points), [[0, 1, 2]]); + + modification.modify(&VectorModificationType::SetPathBreak { segment: ids[1], breaks: true }); + assert_eq!(woven_paths(&modification, &points), [vec![0, 1], vec![1, 2]]); + } + + // A stroke drawn out of an upstream point and one drawn into it are not paired there, and each keeps its direction + #[test] + fn a_stored_soup_pairs_nothing_at_an_upstream_point() { + let (points, ids) = (generated(4, PointId::generate), generated(3, SegmentId::generate)); + let mut soup = drawn(&points[1..], &[]); + for (id, start, end) in [(ids[0], 0, 1), (ids[1], 1, 2), (ids[2], 3, 0)] { + let points = [points[start], points[end]]; + soup.modify(&VectorModificationType::InsertSegment { id, points, handles: [None, None] }); + } + + let mut stored = serde_json::to_value(&soup).unwrap(); + stored.as_object_mut().unwrap().remove("path_breaks"); + let loaded: VectorModification = serde_json::from_value(stored).unwrap(); + + // Paired at the upstream point, the stroke arriving there would have led the one leaving it + assert_eq!(loaded.segments.add, vec![ids[0], ids[1], ids[2]]); + assert_eq!(loaded.segments.start_point[&ids[0]], points[0]); + assert_eq!(loaded.segments.start_point[&ids[2]], points[3]); + assert_eq!(loaded.path_breaks, HashSet::from([ids[2]])); + } + + // Loading turns a stored cubic around along with its handles and G1 pair, and an edit elsewhere naming it by side follows + #[test] + fn a_segment_turned_on_load_keeps_its_handles_and_edits_on_their_anchors() { + let (points, ids) = (generated(3, PointId::generate), generated(2, SegmentId::generate)); + let (toward, away) = (DVec2::new(1., 2.), DVec2::new(3., 4.)); + let mut soup = drawn(&points, &[]); + soup.modify(&VectorModificationType::InsertSegment { + id: ids[0], + points: [points[1], points[0]], + handles: [Some(toward), Some(away)], + }); + soup.modify(&VectorModificationType::InsertSegment { + id: ids[1], + points: [points[1], points[2]], + handles: [None, None], + }); + let pair = [HandleId::primary(ids[0]), HandleId::primary(ids[1])]; + soup.modify(&VectorModificationType::SetG1Continuous { handles: pair, enabled: true }); + + let mut stored = serde_json::to_value(&soup).unwrap(); + stored.as_object_mut().unwrap().remove("path_breaks"); + let mut loaded: VectorModification = serde_json::from_value(stored).unwrap(); + + // The first segment now runs from point 0 into point 1, each handle still on the anchor it was drawn from + assert_eq!(loaded.segments.start_point[&ids[0]], points[0]); + assert_eq!(loaded.segments.handle_primary[&ids[0]], Some(away)); + assert_eq!(loaded.segments.handle_end[&ids[0]], Some(toward)); + assert!(loaded.add_g1_continuous.contains(&[HandleId::end(ids[0]), HandleId::primary(ids[1])])); + + let turned = loaded.take_segments_turned_on_load(); + assert_eq!(turned, HashSet::from([ids[0]])); + + let mut downstream = VectorModification::default(); + downstream.modify(&VectorModificationType::SetHandlePosition { + handle: HandleId::primary(ids[0]), + relative_position: DVec2::X, + }); + assert!(downstream.follow_segments_turned_elsewhere(&turned)); + assert_eq!(downstream.segments.handle_end.get(&ids[0]), Some(&Some(DVec2::X))); + assert!(!downstream.segments.handle_primary.contains_key(&ids[0])); + } + + // Capturing a vector records where its paths begin, so two paths welded head to tail come back as two + #[test] + fn a_captured_vector_keeps_its_paths() { + use crate::vector::path::push_path; + use core_types::list::Item; + + let points = generated(3, PointId::generate); + let mut vector = Vector::default(); + for (index, &id) in points.iter().enumerate() { + vector.0.push(Item::new_from_element(DVec2::new(index as f64 * 10., 0.)).with_attribute(ATTR_ID, id)); + } + let mut paths = List::new(); + push_path(&mut paths, CurveType::Linear, [0, 1], [SegmentId::generate()]); + push_path(&mut paths, CurveType::Linear, [1, 2], [SegmentId::generate()]); + + let captured = VectorModification::create_from_vector(&vector, &paths); + assert_eq!(woven_paths(&captured, &points), [vec![0, 1], vec![1, 2]]); + } + + // Splitting the segment a closed path begins with hands that beginning to its first half, so the path keeps its start + #[test] + fn a_point_inserted_on_a_paths_first_segment_keeps_its_start() { + let (points, ids) = (generated(5, PointId::generate), generated(6, SegmentId::generate)); + let mut modification = drawn(&points[..4], &[(ids[0], 0, 1), (ids[1], 1, 2), (ids[2], 2, 3), (ids[3], 3, 0)]); + modification.modify(&VectorModificationType::SetPathBreak { segment: ids[0], breaks: true }); + + modification.modify(&VectorModificationType::InsertPoint { + id: points[4], + position: DVec2::new(5., 5.), + }); + for (id, points) in [(ids[4], [points[0], points[4]]), (ids[5], [points[4], points[1]])] { + modification.modify(&VectorModificationType::InsertSegment { id, points, handles: [None, None] }); + } + modification.modify(&VectorModificationType::SetPathBreak { segment: ids[4], breaks: true }); + modification.modify(&VectorModificationType::RemoveSegment { id: ids[0] }); + + assert_eq!(woven_paths(&modification, &points), [[0, 4, 1, 2, 3, 0]]); + } + + // Splitting a segment removes it and stores its two halves last, and the path they sit in stays one path from the same start + #[test] + fn a_point_inserted_into_a_path_leaves_it_one_path() { + let (points, ids) = (generated(5, PointId::generate), generated(6, SegmentId::generate)); + let mut modification = drawn(&points[..4], &[(ids[0], 0, 1), (ids[1], 1, 2), (ids[2], 2, 3), (ids[3], 3, 0)]); + assert_eq!(woven_paths(&modification, &points), [[0, 1, 2, 3, 0]]); + + modification.modify(&VectorModificationType::RemoveSegment { id: ids[1] }); + modification.modify(&VectorModificationType::InsertPoint { + id: points[4], + position: DVec2::new(15., 5.), + }); + for (id, points) in [(ids[4], [points[1], points[4]]), (ids[5], [points[4], points[2]])] { + modification.modify(&VectorModificationType::InsertSegment { id, points, handles: [None, None] }); + } + + assert_eq!(woven_paths(&modification, &points), [[0, 1, 4, 2, 3, 0]]); + } } diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 48010419c9a..e0860b03faa 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -416,6 +416,11 @@ fn grid( } } + // A segment begins a path unless it carries on from the one stored before it, so the weave joins nothing around the grid's junctions + working.segment_begins_path = (0..working.segment_ids.len()) + .map(|index| index == 0 || working.segment_start[index] != working.segment_end[index - 1]) + .collect(); + let (mut vector, mut paths) = (Vector::default(), List::new()); working.reweave(&mut vector, &mut paths); @@ -465,6 +470,18 @@ mod tests { } } + // Only segments stored one after another join into paths, so no outline closes around the cells + #[test] + fn grid_joins_segments_only_where_they_follow_each_other() { + let grid = grid((), (), item(GridType::Rectangular), item(10.), item(3_i64), item(3_i64), item((30., 30.).into()), item(true)); + let paths = paths_from_item_attributes(&grid).cloned().unwrap_or_default(); + let vector = grid.element(); + + assert_eq!(vector.segment_ids(&paths).len(), 2 * 3 + 2 * 3); + assert_eq!(paths.len(), 7); + assert!(vector.stroke_manipulator_groups(&paths).all(|(_, closed)| !closed)); + } + #[test] fn grid_disconnected_cells_test() { // A 3x3 rectangular grid has a 2x2 arrangement of cells, each its own closed quad subpath.