From 24ac313428846fe9cecf224623fa1115d7877120 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Sat, 31 Oct 2026 08:02:40 +0800 Subject: [PATCH] M5: a callout given its arrow point alone keeps its knee (#1152) comment_edit with one point on a callout whose /CL has six numbers built the new line from that point and the end only, so the knee was dropped without a word. An agent moving just the arrow lost the knee. place_text_box now keeps the old knee when only the arrow point is given; two points still set both, and a four-number /CL still gains a knee only when one is sent. The points schema says so. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/annot/src/modify.rs | 11 ++++++++++- crates/annot/src/tests.rs | 14 ++++++++++++++ crates/automation/src/tools.rs | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/annot/src/modify.rs b/crates/annot/src/modify.rs index 43b28c146f..07a34100fe 100644 --- a/crates/annot/src/modify.rs +++ b/crates/annot/src/modify.rs @@ -413,7 +413,7 @@ pub fn set_points(doc: &mut Document, page: usize, index: usize, points: &[[f64; } if d.name(b"Subtype") == Some(b"FreeText") && callout_line(doc, &d).is_some() { // A callout's points: its arrow point, then optionally its knee (a four-number `/CL` - // gains one; ISO 32000-2 Table 177 allows both forms). + // gains one; ISO 32000-2 Table 177 allows both forms). The arrow point alone keeps a knee. if !matches!(points.len(), 1 | 2) || !finite(&points.concat()) { return Err(AnnotError::Invalid("a callout has an arrow point and, optionally, a knee".into())); } @@ -545,6 +545,15 @@ fn place_text_box(doc: &mut Document, r: ObjRef, rect: [f64; 4], lead: Option<&[ let old_line = callout_line(doc, &d); let old_box = callout_text_box(doc, &d); let old_end = old_line.as_deref().and_then(<[[f64; 2]]>::last).copied(); + // An arrow point alone moves the arrow: a knee the line has stays where it is. + let kept: [[f64; 2]; 2]; + let lead = match (lead, old_line.as_deref()) { + (Some([point]), Some([_, knee, _])) => { + kept = [*point, *knee]; + Some(&kept[..]) + } + _ => lead, + }; let line: Option> = match (lead, old_line.as_deref()) { (Some(lead @ [.., toward]), _) => { let side = callout_side(rect, *toward); diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index 47b2c866da..47c956640f 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -2446,3 +2446,17 @@ fn summaries_give_no_callout_points_without_a_text_box() { assert_eq!((s.points.len(), s.text_box), (0, None), "/RD {rd:?}"); } } + +/// Review on #4094: a callout given its arrow point alone keeps its knee. The new line was built +/// from the one point and the end, so a six-number `/CL` silently became four. +#[test] +fn a_callouts_arrow_point_alone_keeps_its_knee() { + let (mut doc, c) = callout_fixture(); + let before = nums(&list(&doc, 0)[c], b"CL"); + assert_eq!(before.len(), 6, "{before:?}"); + set_points(&mut doc, 0, c, &[[190.0, 500.0]], &meta("p")).unwrap(); + let after = nums(&list(&doc, 0)[c], b"CL"); + assert_eq!(after.len(), 6, "the knee stays: {after:?}"); + assert_eq!(after[..2], [190.0, 500.0], "the arrow point moved"); + assert_eq!(after[2..], before[2..], "the knee and the end are where they were"); +} diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index fb66442f2e..4ca1ad31ec 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -916,7 +916,7 @@ pub fn tools() -> Vec { "font_size": { "type": "number", "minimum": 1, "maximum": 400, "description": "Font size in points of a text box, typewriter text or callout; the box grows to hold the text." }, "rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4 }, "move": point(), - "points": { "type": "array", "items": point(), "minItems": 1, "description": "A line's two ends, every corner of connected lines, a polygon or a cloud, or a callout's arrow point and (optionally) knee, in page coordinates." }, + "points": { "type": "array", "items": point(), "minItems": 1, "description": "A line's two ends, every corner of connected lines, a polygon or a cloud, or a callout's arrow point and (optionally) knee, in page coordinates. A callout given its arrow point alone keeps the knee it has." }, })), &["doc"], )),