7 Jul 2019

Move Entities of 2D Drawing

Follow @Xiaodong Liang

Note:

  • the code demo in this blog is NOT official methods. You would use at your own risk.

In the last blog, we introduced the way to get geometries of 2D curves by VertexBufferReader.enumGeomsForObject. A further question is asked frequently: how to move 2D entities? As mentioned in the last blog, it does not work if using the same workflow to manipulate 2D entities like 3D object. There is not either yet official method to transform 2D entities. We have logged the wish.

 

It is not easy unlike in 3D, while it is still possible to implement by ourselves with the clues of VertexBufferReader because the vertex values of the geometries is available. We could either try with

#1 hide the original geometries and create a new 2D vertex buffer at the new position.

#2 or modify the vertex buffer(s) in place to move the geometry for just the one object

 

For #1, we could simply reuse the code in the last blog, move the copied geometries, hide the original entities, but I have not figured out if it is a good idea to add copied geometries to the built-in vertex buffer. And it looks to me every time of moving will cause redundant data (hidden entities), unless we also take care of popping the original buffer .

 

For a line segment (on desktop machine) , there will be four vertices that make a quad which corresponds to the line segment. The code moves all of them.

line

Triangles are encoded in three vertices (like a simple mesh) instead of 4 like everything else. It might be possible some triangles are connected in the buffer. They share some vertices. So we need to ensure transform the vertices one time only.

tri

Since VertexBufferReader tells the vertex buffer, it sounds more feasible with way #2. VertexBufferReader.enumGeomsForObject exposes the methods to get primitives of the geometries, unfortunately but they are cloned data from original vertex buffer. And it only tells the logic data, instead of full data that represents the geometry. In addition, RenderProxy.geometry.vb.buffer is the original data, but no method tells those vertices which belong to one entity (dbId).

 

So, I re-used the skeleton of VertexBufferReader.enumGeomsForObject. It iterates the buffer and filter out the start index of the vertices of specific entity (dbId). Next, some functions are created to transform the vertices. The code below is an extension to play with moving 2D entities. The live demo is at: 

https://forge-viewer-unit-test.herokuapp.com/ent-2d/move.html

After model is loaded, click key ‘S’, select one curve, a cross indicator will shown up. Move mouse, the curve will be moved accordingly. Click ‘ESC’, you can select another curve and move. Click ‘Q’, the moving will be disable.

 

Note:

  • the code demo in this blog is NOT official methods. You would use at your own risk.
  • The code does not demo how to rotate, while it would be similar to moving.
  • The code does not work if the entity has no fragments.
  • The code only tries with Line/Arc/Circle/Triangles.
  • The encoding of buffer is different depending on platform. The code demo is tested on desktop only.
//copy from VertexBufferReader.js
var VBB_GT_TRIANGLE_INDEXED = 0,
    VBB_GT_LINE_SEGMENT     = 1,
    VBB_GT_ARC_CIRCULAR     = 2,
    VBB_GT_ARC_ELLIPTICAL   = 3,
    VBB_GT_TEX_QUAD         = 4,
    VBB_GT_ONE_TRIANGLE     = 5;

    var TAU = Math.PI * 2; 
//end copy

//copy some codes from VertexBufferReader.enumGeomsForObject

Autodesk.Viewing.Private.VertexBufferReader.prototype.transformObject = function(dbId,trans){

  if (this.useInstancing) {

          ////////
          //TODO....
          ////////
  }
  else {

      var i = 0;

      //check the continuous triangles, to avoid double translating the same vertex of triangle
      var ibArrayToMove =[];

      while (i < this.ib.length) {
          var vi = this.ib[i];
          var flag = this.getVertexFlagsAt(vi);

          //var vertexId    = (flag >>  0) & 0xff;        //  8 bit
          var geomType    = (flag >>  8) & 0xff;        //  8 bit
          //var linePattern = (flag >> 16) & 0xff;        //  8 bit
          var layerId     = this.getLayerIndexAt(vi);    // 16 bit
          var vpId        = this.getViewportIndexAt(vi); // 16 bit

          var visible = this.getDbIdAt(vi) === dbId; 

          if (geomType === VBB_GT_TRIANGLE_INDEXED) { 
              
              //Triangles are encoded in three vertices (like a simple mesh) instead of 4 like everything else 
                 
              if (visible) {
                  ibArrayToMove.push(this.ib[i]);
                  ibArrayToMove.push(this.ib[i+1]);
                  ibArrayToMove.push(this.ib[i+2]);
                  
                  //do not translate at this moment because we have not yet known if the next primitive is
                  //one more triangle
               }

              //Advance to the next primitive
              i += 3;

          } else {

              if(ibArrayToMove.length>0){
                //remove duplicated vertices 
                let unique_ibArrayToMove = [...new Set(ibArrayToMove)]; 
                //translate all vertices now
                this.transformTriangleIndexed(unique_ibArrayToMove, layerId, vpId, trans); 

                //reset the array for next continuous triangle
                //good way to clear an array? no memory leak?
                //see https://stackoverflow.com/questions/1232040/how-do-i-empty-an-array-in-javascript
                ibArrayToMove=[];
              }
              

              if (visible) { 

                  switch (geomType) {
                      case VBB_GT_LINE_SEGMENT:   this.transformLine( vi, layerId, vpId,trans);break;
                      case VBB_GT_ARC_CIRCULAR:   this.transformCircleArc(  vi, layerId, vpId, trans); break;
                      case VBB_GT_ARC_ELLIPTICAL:    //TODO break;
                      //case VBB_GT_TEX_QUAD:            //TODO break;
                      //case VBB_GT_ONE_TRIANGLE:        //TODO break;
                      default:                         break;
                  }
              }

              //Skip duplicate vertices (when not using instancing and the geometry is not a simple polytriangle,
              //each vertex is listed four times with a different vertexId flag
              i += 6;
          } 

        }
      }


      //update the source buffer of the vertex
      this.vb = this.vbf.buffer;  
} 

//custom function: transform vertex of a line
//layer, vpId: reserved arguments
Autodesk.Viewing.Private.VertexBufferReader.prototype.transformLine = function(vindex, layer, vpId,trans){

  var baseOffset = this.stride * vindex;

  //For a line segment (on desktop machine) 
  //there will be four vertices that make a quad which corresponds to the line segment. 
  
  var i = 0;
  while(i<4){
    console.log('x:' + this.vbf[baseOffset + i*this.stride])
    console.log('y:' + this.vbf[baseOffset + i*this.stride + 1])
    this.vbf[baseOffset + i*this.stride] += trans.x;
    this.vbf[baseOffset + i*this.stride + 1] += trans.y;  
    i++;
  }  
}

//custom function: transform vertex of a circle arc & circle 
//layer, vpId: reserved arguments 
Autodesk.Viewing.Private.VertexBufferReader.prototype.transformCircleArc = function(vindex, layer, vpId,trans){

  var baseOffset = this.stride * vindex;
  
  //arc or circle is also fit by line segments.
  // For a line segment (on desktop machine) 
  //there will be four vertices that make a quad which corresponds to the line segment. 
  var i = 0;
  while(i<4){
    this.vbf[baseOffset + i*this.stride] += trans.x;
    this.vbf[baseOffset + i*this.stride + 1] += trans.y; 
    i++;
  }  
} 
 

//custom function: transform vertex of triangles
//layer, vpId: reserved arguments 
Autodesk.Viewing.Private.VertexBufferReader.prototype.transformTriangleIndexed = 
    function(ibArray, layer, vpId, trans)
{
    var k = 0;
    while(k<ibArray.length){
      var baseOffset = this.stride * ibArray[k]; 
      this.vbf[baseOffset] += trans.x;
      this.vbf[baseOffset + 1] += trans.y;   
      k++;
    } 

 };

function My2DMove(viewer, options) {

    Autodesk.Viewing.Extension.call(this, viewer, options)

    var _viewer = this.viewer
    var _container = _viewer.canvas.parentElement  

    var indicatorLines = null
    var indicatorLayerName = 'indicatorLayer'
    var indicatorLineBias = 0.2
    //is mouse moving
    var _dragging = false
    //selected object id
    var _selectDbId = 0
 
    //current point of when mouse is moving 
    var _mouseStPt = new THREE.Vector3(0,0,0); 
    //last mouse point
    var _mouseLastPt = new THREE.Vector3(0,0,0); 

    //when extension is loaded
    this.load = function() {
        console.log('My2DMove is loaded!'); 
        //bind keyup event
        $(document).bind('keydown', onKeyUp); 
        _viewer.impl.invalidate(true);  
        return true;
    };

    //when extension is unloaded 
    this.unload = function() {
        console.log('My2DMove is now unloaded!');
        //unbind keyup event
        $(document).unbind('keyup', this.onKeyUp);
        return true;
    }; 

    //when key up
    function onKeyUp(evt) {  
        console.log('onKeyUp:' + evt.keyCode); 
  
        //when key 'S' is pressed
        if(evt.keyCode == 83){  
           //start to monitor mouse down
          _container.addEventListener('click',onMouseClick)
          _viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, onSelectObj) 
        }
      
        //when key 'Q' is pressed
        if(evt.keyCode == 81){ 
          _viewer.removeEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, onSelectObj) 

           //remove mouse events
          _container.removeEventListener('click',onMouseClick); 
          _container.removeEventListener('mousemove',onMouseMove);  
  
          _viewer.impl.removeOverlayScene(indicatorLayerName);
          _viewer.impl.invalidate(false, false, true);  
          _dragging = false 
         }
      
        return true;
      } 

      function onSelectObj(evt){ 
             console.log(evt)
            _selectDbId = evt.dbIdArray[0]
            _container.addEventListener("mousemove", onMouseMove)
        } 

      function onMouseClick(evt) {  
        if(!_dragging){ 
          var pt_3d = _viewer.clientToWorld(evt.clientX,evt.clientY)
          createIndicator(pt_3d.point)
          _dragging = true
        } 
      }

      function onMouseMove(evt) {   
        if(_dragging){ 
          //get mouse points 
          var pt_3d = _viewer.clientToWorld(evt.clientX,evt.clientY)  
          moveIndicator(pt_3d.point) 
        }
      } 

      function createIndicator(pt){

        var geometry = new THREE.Geometry () 
        geometry.vertices.push (new THREE.Vector3 ( pt.x-indicatorLineBias,  pt.y,  0))
        geometry.vertices.push (new THREE.Vector3 ( pt.x+indicatorLineBias, pt.y, 0))
        geometry.vertices.push (new THREE.Vector3 ( pt.x, pt.y-indicatorLineBias, 0))
        geometry.vertices.push (new THREE.Vector3 ( pt.x, pt.y+indicatorLineBias, 0)) 

        var linesMaterial = new THREE.LineBasicMaterial ({
                                color: new THREE.Color (0xFF0000),
                                transparent: true,
                                depthWrite: false,
                                depthTest: false,
                                linewidth: 3,
                                opacity: 1.0
                                })
          
        indicatorLines = new THREE.Line (geometry,
                linesMaterial,
                THREE.LinePieces)

        _viewer.impl.createOverlayScene (indicatorLayerName, linesMaterial) 
        _viewer.impl.addOverlay (indicatorLayerName, indicatorLines) 
        _viewer.impl.invalidate (false,false,true) 

        _mouseLastPt.x = _mouseStPt.x = pt.x
        _mouseLastPt.y = _mouseStPt.y = pt.y  
    }

    function moveIndicator(pt){

      if(indicatorLines != null){

        var diffX = pt.x - _mouseStPt.x
        var diffY = pt.y - _mouseStPt.y 
 
        indicatorLines.position.x = diffX
        indicatorLines.position.y = diffY

        _viewer.impl.invalidate(false, false, true)

        move2DEnt = move2DEnt(_selectDbId,{x:pt.x - _mouseLastPt.x,y:pt.y - _mouseLastPt.y })
        _mouseLastPt = pt 
      }


      function move2DEnt(dbId,trans){
        var it = _viewer.model.getData().instanceTree;
        it.enumNodeFragments( dbId, function( fragId ) {
            var m = _viewer.impl.getRenderProxy(_viewer.model, fragId);
            var vbr = new Autodesk.Viewing.Private.VertexBufferReader(m.geometry, 
                     _viewer.impl.use2dInstancing);
            vbr.transformObject(dbId,trans); 
            m.geometry.vbNeedsUpdate = true;
            _viewer.impl.sceneUpdated() 
        });
      }  
       
  }
}

My2DMove.prototype = Object.create(Autodesk.Viewing.Extension.prototype);
My2DMove.prototype.varructor = My2DMove; 
  
Autodesk.Viewing.theExtensionManager.registerExtension('My2DMove', My2DMove);

 

Related Article