1

This is my first time posting a question in Stack Overflow, so feel free to comment on how I can improve my question as well.

I am trying to make two SVG rectangle draggables which cannot overlap. To do this, I have used Snap.svg to make the elements drag, and have taken their bounding boxes with every call of the move function to see if they collide, using the .isBBoxIntersect utility method in the Snap API. If they do collide, I want to make sure they will not overlap, and thus make each object unpassable to the other. The object currently being dragged will then move on a certain axis, until the collision returns false once more. I have some basic code of what I want here:

<html>
<head>
<title>
</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js"></script>

</head>
<body>

<script>
var s = Snap(600,500);

var rect = s.rect(0,0,40,40);
var rectr = s.rect(400,90,50,50);
var b=0;
var c=0;
var isInter;
var move = function(dx,dy, x, y, event) {

var b1 = rect.getBBox();
var b2 = rectr.getBBox();
isInter = Snap.path.isBBoxIntersect(b1, b2);

if (isInter==false) {
b=dx;
c=dy;
}

if (isInter==true) {

if (b1.y2==b2.y&&b1.x2==b2.x||b1.x==b2.x2&&b1.y2==b2.y){c=b2.y-b1.h, b=dx
}
else if (b1.x==b2.x2&&b1.y==b2.y2||b1.x2==b2.x&&b1.y==b2.y2){c=b2.y2; b=dx;}
else if (b1.y2==b2.y){(dy>=b2.y-b1.h) ? (c=b2.y-b1.h, b=dx): (b=dx, c=dy);}
else if (b1.y==b2.y2){(dy<=b1.y) ? (c=b2.y2, b=dx):(b=dx,c=dy);}
else if (b1.x2==b2.x){(dx>=b1.x) ? (b=b2.x-b1.width, c=dy):(b=dx, c=dy);}
else if (b1.x==b2.x2){(dx<=b1.x2) ? (b=b2.x2, c=dy):(b=dx, c=dy);}
else {b=dx; c=dy;}

}

this.attr({
transform: this.data('origTransform') + ((this.data('origTransform')) ? "t": "T") + [b,c]
 });

}

var start = function() {
        this.data('origTransform', this.transform().local );
b=0;
c=0;
}


rect.drag(move, start);
circle.drag(move, start);
</script>
</body>
</html>

These are the three main issues that came up:

  1. If you drag too fast, the engine can't keep up, and the draggables will overlap. I'm hoping for a method to prevent overlap no matter how fast it is dragged.

  2. The collision only works for rect when it is dragged on rectr. I could easily add another block of collision detection for rectr on rect, but I think that would slow the engine WAY too much. My collision detection seems overly complex. Thus, I'm hoping for a more efficient way to test the collision.

  3. If rectr is dragged first, then rect is dragged on rectr, the collision detection completely fails. It might be an issue of .getBBox(), but I can't say for certain.

Any help on these three issues would be extremely appreciated.

Thank You!

Andrew T.
  • 25
  • 1
  • 7
  • Hard to read through the logic in your code without better comments, eg what are b, c. Are you sure you want to be testing bboxes with things like if b1.y==b2.y ? Not sure what the chances of them being equal are, just test if they are >= or <= etc to find the blocking points. Maybe I've not quite understood the code though. – Ian Jun 29 '16 at 16:44
  • Almost any approach to totally prevent overlap will typically provide an unwanted 'stutter' in movement. Consider merely changing the opacity of the collided elements when they collide, returning opacity=1 when collision is false. This provides a 'gentle' user feedback during dragging when the elements have collided. – Francis Hemsher Jun 29 '16 at 17:57
  • @Ian I was hoping that the rect would "stick" to the side of rectr which it collided on. If I used >= or <= operators, there is a possibility of the rect "teleporting" to the conditional with the <= or >= operator even when it collided with another side of rectr. – Andrew T. Jun 29 '16 at 22:56
  • @Francis Hemsher I was hoping to apply the method in some kind of game. If it really is impossible to prevent stuttering (and thus overlap), is it at least possible to detect the rect once it overlaps, then "teleport" it to stick to the side of rectr it collided through? Worse comes to worse, is there a way to solve the other two main issues? – Andrew T. Jun 29 '16 at 23:05

2 Answers2

2

Well, I can only offer an alternative using Javascript, sorry. Maybe this will help you sometime in the future.

This example creates fifty(50) svg shapes which are randomly located. Each shape is draggable. If the dragged shape's bounding box intersects another shape, its opacity is changed until the dragged shape moves out of intersect range.

Wish you luck.

<head>
  <title>Untitled</title>
</head>
<body onLoad=svgGLOB(50,800,800,30)>
<div style=font-family:arial>
Fifty(50) svg shapes are created and randomly located. Each shape is draggable. If the dragged shape's bounding box intersects another shape, its opacity is changed until the dragged shape moves out of intersect range.
</div>
<div id=svgDiv style='width:800px;height:800px;border:1px solid black'>
<svg id=mySVG width="800" height="800"  onmousedown="startDrag(evt)" onmousemove="drag(evt)" onmouseup="endDrag()"></svg>
</div>
</body>
<script>
function intersectShape(target)
{
    var r1 = target.getBoundingClientRect();    //BOUNDING BOX OF THE TARGET OBJECT

    for(k=0;k<globG.childNodes.length;k++)
    {
        var shape=globG.childNodes.item(k)
        if(shape!=target)
        {
            var r2=shape.getBoundingClientRect();

            //CHECK IF ANY TWO BOUNDING BOXES OVERLAP
            if(!(r2.left > r1.right ||
            r2.right < r1.left ||
            r2.top > r1.bottom ||
            r2.bottom < r1.top))
                shape.setAttribute("opacity",.5)
            else
                shape.setAttribute("opacity",1)
        }
    }
}
var TransformRequestObj
var TransList
var DragTarget=null;
var Dragging = false;
var OffsetX = 0;
var OffsetY = 0;

//---mouse down over element---
function startDrag(evt)
{
 if(!Dragging) //---prevents dragging conflicts on other draggable elements---
 {
  if(evt.target.getAttribute("class")=="dragTarget")
  {
   DragTarget = evt.target;
   DragTarget.setAttribute("style","cursor:move")
   //---reference point to its respective viewport--
   var pnt = DragTarget.ownerSVGElement.createSVGPoint();
   pnt.x = evt.clientX;
   pnt.y = evt.clientY;
   //---elements transformed and/or in different(svg) viewports---
   var sCTM = DragTarget.getScreenCTM();
   var Pnt = pnt.matrixTransform(sCTM.inverse());

   TransformRequestObj = DragTarget.ownerSVGElement.createSVGTransform()
   //---attach new or existing transform to element, init its transform list---
   var myTransListAnim=DragTarget.transform
   TransList=myTransListAnim.baseVal

   OffsetX = Pnt.x
   OffsetY = Pnt.y

   Dragging=true;
  }

 }
}
//---mouse move---
function drag(evt)
{
    if(Dragging)
    {
        var pnt = DragTarget.ownerSVGElement.createSVGPoint();
        pnt.x = evt.clientX;
        pnt.y = evt.clientY;
        //---elements in different(svg) viewports, and/or transformed ---
        var sCTM = DragTarget.getScreenCTM();
        var Pnt = pnt.matrixTransform(sCTM.inverse());
        Pnt.x -= OffsetX;
        Pnt.y -= OffsetY;

        TransformRequestObj.setTranslate(Pnt.x,Pnt.y)
        TransList.appendItem(TransformRequestObj)
        TransList.consolidate()

        intersectShape(DragTarget)
    }
}
//--mouse up---
function endDrag()
{
    Dragging = false;
    DragTarget.setAttribute("style","cursor:default")
}

//==================add a bunch of SVG elements============
//---onload: svgGLOB(50,800,800,30)---
function svgGLOB(elems,svgWidth,svgHeight,elemSize)
{
 /*  ---fill empty inline SVG element---
  <div id="svgDiv"><svg id="mySVG" /></div>
 */
 var NS="http://www.w3.org/2000/svg"
 mySVG.setAttribute("width",svgWidth)
 mySVG.setAttribute("height",svgHeight)
 svgDiv.style.width=svgWidth+"px"
 svgDiv.style.height=svgHeight+"px"

 var globG=document.createElementNS(NS,"g")
 globG.id="globG"
 globG.setAttribute("stroke","black")
 globG.setAttribute("stroke-width",1)
 mySVG.appendChild(globG)

 var points=randomPoints(elems,svgWidth,svgHeight,elemSize)
 var n=points.length
 var circleCnt=0
 var ellipseCnt=0
 var rectCnt=0
 var polygonCnt=0

 var RandomElems=[]
 RandomElems[0]="circle"
 RandomElems[1]="rect"
 RandomElems[2]="ellipse"
 RandomElems[3]="polygon_3"
 RandomElems[4]="polygon_4"
 RandomElems[5]="polygon_5"
 RandomElems[6]="polygon_6"
 RandomElems[7]="polygon_7"
 RandomElems[8]="polygon_8"
 RandomElems[9]="polygon_9"
 RandomElems[10]="polygon_10"
 RandomElems[11]="polygon_11"
 RandomElems[12]="polygon_12"

 for(var k=0;k<n;k++)
 {
  var rand=rdm(0,12)
  var elemStr=RandomElems[rand]

  if(!elemStr.indexOf("_"))
   var elemSt=elemStr
  else
   var elemSt=elemStr.split("_")[0]

  var elem=document.createElementNS(NS,elemSt)

  if(elemSt=="circle")
  {
   elem.setAttribute("r",elemSize)
   elem.setAttribute("fill",rcolor())
   elem.setAttribute("cx",points[k][0])
   elem.setAttribute("cy",points[k][1])
   elem.id=elemSt+(circleCnt++)
  }
  else if(elemSt=="ellipse")
  {
   elem.setAttribute("rx",elemSize)
   elem.setAttribute("ry",elemSize/2)
   elem.setAttribute("fill",rcolor())
   elem.setAttribute("cx",points[k][0])
   elem.setAttribute("cy",points[k][1])
   elem.id=elemSt+(ellipseCnt++)
  }
  else if(elemSt=="rect")
  {
   elem.setAttribute("width",elemSize)
   elem.setAttribute("height",elemSize)
   elem.setAttribute("fill",rcolor())
   elem.setAttribute("x",points[k][0])
   elem.setAttribute("y",points[k][1])
   elem.id=elemSt+(rectCnt++)
  }
  else if(elemSt=="polygon")
  {
   var pgonSides=parseInt(elemStr.split("_")[1])
   var pgonPnts=polygon(pgonSides,elemSize,points[k][0],points[k][1])
   elem.setAttribute("fill",rcolor())
   elem.setAttribute("points",pgonPnts.join())
   elem.id=elemSt+(polygonCnt++)
  }
        elem.setAttribute("class","dragTarget")
  globG.appendChild(elem)
 }

 //---obtain a random whole number from a thru b---
 function rdm(a,b)
 {
  return a + Math.floor(Math.random()*(b-a+1));
 }

 function randomPoints(elems,svgWidth,svgHeight,elemSize)
 {
  //--return format:[ [x,y],[x,y],,, ]
  //---Generate  random points---
  function times(n, fn)
  {
   var a = [], i;
   for (i = 0; i < n; i++) {
   a.push(fn(i));
   }
   return a;
  }
  var width=svgWidth-2*elemSize
  var height=svgHeight-2*elemSize

  return  RandomPnts = times(elems, function() { return [Math.floor(width * Math.random()) + elemSize, Math.floor(height * Math.random()) + elemSize] });
 }
    //---random color---
 function rcolor()
 {
  var letters = '0123456789ABCDEF'.split('');
  var color = '#';
  for (var i = 0; i < 6; i++ )
  {
   color += letters[Math.round(Math.random() * 15)];
  }
  return color;
 }
 function polygon(vCnt,radius,centerX,centerY)
 {
  var myPoints=[]
  var polyXPts      = Array(vCnt);
  var polyYPts      = Array(vCnt);
  var vertexAngle   = 360/vCnt;
  //---init polygon points processor---
  for(var v=0; v<vCnt; v++)
  {
   theAngle = (v*vertexAngle)*Math.PI/180;
   polyXPts[v] = radius*Math.cos(theAngle);
   polyYPts[v] = -radius*Math.sin(theAngle);
  }
  //--note points are CCW---
  for(var v=0;v<vCnt; v++)
  {
   var point=[centerX+polyXPts[v],centerY+polyYPts[v]]
   myPoints.push(point)
  }
  return myPoints
 }
}
</script>
Francis Hemsher
  • 3,478
  • 2
  • 13
  • 15
  • Though the answer hasn't totally solved my problem, at the very least I have learnt a lot about manipulating SVGs which I was ignorant of before. Sorry I couldn't at least upvote...my reputation's too low. So I can only say Thank You. :) – Andrew T. Jun 30 '16 at 22:59
0

OK, Let's begin with an example using SVG's native methods and javascript. This example has either rect 'stick' to the other. The drag will not degrade no matter how many elements are involved. The next step is to determine what to do after the rects are stuck together?

<head>
  <title>Untitled</title>
</head>

<body>
<svg width="800" height="800"  onmousedown="startDrag(evt)" onmousemove="drag(evt)" onmouseup="endDrag()">
<rect class="dragTarget" id=rect1 x=200 y=200 width=50 height=90 fill=red />
<rect class="dragTarget"  id=rect2 x=400 y=400 width=50 height=90 fill=blue />
</svg>
</body>
<script>
function intersectRect(shape1, shape2) {
    var r1 = shape1.getBoundingClientRect();    //BOUNDING BOX OF THE FIRST OBJECT
    var r2 = shape2.getBoundingClientRect();    //BOUNDING BOX OF THE SECOND OBJECT

    //CHECK IF THE TWO BOUNDING BOXES OVERLAP
  return !(r2.left > r1.right ||
           r2.right < r1.left ||
           r2.top > r1.bottom ||
           r2.bottom < r1.top);
}
 var TransformRequestObj
var TransList
var DragTarget=null;
var Dragging = false;
var OffsetX = 0;
var OffsetY = 0;
//---mouse down over element---
function startDrag(evt)
{
 if(!Dragging) //---prevents dragging conflicts on other draggable elements---
 {
  if(evt.target.getAttribute("class")=="dragTarget")
  {
   DragTarget = evt.target;
   DragTarget.setAttribute("style","cursor:move")
   //---reference point to its respective viewport--
   var pnt = DragTarget.ownerSVGElement.createSVGPoint();
   pnt.x = evt.clientX;
   pnt.y = evt.clientY;
   //---elements transformed and/or in different(svg) viewports---
   var sCTM = DragTarget.getScreenCTM();
   var Pnt = pnt.matrixTransform(sCTM.inverse());

   TransformRequestObj = DragTarget.ownerSVGElement.createSVGTransform()
   //---attach new or existing transform to element, init its transform list---
   var myTransListAnim=DragTarget.transform
   TransList=myTransListAnim.baseVal

   OffsetX = Pnt.x
   OffsetY = Pnt.y

   Dragging=true;
  }
 }
}
//---mouse move---
function drag(evt)
{

 if(Dragging)
 {
         if(intersectRect(rect1, rect2)==false)
           {
          var pnt = DragTarget.ownerSVGElement.createSVGPoint();
          pnt.x = evt.clientX;
          pnt.y = evt.clientY;
          //---elements in different(svg) viewports, and/or transformed ---
          var sCTM = DragTarget.getScreenCTM();
          var Pnt = pnt.matrixTransform(sCTM.inverse());
          Pnt.x -= OffsetX;
          Pnt.y -= OffsetY;

          TransformRequestObj.setTranslate(Pnt.x,Pnt.y)
          TransList.appendItem(TransformRequestObj)
          TransList.consolidate()
           }
 }
}
//--mouse up---
function endDrag()
{
    Dragging = false;
    DragTarget.setAttribute("style","cursor:default")
}
</script>
Francis Hemsher
  • 3,478
  • 2
  • 13
  • 15
  • I was hoping that, like as my demo went, the dragging will still be enabled even if the two objects collide. In which case, the rect will be stuck on the other rect, not in the sense of becoming completely unmovable, but only draggable on the x-axis if collided above or below, and y-axis if on the left and right sides. I then want this behavior to remain constant UNTIL the pointer(mouse) moves to an area where it can reach once more. The comment is too long, so I'll write the continuation in another. – Andrew T. Jun 30 '16 at 09:06
  • Example, if the red rect in your example collides with the blue rect from above, it will only be movable on the x-axis until the pointer moves an x value below the left side or above the right side of the blue rect, OR if the y value of the pointer is above the center of the red rect. This kind of logic then applies to all four sides of both rectangles. Imagine jQuery UI's draggable and obstacle, where both rects are draggable and obstacles of each other, except it's in SVG. – Andrew T. Jun 30 '16 at 09:14