11import { useEffect } from "react" ;
2- import type Konva from "konva" ;
32import { useFontCacheVersion } from "../../hooks/useFontCacheVersion" ;
43import { Ellipse , Group , Rect , Shape , Text } from "react-konva" ;
54import { lookupBoundVariable , shouldShowFallbackTint } from "../../lib/variableBinding" ;
@@ -15,7 +14,7 @@ import { useLabelStore } from "../../store/labelStore";
1514import { applyBindingToObject , buildActiveCsvRow , clockCtxFromLabel } from "../../lib/variableBinding" ;
1615import { ZPL_FONT_HEIGHT_TO_CSS_RATIO } from "../../lib/labelGeometry/textPositionTransforms" ;
1716import { getTextRenderMetrics } from "../../lib/labelGeometry/textRenderMetrics" ;
18- import { selectionHandlers , type KonvaObjectProps } from "./konvaObjectProps" ;
17+ import { selectionHandlers , CAPTURE_CHROME , type KonvaObjectProps } from "./konvaObjectProps" ;
1918import { setMeasuredBounds , clearMeasuredBounds } from "./measuredBoundsCache" ;
2019import { DEFAULT_GS_SYMBOL_META , GS_SYMBOLS } from "../../registry/symbol" ;
2120import { GS_SYMBOL_PATHS , GS_VECTOR_CODES , type GsVectorCode } from "../../registry/gsSymbolPaths" ;
@@ -148,6 +147,9 @@ function PlaceholderRect({
148147 ) ;
149148}
150149
150+ /** Opacity of the ^TB overset ghost (the clipped-off value shown for editing). */
151+ const TB_OVERSET_GHOST_OPACITY = 0.3 ;
152+
151153function TextFieldContent ( {
152154 obj,
153155 content,
@@ -156,6 +158,7 @@ function TextFieldContent({
156158 dpmm,
157159 fontVersion,
158160 placeholderColor,
161+ isSelected = false ,
159162} : {
160163 obj : TextFieldObj ;
161164 content : string ;
@@ -164,6 +167,7 @@ function TextFieldContent({
164167 dpmm : number ;
165168 fontVersion : number ;
166169 placeholderColor : string ;
170+ isSelected ?: boolean ;
167171} ) {
168172 const shift = base . offsetXPx ?? 0 ;
169173 const shiftY = base . offsetYPx ?? 0 ;
@@ -229,36 +233,6 @@ function TextFieldContent({
229233 />
230234 ) ;
231235 }
232- // Konva's getClientRect ignores a Group's clip and returns the full children
233- // bounds, which would inflate the Transformer box past the ^TB clip. Report
234- // the clip rect instead (the actual visible region) so the box matches.
235- const tbClipRef = ( node : Konva . Group | null ) => {
236- if ( ! node ) return ;
237- node . getClientRect = ( config ?: { skipTransform ?: boolean ; relativeTo ?: Konva . Container } ) => {
238- const local = {
239- x : node . clipX ( ) || 0 ,
240- y : node . clipY ( ) || 0 ,
241- width : node . clipWidth ( ) ,
242- height : node . clipHeight ( ) ,
243- } ;
244- if ( config ?. skipTransform ) return local ;
245- const tr = config ?. relativeTo
246- ? node . getAbsoluteTransform ( config . relativeTo )
247- : node . getAbsoluteTransform ( ) ;
248- const corners = [
249- { x : local . x , y : local . y } ,
250- { x : local . x + local . width , y : local . y } ,
251- { x : local . x + local . width , y : local . y + local . height } ,
252- { x : local . x , y : local . y + local . height } ,
253- ] . map ( ( p ) => tr . point ( p ) ) ;
254- const xs = corners . map ( ( c ) => c . x ) ;
255- const ys = corners . map ( ( c ) => c . y ) ;
256- const minX = Math . min ( ...xs ) ;
257- const minY = Math . min ( ...ys ) ;
258- return { x : minX , y : minY , width : Math . max ( ...xs ) - minX , height : Math . max ( ...ys ) - minY } ;
259- } ;
260- } ;
261-
262236 // ^TB: word-wrap left-aligned, no spacing/indent/justify, clipped at the
263237 // block height (Labelary truncates mid-glyph, so clip rather than drop whole
264238 // lines). At 90deg rotations the block rect stays axis-aligned.
@@ -271,27 +245,41 @@ function TextFieldContent({
271245 const clip = tbBoundsDots ( blockWidth , blockHeight ?? fontHeight , obj . props . rotation ) ;
272246 // Lines sit exactly like ^FB (first line at the block top with Konva's cap
273247 // pad); matches the Labelary preview so toggling it doesn't shift the text.
248+ const tbLineNodes = ( extra ?: Partial < BaseTextProps > ) =>
249+ tbLines . map ( ( line , i ) => {
250+ const startDots = blockLineStartDots ( i , obj . props . rotation , 0 , tbStep ) ;
251+ return (
252+ < Text
253+ key = { `${ fontVersion } -${ i } ` }
254+ x = { dotsToPx ( startDots . x , scale , dpmm ) + shift }
255+ y = { dotsToPx ( startDots . y , scale , dpmm ) + shiftY }
256+ text = { line }
257+ { ...base }
258+ { ...extra }
259+ />
260+ ) ;
261+ } ) ;
274262 return (
275- < Group
276- ref = { tbClipRef }
277- clipX = { dotsToPx ( clip . x , scale , dpmm ) + shift }
278- clipY = { dotsToPx ( clip . y , scale , dpmm ) + shiftY }
279- clipWidth = { dotsToPx ( clip . width , scale , dpmm ) }
280- clipHeight = { dotsToPx ( clip . height , scale , dpmm ) }
281- >
282- { tbLines . map ( ( line , i ) => {
283- const startDots = blockLineStartDots ( i , obj . props . rotation , 0 , tbStep ) ;
284- return (
285- < Text
286- key = { ` ${ fontVersion } - ${ i } ` }
287- x = { dotsToPx ( startDots . x , scale , dpmm ) + shift }
288- y = { dotsToPx ( startDots . y , scale , dpmm ) + shiftY }
289- text = { line }
290- { ... base }
291- / >
292- ) ;
293- } ) }
294- </ Group >
263+ < >
264+ { /* Overset ghost: the full value including the part the box clips,
265+ dimmed and editor-only, so the hidden text stays visible and the
266+ transformer (whole-text bounds) can still scale it. */ }
267+ { isSelected && (
268+ < Group name = { CAPTURE_CHROME } listening = { false } opacity = { TB_OVERSET_GHOST_OPACITY } >
269+ { tbLineNodes ( { stroke : undefined , strokeWidth : 0 } ) }
270+ </ Group >
271+ ) }
272+ { /* Printing text, clipped to the ^TB box. getClientRect ignores the
273+ clip, so the selection bounds stay the full text (scalable). */ }
274+ < Group
275+ clipX = { dotsToPx ( clip . x , scale , dpmm ) + shift }
276+ clipY = { dotsToPx ( clip . y , scale , dpmm ) + shiftY }
277+ clipWidth = { dotsToPx ( clip . width , scale , dpmm ) }
278+ clipHeight = { dotsToPx ( clip . height , scale , dpmm ) }
279+ >
280+ { tbLineNodes ( ) }
281+ </ Group >
282+ </ >
295283 ) ;
296284 }
297285 const justify = blockJustify ?? "L" ;
@@ -767,6 +755,7 @@ function KonvaObjectInner({
767755 scale = { scale }
768756 dpmm = { dpmm }
769757 fontVersion = { fontVersion }
758+ isSelected = { isSelected }
770759 />
771760 { obj . type === "text" && isSelected && obj . props . blockWidth && (
772761 < BlockWrapGuide
0 commit comments