mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 16:30:41 +00:00
807 lines
38 KiB
Rust
807 lines
38 KiB
Rust
//! Direct-selection Copy and Cut tests (#2122).
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_doc::{NodeId, NodeKind};
|
|
use vectorcraft_engine::Session;
|
|
use vectorcraft_geom::FillRule;
|
|
use vectorcraft_testkit::invariants::check_session;
|
|
|
|
fn run(s: &mut Session, command: &str, params: Value) -> Value {
|
|
s.execute(command, ¶ms).unwrap()
|
|
}
|
|
|
|
fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> NodeId {
|
|
NodeId(run(s, "shape.rectangle", json!({"x": x, "y": y, "width": w, "height": h}))["id"].as_u64().unwrap())
|
|
}
|
|
|
|
#[test]
|
|
fn copy_and_paste_direct_selected_segment_of_rect() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
|
|
// Select segment 0 of the rectangle.
|
|
run(&mut s, "select.segments", json!({"id": r.0, "segments": [[0, 0]]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
if let NodeKind::Path { live, .. } = &pasted_node.kind {
|
|
assert!(live.is_none(), "extracted partial path must not retain live shape metadata");
|
|
}
|
|
let path = pasted_node.path_data().unwrap();
|
|
assert_eq!(path.subpaths.len(), 1);
|
|
let sp = &path.subpaths[0];
|
|
assert!(!sp.closed);
|
|
assert_eq!(sp.anchors.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_and_paste_direct_selected_adjacent_anchors_of_rect() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
|
|
// Direct-select two adjacent anchors: 0 and 1.
|
|
run(&mut s, "select.anchors", json!({"id": r.0, "anchors": [[0, 0], [0, 1]], "mode": "set"}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
let path = pasted_node.path_data().unwrap();
|
|
assert_eq!(path.subpaths.len(), 1);
|
|
let sp = &path.subpaths[0];
|
|
assert!(!sp.closed);
|
|
assert_eq!(sp.anchors.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_single_anchor_of_rect_copies_three_anchors_with_two_segments() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
|
|
// Direct-select only anchor 1 of the closed 4-anchor rectangle.
|
|
run(&mut s, "select.anchors", json!({"id": r.0, "anchors": [[0, 1]], "mode": "set"}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
let path = pasted_node.path_data().unwrap();
|
|
assert_eq!(path.subpaths.len(), 1);
|
|
let sp = &path.subpaths[0];
|
|
assert!(!sp.closed);
|
|
// Copying a single anchor on a closed path copies the anchor and both its incident segments.
|
|
assert_eq!(sp.anchors.len(), 3);
|
|
assert_eq!(sp.segment_count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_disjoint_segments_does_not_copy_unselected_segment_in_between() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
|
|
// Rectangle has 4 segments (0, 1, 2, 3). Select disjoint segments 0 and 2.
|
|
run(&mut s, "select.segments", json!({"id": r.0, "segments": [[0, 0], [0, 2]]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
// Two separate open path pieces should be created, each with 2 anchors.
|
|
assert_eq!(ids.len(), 2);
|
|
for id in ids {
|
|
let node = s.doc().unwrap().doc.node(NodeId(id)).unwrap();
|
|
let path = node.path_data().unwrap();
|
|
assert_eq!(path.subpaths.len(), 1);
|
|
let sp = &path.subpaths[0];
|
|
assert!(!sp.closed);
|
|
assert_eq!(sp.anchors.len(), 2);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cut_direct_selected_segment_cuts_source_and_pastes_segment() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
|
|
// Select segment 0 and cut it.
|
|
run(&mut s, "select.segments", json!({"id": r.0, "segments": [[0, 0]]}));
|
|
run(&mut s, "edit.cut", json!({}));
|
|
|
|
// Source rectangle should now have segment 0 removed, turning it into an open path.
|
|
let src_node = s.doc().unwrap().doc.node(r).unwrap();
|
|
let src_path = src_node.path_data().unwrap();
|
|
assert_eq!(src_path.subpaths.len(), 1);
|
|
assert!(!src_path.subpaths[0].closed);
|
|
|
|
// Paste the cut segment:
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
let pasted_path = pasted_node.path_data().unwrap();
|
|
assert_eq!(pasted_path.subpaths.len(), 1);
|
|
assert!(!pasted_path.subpaths[0].closed);
|
|
assert_eq!(pasted_path.subpaths[0].anchors.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_compound_path_direct_selected_segment_inherits_appearance() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let outer = NodeId(run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 80, "height": 80}))["id"].as_u64().unwrap());
|
|
let inner = NodeId(run(&mut s, "shape.rectangle", json!({"x": 30, "y": 30, "width": 40, "height": 40}))["id"].as_u64().unwrap());
|
|
run(&mut s, "select.set", json!({"ids": [outer.0, inner.0]}));
|
|
let compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
|
|
run(&mut s, "paint.setFill", json!({"color": "#ff0000"}));
|
|
|
|
// Select segment 0 of the inner member.
|
|
run(&mut s, "select.segments", json!({"id": inner.0, "segments": [[0, 0]]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
let compound_node = s.doc().unwrap().doc.node(compound).unwrap();
|
|
assert_eq!(pasted_node.appearance, compound_node.appearance);
|
|
}
|
|
|
|
#[test]
|
|
fn direct_selection_copy_preserves_fill_rule_and_attributes() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
run(&mut s, "path.setFillRule", json!({"rule": "evenOdd"}));
|
|
|
|
run(&mut s, "select.segments", json!({"id": r.0, "segments": [[0, 0]]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
assert!(matches!(pasted_node.kind, NodeKind::Path { rule: FillRule::EvenOdd, .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn pen_tool_drawing_copies_whole_path_not_single_anchor() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
// Start drawing with Pen tool
|
|
s.select_tool("pen", vectorcraft_engine::ViewInfo::default()).unwrap();
|
|
let id = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 10, "y": 10}]}))["id"].as_u64().unwrap());
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 50, "y": 10}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 50, "y": 50}));
|
|
|
|
// Pen tool has the last anchor in sel.anchors, but active tool is pen
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
// The whole 3-point path must be copied, not just the last anchor!
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
let path = pasted_node.path_data().unwrap();
|
|
assert_eq!(path.subpaths[0].anchors.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn cut_adjacent_anchors_clipboard_plus_remaining_equals_original() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
|
|
// Direct-select two adjacent anchors: 1 and 2 of the 4-anchor rectangle.
|
|
run(&mut s, "select.anchors", json!({"id": r.0, "anchors": [[0, 1], [0, 2]], "mode": "set"}));
|
|
run(&mut s, "edit.cut", json!({}));
|
|
|
|
// Cut placed 1 segment on the clipboard (anchors 1 to 2).
|
|
assert_eq!(s.clipboard.nodes.len(), 1);
|
|
let clip_path = s.clipboard.nodes[0].path_data().unwrap();
|
|
assert_eq!(clip_path.subpaths[0].segment_count(), 1);
|
|
assert_eq!(clip_path.subpaths[0].anchors.len(), 2);
|
|
|
|
// Remaining source path must have the remaining 3 segments (segments 0, 2, 3), not 1.
|
|
let src_node = s.doc().unwrap().doc.node(r).unwrap();
|
|
let src_path = src_node.path_data().unwrap();
|
|
assert_eq!(src_path.subpaths[0].segment_count(), 3);
|
|
assert_eq!(src_path.subpaths[0].anchors.len(), 4);
|
|
assert!(!src_path.subpaths[0].closed);
|
|
|
|
// Sum of segments in clipboard + remaining = 1 + 3 = 4 (the original 4 segments).
|
|
assert_eq!(clip_path.subpaths[0].segment_count() + src_path.subpaths[0].segment_count(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_non_adjacent_anchors_on_rect_copies_full_object() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
|
|
// Select non-adjacent anchors 1 and 3 of the 4-anchor rectangle.
|
|
// Incident segments for anchor 1 are segments 0 and 1; for anchor 3 are segments 2 and 3.
|
|
// Together they cover all 4 segments, so the path is copied whole.
|
|
run(&mut s, "select.anchors", json!({"id": r.0, "anchors": [[0, 1], [0, 3]], "mode": "set"}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
if let NodeKind::Path { live, .. } = &pasted_node.kind {
|
|
assert!(live.is_some(), "copying all segments keeps live shape metadata");
|
|
}
|
|
let path = pasted_node.path_data().unwrap();
|
|
assert!(path.subpaths[0].closed);
|
|
assert_eq!(path.subpaths[0].segment_count(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_non_adjacent_anchors_on_five_point_path_extracts_selected_segments() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let id = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 10, "y": 10}]}))["id"].as_u64().unwrap());
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 50, "y": 10}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 60, "y": 50}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 30, "y": 80}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 10, "y": 50}));
|
|
run(&mut s, "path.close", json!({"id": id.0}));
|
|
|
|
// Path has 5 anchors (0..4) and 5 segments (0..4).
|
|
// Select non-adjacent anchors 1 and 3.
|
|
// Anchor 1 gives segments 0, 1. Anchor 3 gives segments 2, 3. Segment 4 is unselected.
|
|
run(&mut s, "select.anchors", json!({"id": id.0, "anchors": [[0, 1], [0, 3]], "mode": "set"}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
let path = pasted_node.path_data().unwrap();
|
|
assert!(!path.subpaths[0].closed);
|
|
assert_eq!(path.subpaths[0].segment_count(), 4);
|
|
assert_eq!(path.subpaths[0].anchors.len(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_when_all_segments_selected_preserves_live_shape() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
|
|
// Select all 4 segments explicitly (like Select > Object > Direction Handles).
|
|
run(&mut s, "select.segments", json!({"id": r.0, "segments": [[0, 0], [0, 1], [0, 2], [0, 3]]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
if let NodeKind::Path { live, .. } = &pasted_node.kind {
|
|
assert!(live.is_some(), "all segments selected preserves live shape");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn compound_member_selected_whole_with_direct_selection_promotes_member() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let outer = NodeId(run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 80, "height": 80}))["id"].as_u64().unwrap());
|
|
let inner = NodeId(run(&mut s, "shape.rectangle", json!({"x": 30, "y": 30, "width": 40, "height": 40}))["id"].as_u64().unwrap());
|
|
run(&mut s, "select.set", json!({"ids": [outer.0, inner.0]}));
|
|
let compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
|
|
|
|
let standalone = rect(&mut s, 120.0, 10.0, 50.0, 50.0);
|
|
|
|
// Select outer compound member whole, and direct-select anchor 0 on standalone.
|
|
let outer_anchors: Vec<Vec<usize>> = (0..4).map(|i| vec![0, i]).collect();
|
|
run(
|
|
&mut s,
|
|
"select.anchorsMany",
|
|
json!({
|
|
"items": [
|
|
{"id": outer.0, "anchors": outer_anchors},
|
|
{"id": standalone.0, "anchors": [[0, 0]]}
|
|
]
|
|
}),
|
|
);
|
|
|
|
// outer is selected whole, standalone is partially selected.
|
|
assert!(s.active().unwrap().selection.partial(outer).is_none());
|
|
assert!(s.active().unwrap().selection.partial(standalone).is_some());
|
|
|
|
run(&mut s, "edit.copy", json!({}));
|
|
// Clipboard should have:
|
|
// 1. The compound path (outer promoted to compound #786)
|
|
// 2. The extracted segment piece from standalone
|
|
let clip_ids: Vec<NodeId> = s.clipboard.nodes.iter().map(|n| n.id).collect();
|
|
assert_eq!(clip_ids.len(), 2, "promoted compound plus the extracted standalone piece");
|
|
assert!(clip_ids.contains(&compound), "whole compound member must be promoted to compound parent");
|
|
}
|
|
|
|
#[test]
|
|
fn cut_whole_compound_member_with_direct_selection_cuts_compound_and_extracted_piece() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let outer = NodeId(run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 80, "height": 80}))["id"].as_u64().unwrap());
|
|
let inner = NodeId(run(&mut s, "shape.rectangle", json!({"x": 30, "y": 30, "width": 40, "height": 40}))["id"].as_u64().unwrap());
|
|
run(&mut s, "select.set", json!({"ids": [outer.0, inner.0]}));
|
|
let compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
|
|
|
|
let standalone = NodeId(run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 50, "height": 50}))["id"].as_u64().unwrap());
|
|
|
|
let outer_anchors = (0..4).map(|i| vec![0, i]).collect::<Vec<_>>();
|
|
run(
|
|
&mut s,
|
|
"select.anchorsMany",
|
|
json!({
|
|
"items": [
|
|
{"id": outer.0, "anchors": outer_anchors},
|
|
{"id": standalone.0, "anchors": [[0, 0]]}
|
|
]
|
|
}),
|
|
);
|
|
|
|
run(&mut s, "edit.cut", json!({}));
|
|
let clip_ids: Vec<NodeId> = s.clipboard.nodes.iter().map(|n| n.id).collect();
|
|
assert_eq!(clip_ids.len(), 2, "promoted compound plus the extracted standalone piece");
|
|
assert!(clip_ids.contains(&compound), "whole compound member must be promoted to compound parent");
|
|
|
|
// Compound was removed because member was selected whole
|
|
let doc = &s.doc().unwrap().doc;
|
|
assert!(doc.node(compound).is_none(), "compound path must be removed on cut");
|
|
// Standalone has cut pieces remaining
|
|
assert!(doc.node(standalone).is_some(), "partially cut standalone path remains");
|
|
}
|
|
|
|
#[test]
|
|
fn cut_direct_selected_only_path_in_layer_preserves_layer() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
// Create a new layer
|
|
let layer_id = NodeId(run(&mut s, "layer.new", json!({}))["id"].as_u64().unwrap());
|
|
// Create a path inside the layer
|
|
let rect = NodeId(run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50}))["id"].as_u64().unwrap());
|
|
let doc = &s.doc().unwrap().doc;
|
|
assert_eq!(doc.parent_of(rect), Some(layer_id));
|
|
|
|
// Direct-select all 4 anchors of rect and cut
|
|
let all_anchors = (0..4).map(|i| vec![0, i]).collect::<Vec<_>>();
|
|
run(&mut s, "select.anchors", json!({"id": rect.0, "anchors": all_anchors, "mode": "set"}));
|
|
run(&mut s, "edit.cut", json!({}));
|
|
|
|
// The rect was cut, but the layer must NOT be deleted
|
|
let doc = &s.doc().unwrap().doc;
|
|
assert!(doc.node(rect).is_none(), "rect was cut");
|
|
assert!(doc.node(layer_id).is_some(), "layer must remain even when empty");
|
|
assert_eq!(doc.node(layer_id).unwrap().children().unwrap().len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_and_paste_path_in_compound_path_isolation_adds_member_to_compound() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let outer = NodeId(run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 80, "height": 80}))["id"].as_u64().unwrap());
|
|
let inner = NodeId(run(&mut s, "shape.rectangle", json!({"x": 30, "y": 30, "width": 40, "height": 40}))["id"].as_u64().unwrap());
|
|
run(&mut s, "select.set", json!({"ids": [outer.0, inner.0]}));
|
|
let compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
|
|
|
|
// Enter isolation mode on the compound path.
|
|
run(&mut s, "object.isolate", json!({"id": compound.0}));
|
|
assert_eq!(s.doc().unwrap().isolation, Some(compound));
|
|
|
|
// In isolation mode, select member `inner` whole.
|
|
run(&mut s, "select.set", json!({"ids": [inner.0]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
|
|
// Clipboard copies the selected member path, NOT the entire compound path.
|
|
assert_eq!(s.clipboard.nodes.len(), 1);
|
|
assert_eq!(s.clipboard.nodes[0].id, inner);
|
|
|
|
// Paste in isolation mode: the pasted path must be inserted into the compound path,
|
|
// not as a new path beside the compound path.
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
let pasted_id = NodeId(ids[0]);
|
|
|
|
let doc = &s.doc().unwrap().doc;
|
|
assert_eq!(doc.parent_of(pasted_id), Some(compound), "pasted path must be added to the compound path");
|
|
let compound_node = doc.node(compound).unwrap();
|
|
assert_eq!(compound_node.children().unwrap().len(), 3, "compound path now has 3 member paths");
|
|
}
|
|
|
|
#[test]
|
|
fn cut_and_paste_direct_selected_anchors_in_compound_path_isolation() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let outer = NodeId(run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 80, "height": 80}))["id"].as_u64().unwrap());
|
|
let inner = NodeId(run(&mut s, "shape.rectangle", json!({"x": 30, "y": 30, "width": 40, "height": 40}))["id"].as_u64().unwrap());
|
|
run(&mut s, "select.set", json!({"ids": [outer.0, inner.0]}));
|
|
let compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
|
|
|
|
// Enter isolation mode on the compound path.
|
|
run(&mut s, "object.isolate", json!({"id": compound.0}));
|
|
|
|
// Direct-select anchor 0 of `inner` and cut.
|
|
run(&mut s, "select.anchors", json!({"id": inner.0, "anchors": [[0, 0]], "mode": "set"}));
|
|
run(&mut s, "edit.cut", json!({}));
|
|
|
|
// Clipboard holds the cut segment piece.
|
|
assert_eq!(s.clipboard.nodes.len(), 1);
|
|
|
|
// Paste inside isolation mode:
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
let pasted_id = NodeId(ids[0]);
|
|
|
|
let doc = &s.doc().unwrap().doc;
|
|
assert_eq!(doc.parent_of(pasted_id), Some(compound), "cut and pasted anchors must be inserted into compound path");
|
|
let compound_node = doc.node(compound).unwrap();
|
|
assert_eq!(compound_node.children().unwrap().len(), 3);
|
|
check_session(&s).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn paste_non_path_in_compound_path_isolation_inserts_into_layer() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let a = rect(&mut s, 10.0, 10.0, 40.0, 40.0);
|
|
let b = rect(&mut s, 60.0, 10.0, 40.0, 40.0);
|
|
run(&mut s, "select.set", json!({"ids": [a.0, b.0]}));
|
|
let _group = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap());
|
|
|
|
// Copy the group to the clipboard.
|
|
run(&mut s, "edit.copy", json!({}));
|
|
|
|
// Create a compound path on the layer.
|
|
let c1 = rect(&mut s, 10.0, 100.0, 80.0, 80.0);
|
|
let c2 = rect(&mut s, 30.0, 120.0, 40.0, 40.0);
|
|
run(&mut s, "select.set", json!({"ids": [c1.0, c2.0]}));
|
|
let compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
|
|
let layer_id = s.doc().unwrap().doc.parent_of(compound).unwrap();
|
|
|
|
// Isolate the compound path.
|
|
run(&mut s, "object.isolate", json!({"id": compound.0}));
|
|
assert_eq!(s.doc().unwrap().isolation, Some(compound));
|
|
|
|
// Generic insertion parent inside compound isolation resolves to the layer, not the compound.
|
|
assert_eq!(s.doc().unwrap().insertion_parent(), Some(layer_id));
|
|
|
|
// Paste the group while in compound path isolation.
|
|
// The group cannot be inserted into the compound path; it must be pasted into the layer beside the compound.
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
let pasted_group = NodeId(ids[0]);
|
|
|
|
let doc = &s.doc().unwrap().doc;
|
|
assert_eq!(doc.parent_of(pasted_group), Some(layer_id), "pasted group must be added to layer, not compound");
|
|
let compound_node = doc.node(compound).unwrap();
|
|
assert_eq!(compound_node.children().unwrap().len(), 2, "compound path must keep only its 2 path children");
|
|
|
|
// All document invariants (including compound children being paths only) must hold.
|
|
check_session(&s).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn shape_creation_in_compound_path_isolation_inserts_into_layer() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let outer = rect(&mut s, 10.0, 10.0, 80.0, 80.0);
|
|
let inner = rect(&mut s, 30.0, 30.0, 40.0, 40.0);
|
|
run(&mut s, "select.set", json!({"ids": [outer.0, inner.0]}));
|
|
let compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
|
|
|
|
// Isolate the compound path.
|
|
run(&mut s, "object.isolate", json!({"id": compound.0}));
|
|
|
|
// Draw a new rectangle while isolating the compound path.
|
|
let new_rect = rect(&mut s, 40.0, 40.0, 20.0, 20.0);
|
|
|
|
let doc = &s.doc().unwrap().doc;
|
|
assert_ne!(doc.parent_of(new_rect), Some(compound), "new shape path is placed outside compound path in layer");
|
|
let compound_node = doc.node(compound).unwrap();
|
|
assert_eq!(compound_node.children().unwrap().len(), 2, "compound path maintains original member paths");
|
|
|
|
check_session(&s).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn paste_compound_into_isolated_compound_preserves_child_order() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 300, "height": 300}));
|
|
let r1 = rect(&mut s, 10.0, 10.0, 10.0, 10.0);
|
|
let r2 = rect(&mut s, 30.0, 10.0, 10.0, 10.0);
|
|
let r3 = rect(&mut s, 50.0, 10.0, 10.0, 10.0);
|
|
run(&mut s, "select.set", json!({"ids": [r1.0, r2.0, r3.0]}));
|
|
let src_compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
|
|
|
|
// Copy src_compound (contains [r1, r2, r3] in order)
|
|
run(&mut s, "select.set", json!({"ids": [src_compound.0]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
|
|
// Create target compound
|
|
let t1 = rect(&mut s, 100.0, 100.0, 80.0, 80.0);
|
|
let t2 = rect(&mut s, 120.0, 120.0, 40.0, 40.0);
|
|
run(&mut s, "select.set", json!({"ids": [t1.0, t2.0]}));
|
|
let target_compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
|
|
|
|
// Isolate target compound and select member t1 so paste-in-front targets index 1
|
|
run(&mut s, "object.isolate", json!({"id": target_compound.0}));
|
|
run(&mut s, "select.set", json!({"ids": [t1.0]}));
|
|
|
|
// Paste in front of t1
|
|
let pasted = run(&mut s, "edit.pasteInFront", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 3, "unpacked all 3 children");
|
|
|
|
let doc = &s.doc().unwrap().doc;
|
|
let target_node = doc.node(target_compound).unwrap();
|
|
let children = target_node.children().unwrap();
|
|
assert_eq!(children.len(), 5);
|
|
|
|
// Verify the 3 pasted children appear in children in exact forward order [r1_copy, r2_copy, r3_copy]
|
|
let child_ids: Vec<u64> = children.iter().map(|c| c.id.0).collect();
|
|
let pos0 = child_ids.iter().position(|&id| id == ids[0]).unwrap();
|
|
let pos1 = child_ids.iter().position(|&id| id == ids[1]).unwrap();
|
|
let pos2 = child_ids.iter().position(|&id| id == ids[2]).unwrap();
|
|
assert_eq!(pos1, pos0 + 1, "child 1 must immediately follow child 0");
|
|
assert_eq!(pos2, pos1 + 1, "child 2 must immediately follow child 1");
|
|
|
|
check_session(&s).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn copy_and_paste_direct_selected_segment_of_open_path() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let id = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 10, "y": 20}]}))["id"].as_u64().unwrap());
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 60, "y": 20}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 100, "y": 80}));
|
|
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
|
run(&mut s, "paint.setStroke", json!({"color": "#0000ff"}));
|
|
run(&mut s, "stroke.set", json!({"weight": 4.0}));
|
|
|
|
// Select segment 0 of the open path (from (10, 20) to (60, 20)).
|
|
run(&mut s, "select.segments", json!({"id": id.0, "segments": [[0, 0]]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
|
|
// Paste in place: position and appearance must be preserved.
|
|
let pasted = run(&mut s, "edit.pasteInPlace", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
let src_node = s.doc().unwrap().doc.node(id).unwrap();
|
|
assert_eq!(pasted_node.appearance, src_node.appearance, "appearance (stroke & fill) must match source");
|
|
|
|
let path = pasted_node.path_data().unwrap();
|
|
assert_eq!(path.subpaths.len(), 1);
|
|
let sp = &path.subpaths[0];
|
|
assert!(!sp.closed);
|
|
assert_eq!(sp.segment_count(), 1);
|
|
assert_eq!(sp.anchors.len(), 2);
|
|
assert_eq!(sp.anchors[0].p.x, 10.0);
|
|
assert_eq!(sp.anchors[0].p.y, 20.0);
|
|
assert_eq!(sp.anchors[1].p.x, 60.0);
|
|
assert_eq!(sp.anchors[1].p.y, 20.0);
|
|
|
|
check_session(&s).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn copy_two_non_adjacent_segments_of_open_path_pastes_two_paths() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 300, "height": 200}));
|
|
let id = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 10, "y": 10}]}))["id"].as_u64().unwrap());
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 40, "y": 10}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 80, "y": 10}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 120, "y": 10}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 160, "y": 10}));
|
|
|
|
// Path has 5 anchors (0..4) and 4 segments (0..3).
|
|
// Select disjoint/non-adjacent segments 0 and 2.
|
|
run(&mut s, "select.segments", json!({"id": id.0, "segments": [[0, 0], [0, 2]]}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 2, "two non-adjacent segments paste as two independent paths");
|
|
|
|
for node_id in ids {
|
|
let node = s.doc().unwrap().doc.node(NodeId(node_id)).unwrap();
|
|
let path = node.path_data().unwrap();
|
|
assert_eq!(path.subpaths.len(), 1);
|
|
assert!(!path.subpaths[0].closed);
|
|
assert_eq!(path.subpaths[0].segment_count(), 1);
|
|
assert_eq!(path.subpaths[0].anchors.len(), 2);
|
|
}
|
|
|
|
check_session(&s).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn copy_all_anchors_of_open_path_copies_whole_path() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let id = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 10, "y": 10}]}))["id"].as_u64().unwrap());
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 50, "y": 10}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 80, "y": 40}));
|
|
|
|
// Select all 3 anchors.
|
|
run(&mut s, "select.anchors", json!({"id": id.0, "anchors": [[0, 0], [0, 1], [0, 2]], "mode": "set"}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted = run(&mut s, "edit.paste", json!({}));
|
|
let ids: Vec<u64> = pasted["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids.len(), 1);
|
|
|
|
let pasted_node = s.doc().unwrap().doc.node(NodeId(ids[0])).unwrap();
|
|
let path = pasted_node.path_data().unwrap();
|
|
assert_eq!(path.subpaths.len(), 1);
|
|
assert!(!path.subpaths[0].closed);
|
|
assert_eq!(path.subpaths[0].segment_count(), 2);
|
|
assert_eq!(path.subpaths[0].anchors.len(), 3);
|
|
|
|
check_session(&s).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn copy_single_anchor_on_open_path_and_lone_point_states_what_is_pasted() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
|
|
// 1. Single interior anchor of an open path:
|
|
// Path: (10, 10) -> (50, 10) -> (90, 50).
|
|
// Selecting interior anchor 1 selects both incident segments (0 and 1).
|
|
// Stated behavior: pasting pastes an open path containing the anchor and both its incident segments (3 anchors, 2 segments).
|
|
let p1 = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 10, "y": 10}]}))["id"].as_u64().unwrap());
|
|
run(&mut s, "path.appendAnchor", json!({"id": p1.0, "x": 50, "y": 10}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": p1.0, "x": 90, "y": 50}));
|
|
run(&mut s, "select.anchors", json!({"id": p1.0, "anchors": [[0, 1]], "mode": "set"}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted1 = run(&mut s, "edit.paste", json!({}));
|
|
let ids1: Vec<u64> = pasted1["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids1.len(), 1);
|
|
let path1 = s.doc().unwrap().doc.node(NodeId(ids1[0])).unwrap().path_data().unwrap();
|
|
assert!(!path1.subpaths[0].closed);
|
|
assert_eq!(path1.subpaths[0].anchors.len(), 3);
|
|
assert_eq!(path1.subpaths[0].segment_count(), 2);
|
|
|
|
// 2. Single endpoint anchor of an open path:
|
|
// Selecting endpoint anchor 0 selects its single incident segment (segment 0).
|
|
// Stated behavior: pasting pastes an open path for that segment (2 anchors, 1 segment).
|
|
run(&mut s, "select.anchors", json!({"id": p1.0, "anchors": [[0, 0]], "mode": "set"}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted2 = run(&mut s, "edit.paste", json!({}));
|
|
let ids2: Vec<u64> = pasted2["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids2.len(), 1);
|
|
let path2 = s.doc().unwrap().doc.node(NodeId(ids2[0])).unwrap().path_data().unwrap();
|
|
assert!(!path2.subpaths[0].closed);
|
|
assert_eq!(path2.subpaths[0].anchors.len(), 2);
|
|
assert_eq!(path2.subpaths[0].segment_count(), 1);
|
|
|
|
// 3. A lone point path (a path with only 1 anchor and 0 segments):
|
|
// Selecting the lone anchor on a single-point path copies the object whole.
|
|
// Stated behavior: pasting pastes a standalone lone-point path (1 anchor, 0 segments).
|
|
let p_lone = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 100, "y": 100}]}))["id"].as_u64().unwrap());
|
|
run(&mut s, "select.anchors", json!({"id": p_lone.0, "anchors": [[0, 0]], "mode": "set"}));
|
|
run(&mut s, "edit.copy", json!({}));
|
|
let pasted_lone = run(&mut s, "edit.paste", json!({}));
|
|
let ids_lone: Vec<u64> = pasted_lone["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
|
assert_eq!(ids_lone.len(), 1);
|
|
let path_lone = s.doc().unwrap().doc.node(NodeId(ids_lone[0])).unwrap().path_data().unwrap();
|
|
assert_eq!(path_lone.subpaths[0].anchors.len(), 1);
|
|
assert_eq!(path_lone.subpaths[0].segment_count(), 0);
|
|
|
|
check_session(&s).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cut_anchor_of_closed_path_opens_it_with_single_undo_step() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
|
|
|
// Direct-select anchor 1 of the closed 4-anchor rectangle.
|
|
run(&mut s, "select.anchors", json!({"id": r.0, "anchors": [[0, 1]], "mode": "set"}));
|
|
let undo_before = s.doc().unwrap().history.undo.len();
|
|
|
|
// Cut the anchor.
|
|
run(&mut s, "edit.cut", json!({}));
|
|
assert_eq!(s.doc().unwrap().history.undo.len(), undo_before + 1, "cut is recorded as one undo step");
|
|
|
|
// Source rectangle opens: 1 open subpath with 3 anchors remaining.
|
|
let src_node = s.doc().unwrap().doc.node(r).unwrap();
|
|
let src_path = src_node.path_data().unwrap();
|
|
assert_eq!(src_path.subpaths.len(), 1);
|
|
assert!(!src_path.subpaths[0].closed, "cutting anchor of closed path opens the path");
|
|
assert_eq!(src_path.subpaths[0].anchors.len(), 3);
|
|
assert_eq!(src_path.subpaths[0].segment_count(), 2);
|
|
|
|
// Clipboard holds the cut portion (the anchor and its two incident segments: 3 anchors).
|
|
assert_eq!(s.clipboard.nodes.len(), 1);
|
|
let clip_path = s.clipboard.nodes[0].path_data().unwrap();
|
|
assert!(!clip_path.subpaths[0].closed);
|
|
assert_eq!(clip_path.subpaths[0].anchors.len(), 3);
|
|
assert_eq!(clip_path.subpaths[0].segment_count(), 2);
|
|
|
|
// Verify undo restores the closed path in one step.
|
|
run(&mut s, "edit.undo", json!({}));
|
|
let restored_node = s.doc().unwrap().doc.node(r).unwrap();
|
|
let restored_path = restored_node.path_data().unwrap();
|
|
assert_eq!(restored_path.subpaths.len(), 1);
|
|
assert!(restored_path.subpaths[0].closed, "undo restores closed path");
|
|
assert_eq!(restored_path.subpaths[0].anchors.len(), 4);
|
|
assert_eq!(restored_path.subpaths[0].segment_count(), 4);
|
|
|
|
// Verify redo re-applies the cut.
|
|
run(&mut s, "edit.redo", json!({}));
|
|
let redone_node = s.doc().unwrap().doc.node(r).unwrap();
|
|
let redone_path = redone_node.path_data().unwrap();
|
|
assert!(!redone_path.subpaths[0].closed);
|
|
assert_eq!(redone_path.subpaths[0].anchors.len(), 3);
|
|
|
|
check_session(&s).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cut_middle_segment_of_open_path_splits_into_two_open_paths_with_single_undo_step() {
|
|
let mut s = Session::new();
|
|
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
|
let id = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 10, "y": 10}]}))["id"].as_u64().unwrap());
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 50, "y": 10}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 90, "y": 10}));
|
|
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 130, "y": 10}));
|
|
|
|
// Open path has 4 anchors (0, 1, 2, 3) and 3 segments (0, 1, 2).
|
|
// Direct-select middle segment 1 (between anchor 1 and 2).
|
|
run(&mut s, "select.segments", json!({"id": id.0, "segments": [[0, 1]]}));
|
|
let undo_before = s.doc().unwrap().history.undo.len();
|
|
|
|
// Cut the middle segment.
|
|
run(&mut s, "edit.cut", json!({}));
|
|
assert_eq!(s.doc().unwrap().history.undo.len(), undo_before + 1, "cut is recorded as one undo step");
|
|
|
|
// Source path should now be split into two surviving open runs.
|
|
let src_node = s.doc().unwrap().doc.node(id).unwrap();
|
|
let src_path = src_node.path_data().unwrap();
|
|
assert_eq!(src_path.subpaths.len(), 2, "two open paths/subpaths remain after cutting middle segment");
|
|
assert!(!src_path.subpaths[0].closed);
|
|
assert_eq!(src_path.subpaths[0].anchors.len(), 2);
|
|
assert_eq!(src_path.subpaths[0].anchors[0].p.x, 10.0);
|
|
assert_eq!(src_path.subpaths[0].anchors[1].p.x, 50.0);
|
|
|
|
assert!(!src_path.subpaths[1].closed);
|
|
assert_eq!(src_path.subpaths[1].anchors.len(), 2);
|
|
assert_eq!(src_path.subpaths[1].anchors[0].p.x, 90.0);
|
|
assert_eq!(src_path.subpaths[1].anchors[1].p.x, 130.0);
|
|
|
|
// Clipboard holds the cut middle segment:
|
|
assert_eq!(s.clipboard.nodes.len(), 1);
|
|
let clip_path = s.clipboard.nodes[0].path_data().unwrap();
|
|
assert_eq!(clip_path.subpaths[0].anchors.len(), 2);
|
|
assert_eq!(clip_path.subpaths[0].anchors[0].p.x, 50.0);
|
|
assert_eq!(clip_path.subpaths[0].anchors[1].p.x, 90.0);
|
|
|
|
// Verify undo restores the original intact path in one step:
|
|
run(&mut s, "edit.undo", json!({}));
|
|
let restored_node = s.doc().unwrap().doc.node(id).unwrap();
|
|
let restored_path = restored_node.path_data().unwrap();
|
|
assert_eq!(restored_path.subpaths.len(), 1, "undo restores single subpath");
|
|
assert_eq!(restored_path.subpaths[0].anchors.len(), 4, "undo restores all 4 anchors");
|
|
|
|
// Redo re-applies the split:
|
|
run(&mut s, "edit.redo", json!({}));
|
|
let redone_path = s.doc().unwrap().doc.node(id).unwrap().path_data().unwrap();
|
|
assert_eq!(redone_path.subpaths.len(), 2);
|
|
|
|
check_session(&s).unwrap();
|
|
}
|