Files
cadcraft/crates/engine/src/cmd/modify/break_join.rs
T
Dylan M. Taylor 2721aa8982 Enforce Rust coding standards via Clippy (#2239)
* chore(clippy): enforce workspace Rust quality gates

* refactor(doc): use explicit entity imports

* refactor(constraints): split residual evaluation

* refactor(fonts): split mtext layout

* refactor(render): use explicit entity imports

* fix(fonts): retain height for empty paragraphs

* fix(doc): repair dimension and dynamic block links

* refactor(render): split entity drawing helpers

* docs: document workspace lint inheritance

* refactor(render): isolate dimension geometry

* refactor(engine): use explicit imports

* refactor(engine): split complex edit paths into helpers

* refactor(io): split DXF read and write helpers

* refactor(ui): split canvas interaction and drawing

* refactor(ui): split command and drawing helpers

* test(i18n): split locale preference checks

* fix(ui): use current egui scope id API

* fix(dim): preserve text offset and ellipse grip behavior
2026-10-27 15:55:09 -04:00

281 lines
11 KiB
Rust

use super::trim::{param_on, sub_polyline};
use super::{
Accept, Arc, EngineError, Entity, EntityKind, Handle, Input, Interactive, Line, PolyVertex, Polyline, Prompt, Result, Segment, Session, Step,
Value, Vec2, arc, bad, json, lwpoly, norm_angle, point_param, point_req, targets,
};
/// Gives a rebuilt arc piece its source centre Z, or a rebuilt polyline piece its source elevation.
fn with_z(mut k: EntityKind, z: f64) -> EntityKind {
match &mut k {
EntityKind::Arc(a) => a.center.z = z,
EntityKind::LwPolyline(p) => p.elevation = z,
_ => {}
}
k
}
/// Break `h` between two points (or at one point when p1 == p2).
pub(crate) fn break_entity(s: &mut Session, h: Handle, p1: Vec2, p2: Vec2) -> Result<Vec<Handle>> {
if crate::cmd::curves::is_locked(s, h) {
return Err(EngineError::Other("The object is on a locked layer.".into()));
}
let e = s.doc()?.entity(h).map(|e| (**e).clone()).ok_or_else(|| EngineError::Other("no such object".into()))?;
let pieces: Vec<EntityKind> = match &e.kind {
EntityKind::Line(l) => {
let ln = Line::new(l.a.xy(), l.b.xy());
let (mut t1, mut t2) = (ln.param_of(p1).clamp(0.0, 1.0), ln.param_of(p2).clamp(0.0, 1.0));
if t1 > t2 {
std::mem::swap(&mut t1, &mut t2);
}
let at3 = |t: f64| ln.at(t).to3(l.a.z + (l.b.z - l.a.z) * t);
let mut v = Vec::new();
if t1 > 1e-12 {
v.push(EntityKind::Line(cadcraft_doc::Line { a: l.a, b: at3(t1) }));
}
if t2 < 1.0 - 1e-12 {
v.push(EntityKind::Line(cadcraft_doc::Line { a: at3(t2), b: l.b }));
}
v
}
EntityKind::Circle(c) => {
if p1.near(p2, 1e-12) {
return Err(EngineError::Other("Arc cannot be full 360 degrees.".into()));
}
let a1 = c.center.xy().angle_to(p1);
let a2 = c.center.xy().angle_to(p2);
// AutoCAD removes the CCW portion from the first to the second point.
vec![with_z(arc(&Arc::new(c.center.xy(), c.radius, a2, a1)), c.center.z)]
}
EntityKind::Arc(a) => {
let ga = Arc::new(a.center.xy(), a.radius, a.start, a.end);
let par = |p: Vec2| norm_angle(ga.center.angle_to(p) - ga.start).min(ga.sweep());
let (mut t1, mut t2) = (par(p1), par(p2));
if t1 > t2 {
std::mem::swap(&mut t1, &mut t2);
}
let mut v = Vec::new();
if t1 > 1e-12 {
v.push(with_z(arc(&Arc::new(ga.center, ga.radius, ga.start, ga.start + t1)), a.center.z));
}
if t2 < ga.sweep() - 1e-12 {
v.push(with_z(arc(&Arc::new(ga.center, ga.radius, ga.start + t2, ga.end)), a.center.z));
}
v
}
EntityKind::LwPolyline(p) => {
let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed };
let segs = pl.segments();
let total: f64 = segs.iter().map(Segment::len).sum();
let lp = |q: Vec2| {
let mut acc = 0.0;
let mut best = (f64::INFINITY, 0.0);
for s in &segs {
let c = s.closest(q);
let dd = c.dist(q);
if dd < best.0 {
best = (dd, acc + param_on(s, c) * s.len());
}
acc += s.len();
}
best.1
};
let (mut t1, mut t2) = (lp(p1), lp(p2));
if p.closed {
if (t1 - t2).abs() < 1e-9 {
return Err(EngineError::Other("Cannot break a closed polyline at a single point.".into()));
}
// Like a circle: the stretch from the first point to the second, along the
// vertices, goes; what is left runs from the second point round to the first.
let doubled: Vec<Segment> = segs.iter().chain(segs.iter()).copied().collect();
let end = if t1 > t2 { t1 } else { t1 + total };
vec![with_z(lwpoly(sub_polyline(&doubled, t2, end), false), p.elevation)]
} else {
if t1 > t2 {
std::mem::swap(&mut t1, &mut t2);
}
let mut v = Vec::new();
if t1 > 1e-12 {
v.push(with_z(lwpoly(sub_polyline(&segs, 0.0, t1), false), p.elevation));
}
if t2 < total - 1e-12 {
v.push(with_z(lwpoly(sub_polyline(&segs, t2, total), false), p.elevation));
}
v
}
}
_ => return Err(EngineError::Other("Cannot break that object.".into())),
};
let common = e.common.clone();
let space = s.space();
let d = s.doc_mut()?;
d.remove_entity(h);
let mut out = Vec::new();
for k in pieces {
let nh = d.new_handle();
if let Some(st) = d.space_mut(&space) {
st.push(Entity { handle: nh, common: common.clone(), kind: k });
out.push(nh);
}
}
Ok(out)
}
pub(super) fn run_break(s: &mut Session, p: &Value) -> Result<Value> {
let h = targets(s, p)?.first().copied().ok_or_else(|| bad("break", "`handle` is required"))?;
let p1 = point_req("break", p, "p1")?;
let p2 = point_param(p, "p2").unwrap_or(p1);
let r = break_entity(s, h, p1, p2)?;
Ok(json!({ "handles": r.iter().map(|h| h.hex()).collect::<Vec<_>>() }))
}
pub(super) fn run_breakat(s: &mut Session, p: &Value) -> Result<Value> {
let h = targets(s, p)?.first().copied().ok_or_else(|| bad("breakatpoint", "`handle` is required"))?;
let at = point_req("breakatpoint", p, "at")?;
let r = break_entity(s, h, at, at)?;
Ok(json!({ "handles": r.iter().map(|h| h.hex()).collect::<Vec<_>>() }))
}
/// Join lines/arcs/polylines that share endpoints into one polyline (or merge collinear lines).
pub(crate) fn join(s: &mut Session, hs: &[Handle]) -> Result<Option<Handle>> {
if let Some(h) = crate::cmd::modify2::join_curves(s, hs)? {
return Ok(Some(h));
}
if hs.iter().any(|h| crate::cmd::curves::is_locked(s, *h)) {
return Err(EngineError::Other("An object is on a locked layer.".into()));
}
let d = s.doc()?;
let mut segs: Vec<Segment> = Vec::new();
// Source entity of each segment in `segs`, so only entities that end up in the chain are removed.
let mut owners: Vec<Handle> = Vec::new();
let mut common = None;
for h in hs {
let Some(e) = d.entity(*h) else { continue };
common.get_or_insert_with(|| e.common.clone());
match &e.kind {
EntityKind::Line(l) => segs.push(Segment::Line(Line::new(l.a.xy(), l.b.xy()))),
EntityKind::Arc(a) => segs.push(Segment::Arc { arc: Arc::new(a.center.xy(), a.radius, a.start, a.end), ccw: true }),
EntityKind::LwPolyline(p) if !p.closed => segs.extend(Polyline { vertices: p.vertices.clone(), closed: false }.segments()),
_ => {}
}
owners.resize(segs.len(), *h);
}
if segs.len() < 2 {
return Err(EngineError::Other("Select at least two objects to join.".into()));
}
// Chain greedily from the first segment.
let mut chain = vec![segs.remove(0)];
let mut used = vec![owners.remove(0)];
let tol = 1e-6;
loop {
let start = chain.first().map(Segment::start).unwrap_or_default();
let end = chain.last().map(Segment::end).unwrap_or_default();
let Some(i) =
segs.iter().position(|s| s.start().near(end, tol) || s.end().near(end, tol) || s.start().near(start, tol) || s.end().near(start, tol))
else {
break;
};
let s0 = segs.remove(i);
used.push(owners.remove(i));
if s0.start().near(end, tol) {
chain.push(s0);
} else if s0.end().near(end, tol) {
chain.push(s0.reversed());
} else if s0.end().near(start, tol) {
chain.insert(0, s0);
} else {
chain.insert(0, s0.reversed());
}
}
if chain.len() < 2 {
return Err(EngineError::Other("Objects are not connected.".into()));
}
let closed = chain.first().zip(chain.last()).is_some_and(|(a, b)| a.start().near(b.end(), tol));
let mut vs: Vec<PolyVertex> = chain
.iter()
.map(|s| PolyVertex {
p: s.start(),
bulge: match s {
Segment::Line(_) => 0.0,
Segment::Arc { arc, ccw } => cadcraft_geom::arc_to_bulge(if *ccw { arc.sweep() } else { -arc.sweep() }),
},
..Default::default()
})
.collect();
if !closed && let Some(l) = chain.last() {
vs.push(PolyVertex::new(l.end()));
}
let space = s.space();
let doc = s.doc_mut()?;
for h in hs.iter().filter(|h| used.contains(*h)) {
doc.remove_entity(*h);
}
let nh = doc.new_handle();
if let Some(st) = doc.space_mut(&space) {
st.push(Entity { handle: nh, common: common.unwrap_or_default(), kind: lwpoly(vs, closed) });
}
s.set_selection(Vec::new());
Ok(Some(nh))
}
pub(super) fn run_join(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
if p.get("close").and_then(Value::as_bool) == Some(true) {
let h = crate::cmd::modify2::join_close(s, &hs)?;
return Ok(json!({ "handle": h.hex() }));
}
let r = join(s, &hs)?;
Ok(json!({ "handle": r.map(|h| h.hex()) }))
}
#[derive(Default)]
pub(super) struct BreakM {
obj: Option<(Handle, Vec2)>,
/// The `First point` option: the next point replaces the selection pick as the first break point.
first_point: bool,
}
impl Interactive for BreakM {
fn name(&self) -> &'static str {
"BREAK"
}
fn prompt(&self, _s: &Session) -> Prompt {
match self.obj {
None => Prompt::new("Select object", Accept::POINT),
Some(_) if self.first_point => Prompt::new("Specify first break point", Accept::POINT),
Some(_) => Prompt::new("Specify second break point", Accept::POINT).kw(&["First point"]),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match (self.obj, i) {
(None, Input::Point(p)) => {
let ap = s.pixel_size() * s.settings.pickbox.max(1.0) * 1.5;
let space = s.space();
match crate::select::pick(s.doc()?, &space, p, ap) {
Some(h) => {
self.obj = Some((h, p));
s.set_selection(vec![h]);
}
None => s.echo("*Invalid selection*"),
}
Ok(Step::Continue)
}
(Some(_), Input::Keyword(k)) if k == "First point" => {
self.first_point = true;
Ok(Step::Continue)
}
(Some((h, _)), Input::Point(p1)) if self.first_point => {
self.obj = Some((h, p1));
self.first_point = false;
Ok(Step::Continue)
}
(Some((h, p1)), Input::Point(p2)) => {
break_entity(s, h, p1, p2)?;
s.set_selection(Vec::new());
Ok(Step::Done)
}
(_, Input::Enter) => Ok(Step::Done),
_ => Ok(Step::Continue),
}
}
}