MBCBoardViewMouse.mm [plain text]
/*
File: MBCBoardViewMouse.mm
Contains: Handle mouse coordinate transformations
Version: 1.0
Copyright: © 2002-2008 by Apple Inc., all rights reserved.
File Ownership:
DRI: Matthias Neeracher x43683
Writers:
(MN) Matthias Neeracher
Change History (most recent first):
$Log: MBCBoardViewMouse.mm,v $
Revision 1.24 2008/10/24 22:07:28 neerache
<rdar://problem/5459104> Chess: Rotating the playing board while computer is moving results with the mouse as the chess piece
Revision 1.23 2008/10/24 20:04:48 neerache
<rdar://problem/3726597> Implement diagonal moves, but disable them
Revision 1.22 2008/04/22 19:47:41 neerache
<rdar://problem/5750936> Adoption of Clean / Dirty API by Chess
Revision 1.21 2007/03/02 23:06:00 neerache
<rdar://problem/4038207> Allow the user to type in a move in Chess
Revision 1.20 2004/09/08 00:35:24 neerache
Reduce square sizes to avoid navigation ambiguities
Revision 1.19 2004/08/16 07:50:55 neerache
Support accessibility
Revision 1.18 2003/07/18 22:14:26 neerache
Disable pondering during drag to improve interactive performance (RADAR 2736549)
Revision 1.17 2003/07/14 23:21:49 neerache
Move promotion defaults into MBCBoard
Revision 1.16 2003/07/07 08:47:54 neerache
Switch to textured main window
Revision 1.15 2003/06/18 21:55:17 neerache
More (mostly unsuccessful) tweaking of floating windows
Revision 1.14 2003/06/05 08:31:26 neerache
Added Tuner
Revision 1.13 2003/06/05 00:14:37 neerache
Reduce excessive threshold
Revision 1.12 2003/06/04 23:14:05 neerache
Neater manipulation widget; remove obsolete graphics options
Revision 1.11 2003/06/04 09:25:47 neerache
New and improved board manipulation metaphor
Revision 1.10 2003/06/02 05:44:48 neerache
Implement direct board manipulation
Revision 1.9 2003/05/24 20:28:27 neerache
Address race conditions between ploayer and engine
Revision 1.8 2003/05/02 01:16:33 neerache
Simplify drawing methods
Revision 1.7 2003/04/28 22:11:45 neerache
Handle black promotion square
Revision 1.6 2003/04/25 22:26:23 neerache
Simplify mouse model, fix startup bug
Revision 1.5 2003/04/25 16:37:00 neerache
Clean automake build
Revision 1.4 2003/04/24 23:20:35 neeri
Support pawn promotions
Revision 1.3 2003/04/02 19:01:36 neeri
Explore strategies to speed up dragging
Revision 1.2 2002/12/04 02:30:50 neeri
Experiment (unsuccessfully so far) with ways to speed up piece movement
Revision 1.1 2002/08/22 23:47:06 neeri
Initial Checkin
*/
#import "MBCBoardViewMouse.h"
#import "MBCBoardViewDraw.h" // For drawBoardPlane
#import "MBCInteractivePlayer.h"
#import "MBCController.h"
#import "MBCEngine.h"
#import <OpenGL/glu.h>
#import <algorithm>
using std::min;
using std::max;
extern NSString * kMBCBoardAngle;
extern NSString * kMBCBoardSpin;
//
// We're doing a lot of Projects and UnProjects.
// These classes encapsulate them.
//
class MBCProjector {
public:
MBCProjector();
NSPoint Project(MBCPosition pos);
protected:
GLint fViewport[4];
GLdouble fMVMatrix[16];
GLdouble fProjMatrix[16];
};
class MBCUnProjector : private MBCProjector {
public:
MBCUnProjector(GLdouble winX, GLdouble winY);
MBCPosition UnProject();
MBCPosition UnProject(GLfloat knownY);
private:
GLdouble fWinX;
GLdouble fWinY;
};
MBCProjector::MBCProjector()
{
glGetIntegerv(GL_VIEWPORT, fViewport);
glGetDoublev(GL_MODELVIEW_MATRIX, fMVMatrix);
glGetDoublev(GL_PROJECTION_MATRIX, fProjMatrix);
}
NSPoint MBCProjector::Project(MBCPosition pos)
{
GLdouble w[3];
gluProject(pos[0], pos[1], pos[2], fMVMatrix, fProjMatrix, fViewport,
w+0, w+1, w+2);
NSPoint pt = {w[0], w[1]};
return pt;
}
MBCUnProjector::MBCUnProjector(GLdouble winX, GLdouble winY)
: MBCProjector(), fWinX(winX), fWinY(winY)
{
}
MBCPosition MBCUnProjector::UnProject()
{
MBCPosition pos;
GLfloat z;
GLdouble wv[3];
glReadPixels((GLint)fWinX, (GLint)fWinY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z);
gluUnProject(fWinX, fWinY, z, fMVMatrix, fProjMatrix, fViewport,
wv+0, wv+1, wv+2);
pos[0] = wv[0];
pos[1] = wv[1];
pos[2] = wv[2];
return pos;
}
MBCPosition MBCUnProjector::UnProject(GLfloat knownY)
{
MBCPosition pos;
GLdouble p1[3];
GLdouble p0[3];
gluUnProject(fWinX, fWinY, 1.0f, fMVMatrix, fProjMatrix, fViewport,
p1+0, p1+1, p1+2);
gluUnProject(fWinX, fWinY, 0.0f, fMVMatrix, fProjMatrix, fViewport,
p0+0, p0+1, p0+2);
GLdouble yint = (knownY-p1[1])/(p0[1]-p1[1]);
pos[0] = p1[0]+(p0[0]-p1[0])*yint;
pos[1] = knownY;
pos[2] = p1[2]+(p0[2]-p1[2])*yint;
return pos;
}
MBCPosition operator-(const MBCPosition & a, const MBCPosition & b)
{
MBCPosition res;
res[0] = a[0]-b[0];
res[1] = a[1]-b[1];
res[2] = a[2]-b[2];
return res;
}
@implementation MBCBoardView ( Mouse )
- (NSRect) approximateBoundsOfSquare:(MBCSquare)square
{
const float kSquare = 4.5f;
MBCProjector proj;
MBCPosition pos = [self squareToPosition:square];
pos[0] -= kSquare;
pos[2] -= kSquare;
NSPoint p0 = proj.Project(pos);
pos[0] += 2.0f*kSquare;
NSPoint p1 = proj.Project(pos);
pos[2] += 2.0f*kSquare;
NSPoint p2 = proj.Project(pos);
pos[0] -= 2.0f*kSquare;
NSPoint p3 = proj.Project(pos);
NSRect r;
if (p1.x > p0.x) {
r.origin.x = max(p0.x, p3.x);
r.size.width = min(p1.x, p2.x)-r.origin.x;
} else {
r.origin.x = max(p1.x, p2.x);
r.size.width = min(p0.x, p3.x)-r.origin.x;
}
if (p2.y > p1.y) {
r.origin.y = max(p0.y, p1.y);
r.size.height = min(p2.y, p3.y)-r.origin.y;
} else {
r.origin.y = max(p2.y, p3.y);
r.size.height = min(p0.y, p1.y)-r.origin.y;
}
return r;
}
- (MBCPosition) mouseToPosition:(NSPoint)mouse
{
MBCUnProjector unproj(mouse.x, mouse.y);
return unproj.UnProject();
}
- (MBCPosition) eventToPosition:(NSEvent *)event
{
NSPoint p = [event locationInWindow];
NSPoint l = [self convertPoint:p fromView:nil];
return [self mouseToPosition:l];
}
- (void) mouseMoved:(NSEvent *)event
{
MBCPosition pos = [self eventToPosition:event];
float pxa = fabs(pos[0]);
float pza = fabs(pos[2]);
NSCursor * cursor = fArrowCursor;
if (pxa > kBoardRadius || pza > kBoardRadius)
if (pxa < kBoardRadius+kBorderWidth+.1f
&& pza < kBoardRadius+kBorderWidth+.1f)
cursor = fHandCursor;
[cursor set];
}
- (void) mouseDown:(NSEvent *)event
{
MBCSquare previouslyPicked = fPickedSquare;
NSPoint p = [event locationInWindow];
NSPoint l = [self convertPoint:p fromView:nil];
//
// On mousedown, we determine the point on the board surface that
// corresponds to the mouse location by the frontmost Z value, but
// then pretend that the click happened at board surface level. Weirdly
// enough, this seems to give the most natural feeling mouse behavior.
//
MBCPosition pos = [self mouseToPosition:l];
fSelectedDest = [self positionToSquareOrRegion:&pos];
switch (fSelectedDest) {
case kInvalidSquare:
return;
case kWhitePromoSquare:
case kBlackPromoSquare:
return;
case kBorderRegion:
fInBoardManipulation= true;
fOrigMouse = l;
fCurMouse = l;
fRawAzimuth = fAzimuth;
[NSCursor hide];
[NSEvent startPeriodicEventsAfterDelay:0.1f withPeriod:0.1f];
break;
default:
if (!fWantMouse || fInAnimation || pos[1] < 0.1)
return;
//
// Let interactive player decide whether we hit one of their pieces
//
[fInteractive startSelection:fSelectedDest];
if (!fSelectedPiece) // Apparently not...
return;
break;
}
pos[1] = 0.0f;
gettimeofday(&fLastRedraw, NULL);
fLastSelectedPos = pos;
//
// For better interactivity, we stop the engine while a drag is in progress
//
[[fController engine] interruptEngine];
[self drawNow];
NSDate * whenever = [NSDate distantFuture];
for (bool goOn = true; goOn; ) {
event =
[NSApp nextEventMatchingMask:
NSPeriodicMask|NSLeftMouseUpMask|NSLeftMouseDraggedMask
untilDate:whenever inMode:NSEventTrackingRunLoopMode
dequeue:YES];
switch ([event type]) {
case NSPeriodic:
case NSLeftMouseDragged:
[self dragAndRedraw:event forceRedraw:NO];
break;
case NSLeftMouseUp: {
[self dragAndRedraw:event forceRedraw:YES];
NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
[defaults setFloat:fElevation forKey:kMBCBoardAngle];
[defaults setFloat:fAzimuth forKey:kMBCBoardSpin];
[fInteractive endSelection:fSelectedDest animate:NO];
if (fPickedSquare == previouslyPicked)
fPickedSquare = kInvalidSquare; // Toggle pick
goOn = false;
if (fInBoardManipulation) {
fInBoardManipulation = false;
[NSCursor unhide];
[NSEvent stopPeriodicEvents];
}
break; }
default:
/* Ignore any other kind of event. */
break;
}
}
fSelectedDest = kInvalidSquare;
}
- (void) mouseUp:(NSEvent *)event
{
if (!fWantMouse || fInAnimation)
return;
MBCPiece promo;
if (fSelectedDest == kWhitePromoSquare) {
promo = [fBoard defaultPromotion:YES];
} else if (fSelectedDest == kBlackPromoSquare) {
promo = [fBoard defaultPromotion:NO];
} else if (fPickedSquare != kInvalidSquare) {
[fInteractive startSelection:fPickedSquare];
[fInteractive endSelection:fSelectedDest animate:YES];
return;
} else
return;
switch (promo) {
case QUEEN:
if (fVariant == kVarSuicide)
promo = KING; // King promotion is very popular in suicide
else
promo = KNIGHT; // Second most useful
break;
case KING: // Suicide only
promo = KNIGHT;
break;
case KNIGHT:
promo = ROOK;
break;
case ROOK:
promo = BISHOP;
break;
case BISHOP:
promo = QUEEN;
break;
}
[fBoard setDefaultPromotion:promo
for:fSelectedDest == kWhitePromoSquare];
[self setNeedsDisplay:YES];
}
- (void) dragAndRedraw:(NSEvent *)event forceRedraw:(BOOL)force
{
if ([event type] != NSPeriodic) {
NSPoint p = [event locationInWindow];
NSPoint l = [self convertPoint:p fromView:nil];
fCurMouse = l;
if (!fInAnimation) {
//
// On drag, we can use a fairly fast interpolation to determine
// the 3D coordinate using the y where we touched the piece
//
MBCUnProjector unproj(l.x, l.y);
fSelectedPos = unproj.UnProject(0.0f);
[self snapToSquare:&fSelectedPos];
}
}
struct timeval now;
gettimeofday(&now, NULL);
NSTimeInterval dt =
now.tv_sec - fLastRedraw.tv_sec
+ 0.000001 * (now.tv_usec - fLastRedraw.tv_usec);
const float kTiltSpeed = 0.50f;
const float kSpinSpeed = 0.50f;
const float kThreshold = 10.0f;
const float kAzimuthRound = 5.0f;
if (force) {
[self setNeedsDisplay:YES];
} else if (fSelectedDest == kBorderRegion) {
float dx = fCurMouse.x-fOrigMouse.x;
float dy = fCurMouse.y-fOrigMouse.y;
#if FULL_DIAGONAL_MOVES
bool mustDraw = false;
if (fabs(dx) > kThreshold) {
fRawAzimuth += dx*dt*kSpinSpeed;
fRawAzimuth = fmod(fRawAzimuth+360.0f, 360.0f);
float angle = fmod((fAzimuth = fRawAzimuth), 90.0f);
if (angle < kAzimuthRound)
fAzimuth -= angle;
else if (angle > 90.0f-kAzimuthRound)
fAzimuth += 90.0f-angle;
mustDraw = true;
}
if (fabs(dy) > kThreshold) {
fElevation -= dy*dt*kTiltSpeed;
fElevation = max(kMinElevation, min(kMaxElevation, fElevation));
mustDraw = true;
}
if (mustDraw) {
fNeedPerspective= true;
fLastRedraw = now;
[self drawNow];
}
#else
if (fabs(dx) > fabs(dy) && fabs(dx) > kThreshold) {
fRawAzimuth += dx*dt*kSpinSpeed;
fRawAzimuth = fmod(fRawAzimuth+360.0f, 360.0f);
float angle = fmod((fAzimuth = fRawAzimuth), 90.0f);
if (angle < kAzimuthRound)
fAzimuth -= angle;
else if (angle > 90.0f-kAzimuthRound)
fAzimuth += 90.0f-angle;
fNeedPerspective= true;
fLastRedraw = now;
[self drawNow];
} else if (fabs(dy) > kThreshold) {
fElevation -= dy*dt*kTiltSpeed;
fElevation =
max(kMinElevation, min(kMaxElevation, fElevation));
fNeedPerspective= true;
fLastRedraw = now;
[self drawNow];
}
#endif
} else {
MBCPosition delta = fSelectedPos-fLastSelectedPos;
GLfloat d2 = delta[0]*delta[0]+delta[2]*delta[2];
if (d2 > 25.0f || (d2 > 1.0f && dt > 0.02)) {
fSelectedDest = [self positionToSquare:&fSelectedPos];
fLastRedraw = now;
[self drawNow];
}
}
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (void)keyDown:(NSEvent *)event
{
NSString * chr = [event characters];
if ([chr length] != 1)
return; // Ignore
switch (char ch = [chr characterAtIndex:0]) {
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
case 'H':
ch = tolower(ch);
// Fall through
case 'b':
if (fKeyBuffer == '=')
goto promotion_piece;
// Else fall through
case 'a':
case 'c':
case 'd':
case 'e':
case 'f':
case 'g':
case 'h':
case '=':
fKeyBuffer = ch;
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
if (isalpha(fKeyBuffer)) {
MBCSquare sq = Square(fKeyBuffer, ch-'0');
if (fPickedSquare != kInvalidSquare) {
[fInteractive startSelection:fPickedSquare];
[fInteractive endSelection:sq animate:YES];
} else {
[fInteractive startSelection:sq];
[self clickPiece];
}
} else
NSBeep();
fKeyBuffer = 0;
break;
case '\177': // Delete
case '\r':
if (fKeyBuffer) {
fKeyBuffer = 0;
} else if (fPickedSquare != kInvalidSquare) {
[fInteractive endSelection:fPickedSquare animate:NO];
fPickedSquare = kInvalidSquare;
[self setNeedsDisplay:YES];
}
break;
case 'K':
if (fVariant != kVarSuicide) {
NSBeep();
break;
}
// Fall through
case 'Q':
case 'N':
case 'R':
ch = tolower(ch);
// Fall through
case 'k':
if (fVariant != kVarSuicide) {
NSBeep();
break;
}
// Fall through
case 'q':
case 'n':
case 'r':
promotion_piece:
if (fKeyBuffer == '=') {
const char * kPiece = " kqbnr";
[fBoard setDefaultPromotion:strchr(kPiece, ch)-kPiece for:YES];
[fBoard setDefaultPromotion:strchr(kPiece, ch)-kPiece for:NO];
[self setNeedsDisplay:YES];
} else {
NSBeep();
}
fKeyBuffer = 0;
break;
}
}
@end
// Local Variables:
// mode:ObjC
// End: