1- import React from "react" ;
1+ import React , { useMemo } from "react" ;
22import { useGesture } from "@use-gesture/react" ;
33import { useCurrentEditor } from "../../use-editor" ;
4- import { useNode } from "../../provider" ;
5- import type cmath from "@grida/cmath" ;
4+ import { useNode , useGestureState , useTransformState } from "../../provider" ;
5+ import cmath from "@grida/cmath" ;
66
77export function NodeOverlayCornerRadiusHandle ( {
88 node_id,
@@ -16,34 +16,202 @@ export function NodeOverlayCornerRadiusHandle({
1616 size ?: number ;
1717} ) {
1818 const editor = useCurrentEditor ( ) ;
19+ const { gesture } = useGestureState ( ) ;
20+ const { transform } = useTransformState ( ) ;
1921
2022 const bind = useGesture ( {
2123 onDragStart : ( { event } ) => {
2224 event . preventDefault ( ) ;
23- editor . surface . surfaceStartCornerRadiusGesture ( node_id , anchor ) ;
25+ const altKey = ( event as PointerEvent ) . altKey || false ;
26+ editor . surface . surfaceStartCornerRadiusGesture ( node_id , anchor , altKey ) ;
2427 } ,
2528 } ) ;
2629
2730 const node = useNode ( node_id ) ;
28- const radii = typeof node . corner_radius === "number" ? node . corner_radius : 0 ;
29- const minmargin = Math . max ( radii + size , margin ) ;
31+
32+ // Get current radius value for this corner
33+ const currentRadius = useMemo ( ( ) => {
34+ if (
35+ node . type === "rectangle" ||
36+ node . type === "container" ||
37+ node . type === "component" ||
38+ node . type === "image" ||
39+ node . type === "video"
40+ ) {
41+ const keyMap = {
42+ nw : "rectangular_corner_radius_top_left" ,
43+ ne : "rectangular_corner_radius_top_right" ,
44+ se : "rectangular_corner_radius_bottom_right" ,
45+ sw : "rectangular_corner_radius_bottom_left" ,
46+ } as const ;
47+ return ( node as any ) [ keyMap [ anchor ] ] ?? 0 ;
48+ }
49+ return typeof node . corner_radius === "number" ? node . corner_radius : 0 ;
50+ } , [ node , anchor ] ) ;
51+
52+ // Mathematical constants: computed once per size change
53+ const labelOffsets = useMemo ( ( ) => ( {
54+ X : size / 2 + 4 ,
55+ Y_TOP : size / 2 + 4 ,
56+ Y_BOTTOM : size / 2 + 20 ,
57+ } ) , [ size ] ) ;
58+
59+ // Shared geometry calculations: compute once, use multiple times
60+ const geometry = useMemo ( ( ) => {
61+ const br = editor . geometryProvider . getNodeAbsoluteBoundingRect ( node_id ) ;
62+ if ( ! br ) return null ;
63+
64+ const boundingSurfaceRect = cmath . rect . transform ( br , transform ) ;
65+ const [ scaleX , scaleY ] = cmath . transform . getScale ( transform ) ;
66+ const w = boundingSurfaceRect . width ;
67+ const h = boundingSurfaceRect . height ;
68+ const minmargin = Math . max ( currentRadius + size , margin ) ;
69+ const useMarginBased = currentRadius < margin ;
70+
71+ // Corner coordinates: C = (C_x, C_y)
72+ const corners = {
73+ nw : [ 0 , 0 ] ,
74+ ne : [ w , 0 ] ,
75+ se : [ w , h ] ,
76+ sw : [ 0 , h ] ,
77+ } as const ;
78+ const [ C_x , C_y ] = corners [ anchor ] ;
79+
80+ // Arc center offset: O = (O_x, O_y) = (r * s_x * sign_x, r * s_y * sign_y)
81+ const offsets = {
82+ nw : [ currentRadius * scaleX , currentRadius * scaleY ] ,
83+ ne : [ - currentRadius * scaleX , currentRadius * scaleY ] ,
84+ se : [ - currentRadius * scaleX , - currentRadius * scaleY ] ,
85+ sw : [ currentRadius * scaleX , - currentRadius * scaleY ] ,
86+ } as const ;
87+ const [ O_x , O_y ] = offsets [ anchor ] ;
88+
89+ // Center coordinates: M = (M_x, M_y) = (w/2, h/2)
90+ const M_x = w / 2 ;
91+ const M_y = h / 2 ;
92+
93+ // Handle position relative to center: H = (H_x, H_y) = (C + O - M)
94+ const H_x = C_x + O_x - M_x ;
95+ const H_y = C_y + O_y - M_y ;
96+
97+ return {
98+ w,
99+ h,
100+ scaleX,
101+ scaleY,
102+ minmargin,
103+ useMarginBased,
104+ H_x,
105+ H_y,
106+ M_x,
107+ M_y,
108+ } ;
109+ } , [ editor . geometryProvider , node_id , anchor , currentRadius , transform , size , margin ] ) ;
110+
111+ // Calculate handle position: at arc center O when radius >= margin, otherwise at corner with margin
112+ const handleStyle = useMemo ( ( ) => {
113+ if ( ! geometry ) return null ;
114+
115+ const { useMarginBased, H_x, H_y, minmargin } = geometry ;
116+
117+ if ( ! useMarginBased && currentRadius > 0 ) {
118+ // Handle at arc center: H = (C + O - M) relative to center
119+ return {
120+ left : `calc(50% + ${ H_x } px)` ,
121+ top : `calc(50% + ${ H_y } px)` ,
122+ transform : "translate(-50%, -50%)" ,
123+ } ;
124+ }
125+
126+ // Handle at corner with margin: position = minmargin from edge
127+ const positions = {
128+ nw : { top : `${ minmargin } px` , left : `${ minmargin } px` } ,
129+ ne : { top : `${ minmargin } px` , right : `${ minmargin } px` } ,
130+ se : { bottom : `${ minmargin } px` , right : `${ minmargin } px` } ,
131+ sw : { bottom : `${ minmargin } px` , left : `${ minmargin } px` } ,
132+ } as const ;
133+
134+ return {
135+ ...positions [ anchor ] ,
136+ transform : `translate(${ anchor [ 1 ] === "w" ? "-50%" : "50%" } , ${ anchor [ 0 ] === "n" ? "-50%" : "50%" } )` ,
137+ } ;
138+ } , [ geometry , anchor , currentRadius ] ) ;
139+
140+ // Only show label for the specific handle being dragged
141+ const isDragging =
142+ gesture . type === "corner-radius" &&
143+ gesture . node_id === node_id &&
144+ gesture . anchor === anchor ;
145+
146+ // Label position relative to handle (inside direction, toward center)
147+ const labelStyle = useMemo ( ( ) => {
148+ if ( ! isDragging || ! geometry ) return null ;
149+
150+ const { useMarginBased, H_x, H_y, M_x, minmargin } = geometry ;
151+
152+ if ( ! useMarginBased && currentRadius > 0 ) {
153+ // Label offset from handle: L_offset = (L_x, L_y) in inside direction
154+ const labelOffsetMap = {
155+ nw : [ labelOffsets . X , labelOffsets . Y_TOP ] ,
156+ ne : [ - labelOffsets . X , labelOffsets . Y_TOP ] ,
157+ se : [ - labelOffsets . X , - labelOffsets . Y_BOTTOM ] ,
158+ sw : [ labelOffsets . X , - labelOffsets . Y_BOTTOM ] ,
159+ } as const ;
160+ const [ L_x , L_y ] = labelOffsetMap [ anchor ] ;
161+
162+ // Label position relative to center: L = H + L_offset
163+ const L_x_center = H_x + L_x ;
164+ const L_y_center = H_y + L_y ;
165+
166+ // For right-side corners (ne, se), use 'right' instead of 'left' to maintain consistency
167+ if ( anchor === "ne" || anchor === "se" ) {
168+ // Convert from center-relative to right-edge distance: right = M_x - L_x_center
169+ return {
170+ right : `${ M_x - L_x_center } px` ,
171+ top : `calc(50% + ${ L_y_center } px)` ,
172+ } ;
173+ }
174+
175+ return {
176+ left : `calc(50% + ${ L_x_center } px)` ,
177+ top : `calc(50% + ${ L_y_center } px)` ,
178+ } ;
179+ }
180+
181+ // Margin-based: label inside direction from handle
182+ const labelPositions = {
183+ nw : { top : `${ minmargin + labelOffsets . Y_TOP } px` , left : `${ minmargin + labelOffsets . X } px` } ,
184+ ne : { top : `${ minmargin + labelOffsets . Y_TOP } px` , right : `${ minmargin + labelOffsets . X } px` } ,
185+ se : { bottom : `${ minmargin + labelOffsets . X } px` , right : `${ minmargin + labelOffsets . X } px` } ,
186+ sw : { bottom : `${ minmargin + labelOffsets . X } px` , left : `${ minmargin + labelOffsets . X } px` } ,
187+ } as const ;
188+
189+ return labelPositions [ anchor ] ;
190+ } , [ isDragging , geometry , anchor , currentRadius , labelOffsets ] ) ;
191+
192+ if ( ! handleStyle ) return null ;
30193
31194 return (
32- < div
33- { ...bind ( ) }
34- className = "hidden group-hover:block border rounded-full bg-white border-workbench-accent-sky absolute z-10 pointer-events-auto"
35- style = { {
36- top : anchor [ 0 ] === "n" ? minmargin : "auto" ,
37- bottom : anchor [ 0 ] === "s" ? minmargin : "auto" ,
38- left : anchor [ 1 ] === "w" ? minmargin : "auto" ,
39- right : anchor [ 1 ] === "e" ? minmargin : "auto" ,
40- width : size ,
41- height : size ,
42- transform : `translate(${ anchor [ 1 ] === "w" ? "-50%" : "50%" } , ${ anchor [ 0 ] === "n" ? "-50%" : "50%" } )` ,
43- cursor : "pointer" ,
44- touchAction : "none" ,
45- } }
46- />
195+ < >
196+ < div
197+ { ...bind ( ) }
198+ className = "hidden group-hover:block border rounded-full bg-white border-workbench-accent-sky absolute z-10 pointer-events-auto"
199+ style = { {
200+ ...handleStyle ,
201+ width : size ,
202+ height : size ,
203+ cursor : "pointer" ,
204+ touchAction : "none" ,
205+ } }
206+ />
207+ { isDragging && labelStyle && (
208+ < div className = "absolute pointer-events-none z-20" style = { labelStyle } >
209+ < div className = "bg-workbench-accent-sky text-white text-xs px-1.5 py-0.5 rounded-sm shadow whitespace-nowrap" >
210+ Radius { currentRadius }
211+ </ div >
212+ </ div >
213+ ) }
214+ </ >
47215 ) ;
48216}
49217
0 commit comments