11import {
2+ forwardRef ,
3+ useImperativeHandle ,
4+ useMemo ,
25 useRef ,
36 useEffect ,
47 useState ,
@@ -14,6 +17,8 @@ import { SNAP_OPTIONS } from "../../lib/units";
1417import type { Unit } from "../../lib/units" ;
1518import { computeSnap } from "../../lib/snapGuides" ;
1619import type { SnapGuide } from "../../lib/snapGuides" ;
20+ import { computeGroupCenterDelta } from "../../lib/alignment" ;
21+ import type { AlignAxis } from "../../lib/alignment" ;
1722import { KonvaObject } from "./KonvaObject" ;
1823import { Grid } from "./Grid" ;
1924import { GuideLines } from "./GuideLines" ;
@@ -50,7 +55,14 @@ interface Props {
5055 onViewRotationChange : ( rotation : ViewRotation ) => void ;
5156}
5257
53- export function LabelCanvas ( {
58+ /** Imperative actions sibling components (PropertiesPanel) need from the
59+ * canvas. The canvas owns the live render bboxes via Konva — co-locating
60+ * the action with the data avoids round-tripping through the store. */
61+ export interface LabelCanvasHandle {
62+ alignSelectionToLabel : ( axis : AlignAxis ) => void ;
63+ }
64+
65+ export const LabelCanvas = forwardRef < LabelCanvasHandle , Props > ( function LabelCanvas ( {
5466 unit,
5567 showGrid,
5668 onGridToggle,
@@ -62,7 +74,7 @@ export function LabelCanvas({
6274 onZoomChange,
6375 viewRotation,
6476 onViewRotationChange,
65- } : Props ) {
77+ } , ref ) {
6678 const containerRef = useRef < HTMLDivElement > ( null ) ;
6779 const stageRef = useRef < Konva . Stage > ( null ) ;
6880 const transformerRef = useRef < Konva . Transformer > ( null ) ;
@@ -246,13 +258,73 @@ export function LabelCanvas({
246258 // here matches the visual (rotation-aware) bounds — snap math operates in
247259 // stage-screen space, so it must reflect what the user sees, not the
248260 // un-rotated layout coordinates.
249- const transformerSnapLabelRect = {
250- id : "_lbl" ,
251- x : visualLabelX ,
252- y : visualLabelY ,
253- width : visualLabelWidthPx ,
254- height : visualLabelHeightPx ,
255- } ;
261+ const transformerSnapLabelRect = useMemo (
262+ ( ) => ( {
263+ id : "_lbl" ,
264+ x : visualLabelX ,
265+ y : visualLabelY ,
266+ width : visualLabelWidthPx ,
267+ height : visualLabelHeightPx ,
268+ } ) ,
269+ [ visualLabelX , visualLabelY , visualLabelWidthPx , visualLabelHeightPx ] ,
270+ ) ;
271+
272+ // Imperative align-to-label: PropertiesPanel calls this directly via the
273+ // forwarded ref. Co-located with the render data — measure each selected
274+ // node's rendered bbox via Konva clientRect (single source of truth for
275+ // type-specific footprints like text baselines / barcode text-zones),
276+ // compute the centre-delta in screen px, map back through view rotation
277+ // and px-per-dot to model coordinates.
278+ useImperativeHandle (
279+ ref ,
280+ ( ) => ( {
281+ alignSelectionToLabel : ( axis ) => {
282+ const stage = stageRef . current ;
283+ if ( ! stage ) return ;
284+ const state = useLabelStore . getState ( ) ;
285+ const ids = state . selectedIds ;
286+ if ( ids . length === 0 ) return ;
287+ const objs = currentObjects ( state ) ;
288+
289+ const boxes = ids . flatMap ( ( id ) => {
290+ const node = stage . findOne < Konva . Node > ( `#${ id } ` ) ;
291+ if ( ! node ) return [ ] ;
292+ const r = node . getClientRect ( { relativeTo : stage } ) ;
293+ return [ { id, x : r . x , y : r . y , width : r . width , height : r . height } ] ;
294+ } ) ;
295+ if ( boxes . length === 0 ) return ;
296+
297+ const { dx : screenDx , dy : screenDy } = computeGroupCenterDelta (
298+ boxes ,
299+ transformerSnapLabelRect ,
300+ axis ,
301+ ) ;
302+ if ( screenDx === 0 && screenDy === 0 ) return ;
303+
304+ const [ layoutDx , layoutDy ] = inverseRotateDelta (
305+ screenDx ,
306+ screenDy ,
307+ viewRotation ,
308+ ) ;
309+ // Round to integer dots — matches the `mmToDots` convention used by
310+ // PropertiesPanel inputs and keeps the store's x/y invariant so ZPL
311+ // emit doesn't see fractional coordinates.
312+ const pxPerDot = scale / label . dpmm ;
313+ const dxDots = Math . round ( layoutDx / pxPerDot ) ;
314+ const dyDots = Math . round ( layoutDy / pxPerDot ) ;
315+
316+ const updates = ids . flatMap ( ( id ) => {
317+ const obj = objs . find ( ( o ) => o . id === id ) ;
318+ if ( ! obj ) return [ ] ;
319+ return [
320+ { id, changes : { x : obj . x + dxDots , y : obj . y + dyDots } } ,
321+ ] ;
322+ } ) ;
323+ if ( updates . length > 0 ) updateObjects ( updates ) ;
324+ } ,
325+ } ) ,
326+ [ transformerSnapLabelRect , scale , label . dpmm , viewRotation , updateObjects ] ,
327+ ) ;
256328
257329 const {
258330 rotateEnabled,
@@ -673,4 +745,4 @@ export function LabelCanvas({
673745 ) }
674746 </ div >
675747 ) ;
676- }
748+ } ) ;
0 commit comments