|
// |
|
// MusicStaff.m |
|
// |
|
// Created by Michael J Albanese on 10/9/12. |
|
// Copyright (c) 2014 WayFwonts.com. All rights reserved. |
|
// |
|
|
|
#import <CoreText/CoreText.h> |
|
#import "MusicStaffRenderMetrics.h" |
|
#import "NoteData+MoreFuncs.h" |
|
#import "SWFDraggable.h" |
|
#import "MusicStaffDropReply.h" |
|
#import "MusicStaffDropRequest.h" |
|
#import "MusicStaffDropZone.h" |
|
#import "MusicStaffNotifications_p.h" |
|
#import "MusicPageRenderCriteria.h" |
|
#import "MusicStaffLineVectorMap.h" |
|
#import "MusicNamesFormatter.h" |
|
#import "MusicStaff.h" |
|
|
|
#pragma mark - MusicStaff main Class begins |
|
|
|
@interface MusicStaff () |
|
@property (strong, nonatomic) NSMutableArray *dropZones; |
|
@property (strong, nonatomic) NSMutableArray *multiZoneHilite; |
|
@property (strong, nonatomic, readwrite) NSMutableArray *verticalBarCoordinates; |
|
@property (nonatomic, readwrite) CGFloat topLineY; |
|
@property (nonatomic, readwrite) CGFloat bottomLineY; |
|
|
|
/** attempt at quasi protected methods for subclasses to override */ |
|
- (void) mapStaffOctaveAndNoteCoordinates; |
|
- (void) calcDropZonesFromCombinedVectorMappings; |
|
@end |
|
|
|
@implementation MusicStaff |
|
/** Need to manually synthesize properties delared in the Protocol */ |
|
@synthesize staffId = _staffId; |
|
@synthesize clefId = _clefId; |
|
@synthesize staffMetrics = _staffMetrics; |
|
@synthesize isVisible = _isVisible; |
|
@synthesize isSelected = _isSelected; |
|
@synthesize lowestYCoordinate = _lowestYCoordinate; |
|
@synthesize centerYCoordinate = _centerYCoordinate; |
|
@synthesize staffYCenter = _staffYCenter; |
|
@synthesize middleXCoordinate = _middleXCoordinate; |
|
|
|
+ (MusicStaff *) musicStaffWithLayoutInfo:(MusicStaffLayoutInfo *)layoutInfo |
|
andMiddleXCoordinate:(float)middleX |
|
andLowestYCoordinate:(float)lowY |
|
andStaffMetrics:(MusicStaffRenderMetrics *)staffMetrics |
|
andPageRenderCriteria:(MusicPageRenderCriteria *)renderCriteria |
|
{ |
|
return [[self alloc] initWithLayoutInfo:layoutInfo |
|
andMiddleXCoordinate:middleX |
|
andLowestYCoordinate:lowY |
|
andStaffMetrics:staffMetrics |
|
andPageRenderCriteria:renderCriteria]; |
|
} |
|
|
|
- (id) initWithLayoutInfo:(MusicStaffLayoutInfo *)layoutInfo |
|
andMiddleXCoordinate:(float)middleX |
|
andLowestYCoordinate:(float)lowY |
|
andStaffMetrics:(MusicStaffRenderMetrics *)staffMetrics |
|
andPageRenderCriteria:(MusicPageRenderCriteria *)renderCriteria |
|
|
|
{ |
|
if (self = [super init]) { |
|
_isVisible = YES; |
|
_staffLayoutInfo = *layoutInfo; |
|
_middleXCoordinate = middleX; |
|
_lowestYCoordinate = lowY; |
|
_staffMetrics = staffMetrics; |
|
_pageRenderCriteria = renderCriteria; |
|
_lowestLineVectorMapIndex = _highestLineVectorMapIndex = kMaxStaffLines + 10; |
|
_dropZones = [NSMutableArray array]; |
|
_staffVectorMappings = [NSMutableArray array]; |
|
_verticalBarCoordinates = [NSMutableArray array]; |
|
_linesVectorMappingsIndex = [NSMutableArray array]; |
|
_spacesVectorMappingsIndex = [NSMutableArray array]; |
|
_loToHiCombinedMappingsIndex = [NSMutableArray array]; |
|
|
|
[self mapStaffOctaveAndNoteCoordinates]; |
|
[self calcDropZonesFromCombinedVectorMappings]; |
|
} |
|
return self; |
|
} |
|
|
|
- (void) dealloc |
|
{ |
|
[_staffVectorMappings removeAllObjects]; |
|
_staffVectorMappings = nil; |
|
|
|
[_dropZones removeAllObjects]; |
|
_dropZones = nil; |
|
|
|
[_multiZoneHilite removeAllObjects]; |
|
_multiZoneHilite = nil; |
|
|
|
[_staffVectorMappings removeAllObjects]; |
|
[_verticalBarCoordinates removeAllObjects]; |
|
[_linesVectorMappingsIndex removeAllObjects]; |
|
[_spacesVectorMappingsIndex removeAllObjects]; |
|
[_loToHiCombinedMappingsIndex removeAllObjects]; |
|
} |
|
|
|
- (kSongStaffId) staffId |
|
{ |
|
return _staffLayoutInfo.staffId; |
|
} |
|
|
|
- (kSongClefId) clefId |
|
{ |
|
return _staffLayoutInfo.clefId; |
|
} |
|
|
|
- (NSString *) myStaffName |
|
{ |
|
NSString *myName = nil; |
|
|
|
if (_staffLayoutInfo.staffId == kStaffTreble) { |
|
myName = @"Treble Staff"; |
|
} else if (_staffLayoutInfo.staffId == kStaffBass) { |
|
myName = @"Bass Staff"; |
|
} else if (_staffLayoutInfo.staffId == kStaffTABSix) { |
|
myName = @"Guitar TAB"; |
|
} else if (_staffLayoutInfo.staffId == kStaffTABFour) { |
|
myName = @"Bass Guitar TAB"; |
|
} else if (_staffLayoutInfo.staffId == kStaffMelody) { |
|
myName = @"Melody Staff"; |
|
} |
|
|
|
return myName; |
|
} |
|
|
|
#pragma mark - Custom Bounds Getters |
|
|
|
- (CGRect) retrieveCalcdBounds |
|
{ |
|
// Note: this is not a traditional CGRect as last element is not the height |
|
// but the bottom Y coordinate, e.g. these are 2 points TL and BR |
|
return CGRectMake(_leftmostX, _highestYCoordinate, _rightmostX, _lowestYCoordinate); |
|
} |
|
|
|
- (CGRect) retrieveSwipeUpBounds |
|
{ |
|
// for chords residing at/near bottom of staff, the starting point of |
|
// a swipe up often begins in the staff below this staff's actual bounds. |
|
// In order for swipeUp detection to appear accurate to the user, and deal |
|
// with the fact that iOS reports only the beginning touch of a swipe, we |
|
// temporarily extend our bottom bounds. |
|
return CGRectMake(_leftmostX, |
|
_highestYCoordinate, |
|
_rightmostX, |
|
_lowestYCoordinate + (2. * _staffMetrics.yGapBetweenLines)); |
|
} |
|
|
|
#pragma mark - Mapping Staff Verticies |
|
|
|
- (void) remapStaffCoordinates |
|
{ |
|
[self mapStaffOctaveAndNoteCoordinates]; |
|
[self calcDropZonesFromCombinedVectorMappings]; |
|
} |
|
|
|
- (void) mapStaffOctaveAndNoteCoordinates |
|
{ |
|
// All mapping is constructed from the bottom of the staff 'Up' to top |
|
|
|
float x = _middleXCoordinate; |
|
float y = _lowestYCoordinate; |
|
float z = 0.; |
|
int mapOffset = 0; |
|
_lowestLineVectorMapIndex = _highestLineVectorMapIndex = kMaxStaffLines + 10; |
|
_highestYCoordinate = 2000.0; // because in 2D 0,0 is top left |
|
float yincr = _staffMetrics.yGapBetweenLines; |
|
|
|
if (!_staffVectorMappings) { |
|
_staffVectorMappings = [NSMutableArray array]; |
|
_linesVectorMappingsIndex = [NSMutableArray array]; |
|
_spacesVectorMappingsIndex = [NSMutableArray array]; |
|
_loToHiCombinedMappingsIndex = [NSMutableArray array]; |
|
} else { |
|
[_staffVectorMappings removeAllObjects]; |
|
[_linesVectorMappingsIndex removeAllObjects]; |
|
[_spacesVectorMappingsIndex removeAllObjects]; |
|
[_loToHiCombinedMappingsIndex removeAllObjects]; |
|
} |
|
MusicStaffLineVectorMap *staffMapping = nil; |
|
|
|
// first loop for lines, from middle C Up |
|
for (int i = 0; i < _staffLayoutInfo.usedStaffLines; i++, mapOffset++) { |
|
staffMapping = [[MusicStaffLineVectorMap alloc] init]; |
|
MusicNoteToOctaveMap noteOctMap; |
|
noteOctMap.lineSpaceIndicator = kMusicLineZone; |
|
noteOctMap.octave = _staffLayoutInfo.lineNotes[i].octave; |
|
noteOctMap.noteId = _staffLayoutInfo.lineNotes[i].noteId; |
|
|
|
staffMapping.octaveInfo = noteOctMap; |
|
staffMapping.vec = [SWFVector3 vectorFromValues:x y:y z:z]; |
|
staffMapping.zoneType = kMusicLineZone; |
|
|
|
[_staffVectorMappings addObject:staffMapping]; |
|
|
|
if (i == _staffLayoutInfo.lineOffsetMiddleC) { |
|
_middleCVector = staffMapping.vec; |
|
} |
|
|
|
if (i == 0) { |
|
_lowestLineVector = staffMapping.vec; |
|
_lowestLineVectorMapIndex = i; |
|
} |
|
|
|
if (y < _highestYCoordinate) { |
|
_highestYCoordinate = y; |
|
_highestLineVectorMapIndex = i; |
|
} |
|
|
|
// update the lines index with offset of newly added line mapping |
|
[_linesVectorMappingsIndex addObject:@(_staffVectorMappings.count - 1)]; |
|
|
|
y -= yincr; |
|
} |
|
|
|
int numStaffPositions = _staffLayoutInfo.usedStaffLines + _staffLayoutInfo.usedStaffSpaces; |
|
|
|
// next loop maps the spaces, from B below middle C Up |
|
y = _lowestYCoordinate + (_staffMetrics.yGapBetweenLines / 2); |
|
for (int i = 0; i < _staffLayoutInfo.usedStaffSpaces && mapOffset < numStaffPositions; i++, mapOffset++) { |
|
staffMapping = [[MusicStaffLineVectorMap alloc] init]; |
|
MusicNoteToOctaveMap noteOctMap; |
|
noteOctMap.lineSpaceIndicator = kMusicSpaceZone; |
|
noteOctMap.octave = _staffLayoutInfo.spaceNotes[i].octave; |
|
noteOctMap.noteId = _staffLayoutInfo.spaceNotes[i].noteId; |
|
|
|
staffMapping.octaveInfo = noteOctMap; |
|
staffMapping.vec = [SWFVector3 vectorFromValues:x y:y z:z]; |
|
staffMapping.zoneType = kMusicSpaceZone; |
|
|
|
[_staffVectorMappings addObject:staffMapping]; |
|
|
|
if (i == 0) { |
|
_lowestSpaceVector = staffMapping.vec; |
|
} |
|
|
|
if (y < _highestYCoordinate) { |
|
_highestYCoordinate = y; |
|
} |
|
|
|
// update the spaces index with offset of newly added space mapping |
|
[_spacesVectorMappingsIndex addObject:@(_staffVectorMappings.count - 1)]; |
|
|
|
y -= yincr; |
|
} |
|
// _highestYCoordinate -= _staffMetrics.yGapBetweenLines; |
|
|
|
NSAssert(_highestYCoordinate > 0., @"NEGATIVE mapping of _highestYCoordinate not allowed"); |
|
|
|
// build the combined from the just composed spaces and lines separate indexes |
|
[self buildCombinedVectorMappingsIndex]; |
|
} |
|
|
|
#pragma mark - VectorMaps |
|
|
|
- (void) buildCombinedVectorMappingsIndex |
|
{ |
|
// Dependent upon both the _linesVectorMappingIndex and _spacesVectorMappingIndex |
|
// to already be populated with accurate indexes into _staffVectorMappings array. |
|
// This will build one array which when sequentially traverse the mappings by |
|
// encountering the standard alternating space-line staff mappings starting with the |
|
// lowest space mapping of this staff |
|
NSInteger totalMappings = _staffVectorMappings.count; |
|
|
|
if (_linesVectorMappingsIndex.count + _spacesVectorMappingsIndex.count > totalMappings) { |
|
return; |
|
} |
|
|
|
[_loToHiCombinedMappingsIndex removeAllObjects]; |
|
MusicStaffLineVectorMap *staffMapping = nil; |
|
NSInteger spaceOff = 0, lineOff = 0; |
|
BOOL doSpace = YES; |
|
|
|
for (NSInteger x = 0; x < totalMappings; x++) { |
|
|
|
if (doSpace && spaceOff < _spacesVectorMappingsIndex.count) { |
|
|
|
NSNumber *spaceIndex = [_spacesVectorMappingsIndex objectAtIndex:spaceOff++]; |
|
staffMapping = [_staffVectorMappings objectAtIndex:spaceIndex.integerValue]; |
|
|
|
if (staffMapping) { |
|
[_loToHiCombinedMappingsIndex addObject:@(spaceIndex.integerValue)]; |
|
} |
|
doSpace = NO; |
|
|
|
} else if (lineOff < _linesVectorMappingsIndex.count) { |
|
|
|
NSNumber *lineIndex = [_linesVectorMappingsIndex objectAtIndex:lineOff++]; |
|
staffMapping = [_staffVectorMappings objectAtIndex:lineIndex.integerValue]; |
|
|
|
if (staffMapping) { |
|
[_loToHiCombinedMappingsIndex addObject:@(lineIndex.integerValue)]; |
|
} |
|
doSpace = YES; |
|
} |
|
} |
|
} |
|
|
|
- (NSArray *) staffVectorMappingsBottomUp |
|
{ |
|
NSMutableArray *combinedMappings = [NSMutableArray array]; |
|
MusicStaffLineVectorMap *aMap; |
|
|
|
// While _staffVectorMappings does hold all mappings, their order |
|
// is grouped as all lines, then all spaces. This method produces |
|
// a interweaved resulting array as dictated by the combined indexes. |
|
for (NSNumber *indexNum in _loToHiCombinedMappingsIndex) { |
|
aMap = [_staffVectorMappings objectAtIndex:indexNum.integerValue]; |
|
|
|
// FIXME implement NSCopying protocol on vectorMaps and hand out copies ! |
|
[combinedMappings addObject:aMap]; |
|
} |
|
|
|
return combinedMappings; |
|
} |
|
|
|
- (MusicStaffLineVectorMap *) findStaffVectorMapForNoteId:(kMusicNoteId)noteId |
|
inOctave:(kMusicStaffOctave)oct |
|
{ |
|
MusicStaffLineVectorMap *staffMapping = nil; |
|
MusicStaffLineVectorMap *aMap; |
|
|
|
for (NSNumber *indexNum in _loToHiCombinedMappingsIndex) { |
|
aMap = [_staffVectorMappings objectAtIndex:indexNum.integerValue]; |
|
if (aMap.octaveInfo.octave == oct) { |
|
if (aMap.octaveInfo.noteId == noteId) { |
|
staffMapping = aMap; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
return staffMapping; |
|
} |
|
|
|
- (MusicStaffLineVectorMap *) findStaffVectorMapForNoteId:(kMusicNoteId)noteId |
|
inOctave:(kMusicStaffOctave)oct |
|
ofZoneType:(kMusicStaffZone)zoneType |
|
{ |
|
MusicStaffLineVectorMap *staffMapping = nil; |
|
MusicStaffLineVectorMap *aMap; |
|
|
|
for (NSNumber *indexNum in _loToHiCombinedMappingsIndex) { |
|
aMap = [_staffVectorMappings objectAtIndex:indexNum.integerValue]; |
|
if (aMap.octaveInfo.octave == oct) { |
|
if (aMap.octaveInfo.noteId == noteId) { |
|
if (aMap.zoneType == zoneType) { |
|
staffMapping = aMap; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
return staffMapping; |
|
} |
|
|
|
#pragma mark - Find Staff Coordinate methods |
|
|
|
- (CGPoint) findStaffCoordinateForNoteId:(kMusicNoteId)noteId |
|
inOctave:(kMusicStaffOctave)oct |
|
{ |
|
MusicStaffLineVectorMap *staffMapping = nil; |
|
CGPoint nowhere = CGPointMake(0, 0); |
|
int numStaffPositions = _staffLayoutInfo.usedStaffLines + _staffLayoutInfo.usedStaffSpaces; |
|
|
|
// simple linear scan for now, very small array. |
|
for (int i = 0; i < numStaffPositions; i++) { |
|
staffMapping = [_staffVectorMappings objectAtIndex:i]; |
|
if (staffMapping.octaveInfo.octave == oct) { |
|
if (staffMapping.octaveInfo.noteId == noteId) { |
|
return staffMapping.coordinate; |
|
} |
|
} |
|
} |
|
return nowhere; |
|
} |
|
|
|
- (CGPoint) findStaffCoordinateForNoteUsingItsOctave:(NoteData *)aNote |
|
{ |
|
SWFVector3 *staffVector = [self findStaffVectorForNoteUsingItsOctave:aNote]; |
|
|
|
return staffVector.pointFromVector; |
|
} |
|
|
|
- (kMusicStaffZone) findStaffZoneTypeForNoteId:(kMusicNoteId)noteId |
|
inOctave:(kMusicStaffOctave)oct |
|
{ |
|
kMusicStaffZone zoneType = kMusicStaffNOZone; |
|
MusicStaffLineVectorMap *staffMapping = nil; |
|
int numStaffPositions = _staffLayoutInfo.usedStaffLines + _staffLayoutInfo.usedStaffSpaces; |
|
|
|
// simple linear scan for now, very small array. |
|
for (int i = 0; i < numStaffPositions; i++) { |
|
staffMapping = [_staffVectorMappings objectAtIndex:i]; |
|
|
|
if (staffMapping.octaveInfo.octave == oct) { |
|
if (staffMapping.octaveInfo.noteId == noteId) { |
|
zoneType = staffMapping.zoneType; |
|
break; |
|
} |
|
} |
|
} |
|
return zoneType; |
|
} |
|
|
|
- (MusicStaffLineVectorMap *) findLowestLineVectorMap |
|
{ |
|
if (_lowestLineVectorMapIndex > kMaxStaffLines) { |
|
return nil; |
|
} |
|
MusicStaffLineVectorMap *staffMapping = [_staffVectorMappings objectAtIndex:_lowestLineVectorMapIndex]; |
|
|
|
return staffMapping; |
|
} |
|
|
|
- (MusicStaffLineVectorMap *) findHighestLineVectorMap |
|
{ |
|
if (_highestLineVectorMapIndex > kMaxStaffLines) { |
|
return nil; |
|
} |
|
MusicStaffLineVectorMap *staffMapping = [_staffVectorMappings objectAtIndex:_highestLineVectorMapIndex]; |
|
|
|
return staffMapping; |
|
} |
|
|
|
|
|
- (float) calcTopRealLineUsingMetrics |
|
{ |
|
// FIXME DANGER this assumes only 1 stub will be below and above |
|
CGFloat upperStub = _lowestYCoordinate - (6 * _staffMetrics.yGapBetweenLines); |
|
|
|
// calc top lines Y offset, and round to land on half pixel |
|
CGFloat topLine = upperStub + _staffMetrics.yGapBetweenLines; |
|
if (_staffMetrics.staffLineWidth == 1) { |
|
topLine = [MusicStaffRenderMetrics roundToLeastHalfPoint:topLine]; |
|
} |
|
return topLine; |
|
} |
|
|
|
- (float) calcBottomRealLineUsingMetrics |
|
{ |
|
// FIXME DANGER |
|
return _lowestYCoordinate - _staffMetrics.yGapBetweenLines; |
|
} |
|
|
|
- (BOOL) isYCoordinateAboveTopStaffLine:(float)proposedY |
|
{ |
|
if (proposedY < [self calcTopRealLineUsingMetrics]) { |
|
return YES; |
|
} |
|
|
|
return NO; |
|
} |
|
|
|
- (BOOL) isYCoordinateBelowBottomStaffLine:(float)proposedY |
|
{ |
|
if (proposedY > [self calcBottomRealLineUsingMetrics]) { |
|
return YES; |
|
} |
|
|
|
return NO; |
|
} |
|
|
|
#pragma mark - MusicStaffDisplayable Protocol Methods |
|
|
|
- (SWFVector3 *) middleCVector |
|
{ |
|
return _middleCVector; |
|
} |
|
|
|
- (SWFVector3 *) findStaffVertexForNoteId:(kMusicNoteId)noteId inOctave:(kMusicStaffOctave)oct |
|
{ |
|
MusicStaffLineVectorMap *staffMapping = nil; |
|
int numStaffPositions = _staffLayoutInfo.usedStaffLines + _staffLayoutInfo.usedStaffSpaces; |
|
|
|
// simple linear scan for now, very small array. |
|
for (int i = 0; i < numStaffPositions; i++) { |
|
staffMapping = [_staffVectorMappings objectAtIndex:i]; |
|
if (staffMapping.octaveInfo.octave == oct) { |
|
if (staffMapping.octaveInfo.noteId == noteId) { |
|
return staffMapping.vec; |
|
} |
|
} |
|
} |
|
return nil; |
|
} |
|
|
|
- (SWFVector3 *) findStaffVectorForNoteUsingItsOctave:(NoteData *)aNote |
|
{ |
|
MusicStaffLineVectorMap *staffMapping; |
|
SWFVector3 *staffVector = nil; |
|
int numOctaves; |
|
float noteGap = [self distanceBetweenStaffLines] / 2.0; |
|
|
|
for (NSNumber *indexNum in _loToHiCombinedMappingsIndex) { |
|
staffMapping = [_staffVectorMappings objectAtIndex:indexNum.integerValue]; |
|
|
|
if (aNote.noteId.integerValue == staffMapping.octaveInfo.noteId) { |
|
staffVector = [staffMapping.vec mutableCopy]; |
|
|
|
// simple case note matches found staff location |
|
if (aNote.octave.integerValue == staffMapping.octaveInfo.octave) { |
|
break; |
|
} |
|
|
|
// Adjust position factor diff of chordNotes octave and staff note's octave |
|
if (aNote.octave.integerValue > staffMapping.octaveInfo.octave) { |
|
numOctaves = aNote.octave.intValue - staffMapping.octaveInfo.octave; |
|
staffVector.y -= ((numOctaves * kNotesPerOctave) * noteGap); |
|
|
|
// NSLog(@"Staff adj Y coord for noteId: %d to >: %.2f NoteOct: %d", |
|
// aNote.noteId.intValue, staffVector.y, aNote.octave.integerValue); |
|
break; |
|
|
|
} else if (aNote.octave.integerValue < staffMapping.octaveInfo.octave) { |
|
numOctaves = staffMapping.octaveInfo.octave - aNote.octave.intValue; |
|
staffVector.y += ((numOctaves * kNotesPerOctave) * noteGap); |
|
|
|
NSLog(@"Staff adj Y coord for noteId: %d to <: %.2f NoteOct: %ld", |
|
aNote.noteId.intValue, staffVector.y, aNote.octave.longValue); |
|
|
|
break; |
|
} |
|
} |
|
} |
|
|
|
return staffVector; |
|
} |
|
|
|
- (kMusicStaffOctave) nearestOctaveForNote:(kMusicNoteId)noteId andVertex:(SWFVector3 *)vec |
|
{ |
|
kMusicStaffOctave oct = kOctaveNone; |
|
MusicStaffDropZone *mdz; |
|
|
|
// start at bottom zone and work up |
|
unsigned count = 0; |
|
while (count < _dropZones.count) { |
|
mdz = [_dropZones objectAtIndex:count]; |
|
SWFBoundingBox zbb = mdz.boundingZone; |
|
|
|
if (vec.y <= zbb.upr_y && vec.y >= zbb.llr_y) { |
|
if (vec.x >= zbb.llr_x && vec.x <= zbb.upr_x) { |
|
// cheap hit OR go after 'Z' too |
|
if (vec.z >= zbb.llr_z && vec.z <= zbb.upr_z) { |
|
oct = mdz.octave; |
|
break; |
|
} |
|
} |
|
} |
|
count++; |
|
} |
|
if (oct == kOctaveNone) { |
|
NSLog(@"Music Staff returning OCTAVE NONE for noteId: %ld", (long)noteId); |
|
} |
|
return oct; |
|
} |
|
|
|
- (MusicStaffDropZone *) nearestDropZoneFromVector:(SWFVector3 *)vec |
|
usingGap:(kDropZoneGap)dzGap |
|
{ |
|
MusicStaffDropZone *mdz = nil; |
|
kDropStatus hitStatus = kDropStatus_None; |
|
SWFVector3 *targetVec = vec; |
|
float gapFudge = _staffMetrics.yGapBetweenLines / (float)dzGap; |
|
|
|
// NSLog(@"MusicStaff nearestZone using GAP: %f", gapFudge); |
|
|
|
unsigned count = (unsigned)[_dropZones count]; |
|
while (count--) { |
|
mdz = [_dropZones objectAtIndex:count]; |
|
|
|
hitStatus = [mdz detectHitFromVector:targetVec gapAllowance:gapFudge]; |
|
if (hitStatus == kDropStatus_DirectHit || hitStatus == kDropStatus_NearZone) { |
|
break; |
|
} |
|
mdz = nil; |
|
} |
|
return mdz; |
|
} |
|
|
|
- (float) distanceBetweenStaffLines |
|
{ |
|
return _staffMetrics.yGapBetweenLines; |
|
} |
|
|
|
- (SWFVector3 *) lowestStaffPositionVector |
|
{ |
|
if (_lowestLineVector && _lowestSpaceVector) { |
|
if (_lowestLineVector.y < _lowestSpaceVector.y) { |
|
return _lowestLineVector; |
|
} else { |
|
return _lowestSpaceVector; |
|
} |
|
} |
|
return nil; |
|
} |
|
|
|
- (NSArray *) allDropZones |
|
{ |
|
return self.dropZones; |
|
} |
|
|
|
- (NSArray *) measureBarCoordinatesForLine |
|
{ |
|
return [NSArray arrayWithArray:_verticalBarCoordinates]; |
|
} |
|
|
|
#pragma mark - Drop Zone Methods |
|
|
|
- (void) calcDropZonesFromCombinedVectorMappings |
|
{ |
|
MusicStaffLineVectorMap *staffMapping = nil; |
|
MusicStaffDropZone *last1back = nil; |
|
MusicStaffDropZone *dz; |
|
|
|
[_dropZones removeAllObjects]; |
|
_currZoneHilite = nil; |
|
|
|
// calc the uniform height of all drop zones |
|
float zoneHeight = [self calcUniformDropZoneHeightBasedOnYGap]; |
|
|
|
// update the members consulted for swipe up and boundary dimensions |
|
[self establishLeftAndRightXBoundaryFromMetrics]; |
|
|
|
// accessing the _staffVectorMappings thru the combined indexing array |
|
// delivers staff positions in sequence starting at the lowest Space of staff |
|
NSInteger dropZoneId = 1; |
|
for (NSNumber *indexOffset in _loToHiCombinedMappingsIndex) { |
|
|
|
staffMapping = [_staffVectorMappings objectAtIndex:indexOffset.integerValue]; |
|
if (staffMapping) { |
|
// calc a bounding rect based on 1/2 zoneHeight either side of the |
|
// 'Y' postion of staffMapping of given line or space |
|
SWFVector3 *mappingVector = staffMapping.vec; |
|
float zoneTop = mappingVector.y - (zoneHeight / 2.); |
|
float zoneBottom = mappingVector.y + (zoneHeight / 2.); |
|
|
|
SWFBoundingBox boundingBox; |
|
boundingBox.llr_x = _leftmostX; |
|
boundingBox.llr_y = zoneBottom; |
|
boundingBox.llr_z = 0; |
|
boundingBox.upr_x = _rightmostX; |
|
boundingBox.upr_y = zoneTop; |
|
boundingBox.upr_z = 0; |
|
|
|
dz = [[MusicStaffDropZone alloc] initWithBoundingBox:&boundingBox |
|
andType:staffMapping.zoneType]; |
|
dz.noteId = staffMapping.octaveInfo.noteId; |
|
dz.octave = staffMapping.octaveInfo.octave; |
|
dz.staffId = _staffLayoutInfo.staffId; |
|
|
|
// do the linked list thing |
|
if (indexOffset.integerValue == 0) { |
|
last1back = dz; |
|
} else { |
|
dz.previousZone = last1back; |
|
last1back.nextZone = dz; |
|
last1back = dz; |
|
} |
|
dz.zoneId = dropZoneId; |
|
[self addDropZone:dz]; |
|
|
|
dropZoneId++; |
|
} |
|
} |
|
// NSLog(@"MyStaff DropZones--> %@", [self dropZoneDescriptions]); |
|
} |
|
|
|
- (void) establishLeftAndRightXBoundaryFromMetrics |
|
{ |
|
_leftmostX = _staffMetrics.leftMostXOffset; |
|
_rightmostX = _leftmostX; |
|
_rightmostX += (_staffMetrics.numberOfMeasures * |
|
_staffMetrics.lineLengthOneMeasure); |
|
} |
|
|
|
- (float) calcUniformDropZoneHeightBasedOnYGap |
|
{ |
|
float spaceBetweenLines = _staffMetrics.yGapBetweenLines; |
|
float zoneHeight = (spaceBetweenLines * _staffMetrics.dropZonePercentage); |
|
zoneHeight = [MusicStaffRenderMetrics roundToLeastHalfPoint:zoneHeight]; |
|
|
|
return zoneHeight; |
|
} |
|
|
|
- (void) addDropZone:(id<SWFDropZone_p>)dropZone |
|
{ |
|
[self.dropZones addObject:dropZone]; |
|
} |
|
|
|
- (void) clearDropZones |
|
{ |
|
[self.dropZones removeAllObjects]; |
|
} |
|
|
|
- (BOOL) doDropZoneSetsMatch:(NSArray *)zoneSet1 secondSet:(NSArray *)zoneSet2 |
|
{ |
|
BOOL setsEqual = NO; |
|
if (zoneSet1.count != zoneSet2.count) { |
|
return NO; |
|
} |
|
MusicStaffDropZone *zoneA; |
|
MusicStaffDropZone *zoneB; |
|
setsEqual = YES; |
|
for (int i = 0; i < zoneSet1.count; i++) { |
|
zoneA = [zoneSet1 objectAtIndex:i]; |
|
zoneB = [zoneSet2 objectAtIndex:i]; |
|
|
|
// currently just look at noteId's |
|
if (zoneA.noteId == zoneB.noteId) { |
|
setsEqual = YES; |
|
|
|
#ifdef MORE_DETAILED_MATCHING_NOT_YET_NEEDED |
|
if (zoneA.octave == zoneB.octave) { |
|
if (zoneA.center.y == zoneB.center.y) { |
|
continue; |
|
} else { |
|
setsEqual = NO; |
|
break; |
|
} |
|
} else { |
|
setsEqual = NO; |
|
break; |
|
} |
|
#endif |
|
} else { |
|
setsEqual = NO; |
|
break; |
|
} |
|
} |
|
return setsEqual; |
|
} |
|
|
|
#pragma mark - SWFDroppable Protocol Methods |
|
|
|
- (id<SWFDropZone_p>) queryDropZoneAt:(float)x y:(float)y z:(float)z |
|
{ |
|
MusicStaffDropZone *retZone = nil; |
|
SWFVector3 *targetVec = [SWFVector3 vectorFromValues:x y:y z:z]; |
|
|
|
// Being a little forgiving here as point-perfect direct hits for |
|
// drag an drop are a little much to ask of users |
|
retZone = [self nearestDropZoneFromVector:targetVec |
|
usingGap:kDropZoneGap_ThirdSpace]; |
|
|
|
return retZone; |
|
} |
|
|
|
- (SWFDropReply *) dropRequest:(SWFDropRequest *)dr |
|
{ |
|
MusicStaffDropRequest *dropRequest = (MusicStaffDropRequest *)dr; |
|
|
|
if ([dropRequest isGroupRequest]) { |
|
return [self handleGroupDropRequest:dr]; |
|
} else { |
|
return [self handleSoloDropRequest:dr]; |
|
} |
|
} |
|
|
|
- (SWFDropReply *) handleSoloDropRequest:(SWFDropRequest *)dr |
|
{ |
|
MusicStaffDropReply *reply = nil; |
|
MusicStaffDropRequest *dropRequest = (MusicStaffDropRequest *)dr; |
|
MusicStaffDropZone *mdz; |
|
bool hitZone = NO; |
|
kDropStatus hitStatus = kDropStatus_None; |
|
SWFVector3 *requestPos = dropRequest.requestPos; |
|
|
|
mdz = (MusicStaffDropZone *)[self queryDropZoneAt:requestPos.x y:requestPos.y z:requestPos.z]; |
|
if (mdz != nil) { |
|
hitZone = YES; |
|
hitStatus = kDropStatus_DirectHit; |
|
} |
|
|
|
// unhilite prev zone if its not same as this (potential) hit |
|
if (_currZoneHilite != nil) { |
|
if (mdz == nil || ((mdz != nil) && (_currZoneHilite != mdz)) ) { |
|
[self unHiliteZone:_currZoneHilite]; |
|
_currZoneHilite = nil; |
|
} |
|
} |
|
|
|
// take care of no hit |
|
if (!hitZone) { |
|
// unhilite any previsouly lit zone |
|
if (_currZoneHilite != nil) { |
|
[self unHiliteZone:_currZoneHilite]; |
|
_currZoneHilite = nil; |
|
} |
|
// inform Draggable no hit detected on valid dropZone |
|
reply = [[MusicStaffDropReply alloc] initWith:kDropStatus_None |
|
andOrigRequest:dropRequest]; |
|
reply.staffId = self.staffId; |
|
|
|
} else { |
|
// Hilite the new hit zone |
|
if (_currZoneHilite == nil) { |
|
[self hiliteZone:mdz]; |
|
_currZoneHilite = mdz; |
|
} |
|
|
|
// simple Rules Passage, either direct or near hit a valid dropzone |
|
reply = [[MusicStaffDropReply alloc] initWith:hitStatus |
|
andOrigRequest:dropRequest |
|
andHitZone:mdz |
|
andNoteId:mdz.noteId]; |
|
reply.staffId = self.staffId; |
|
} |
|
return reply; |
|
} |
|
|
|
- (SWFDropReply *) handleGroupDropRequest:(SWFDropRequest *)dr |
|
{ |
|
MusicStaffDropReply *reply = nil; |
|
MusicStaffDropRequest *dropRequest = (MusicStaffDropRequest *)dr; |
|
MusicStaffDropZone *mdz; |
|
kDropStatus hitStatus = kDropStatus_None; |
|
NSArray *acceptableHiliteZones = nil; |
|
NSMutableArray *hitZones = [[NSMutableArray alloc] init]; |
|
BOOL allZonesMatch = NO; |
|
|
|
NSArray *arrayOfPos = dropRequest.arrayPos; |
|
for (int i = 0; i < arrayOfPos.count; i++) { |
|
SWFVector3 *vec = [arrayOfPos objectAtIndex:i]; |
|
float stupidX = vec.x, stupidY = vec.y, stupidZ = vec.z; |
|
|
|
mdz = (MusicStaffDropZone *)[self queryDropZoneAt:stupidX y:stupidY z:stupidZ]; |
|
if (mdz != nil) { |
|
[hitZones addObject:mdz]; |
|
|
|
// FIXME Consider producing an array of hitZone status's, one for each vec |
|
hitStatus = kDropStatus_DirectHit; |
|
} |
|
} |
|
|
|
if (hitZones.count == arrayOfPos.count) { |
|
if (self.multiZoneHilite == nil) { |
|
// only do hiliting of multi zones if request supplied matching zoneIds |
|
acceptableHiliteZones = dropRequest.acceptableHiliteZones; |
|
if ([self doDropZoneSetsMatch:hitZones secondSet:acceptableHiliteZones]) { |
|
[self hiliteMultiZones:hitZones]; |
|
_multiZoneHilite = hitZones; |
|
allZonesMatch = YES; |
|
|
|
NSLog(@"MusicStaffId %d All Zones MATCH:\n %@", self.staffId, hitZones); |
|
} |
|
|
|
} else { |
|
// verify that existing hilites may stay that way |
|
acceptableHiliteZones = dropRequest.acceptableHiliteZones; |
|
if ([self doDropZoneSetsMatch:hitZones secondSet:acceptableHiliteZones]) { |
|
allZonesMatch = YES; |
|
} |
|
} |
|
|
|
// only if hits on all requested zones return a happy Reply |
|
if (allZonesMatch) { |
|
reply = [[MusicStaffDropReply alloc] initWith:hitStatus |
|
andOrigRequest:dropRequest |
|
andZonesArray:hitZones]; |
|
reply.staffId = self.staffId; |
|
|
|
return reply; |
|
} |
|
|
|
} else if (hitZones.count > 0) { |
|
NSLog(@"MusicStaffId %d PARTIAL Zone Match: %d", self.staffId, hitZones.count); |
|
} |
|
|
|
// else |
|
{ |
|
// unHilite previous set of multiZones |
|
if (self.multiZoneHilite != nil) { |
|
[self unHiliteMultiZones:_multiZoneHilite]; |
|
[_multiZoneHilite removeAllObjects]; |
|
_multiZoneHilite = nil; |
|
} |
|
|
|
// inform Draggable no hit detected on any valid dropZone |
|
reply = [[MusicStaffDropReply alloc] initWith:kDropStatus_None |
|
andOrigRequest:dropRequest]; |
|
reply.staffId = self.staffId; |
|
} |
|
|
|
return reply; |
|
} |
|
|
|
- (void) dropComplete:(SWFDropReply *)dropReply |
|
{ |
|
// MusicStaffDropReply *mdr = (MusicStaffDropReply *)dropReply; |
|
|
|
// unhilite previsouly lit zone |
|
if (_currZoneHilite != nil) { |
|
[self unHiliteZone:_currZoneHilite]; |
|
_currZoneHilite = nil; |
|
} |
|
} |
|
|
|
- (void) cancelAllRequests |
|
{ |
|
// unhilite any previsouly lit zone |
|
if (_currZoneHilite != nil) { |
|
[self unHiliteZone:_currZoneHilite]; |
|
_currZoneHilite = nil; |
|
} |
|
|
|
if (self.multiZoneHilite != nil) { |
|
for (MusicStaffDropZone *mdz in self.multiZoneHilite) { |
|
[self unHiliteZone:mdz]; |
|
} |
|
[self.multiZoneHilite removeAllObjects]; |
|
self.multiZoneHilite = nil; |
|
} |
|
} |
|
|
|
- (NSString *) dropZoneDescriptions |
|
{ |
|
NSMutableString *mutString = [NSMutableString string]; |
|
for (MusicStaffDropZone *z in _dropZones) { |
|
NSString *firstLine = [NSString stringWithFormat:@"ZoneId: %d staffId: %d noteId: %d", |
|
z.zoneId, z.staffId, z.noteId]; |
|
|
|
NSString *secondLine = [NSString stringWithFormat:@"ZoneCenter: %@ boundingBox: %@", |
|
z.center, |
|
NSStringFromCGRect(z.boundingRect)]; |
|
[mutString appendFormat:@"\n %@ %@", firstLine, secondLine]; |
|
} |
|
|
|
return mutString; |
|
} |
|
|
|
- (void) hilightOptions:(kDropHilite)clue |
|
{ |
|
} |
|
|
|
#pragma mark - HiLight UnHilight workers call Delegate Notifcation Protocol |
|
|
|
- (void) hiliteMultiZones:(NSArray *)zonesArray |
|
{ |
|
for (MusicStaffDropZone *mdz in zonesArray) { |
|
[self hiliteZone:mdz]; |
|
} |
|
} |
|
|
|
- (void) unHiliteMultiZones:(NSArray *)zonesArray |
|
{ |
|
for (MusicStaffDropZone *mdz in zonesArray) { |
|
[self unHiliteZone:mdz]; |
|
} |
|
} |
|
|
|
- (void) hiliteZone:(MusicStaffDropZone *)dz |
|
{ |
|
if (_staffDelegate) |
|
[_staffDelegate musicStaff:self willHiliteZone:dz]; |
|
|
|
#ifdef NO_HILIGHTING_IN_THIS_STAFF |
|
if (dz.type == kMusicSpaceZone) { |
|
[dz.swfMesh.isglMeshNode setMaterial:_hiliteSpaceMaterial]; |
|
dz.swfMesh.isglMeshNode.alpha = 0.6; |
|
|
|
} else if (dz.type == kMusicLineZone) { |
|
[dz.swfMesh.isglMeshNode setMaterial:_hiliteLineMaterial]; |
|
} |
|
|
|
// FIXME toggle x or -x depending on users finger |
|
|
|
// display hint note leter |
|
_hintLetterOrigPos = _hintLetters[dz.noteId].position; |
|
_hintLetters[dz.noteId].position = iv3(-2.0, dz.swfMesh.meshCenter.y, _hintLetterOrigPos.z); |
|
#endif |
|
} |
|
|
|
- (void) unHiliteZone:(MusicStaffDropZone *)dz |
|
{ |
|
if (_staffDelegate) |
|
[_staffDelegate musicStaff:self willUnHiliteZone:dz]; |
|
|
|
#ifdef NO_HILIGHTING_IN_THIS_STAFF |
|
// call into the wrapped Isgl mesh |
|
if (dz.type == kMusicSpaceZone) { |
|
[dz.swfMesh.isglMeshNode setMaterial:_spaceMaterial]; |
|
dz.swfMesh.isglMeshNode.alpha = 0.03; |
|
|
|
} else if (dz.type == kMusicLineZone) { |
|
[dz.swfMesh.isglMeshNode setMaterial:_lineMaterial]; |
|
} |
|
|
|
// pull hint note letter out of view |
|
_hintLetters[dz.noteId].position = iv3(-20.0, _hintLetterOrigPos.y, _hintLetterOrigPos.z); |
|
#endif |
|
} |
|
|
|
- (NSDictionary *) textAttributesDictionaryForClef |
|
{ |
|
NSMutableDictionary *attrsDict = [NSMutableDictionary dictionary]; |
|
|
|
if (_staffMetrics.musicSymbolsUnicodeFont != nil) { |
|
attrsDict[NSFontAttributeName] = _staffMetrics.musicSymbolsUnicodeFont; |
|
|
|
// ios defaults to horizontal, here we set to vertical for the music symbols |
|
attrsDict[NSVerticalGlyphFormAttributeName] = @(1); |
|
|
|
} else { |
|
// Exit if No Font supplied !!!! |
|
return nil; |
|
} |
|
|
|
if (_staffMetrics.clefAlphaComponent > 0. && _staffMetrics.clefAlphaComponent < 1.0) { |
|
attrsDict[NSForegroundColorAttributeName] = [_staffMetrics.lineColor colorWithAlphaComponent:_staffMetrics.clefAlphaComponent]; |
|
|
|
} else if (_staffMetrics.clefAlphaComponent == 1.0) { |
|
attrsDict[NSForegroundColorAttributeName] = _staffMetrics.lineColor; |
|
} |
|
|
|
return attrsDict; |
|
} |
|
|
|
- (void) pushContextIfEmpty:(CGContextRef)ctx |
|
{ |
|
if (CGContextIsPathEmpty(ctx)) { |
|
UIGraphicsPushContext(ctx); |
|
|
|
} else { |
|
NSLog(@"Staff ContextPathNOT Empty"); |
|
} |
|
} |
|
|
|
- (void) popContextIfEmpty:(CGContextRef)ctx |
|
{ |
|
if (CGContextIsPathEmpty(ctx)) { |
|
UIGraphicsPopContext(); |
|
} |
|
} |
|
|
|
#pragma mark - Draw Rendering Methods |
|
|
|
- (void) drawStaffInContext:(CGContextRef)ctx withMeasureCount:(int)measureCount |
|
{ |
|
CGFloat leftX = _staffMetrics.leftMostXOffset; |
|
CGFloat rightX = leftX + (measureCount * _staffMetrics.lineLengthOneMeasure); |
|
CGFloat lineGap = _staffMetrics.yGapBetweenLines; |
|
|
|
// UIGraphicsPushContext(ctx); |
|
[self pushContextIfEmpty:ctx]; |
|
|
|
CGContextSetStrokeColorWithColor(ctx, _staffMetrics.lineColor.CGColor); |
|
CGContextSetLineWidth(ctx, _staffMetrics.staffLineWidth); |
|
|
|
// DANGER this assumes only 1 stub will be below and above |
|
CGFloat upperStubY = _lowestYCoordinate - (6 * lineGap); |
|
|
|
// calc top lines Y offset, and round to land on half pixel |
|
CGFloat firstY = upperStubY + lineGap; |
|
if (_staffMetrics.staffLineWidth == 1) { |
|
firstY = [MusicStaffRenderMetrics roundToLeastHalfPoint:firstY]; |
|
} |
|
|
|
// FIXME ? these may need conditional half pixel tweaks (or whole pixel) |
|
CGFloat barLineTopY = firstY - (_staffMetrics.staffLineWidth / 2); |
|
CGFloat barLineBottomY = firstY + (_staffMetrics.staffLineWidth / 2); |
|
|
|
int numFixedLines = _staffMetrics.fixedLineCount; // 5; |
|
for (int x=0; x < numFixedLines; x++) { |
|
CGContextMoveToPoint(ctx, leftX, firstY); |
|
CGContextAddLineToPoint(ctx, rightX, firstY); |
|
CGContextStrokePath(ctx); |
|
firstY += lineGap; |
|
if (x < (numFixedLines - 1)) { |
|
barLineBottomY += lineGap; |
|
} |
|
} |
|
// UIGraphicsPopContext(); |
|
[self popContextIfEmpty:ctx]; |
|
|
|
// now draw the vertical bar lines, optionally skipping last for open bar |
|
[_verticalBarCoordinates removeAllObjects]; |
|
SWFVector3 *barVec; |
|
|
|
BOOL skipBarLine = NO; |
|
int barCount = measureCount + 1; |
|
float barX = _staffMetrics.leftMostXOffset; |
|
float measureLength = _staffMetrics.lineLengthOneMeasure; |
|
|
|
// UIGraphicsPushContext(ctx); |
|
[self pushContextIfEmpty:ctx]; |
|
|
|
CGContextSetStrokeColorWithColor(ctx, _staffMetrics.lineColor.CGColor); |
|
|
|
for (int z = 0; z < barCount; z++) { |
|
if (z == 0) { |
|
CGContextSetLineWidth(ctx, _staffMetrics.firstMeasureBarLineWidth); |
|
skipBarLine = _staffMetrics.skipFirstMeasureBarLine; |
|
|
|
} else { |
|
CGContextSetLineWidth(ctx, _staffMetrics.measureBarLineWidth); |
|
} |
|
|
|
// draw vertical bars, ...but sometimes don't draw the last bar (guitar TAB) |
|
if (skipBarLine == NO) { |
|
if (((_staffMetrics.lastMeasureOpenBar == YES) && (z + 1 < barCount)) || |
|
(_staffMetrics.lastMeasureOpenBar == NO)) { |
|
CGContextMoveToPoint(ctx, barX, barLineTopY); |
|
CGContextAddLineToPoint(ctx, barX, barLineBottomY); |
|
CGContextStrokePath(ctx); |
|
} |
|
} |
|
|
|
// stash their locations for further use |
|
barVec = [SWFVector3 vectorFromValues:barX y:barLineTopY z:barLineBottomY]; |
|
[_verticalBarCoordinates addObject:barVec]; |
|
|
|
barX += measureLength; |
|
skipBarLine = NO; |
|
} |
|
// update properties for drawing down the call stack |
|
_topLineY = barLineTopY; |
|
_bottomLineY = barLineBottomY; |
|
|
|
CGContextSetLineWidth(ctx, _staffMetrics.staffLineWidth); |
|
// UIGraphicsPopContext(); |
|
[self popContextIfEmpty:ctx]; |
|
} |
|
|
|
#pragma mark - Clef Sign Calculations |
|
|
|
- (CGRect) boundingRectForClef |
|
{ |
|
NSString *clefString = [[MusicNamesFormatter sharedInstance] clefNameForId:self.clefId]; |
|
NSDictionary *attrsDict = [self textAttributesDictionaryForClef]; |
|
if (clefString == nil || attrsDict == nil) { |
|
return CGRectZero; |
|
} |
|
|
|
CGPoint ptOrigin = CGPointMake(_staffMetrics.leftMostXOffset, _topLineY); |
|
CGRect textRect = [self boundingRectForClefString:clefString |
|
usingAttrs:attrsDict atOrigin:ptOrigin]; |
|
|
|
return textRect; |
|
} |
|
|
|
- (CGRect) boundingRectForClefString:(NSString *)clefString |
|
usingAttrs:(NSDictionary *)attrsDict atOrigin:(CGPoint)ptOrigin |
|
{ |
|
if (clefString == nil || attrsDict == nil) { |
|
return CGRectZero; |
|
} |
|
CGRect textRect = CGRectZero; |
|
|
|
// obtain bounding box for first glyph in surrogate pair codepoints, |
|
// used for left/top/height |
|
CGRect bbBox = [self getBoundingRectForGlyphFromString:clefString |
|
withFont:attrsDict[NSFontAttributeName]]; |
|
CGFloat glyphLeftOffset = bbBox.origin.x; |
|
if (glyphLeftOffset < 0) { |
|
glyphLeftOffset = fabsf(glyphLeftOffset); |
|
} |
|
|
|
// If the glyph is positioned 'down' e.g. positive Y, then negate that |
|
CGFloat glyphTopOffset = 0; |
|
if (bbBox.origin.y > 0) { |
|
glyphTopOffset = bbBox.origin.y * -1; |
|
glyphTopOffset = [MusicStaffRenderMetrics roundToLeastHalfPoint:glyphTopOffset]; |
|
} |
|
|
|
// position top/left account for any left negative bearing, or top positive |
|
textRect.origin.x = (ptOrigin.x + glyphLeftOffset + _staffMetrics.measureBarLineWidth); |
|
textRect.origin.x = [MusicStaffRenderMetrics roundToLeastHalfPoint:textRect.origin.x]; |
|
textRect.origin.y = ptOrigin.y + glyphTopOffset + _staffMetrics.staffLineWidth; |
|
|
|
// can only rely on the .height being accurate, width is incorrect at 1 |
|
CGSize strSize = [clefString sizeWithAttributes:attrsDict]; |
|
textRect.size.height = ceilf(strSize.height); |
|
textRect.size.width = ceilf(bbBox.size.width); |
|
|
|
return textRect; |
|
} |
|
|
|
- (CGPoint) glyphOffsetFromString:(NSString *)glyphString withFont:(UIFont *)fnt |
|
{ |
|
// obtain bounding box for first glyph in surrogate pair codepoints |
|
CGRect bbBox = [self getBoundingRectForGlyphFromString:glyphString |
|
withFont:fnt]; |
|
CGFloat glyphLeftOffset = bbBox.origin.x; |
|
if (glyphLeftOffset < 0) { |
|
glyphLeftOffset = fabsf((float)glyphLeftOffset); |
|
} |
|
glyphLeftOffset = [MusicStaffRenderMetrics roundToLeastHalfPoint:glyphLeftOffset]; |
|
|
|
// If the glyph is positioned 'down' e.g. positive Y, then negate that |
|
CGFloat glyphTopOffset = 0; |
|
if (bbBox.origin.y > 0) { |
|
glyphTopOffset = bbBox.origin.y * -1; |
|
glyphTopOffset = [MusicStaffRenderMetrics roundToLeastHalfPoint:glyphTopOffset]; |
|
} |
|
|
|
return CGPointMake(glyphLeftOffset, glyphTopOffset); |
|
} |
|
|
|
- (CGRect) getBoundingRectForGlyphFromString:(NSString *)string withFont:(UIFont *)fnt |
|
{ |
|
CGRect bbRectFirstGlyph; |
|
|
|
// get characters from NSString |
|
NSUInteger len = [string length]; |
|
UniChar *characters = (UniChar *)malloc(sizeof(UniChar)*len); |
|
CFStringGetCharacters((__bridge CFStringRef)string, CFRangeMake(0, [string length]), characters); |
|
CTFontRef coreTextFont = CTFontCreateWithName((CFStringRef)fnt.fontName, fnt.pointSize, NULL); |
|
|
|
// allocate glyphs and bounding box arrays for holding the result |
|
// assuming that each character is only one glyph, which is wrong |
|
CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph)*len); |
|
CTFontGetGlyphsForCharacters(coreTextFont, characters, glyphs, len); |
|
|
|
// get bounding boxes for glyphs |
|
CGRect *bb = (CGRect *)malloc(sizeof(CGRect)*len); |
|
CTFontGetBoundingRectsForGlyphs(coreTextFont, kCTFontDefaultOrientation, glyphs, bb, len); |
|
|
|
// only interested in first glyph of surrogate pair that identify Clefs |
|
bbRectFirstGlyph = bb[0]; |
|
|
|
CFRelease(coreTextFont); |
|
free(characters); |
|
free(glyphs); |
|
free(bb); |
|
|
|
return bbRectFirstGlyph; |
|
} |
|
|
|
#pragma mark - Draw Clef |
|
|
|
- (void) drawClefOnStaffInContext:(CGContextRef)ctx |
|
{ |
|
NSString *clefString = [[MusicNamesFormatter sharedInstance] clefNameForId:self.clefId]; |
|
NSDictionary *attrsDict = [self textAttributesDictionaryForClef]; |
|
if (clefString == nil || attrsDict == nil) { |
|
return; |
|
} |
|
|
|
CGPoint ptOrigin = CGPointMake(_staffMetrics.leftMostXOffset, _topLineY); |
|
CGRect textRect = [self boundingRectForClefString:clefString |
|
usingAttrs:attrsDict atOrigin:ptOrigin]; |
|
CGRect bbBox = [self boundingRectForClef]; |
|
|
|
NSLog(@"Clef: %@ bbBox: %@\n\t\t\t\ttextRect: %@", clefString, NSStringFromCGRect(bbBox), NSStringFromCGRect(textRect)); |
|
|
|
UIGraphicsPushContext(ctx); |
|
[clefString drawInRect:textRect withAttributes:attrsDict]; |
|
UIGraphicsPopContext(); |
|
} |
|
|
|
- (CGRect) getBoundingRectsForGlyphUsingPathWithString:(NSString *)string |
|
andFont:(UIFont *)fnt |
|
{ |
|
// get characters from NSString |
|
NSUInteger len = [string length]; |
|
UniChar *characters = (UniChar *)malloc(sizeof(UniChar)*len); |
|
CFStringGetCharacters((__bridge CFStringRef)string, CFRangeMake(0, [string length]), characters); |
|
CTFontRef coreTextFont = CTFontCreateWithName((CFStringRef)fnt.fontName, fnt.pointSize, NULL); |
|
|
|
// allocate glyphs and bounding box arrays for holding the result |
|
// assuming that each character is only one glyph, which is wrong |
|
CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph)*len); |
|
CTFontGetGlyphsForCharacters(coreTextFont, characters, glyphs, len); |
|
|
|
CGPathRef glyphPath = CTFontCreatePathForGlyph(coreTextFont, glyphs[0], NULL); |
|
CGRect rect = CGPathGetBoundingBox(glyphPath); |
|
|
|
CFRelease(coreTextFont); |
|
CGPathRelease(glyphPath); |
|
free(characters); |
|
free(glyphs); |
|
|
|
return rect; |
|
} |
|
|
|
#pragma mark - Stub Lines |
|
|
|
- (BOOL) notePositionNeedsStubLine:(SWFVector3 *)noteVec |
|
{ |
|
BOOL needsStub = YES; |
|
float proposedY = noteVec.pointFromVector.y; |
|
float topRealY = [self calcTopRealLineUsingMetrics]; |
|
float bottomRealY = [self calcBottomRealLineUsingMetrics]; |
|
|
|
if (proposedY >= topRealY && proposedY <= bottomRealY) { |
|
// nothing to do, proposed location is in staff main body |
|
needsStub = NO; |
|
} |
|
return needsStub; |
|
} |
|
|
|
- (float) stubLineYCoordinateForNoteId:(kMusicNoteId)noteId |
|
inOctave:(kMusicStaffOctave)octave |
|
withNotePosition:(SWFVector3 *)noteVec |
|
{ |
|
float stubY = -1.; |
|
float proposedY = 0.; |
|
MusicStaffLineVectorMap *staffVecMap = nil; |
|
float spaceHalfLineCheck = [self distanceBetweenStaffLines] / 2.0; |
|
|
|
if ([self notePositionNeedsStubLine:noteVec] == NO) { |
|
// nothing to do, proposed location is in staff main body |
|
return -1.; |
|
} |
|
|
|
kMusicStaffZone zoneForNote = [self findStaffZoneTypeForNoteId:noteId inOctave:octave]; |
|
if (zoneForNote == kMusicLineZone) { |
|
|
|
// when note's center position matches lineZone, just use it |
|
stubY = noteVec.y; |
|
|
|
} else if (zoneForNote == kMusicSpaceZone) { |
|
// for spaces ensure the stub line has room _underneath_ the note |
|
staffVecMap = [self findStaffVectorMapForNoteId:noteId |
|
inOctave:octave |
|
ofZoneType:zoneForNote]; |
|
if (staffVecMap) { |
|
// first test the center of the space zone |
|
proposedY = staffVecMap.vec.y; |
|
if ([self isYCoordinateAboveTopStaffLine:proposedY] == YES) { |
|
|
|
// now ensure the proposed stub line Y coord is valid location above main staff body |
|
proposedY += spaceHalfLineCheck; |
|
if ([self isYCoordinateAboveTopStaffLine:proposedY] == NO) { |
|
// must be the first space above staff, no need for stub here |
|
return -1.; |
|
} |
|
// Attn: observe the Y coordinate assigned to the stub line is based |
|
// from the _notes Vector_ rather than the staffVectorMapping. While |
|
// in 'most' cases the two will be equal, in scenarios such as the |
|
// Selection Halo, the noteVectors are relative to the Halo View. As such |
|
// the stub line needs to be rendered relative to that notes local space. |
|
// Under 'normal' staff rendering on music page the noteVector and |
|
// staffVectorMap will be the same. This disjointed useage works |
|
// because the MusicStaffLineVectorMap is |
|
// also matched up with the given notes, noteId and octave. |
|
|
|
stubY = noteVec.y + spaceHalfLineCheck; // proposedY; |
|
|
|
} else if ([self isYCoordinateBelowBottomStaffLine:proposedY] == YES) { |
|
// under the main staff, ensure adjusted stub location is below the main staff body |
|
proposedY -= spaceHalfLineCheck; |
|
if ([self isYCoordinateBelowBottomStaffLine:proposedY] == NO) { |
|
// must be the first space below the staff, no need for stub here |
|
return -1.; |
|
} |
|
|
|
// Attn: read the observation written in the above if() condition, applies here. |
|
stubY = noteVec.y + spaceHalfLineCheck; // proposedY; |
|
} |
|
} |
|
} else if (zoneForNote == kMusicStaffNOZone) { |
|
// octave for proposed Note is off the staff's mapping vectors. Here a cheap stop-gap |
|
// is just draw a stub line at the 'last' mapped line coordinate farthest from main staff |
|
// body. Now determine if the unmapped octave is too far above or below the staff. |
|
|
|
// look hi |
|
staffVecMap = [self findHighestLineVectorMap]; |
|
if (octave >= staffVecMap.octaveInfo.octave) { |
|
stubY = staffVecMap.vec.y; |
|
|
|
} else { |
|
// look low |
|
staffVecMap = [self findLowestLineVectorMap]; |
|
if (octave <= staffVecMap.octaveInfo.octave) { |
|
stubY = staffVecMap.vec.y; |
|
|
|
} else { |
|
// get out of town (extremely rare as in never) |
|
return -1.; |
|
} |
|
} |
|
} |
|
|
|
return stubY; |
|
} |
|
|
|
- (void) drawStubLineInContext:(CGContextRef)ctx |
|
withNotePosition:(SWFVector3 *)noteVec |
|
andNoteGeometry:(SWFVector3 *)noteGeometry |
|
forNoteId:(kMusicNoteId)noteId |
|
inOctave:(kMusicStaffOctave)octave |
|
{ |
|
// determine valid Y coordinate for stub line, a negative value means no stub possible |
|
float stubY = [self stubLineYCoordinateForNoteId:noteId inOctave:octave withNotePosition:noteVec]; |
|
|
|
if (stubY < 0.) { |
|
return; |
|
} |
|
CGPoint notePos = noteVec.pointFromVector; |
|
|
|
// craft line length to be slightly longer than width of note |
|
float stubLineLength = noteGeometry.x * 1.4; |
|
float stubLeftX = notePos.x - (stubLineLength / 2); |
|
|
|
// UIGraphicsPushContext(ctx); |
|
[self pushContextIfEmpty:ctx]; |
|
|
|
CGContextSetStrokeColorWithColor(ctx, _staffMetrics.lineColor.CGColor); |
|
CGContextSetLineWidth(ctx, _staffMetrics.staffLineWidth); |
|
|
|
CGContextMoveToPoint(ctx, stubLeftX, stubY); |
|
CGContextAddLineToPoint(ctx, stubLeftX + stubLineLength, stubY); |
|
CGContextStrokePath(ctx); |
|
|
|
// UIGraphicsPopContext(); |
|
[self popContextIfEmpty:ctx]; |
|
} |
|
|
|
@end |
|
|
|
|
|
|
|
|
Came here because of this post, do you have this as a complete project in a public repository? I would like to play a bit with your app.