Mercurial > hg > ccmieditor
diff native/Falcon/HapticManager.cpp @ 5:d66dd5880081
Added support for Falcon Haptic device and Tablet/Mouse as haptic device
author | Fiore Martin <fiore@eecs.qmul.ac.uk> |
---|---|
date | Tue, 10 Jul 2012 22:39:37 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/Falcon/HapticManager.cpp Tue Jul 10 22:39:37 2012 +0100 @@ -0,0 +1,550 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "HapticManager.h" + +using namespace HAPI; + +// Render a sphere using OpenGL calls. +void drawSphere() { + GLUquadricObj* pObj = gluNewQuadric(); + gluSphere(pObj, 0.0010, 10, 10); + gluDeleteQuadric(pObj ); +} + +HapticManager::HapticManager(CollectionsManager * cManager, + void (*func)( + const jchar cmd, + const jint ID, + const jdouble startx, + const jdouble starty, + const jdouble endx, + const jdouble endy) + ) + : executeCommand(func), cm(cManager), force_effect(0), last_touched_point(NULL), + last_touched_line(NULL),magnetic_mode(LOOSE), pickup_mode(RELEASED), object_unselected(false), + magnetic_mode_changed(false), pickedup_point(NULL), pickedup_line(NULL) {} + + +bool HapticManager::init (void){ + + hd = new AnyHapticsDevice(); + + // The haptics renderer to use. + hd->setHapticsRenderer( new HAPI::GodObjectRenderer() ); + + /* init the device */ + if(hd->initDevice() != HAPIHapticsDevice::SUCCESS){ + return false; + } + + /* enable the device ( forces and positions will be updated) */ + hd->enableDevice(); + + return true; +} + +/* draws the visual diagram every time it's called. The haptic diagram is redrawn only if * + * something has changed in the diagram ( redraw_haptic_scene = true ) as, unlike openGL, * + * it doesn't need to be drawn at each frame but only once */ +void HapticManager::drawDiagram(bool redraw_haptics_scene, bool pickup, int _attract_to ){ + + /* STOP_ATTRACTION means the object has been reached (or eventually deleted). The attraction * + * force must therefore be stopped and a redrawing of the haptic forces is necessary */ + if(attraction_mode == STOP_ATTRACTION){ + attraction_mode = NO_ATTRACTION; + attract_to = NO_ID; + redraw_haptics_scene = true; + } + + /* _attract_to is different from NO_ID there is no attraction going on (attraction_mode = NO_ATTRACTION)* + * It differs from STOP_ATTRACTION in that the haptic scene doesn't have to be repainted */ + if(_attract_to != NO_ID){ + attract_to = _attract_to; + attraction_mode = ACTIVE; + redraw_haptics_scene = true; + } + + if(magnetic_mode_changed){ + redraw_haptics_scene = true; + magnetic_mode_changed = false; + } + + if(pickup){ + pickup_mode = START_DRAGGING; + redraw_haptics_scene = true; + } + + /* wash before use */ + if(redraw_haptics_scene){ + point_set.clear(); + line_set.clear(); + point_id_map.clear(); + line_id_map.clear(); + } + + /* --- draw the edges --- */ + glPushAttrib(GL_ENABLE_BIT); + glColor3f(1.0f,0.0f,0.0f); // Red + glLineWidth(2.0); + glDisable(GL_LIGHTING); + glEnable(GL_COLOR_MATERIAL); + int numEdges = cm->getEdgesNum(); + for ( int i = 0; i < numEdges; i++){ + /* draw the edge in openGL */ + CollectionsManager::EdgeData & ed = cm->getEdgeData(i); + glPushAttrib(GL_ENABLE_BIT); + glLineStipple(1, ed.stipplePattern); + glEnable(GL_LINE_STIPPLE); + glBegin(GL_LINES); + for(unsigned int j = 0; j < ed.getSize(); j++){ + for(unsigned int k = j; k < ed.getSize(); k++){ + if(ed.adjMatrix[j][k]){ + glVertex3d(ed.x[j]*2,ed.y[j]*GRAPHIC_SCALE,0); + glVertex3d(ed.x[k]*2,ed.y[k]*GRAPHIC_SCALE,0); + } + } + } + glEnd(); + glPopAttrib(); + /* update haptics edge line set if a change in the collection occurred */ + if(redraw_haptics_scene){ + /* build the vector with the edges*/ + for(unsigned int j = 0; j < ed.getSize(); j++){ + for(unsigned int k = j; k < ed.getSize(); k++){ + if(ed.adjMatrix[j][k]){ + line_set.push_back(Collision::LineSegment ( + Vec3(ed.x[j],ed.y[j],0), + Vec3(ed.x[k],ed.y[k],0) + )); + } + } + } + + for(vector< HAPI::Collision::LineSegment>::iterator itr = line_set.begin(); itr != line_set.end(); itr++){ + line_id_map.insert(pair<Collision::LineSegment*,int>(&(*itr),ed.hapticId)); + } + } + } + glPopAttrib(); + + /* --- draw the nodes --- */ + int numNodes = cm->getNodesNum(); + glColor3f(1.0f, 1.0f, 1.0f); // white + for( int i = 0; i < numNodes; i++){ + /* draw the nodes in openGL */ + glPushMatrix(); + CollectionsManager::NodeData &nd = cm->getNodeData(i); + glTranslated(nd.x*GRAPHIC_SCALE,nd.y*GRAPHIC_SCALE, 0); + drawSphere(); + glPopMatrix(); + + /* update haptics node line set if a change in the collection occurred */ + if(redraw_haptics_scene){ + point_set.push_back(Vec3(nd.x,nd.y,0)); + point_id_map.insert(pair<Collision::Point*,int>(&(point_set.back()),nd.hapticId)) ; + } + } + + /* --- update the haptic force if a change in the collection occurred --- */ + if(redraw_haptics_scene){ + /* reset the effects and set up new one, otherwise they would accumulate */ + hd->clearEffects(); + /* first lines */ + if(!line_set.empty()){ + /* force according to the current attraction_mode */ + int force_factor = (magnetic_mode == STICKY) ? LINE_FORCE_FACTOR_STICKY : LINE_FORCE_FACTOR_LOOSE; + /* if the user is dragging or looking for an object, the force * + * factor is always loose until she drops or find the object */ + if(pickup_mode == DRAGGING || pickup_mode == START_DRAGGING || attraction_mode == ACTIVE){ + force_factor = LOOSE; + } + + /* update the force */ + force_effect.reset( new HapticShapeConstraint(new HapticLineSet( line_set, 0 ),force_factor)); + hd->addEffect(force_effect.get()); + + /* recalculate last_touched_line if it was != NULL as the pointed * + object is now destroyed after clear() and replaced with a new one */ + if(last_touched_line != NULL){ + last_touched_line = NULL; + vector<Collision::LineSegment>::iterator itr; + Vec3 closest_point, normal, tex_coord; + for( itr=line_set.begin(); itr != line_set.end(); itr++ ){ + itr->closestPoint(proxy_pos,closest_point,normal,tex_coord); + if(pointsDistance(closest_point,proxy_pos) < LINE_COLLISION_THRESHOLD){ + last_touched_line = &(*itr); + break; + } + } + if(last_touched_line == NULL){ + /* touched line was != NULL and now is NULL. It means it has been deleted * + * while being touched, therefore an unselect command must be issued */ + object_unselected = true; + } + } + }else if(last_touched_line != NULL){ + /* if last_touuched_line was != null it means an edge has been deleted * + * which was being touched, therefore un unselect command must be issued */ + object_unselected = true; + /* if there are no lines there cannot be a last touched one */ + last_touched_line = NULL; + } + + /* now points */ + if(!point_set.empty()){ + /* update the force */ + force_effect.reset( new HapticShapeConstraint(new HapticPointSet( point_set, 0 ),POINT_FORCE_FACTOR ) ); + hd->addEffect(force_effect.get()); + + /* recalculate lastTouchedNode if it was != NULL as the pointed object is now destroyed after clear() */ + if(last_touched_point != NULL){ + last_touched_point = NULL; + vector<Collision::Point>::iterator itr; + for(itr=point_set.begin(); itr != point_set.end(); itr++ ){ + if(pointsDistance(itr->position,proxy_pos) < POINT_COLLISION_THRESHOLD ){ + last_touched_point = &(*itr); + break; + } + } + if(last_touched_point == NULL){ + /* touched point was != NULL and now is NULL. It means it has been deleted * + * while being touched, therefore an unselect command must be issued */ + object_unselected = true; + } + } + }else if(last_touched_point != NULL){ + /* if last_touuched_point was != null it means a node has been deleted * + * which was being touched, therefore un unselect command must be issued */ + object_unselected = true; + /* if there are no points there cannot be a last touched one */ + last_touched_point = NULL; + } + + if(pickup_mode == START_DRAGGING && attraction_mode == NO_ATTRACTION){ + force_effect.reset(new HapticSpring(proxy_pos,SPRING_FORCE_FACTOR)); + hd->addEffect(force_effect.get()); + /* spring force is initialized not pickup_mode goes to DRAGGING*/ + pickup_mode = DRAGGING; + }else if(attraction_mode == ACTIVE){ + bool found = false; + /* let's look for the object into the nodes */ + for(map<HAPI::Collision::Point*,int>::iterator itr = point_id_map.begin();itr != point_id_map.end(); itr++){ + if( (*itr).second == attract_to ){ + force_effect.reset(new HapticSpring(((*itr).first)->position,SPRING_FORCE_FACTOR)); + hd->addEffect(force_effect.get()); + found = true; + break; + } + } + /* if not found look into the edges */ + if(!found){ + for(map<HAPI::Collision::LineSegment*,int>::iterator itr = line_id_map.begin();itr != line_id_map.end(); itr++){ + if( (*itr).second == attract_to ){ + HAPI::Collision::LineSegment* line_ptr = (*itr).first; + force_effect.reset(new HapticSpring( + midPoint(line_ptr->start, line_ptr->end), + SPRING_FORCE_FACTOR)); + hd->addEffect(force_effect.get()); + found = true; + break; + } + } + } + + if(!found){ + /* element has been deleted before user could reach it: go back to normal */ + attraction_mode = STOP_ATTRACTION; + } + + } + /* just deallocate the memory for the last force effect */ + force_effect.reset(); + /* tranfer all the forces to the device */ + hd->transferObjects(); + } +} + + +void HapticManager::drawCursor(){ + HAPIHapticsRenderer *hr = hd->getHapticsRenderer(); + if( hr ) { + /* save the proxy pos in a global variable */ + proxy_pos = static_cast< HAPIProxyBasedRenderer * >(hr)->getProxyPosition(); + + glPushMatrix(); + glPushAttrib(GL_CURRENT_BIT | GL_ENABLE_BIT | GL_LIGHTING_BIT); + glTranslatef( (GLfloat)proxy_pos.x*GRAPHIC_SCALE, + (GLfloat)proxy_pos.y*GRAPHIC_SCALE, + (GLfloat)proxy_pos.z ); + glEnable(GL_COLOR_MATERIAL); + glColor3f(0.0f, 0.5f, 1.0f);// Blue + drawSphere(); + glPopAttrib(); + glPopMatrix(); + } +} + +bool HapticManager::checkNodeCollision(){ + /* if the proxy goes far enough from the last touched node, then * + * lastTouched node can be set back to NULL */ + bool do_unselect = false; + if(last_touched_point != NULL){ + if(pointsDistance(last_touched_point->position, proxy_pos) > POINT_COLLISION_THRESHOLD){ + last_touched_point = NULL; + do_unselect = true; + } + } + + /* find the closest point among those closer than POINT_COLLISION_THRESHOLD */ + vector<Collision::Point>::iterator itr; + HAPI::Collision::Point* found_point = NULL; + double min_dist = POINT_COLLISION_THRESHOLD; + double dist; + for(itr=point_set.begin(); itr != point_set.end(); itr++ ){ + if((dist = pointsDistance(itr->position,proxy_pos)) < min_dist ){ + found_point = &(*itr); + min_dist = dist; + } + } + + if(found_point != NULL){ + /* if the last touched point is the same, it means the cursor was * + * already on it therefore do nothing as the node name is uttered only when * + * the proxy comes across it and not at each frame (it would make a mess) */ + if(last_touched_point != found_point){ + last_touched_point = found_point; + /* Setting last_touched_line to NULL, means untouching the eventual touching line. * + * When a node is touched, the connected lines are indeed untouched as only one object * + * at time can be touching the position proxy */ + last_touched_line = NULL; + int point_id = point_id_map[last_touched_point]; + /* select the node for eventual highlighting int the java tree */ + executeCommand(SELECT_CMD,point_id,0,0,0,0); + /* utter the name of the node */ + executeCommand(SPEAK_NAME_CMD,point_id,0,0,0,0); + if(attraction_mode == ACTIVE && point_id == attract_to){ + /* we reached the attracting node, stop the attraction force */ + attraction_mode = STOP_ATTRACTION; + } + } + return true; + } + + if(do_unselect) + executeCommand(UNSELECT_CMD,0,0,0,0,0); + + return false; +} + +bool HapticManager::checkEdgeCollision(){ + Vec3 closest_point, normal, tex_coord; + + /* id is taken from the last touched line is overwritten. To understand why suppose we have an edge * + * made of two lines A and B. When going from one line to another we don't have to utter the name (see * + * comment below. last_touched_line goes from a to NULL before being set to B. If lastTouchedLineId is * + * calculated before checking for NULL-ness it will become -1 and it won't be useful anymore to avoid * + * uttering the edge name when going from A to B. In this way instead proxy goes from A to B, the id is * + * to A's, last_touched_line becomes NULL and then it becomes B, but the name is not uttered because id * + * is the same. This is a very long comment. I want to finish the line to make it look better. bye bye */ + int lastTouchedLineId = (last_touched_line == NULL) ? - 1 : line_id_map[last_touched_line]; + + bool do_unselect = false; + /* if the proxy goes far enough from the last touched line, then last_touched_line can be set back to NULL */ + if(last_touched_line != NULL){ + last_touched_line->closestPoint(proxy_pos,closest_point,normal,tex_coord); + if(pointsDistance(closest_point, proxy_pos) > LINE_COLLISION_THRESHOLD){ + last_touched_line = NULL; + do_unselect = true; + } + } + + /* find the closest line to the proxy among those closer than LINE_COLLISION_THRESHOLD */ + vector<Collision::LineSegment>::iterator itr; + HAPI::Collision::LineSegment* found_line = NULL; + double min_dist = LINE_COLLISION_THRESHOLD; + double dist; + for( itr=line_set.begin(); itr != line_set.end(); itr++ ){ + itr->closestPoint(proxy_pos,closest_point,normal,tex_coord); + if((dist = pointsDistance(closest_point,proxy_pos)) < min_dist){ + found_line = &(*itr); + min_dist = dist; + } + } + + if(found_line != NULL){ + last_touched_line = found_line; + /* proxy can touch only one object at time either line or point */ + last_touched_point = NULL; + /* An edge can be broken into several lines. Such lines will map to the same edge id * + * If the proxy detouches from a line but immediately touches another line mapped to * + * the same id, it just means the proxy is going aling the line and therefore no * + * name must be uttered again (the name must be uttered when touching an edge and * + * not when touching each line. We keep track of the last touched id and if it's the * + * as te new touched line id, then nothing is uttered, for we're on the same edge */ + if(lastTouchedLineId != line_id_map[last_touched_line]){ + int line_id = line_id_map[last_touched_line]; + /* select the node for eventual highlighting int the java tree */ + executeCommand(SELECT_CMD,line_id,0,0,0,0); + /* utter the name of the edge */ + executeCommand(SPEAK_NAME_CMD,line_id,0,0,0,0); + if(attraction_mode == ACTIVE && line_id == attract_to){ + /* we reached the attracting node, stop the attraction force */ + attraction_mode = STOP_ATTRACTION; + } + } + return true; + } + + if(do_unselect){ + executeCommand(UNSELECT_CMD,0,0,0,0,0); + } + + return false; +} + +bool HapticManager::checkUnselection(void){ + if(object_unselected){ + object_unselected = false; + executeCommand(UNSELECT_CMD,0,0,0,0,0); + return true; + } + return false; +} + +bool HapticManager::checkButtons(void){ + HAPIInt32 status = hd->getButtonStatus(); + /* handle the buttons. Buttons can be pressed one at time. If a button is pressed all the * + * other buttons (either when pressed or released) are ignored untill the button is released */ + if(pressed_button == NO_BUTTON){ + if(status & FRONT_BUTTON){ + /* front button pressed, change how magnetic the lines are */ + pressed_button = FRONT_BUTTON; + /* switch from sticky to loose and vice versa */ + magnetic_mode = (magnetic_mode == STICKY) ? LOOSE : STICKY; + magnetic_mode_changed = true; + /* make a sound according to the new attraction mode */ + executeCommand(PLAY_SOUND_CMD, (magnetic_mode == STICKY) ? STICKY_MODE_SOUND : LOOSE_MODE_SOUND ,0,0,0,0); + return true; + }else if(status & REAR_BUTTON){ + pressed_button = REAR_BUTTON; + /* rear button pressed: First time it picks up an object, second time it drops it */ + if(pickup_mode == RELEASED){ // pickup mode was released, then now user picked up an object + if(last_touched_point != NULL){ + pickedup_point = last_touched_point; + last_dragging_pos = pickedup_point->position; // last_dragging_pos used in checkMotion + /* send a pickup command to the Java thread which will in turn issue another pickup * + * command to this thread (pickup = true in drawDiagram) if the lock could be granted */ + executeCommand(PICKUP_CMD,point_id_map[last_touched_point],0,0,0,0); + }else if(last_touched_line != NULL){ + pickedup_line = last_touched_line; + pickup_line_pos = proxy_pos; // the point where the line was picked up + last_dragging_pos = proxy_pos;// last_dragging_pos used in checkMotion + executeCommand(PICKUP_CMD,line_id_map[last_touched_line],0,0,0,0); + }/* else user picked up thin air. Do nothing. */ + }else if(pickup_mode == DRAGGING){ // pickup mode was dragging, then now user dropped an object + pickup_mode = RELEASED; + Vec3 & new_position = hapticToScreenSpace(Vec3(proxy_pos), + cm->getScreenWidth(), + cm->getScreenHeight()); + if(pickedup_point){ + executeCommand(MOVE_CMD,point_id_map[pickedup_point],new_position.x,new_position.y,0,0); + pickedup_point = NULL; + }else{ + hapticToScreenSpace(pickup_line_pos, + cm->getScreenWidth(), + cm->getScreenHeight()); + executeCommand(MOVE_CMD, + line_id_map[pickedup_line], + new_position.x, + new_position.y, + pickup_line_pos.x, + pickup_line_pos.y); + pickedup_line = NULL; + } + return true; + } + } else if(status & ( LEFT_BUTTON | RIGHT_BUTTON )){ // either left or right button pressed + pressed_button = (status == LEFT_BUTTON) ? LEFT_BUTTON : RIGHT_BUTTON; + if(last_touched_point != NULL){ // priority to nodes + executeCommand(SPEAK_INFO_CMD,point_id_map[last_touched_point], 0,0,0,0); + return true; + } + if(last_touched_line != NULL){ + executeCommand(SPEAK_INFO_CMD,line_id_map[last_touched_line], 0,0,0,0); + return true; + } + } + } else if ( /* deactivate the buttons */ + (pressed_button == FRONT_BUTTON && !(status & FRONT_BUTTON)) || + (pressed_button == REAR_BUTTON && !(status & REAR_BUTTON)) || + (pressed_button == LEFT_BUTTON && !(status & LEFT_BUTTON)) || + (pressed_button == RIGHT_BUTTON && !(status & RIGHT_BUTTON))) + { + pressed_button = NO_BUTTON; + } + return false; +} + +bool HapticManager::checkMotion(void){ + if(pickup_mode != DRAGGING) + return false; + if(pointsDistance(proxy_pos,last_dragging_pos) > DRAGGING_SOUND_THRESHOLD){ + last_dragging_pos = proxy_pos; + executeCommand(PLAY_SOUND_CMD,DRAGGING_SOUND,0,0,0,0); + return true; + } + return false; +} + +void HapticManager::dispose(void){ + hd->releaseDevice(); + delete hd; +} + + +/* static constants initialization */ +const int HapticManager::NO_ID = 0; + +const int HapticManager::GRAPHIC_SCALE = 2; +const int HapticManager::SPRING_FORCE_FACTOR = 200; +const int HapticManager::LINE_FORCE_FACTOR_STICKY = 2000; +const int HapticManager::LINE_FORCE_FACTOR_LOOSE = 100; +const int HapticManager::POINT_FORCE_FACTOR = 20; +const double HapticManager::POINT_COLLISION_THRESHOLD = 0.0025; +const double HapticManager::LINE_COLLISION_THRESHOLD = 0.0025; +const double HapticManager::DRAGGING_SOUND_THRESHOLD = 0.01; + +const int HapticManager::LOOSE_MODE_SOUND = 0; +const int HapticManager::STICKY_MODE_SOUND = 1; +const int HapticManager::DRAGGING_SOUND = 3; + +const char HapticManager::MOVE_CMD = 'm'; +const char HapticManager::PLAY_SOUND_CMD = 'g'; +const char HapticManager::SPEAK_NAME_CMD = 't'; +const char HapticManager::PICKUP_CMD = 'c'; +const char HapticManager::SPEAK_INFO_CMD = 'i'; +const char HapticManager::SELECT_CMD = 's'; +const char HapticManager::UNSELECT_CMD = 'u'; + +const HAPIInt32 HapticManager::FRONT_BUTTON = (1 << 2); +const HAPIInt32 HapticManager::LEFT_BUTTON = (1 << 1); +const HAPIInt32 HapticManager::RIGHT_BUTTON = (1 << 3); +const HAPIInt32 HapticManager::REAR_BUTTON = (1 << 0); +const HAPIInt32 HapticManager::NO_BUTTON = 0;