You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Fabric.js:移动多边形节点后边界框不更新问题及解决

Fabric.js 多边形节点移动后边界框不更新的解决方法

问题说明

移动Fabric.js多边形(polygon)的节点(node)时,边界框(bounding box)无法自动更新以包围所有新的点。尝试过站内所有矩阵操作、setCoords()等方法,耗时多日仍未解决;Fabric官方演示中多边形翻转功能已失效3年多未修复。最初仅需用少量代码实现多边形节点移动、位置平移功能(示例未包含旋转和缩放)。

变通修复方案

已实现完整的多边形/折线(polyline)编辑功能,支持绘制、节点移动、平移、缩放、翻转、旋转。完整代码如下:

<!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>

内容的提问来源于stack exchange,提问作者Robbie Stewart

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 02:52:36