Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions editor/src/messages/clipboard/clipboard_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> 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");
Expand All @@ -474,6 +475,15 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> 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
Expand Down
244 changes: 243 additions & 1 deletion editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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>, NodeId, usize, NodeInput)> {
type Location = (Vec<NodeId>, NodeId);
Comment thread
Keavon marked this conversation as resolved.

// The nodes reading each node's data, wired straight to the node producing it through nested networks' imports and exports
let mut readers: HashMap<Location, Vec<Location>> = HashMap::new();
let mut turned_by: Vec<(Location, HashSet<graphene_std::vector::SegmentId>)> = 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<Location, HashSet<graphene_std::vector::SegmentId>> = 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<Location> = 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))
Comment thread
Keavon marked this conversation as resolved.
.collect(),
_ => vec![origin.clone()],
};

let mut reached = HashSet::new();
let mut pending: Vec<Location> = 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>, 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();
Expand Down Expand Up @@ -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<NodeInput>| 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<NodeInput>| 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Message>) {
pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, first_endpoint: PointId, second_endpoint: PointId, responses: &mut VecDeque<Message>) {
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;
Expand All @@ -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.
Expand Down
Loading
Loading