Skip to content

Commit 25f3eb7

Browse files
committed
Fix some size issue with the callout
1 parent 7ce5e9a commit 25f3eb7

4 files changed

Lines changed: 72 additions & 26 deletions

File tree

3.7 KB
Binary file not shown.

packages/plugin-annotation/src/lib/patching/patch-utils.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -253,20 +253,21 @@ export function computeCalloutConnectionPoint(knee: Position, textBox: Rect): Po
253253
/**
254254
* Compute the overall bounding rect for a callout FreeText, encompassing
255255
* the text box, callout line, and line ending geometry.
256+
*
257+
* The text box border grows inward (stroke outer edge = textBox boundary),
258+
* so no outward padding is added for the text box. The callout line and
259+
* arrow stroke extend outward and miter joins at the knee can protrude
260+
* further, so they get strokeWidth padding (half for stroke + half for
261+
* miter/join clearance).
256262
*/
257263
export function computeCalloutOverallRect(
258264
textBox: Rect,
259265
calloutLine: Position[],
260266
lineEnding: PdfAnnotationLineEnding | undefined,
261267
strokeWidth: number,
262268
): Rect {
263-
const textBoxCorners: Position[] = [
264-
{ x: textBox.origin.x, y: textBox.origin.y },
265-
{ x: textBox.origin.x + textBox.size.width, y: textBox.origin.y + textBox.size.height },
266-
];
267-
const allPoints = [...textBoxCorners, ...calloutLine];
269+
const linePoints = [...calloutLine];
268270

269-
// Include line ending geometry at the arrow tip (first point of callout line)
270271
if (lineEnding && calloutLine.length >= 2) {
271272
const handler = LINE_ENDING_HANDLERS[lineEnding];
272273
if (handler) {
@@ -279,11 +280,24 @@ export function computeCalloutOverallRect(
279280
const transformed = localPts.map((p) =>
280281
rotateAndTranslatePoint(p, rotationAngle, calloutLine[0]),
281282
);
282-
allPoints.push(...transformed);
283+
linePoints.push(...transformed);
283284
}
284285
}
285286

286-
const baseRect = rectFromPoints(allPoints);
287-
const pad = strokeWidth / 2 + EXTRA_PADDING * strokeWidth;
288-
return expandRect(baseRect, pad);
287+
const lineBbox = expandRect(rectFromPoints(linePoints), strokeWidth);
288+
289+
const tbRight = textBox.origin.x + textBox.size.width;
290+
const tbBottom = textBox.origin.y + textBox.size.height;
291+
const lnRight = lineBbox.origin.x + lineBbox.size.width;
292+
const lnBottom = lineBbox.origin.y + lineBbox.size.height;
293+
294+
const minX = Math.min(textBox.origin.x, lineBbox.origin.x);
295+
const minY = Math.min(textBox.origin.y, lineBbox.origin.y);
296+
const maxX = Math.max(tbRight, lnRight);
297+
const maxY = Math.max(tbBottom, lnBottom);
298+
299+
return {
300+
origin: { x: minX, y: minY },
301+
size: { width: maxX - minX, height: maxY - minY },
302+
};
289303
}

packages/plugin-annotation/src/shared/components/annotations/callout-free-text.tsx

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ export function CalloutFreeText({
5555
// Text box position relative to the annotation rect (for CSS positioning)
5656
const textBoxRelative = useMemo(
5757
() => ({
58-
left: (textBox.origin.x - rect.origin.x) * scale,
59-
top: (textBox.origin.y - rect.origin.y) * scale,
60-
width: textBox.size.width * scale,
61-
height: textBox.size.height * scale,
58+
left: (textBox.origin.x - rect.origin.x + strokeWidth / 2) * scale,
59+
top: (textBox.origin.y - rect.origin.y + strokeWidth / 2) * scale,
60+
width: (textBox.size.width - strokeWidth) * scale,
61+
height: (textBox.size.height - strokeWidth) * scale,
6262
}),
63-
[textBox, rect, scale],
63+
[textBox, rect, scale, strokeWidth],
6464
);
6565

6666
// Callout line segments in SVG viewBox coords (relative to rect origin)
@@ -85,6 +85,22 @@ export function CalloutFreeText({
8585
);
8686
}, [lineCoords, obj.lineEnding, strokeWidth]);
8787

88+
const visualLineCoords = useMemo(() => {
89+
if (!lineCoords || lineCoords.length < 2) return lineCoords;
90+
const pts = lineCoords.map((p) => ({ ...p }));
91+
const last = pts.length - 1;
92+
const prev = last - 1;
93+
const dx = pts[last].x - pts[prev].x;
94+
const dy = pts[last].y - pts[prev].y;
95+
const len = Math.sqrt(dx * dx + dy * dy);
96+
if (len > 0) {
97+
const halfBw = strokeWidth / 2;
98+
pts[last].x += (dx / len) * halfBw;
99+
pts[last].y += (dy / len) * halfBw;
100+
}
101+
return pts;
102+
}, [lineCoords, strokeWidth]);
103+
88104
const { adjustedFontPx, wrapperStyle } = useIOSZoomPrevention(obj.fontSize * scale, isEditing);
89105

90106
useEffect(() => {
@@ -189,10 +205,10 @@ export function CalloutFreeText({
189205
{/* Visual callout line + text box rect */}
190206
{!appearanceActive && (
191207
<>
192-
{lineCoords && (
208+
{visualLineCoords && (
193209
<>
194210
<polyline
195-
points={lineCoords.map((p) => `${p.x},${p.y}`).join(' ')}
211+
points={visualLineCoords.map((p) => `${p.x},${p.y}`).join(' ')}
196212
fill="none"
197213
stroke={strokeColor}
198214
strokeWidth={strokeWidth}
@@ -213,10 +229,10 @@ export function CalloutFreeText({
213229
</>
214230
)}
215231
<rect
216-
x={textBox.origin.x - rect.origin.x}
217-
y={textBox.origin.y - rect.origin.y}
218-
width={textBox.size.width}
219-
height={textBox.size.height}
232+
x={textBox.origin.x - rect.origin.x + strokeWidth / 2}
233+
y={textBox.origin.y - rect.origin.y + strokeWidth / 2}
234+
width={textBox.size.width - strokeWidth}
235+
height={textBox.size.height - strokeWidth}
220236
fill={obj.color ?? obj.backgroundColor ?? 'transparent'}
221237
stroke={strokeColor}
222238
strokeWidth={strokeWidth}
@@ -227,15 +243,15 @@ export function CalloutFreeText({
227243
)}
228244
</svg>
229245

230-
{/* Text box hit area */}
246+
{/* Text box hit area (covers full textBox including border) */}
231247
<div
232248
onPointerDown={onClick ? (e) => onClick(e) : undefined}
233249
style={{
234250
position: 'absolute',
235-
left: textBoxRelative.left,
236-
top: textBoxRelative.top,
237-
width: textBoxRelative.width,
238-
height: textBoxRelative.height,
251+
left: (textBox.origin.x - rect.origin.x) * scale,
252+
top: (textBox.origin.y - rect.origin.y) * scale,
253+
width: textBox.size.width * scale,
254+
height: textBox.size.height * scale,
239255
cursor: isSelected && !isEditing ? 'move' : onClick ? 'pointer' : 'default',
240256
pointerEvents: !onClick ? 'none' : isSelected && !isEditing ? 'none' : 'auto',
241257
}}

viewers/snippet/src/components/annotation-sidebar/property-schema.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export const PROPERTY_CONFIGS: Record<string, PropertyConfig> = {
5656
type: 'colorWithTransparent',
5757
labelKey: 'annotation.strokeColor',
5858
},
59+
strokeColorWithoutTransparent: {
60+
key: 'strokeColor',
61+
type: 'color',
62+
labelKey: 'annotation.strokeColor',
63+
},
5964

6065
// Common properties
6166
opacity: {
@@ -210,6 +215,17 @@ export const TOOL_PROPERTIES: Record<string, string[]> = {
210215
'color',
211216
'rotation',
212217
],
218+
freeTextCallout: [
219+
'fontFamily',
220+
'fontSize',
221+
'fontColor',
222+
'textAlign',
223+
'verticalAlign',
224+
'opacity',
225+
'color',
226+
'strokeColorWithoutTransparent',
227+
'strokeWidth',
228+
],
213229

214230
// Stamp
215231
stamp: ['rotation'],

0 commit comments

Comments
 (0)