BOUNDARY: cap point list and aggregate output per call (#723) (#795)

* BOUNDARY: cap point list and aggregate output per call (#723)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* BOUNDARY: a call over the output budget restores the drawing

The session does not roll back a command that returns an error, so the
polylines created before the cap was hit stayed in the drawing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein 2026-10-13 10:36:56 +02:00 committed by GitHub
parent 89c834155d
commit 8073453d86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 31 additions and 0 deletions

View File

@ -468,11 +468,27 @@ fn boundary_at(s: &mut Session, q: Vec2) -> Result<Vec<Handle>> {
Ok(out)
}
/// Most internal points one BOUNDARY call accepts (checked before the list is copied).
const MAX_BOUNDARY_POINTS: usize = 256;
/// Most polylines one BOUNDARY call may create. Each pick adds polylines the next pick traces
/// against, so this bounds the aggregate tracing work of the batch.
const MAX_BOUNDARY_OUTPUT: usize = 1024;
fn run_boundary(s: &mut Session, p: &Value) -> Result<Value> {
let raw = p.get("points").and_then(Value::as_array).ok_or_else(|| bad("boundary", "`points` is required"))?;
if raw.len() > MAX_BOUNDARY_POINTS {
return Err(bad("boundary", format!("at most {MAX_BOUNDARY_POINTS} points per call (got {})", raw.len())));
}
let pts = points_param(p, "points").ok_or_else(|| bad("boundary", "`points` is required"))?;
let before = s.state()?.doc.clone();
let mut out = Vec::new();
for q in pts {
out.extend(boundary_at(s, q)?.into_iter().map(|h| h.hex()));
if out.len() > MAX_BOUNDARY_OUTPUT {
// The refused call leaves no partial result behind.
s.state_mut()?.doc = before;
return Err(bad("boundary", format!("more than {MAX_BOUNDARY_OUTPUT} boundary polylines in one call; split the work")));
}
}
Ok(json!({ "handles": out, "message": format!("BOUNDARY created {} polyline(s)", out.len()) }))
}

View File

@ -861,6 +861,21 @@ fn gradient_pick_point_keeps_island_of_separate_lines() {
}
}
#[test]
fn boundary_point_batch_is_capped() {
let mut s = Session::new();
let sq = [[0, 0], [20, 0], [20, 20], [0, 20]];
for i in 0..4 {
s.execute("line", &json!({"points": [sq[i], sq[(i + 1) % 4]]})).unwrap();
}
let many: Vec<_> = (0..100_000).map(|_| json!([2, 2])).collect();
let t = std::time::Instant::now();
assert!(s.execute("boundary", &json!({"points": many})).is_err());
assert!(t.elapsed().as_secs() < 5);
let ok: Vec<_> = (0..8).map(|_| json!([2, 2])).collect();
assert!(s.execute("boundary", &json!({"points": ok})).is_ok());
}
#[test]
fn boundary_pick_point_keeps_island_of_separate_lines() {
for lines in [true, false] {