dojox.gfx.Moveable convert screen coordinates to world coordinates for dragging

148 Views Asked by At

tl; dr How would I use the CTM returned by:

var ctm = canvas.rawNode.getScreenCTM();

to modify a dx, dy vector in screen coordinates, such that it is in world coordinates? i.e. { dx: 1, dy: 0} should become { dx: 3.6, dy: 0} given the above example of an SVG with viewBox 0 0 1800 1800 in a window 500px wide.

I thought the following would work:

  var ctm = canvas.rawNode.getScreenCTM();
  // Turn this into a dojox/gfx/matrix Matrix2D
  var ctmm = new matrix.Matrix2D({xx: ctm.a, xy: ctm.b, dx: ctm.c,
                                  yx: ctm.d, yy: ctm.e, dy: ctm.f});
  // Invert this
  var itm = matrix.invert(ctmm);
  // Multiply screen coords by transform matrix
  var worldshift = matrix.multiplyPoint(itm, shift.dx, shift.dy);
  console.log('ctm ', ctm, ', shift ', shift, ' became worldshift ', worldshift);
  shift.dx = worldshift.x;
  shift.dy = worldshift.y;

But itm comes out full of NaN and Infinity.

long version of question follows with CodePen samples

I know the basic maths behind this but found myself stumped trying to do it with matrix transformations. The documentation seems to avoid this subject. The situation is:

  • SVG node has viewBox definiting world coordinates, say 0,0 to 1800,1800
  • SVG node is in a document that scales its to the window size, about 500px wide So world coords in the SVG (1800 x 1800 units) do not map 1:1 to screen coords.. each pixel across is 1800/500 = 3.6 world units
  • dojox/gfx/Moveable uses dojox/gfx/Mover whose onMouseMove function passes the amount it moved by in screen coordinates:

    this.host.onMove(this, {dx: x - this.lastX, dy: y - this.lastY});

That last argument is passed into dojox/gfx/Moveable.onMoving as the shift argument and might be e.g. { dx: 1, dy: 0 } if the mouse moved right by a pixel.

If we dumbly allow the framework to apply this to the translation transform of the shape being dragged, its position does not exactly match the mouse coordinates: https://codepen.io/neekfenwick/pen/RxpoMq (this works fine in the dojox demos because their SVG coordinate system matches the screen coordinate system 1:1).

I found some inspiration at http://grokbase.com/t/dojo/dojo-interest/08anymq4t9/gfx-constrainedmoveable where Eugene says "override onMoving on your object and modify the "shift" object so it never moves a shape outside of a specified boundaries.", this seems a good point to modify the shift object, so my next attempts declare a new type of dojox/gfx/Moveable and override onMoving.

I have tried using matrix transformations to get the Screen CTM of the SVG (which comes in an object of { a, b, c, d, e, f }) and use it as a dojox/gfx Matrix2D ({ xx, xy, dx, yx, yy, dy }) using straight matrix operations. The aim is to modify the shift object to convert screen units to world units before it is used on the shape's transformation matrix, but found myself very confused. For a start the CTM seems to have a large dy of about 50 which immediately makes the shape shoot off the bottom of the screen. Here's my latest very messy and broken attempt: https://codepen.io/neekfenwick/pen/EoWNOb

I can manually take the CTM's x and y scale values and apply them to the shift object: https://codepen.io/neekfenwick/pen/KZWapa

How would one use matrix operations such as Matrix2D.invert() and Matrix2D.multiplyPoint() to take the coordinate system of the SVG, product a transformation matrix to convert from screen coordinates to world coordinates, and apply that to the dx,dy that the mouse moved by?

1

There are 1 best solutions below

0
On

I got a working example, by taking only a and d from the DOMMatrix populating only the xx and yy elements of the Matrix2D: https://codepen.io/neekfenwick/pen/mpWgrO

 var MyMoveable = declare(Moveable, {
    onMoving: function(/* dojox/gfx/Mover */ mover, /* Object */ shift){
      // console.log('mover ', mover, ' shift ', shift);
      // shift comes in screen coordinates
      // Get the Screen Coordinate Transform Matrix
      var ctm = canvas.rawNode.getScreenCTM();
      // Turn this into a dojox/gfx/matrix Matrix2D
      var ctmm = new matrix.Matrix2D({xx: ctm.a, yy: ctm.d});
      // Invert this
      var itm = matrix.invert(ctmm);
      // Multiply screen coords by transform matrix
      var worldshift = matrix.multiplyPoint(itm, shift.dx, shift.dy);
      console.log('ctm ', ctm, ', shift ', shift, ' became worldshift ', worldshift);
      // Replace the shift dx,dy vector with the transformed coordinates
      shift.dx = worldshift.x;
      shift.dy = worldshift.y;
    }
  });

With this, the red circle moves in sync with the mouse cursor.

Looks like I was overzealous in using the DOMMatrix attributes, trying to use all elements to fill out the xy, yx etc elements of my Matrix2D to use as the basis of the transformation matrix to use on the mouse movement vector. I've found the documentation https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix rather difficult to follow, because it doesn't lay out the matrix elements in a grid so I thought a, b, c, d, e, f were simply laid out in a 3x3 grid:

{ a, b, c,
  d, e, f,
  0, 0, 1 }

However the '3D Equivalent' part of the docs indicate it's actually some kind of 4x4 matrix (here I use ? for an unspecified element, I'm not sure what the defaults would be):

{ a, b, ?, ?,
  c, d, ?, ?,
  ?, ?, ?, ?,
  e, f, ?, ? }

So I'm still rather confused, but do at least have a solution using Matrix2D operations.

I also tried briefly to use pure SVG operations to create a SVGPoint and transform it using DOMMatrix operations, but the transformation matrix comes out with offsets of 100 or 200 and I've given up on it: https://codepen.io/neekfenwick/pen/BJWEZw