@@ -23,6 +23,8 @@ class CoreGraph {
2323
2424 bendNode ;
2525
26+ gridSize = 20 ; // Configurable grid size in pixels
27+
2628 constructor ( id , element , dispatcher , superState , projectName , nodeValidator , edgeValidator , authorName ) {
2729 if ( dispatcher ) this . dispatcher = dispatcher ;
2830 if ( superState ) this . superState = superState ;
@@ -49,23 +51,92 @@ class CoreGraph {
4951 this . initizialize ( ) ;
5052 }
5153
54+ // Helper function to snap a value to the nearest grid point
55+ snapToGrid ( value ) {
56+ return Math . round ( value / this . gridSize ) * this . gridSize ;
57+ }
58+
59+ // Helper function to snap position to grid
60+ snapPositionToGrid ( position ) {
61+ return {
62+ x : this . snapToGrid ( position . x ) ,
63+ y : this . snapToGrid ( position . y ) ,
64+ } ;
65+ }
66+
67+ // Helper function to snap dimension to EVEN grid multiples
68+ // This ensures all borders lie on grid lines when center is on a grid point
69+ snapDimensionToGrid ( dimension ) {
70+ let gridCells = Math . round ( dimension / this . gridSize ) ;
71+ // Ensure at least 2 grid cells
72+ if ( gridCells < 2 ) {
73+ gridCells = 2 ;
74+ }
75+ // Ensure always an even number
76+ const evenCells = gridCells % 2 === 0 ? gridCells : gridCells + 1 ;
77+ return evenCells * this . gridSize ;
78+ }
79+
5280 initizialize ( ) {
5381 this . cy . nodeEditing ( {
5482 resizeToContentCueEnabled : ( ) => false ,
55- setWidth ( node , width ) {
56- node . data ( 'style' , { ...node . data ( 'style' ) , width } ) ;
83+ setWidth : ( node , width ) => {
84+ // HARD ENFORCEMENT: Snap width every frame during resize
85+ const snappedWidth = this . snapDimensionToGrid ( width ) ;
86+ node . data ( 'style' , { ...node . data ( 'style' ) , width : snappedWidth } ) ;
87+
88+ // Adjust position to maintain edge alignment based on resize handle
89+ const resizeType = node . scratch ( 'resizeType' ) ;
90+ if ( resizeType && ( resizeType . includes ( 'left' ) || resizeType . includes ( 'right' ) ) ) {
91+ const currentPos = node . position ( ) ;
92+ const initialPos = node . scratch ( 'resizeInitialPos' ) ;
93+ const initialWidth = node . scratch ( 'width' ) ;
94+ const widthDelta = snappedWidth - initialWidth ;
95+
96+ let newX = currentPos . x ;
97+ if ( resizeType . includes ( 'left' ) ) {
98+ newX = initialPos . x - widthDelta / 2 ;
99+ } else if ( resizeType . includes ( 'right' ) ) {
100+ newX = initialPos . x + widthDelta / 2 ;
101+ }
102+ node . position ( { x : this . snapToGrid ( newX ) , y : currentPos . y } ) ;
103+ }
104+ return snappedWidth ;
57105 } ,
58- setHeight ( node , height ) {
59- node . data ( 'style' , { ...node . data ( 'style' ) , height } ) ;
106+ setHeight : ( node , height ) => {
107+ // HARD ENFORCEMENT: Snap height every frame during resize
108+ const snappedHeight = this . snapDimensionToGrid ( height ) ;
109+ node . data ( 'style' , { ...node . data ( 'style' ) , height : snappedHeight } ) ;
110+
111+ // Adjust position to maintain edge alignment based on resize handle
112+ const resizeType = node . scratch ( 'resizeType' ) ;
113+ if ( resizeType && ( resizeType . includes ( 'top' ) || resizeType . includes ( 'bottom' ) ) ) {
114+ const currentPos = node . position ( ) ;
115+ const initialPos = node . scratch ( 'resizeInitialPos' ) ;
116+ const initialHeight = node . scratch ( 'height' ) ;
117+ const heightDelta = snappedHeight - initialHeight ;
118+
119+ let newY = currentPos . y ;
120+ if ( resizeType . includes ( 'top' ) ) {
121+ newY = initialPos . y - heightDelta / 2 ;
122+ } else if ( resizeType . includes ( 'bottom' ) ) {
123+ newY = initialPos . y + heightDelta / 2 ;
124+ }
125+ node . position ( { x : currentPos . x , y : this . snapToGrid ( newY ) } ) ;
126+ }
127+ return snappedHeight ;
60128 } ,
61129 isNoResizeMode ( node ) { return node . data ( 'type' ) !== 'ordin' ; } ,
62130 isNoControlsMode ( node ) { return node . data ( 'type' ) !== 'ordin' ; } ,
63131 } ) ;
64132
65133 this . cy . gridGuide ( {
66- snapToGridOnRelease : false ,
134+ snapToGridOnRelease : true ,
135+ snapToGridDuringDrag : true ,
67136 zoomDash : true ,
68137 panGrid : true ,
138+ gridSpacing : this . gridSize ,
139+ snapToAlignmentLocationOnRelease : true ,
69140 } ) ;
70141 this . cy . edgehandles ( {
71142 preview : false ,
@@ -164,10 +235,40 @@ class CoreGraph {
164235 } ) ;
165236 } ) ;
166237
238+ this . cy . on ( 'free' , 'node[type = "ordin"]' , ( e ) => {
239+ e . target . forEach ( ( node ) => {
240+ const initialPos = node . scratch ( 'position' ) ;
241+ const currentPos = node . position ( ) ;
242+ // Only snap if the node actually moved
243+ const moved = ! initialPos || initialPos . x !== currentPos . x || initialPos . y !== currentPos . y ;
244+ if ( moved ) {
245+ const snappedPos = this . snapPositionToGrid ( currentPos ) ;
246+ node . position ( snappedPos ) ;
247+ }
248+ } ) ;
249+ } ) ;
250+
167251 this . cy . on ( 'nodeediting.resizestart' , ( e , type , node ) => {
252+ // Store initial state for resize operation
168253 node . scratch ( 'height' , node . data ( 'style' ) . height ) ;
169254 node . scratch ( 'width' , node . data ( 'style' ) . width ) ;
170- node . scratch ( 'position' , { ...node . position ( ) } ) ;
255+ node . scratch ( 'resizeInitialPos' , { ...node . position ( ) } ) ;
256+ node . scratch ( 'resizeType' , type ) ;
257+ } ) ;
258+
259+ this . cy . on ( 'nodeediting.resizeend' , ( e , type , node ) => {
260+ // Clean up scratch data
261+ node . removeScratch ( 'resizeType' ) ;
262+ node . removeScratch ( 'resizeInitialPos' ) ;
263+
264+ // Final enforcement: ensure position and dimensions are grid-aligned
265+ const style = node . data ( 'style' ) || { } ;
266+ const snappedWidth = this . snapDimensionToGrid ( style . width || 100 ) ;
267+ const snappedHeight = this . snapDimensionToGrid ( style . height || 50 ) ;
268+ node . data ( 'style' , { ...style , width : snappedWidth , height : snappedHeight } ) ;
269+
270+ const snappedPos = this . snapPositionToGrid ( node . position ( ) ) ;
271+ node . position ( snappedPos ) ;
171272 } ) ;
172273
173274 this . cy . on ( 'hide-bend remove' , ( ) => {
0 commit comments