@@ -61,13 +61,19 @@ const { left: slideLeft, top: slideTop } = useSlideBounds()
6161const ctrlSize = 10
6262const minRemain = 10
6363
64- // Aggregate snap-line guides from all selected elements. The v-drag directive's body-drag
65- // handler calls `state.applySnap(...)` on the clicked element during multi-select drags,
66- // which writes into that element's `activeSnapLines` — but its `DragControl` isn't mounted
67- // while multi-selected, so without this aggregation the red snap guides wouldn't render.
64+ // Snap-line guides during corner resize. The body-drag handler in `v-drag.ts` writes
65+ // snap lines onto the clicked element's `activeSnapLines`; corner resize lives here in
66+ // GroupDragControl and writes onto this local ref. The merged `activeSnapLines` computed
67+ // (below) renders both.
68+ const groupResizeSnapLines = ref <{ x: number [], y: number [] }>({ x: [], y: [] })
69+
70+ // Aggregate snap-line guides from the local resize ref plus each selected element's own
71+ // lines (set by the v-drag directive during a multi-select body drag — that element's
72+ // `DragControl` isn't mounted while multi-selected, so without this aggregation the
73+ // red snap guides wouldn't render).
6874const activeSnapLines = computed (() => {
69- const x = new Set <number >()
70- const y = new Set <number >()
75+ const x = new Set <number >(groupResizeSnapLines . value . x )
76+ const y = new Set <number >(groupResizeSnapLines . value . y )
7177 for (const el of selectedElements .value ) {
7278 for (const lx of el .activeSnapLines .value .x ) x .add (lx )
7379 for (const ly of el .activeSnapLines .value .y ) y .add (ly )
@@ -154,9 +160,55 @@ function onPointerdown(ev: PointerEvent) {
154160 ;(ev .currentTarget as HTMLElement ).setPointerCapture (ev .pointerId )
155161}
156162
157- // Reserved for future snap alignment support
158- void findSnap
159- void getSnapTargets
163+ // Snap the dragged corner `(cornerX, cornerY)` of the group's bounding box to alignment
164+ // targets (slide edges/center + other elements' edges/centers). For free resize, snap each
165+ // axis independently. With `lockAR`, snap along the diagonal from the anchor through the
166+ // corner so the AR constraint isn't broken. Returns the snapped corner plus the guide
167+ // lines to render.
168+ function applyGroupCornerSnap(
169+ cornerX : number ,
170+ cornerY : number ,
171+ anchorX : number ,
172+ anchorY : number ,
173+ lockAR : boolean ,
174+ excludeIds : Set <string >,
175+ pageNum : number ,
176+ ): { x: number , y: number , linesX: number [], linesY: number [] } {
177+ const targets = getSnapTargets (pageNum , excludeIds )
178+ if (! lockAR ) {
179+ const sX = findSnap (cornerX , 0 , targets .x )
180+ const sY = findSnap (cornerY , 0 , targets .y )
181+ return { x: sX .value , y: sY .value , linesX: sX .lines , linesY: sY .lines }
182+ }
183+ // AR-locked: snap projects guide-aligned positions onto the AR-preserving diagonal.
184+ const dx = cornerX - anchorX
185+ const dy = cornerY - anchorY
186+ if (Math .abs (dx ) < 0.001 && Math .abs (dy ) < 0.001 )
187+ return { x: cornerX , y: cornerY , linesX: [], linesY: [] }
188+ const sX = findSnap (cornerX , 0 , targets .x )
189+ let xDist = Infinity
190+ let xSnX = cornerX
191+ let xSnY = cornerY
192+ if (sX .lines .length > 0 && Math .abs (dx ) > 0.001 ) {
193+ xSnX = sX .value
194+ xSnY = anchorY + (xSnX - anchorX ) / dx * dy
195+ xDist = Math .abs (sX .value - cornerX )
196+ }
197+ const sY = findSnap (cornerY , 0 , targets .y )
198+ let yDist = Infinity
199+ let ySnX = cornerX
200+ let ySnY = cornerY
201+ if (sY .lines .length > 0 && Math .abs (dy ) > 0.001 ) {
202+ ySnY = sY .value
203+ ySnX = anchorX + (ySnY - anchorY ) / dy * dx
204+ yDist = Math .abs (sY .value - cornerY )
205+ }
206+ if (xDist < yDist && xDist < Infinity )
207+ return { x: xSnX , y: xSnY , linesX: [xSnX ], linesY: [] }
208+ if (yDist < Infinity )
209+ return { x: ySnX , y: ySnY , linesX: [], linesY: [ySnY ] }
210+ return { x: cornerX , y: cornerY , linesX: [], linesY: [] }
211+ }
160212
161213function onPointermoveResize(ev : PointerEvent , isLeft : boolean , isTop : boolean ) {
162214 if (! currentDrag || ev .buttons !== 1 || ! bounds .value )
@@ -189,7 +241,7 @@ function onPointermoveResize(ev: PointerEvent, isLeft: boolean, isTop: boolean)
189241 newWidth = Math .max (newWidth , minSize )
190242 newHeight = Math .max (newHeight , minSize )
191243
192- // With Shift: preserve aspect ratio
244+ // With Shift: preserve aspect ratio (per the deck-wide modifier convention).
193245 if (ev .shiftKey ) {
194246 const origRatio = origWidth / origHeight
195247 const newRatio = newWidth / newHeight
@@ -201,6 +253,24 @@ function onPointermoveResize(ev: PointerEvent, isLeft: boolean, isTop: boolean)
201253 }
202254 }
203255
256+ // Snap the dragged corner unless ⌘ is held (per the modifier convention). For AR-locked
257+ // (Shift) resize, snap along the AR-preserving diagonal so we don't break the constraint.
258+ if (! ev .metaKey ) {
259+ const slideNo = selectedElements .value [0 ]?.page .value
260+ if (slideNo != null ) {
261+ const cornerX = isLeft ? (anchorX - newWidth ) : (anchorX + newWidth )
262+ const cornerY = isTop ? (anchorY - newHeight ) : (anchorY + newHeight )
263+ const excludeIds = new Set (selectedElements .value .map (s => s .dragId ))
264+ const snapped = applyGroupCornerSnap (cornerX , cornerY , anchorX , anchorY , ev .shiftKey , excludeIds , slideNo )
265+ groupResizeSnapLines .value = { x: snapped .linesX , y: snapped .linesY }
266+ newWidth = Math .max (Math .abs (snapped .x - anchorX ), minSize )
267+ newHeight = Math .max (Math .abs (snapped .y - anchorY ), minSize )
268+ }
269+ }
270+ else {
271+ groupResizeSnapLines .value = { x: [], y: [] }
272+ }
273+
204274 // Calculate scale factors
205275 const scaleX = newWidth / origWidth
206276 const scaleY = newHeight / origHeight
@@ -244,9 +314,11 @@ function onPointerup(ev: PointerEvent) {
244314 selectedElements .value ,
245315 )
246316 // Clear any snap-line guides that the body-drag handler left on individual elements
247- // (the v-drag directive's pointerup only clears the primary element's lines).
317+ // (the v-drag directive's pointerup only clears the primary element's lines), and the
318+ // corner-resize ones we wrote locally.
248319 for (const el of selectedElements .value )
249320 el .clearSnapLines ()
321+ groupResizeSnapLines .value = { x: [], y: [] }
250322 currentDrag = null
251323}
252324
0 commit comments