1/* 2 PPDocument_Selection.m 3 4 Copyright 2013-2018 Josh Freeman 5 http://www.twilightedge.com 6 7 This file is part of PikoPixel for Mac OS X and GNUstep. 8 PikoPixel is a graphical application for drawing & editing pixel-art images. 9 10 PikoPixel is free software: you can redistribute it and/or modify it under 11 the terms of the GNU Affero General Public License as published by the 12 Free Software Foundation, either version 3 of the License, or (at your 13 option) any later version approved for PikoPixel by its copyright holder (or 14 an authorized proxy). 15 16 PikoPixel is distributed in the hope that it will be useful, but WITHOUT ANY 17 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 19 details. 20 21 You should have received a copy of the GNU Affero General Public License 22 along with this program. If not, see <http://www.gnu.org/licenses/>. 23*/ 24 25#import "PPDocument.h" 26 27#import "PPDocument_Notifications.h" 28#import "NSBitmapImageRep_PPUtilities.h" 29#import "NSColor_PPUtilities.h" 30#import "NSBezierPath_PPUtilities.h" 31#import "PPGeometry.h" 32 33 34@interface PPDocument (SelectionPrivateMethods) 35 36- (void) updateSelectionMaskWithBitmap: (NSBitmapImageRep *) bitmap atPoint: (NSPoint) origin; 37- (void) updateSelectionMaskWithTIFFData: (NSData *) tiffData atPoint: (NSPoint) origin; 38- (void) selectPath: (NSBezierPath *) path 39 selectionMode: (PPSelectionMode) selectionMode 40 shouldAntialias: (bool) shouldAntialias; 41- (bool) validateSelectionMode: (PPSelectionMode *) inOutSelectionMode; 42- (bool) selectionMaskIsNotEmpty; 43- (void) handleSelectionMaskUpdateInBounds: (NSRect) bounds 44 undoBitmap: (NSBitmapImageRep *) undoBitmap; 45 46- (NSString *) actionNameForSelectionMode: (PPSelectionMode) selectionMode; 47 48@end 49 50@implementation PPDocument (Selection) 51 52- (bool) setupSelectionMaskBitmapOfSize: (NSSize) maskSize 53{ 54 if (PPGeometry_IsZeroSize(maskSize)) 55 { 56 goto ERROR; 57 } 58 59 if (!_selectionMask 60 || !NSEqualSizes([_selectionMask ppSizeInPixels], maskSize)) 61 { 62 NSBitmapImageRep *selectionMask = [NSBitmapImageRep ppMaskBitmapOfSize: maskSize]; 63 64 if (!selectionMask) 65 goto ERROR; 66 67 [_selectionMask autorelease]; // use autorelease when releasing accessible members 68 _selectionMask = [selectionMask retain]; 69 } 70 else 71 { 72 [_selectionMask ppClearBitmap]; 73 } 74 75 _selectionBounds = NSZeroRect; 76 _hasSelection = NO; 77 78 return YES; 79 80ERROR: 81 return NO; 82} 83 84- (bool) hasSelection 85{ 86 return _hasSelection; 87} 88 89- (NSRect) selectionBounds 90{ 91 return _selectionBounds; 92} 93 94- (NSBitmapImageRep *) selectionMask 95{ 96 return _selectionMask; 97} 98 99- (void) setSelectionMask: (NSBitmapImageRep *) selectionMask 100{ 101 if (![selectionMask ppIsMaskBitmap] 102 || !NSEqualSizes([_selectionMask ppSizeInPixels], [selectionMask ppSizeInPixels])) 103 { 104 return; 105 } 106 107 [self updateSelectionMaskWithBitmap: selectionMask atPoint: NSZeroPoint]; 108} 109 110- (void) setSelectionMaskAreaWithBitmap: (NSBitmapImageRep *) selectionMask 111 atPoint: (NSPoint) origin 112{ 113 if (![selectionMask ppIsMaskBitmap]) 114 { 115 return; 116 } 117 118 [self updateSelectionMaskWithBitmap: selectionMask atPoint: origin]; 119} 120 121- (void) selectRect: (NSRect) rect 122 selectionMode: (PPSelectionMode) selectionMode 123{ 124 rect = PPGeometry_PixelCenteredRect(rect); 125 126 [self selectPath: [NSBezierPath bezierPathWithRect: rect] 127 selectionMode: selectionMode 128 shouldAntialias: NO]; 129} 130 131- (void) selectPath: (NSBezierPath *) path 132 selectionMode: (PPSelectionMode) selectionMode 133{ 134 [self selectPath: path 135 selectionMode: selectionMode 136 shouldAntialias: YES]; 137} 138 139- (void) selectPixelsMatchingColorAtPoint: (NSPoint) point 140 colorMatchTolerance: (unsigned) colorMatchTolerance 141 pixelMatchingMode: (PPPixelMatchingMode) pixelMatchingMode 142 selectionMode: (PPSelectionMode) selectionMode 143{ 144 NSBitmapImageRep *matchMask, *croppedMatchMask, *croppedSelectionMask; 145 NSRect matchMaskBounds; 146 bool matchMaskShouldIntersectSelectionMask; 147 148 if (![self validateSelectionMode: &selectionMode]) 149 { 150 goto ERROR; 151 } 152 153 if ((selectionMode == kPPSelectionMode_Intersect) 154 || (selectionMode == kPPSelectionMode_Subtract)) 155 { 156 matchMaskShouldIntersectSelectionMask = 157 (_hasSelection && [_selectionMask ppMaskCoversPoint: point]) ? YES : NO; 158 } 159 else 160 { 161 matchMaskShouldIntersectSelectionMask = NO; 162 } 163 164 matchMask = [self maskForPixelsMatchingColorAtPoint: point 165 colorMatchTolerance: colorMatchTolerance 166 pixelMatchingMode: pixelMatchingMode 167 shouldIntersectSelectionMask: matchMaskShouldIntersectSelectionMask]; 168 169 if (!matchMask) 170 goto ERROR; 171 172 matchMaskBounds = [matchMask ppMaskBounds]; 173 174 if (NSIsEmptyRect(matchMaskBounds)) 175 { 176 goto ERROR; 177 } 178 179 if (selectionMode == kPPSelectionMode_Intersect) 180 { 181 if (_hasSelection && !matchMaskShouldIntersectSelectionMask) 182 { 183 // matchMask wasn't intersected with the selection mask during construction by 184 // the maskForPixelsMatchingColorAtPoint:... method, so intersect it manually here 185 [matchMask ppIntersectMaskWithMaskBitmap: _selectionMask]; 186 } 187 188 selectionMode = kPPSelectionMode_Replace; 189 } 190 191 if (selectionMode == kPPSelectionMode_Replace) 192 { 193 if (_hasSelection) 194 { 195 matchMaskBounds = NSUnionRect(matchMaskBounds, _selectionBounds); 196 } 197 } 198 199 croppedMatchMask = [matchMask ppShallowDuplicateFromBounds: matchMaskBounds]; 200 201 if (!croppedMatchMask) 202 goto ERROR; 203 204 if (selectionMode == kPPSelectionMode_Subtract) 205 { 206 croppedSelectionMask = [_selectionMask ppBitmapCroppedToBounds: matchMaskBounds]; 207 208 if (!croppedSelectionMask) 209 goto ERROR; 210 211 [croppedSelectionMask ppSubtractMaskBitmap: croppedMatchMask]; 212 213 croppedMatchMask = croppedSelectionMask; 214 } 215 else if (selectionMode == kPPSelectionMode_Add) 216 { 217 croppedSelectionMask = [_selectionMask ppShallowDuplicateFromBounds: matchMaskBounds]; 218 219 if (!croppedSelectionMask) 220 goto ERROR; 221 222 [croppedMatchMask ppMergeMaskWithMaskBitmap: croppedSelectionMask]; 223 } 224 225 [self updateSelectionMaskWithBitmap: croppedMatchMask 226 atPoint: matchMaskBounds.origin]; 227 228 [[self undoManager] setActionName: [self actionNameForSelectionMode: selectionMode]]; 229 230 return; 231 232ERROR: 233 return; 234} 235 236- (void) selectAll 237{ 238 [self selectRect: _canvasFrame 239 selectionMode: kPPSelectionMode_Add]; 240 241 [[self undoManager] setActionName: @"Select All"]; 242} 243 244- (void) selectVisibleTargetPixels 245{ 246 NSBitmapImageRep *visiblePixelsMask = 247 [[self sourceBitmapForLayerOperationTarget: _layerOperationTarget] 248 ppMaskBitmapForVisiblePixelsInImageBitmap]; 249 250 if (!visiblePixelsMask) 251 return; 252 253 [self setSelectionMask: visiblePixelsMask]; 254 255 [[self undoManager] setActionName: @"Select Visible Pixels"]; 256} 257 258- (void) deselectAll 259{ 260 if (!_hasSelection) 261 return; 262 263 [self selectRect: _selectionBounds 264 selectionMode: kPPSelectionMode_Subtract]; 265 266 [[self undoManager] setActionName: @"Deselect All"]; 267} 268 269- (void) deselectInvisibleTargetPixels 270{ 271 NSBitmapImageRep *workingMask, *croppedSelectionMask; 272 273 if (!_hasSelection) 274 return; 275 276 // crop target bitmap to selection bounds & make a mask of its visible pixels 277 workingMask = 278 [[[self sourceBitmapForLayerOperationTarget: _layerOperationTarget] 279 ppShallowDuplicateFromBounds: _selectionBounds] 280 ppMaskBitmapForVisiblePixelsInImageBitmap]; 281 282 if (!workingMask) 283 goto ERROR; 284 285 croppedSelectionMask = [_selectionMask ppShallowDuplicateFromBounds: _selectionBounds]; 286 287 if (!croppedSelectionMask) 288 goto ERROR; 289 290 [workingMask ppIntersectMaskWithMaskBitmap: croppedSelectionMask]; 291 292 if ([workingMask ppIsEqualToBitmap: croppedSelectionMask]) 293 { 294 return; 295 } 296 297 [self setSelectionMaskAreaWithBitmap: workingMask atPoint: _selectionBounds.origin]; 298 299 [[self undoManager] setActionName: @"Deselect Invisible Pixels"]; 300 301 return; 302 303ERROR: 304 return; 305} 306 307- (void) invertSelection 308{ 309 NSUndoManager *undoManager = [self undoManager]; 310 311 [undoManager disableUndoRegistration]; 312 313 if (!_hasSelection) 314 { 315 [self selectAll]; 316 } 317 else 318 { 319 NSBitmapImageRep *invertedSelectionMask = [[_selectionMask copy] autorelease]; 320 321 [invertedSelectionMask ppInvertMaskBitmap]; 322 323 if (!invertedSelectionMask) 324 goto ERROR; 325 326 if ([invertedSelectionMask ppMaskIsNotEmpty]) 327 { 328 [self setSelectionMask: invertedSelectionMask]; 329 } 330 else 331 { 332 [self deselectAll]; 333 } 334 } 335 336 [undoManager enableUndoRegistration]; 337 338 [[undoManager prepareWithInvocationTarget: self] invertSelection]; 339 340 if (![undoManager isUndoing] && ![undoManager isRedoing]) 341 { 342 [undoManager setActionName: @"Invert Selection"]; 343 } 344 345 return; 346 347ERROR: 348 [undoManager enableUndoRegistration]; 349 350 return; 351} 352 353- (void) closeHolesInSelection 354{ 355 NSBitmapImageRep *updatedMask; 356 357 if (!_hasSelection) 358 goto ERROR; 359 360 updatedMask = [_selectionMask ppBitmapCroppedToBounds: _selectionBounds]; 361 362 if (!updatedMask) 363 goto ERROR; 364 365 [updatedMask ppCloseHolesInMaskBitmap]; 366 367 [self setSelectionMaskAreaWithBitmap: updatedMask atPoint: _selectionBounds.origin]; 368 369 [[self undoManager] setActionName: @"Close Holes in Selection"]; 370 371 return; 372 373ERROR: 374 return; 375} 376 377- (PPDocument *) ppDocumentFromSelection 378{ 379 PPDocument *ppDocument; 380 381 if (!_hasSelection || ![self layerOperationTargetHasEnabledLayer]) 382 { 383 goto ERROR; 384 } 385 386 ppDocument = [[[PPDocument alloc] init] autorelease]; 387 388 if (!ppDocument) 389 goto ERROR; 390 391 [ppDocument loadFromPPDocument: self]; 392 [ppDocument cropToSelectionBounds]; 393 [ppDocument removeNontargetLayers]; 394 395 [[ppDocument undoManager] removeAllActions]; 396 397 return ppDocument; 398 399ERROR: 400 return nil; 401} 402 403#pragma mark Private methods 404 405- (void) updateSelectionMaskWithBitmap: (NSBitmapImageRep *) bitmap atPoint: (NSPoint) origin 406{ 407 NSRect updateRect; 408 NSBitmapImageRep *undoBitmap; 409 410 updateRect.origin = origin; 411 updateRect.size = [bitmap ppSizeInPixels]; 412 updateRect = NSIntersectionRect(updateRect, _canvasFrame); 413 414 if (NSIsEmptyRect(updateRect)) 415 { 416 return; 417 } 418 419 undoBitmap = [_selectionMask ppBitmapCroppedToBounds: updateRect]; 420 421 [_selectionMask ppCopyFromBitmap: bitmap toPoint: origin]; 422 423 [self handleSelectionMaskUpdateInBounds: updateRect undoBitmap: undoBitmap]; 424} 425 426- (void) updateSelectionMaskWithTIFFData: (NSData *) tiffData atPoint: (NSPoint) origin 427{ 428 if (!tiffData) 429 return; 430 431 [self updateSelectionMaskWithBitmap: [NSBitmapImageRep imageRepWithData: tiffData] 432 atPoint: origin]; 433} 434 435- (void) selectPath: (NSBezierPath *) path 436 selectionMode: (PPSelectionMode) selectionMode 437 shouldAntialias: (bool) shouldAntialias 438{ 439 NSRect pathBounds, updateBounds; 440 NSBitmapImageRep *undoBitmap; 441 442 if (![self validateSelectionMode: &selectionMode]) 443 { 444 goto ERROR; 445 } 446 447 pathBounds = NSIntersectionRect(PPGeometry_PixelBoundsCoveredByRect([path bounds]), 448 _canvasFrame); 449 450 if (NSIsEmptyRect(pathBounds)) 451 { 452 goto ERROR; 453 } 454 455 if (((selectionMode == kPPSelectionMode_Replace) && _hasSelection) 456 || (selectionMode == kPPSelectionMode_Intersect)) 457 { 458 updateBounds = NSUnionRect(pathBounds, _selectionBounds); 459 460 undoBitmap = [_selectionMask ppBitmapCroppedToBounds: updateBounds]; 461 462 [_selectionMask ppClearBitmapInBounds: updateBounds]; 463 } 464 else 465 { 466 updateBounds = pathBounds; 467 468 undoBitmap = [_selectionMask ppBitmapCroppedToBounds: updateBounds]; 469 } 470 471 [_selectionMask ppSetAsCurrentGraphicsContext]; 472 473 if (selectionMode == kPPSelectionMode_Subtract) 474 { 475 [[NSColor ppMaskBitmapOffColor] set]; 476 } 477 else 478 { 479 [[NSColor ppMaskBitmapOnColor] set]; 480 } 481 482 if (shouldAntialias) 483 { 484 // antialiasing is necessary when filling a non-rectangular path, otherwise the fill 485 // will cover a larger area than the stroke (some curve edges will add a pixel); 486 // make sure to correct the antialiasing afterwards by thresholding the mask's pixel 487 // values to 0 & 255 488 489 [path ppAntialiasedFill]; 490 } 491 else 492 { 493 [path fill]; 494 } 495 496 [path stroke]; 497 498 [_selectionMask ppRestoreGraphicsContext]; 499 500 if (shouldAntialias) 501 { 502 [_selectionMask ppThresholdMaskBitmapPixelValuesInBounds: pathBounds]; 503 } 504 505 if (selectionMode == kPPSelectionMode_Intersect) 506 { 507 NSBitmapImageRep *croppedSelectionMask; 508 509 croppedSelectionMask = [_selectionMask ppShallowDuplicateFromBounds: updateBounds]; 510 511 // overwriting (intersecting) croppedSelectionMask also overwrites _selectionMask, 512 // since they share bitmapData (ShallowDuplicate) 513 [croppedSelectionMask ppIntersectMaskWithMaskBitmap: undoBitmap]; 514 } 515 516 [self handleSelectionMaskUpdateInBounds: updateBounds 517 undoBitmap: undoBitmap]; 518 519 [[self undoManager] setActionName: [self actionNameForSelectionMode: selectionMode]]; 520 521 return; 522 523ERROR: 524 return; 525} 526 527- (bool) validateSelectionMode: (PPSelectionMode *) inOutSelectionMode 528{ 529 PPSelectionMode selectionMode; 530 531 if (!inOutSelectionMode) 532 goto ERROR; 533 534 selectionMode = *inOutSelectionMode; 535 536 if (!PPSelectionMode_IsValid(selectionMode)) 537 { 538 goto ERROR; 539 } 540 541 if (!_hasSelection) 542 { 543 if ((selectionMode == kPPSelectionMode_Subtract) 544 || (selectionMode == kPPSelectionMode_Intersect)) 545 { 546 goto ERROR; 547 } 548 549 selectionMode = kPPSelectionMode_Replace; 550 } 551 552 *inOutSelectionMode = selectionMode; 553 554 return YES; 555 556ERROR: 557 return NO; 558} 559 560- (bool) selectionMaskIsNotEmpty 561{ 562 return [_selectionMask ppMaskIsNotEmpty]; 563} 564 565- (void) handleSelectionMaskUpdateInBounds: (NSRect) bounds 566 undoBitmap: (NSBitmapImageRep *) undoBitmap 567{ 568 NSUndoManager *undoManager; 569 570 _hasSelection = [self selectionMaskIsNotEmpty]; 571 572 if (_hasSelection) 573 { 574 _selectionBounds = 575 [_selectionMask ppMaskBoundsInRect: NSUnionRect(_selectionBounds, bounds)]; 576 } 577 else 578 { 579 _selectionBounds = NSZeroRect; 580 } 581 582 [self postNotification_UpdatedSelection]; 583 584 undoManager = [self undoManager]; 585 586 [[undoManager prepareWithInvocationTarget: self] 587 updateSelectionMaskWithTIFFData: 588 [undoBitmap ppCompressedTIFFData] 589 atPoint: bounds.origin]; 590 591 if (![undoManager isUndoing] && ![undoManager isRedoing]) 592 { 593 [undoManager setActionName: @"Selection"]; 594 } 595} 596 597- (NSString *) actionNameForSelectionMode: (PPSelectionMode) selectionMode 598{ 599 switch (selectionMode) 600 { 601 case kPPSelectionMode_Add: 602 { 603 return @"Add to Selection"; 604 } 605 break; 606 607 case kPPSelectionMode_Subtract: 608 { 609 return @"Subtract from Selection"; 610 } 611 break; 612 613 case kPPSelectionMode_Intersect: 614 { 615 return @"Intersect Selection"; 616 } 617 break; 618 619 case kPPSelectionMode_Replace: 620 default: 621 { 622 return @"Make Selection"; 623 } 624 break; 625 } 626} 627 628@end 629