andytwoods

dragged objects ‘snap’ to a regular grid

Recommended Posts

Hi Jack. Could you advise me how to implement a way so that dragged objects ‘snap’ to a regular grid of positions? I’m hoping this feature is already implemented but i’m sure I could hack this feature up (happy to share resulting code - I've this lovely binarySearch array function that could be useful). I’d link this ‘shap-to-grid’ property to your ‘rotate around point’ feature so that the snapping would occur at a set point on the object.

Cheers,

Andy

 

PS Sorry, I posted this message also in your TransformManager comments page (free free to delete).

Share this post


Link to post
Share on other sites

Well, you could either edit the raw code inside the TransformManager class or you could listen externally for the TransformEvent.MOVE event and make your adjustments there (just don't forget to updateSelection() after you do your adjustments).

 

The feature you're talking about would probably be fine to do with moves of single objects, but not with scaling or rotating. Imagine scaling from one corner (which sets the opposite corner as the transform origin) and having some arbitrary point on your object always snap to a grid - extremely difficult to implement math-wise, especially if the object is rotated and/or skewed and/or if there are multiple selections. In fact, the multi-selection capability would make it virtually impossible to implement your "snap to grid" feature accurately because you'd want the objects to move together yet what if their points didn't line up with the grid accordingly? That would make your objects jump around quite a bit while moving and it'd look/feel very odd.

 

See what I mean?

Share this post


Link to post
Share on other sites

Cheers for your advice :)

 

Totally agree about not having this snap to grid property for scaling and rotating. Would be quite bizarre!

 

I'll have a go now (within the TransformManager class and failing that via that external way) and let people know how I did things in the end.

 

Cheers,

Andy.

Share this post


Link to post
Share on other sites

Hi there,

 

cool, all working :). Jack, afraid I don't know what the updateSelection() function is, so my solution below is messy in that I had to insert a public function into your TransformManager to access it. Don't suppose there is a better way? Thanks!

 

For the googler wondering what on earth I did, I firstly created Arrays containing the fixed positions I would like the given Sprite to 'snapto' (where 'card' is the parent Sprite in which my other Sprites are held; I liked the idea of allowing the user to go outside the parent sprite, hence 'additionalStepJumpZones'):

 


private var stepJumpArrX:Array;
private var stepJumpArrY:Array;
private var additionalStepZones:uint=10;

private function computeSnapToGrid():void
 {
  var myStepJumps:Array=new Array;
  myStepJumps.x=5; //percentage stepsize
  myStepJumps.y=20; //percentage stepsize
  constrainedX=true;
  constrainedY=true;


  if(myStepJumps.x!=0){
this.stepJumpArrX=new Array;
for(var percent:Number=-(additionalStepZones*myStepJumps.x);percent<100+(additionalStepZones*myStepJumps.x);percent+=myStepJumps.x){
 stepJumpArrX.push(percent*card.width*.01);//-correctionX); // containerWidth=cardWidth/myScaleX
}
  }
  else(stepJumpArrX=null);


  if(myStepJumps.y!=0){
this.stepJumpArrY=new Array;
for(percent=-(additionalStepZones*myStepJumps.y);percent<100+(additionalStepZones*myStepJumps.y);percent+=myStepJumps.y){
 stepJumpArrY.push(percent*card.height*.01);//-correctionY); // containerHeight=cardHeight/myScaleY
}
  }
  else(stepJumpArrY=null);
 }

 

I next listened for when objects in the TransformManager were moved:

manager.addEventListener(TransformEvent.MOVE,moved,false,0,true);

 

When the objects were moved, I checked which point they were closest to and 'snapped them' to this point (note I've removed correctionX and correctionY, you could use these to offset the snapping, say to the middle of the moving object: correctionX=movingObj.width/2) :

 


 private var oldX:Number,oldY:Number,newX:Number,newY:Number;
 protected function moved(e:Event):void
 {

  movingObj=manager.selectedTargetObjects[0];

  newX=movingObj.x;
  newY=movingObj.y;

  if(newX!=oldX){
newX=binary_search(stepJumpArrX,newX);//-correctionX);
oldX=newX;
movingObj.x=newX;
  }

  if(newX!=oldX){
newY=binary_search(stepJumpArrY,newY);//-correctionY);
oldY=newY;
movingObj.y=newY;
  }

  manager.uS();
 }

 

I searched for the nearest points using the below binary search that the folks at www.actionscript.org helped me refine (wikipedia recommended this searched as v v efficient). Note the horrid 'manager.uS()' function I hacked into Jack's TransformManager (public function uS():void{updateSelection();} ... hoping there's a more elegant solution to this!

private function binary_search(arr:Array, key:int):int {

  //as discussed here: http://www.actionscript.org/forums/showthread.php3?p=1129506#post1129506
  //originally taken from en.wikipedia.org/wiki/Binary_search_algorithm
  var imin:int;
  var imax:int = arr.length - 1;
  var imid:int;
  var best:int;

  while (imax >= imin) {
imid = imin + imax >> 1;
best = arr[imid];

if (best < key) imin = imid + 1;
else if (best > key ) imax = imid - 1;
else return best;
  }
  if (key - arr[imin] > arr[imax] - key) return arr[imin];
  return arr[imax];
 }

 

Please let me know if you spot ways to improve this code :)

Cheers and happy coding, Andy.

Share this post


Link to post
Share on other sites

Note that using modulus you could potentially strip away most of the code in the above solution. I couldn't get it to work (there was some unwanted movement in some locations on screen) but if anyone does please let me know (there's an annoying bug with flash and floating point errors which I think are to blame). This could works most of the time:

 

protected function moved(e:Event):void  //couldn't get to work!!
 {

  var xPerStepSizes:Number=5; //5% steps along x-axis
  var yPerStepSizes:Number=15; //15% steps along y-axis


  movingObj=manager.selectedTargetObjects[0];

  newX=movingObj.x;
  newY=movingObj.y;

  if(newX!=oldX){

newX=newX-newX%(xPerStepSizes*.01*card.width));  
oldX=newX;
movingObj.x=newX;
  }

  if(newY!=oldY){
newY=newY-newY%(yPerStepSizes*.01*card.height)); //-correctionX);
oldY=newY;
movingObj.y=newY;
  }

  manager.uS();
 }

Share this post


Link to post
Share on other sites

Here is a more refined snap function that you can feed any value and the spacing you want and it'll spit back the appropriate value:

 

function snap(value:Number, spacing:Number):Number {
   return (((value + (spacing / 2)) / spacing) >> 0) * spacing;
}

 

Applying it to your method would look something like this (untested):

 

protected function moved(e:Event):void {
  var xPerStepSizes:Number = 5; //5% steps along x-axis
  var yPerStepSizes:Number = 15; //15% steps along y-axis
  var xSpacing:Number = xPerStepSizes * 0.1 * card.width;
  var ySpacing:Number = yPerStepSizes * 0.1 * card.height;

  movingObj = manager.selectedTargetObjects[0];

  movingObj.x = (((movingObj.x + (xSpacing / 2)) / xSpacing) >> 0) * xSpacing;
  movingObj.y = (((movingObj.y + (ySpacing / 2)) / ySpacing) >> 0) * ySpacing;

  manager.uS();
}

Share this post


Link to post
Share on other sites

Cool. Thanks! That weird motion I described was due to the moving Object expanding the size of the parent object (in my case, card). You just need to define the parent's width and keep it constant before running this function:

 

 protected function moved(e:Event):void {
  var xPerStepSizes:Number = 5; //5% steps along x-axis
  var yPerStepSizes:Number = 15; //15% steps along y-axis
  var xSpacing:Number = xPerStepSizes * 0.01 * card.myWidth;
  var ySpacing:Number = yPerStepSizes * 0.01 * card.myHeight;

  movingObj = manager.selectedTargetObjects[0];

  movingObj.x = (((movingObj.x + (xSpacing / 2)) / xSpacing) >> 0) * xSpacing;
  movingObj.y = (((movingObj.y + (ySpacing / 2)) / ySpacing) >> 0) * ySpacing;

  manager.uS();
 }

 

If you want your object to move beyond the parent object though you can use this function instead (afraid I do not know how efficient modulus [%] is but it works fine my end):

 

 protected function moved(e:Event):void
 {

  movingObj=manager.selectedTargetObjects[0];

  var xPerStepSizes:Number = 5; //5% steps along x-axis
  var yPerStepSizes:Number = 15; //15% steps along y-axis
  var xSpacing:Number = xPerStepSizes * 0.01 * card.myWidth;
  var ySpacing:Number = yPerStepSizes * 0.01 * card.myWidth;

  movingObj.x=movingObj.x-movingObj.x%xSpacing;
  movingObj.y=movingObj.y-movingObj.y%ySpacing;
  manager.uS();
 }

Share this post


Link to post
Share on other sites

First off, thanks for this. It was exactly the breadcrumbs that I needed to get a snap to grid function working. The transformManager appears to have a public updateSelection() method now so all that was required to get it function for position was to listen for the TransformEvent.MOVE, adjust the position and update the selection.


 


Where I'm running into challenges is getting the snap to work on the TransformEvent.SCALE. Adjusting the display object, like in the move above, doesn't adjust the TransformManagers handles. So the display object snaps to the scale but the handle position remains unadjusted and is out of position with the display object. Any suggestions?


Share this post


Link to post
Share on other sites

For the scale, I have added to the updateScale method in the TransformManager:

		/** @private **/
		private function updateScale($x:Boolean = true, $y:Boolean = true):void {
			var ti:Object = _trackingInfo; //to speed things up
			var mx:Number = _parent.mouseX - ti.mouseOffsetX, my:Number = _parent.mouseY - ti.mouseOffsetY;
			if (_bounds != null) {
				if (mx >= _bounds.right) {
					mx = _bounds.right - 0.02;
				} else if (mx <= _bounds.left) {
					mx = _bounds.left + 0.02;
				}
				if (my >= _bounds.bottom) {
					my = _bounds.bottom - 0.02;
				} else if (my <= _bounds.top) {
					my = _bounds.top + 0.02;
				}
			}
			
			if ( _snapScaleTo )
			{
				mx = (((mx + (_snapScaleTo / 2)) / _snapScaleTo) >> 0) * _snapScaleTo;
				my = (((my + (_snapScaleTo / 2)) / _snapScaleTo) >> 0) * _snapScaleTo;
			}

The _snapScaleTo variable is a public Number that I added as well. Whenever snap is turned on or the size of the grid is changed I update the transformManager.snapScaleTo to equal the grid value and set it to 0 if the grid or snap are turned off.

  • Like 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.