diff --git a/crates/engine/src/cmd/modify.rs b/crates/engine/src/cmd/modify.rs index 7134f73a..059beb6a 100644 --- a/crates/engine/src/cmd/modify.rs +++ b/crates/engine/src/cmd/modify.rs @@ -219,6 +219,22 @@ impl StretchArea { } } +/// Add to `hs` the visible multilines in `space` with a control vertex inside `area`. A crossing +/// selection tests the drawn element lines, which sit away from the control vertices by the +/// multiline's scale, so a small area around a vertex alone would miss them. +fn add_mline_vertex_hits(d: &cadcraft_doc::Drawing, space: &cadcraft_doc::Space, area: &StretchArea, hs: &mut Vec) { + let Some(store) = d.space(space) else { return }; + for e in store.iter() { + if let EntityKind::MLine(m) = &e.kind + && d.is_visible(e) + && !hs.contains(&e.handle) + && m.vertices.iter().any(|v| area.contains(v.p.xy())) + { + hs.push(e.handle); + } + } +} + /// Move the vertices of `e` that lie inside any of the areas `wins` by `d` (STRETCH). fn stretch_entity(e: &mut Entity, wins: &[StretchArea], d: Vec2) { let inside = |p: Vec2| wins.iter().any(|a| a.contains(p)); @@ -240,6 +256,7 @@ fn stretch_entity(e: &mut Entity, wins: &[StretchArea], d: Vec2) { } } } + EntityKind::MLine(m) => m.vertices.iter_mut().for_each(|v| mv(&mut v.p)), EntityKind::Arc(a) => { // Arcs keep their shape: move when the centre is inside. if inside(a.center.xy()) { @@ -287,6 +304,7 @@ fn run_stretch(s: &mut Session, p: &Value) -> Result { let bx = cadcraft_geom::Bounds2::new(*a, *b); (crate::select::select_window(s.doc()?, &space, bx, true), StretchArea::Box(bx)) }; + add_mline_vertex_hits(s.doc()?, &space, &area, &mut hs); hs.retain(|h| !super::curves::is_locked(s, *h)); let doc = s.doc_mut()?; let areas = [area]; @@ -353,7 +371,12 @@ impl StretchM { /// of two) the mode is dropped. fn finish_mode(&mut self, s: &mut Session, m: SelectMode) -> Result<()> { let space = s.space(); - let Some(hs) = m.select(s.doc()?, &space) else { return Ok(()) }; + let Some(mut hs) = m.select(s.doc()?, &space) else { return Ok(()) }; + let crossing = match (m.kind, m.pts.first().zip(m.pts.get(1))) { + (SelectKind::Crossing | SelectKind::CPolygon, _) => true, + (SelectKind::Box, Some((a, b))) => b.x < a.x, + _ => false, + }; let area = match (m.kind, m.pts.first().zip(m.pts.get(1))) { (SelectKind::WPolygon | SelectKind::CPolygon, _) => Some(StretchArea::Polygon(m.pts)), (SelectKind::Window | SelectKind::Crossing | SelectKind::Box, Some((a, b))) => { @@ -363,6 +386,9 @@ impl StretchM { _ => None, }; let whole = area.is_none(); + if let (Some(a), true, false) = (&area, crossing, self.removing) { + add_mline_vertex_hits(s.doc()?, &space, a, &mut hs); + } if let (Some(a), false) = (area, self.removing) { self.windows.push(a); } @@ -400,8 +426,11 @@ impl StretchM { Some(c) => { let bx = cadcraft_geom::Bounds2::new(c, p); let space = s.space(); - let hs = crate::select::select_window(s.doc()?, &space, bx, p.x < c.x); + let mut hs = crate::select::select_window(s.doc()?, &space, bx, p.x < c.x); if !self.removing { + if p.x < c.x { + add_mline_vertex_hits(s.doc()?, &space, &StretchArea::Box(bx), &mut hs); + } self.windows.push(StretchArea::Box(bx)); } self.add(s, hs, false); diff --git a/crates/engine/tests/it/stretch_select.rs b/crates/engine/tests/it/stretch_select.rs index 252d2cb7..11888b4c 100644 --- a/crates/engine/tests/it/stretch_select.rs +++ b/crates/engine/tests/it/stretch_select.rs @@ -128,3 +128,49 @@ fn hostile_polygon_and_fence_never_panic() { s.cancel(); } } + +#[test] +fn polygon_moves_only_the_multiline_vertices_inside_it() { + let mut s = Session::new(); + s.execute("mline", &json!({ "points": [[0, 0], [3000, 0], [3000, 2000]], "scale": 200 })).unwrap(); + let r = s.execute("stretch", &json!({ "polygon": [[2500, 1500], [3500, 1500], [3500, 2500], [2500, 2500]], "delta": [610, 0] })).unwrap(); + assert_eq!(r["stretched"], 1); + let pts = |s: &Session| -> Vec<(f64, f64)> { + s.doc() + .unwrap() + .model + .iter() + .find_map(|e| match &e.kind { + EntityKind::MLine(m) => Some(m.vertices.iter().map(|v| (v.p.x, v.p.y)).collect()), + _ => None, + }) + .unwrap() + }; + assert_eq!(pts(&s), vec![(0.0, 0.0), (3000.0, 0.0), (3610.0, 2000.0)]); + // A window over the first vertex moves that vertex only, not the whole multiline. + s.execute("stretch", &json!({ "window": [[-100, -100], [100, 100]], "delta": [610, 0] })).unwrap(); + assert_eq!(pts(&s), vec![(610.0, 0.0), (3000.0, 0.0), (3610.0, 2000.0)]); +} + +#[test] +fn small_area_around_a_multiline_vertex_moves_it_at_a_large_scale() { + let mline_points = |s: &Session| -> Vec<(f64, f64)> { + s.doc() + .unwrap() + .model + .iter() + .find_map(|e| match &e.kind { + EntityKind::MLine(m) => Some(m.vertices.iter().map(|v| (v.p.x, v.p.y)).collect()), + _ => None, + }) + .unwrap() + }; + let mut s = Session::new(); + s.execute("mline", &json!({ "points": [[0, 0], [3000, 0], [3000, 2000]], "scale": 200 })).unwrap(); + // The element lines run 100 away from the middle vertex, so this area touches none of them. + let r = s.execute("stretch", &json!({ "polygon": [[2995, -5], [3005, -5], [3005, 5], [2995, 5]], "delta": [0, 610] })).unwrap(); + assert_eq!(r["stretched"], 1); + assert_eq!(mline_points(&s), vec![(0.0, 0.0), (3000.0, 610.0), (3000.0, 2000.0)]); + s.execute("stretch", &json!({ "window": [[2995, 605], [3005, 615]], "delta": [0, -610] })).unwrap(); + assert_eq!(mline_points(&s), vec![(0.0, 0.0), (3000.0, 0.0), (3000.0, 2000.0)]); +}