Search code examples
polygoneditpointfabricbounding

fabric.js reset polygon bounding box after a point is moved


When I move a polyon's point (node) with the mouse, the bounding box does not update to enclose all new points. I've tries every matrix, setCoords(), etc. on this site for days. Fabric's own demo is useless, due to it fails when the polygon is flipped (3+ years and no fix). I just want to edit a polygon by moving it's nodes (points) and/or moving the polygon position without a thousand lines of code. Rotation and resizing is not implemented (no idea how) on the example.

Update: I made work around fix. The CodePen link below has the code to draw polygons or polylines and can they can be edited, moved, resized, flipped, rotated, etc.

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Fabric.js Polygon move,flip,scale,resize,rotate by Robert W. Stewart</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/fabric@latest/dist/fabric.js"></script>
<script>
$(document).ready(function() {
  var pts=[],polyShape,nodeNum=0,nodes=[],polyType,polyline,polygon,lastPt=1,poly,btn,isDown=false,mouse,zindex,redrawAll;
  let defaults = {objectCaching:false,preserveObjectStacking:true,perPixelTargetFind:true}//JSON ...spread syntax
  canvas = new fabric.Canvas($('canvas')[0],{...defaults,targetFindTolerance:10});
  let Props = {objectCaching:false,hasControls:false,hasBorders:true,originX:'center',originY:'center',
  strokeUniform:true,borderOpacityWhenMoving:0,cornerColor:'magenta',cornerStyle:'circle',padding:10,borderDashArray:[4,4]};
///////////////a///////////////////////////////////////////////////// MOUSE:DOWN 
canvas.on('mouse:down', function (e) {
  if (btn=='Polyline' || btn=='Polygon' && poly==true) {
    if (pts.length > 1) { pts.splice(-1,1); } //remove duplicate start pts
    polyline = new fabric.Polyline(pts,{...Props,name:'temp',fill:'',stroke:'black'});
    canvas.add(polyline); polyline.points[pts.length] = {x: parseInt(mouse.x), y: parseInt(mouse.y)}; 
    lastPt++; isDown=true; $('#debug').text(JSON.stringify(pts)); info();
  }
  if (e.target && isDown==false && e.target.name=='poly') {clearNodes();}
}); 
//////////////////////////////////////////////////////////////////// MOUSE:MOVE (NEW POLYLINES)
canvas.on('mouse:move', function(e){
  mouse = canvas.getPointer(e);
  if (poly==true && isDown) { polyline.points[lastPt-1] = {x: mouse.x, y: mouse.y}; canvas.renderAll();  }
});
//////////////////////////////////////////////////////////////////// OBJECT:MOVING (NODES)
canvas.on('object:moving', function (e) {
  if (e.target && e.target.name=='node') {
    polyShape.points[ e.target.nodeNum] = {x:e.target.getCenterPoint().x, y:e.target.getCenterPoint().y}; 
  }
});
//////////////////////////////////////////////////////////////////// MOUSE:DBLCLICK
canvas.on('mouse:dblclick', function (e) {
  if (btn=='Polyline' || btn=='Polygon') {
    canvas.forEachObject(function(i) { if (i.name=='temp') { canvas.remove(i); } });
    makePolygon();
    addNodes(polyShape); canvas.add.apply(canvas,nodes); info(); //place Line nodes (drag node)
  }
  if (e.target && poly==false && (e.target.name=='poly'|| e.target.name=='node') ) {
    addNodes(polyShape);
    canvas.add.apply(canvas,nodes); info(); //replace with new nodes after dblclick
    canvas.discardActiveObject().renderAll();
  }
  resetToolBar('cursor'); canvas.renderAll();
  isDown=false; poly=false; lastPt=1; //finished so reset 
});
//////////////////////////////////////////////////////////////////// OBJECT:MODIFIED
canvas.on('object:modified', function(e) {
  if (e.target && poly==false && (e.target.name=='poly' || e.target.name=='node')) {
    addNodes(polyShape); pts = []; //put the node points around poly, reset points
    polyShape.get('points').forEach((point, i) => { pts[i]={x:nodes[i].left,y:nodes[i].top}; });
    polyShape.set({redraw:true}); //flag to delete polygon (no duplicates)
    zindex=canvas.getObjects().indexOf(polyShape)
    makePolygon();
    polyShape.moveTo(zindex);
    addNodes(polyShape);
    canvas.add.apply(canvas,nodes); info();
  }
});
canvas.on('selection:created', function() { if (canvas.getActiveObjects().length>1) {redrawAll=true; clearNodes();} });
canvas.on('selection:cleared', function() {   if (redrawAll==true) {redrawAll=false; redrawAllPolys();} });
//canvas.on('object:scaling,object:rotating,object:moving', function() {clearNodes();})
function clearNodes() { nodes.forEach((node, i)=>{canvas.remove(node);}) }

function redrawAllPolys() {
  canvas.getObjects().forEach((obj, i) => {
    if (obj.name=='poly') {
      addNodes(obj); //put the node points around poly
      pts = []; //reset points
      obj.get('points').forEach((point, i) => { pts[i]={x:nodes[i].left,y:nodes[i].top}; });
      obj.set({redraw:true}); //flag to delete polygon (no duplicates)
      zindex=canvas.getObjects().indexOf(obj)
      if (obj.pType) { polyType = obj.pType; } //polygon or polyline
      makePolygon();
      obj.moveTo(zindex);
    } 
  });
};
//////////////////////////////////////////////////////////////////// MAKE POLYGON
function makePolygon() {
  canvas.getObjects().forEach((obj, i) => {
    if (obj.redraw==true) { canvas.setActiveObject(obj); canvas.remove(canvas.getActiveObject()); }
  });
  polyShape = new fabric[polyType](pts,{...Props,name:'poly',redraw:null,objectCaching:false });
  Fill = polyType=='Polygon' ? '#'+Math.random().toString(16).slice(-6) : ''; //random color
  polyShape.set({pType:polyType,hasControls:true,fill:Fill,alphaFill:1,stroke:'black',strokeWidth:1,});
  canvas.add(polyShape);
  
  polyShape.on('mousedown', function(e) {
    polyShape = e.target; //current poly
    if (e.target.get('type') =='polyline') {polyType='Polyline'} else {polyType='Polygon'}
  });
};
//////////////////////////////////////////////////////////////////// ADD NODES
function addNodes(polyShape) {
  clearNodes();
  matrix = polyShape.calcTransformMatrix(); nodeNum=0;
  var transformedpts = polyShape.get('points').map(function(p){
    return new fabric.Point( p.x - polyShape.pathOffset.x, p.y - polyShape.pathOffset.y);
    }).map(function(p){ return fabric.util.transformPoint(p, matrix); });
  
  nodes = transformedpts.map(function(p){
    return new fabric.Circle({...Props,name:'node',nodeNum:nodeNum++,left:p.x,top:p.y,radius:7,fill:'red',hoverCursor:'pointer',selectable:false,opacity:.8});
  });
  
  nodes.forEach((node, i) => {
    node.on('mouseover', function(e) { node.selectable = true; node.set('fill','green'); canvas.renderAll(); });
    node.on('mouseout', function(e) { node.selectable = false; node.set('fill','red'); canvas.renderAll(); });
  });
 };
 //////////////////////////////////////////////////////////////////// UI DISPLAY 
$('#toolBarTable').on('click', 'tr:first-child td', function(e) {
  resetToolBar(this.id);
  btn =  $(this).text();
  clearNodes();
  if (btn=='Cursor') { polyType=''; poly=false; }
  if (btn=='Polyline') { polyType='Polyline'; poly=true; pts=[]; lastPt=1; }
  if (btn=='Polygon') { polyType='Polygon'; poly=true; pts=[]; lastPt=1; }
});

function resetToolBar(id) {
  $('#toolBarTable td').css( {'background-color':'white','color':'black'} );
  $('#'+id).css( {'background-color':'green','color':'white' }); btn='Cursor';
}
 //////////////////////////////////////////////////////////////////// HTML TABLE
 function info() {
  $('.info td').each(function (i) {
    if(i==0 && polyShape) {$(this).text('nodes:'+nodes.length)};
    if(i==1 && pts) $(this).text('points:'+nodes.length);
    if(i==2) $(this).text('objects:'+canvas.getObjects().length);
    if(i==3 && polyShape) $(this).text('originX:'+parseInt(polyShape.left));
    if(i==4 && polyShape) $(this).text('originY:'+parseInt(polyShape.top));
  })
}
}); //end ready
</script>

<style>
* {user-select: none;}
#toolBarTable {
  margin: 20px auto;
  border-collapse: collapse;
  font-family: Arial, Helvetica, sans-serif;
  width: 600px;
}
#toolBarTable tr:first-child td {
  padding: 4px;
  text-align: center;
  border: 1px black solid;
  min-width: 120px;
  cursor: pointer;
}
#td2 {
  max-width:240px;
  pointer-events: none; 
}
#toolBarTable tr:first-child td:hover{
  background-color: Gainsboro;
}
.info td {
  padding: 10px;
}
</style>
</head>

<body ondragstart="return false;" ondrop="return false;">
  <table id="toolBarTable">
    <tr>
      <td id="cursor">Cursor:</td>
      <td colspan="2" id="td2">dblclick poly for nodes</td>
      <td id="polyline">Polygon</td>
      <td id="polygon">Polyline</td>
    </tr>
    <tr>
      <td colspan="5">
        <canvas id="canvas" width="600" height="400" style="border:1px solid black"></canvas></td>
    </tr>
    <tr class="info">
      <td>nodes:</td>
      <td>points:</td>
      <td>objects:</td>
      <td>originX:</td>
      <td>originY:</td>
    </tr>
    <tr>
      <td colspan="5">
        <div id="debug">double click to close polygon</div>
      </td>
    </tr>
  </table>  

</body>
</html>

http://codepen.io/Rstewart/details/VwNBZwJ


Solution

  • After several weeks of trying to do it via Fabric's Matrix, I gave up and decided to just redraw the polygon every time it was modified. It works perfectly when moved, scaled, rotated, resized or the points moved by mouse. Maybe the Fabric team can rework their demo (that does not work when flipped) and use my method? NOTE: The complete and much better VERSION 2 link is in Comments after this code. Please use it for your projects.

    JSfiddle: Here's how to do it the easy way

    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <title>Fabric.js Polygon move,flip,scale,resize,rotate by Robert W. Stewart</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script type="text/javascript" src="https://unpkg.com/fabric@latest/dist/fabric.js"></script>
    <script>
        $(document).ready(function() {
      var canvas,points,shape,matrix,nodeNum=0,mouse,nodeX,nodeY,nodes;
      canvas = new fabric.Canvas($('canvas')[0],{ objectCaching:false,preserveObjectStacking:true,perPixelTargetFind:true });
    
    canvas.on('mouse:move', function(e){
      mouse = canvas.getPointer(e);
      $('#debug').text('x:'+parseInt(mouse.x)+' y:'+parseInt(mouse.y));
    });
    
    canvas.on('object:moving', function (e) {
      if (e.target && e.target.get('type')=='circle') {
        var node = e.target;
        nodeX = node.getCenterPoint().x; nodeY = node.getCenterPoint().y;
        shape.points[node.nodeNum] = {x:nodeX, y:nodeY};
      }
    });
    
    canvas.on('mouse:up', function(e) {
      if (e.target && e.target.get('type')=='circle') { resetPoints(); }
    });
    
    points = [{"x":60,"y":40},{"x":100,"y":60},{"x":100,"y":100},{"x":60,"y":120},{"x":20,"y":100},{"x":20,"y":60}];
    makePolygon();
    
    function resetPoints() {
      points.forEach((point, i) => { points[i]={x:nodes[i].left,y:nodes[i].top}; });
      makePolygon(); 
    }
    
    function makePolygon(e) {
      shape = new fabric.Polygon(points,{selectable:true,objectCaching:false});
      addNodes(shape);
      
      nodes.forEach((node, i) => {
        node.on('mouseover', function(e) { node.selectable = true; node.set('fill','green'); canvas.renderAll(); });
        node.on('mouseout', function(e) { node.selectable = false; node.set('fill','red'); canvas.renderAll(); });
      });
      
      shape.on('mousedown', function(e) {nodes.forEach((node, i) => { canvas.remove(node); })});
      shape.on('modified', function() { addNodes(this); resetPoints() });
    }
    
    function addNodes(shape) {
      matrix = shape.calcTransformMatrix(); nodeNum=0;
      var transformedPoints = shape.get("points").map(function(p){
        return new fabric.Point( p.x - shape.pathOffset.x, p.y - shape.pathOffset.y);
        }).map(function(p){ return fabric.util.transformPoint(p, matrix); });
      
      nodes = transformedPoints.map(function(p){
        return new fabric.Circle({nodeNum:nodeNum++,shape:'node',left:p.x,top:p.y,radius:9,fill:"red",originX:"center",originY:"center",
        hoverCursor:'pointer',objectCaching:false,hasControls:false,hasBorders:false,selectable:false,opacity:.5});
      });
      canvas.clear().add(shape).add.apply(canvas,nodes).renderAll();
    };
    
    }); //end ready
    
    </script>
    </head>
    
    <body>
      <canvas id="canvas" class="" width="640" height="512" style="border:1px solid black"></canvas>
      <div id="debug" style="width: 640px; padding: 5px 0 0 5px;">debug:</div>
    </body>
    </html>