//! The Open compound and Join/Split modes (plan D1, D2): the mouse side of //! `e_compound.c` and `e_joinsplit.c`. The work is done by the commands //! `compound.open`, `object.join` and `object.split`. // // Ported from xfig src/e_compound.c and src/e_joinsplit.c (xfig licence, see LICENSE-xfig). use super::{markers::ObjMask, Button, ToolEvent, ToolKind, ToolState}; use crate::{Engine, EngineError}; use rustfig_geom::Point; use rustfig_model::{Object, ObjectId, ObjectKind, PolyKind}; use serde_json::json; /// The end point of a line or spline under the pointer. struct EndHit { id: ObjectId, /// The first point of the object (else the last). first: bool, spline: bool, } impl Engine { /// Open compound (`init_open_compound`): left opens the compound, middle /// opens it and keeps the rest of the figure visible (gray). pub(super) fn open_compound_event(&mut self, ev: ToolEvent) -> Result<(), EngineError> { let ToolEvent::Press { pos, button, .. } = ev else { return Ok(()); }; let keep_visible = match button { Button::Left => false, Button::Middle => true, Button::Right => return Ok(()), }; let tol = self.pick_tol(); let mask = ObjMask::COMPOUND; if let Some(id) = self.hit_test_where(pos, tol, &|o| mask.admits(o)) { self.execute("compound.open", &json!({"id": id.0, "keep_visible": keep_visible}))?; } Ok(()) } /// The end point of an open line or spline within `tol` of `pos`; /// `only` limits the search to lines (`Some(false)`) or splines. fn end_point_at(&self, pos: Point, tol: f64, only: Option) -> Option { let mask = match only { Some(false) => ObjMask::POLYLINE_LINE, Some(true) => ObjMask::SPLINE_OPEN, None => ObjMask::POLYLINE_LINE.union(ObjMask::SPLINE_OPEN), }; let mut best: Option<(f64, EndHit)> = None; for o in &self.doc().objects { if !mask.admits(o) || !self.is_visible(o) { continue; } let (pts, spline) = match &o.kind { ObjectKind::Polyline(l) => (&l.points, false), ObjectKind::Spline(s) => (&s.points, true), _ => continue, }; let (Some(first), Some(last)) = (pts.first(), pts.last()) else { continue; }; for (p, is_first) in [(first, true), (last, false)] { let d = p.dist(pos); if d <= tol && best.as_ref().is_none_or(|(bd, _)| d < *bd) { best = Some(( d, EndHit { id: o.id, first: is_first, spline, }, )); } } } best.map(|(_, h)| h) } /// Join/Split. Left picks an end point (`init_join`) and then the end /// point of the second line or spline (`join_line2`, `join_spline2`); /// middle splits at the pointer (`init_split`); right cancels a join. pub(super) fn join_split_event(&mut self, ev: ToolEvent) -> Result<(), EngineError> { let ToolEvent::Press { pos, button, .. } = ev else { return Ok(()); }; let tol = self.pick_tol(); if let ToolState::Join { id, first, spline } = self.tool_state { match button { Button::Left => { let Some(hit) = self.end_point_at(pos, tol, Some(spline)) else { return Ok(()); }; // The same point twice does nothing; both ends of one // object close it. if hit.id == id && hit.first == first { return Ok(()); } let end = |f: bool| if f { "first" } else { "last" }; let mut params = json!({"id": id.0, "end": end(first), "other_end": end(hit.first)}); if hit.id != id { params["other"] = json!(hit.id.0); } self.tool_state = ToolState::Idle; if let Err(e) = self.execute("object.join", ¶ms) { self.set_status(e.message); } } Button::Right => self.tool_state = ToolState::Idle, Button::Middle => {} } return Ok(()); } let split = button == Button::Middle; if button == Button::Left { let Some(hit) = self.end_point_at(pos, tol, None) else { return Ok(()); }; self.tool_state = ToolState::Join { id: hit.id, first: hit.first, spline: hit.spline, }; } else if split { let mask = ObjMask::POLYLINE.union(ObjMask::SPLINE); let Some(id) = self.hit_test_where(pos, tol, &|o: &Object| mask.admits(o)) else { return Ok(()); }; let params = json!({"id": id.0, "x": pos.x, "y": pos.y, "tolerance": tol}); if let Err(e) = self.execute("object.split", ¶ms) { // xfig: put_msg and beep; the figure stays as it is. self.set_status(e.message); } } Ok(()) } /// The end point a join started at, for the marker (`draw_join_marker`). pub(super) fn join_marker(&self) -> Option { let ToolState::Join { id, first, .. } = self.tool_state else { return None; }; let pts = match &self.doc().get(id)?.kind { ObjectKind::Polyline(l) if l.kind == PolyKind::Polyline => &l.points, ObjectKind::Spline(s) => &s.points, _ => return None, }; if first { pts.first().copied() } else { pts.last().copied() } } /// Whether the tool is Join/Split and has picked the first end. pub(super) fn joining(&self) -> bool { self.tool == ToolKind::JoinSplit && matches!(self.tool_state, ToolState::Join { .. }) } } .