Back to Demos

This demo shoes how you can attach an object to another.

Click to select a box to enable nodeMove command to move the box. The attach will happen when the attach point (yellow sphere) in two boxes are close enough.

Clara.io demo resources:

Visit Base Scene


var sceneId = '487e794c-5b75-467f-a278-baf1c5d85b0d';

var groupIdAttachIdsMap = {};
var attachPointLocation = {};
var attach = {};
var attachHintId;
var baseGroupId;
var api = claraplayer('player');
var THREE = api.deps.THREE;
var opts = {
  displayGizmo: false,
  mode: 'plane',
  plane: {normal: {x: 0, y: 1, z: 0}, constant: 0},
}

api.sceneIO.fetchAndUse(sceneId).then(function() {
  attachHintId = api.scene.find({type: 'Null', name: 'HintGroup'});
  baseGroupId = api.scene.find({type:'Null', name: 'BaseGroup'});
  var attachGroupIds = api.scene.filter({type: 'Null', name: 'BoxGroup'});
  attachGroupIds.forEach(function(id) {
    groupIdAttachIdsMap[id] = filterAttachPointId(id);
    updateAttachPointPosition(id)
  });
  toggleAllAttachPoints(false);
  api.player.showTool('nodeMove');
  api.player.removeTool('select');
  api.selection.setHighlighting(true);

  api.commands.addCommand({
    enabled: true,
    active: true,
    tool: {
      mousedown: function(ev) {
        if (!api.selection.lastSelectedNode()) return;
        toggleAllAttachPoints(true);
      },

      mouseup: function(ev) {
        var groupId = api.selection.lastSelectedNode();
        if (!groupId) return;
        if (attach.canAttach) attachObject(attach.moveGroupId, attach.moveAttachId);

        //always hide hint object and show select object when mouse up
        api.selection.setHighlighting(true);
        api.scene.setAll({from: {id: attachHintId}, type: 'PolyMesh', plug: 'Properties', property: 'visible'}, false);
        api.scene.setAll({from: {id: groupId}, type: 'PolyMesh', plug: 'Properties', property: 'visible'}, true);
        toggleAllAttachPoints(false);
      },

      click: function(ev) {
        var selectId = api.player.filterNodesFromPosition(ev);
        var groupId = findParentGroupId(selectId[0]);

        //active nodeMove tool if box are selected, otherwise active orbit tool
        if (!selectId || !groupId) {
          api.selection.deselectAll();
          api.commands.activateCommand('orbit');
        } else {
          api.selection.selectNode(groupId);
          api.commands.activateCommand('nodeMove');

          //If defined, onChange will be called at every frame by nodeMove tool
          opts.onChange = moveCheck(groupId);
          api.commands.setCommandOptions('nodeMove', opts);
        }
      },
    },
  });
  document.getElementById('baseScene').setAttribute('href','https://clara.io/view/'+sceneId);
});

function filterAttachPointId(groupId){
  var attachGroupIds = api.scene.find({from: {id: groupId}, type: 'Null', name: 'AttachGroup'});
  return api.scene.filter({from: {id: attachGroupIds}, type: 'Null'});
};

function updateAttachPointPosition(groupId){
  attachPointLocation[groupId] = {};
  var pointIds = groupIdAttachIdsMap[groupId];
  if (pointIds) {
    pointIds.forEach(function(id){
      var transformInfo = api.scene.getWorldTransform(id);
      attachPointLocation[groupId][id] = new THREE.Vector3().setFromMatrixPosition(transformInfo);
    });
  }
};

function findParentGroupId(selectId){
  var groupId = selectId;
  var nodeName = api.scene.get({id: groupId, property: 'name'});
  while ( nodeName !== 'BoxGroup' ){
    if (!nodeName) return null;
    groupId = api.scene.find({id: groupId, parent: true});
    nodeName = api.scene.get({id: groupId, property: 'name'});
  }
  return groupId;
};

function toggleAllAttachPoints( opts ){
  for (var groupId in groupIdAttachIdsMap) {
    attachGroupIds = api.scene.find({from: {id: groupId}, name: 'AttachGroup'});
    api.scene.setAll({from: {id: attachGroupIds}, type: 'PolyMesh', plug: 'Properties', property: 'visible'}, opts);
  }
};

// In this demo, the moveCheck function will to called at every frame to check the attach
function moveCheck(groupId){
  return function(ev){
    updateAttachPointPosition(groupId);
    checkAttach(1);
    attachHint();
  };
};

/**
 * if move object can attach
 * show hint object, hide move object and attach the hint object to the corresponding position
 * if move object can not attach
 * hide hint object and show move object 
*/
function attachHint(){
  var groupId = api.selection.lastSelectedNode();
  if (attach.canAttach) {
    api.selection.setHighlighting(false);
    api.scene.setAll({from: {id: attachHintId}, type: 'PolyMesh', plug: 'Properties', property: 'visible'}, true);
    api.scene.setAll({from: {id: groupId}, type: 'PolyMesh', plug: 'Properties', property: 'visible'}, false);
    let attachPointName = api.scene.get({id: attach.moveAttachId, property: 'name'});
    hintAttachPointId = api.scene.find({from: {id: attachHintId}, name: attachPointName});
    attachObject(attachHintId, hintAttachPointId);
  } else {
    api.selection.setHighlighting(true);
    api.scene.setAll({from: {id: attachHintId}, type: 'PolyMesh', plug: 'Properties', property: 'visible'}, false);
    api.scene.setAll({from: {id: groupId}, type: 'PolyMesh', plug: 'Properties', property: 'visible'}, true);
  }
};

function checkAttach(distance){
  var groupId = api.selection.lastSelectedNode();
  var dSquare = distance * distance;
  attach = {
    canAttach: false,
    minDistance: dSquare * 2,
    moveGroupId: '',
    moveAttachId: '',
    targetGroupId: '',
    targetAttachId: '',
  };
  var moveAttachIds = groupIdAttachIdsMap[groupId];

  for (var attachId in groupIdAttachIdsMap) {
    if (attachId === groupId) continue;

    var targetAttachIds = groupIdAttachIdsMap[attachId];

    moveAttachIds.forEach(function(moveId){
      targetAttachIds.forEach(function(targetId){
        var moveAttachLocation = attachPointLocation[groupId][moveId];
        var targetAttachLocation = attachPointLocation[attachId][targetId];
        var d = distanceSquare(moveAttachLocation, targetAttachLocation);
        if (d <= dSquare && d < attach.minDistance) {
          attach.canAttach = true;
          attach.minDistance = d;
          attach.moveGroupId = groupId;
          attach.targetAttachId = attachId;
          attach.moveAttachId = moveId;
          attach.targetAttachId = targetId;
        }
      });
    });
  }
};

function distanceSquare(v1, v2){
  var dx = v1.x - v2.x;
  var dy = v1.y - v2.y;
  var dz = v1.z - v2.z;
  return dx*dx + dy*dy + dz*dz;
}

/**
 * attachObject function will attach the moveObject to the targetObject
 * by applying the vector between the attach point of move object and the 
 * attach point of the target object to the moving object. Which will result 
 * that two attach point have the same world coordinate.
*/
function attachObject(moveGroupId, moveAttachId){
  var moveObjectMatrix = api.scene.getWorldTransform(moveGroupId); 
  var moveAttachMatrix = api.scene.getWorldTransform(moveAttachId); 
  var targetAttachMatrix = api.scene.getWorldTransform(attach.targetAttachId); 
  var moveObjectPosition = new THREE.Vector3().setFromMatrixPosition(moveObjectMatrix);
  var moveAttachPointPosition = new THREE.Vector3().setFromMatrixPosition(moveAttachMatrix);
  var targetAttachPointPosition = new THREE.Vector3().setFromMatrixPosition(targetAttachMatrix);
  targetAttachPointPosition.sub(moveAttachPointPosition).add(moveObjectPosition);
  api.scene.set({id: moveGroupId, plug: 'Transform', property: 'translation'}, targetAttachPointPosition);
  updateAttachPointPosition(moveGroupId);
}