Fidel90
Fidel90

Reputation: 1838

How to select covered objects via mouse in fabricJS?

I'm trying to develop a way to select objects that are layered below and (totally) covered by other objects. One idea is to select the top object and then via doubleclick walk downwards through the layers. This is what I got at the moment:

var canvas = new fabric.Canvas("c");

fabric.util.addListener(canvas.upperCanvasEl, "dblclick", function (e) {
  var _canvas = canvas;
  var _mouse = _canvas.getPointer(e);
  var _active = _canvas.getActiveObject();
    
  if (e.target) {
    var _targets = _canvas.getObjects().filter(function (_obj) {
      return _obj.containsPoint(_mouse);
    });
      
    //console.warn(_targets);
      
    for (var _i=0, _max=_targets.length; _i<_max; _i+=1) {
      //check if target is currently active
      if (_targets[_i] == _active) {
       	//then select the one on the layer below
       	_targets[_i-1] && _canvas.setActiveObject(_targets[_i-1]);
         break;
        }
      }
    }
});

canvas
  .add(new fabric.Rect({
    top: 25,
    left: 25,
    width: 100,
    height: 100,
    fill: "red"
  }))
  .add(new fabric.Rect({
    top: 50,
    left: 50,
    width: 100,
    height: 100,
    fill: "green"
  }))
  .add(new fabric.Rect({
    top: 75,
    left: 75,
    width: 100,
    height: 100,
    fill: "blue"
  }))
  .renderAll();
canvas {
 border: 1px solid;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.3/fabric.min.js"></script>
<canvas id="c" width="300" height="200"></canvas>

As you can see, trying to select the red rectangle from within the blue one is not working. I'm only able to select the green or the blue. I guess that after the first doubleclick worked (green is selected), clicking again just selects blue so the following doubleclick is only able to get green again.

Is there a way around this? Any other ideas?

Upvotes: 5

Views: 4999

Answers (3)

Rohit Tagadiya
Rohit Tagadiya

Reputation: 3738

Try perPixelTargetFind

When fdirst creating the object, add an additional property:

perPixelTargetFind: true

Docs: http://fabricjs.com/docs/fabric.Object.html#perPixelTargetFind

When set to true, objects are "found" on canvas on per-pixel basis rather than according to bounding box

enter image description here

Upvotes: 4

rakasha
rakasha

Reputation: 11

My task is a bit different - the mission is to pick the overlapping obj behind the current one:

  1. [Left-click]+[Cmd-Key] on an selected-object to pick the first overlapping object which is right behind it.
  2. If no overlapping objs are found, restart the search from top-layer

The idea is, for each click-event, intercept the selected object and replace it with our desired object.

A Fabric mouse-down event is like this:

  1. User clicks on canvas
  2. On canvas: find the target-obj by the coordinates of mouse cursor and stores it in the instance-variable (canvas._target)
  3. Run event-handlers for mouse:down:before
  4. Compare the target-obj found from step(2) with current selected object, fire selection:cleared/update/create events according to the results of comparison.
  5. Set new activeObject(s)
  6. Run event-handlers for mouse:down

We can use a customized event handler on mouse:down:before to intercept the target-obj found on Step(2) and replace it by our desired-object

fCanvas = new fabric.Canvas('my-canvas', {
  backgroundColor: '#cbf1f1',
  width: 800,
  height: 600,
  preserveObjectStacking: true
})

const r1 = new fabric.Rect({ width: 200, height: 200, stroke: null, top: 0, left: 0, fill:'red'})
const r2 = new fabric.Rect({ width: 200, height: 200, stroke: null, top: 50, left: 50, fill:'green'})
const r3 = new fabric.Rect({ width: 200, height: 200, stroke: null, top: 100, left: 100, fill:'yellow'})

fCanvas.add(r1, r2, r3)
fCanvas.requestRenderAll()

fCanvas.on('mouse:down:before', ev => {
  if (!ev.e.metaKey) {
    return
  }
  // Prevent conflicts with multi-selection
  if (ev.e[fCanvas.altSelectionKey]) {
    return
  }
  const currActiveObj = fCanvas.getActiveObject()
  if (!currActiveObj) {
    return
  }

  const pointer = fCanvas.getPointer(ev, true)
  const hitObj = fCanvas._searchPossibleTargets([currActiveObj], pointer)
  if (!hitObj) {
    return
  }

  let excludeObjs = []
  if (currActiveObj instanceof fabric.Group) {
    currActiveObj._objects.forEach(x => { excludeObjs.push(x) })
  } else {
    // Target is single active object
    excludeObjs.push(currActiveObj)
  }

  let remain = excludeObjs.length
  let objsToSearch = []
  let lastIdx = -1
  const canvasObjs = fCanvas._objects

  for (let i = canvasObjs.length-1; i >=0 ; i--) {
    if (remain === 0) {
      lastIdx = i
      break
    }
    const obj = canvasObjs[i]
    if (excludeObjs.includes(obj)) {
      remain -= 1
    } else {
      objsToSearch.push(obj)
    }
  }

  const headObjs = canvasObjs.slice(0, lastIdx+1)
  objsToSearch = objsToSearch.reverse().concat(headObjs)
  const found = fCanvas._searchPossibleTargets(objsToSearch, pointer)
  if (found) {
    fCanvas._target = found
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.3.1/fabric.min.js"></script>
<html>
<h4>Left-click + Cmd-key on overlapping area to pick the obj which is behind current one</h4>
<canvas id="my-canvas"></canvas>

</html>

Upvotes: 1

Fidel90
Fidel90

Reputation: 1838

After some time I finally was able to solve that by myself. Clicking on an object brings it to the top. On double-clicking I try to get the object one layer behind the current object. On another dblclick I get the one behind and so on. Works great for me and also allows for the selection of fully covered objects without the need to move others.

var canvas = new fabric.Canvas("c");

canvas.on("object:selected", function (e) {
  if (e.target) {
    e.target.bringToFront();
    this.renderAll();
  }
});

var _prevActive = 0;
var _layer = 0;

//
fabric.util.addListener(canvas.upperCanvasEl, "dblclick", function (e) {
    var _canvas = canvas;
    //current mouse position
    var _mouse = _canvas.getPointer(e);
    //active object (that has been selected on click)
    var _active = _canvas.getActiveObject();
    //possible dblclick targets (objects that share mousepointer)
    var _targets = _canvas.getObjects().filter(function (_obj) {
        return _obj.containsPoint(_mouse) && !_canvas.isTargetTransparent(_obj, _mouse.x, _mouse.y);
    });
    
    _canvas.deactivateAll();
      
    //new top layer target
    if (_prevActive !== _active) {
        //try to go one layer below current target
        _layer = Math.max(_targets.length-2, 0);
    }
    //top layer target is same as before
    else {
        //try to go one more layer down
        _layer = --_layer < 0 ? Math.max(_targets.length-2, 0) : _layer;
    }

    //get obj on current layer
    var _obj = _targets[_layer];

    if (_obj) {
    	_prevActive = _obj;
    	_obj.bringToFront();
    	_canvas.setActiveObject(_obj).renderAll();
    }
});

//create something to play with
canvas
  //fully covered rect is selectable with dblclicks
  .add(new fabric.Rect({
    top: 75,
    left: 75,
    width: 50,
    height: 50,
    fill: "black",
    stroke: "black",
    globalCompositeOperation: "xor",
    perPixelTargetFind: true
  }))
  .add(new fabric.Circle({
    top: 25,
    left: 25,
    radius: 50,
    fill: "rgba(255,0,0,.5)",
    stroke: "black",
    perPixelTargetFind: true
  }))
  .add(new fabric.Circle({
    top: 50,
    left: 50,
    radius: 50,
    fill: "rgba(0,255,0,.5)",
    stroke: "black",
    perPixelTargetFind: true
  }))
  .add(new fabric.Circle({
    top: 75,
    left: 75,
    radius: 50,
    fill: "rgba(0,0,255,.5)",
    stroke: "black",
    perPixelTargetFind: true
  }))
  .renderAll();
canvas {
 border: 1px solid;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.4/fabric.min.js"></script>
<canvas id="c" width="300" height="200"></canvas>

Upvotes: 5

Related Questions