1/*
2	File:		MBCBoardViewMouse.mm
3	Contains:	Handle mouse coordinate transformations
4	Copyright:	© 2002-2012 by Apple Inc., all rights reserved.
5
6	IMPORTANT: This Apple software is supplied to you by Apple Computer,
7	Inc.  ("Apple") in consideration of your agreement to the following
8	terms, and your use, installation, modification or redistribution of
9	this Apple software constitutes acceptance of these terms.  If you do
10	not agree with these terms, please do not use, install, modify or
11	redistribute this Apple software.
12
13	In consideration of your agreement to abide by the following terms,
14	and subject to these terms, Apple grants you a personal, non-exclusive
15	license, under Apple's copyrights in this original Apple software (the
16	"Apple Software"), to use, reproduce, modify and redistribute the
17	Apple Software, with or without modifications, in source and/or binary
18	forms; provided that if you redistribute the Apple Software in its
19	entirety and without modifications, you must retain this notice and
20	the following text and disclaimers in all such redistributions of the
21	Apple Software.  Neither the name, trademarks, service marks or logos
22	of Apple Inc. may be used to endorse or promote products
23	derived from the Apple Software without specific prior written
24	permission from Apple.  Except as expressly stated in this notice, no
25	other rights or licenses, express or implied, are granted by Apple
26	herein, including but not limited to any patent rights that may be
27	infringed by your derivative works or by other works in which the
28	Apple Software may be incorporated.
29
30	The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
31	MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
32	THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND
33	FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS
34	USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
35
36	IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
37	INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
38	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
39	PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE,
40	REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE,
41	HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING
42	NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
43	ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44*/
45
46#import "MBCBoardViewMouse.h"
47#import "MBCBoardViewDraw.h" // For drawBoardPlane
48#import "MBCInteractivePlayer.h"
49#import "MBCController.h"
50#import "MBCEngine.h"
51#import "MBCBoardWin.h"
52#import "MBCDebug.h"
53
54#import <OpenGL/glu.h>
55
56#import <algorithm>
57
58using std::min;
59using std::max;
60
61//
62// We're doing a lot of Projects and UnProjects.
63// These classes encapsulate them.
64//
65class MBCProjector {
66public:
67	MBCProjector();
68
69	NSPoint Project(MBCPosition pos);
70protected:
71    GLint		fViewport[4];
72    GLdouble	fMVMatrix[16];
73	GLdouble	fProjMatrix[16];
74};
75
76class MBCUnProjector : private MBCProjector {
77public:
78	MBCUnProjector(GLdouble winX, GLdouble winY);
79
80	MBCPosition UnProject(MBCBoardView * view);
81	MBCPosition UnProject(GLfloat knownY);
82private:
83	GLdouble	fWinX;
84	GLdouble	fWinY;
85};
86
87MBCProjector::MBCProjector()
88{
89    glGetIntegerv(GL_VIEWPORT, fViewport);
90    glGetDoublev(GL_MODELVIEW_MATRIX, fMVMatrix);
91    glGetDoublev(GL_PROJECTION_MATRIX, fProjMatrix);
92}
93
94NSPoint MBCProjector::Project(MBCPosition pos)
95{
96	GLdouble 	w[3];
97
98	gluProject(pos[0], pos[1], pos[2], fMVMatrix, fProjMatrix, fViewport,
99			   w+0, w+1, w+2);
100
101	NSPoint pt = {w[0], w[1]};
102
103	return pt;
104}
105
106MBCUnProjector::MBCUnProjector(GLdouble winX, GLdouble winY)
107	: MBCProjector(), fWinX(winX), fWinY(winY)
108{
109}
110
111MBCPosition MBCUnProjector::UnProject(MBCBoardView * view)
112{
113	MBCPosition	pos;
114	GLfloat		z;
115	GLdouble 	wv[3];
116
117    glReadPixels((GLint)fWinX, (GLint)fWinY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z);
118    if (z < 0.0001) {
119        if (MBCDebug::LogMouse())
120            fprintf(stderr, "Z buffer corruption, redrawing scene\n");
121        [view drawNow];
122        glReadPixels((GLint)fWinX, (GLint)fWinY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z);
123    }
124    gluUnProject(fWinX, fWinY, z, fMVMatrix, fProjMatrix, fViewport,
125				 wv+0, wv+1, wv+2);
126
127	pos[0] = wv[0];
128	pos[1] = wv[1];
129	pos[2] = wv[2];
130
131    if (MBCDebug::LogMouse())
132        fprintf(stderr, "Mouse (%.0f,%.0f) @ %5.3f -> (%4.1f,%4.1f,%4.1f)\n",
133                fWinX, fWinY, z, pos[0], pos[1], pos[2]);
134
135	return pos;
136}
137
138MBCPosition MBCUnProjector::UnProject(GLfloat knownY)
139{
140	MBCPosition	pos;
141	GLdouble 	p1[3];
142	GLdouble 	p0[3];
143
144    gluUnProject(fWinX, fWinY, 1.0f, fMVMatrix, fProjMatrix, fViewport,
145				 p1+0, p1+1, p1+2);
146    gluUnProject(fWinX, fWinY, 0.0f, fMVMatrix, fProjMatrix, fViewport,
147				 p0+0, p0+1, p0+2);
148	GLdouble yint = (knownY-p1[1])/(p0[1]-p1[1]);
149	pos[0] = p1[0]+(p0[0]-p1[0])*yint;
150	pos[1] = knownY;
151	pos[2] = p1[2]+(p0[2]-p1[2])*yint;
152    if (MBCDebug::LogMouse())
153        fprintf(stderr, "Mouse (%.0f,%.0f) [%5.3f] -> (%4.1f,%4.1f,%4.1f)\n",
154                fWinX, fWinY, knownY, pos[0], pos[1], pos[2]);
155
156	return pos;
157}
158
159MBCPosition operator-(const MBCPosition & a, const MBCPosition & b)
160{
161	MBCPosition	res;
162
163	res[0]	= a[0]-b[0];
164	res[1]	= a[1]-b[1];
165	res[2]	= a[2]-b[2];
166
167	return res;
168}
169
170@implementation MBCBoardView ( Mouse )
171
172- (NSRect) approximateBoundsOfSquare:(MBCSquare)square
173{
174	const float kSquare = 4.5f;
175
176	MBCProjector proj;
177	MBCPosition  pos = [self squareToPosition:square];
178
179	pos[0] -= kSquare;
180	pos[2] -= kSquare;
181	NSPoint p0	= proj.Project(pos);
182
183	pos[0] += 2.0f*kSquare;
184	NSPoint p1 	= proj.Project(pos);
185
186	pos[2] += 2.0f*kSquare;
187	NSPoint p2 	= proj.Project(pos);
188
189	pos[0] -= 2.0f*kSquare;
190	NSPoint p3 	= proj.Project(pos);
191
192	NSRect r;
193	if (p1.x > p0.x) {
194		r.origin.x 		= max(p0.x, p3.x);
195		r.size.width	= min(p1.x, p2.x)-r.origin.x;
196	} else {
197		r.origin.x 		= max(p1.x, p2.x);
198		r.size.width	= min(p0.x, p3.x)-r.origin.x;
199	}
200	if (p2.y > p1.y) {
201		r.origin.y 		= max(p0.y, p1.y);
202		r.size.height	= min(p2.y, p3.y)-r.origin.y;
203	} else {
204		r.origin.y 		= max(p2.y, p3.y);
205		r.size.height	= min(p0.y, p1.y)-r.origin.y;
206	}
207
208	return [self convertRectFromBacking:r];
209}
210
211- (MBCPosition) mouseToPosition:(NSPoint)mouse
212{
213    if (MBCDebug::LogMouse())
214        fprintf(stderr, "[%.0f,%.0f] ", mouse.x, mouse.y);
215    mouse = [self convertPointToBacking:mouse];
216	MBCUnProjector	unproj(mouse.x, mouse.y);
217
218	return unproj.UnProject(self);
219}
220
221- (MBCPosition) mouseToPositionIgnoringY:(NSPoint)mouse
222{
223    mouse = [self convertPointToBacking:mouse];
224	MBCUnProjector	unproj(mouse.x, mouse.y);
225
226	return unproj.UnProject(0.0f);
227}
228
229- (MBCPosition) eventToPosition:(NSEvent *)event
230{
231    [[self openGLContext] makeCurrentContext];
232
233    NSPoint p = [event locationInWindow];
234    NSPoint l = [self convertPoint:p fromView:nil];
235
236	return [self mouseToPosition:l];
237}
238
239- (void) mouseEntered:(NSEvent *)theEvent
240{
241    if (MBCDebug::LogMouse())
242        fprintf(stderr, "mouseEntered\n");
243
244	[[self window] setAcceptsMouseMovedEvents:YES];
245    [[self window] makeFirstResponder:self];
246}
247
248- (void)mouseExited:(NSEvent *)theEvent
249{
250    if (MBCDebug::LogMouse())
251        fprintf(stderr, "mouseExited\n");
252
253    [[self window] setAcceptsMouseMovedEvents:NO];
254}
255
256- (void) mouseMoved:(NSEvent *)event
257{
258    if (MBCDebug::LogMouse())
259        fprintf(stderr, "mouseMoved\n");
260
261	MBCPosition 	pos 	= [self eventToPosition:event];
262	float 			pxa		= fabs(pos[0]);
263	float			pza		= fabs(pos[2]);
264	NSCursor *		cursor	= fArrowCursor;
265
266	if (pxa > kBoardRadius || pza > kBoardRadius)
267		if (pxa < kBoardRadius+kBorderWidth+.1f
268		 && pza < kBoardRadius+kBorderWidth+.1f)
269			cursor	=	fHandCursor;
270	[cursor set];
271}
272
273- (void) mouseDown:(NSEvent *)event
274{
275    if (MBCDebug::LogMouse())
276        fprintf(stderr, "mouseDown\n");
277
278	MBCSquare previouslyPicked = fPickedSquare;
279
280    NSPoint p = [event locationInWindow];
281    NSPoint l = [self convertPoint:p fromView:nil];
282
283	//
284	// On mousedown, we determine the point on the board surface that
285	// corresponds to the mouse location by the frontmost Z value, but
286	// then pretend that the click happened at board surface level. Weirdly
287	// enough, this seems to give the most natural feeling mouse behavior.
288	//
289    [[self openGLContext] makeCurrentContext];
290	MBCPosition pos = [self mouseToPosition:l];
291	fSelectedDest	= [self positionToSquareOrRegion:&pos];
292    switch (fSelectedDest) {
293	case kInvalidSquare:
294		return;
295	case kWhitePromoSquare:
296	case kBlackPromoSquare:
297		return;
298	case kBorderRegion:
299		fInBoardManipulation= true;
300		fOrigMouse 			= l;
301		fCurMouse 			= l;
302		fRawAzimuth 		= fAzimuth;
303		[NSCursor hide];
304		[NSEvent startPeriodicEventsAfterDelay: 0.008f withPeriod: 0.008f];
305		break;
306	default:
307        if (!fWantMouse || fInAnimation || pos[1] < 0.1)
308            return;
309        if (fSelectedDest == fPickedSquare) {
310            //
311            // When trying to move a large piece by clicking the destination, the piece
312            // sometimes can hide the destination. We try again by ignoring y.
313            //
314            MBCPosition altPos  = [self mouseToPositionIgnoringY:l];
315            MBCSquare   altDest = [self positionToSquareOrRegion:&altPos];
316            if (altDest < kSyntheticSquare) {
317                pos             = altPos;
318                fSelectedDest   = altDest;
319            }
320        }
321		//
322		// Let interactive player decide whether we hit one of their pieces
323		//
324		[fInteractive startSelection:fSelectedDest];
325		if (!fSelectedPiece) // Apparently not...
326			return;
327		break;
328	}
329	pos[1]		    	= 0.0f;
330	gettimeofday(&fLastRedraw, NULL);
331	fLastSelectedPos	= pos;
332	[self drawNow];
333
334	NSDate * whenever = [NSDate distantFuture];
335	for (bool goOn = true; goOn; ) {
336		event =
337			[NSApp nextEventMatchingMask:
338					   NSPeriodicMask|NSLeftMouseUpMask|NSLeftMouseDraggedMask
339				   untilDate:whenever inMode:NSEventTrackingRunLoopMode
340				   dequeue:YES];
341        switch ([event type]) {
342		case NSPeriodic:
343		case NSLeftMouseDragged:
344			[self dragAndRedraw:event forceRedraw:NO];
345			break;
346		case NSLeftMouseUp: {
347			[self dragAndRedraw:event forceRedraw:YES];
348            [fController setAngle:fElevation spin:fAzimuth];
349			[fInteractive endSelection:fSelectedDest animate:NO];
350			if (fPickedSquare == previouslyPicked)
351				fPickedSquare = kInvalidSquare; // Toggle pick
352			goOn = false;
353			if (fInBoardManipulation) {
354				fInBoardManipulation = false;
355				[NSCursor unhide];
356				[NSEvent stopPeriodicEvents];
357			}
358			break; }
359		default:
360			/* Ignore any other kind of event. */
361			break;
362		}
363	}
364	fSelectedDest = kInvalidSquare;
365}
366
367- (void) mouseUp:(NSEvent *)event
368{
369    if (MBCDebug::LogMouse())
370        fprintf(stderr, "mouseUp\n");
371
372	if (!fWantMouse || fInAnimation)
373		return;
374
375	MBCPiece promo;
376	if (fSelectedDest == kWhitePromoSquare) {
377		promo = [fBoard defaultPromotion:YES];
378	} else if (fSelectedDest == kBlackPromoSquare) {
379		promo = [fBoard defaultPromotion:NO];
380	} else if (fPickedSquare != kInvalidSquare) {
381		[fInteractive startSelection:fPickedSquare];
382		[fInteractive endSelection:fSelectedDest animate:YES];
383
384		return;
385	} else
386		return;
387
388	switch (promo) {
389	case QUEEN:
390		if (fVariant == kVarSuicide)
391			promo = KING;	// King promotion is very popular in suicide
392		else
393			promo = KNIGHT; // Second most useful
394		break;
395	case KING: // Suicide only
396		promo = KNIGHT;
397		break;
398	case KNIGHT:
399		promo = ROOK;
400		break;
401	case ROOK:
402		promo = BISHOP;
403		break;
404	case BISHOP:
405		promo = QUEEN;
406		break;
407	}
408	[fBoard setDefaultPromotion:promo
409			for:fSelectedDest == kWhitePromoSquare];
410
411	[self setNeedsDisplay:YES];
412}
413
414- (void) dragAndRedraw:(NSEvent *)event forceRedraw:(BOOL)force
415{
416	if ([event type] != NSPeriodic) {
417		NSPoint p = [event locationInWindow];
418		NSPoint l = [self convertPoint:p fromView:nil];
419		fCurMouse = l;
420
421		if (!fInAnimation) {
422			//
423			// On drag, we can use a fairly fast interpolation to determine
424			// the 3D coordinate using the y where we touched the piece
425			//
426            [[self openGLContext] makeCurrentContext];
427           l = [self convertPointToBacking:l];
428			MBCUnProjector	unproj(l.x, l.y);
429
430			fSelectedPos 				= unproj.UnProject(0.0f);
431			[self snapToSquare:&fSelectedPos];
432		}
433	}
434	struct timeval	now;
435	gettimeofday(&now, NULL);
436	NSTimeInterval	dt			=
437		now.tv_sec - fLastRedraw.tv_sec
438		+ 0.000001 * (now.tv_usec - fLastRedraw.tv_usec);
439
440	const float	kTiltSpeed		=  0.50f;
441	const float kSpinSpeed		=  0.50f;
442	const float	kThreshold		= 10.0f;
443	const float	kAzimuthRound	=  5.0f;
444
445	if (force) {
446		[self setNeedsDisplay:YES];
447	} else if (fSelectedDest == kBorderRegion) {
448		float dx =  fCurMouse.x-fOrigMouse.x;
449		float dy =	fCurMouse.y-fOrigMouse.y;
450#if FULL_DIAGONAL_MOVES
451		bool mustDraw = false;
452		if (fabs(dx) > kThreshold) {
453			fRawAzimuth += dx*dt*kSpinSpeed;
454			fRawAzimuth = fmod(fRawAzimuth+360.0f, 360.0f);
455			float angle	= fmod((fAzimuth = fRawAzimuth), 90.0f);
456			if (angle < kAzimuthRound)
457				fAzimuth	-= angle;
458			else if (angle > 90.0f-kAzimuthRound)
459				fAzimuth 	+= 90.0f-angle;
460			mustDraw		= true;
461		}
462		if (fabs(dy) > kThreshold) {
463			fElevation -= dy*dt*kTiltSpeed;
464			fElevation = max(kMinElevation, min(kMaxElevation, fElevation));
465			mustDraw		= true;
466		}
467		if (mustDraw) {
468			fNeedPerspective= true;
469			fLastRedraw 	= now;
470			[self drawNow];
471		}
472#else
473		if (fabs(dx) > fabs(dy) && fabs(dx) > kThreshold) {
474			fRawAzimuth += dx*dt*kSpinSpeed;
475			fRawAzimuth = fmod(fRawAzimuth+360.0f, 360.0f);
476			float angle	= fmod((fAzimuth = fRawAzimuth), 90.0f);
477			if (angle < kAzimuthRound)
478				fAzimuth	-= angle;
479			else if (angle > 90.0f-kAzimuthRound)
480				fAzimuth 	+= 90.0f-angle;
481			fNeedPerspective= true;
482			fLastRedraw 	= now;
483			[self drawNow];
484		} else if (fabs(dy) > kThreshold) {
485			fElevation -= dy*dt*kTiltSpeed;
486			fElevation =
487				max(kMinElevation, min(kMaxElevation, fElevation));
488			fNeedPerspective= true;
489			fLastRedraw 	= now;
490			[self drawNow];
491		}
492#endif
493	} else {
494		MBCPosition		delta		= fSelectedPos-fLastSelectedPos;
495		GLfloat			d2      	= delta[0]*delta[0]+delta[2]*delta[2];
496
497		if (d2 > 25.0f || (d2 > 1.0f && dt > 0.02)) {
498			fSelectedDest	= [self positionToSquare:&fSelectedPos];
499			fLastRedraw 	= now;
500			[self drawNow];
501		}
502	}
503}
504
505- (BOOL)acceptsFirstResponder
506{
507	return YES;
508}
509
510- (void)keyDown:(NSEvent *)event
511{
512	NSString * chr = [event characters];
513	if ([chr length] != 1)
514		return; // Ignore
515	switch (char ch = [chr characterAtIndex:0]) {
516	case 'A':
517	case 'B':
518	case 'C':
519	case 'D':
520	case 'E':
521	case 'F':
522	case 'G':
523	case 'H':
524		ch = tolower(ch);
525		// Fall through
526	case 'b':
527	case 'a':
528	case 'c':
529	case 'd':
530	case 'e':
531	case 'f':
532	case 'g':
533	case 'h':
534	case '=':
535        if (ch == 'b' && fKeyBuffer == '=')
536            goto promotion_piece;
537        if (fWantMouse)
538            fKeyBuffer	= ch;
539        else
540            NSBeep();
541		break;
542	case '1':
543	case '2':
544	case '3':
545	case '4':
546	case '5':
547	case '6':
548	case '7':
549	case '8':
550		if (fWantMouse && isalpha(fKeyBuffer)) {
551			MBCSquare sq = Square(fKeyBuffer, ch-'0');
552			if (fPickedSquare != kInvalidSquare) {
553				[fInteractive startSelection:fPickedSquare];
554				[fInteractive endSelection:sq animate:YES];
555			} else {
556				[fInteractive startSelection:sq];
557				[self clickPiece];
558			}
559		} else
560			NSBeep();
561		fKeyBuffer = 0;
562		break;
563	case '\177':	// Delete
564	case '\r':
565		if (fKeyBuffer) {
566			fKeyBuffer 	= 0;
567		} else if (fPickedSquare != kInvalidSquare) {
568			[fInteractive endSelection:fPickedSquare animate:NO];
569			fPickedSquare	= kInvalidSquare;
570			[self setNeedsDisplay:YES];
571		}
572		break;
573	case 'K':
574		if (fVariant != kVarSuicide) {
575			NSBeep();
576			break;
577		}
578		// Fall through
579	case 'Q':
580	case 'N':
581	case 'R':
582		ch = tolower(ch);
583		// Fall through
584	case 'k':
585		if (fVariant != kVarSuicide) {
586			NSBeep();
587			break;
588		}
589		// Fall through
590	case 'q':
591	case 'n':
592	case 'r':
593	promotion_piece:
594		if (fKeyBuffer == '=') {
595			const char * kPiece = " kqbnr";
596			[fBoard setDefaultPromotion:strchr(kPiece, ch)-kPiece for:YES];
597			[fBoard setDefaultPromotion:strchr(kPiece, ch)-kPiece for:NO];
598			[self setNeedsDisplay:YES];
599		} else {
600			NSBeep();
601		}
602	    fKeyBuffer = 0;
603	    break;
604	default:
605		//
606		// Propagate ESC etc.
607		//
608		[super keyDown:event];
609		break;
610	}
611}
612
613@end
614
615// Local Variables:
616// mode:ObjC
617// End:
618