* Working on 3D rotation of game board.
* Added some colored balls ("drawn" myself using Photoshop glass effect.)
1 /* This code is based on Apple's "GeekGameBoard" sample code, version 1.0.
2 http://developer.apple.com/samplecode/GeekGameBoard/
3 Copyright © 2007 Apple Inc. Copyright © 2008 Jens Alfke. All Rights Reserved.
5 Redistribution and use in source and binary forms, with or without modification, are permitted
6 provided that the following conditions are met:
8 * Redistributions of source code must retain the above copyright notice, this list of conditions
9 and the following disclaimer.
10 * Redistributions in binary form must reproduce the above copyright notice, this list of
11 conditions and the following disclaimer in the documentation and/or other materials provided
12 with the distribution.
14 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
15 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
16 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
17 BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
19 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
20 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
21 THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 #import "Game+Protected.h"
29 #import "QuartzUtils.h"
33 #define kMaxPerspective 0.965 // 55 degrees
36 @interface BoardView ()
37 - (void) _findDropTarget: (NSPoint)pos;
41 @implementation BoardView
44 @synthesize table=_table, gameBoardInset=_gameBoardInset;
54 - (void) _applyPerspective
57 if( fabs(_perspective) >= M_PI/180 ) {
58 CGSize size = self.layer.bounds.size;
59 t = CATransform3DMakeTranslation(-size.width/2, -size.height/4, 0);
60 t = CATransform3DConcat(t, CATransform3DMakeRotation(-_perspective, 1,0,0));
62 CATransform3D pers = CATransform3DIdentity;
64 t = CATransform3DConcat(t, pers);
65 t = CATransform3DConcat(t, CATransform3DMakeTranslation(size.width/2,
66 size.height*(0.25 + 0.05*sin(2*_perspective)),
68 self.layer.borderWidth = 3;
70 t = CATransform3DIdentity;
71 self.layer.borderWidth = 0;
73 self.layer.transform = t;
76 - (CGFloat) perspective {return _perspective;}
78 - (void) setPerspective: (CGFloat)p
80 p = MAX(0.0, MIN(kMaxPerspective, p));
81 if( p != _perspective ) {
83 [self _applyPerspective];
84 _game.tablePerspectiveAngle = p;
89 - (void) _removeGameBoard
92 RemoveImmediately(_table);
97 - (void) createGameBoard
99 [self _removeGameBoard];
100 _table = [[CALayer alloc] init];
101 _table.frame = [self gameBoardFrame];
102 _table.autoresizingMask = kCALayerMinXMargin | kCALayerMaxXMargin | kCALayerMinYMargin | kCALayerMaxYMargin;
104 // Tell the game to set up the board:
105 _game.table = _table;
107 [self.layer addSublayer: _table];
117 - (void) setGame: (Game*)game
122 [self createGameBoard];
126 - (void) startGameNamed: (NSString*)gameClassName
128 Class gameClass = NSClassFromString(gameClassName);
129 Game *game = [[gameClass alloc] init];
137 - (CGRect) gameBoardFrame
139 return CGRectInset(self.layer.bounds, _gameBoardInset.width,_gameBoardInset.height);
143 - (void)resetCursorRects
145 [super resetCursorRects];
147 [self addCursorRect: self.bounds cursor: [NSCursor openHandCursor]];
151 - (NSView*) fullScreenView
153 return _fullScreenView ?: self;
156 - (IBAction) enterFullScreen: (id)sender
158 //[self _removeGameBoard];
159 if( self.fullScreenView.isInFullScreenMode ) {
160 [self.fullScreenView exitFullScreenModeWithOptions: nil];
162 [self.fullScreenView enterFullScreenMode: self.window.screen
165 //[self createGameBoard];
169 - (void)viewWillStartLiveResize
171 [super viewWillStartLiveResize];
172 _oldSize = self.frame.size;
175 - (void)setFrameSize:(NSSize)newSize
177 [super setFrameSize: newSize];
178 if( _oldSize.width > 0.0f ) {
179 CGAffineTransform xform = _table.affineTransform;
180 xform.a = xform.d = MIN(newSize.width,newSize.height)/MIN(_oldSize.width,_oldSize.height);
181 BeginDisableAnimations();
182 [self _applyPerspective];
183 _table.affineTransform = xform;
184 EndDisableAnimations();
186 [self createGameBoard];
189 - (void)viewDidEndLiveResize
191 [super viewDidEndLiveResize];
192 _oldSize.width = _oldSize.height = 0.0f;
193 [self createGameBoard];
198 #pragma mark KEY EVENTS:
201 - (BOOL) performKeyEquivalent: (NSEvent*)ev
203 if( [ev.charactersIgnoringModifiers hasPrefix: @"\033"] ) { // Esc key
204 if( self.fullScreenView.isInFullScreenMode ) {
205 [self performSelector: @selector(enterFullScreen:) withObject: nil afterDelay: 0.0];
206 // without the delayed-perform, NSWindow crashes right after this method returns!
215 #pragma mark HIT-TESTING:
218 /** Converts a point from window coords, to this view's root layer's coords. */
219 - (CGPoint) _convertPointFromWindowToLayer: (NSPoint)locationInWindow
221 NSPoint where = [self convertPoint: locationInWindow fromView: nil]; // convert to view coords
222 where = [self convertPointToBase: where]; // then to layer base coords
223 return [self.layer convertPoint: NSPointToCGPoint(where) // then to transformed layer coords
224 fromLayer: self.layer.superlayer];
228 // Hit-testing callbacks (to identify which layers caller is interested in):
229 typedef BOOL (*LayerMatchCallback)(CALayer*);
231 static BOOL layerIsBit( CALayer* layer ) {return [layer isKindOfClass: [Bit class]];}
232 static BOOL layerIsBitHolder( CALayer* layer ) {return [layer conformsToProtocol: @protocol(BitHolder)];}
233 static BOOL layerIsDropTarget( CALayer* layer ) {return [layer respondsToSelector: @selector(draggingEntered:)];}
236 /** Locates the layer at a given point in window coords.
237 If the leaf layer doesn't pass the layer-match callback, the nearest ancestor that does is returned.
238 If outOffset is provided, the point's position relative to the layer is stored into it. */
239 - (CALayer*) hitTestPoint: (NSPoint)locationInWindow
240 forLayerMatching: (LayerMatchCallback)match
241 offset: (CGPoint*)outOffset
243 CGPoint where = [self _convertPointFromWindowToLayer: locationInWindow ];
244 CALayer *layer = [_table hitTest: where];
247 CGPoint bitPos = [self.layer convertPoint: layer.position
248 fromLayer: layer.superlayer];
250 *outOffset = CGPointMake( bitPos.x-where.x, bitPos.y-where.y);
253 layer = layer.superlayer;
260 #pragma mark MOUSE CLICKS & DRAGS:
263 - (void) mouseDown: (NSEvent*)ev
265 if( ! _game.okToMove ) {
271 _dragStartPos = ev.locationInWindow;
272 _dragBit = (Bit*) [self hitTestPoint: _dragStartPos
273 forLayerMatching: layerIsBit
274 offset: &_dragOffset];
277 // If no bit was clicked, see if it's a BitHolder the game will let the user add a Bit to:
278 id<BitHolder> holder = (id<BitHolder>) [self hitTestPoint: _dragStartPos
279 forLayerMatching: layerIsBitHolder
282 _dragBit = [_game bitToPlaceInHolder: holder];
284 _dragOffset.x = _dragOffset.y = 0;
285 if( _dragBit.superlayer==nil )
286 _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
300 _oldHolder = _dragBit.holder;
301 // Ask holder's and game's permission before dragging:
303 _dragBit = [_oldHolder canDragBit: _dragBit];
304 if( _dragBit && ! [_game canBit: _dragBit moveFrom: _oldHolder] ) {
305 [_oldHolder cancelDragBit: _dragBit];
316 _oldSuperlayer = _dragBit.superlayer;
317 _oldLayerIndex = [_oldSuperlayer.sublayers indexOfObjectIdenticalTo: _dragBit];
318 _oldPos = _dragBit.position;
319 ChangeSuperlayer(_dragBit, self.layer, self.layer.sublayers.count);
320 _dragBit.pickedUp = YES;
321 [[NSCursor closedHandCursor] push];
325 _dragBit.position = [self _convertPointFromWindowToLayer: _dragStartPos];
327 [self _findDropTarget: _dragStartPos];
332 - (void) mouseDragged: (NSEvent*)ev
335 // Get the mouse position, and see if we've moved 3 pixels since the mouseDown:
336 NSPoint pos = ev.locationInWindow;
337 if( fabs(pos.x-_dragStartPos.x)>=3 || fabs(pos.y-_dragStartPos.y)>=3 )
340 // Move the _dragBit (without animation -- it's unnecessary and slows down responsiveness):
341 CGPoint where = [self _convertPointFromWindowToLayer: pos];
342 where.x += _dragOffset.x;
343 where.y += _dragOffset.y;
345 CGPoint newPos = [_dragBit.superlayer convertPoint: where fromLayer: self.layer];
347 [CATransaction flush];
348 [CATransaction begin];
349 [CATransaction setValue:(id)kCFBooleanTrue
350 forKey:kCATransactionDisableActions];
351 _dragBit.position = newPos;
352 [CATransaction commit];
354 // Find what it's over:
355 [self _findDropTarget: pos];
360 - (void) _findDropTarget: (NSPoint)locationInWindow
362 locationInWindow.x += _dragOffset.x;
363 locationInWindow.y += _dragOffset.y;
364 id<BitHolder> target = (id<BitHolder>) [self hitTestPoint: locationInWindow
365 forLayerMatching: layerIsBitHolder
367 if( target == _oldHolder )
369 if( target != _dropTarget ) {
370 [_dropTarget willNotDropBit: _dragBit];
371 _dropTarget.highlighted = NO;
375 CGPoint targetPos = [(CALayer*)target convertPoint: _dragBit.position
376 fromLayer: _dragBit.superlayer];
377 if( [target canDropBit: _dragBit atPoint: targetPos]
378 && [_game canBit: _dragBit moveFrom: _oldHolder to: target] ) {
379 _dropTarget = target;
380 _dropTarget.highlighted = YES;
386 - (void) mouseUp: (NSEvent*)ev
390 // Update the drag tracking to the final mouse position:
391 [self mouseDragged: ev];
392 _dropTarget.highlighted = NO;
393 _dragBit.pickedUp = NO;
395 // Is the move legal?
396 if( _dropTarget && [_dropTarget dropBit: _dragBit
397 atPoint: [(CALayer*)_dropTarget convertPoint: _dragBit.position
398 fromLayer: _dragBit.superlayer]] ) {
399 // Yes, notify the interested parties:
400 [_oldHolder draggedBit: _dragBit to: _dropTarget];
401 [_game bit: _dragBit movedFrom: _oldHolder to: _dropTarget];
404 [_dropTarget willNotDropBit: _dragBit];
405 if( _oldSuperlayer ) {
406 ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
407 _dragBit.position = _oldPos;
408 [_oldHolder cancelDragBit: _dragBit];
410 [_dragBit removeFromSuperlayer];
414 // Just a click, without a drag:
415 _dropTarget.highlighted = NO;
416 _dragBit.pickedUp = NO;
417 ChangeSuperlayer(_dragBit, _oldSuperlayer, _oldLayerIndex);
418 [_oldHolder cancelDragBit: _dragBit];
419 if( ! [_game clickedBit: _dragBit] )
430 - (void)scrollWheel:(NSEvent *)e
432 self.perspective += e.deltaY * M_PI/180;
433 //Log(@"Perspective = %2.0f degrees (%5.3f radians)", self.perspective*180/M_PI, self.perspective);
438 #pragma mark INCOMING DRAGS:
441 // subroutine to call the target
442 static int tell( id target, SEL selector, id arg, int defaultValue )
444 if( target && [target respondsToSelector: selector] )
445 return (ssize_t) [target performSelector: selector withObject: arg];
451 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
453 _viewDropTarget = [self hitTestPoint: [sender draggingLocation]
454 forLayerMatching: layerIsDropTarget
456 _viewDropOp = _viewDropTarget ?[_viewDropTarget draggingEntered: sender] :NSDragOperationNone;
460 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
462 CALayer *target = [self hitTestPoint: [sender draggingLocation]
463 forLayerMatching: layerIsDropTarget
465 if( target == _viewDropTarget ) {
466 if( _viewDropTarget )
467 _viewDropOp = tell(_viewDropTarget,@selector(draggingUpdated:),sender,_viewDropOp);
469 tell(_viewDropTarget,@selector(draggingExited:),sender,0);
470 _viewDropTarget = target;
471 if( _viewDropTarget )
472 _viewDropOp = [_viewDropTarget draggingEntered: sender];
474 _viewDropOp = NSDragOperationNone;
479 - (BOOL)wantsPeriodicDraggingUpdates
481 return (_viewDropTarget!=nil);
484 - (void)draggingExited:(id <NSDraggingInfo>)sender
486 tell(_viewDropTarget,@selector(draggingExited:),sender,0);
487 _viewDropTarget = nil;
490 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
492 return tell(_viewDropTarget,@selector(prepareForDragOperation:),sender,YES);
495 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
497 return [_viewDropTarget performDragOperation: sender];
500 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
502 tell(_viewDropTarget,@selector(concludeDragOperation:),sender,0);
505 - (void)draggingEnded:(id <NSDraggingInfo>)sender
507 tell(_viewDropTarget,@selector(draggingEnded:),sender,0);