448 lines
18 KiB
Rust
448 lines
18 KiB
Rust
//! Pattern swatches, pattern editing mode and Repeat, driven through `Session::execute`.
|
||
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_color::Paint;
|
||
use vectorcraft_doc::pattern::{PATTERN_EDIT_LAYER, RepeatKind, TileType};
|
||
use vectorcraft_doc::{Node, NodeKind};
|
||
use vectorcraft_geom::{Point, Rect};
|
||
|
||
use super::*;
|
||
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn id_of(v: &Value) -> NodeId {
|
||
NodeId(v["id"].as_u64().unwrap())
|
||
}
|
||
|
||
fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> NodeId {
|
||
id_of(&s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": w, "height": h})).unwrap())
|
||
}
|
||
|
||
fn node(s: &Session, id: NodeId) -> Node {
|
||
s.doc().unwrap().doc.node(id).unwrap().clone()
|
||
}
|
||
|
||
fn sel(s: &mut Session, ids: &[NodeId]) {
|
||
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
|
||
}
|
||
|
||
fn doc(s: &Session) -> &vectorcraft_doc::Document {
|
||
&s.doc().unwrap().doc
|
||
}
|
||
|
||
/// A red 10×10 square made into pattern "Dots" (editing finished).
|
||
fn make_dots(s: &mut Session) -> NodeId {
|
||
let r = rect(s, 0.0, 0.0, 10.0, 10.0);
|
||
s.execute("paint.setFill", &json!({"ids": [r.0], "color": "#ff0000"})).unwrap();
|
||
s.execute("paint.setStroke", &json!({"ids": [r.0], "none": true})).unwrap();
|
||
sel(s, &[r]);
|
||
s.execute("object.pattern.make", &json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
|
||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||
r
|
||
}
|
||
|
||
fn close(a: Point, b: Point) -> bool {
|
||
(a - b).hypot() < 1e-6
|
||
}
|
||
|
||
// ---------- patterns ----------
|
||
|
||
#[test]
|
||
fn make_creates_definition_swatch_and_edit_mode() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
let v = s.execute("object.pattern.make", &json!({})).unwrap();
|
||
assert_eq!(v["name"], "New Pattern 1");
|
||
let d = doc(&s);
|
||
let def = d.pattern("New Pattern 1").unwrap();
|
||
assert_eq!(def.tile, Rect::new(0.0, 0.0, 10.0, 10.0));
|
||
assert!(d.swatch("New Pattern 1").is_some_and(|sw| matches!(sw.paint, Paint::Pattern { .. })));
|
||
let pe = d.pattern_edit.as_ref().unwrap();
|
||
assert_eq!(d.node(pe.layer).unwrap().name.as_deref(), Some(PATTERN_EDIT_LAYER));
|
||
assert_eq!(d.node(pe.layer).unwrap().children().unwrap().len(), 1);
|
||
// The original art stays in the document; new art goes into the edit layer.
|
||
assert!(d.node(r).is_some());
|
||
assert_eq!(s.doc().unwrap().insertion_parent(), Some(pe.layer));
|
||
}
|
||
|
||
#[test]
|
||
fn done_saves_edited_art_and_leaves_mode() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
s.execute("object.pattern.make", &json!({"name": "P"})).unwrap();
|
||
// Draw another shape inside the pattern.
|
||
rect(&mut s, 2.0, 2.0, 3.0, 3.0);
|
||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||
let d = doc(&s);
|
||
assert!(d.pattern_edit.is_none());
|
||
assert_eq!(d.pattern("P").unwrap().art.len(), 2);
|
||
assert!(d.layers.iter().all(|l| l.name.as_deref() != Some(PATTERN_EDIT_LAYER)));
|
||
assert!(s.doc().unwrap().isolation.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn cancel_new_pattern_removes_it() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
s.execute("object.pattern.make", &json!({"name": "P"})).unwrap();
|
||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||
assert!(doc(&s).pattern("P").is_none());
|
||
assert!(doc(&s).swatch("P").is_none());
|
||
assert_eq!(doc(&s).layers.len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn edit_then_cancel_restores_definition() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
s.execute("object.pattern.edit", &json!({"name": "Dots"})).unwrap();
|
||
s.execute("pattern.options", &json!({"tileType": "hexByRow", "width": 40})).unwrap();
|
||
assert_eq!(doc(&s).pattern("Dots").unwrap().tile_type, TileType::HexByRow);
|
||
s.execute("object.pattern.cancel", &json!({})).unwrap();
|
||
let def = doc(&s).pattern("Dots").unwrap();
|
||
assert_eq!(def.tile_type, TileType::Grid);
|
||
assert_eq!(def.tile.width(), 20.0);
|
||
}
|
||
|
||
#[test]
|
||
fn edit_defaults_to_selected_objects_pattern() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
let big = rect(&mut s, 100.0, 100.0, 100.0, 100.0);
|
||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||
sel(&mut s, &[big]);
|
||
let v = s.execute("object.pattern.edit", &json!({})).unwrap();
|
||
assert_eq!(v["name"], "Dots");
|
||
assert!(doc(&s).pattern_edit.is_some());
|
||
// Edit again while editing is refused.
|
||
assert!(s.execute("object.pattern.edit", &json!({"name": "Dots"})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn set_fill_with_pattern_swatch() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
let big = rect(&mut s, 100.0, 100.0, 100.0, 100.0);
|
||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||
assert!(matches!(node(&s, big).appearance.fill_paint(), Paint::Pattern { pattern, .. } if pattern == "Dots"));
|
||
}
|
||
|
||
#[test]
|
||
fn set_fill_with_pattern_name_without_swatch() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
Arc::make_mut(&mut s.doc_mut().unwrap().doc).swatches.retain(|sw| sw.name != "Dots");
|
||
let big = rect(&mut s, 100.0, 100.0, 100.0, 100.0);
|
||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||
assert!(matches!(node(&s, big).appearance.fill_paint(), Paint::Pattern { .. }));
|
||
}
|
||
|
||
#[test]
|
||
fn options_set_tile_type_spacing_and_overlap() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
s.execute(
|
||
"pattern.options",
|
||
&json!({"name": "Dots", "tileType": "brickByColumn", "brickOffset": 0.25, "overlap": {"h": "right", "v": "bottom"}, "copies": 7, "dimCopies": 40}),
|
||
)
|
||
.unwrap();
|
||
let def = doc(&s).pattern("Dots").unwrap();
|
||
assert_eq!(def.tile_type, TileType::BrickByColumn { offset: 0.25 });
|
||
assert!(def.overlap.right_in_front && def.overlap.bottom_in_front);
|
||
assert_eq!(def.copies, 7);
|
||
assert_eq!(def.dim_copies, 40.0);
|
||
// Size tile to art with spacing.
|
||
s.execute("pattern.options", &json!({"name": "Dots", "sizeTileToArt": true, "hSpacing": 4, "vSpacing": 6})).unwrap();
|
||
let def = doc(&s).pattern("Dots").unwrap();
|
||
assert!((def.tile.width() - 14.0).abs() < 1e-6 && (def.tile.height() - 16.0).abs() < 1e-6, "{:?}", def.tile);
|
||
assert!(s.execute("pattern.options", &json!({"name": "Dots", "tileType": "zigzag"})).is_err());
|
||
assert!(s.execute("pattern.options", &json!({"name": "Dots", "width": -1})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn options_rename_updates_swatch_and_paints() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||
s.execute("pattern.options", &json!({"name": "Dots", "newName": "Spots"})).unwrap();
|
||
assert!(doc(&s).pattern("Spots").is_some() && doc(&s).pattern("Dots").is_none());
|
||
assert!(doc(&s).swatch("Spots").is_some());
|
||
assert!(matches!(node(&s, big).appearance.fill_paint(), Paint::Pattern { pattern, .. } if pattern == "Spots"));
|
||
}
|
||
|
||
#[test]
|
||
fn delete_pattern_unpaints_objects() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||
let v = s.execute("pattern.delete", &json!({"name": "Dots"})).unwrap();
|
||
assert_eq!(v["unpainted"], 1);
|
||
assert!(doc(&s).pattern("Dots").is_none() && doc(&s).swatch("Dots").is_none());
|
||
assert!(node(&s, big).appearance.fill_paint().is_none());
|
||
assert!(s.execute("pattern.delete", &json!({"name": "Dots"})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn transform_patterns_only_moves_placement() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||
let before = node(&s, big).geometric_bounds();
|
||
s.execute("pattern.transform", &json!({"ids": [big.0], "matrix": [2, 0, 0, 2, 5, 0]})).unwrap();
|
||
let n = node(&s, big);
|
||
assert_eq!(n.geometric_bounds(), before);
|
||
let Paint::Pattern { xf, .. } = n.appearance.fill_paint() else { panic!() };
|
||
assert_eq!(xf.as_coeffs(), [2.0, 0.0, 0.0, 2.0, 5.0, 0.0]);
|
||
}
|
||
|
||
#[test]
|
||
fn list_reports_patterns_and_edit_state() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
let v = s.execute("pattern.list", &json!({})).unwrap();
|
||
assert_eq!(v["patterns"][0]["name"], "Dots");
|
||
assert_eq!(v["patterns"][0]["tileType"], "grid");
|
||
assert_eq!(v["patterns"][0]["width"], 20.0);
|
||
assert!(v["editing"].is_null());
|
||
s.execute("object.pattern.edit", &json!({"name": "Dots"})).unwrap();
|
||
assert_eq!(s.execute("pattern.list", &json!({})).unwrap()["editing"]["name"], "Dots");
|
||
}
|
||
|
||
#[test]
|
||
fn save_a_copy_while_editing() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
s.execute("object.pattern.edit", &json!({"name": "Dots"})).unwrap();
|
||
let v = s.execute("object.pattern.saveCopy", &json!({})).unwrap();
|
||
assert_eq!(v["name"], "Dots copy");
|
||
assert!(doc(&s).pattern("Dots copy").is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn undo_make_pattern() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
s.execute("object.pattern.make", &json!({"name": "P"})).unwrap();
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert!(doc(&s).pattern("P").is_none() && doc(&s).pattern_edit.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn hit_testing_in_edit_mode_only_reaches_tile_layer() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let other = rect(&mut s, 200.0, 200.0, 50.0, 50.0);
|
||
sel(&mut s, &[r]);
|
||
s.execute("object.pattern.make", &json!({"name": "P"})).unwrap();
|
||
let d = doc(&s);
|
||
let opt = vectorcraft_doc::hit::HitOptions::default();
|
||
assert!(vectorcraft_doc::hit::hit_test(d, Point::new(220.0, 220.0), opt).is_none());
|
||
let h = vectorcraft_doc::hit::hit_test(d, Point::new(5.0, 5.0), opt).unwrap();
|
||
assert_ne!(h.leaf, r);
|
||
let _ = other;
|
||
}
|
||
|
||
#[test]
|
||
fn save_load_roundtrip_keeps_patterns_and_repeats() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||
let r = rect(&mut s, 300.0, 50.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
s.execute("object.repeat.radial", &json!({"instances": 6})).unwrap();
|
||
let bytes = vectorcraft_format::save(doc(&s), false);
|
||
let back = vectorcraft_format::load(&bytes).unwrap();
|
||
assert_eq!(back.patterns, doc(&s).patterns);
|
||
assert_eq!(back.node(big).unwrap().appearance.fill_paint(), node(&s, big).appearance.fill_paint());
|
||
assert!(back.layers[0].children().unwrap().iter().any(|n| matches!(n.kind, NodeKind::Repeat(_))));
|
||
}
|
||
|
||
#[test]
|
||
fn svg_export_writes_pattern_def() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||
let svg = vectorcraft_svg::export(doc(&s), &vectorcraft_svg::ExportOptions::default());
|
||
assert!(svg.contains("<pattern id=\"pattern-1\""), "{svg}");
|
||
assert!(svg.contains("patternUnits=\"userSpaceOnUse\""));
|
||
assert!(svg.contains("width=\"20\" height=\"20\""));
|
||
assert!(svg.contains("fill=\"url(#pattern-1)\"") || svg.contains("fill:url(#pattern-1)"), "{svg}");
|
||
// The tile art is inside the pattern.
|
||
let start = svg.find("<pattern").unwrap();
|
||
let end = svg.find("</pattern>").unwrap();
|
||
assert!(svg[start..end].contains("<path"));
|
||
}
|
||
|
||
#[test]
|
||
fn pdf_export_expands_pattern_fills() {
|
||
let mut s = session();
|
||
make_dots(&mut s);
|
||
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
|
||
let rep = vectorcraft_pdf::export_with_report(doc(&s), &Default::default()).unwrap();
|
||
assert!(rep.warnings.iter().all(|w| !w.contains("pattern")), "{:?}", rep.warnings);
|
||
assert!(rep.bytes.len() > 500);
|
||
}
|
||
|
||
// ---------- repeat ----------
|
||
|
||
#[test]
|
||
fn radial_repeat_wraps_selection() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 195.0, 95.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
let v = s.execute("object.repeat.radial", &json!({"instances": 8, "radius": 50})).unwrap();
|
||
let n = node(&s, id_of(&v));
|
||
let NodeKind::Repeat(spec) = &n.kind else { panic!("{:?}", n.kind) };
|
||
assert_eq!(spec.source.len(), 1);
|
||
let RepeatKind::Radial { instances, radius, center, .. } = spec.kind else { panic!() };
|
||
assert_eq!((instances, radius), (8, 50.0));
|
||
assert!(close(center, Point::new(200.0, 150.0)));
|
||
assert_eq!(spec.transforms().len(), 8);
|
||
assert_eq!(n.kind_label(), "Radial Repeat");
|
||
}
|
||
|
||
#[test]
|
||
fn radial_instance_transforms_rotate_around_center() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 195.0, 95.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
let v = s.execute("object.repeat.radial", &json!({"instances": 4, "radius": 50})).unwrap();
|
||
let NodeKind::Repeat(spec) = node(&s, id_of(&v)).kind else { panic!() };
|
||
let t = spec.transforms();
|
||
let c = Point::new(200.0, 100.0);
|
||
assert!(close(t[0] * c, Point::new(200.0, 100.0)));
|
||
assert!(close(t[1] * c, Point::new(250.0, 150.0)));
|
||
assert!(close(t[2] * c, Point::new(200.0, 200.0)));
|
||
assert!(close(t[3] * c, Point::new(150.0, 150.0)));
|
||
}
|
||
|
||
#[test]
|
||
fn expand_yields_n_copies() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 195.0, 95.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
let v = s.execute("object.repeat.radial", &json!({"instances": 5})).unwrap();
|
||
let id = id_of(&v);
|
||
s.execute("object.repeat.expand", &json!({})).unwrap();
|
||
let g = node(&s, id);
|
||
assert!(matches!(g.kind, NodeKind::Group { .. }));
|
||
assert_eq!(g.children().unwrap().len(), 5);
|
||
// Every generated node has a real, unique id.
|
||
let mut ids = vec![];
|
||
g.walk(&mut |n| ids.push(n.id));
|
||
assert!(ids.iter().all(|i| i.0 != 0));
|
||
let mut u = ids.clone();
|
||
u.sort();
|
||
u.dedup();
|
||
assert_eq!(u.len(), ids.len());
|
||
}
|
||
|
||
#[test]
|
||
fn grid_repeat_and_options() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
let v = s.execute("object.repeat.grid", &json!({"hSpacing": 5, "vSpacing": 5})).unwrap();
|
||
let id = id_of(&v);
|
||
assert_eq!(node(&s, id).geometric_bounds(), Some(Rect::new(0.0, 0.0, 40.0, 40.0)));
|
||
s.execute("object.repeat.options", &json!({"rows": 2, "cols": 4, "flipCols": true, "gridType": "brickByRow"})).unwrap();
|
||
let NodeKind::Repeat(spec) = node(&s, id).kind else { panic!() };
|
||
let RepeatKind::Grid { rows, cols, flip_cols, grid_type, .. } = spec.kind else { panic!() };
|
||
assert_eq!((rows, cols, flip_cols), (2, 4, true));
|
||
assert_eq!(grid_type, TileType::BrickByRow { offset: 0.5 });
|
||
assert_eq!(spec.expand().len(), 8);
|
||
}
|
||
|
||
#[test]
|
||
fn mirror_repeat_reflects() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
let v = s.execute("object.repeat.mirror", &json!({"offset": 5})).unwrap();
|
||
let n = node(&s, id_of(&v));
|
||
let b = n.geometric_bounds().unwrap();
|
||
assert!((b.x1 - 30.0).abs() < 1e-9 && b.x0.abs() < 1e-9, "{b:?}");
|
||
s.execute("object.repeat.options", &json!({"angle": 0})).unwrap();
|
||
let b = node(&s, id_of(&v)).geometric_bounds().unwrap();
|
||
assert!((b.width() - 10.0).abs() < 1e-9, "{b:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn switching_repeat_kind_keeps_source() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
let v = s.execute("object.repeat.radial", &json!({})).unwrap();
|
||
let id = id_of(&v);
|
||
let v2 = s.execute("object.repeat.grid", &json!({})).unwrap();
|
||
assert_eq!(id_of(&v2), id);
|
||
let NodeKind::Repeat(spec) = node(&s, id).kind else { panic!() };
|
||
assert!(matches!(spec.kind, RepeatKind::Grid { .. }));
|
||
assert_eq!(spec.source[0].id, r);
|
||
}
|
||
|
||
#[test]
|
||
fn release_restores_source() {
|
||
let mut s = session();
|
||
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[a, b]);
|
||
s.execute("object.repeat.mirror", &json!({})).unwrap();
|
||
assert!(doc(&s).layers[0].children().unwrap().len() == 1);
|
||
let v = s.execute("object.repeat.release", &json!({})).unwrap();
|
||
assert_eq!(v["ids"], json!([a.0, b.0]));
|
||
assert_eq!(node(&s, a).geometric_bounds(), Some(Rect::new(0.0, 0.0, 10.0, 10.0)));
|
||
assert!(s.execute("object.repeat.release", &json!({})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn repeat_moves_with_transform() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 195.0, 95.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
let v = s.execute("object.repeat.radial", &json!({"instances": 4, "radius": 50})).unwrap();
|
||
let id = id_of(&v);
|
||
let before = node(&s, id).geometric_bounds().unwrap();
|
||
s.execute("object.transform", &json!({"ids": [id.0], "matrix": [1, 0, 0, 1, 10, 20]})).unwrap();
|
||
let after = node(&s, id).geometric_bounds().unwrap();
|
||
assert!(after.x0 > before.x0 && after.y0 > before.y0, "{before:?} {after:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn repeat_hit_test_uses_instances() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 195.0, 95.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
s.execute("object.repeat.radial", &json!({"instances": 4, "radius": 50})).unwrap();
|
||
let d = doc(&s);
|
||
let opt = vectorcraft_doc::hit::HitOptions::default();
|
||
assert!(vectorcraft_doc::hit::hit_test(d, Point::new(250.0, 150.0), opt).is_some());
|
||
// The empty centre of the ring is not the repeat.
|
||
assert!(vectorcraft_doc::hit::hit_test(d, Point::new(200.0, 150.0), opt).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn repeat_commands_validate() {
|
||
let mut s = session();
|
||
let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||
sel(&mut s, &[r]);
|
||
assert!(s.execute("object.repeat.radial", &json!({"instances": 0})).is_err());
|
||
assert!(s.execute("object.repeat.options", &json!({"instances": 4})).is_err(), "no repeat selected");
|
||
s.execute("select.none", &json!({})).ok();
|
||
assert!(s.execute("object.pattern.make", &json!({})).is_err());
|
||
}
|