With DIMASSOC on, every dimension QDIM added looked up the objects under its definition points by scanning the whole space, and each dimension adds an entity, so QDIM over many objects took quadratic time (20,000 lines: 28 s). QDIM now builds the SnapIndex DIMCONTINUE/DIMBASELINE use (#281) once per call; dimensions are no snap targets, so it stays valid while QDIM adds them. A call naming more than 100,000 objects, or whose objects give more than 100,000 distinct points, is refused before anything is created. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
776d99a1e0
commit
2f7a1e60d7
|
|
@ -378,6 +378,9 @@ fn run_baseline(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
/// QDIM: continuous dimensions across the endpoints of the selected objects.
|
||||
fn run_qdim(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let hs = targets(s, p)?;
|
||||
if hs.len() > MAX_CHAIN {
|
||||
return Err(bad("qdim", format!("at most {MAX_CHAIN} objects per call")));
|
||||
}
|
||||
let at = point_req("qdim", p, "at")?;
|
||||
let vertical = bool_or(p, "vertical", false);
|
||||
let d = s.doc()?;
|
||||
|
|
@ -400,12 +403,23 @@ fn run_qdim(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if pts.len() < 2 {
|
||||
return Err(bad("qdim", "select objects with at least two distinct points"));
|
||||
}
|
||||
if pts.len() > MAX_CHAIN {
|
||||
return Err(bad("qdim", format!("at most {MAX_CHAIN} distinct points per call")));
|
||||
}
|
||||
let rot = if vertical { std::f64::consts::FRAC_PI_2 } else { 0.0 };
|
||||
// One snap index for all the dimensions, as DIMCONTINUE does: looking up each dimension's
|
||||
// objects by scanning the drawing made QDIM quadratic (the added dimensions are no snap targets).
|
||||
let snaps = if assoc_enabled(s) { Some(crate::assoc::SnapIndex::new(s.doc()?, &s.space())) } else { None };
|
||||
let mut out = Vec::new();
|
||||
for w in pts.windows(2) {
|
||||
if let [a, b] = w {
|
||||
let k = dim(s, DimKind::Linear { rotation: rot }, at, *a, *b, Vec2::ZERO, Vec2::ZERO, "");
|
||||
out.push(add_dim(s, k)?.hex());
|
||||
let mut k = dim(s, DimKind::Linear { rotation: rot }, at, *a, *b, Vec2::ZERO, Vec2::ZERO, "");
|
||||
if let (Some(ix), EntityKind::Dimension(dm)) = (&snaps, &mut k) {
|
||||
dm.assoc = ix.auto_assoc(&[("p13", dm.p13.xy()), ("p14", dm.p14.xy())]);
|
||||
}
|
||||
let h = s.add_entity(k)?;
|
||||
s.last_dim = Some(h);
|
||||
out.push(h.hex());
|
||||
}
|
||||
}
|
||||
Ok(json!({ "handles": out }))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
//! QDIM over many objects: one snap index per call (no rescan of the drawing per dimension), the
|
||||
//! same associations as DIMREASSOCIATE, and a cap on the input size.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{DimAssoc, EntityKind, Handle};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn assoc_of(s: &Session, handles: &[Value]) -> Vec<Vec<DimAssoc>> {
|
||||
let d = s.doc().unwrap();
|
||||
handles
|
||||
.iter()
|
||||
.map(|h| match &d.entity(Handle::parse_hex(h.as_str().unwrap()).unwrap()).unwrap().kind {
|
||||
EntityKind::Dimension(dm) => dm.assoc.clone(),
|
||||
k => panic!("not a dimension: {k:?}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn handles(r: &Value) -> Vec<Value> {
|
||||
r["handles"].as_array().unwrap().clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qdim_attaches_like_dimreassociate_and_large_input_is_capped() {
|
||||
// Lines end to end with a point on every joint (ties with the line ends) and a circle around
|
||||
// every midpoint (its centre is a QDIM point and outranks the line midpoint).
|
||||
let mut s = Session::new();
|
||||
for i in 0..40 {
|
||||
let x = f64::from(i);
|
||||
s.execute("line", &json!({"points": [[x, 0], [x + 1.0, 0]]})).unwrap();
|
||||
s.execute("point", &json!({"at": [x, 0]})).unwrap();
|
||||
s.execute("circle", &json!({"center": [x + 0.5, 0], "radius": 0.1})).unwrap();
|
||||
}
|
||||
let objs: Vec<String> = s.doc().unwrap().model.iter().map(|e| e.handle.hex()).collect();
|
||||
let r = s.execute("qdim", &json!({"handles": objs, "at": [0, 3]})).unwrap();
|
||||
let hs = handles(&r);
|
||||
assert_eq!(hs.len(), 80, "one dimension between each pair of neighbouring points");
|
||||
let quick = assoc_of(&s, &hs);
|
||||
assert!(quick.iter().all(|a| a.len() == 2), "every point is on an object");
|
||||
// DIMREASSOCIATE scans the drawing per point; the indexed QDIM must agree with it.
|
||||
s.execute("dimreassociate", &json!({"handles": hs})).unwrap();
|
||||
assert_eq!(assoc_of(&s, &hs), quick);
|
||||
|
||||
// Many objects finish (this was quadratic in the number of dimensions).
|
||||
let mut s = Session::new();
|
||||
let pts: Vec<[f64; 2]> = (0..=20_000).map(|i| [f64::from(i), 0.0]).collect();
|
||||
let objs = handles(&s.execute("line", &json!({"points": pts})).unwrap());
|
||||
let r = s.execute("qdim", &json!({"handles": objs, "at": [0, 3]})).unwrap();
|
||||
let hs = handles(&r);
|
||||
assert_eq!(hs.len(), 20_000);
|
||||
assert!(assoc_of(&s, &hs[..10]).iter().all(|a| a.len() == 2));
|
||||
|
||||
// More than the cap (objects, or distinct points) is refused with nothing created.
|
||||
let n = s.doc().unwrap().model.iter().count();
|
||||
let too_many: Vec<Value> = (0..100_001).map(|_| objs[0].clone()).collect();
|
||||
let e = s.execute("qdim", &json!({"handles": too_many, "at": [0, 3]})).unwrap_err();
|
||||
assert!(e.to_string().contains("100000"), "{e}");
|
||||
let vertices: Vec<[f64; 2]> = (0..100_002).map(|i| [f64::from(i), f64::from(i % 2)]).collect();
|
||||
s.execute("pline", &json!({"vertices": vertices})).unwrap();
|
||||
let pl = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||||
let n = n + 1;
|
||||
let e = s.execute("qdim", &json!({"handles": [pl], "at": [0, 3]})).unwrap_err();
|
||||
assert!(e.to_string().contains("100000"), "{e}");
|
||||
assert_eq!(s.doc().unwrap().model.iter().count(), n);
|
||||
}
|
||||
Loading…
Reference in New Issue