11import { Box } from "@chakra-ui/react"
2- import { type FC , useCallback , useEffect , useMemo , useRef , useState } from "react"
2+ import {
3+ type FC ,
4+ useCallback ,
5+ useEffect ,
6+ useMemo ,
7+ useRef ,
8+ useState ,
9+ } from "react"
310import Moveable from "react-moveable"
411import type { Transformation } from "../../store"
512import { useEditorStore } from "../../store"
@@ -41,8 +48,11 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
4148 onClick,
4249} ) => {
4350 const targetRef = useRef < HTMLImageElement > ( null )
44- const { updateTransformation, _internalState, _acknowledgeExpressionOverwrite } =
45- useEditorStore ( )
51+ const {
52+ updateTransformation,
53+ _internalState,
54+ _acknowledgeExpressionOverwrite,
55+ } = useEditorStore ( )
4656 const [ showExpressionDialog , setShowExpressionDialog ] = useState ( false )
4757 const [ dragBlocked , setDragBlocked ] = useState ( false )
4858 const pendingActionRef = useRef < ( ( ) => void ) | null > ( null )
@@ -51,7 +61,10 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
5161 // Track the actual rendered dimensions of the single-layer image so we can
5262 // apply the correct centering offset when the layer config has no explicit
5363 // width/height (e.g. auto-sized text layers).
54- const [ naturalDims , setNaturalDims ] = useState < { w : number ; h : number } | null > ( null )
64+ const [ naturalDims , setNaturalDims ] = useState < {
65+ w : number
66+ h : number
67+ } | null > ( null )
5568
5669 // Reset when the layer URL changes (layer content was re-rendered).
5770 useEffect ( ( ) => {
@@ -74,8 +87,9 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
7487 const value = layer . value as Record < string , unknown >
7588 const posConfig = extractLayerPositionConfig ( value )
7689 const hasExpressions = hasExpressionCoords ( posConfig )
77- const isAcknowledged =
78- _internalState . acknowledgedExpressionOverwrites . has ( layer . id )
90+ const isAcknowledged = _internalState . acknowledgedExpressionOverwrites . has (
91+ layer . id ,
92+ )
7993
8094 const checkExpressionGuard = useCallback (
8195 ( action : ( ) => void ) : boolean => {
@@ -196,7 +210,11 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
196210 if ( posConfig . layerHeight == null ) posConfig . layerHeight = naturalDims . h
197211 }
198212
199- const rect = resolveLayerRect ( posConfig , coordSpace . canvasW , coordSpace . canvasH )
213+ const rect = resolveLayerRect (
214+ posConfig ,
215+ coordSpace . canvasW ,
216+ coordSpace . canvasH ,
217+ )
200218
201219 const left = rect . x * coordSpace . scale
202220 const top = rect . y * coordSpace . scale
@@ -220,6 +238,7 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
220238 ref = { targetRef }
221239 src = { layerUrl }
222240 alt = ""
241+ data-layer-id = { layer . id }
223242 style = { {
224243 display : "block" ,
225244 maxWidth : "none" ,
@@ -254,70 +273,140 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
254273 </ Box >
255274
256275 { /* Distance-to-edge overlay shown while dragging */ }
257- { isSelected && dragOffset && ( ( ) => {
258- const canvasDisplayW = coordSpace . canvasW * coordSpace . scale
259- const canvasDisplayH = coordSpace . canvasH * coordSpace . scale
260- const lw = displayRect . width ?? targetRef . current ?. offsetWidth ?? 0
261- const lh = displayRect . height ?? targetRef . current ?. offsetHeight ?? 0
262- const lx = displayRect . left + dragOffset [ 0 ]
263- const ly = displayRect . top + dragOffset [ 1 ]
264- const lcx = lx + lw / 2
265- const lcy = ly + lh / 2
266-
267- const distTop = Math . round ( ly / coordSpace . scale )
268- const distBottom = Math . round ( ( canvasDisplayH - ly - lh ) / coordSpace . scale )
269- const distLeft = Math . round ( lx / coordSpace . scale )
270- const distRight = Math . round ( ( canvasDisplayW - lx - lw ) / coordSpace . scale )
271-
272- const lineStyle = {
273- position : 'absolute' as const ,
274- background : '#E53E3E' ,
275- zIndex : 10000 ,
276- pointerEvents : 'none' as const ,
277- }
278- const labelStyle = {
279- position : 'absolute' as const ,
280- background : '#E53E3E' ,
281- color : '#fff' ,
282- fontSize : '10px' ,
283- lineHeight : '14px' ,
284- padding : '0 4px' ,
285- borderRadius : '2px' ,
286- whiteSpace : 'nowrap' as const ,
287- zIndex : 10001 ,
288- pointerEvents : 'none' as const ,
289- transform : 'translate(-50%, -50%)' ,
290- }
291-
292- return (
293- < >
294- { ly > 1 && (
295- < >
296- < div style = { { ...lineStyle , left : `${ lcx } px` , top : '0px' , width : '1px' , height : `${ ly } px` } } />
297- < div style = { { ...labelStyle , left : `${ lcx } px` , top : `${ ly / 2 } px` } } > { distTop } px</ div >
298- </ >
299- ) }
300- { canvasDisplayH - ly - lh > 1 && (
301- < >
302- < div style = { { ...lineStyle , left : `${ lcx } px` , top : `${ ly + lh } px` , width : '1px' , height : `${ canvasDisplayH - ly - lh } px` } } />
303- < div style = { { ...labelStyle , left : `${ lcx } px` , top : `${ ly + lh + ( canvasDisplayH - ly - lh ) / 2 } px` } } > { distBottom } px</ div >
304- </ >
305- ) }
306- { lx > 1 && (
307- < >
308- < div style = { { ...lineStyle , left : '0px' , top : `${ lcy } px` , width : `${ lx } px` , height : '1px' } } />
309- < div style = { { ...labelStyle , left : `${ lx / 2 } px` , top : `${ lcy } px` } } > { distLeft } px</ div >
310- </ >
311- ) }
312- { canvasDisplayW - lx - lw > 1 && (
313- < >
314- < div style = { { ...lineStyle , left : `${ lx + lw } px` , top : `${ lcy } px` , width : `${ canvasDisplayW - lx - lw } px` , height : '1px' } } />
315- < div style = { { ...labelStyle , left : `${ lx + lw + ( canvasDisplayW - lx - lw ) / 2 } px` , top : `${ lcy } px` } } > { distRight } px</ div >
316- </ >
317- ) }
318- </ >
319- )
320- } ) ( ) }
276+ { isSelected &&
277+ dragOffset &&
278+ ( ( ) => {
279+ const canvasDisplayW = coordSpace . canvasW * coordSpace . scale
280+ const canvasDisplayH = coordSpace . canvasH * coordSpace . scale
281+ const lw = displayRect . width ?? targetRef . current ?. offsetWidth ?? 0
282+ const lh = displayRect . height ?? targetRef . current ?. offsetHeight ?? 0
283+ const lx = displayRect . left + dragOffset [ 0 ]
284+ const ly = displayRect . top + dragOffset [ 1 ]
285+ const lcx = lx + lw / 2
286+ const lcy = ly + lh / 2
287+
288+ const distTop = Math . round ( ly / coordSpace . scale )
289+ const distBottom = Math . round (
290+ ( canvasDisplayH - ly - lh ) / coordSpace . scale ,
291+ )
292+ const distLeft = Math . round ( lx / coordSpace . scale )
293+ const distRight = Math . round (
294+ ( canvasDisplayW - lx - lw ) / coordSpace . scale ,
295+ )
296+
297+ const lineStyle = {
298+ position : "absolute" as const ,
299+ background : "#E53E3E" ,
300+ zIndex : 10000 ,
301+ pointerEvents : "none" as const ,
302+ }
303+ const labelStyle = {
304+ position : "absolute" as const ,
305+ background : "#E53E3E" ,
306+ color : "#fff" ,
307+ fontSize : "10px" ,
308+ lineHeight : "14px" ,
309+ padding : "0 4px" ,
310+ borderRadius : "2px" ,
311+ whiteSpace : "nowrap" as const ,
312+ zIndex : 10001 ,
313+ pointerEvents : "none" as const ,
314+ transform : "translate(-50%, -50%)" ,
315+ }
316+
317+ return (
318+ < >
319+ { ly > 1 && (
320+ < >
321+ < div
322+ style = { {
323+ ...lineStyle ,
324+ left : `${ lcx } px` ,
325+ top : "0px" ,
326+ width : "1px" ,
327+ height : `${ ly } px` ,
328+ } }
329+ />
330+ < div
331+ style = { {
332+ ...labelStyle ,
333+ left : `${ lcx } px` ,
334+ top : `${ ly / 2 } px` ,
335+ } }
336+ >
337+ { distTop } px
338+ </ div >
339+ </ >
340+ ) }
341+ { canvasDisplayH - ly - lh > 1 && (
342+ < >
343+ < div
344+ style = { {
345+ ...lineStyle ,
346+ left : `${ lcx } px` ,
347+ top : `${ ly + lh } px` ,
348+ width : "1px" ,
349+ height : `${ canvasDisplayH - ly - lh } px` ,
350+ } }
351+ />
352+ < div
353+ style = { {
354+ ...labelStyle ,
355+ left : `${ lcx } px` ,
356+ top : `${ ly + lh + ( canvasDisplayH - ly - lh ) / 2 } px` ,
357+ } }
358+ >
359+ { distBottom } px
360+ </ div >
361+ </ >
362+ ) }
363+ { lx > 1 && (
364+ < >
365+ < div
366+ style = { {
367+ ...lineStyle ,
368+ left : "0px" ,
369+ top : `${ lcy } px` ,
370+ width : `${ lx } px` ,
371+ height : "1px" ,
372+ } }
373+ />
374+ < div
375+ style = { {
376+ ...labelStyle ,
377+ left : `${ lx / 2 } px` ,
378+ top : `${ lcy } px` ,
379+ } }
380+ >
381+ { distLeft } px
382+ </ div >
383+ </ >
384+ ) }
385+ { canvasDisplayW - lx - lw > 1 && (
386+ < >
387+ < div
388+ style = { {
389+ ...lineStyle ,
390+ left : `${ lx + lw } px` ,
391+ top : `${ lcy } px` ,
392+ width : `${ canvasDisplayW - lx - lw } px` ,
393+ height : "1px" ,
394+ } }
395+ />
396+ < div
397+ style = { {
398+ ...labelStyle ,
399+ left : `${ lx + lw + ( canvasDisplayW - lx - lw ) / 2 } px` ,
400+ top : `${ lcy } px` ,
401+ } }
402+ >
403+ { distRight } px
404+ </ div >
405+ </ >
406+ ) }
407+ </ >
408+ )
409+ } ) ( ) }
321410
322411 { isSelected && targetRef . current && (
323412 < Moveable
@@ -327,28 +416,30 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
327416 resizable = { isResizable && ! dragBlocked }
328417 keepRatio
329418 snappable
330- // snapGridWidth={10}
331- // snapGridHeight={10}
332- isDisplayGridGuidelines = { false }
333419 horizontalGuidelines = { horizontalGuidelines }
334420 verticalGuidelines = { verticalGuidelines }
421+ elementGuidelines = { Array . from (
422+ document . querySelectorAll < HTMLElement > ( "[data-layer-id]" ) ,
423+ ) . filter ( ( el ) => el . dataset . layerId !== layer . id ) }
424+ snapThreshold = { 5 }
335425 snapDistFormat = { ( v ) => `${ Math . round ( v ) } px` }
336426 snapDirections = { {
337427 top : true ,
338428 left : true ,
339429 bottom : true ,
340430 right : true ,
341- // center: true,
342- // middle: true,
431+ center : true ,
432+ middle : true ,
343433 } }
344434 elementSnapDirections = { {
345435 top : true ,
346436 left : true ,
347437 bottom : true ,
348438 right : true ,
349- // center: true,
350- // middle: true,
439+ center : true ,
440+ middle : true ,
351441 } }
442+ snapGap = { true }
352443 onDragStart = { ( ) => {
353444 const proceed = checkExpressionGuard ( ( ) => {
354445 // User confirmed — they can drag again now
0 commit comments