@@ -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 } }
0 commit comments