diff --git a/crates/engine/src/cmd/constraints.rs b/crates/engine/src/cmd/constraints.rs index 368c778c..1873818c 100644 --- a/crates/engine/src/cmd/constraints.rs +++ b/crates/engine/src/cmd/constraints.rs @@ -806,7 +806,20 @@ fn run_delconstraint(s: &mut Session, p: &Value) -> Result { }; let n = s.doc()?.constraints.iter().filter(|c| doomed(c)).count(); if n > 0 { - s.doc_mut()?.constraints.retain(|c| !doomed(c)); + let mut d = s.doc()?.clone(); + let broken_before: Vec = cs::parameter_table(&d).into_iter().filter(|r| r.error.is_some()).map(|r| r.name).collect(); + let removed: Vec = d.constraints.iter().filter(|c| doomed(c)).map(|c| c.name.clone()).collect(); + d.constraints.retain(|c| !doomed(c)); + // Reject when a surviving expression would lose a dimension it refers to (same guard as `parameters delete`). + if let Some(row) = cs::parameter_table(&d).into_iter().find(|r| r.error.is_some() && !broken_before.contains(&r.name)) { + let mut refs = Vec::new(); + if let Ok(e) = cs::expr::parse(&row.expr) { + cs::expr::names(&e, &mut refs); + } + let used = removed.iter().find(|n| refs.contains(*n)).or(removed.first()).cloned().unwrap_or_default(); + return Err(EngineError::Other(format!("`{used}` is used by `{}`", row.name))); + } + *s.doc_mut()? = d; } Ok(json!({ "removed": n, "message": format!("{n} constraint(s) removed") })) } diff --git a/crates/engine/src/cmd/constraints_tests.rs b/crates/engine/src/cmd/constraints_tests.rs index 46e418d3..a07deb54 100644 --- a/crates/engine/src/cmd/constraints_tests.rs +++ b/crates/engine/src/cmd/constraints_tests.rs @@ -190,3 +190,23 @@ fn hostile_constraint_params_never_panic() { assert!(s.running.is_none()); } } + +#[test] +fn delconstraint_rejects_referenced_dimension() { + let mut s = Session::new(); + let a = line(&mut s, [0.0, 0.0], [10.0, 0.0]); + let b = line(&mut s, [0.0, 5.0], [4.0, 5.0]); + s.execute("dimconstraint", &json!({ "type": "horizontal", "h1": a, "expr": "8" })).unwrap(); + s.execute("dimconstraint", &json!({ "type": "horizontal", "h1": b, "expr": "d1/2" })).unwrap(); + let before = s.doc().unwrap().clone(); + let first = s.doc().unwrap().constraints[0].id; + let e = s.execute("delconstraint", &json!({ "ids": [first] })).unwrap_err().to_string(); + assert!(e.contains("`d1` is used by `d2`"), "{e}"); + assert_eq!(*s.doc().unwrap(), before); + assert!(s.execute("delconstraint", &json!({ "handles": [a] })).is_err()); + assert_eq!(*s.doc().unwrap(), before); + // Selecting both dimensions removes both; the dependent one may go with its source. + let ids: Vec = s.doc().unwrap().constraints.iter().map(|c| c.id).collect(); + s.execute("delconstraint", &json!({ "ids": ids })).unwrap(); + assert!(s.doc().unwrap().constraints.is_empty()); +}