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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-10-31 08:02:40 +08:00
1 parent a36dcbb3bc
commit 24ac313428
3 files changed
+25 -2

No files matched your search

+10 -1
View File
@@ -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<Vec<[f64; 2]>> = match (lead, old_line.as_deref()) {
(Some(lead @ [.., toward]), _) => {
let side = callout_side(rect, *toward);
+14
View File
@@ -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");
}
+1 -1
View File
@@ -916,7 +916,7 @@ pub fn tools() -> Vec<ToolDef> {
"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"],
)),