/* PPDocument_Selection.m Copyright 2013-2018 Josh Freeman http://www.twilightedge.com This file is part of PikoPixel for Mac OS X and GNUstep. PikoPixel is a graphical application for drawing & editing pixel-art images. PikoPixel is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version approved for PikoPixel by its copyright holder (or an authorized proxy). PikoPixel is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ #import "PPDocument.h" #import "PPDocument_Notifications.h" #import "NSBitmapImageRep_PPUtilities.h" #import "NSColor_PPUtilities.h" #import "NSBezierPath_PPUtilities.h" #import "PPGeometry.h" @interface PPDocument (SelectionPrivateMethods) - (void) updateSelectionMaskWithBitmap: (NSBitmapImageRep *) bitmap atPoint: (NSPoint) origin; - (void) updateSelectionMaskWithTIFFData: (NSData *) tiffData atPoint: (NSPoint) origin; - (void) selectPath: (NSBezierPath *) path selectionMode: (PPSelectionMode) selectionMode shouldAntialias: (bool) shouldAntialias; - (bool) validateSelectionMode: (PPSelectionMode *) inOutSelectionMode; - (bool) selectionMaskIsNotEmpty; - (void) handleSelectionMaskUpdateInBounds: (NSRect) bounds undoBitmap: (NSBitmapImageRep *) undoBitmap; - (NSString *) actionNameForSelectionMode: (PPSelectionMode) selectionMode; @end @implementation PPDocument (Selection) - (bool) setupSelectionMaskBitmapOfSize: (NSSize) maskSize { if (PPGeometry_IsZeroSize(maskSize)) { goto ERROR; } if (!_selectionMask || !NSEqualSizes([_selectionMask ppSizeInPixels], maskSize)) { NSBitmapImageRep *selectionMask = [NSBitmapImageRep ppMaskBitmapOfSize: maskSize]; if (!selectionMask) goto ERROR; [_selectionMask autorelease]; // use autorelease when releasing accessible members _selectionMask = [selectionMask retain]; } else { [_selectionMask ppClearBitmap]; } _selectionBounds = NSZeroRect; _hasSelection = NO; return YES; ERROR: return NO; } - (bool) hasSelection { return _hasSelection; } - (NSRect) selectionBounds { return _selectionBounds; } - (NSBitmapImageRep *) selectionMask { return _selectionMask; } - (void) setSelectionMask: (NSBitmapImageRep *) selectionMask { if (![selectionMask ppIsMaskBitmap] || !NSEqualSizes([_selectionMask ppSizeInPixels], [selectionMask ppSizeInPixels])) { return; } [self updateSelectionMaskWithBitmap: selectionMask atPoint: NSZeroPoint]; } - (void) setSelectionMaskAreaWithBitmap: (NSBitmapImageRep *) selectionMask atPoint: (NSPoint) origin { if (![selectionMask ppIsMaskBitmap]) { return; } [self updateSelectionMaskWithBitmap: selectionMask atPoint: origin]; } - (void) selectRect: (NSRect) rect selectionMode: (PPSelectionMode) selectionMode { rect = PPGeometry_PixelCenteredRect(rect); [self selectPath: [NSBezierPath bezierPathWithRect: rect] selectionMode: selectionMode shouldAntialias: NO]; } - (void) selectPath: (NSBezierPath *) path selectionMode: (PPSelectionMode) selectionMode { [self selectPath: path selectionMode: selectionMode shouldAntialias: YES]; } - (void) selectPixelsMatchingColorAtPoint: (NSPoint) point colorMatchTolerance: (unsigned) colorMatchTolerance pixelMatchingMode: (PPPixelMatchingMode) pixelMatchingMode selectionMode: (PPSelectionMode) selectionMode { NSBitmapImageRep *matchMask, *croppedMatchMask, *croppedSelectionMask; NSRect matchMaskBounds; bool matchMaskShouldIntersectSelectionMask; if (![self validateSelectionMode: &selectionMode]) { goto ERROR; } if ((selectionMode == kPPSelectionMode_Intersect) || (selectionMode == kPPSelectionMode_Subtract)) { matchMaskShouldIntersectSelectionMask = (_hasSelection && [_selectionMask ppMaskCoversPoint: point]) ? YES : NO; } else { matchMaskShouldIntersectSelectionMask = NO; } matchMask = [self maskForPixelsMatchingColorAtPoint: point colorMatchTolerance: colorMatchTolerance pixelMatchingMode: pixelMatchingMode shouldIntersectSelectionMask: matchMaskShouldIntersectSelectionMask]; if (!matchMask) goto ERROR; matchMaskBounds = [matchMask ppMaskBounds]; if (NSIsEmptyRect(matchMaskBounds)) { goto ERROR; } if (selectionMode == kPPSelectionMode_Intersect) { if (_hasSelection && !matchMaskShouldIntersectSelectionMask) { // matchMask wasn't intersected with the selection mask during construction by // the maskForPixelsMatchingColorAtPoint:... method, so intersect it manually here [matchMask ppIntersectMaskWithMaskBitmap: _selectionMask]; } selectionMode = kPPSelectionMode_Replace; } if (selectionMode == kPPSelectionMode_Replace) { if (_hasSelection) { matchMaskBounds = NSUnionRect(matchMaskBounds, _selectionBounds); } } croppedMatchMask = [matchMask ppShallowDuplicateFromBounds: matchMaskBounds]; if (!croppedMatchMask) goto ERROR; if (selectionMode == kPPSelectionMode_Subtract) { croppedSelectionMask = [_selectionMask ppBitmapCroppedToBounds: matchMaskBounds]; if (!croppedSelectionMask) goto ERROR; [croppedSelectionMask ppSubtractMaskBitmap: croppedMatchMask]; croppedMatchMask = croppedSelectionMask; } else if (selectionMode == kPPSelectionMode_Add) { croppedSelectionMask = [_selectionMask ppShallowDuplicateFromBounds: matchMaskBounds]; if (!croppedSelectionMask) goto ERROR; [croppedMatchMask ppMergeMaskWithMaskBitmap: croppedSelectionMask]; } [self updateSelectionMaskWithBitmap: croppedMatchMask atPoint: matchMaskBounds.origin]; [[self undoManager] setActionName: [self actionNameForSelectionMode: selectionMode]]; return; ERROR: return; } - (void) selectAll { [self selectRect: _canvasFrame selectionMode: kPPSelectionMode_Add]; [[self undoManager] setActionName: @"Select All"]; } - (void) selectVisibleTargetPixels { NSBitmapImageRep *visiblePixelsMask = [[self sourceBitmapForLayerOperationTarget: _layerOperationTarget] ppMaskBitmapForVisiblePixelsInImageBitmap]; if (!visiblePixelsMask) return; [self setSelectionMask: visiblePixelsMask]; [[self undoManager] setActionName: @"Select Visible Pixels"]; } - (void) deselectAll { if (!_hasSelection) return; [self selectRect: _selectionBounds selectionMode: kPPSelectionMode_Subtract]; [[self undoManager] setActionName: @"Deselect All"]; } - (void) deselectInvisibleTargetPixels { NSBitmapImageRep *workingMask, *croppedSelectionMask; if (!_hasSelection) return; // crop target bitmap to selection bounds & make a mask of its visible pixels workingMask = [[[self sourceBitmapForLayerOperationTarget: _layerOperationTarget] ppShallowDuplicateFromBounds: _selectionBounds] ppMaskBitmapForVisiblePixelsInImageBitmap]; if (!workingMask) goto ERROR; croppedSelectionMask = [_selectionMask ppShallowDuplicateFromBounds: _selectionBounds]; if (!croppedSelectionMask) goto ERROR; [workingMask ppIntersectMaskWithMaskBitmap: croppedSelectionMask]; if ([workingMask ppIsEqualToBitmap: croppedSelectionMask]) { return; } [self setSelectionMaskAreaWithBitmap: workingMask atPoint: _selectionBounds.origin]; [[self undoManager] setActionName: @"Deselect Invisible Pixels"]; return; ERROR: return; } - (void) invertSelection { NSUndoManager *undoManager = [self undoManager]; [undoManager disableUndoRegistration]; if (!_hasSelection) { [self selectAll]; } else { NSBitmapImageRep *invertedSelectionMask = [[_selectionMask copy] autorelease]; [invertedSelectionMask ppInvertMaskBitmap]; if (!invertedSelectionMask) goto ERROR; if ([invertedSelectionMask ppMaskIsNotEmpty]) { [self setSelectionMask: invertedSelectionMask]; } else { [self deselectAll]; } } [undoManager enableUndoRegistration]; [[undoManager prepareWithInvocationTarget: self] invertSelection]; if (![undoManager isUndoing] && ![undoManager isRedoing]) { [undoManager setActionName: @"Invert Selection"]; } return; ERROR: [undoManager enableUndoRegistration]; return; } - (void) closeHolesInSelection { NSBitmapImageRep *updatedMask; if (!_hasSelection) goto ERROR; updatedMask = [_selectionMask ppBitmapCroppedToBounds: _selectionBounds]; if (!updatedMask) goto ERROR; [updatedMask ppCloseHolesInMaskBitmap]; [self setSelectionMaskAreaWithBitmap: updatedMask atPoint: _selectionBounds.origin]; [[self undoManager] setActionName: @"Close Holes in Selection"]; return; ERROR: return; } - (PPDocument *) ppDocumentFromSelection { PPDocument *ppDocument; if (!_hasSelection || ![self layerOperationTargetHasEnabledLayer]) { goto ERROR; } ppDocument = [[[PPDocument alloc] init] autorelease]; if (!ppDocument) goto ERROR; [ppDocument loadFromPPDocument: self]; [ppDocument cropToSelectionBounds]; [ppDocument removeNontargetLayers]; [[ppDocument undoManager] removeAllActions]; return ppDocument; ERROR: return nil; } #pragma mark Private methods - (void) updateSelectionMaskWithBitmap: (NSBitmapImageRep *) bitmap atPoint: (NSPoint) origin { NSRect updateRect; NSBitmapImageRep *undoBitmap; updateRect.origin = origin; updateRect.size = [bitmap ppSizeInPixels]; updateRect = NSIntersectionRect(updateRect, _canvasFrame); if (NSIsEmptyRect(updateRect)) { return; } undoBitmap = [_selectionMask ppBitmapCroppedToBounds: updateRect]; [_selectionMask ppCopyFromBitmap: bitmap toPoint: origin]; [self handleSelectionMaskUpdateInBounds: updateRect undoBitmap: undoBitmap]; } - (void) updateSelectionMaskWithTIFFData: (NSData *) tiffData atPoint: (NSPoint) origin { if (!tiffData) return; [self updateSelectionMaskWithBitmap: [NSBitmapImageRep imageRepWithData: tiffData] atPoint: origin]; } - (void) selectPath: (NSBezierPath *) path selectionMode: (PPSelectionMode) selectionMode shouldAntialias: (bool) shouldAntialias { NSRect pathBounds, updateBounds; NSBitmapImageRep *undoBitmap; if (![self validateSelectionMode: &selectionMode]) { goto ERROR; } pathBounds = NSIntersectionRect(PPGeometry_PixelBoundsCoveredByRect([path bounds]), _canvasFrame); if (NSIsEmptyRect(pathBounds)) { goto ERROR; } if (((selectionMode == kPPSelectionMode_Replace) && _hasSelection) || (selectionMode == kPPSelectionMode_Intersect)) { updateBounds = NSUnionRect(pathBounds, _selectionBounds); undoBitmap = [_selectionMask ppBitmapCroppedToBounds: updateBounds]; [_selectionMask ppClearBitmapInBounds: updateBounds]; } else { updateBounds = pathBounds; undoBitmap = [_selectionMask ppBitmapCroppedToBounds: updateBounds]; } [_selectionMask ppSetAsCurrentGraphicsContext]; if (selectionMode == kPPSelectionMode_Subtract) { [[NSColor ppMaskBitmapOffColor] set]; } else { [[NSColor ppMaskBitmapOnColor] set]; } if (shouldAntialias) { // antialiasing is necessary when filling a non-rectangular path, otherwise the fill // will cover a larger area than the stroke (some curve edges will add a pixel); // make sure to correct the antialiasing afterwards by thresholding the mask's pixel // values to 0 & 255 [path ppAntialiasedFill]; } else { [path fill]; } [path stroke]; [_selectionMask ppRestoreGraphicsContext]; if (shouldAntialias) { [_selectionMask ppThresholdMaskBitmapPixelValuesInBounds: pathBounds]; } if (selectionMode == kPPSelectionMode_Intersect) { NSBitmapImageRep *croppedSelectionMask; croppedSelectionMask = [_selectionMask ppShallowDuplicateFromBounds: updateBounds]; // overwriting (intersecting) croppedSelectionMask also overwrites _selectionMask, // since they share bitmapData (ShallowDuplicate) [croppedSelectionMask ppIntersectMaskWithMaskBitmap: undoBitmap]; } [self handleSelectionMaskUpdateInBounds: updateBounds undoBitmap: undoBitmap]; [[self undoManager] setActionName: [self actionNameForSelectionMode: selectionMode]]; return; ERROR: return; } - (bool) validateSelectionMode: (PPSelectionMode *) inOutSelectionMode { PPSelectionMode selectionMode; if (!inOutSelectionMode) goto ERROR; selectionMode = *inOutSelectionMode; if (!PPSelectionMode_IsValid(selectionMode)) { goto ERROR; } if (!_hasSelection) { if ((selectionMode == kPPSelectionMode_Subtract) || (selectionMode == kPPSelectionMode_Intersect)) { goto ERROR; } selectionMode = kPPSelectionMode_Replace; } *inOutSelectionMode = selectionMode; return YES; ERROR: return NO; } - (bool) selectionMaskIsNotEmpty { return [_selectionMask ppMaskIsNotEmpty]; } - (void) handleSelectionMaskUpdateInBounds: (NSRect) bounds undoBitmap: (NSBitmapImageRep *) undoBitmap { NSUndoManager *undoManager; _hasSelection = [self selectionMaskIsNotEmpty]; if (_hasSelection) { _selectionBounds = [_selectionMask ppMaskBoundsInRect: NSUnionRect(_selectionBounds, bounds)]; } else { _selectionBounds = NSZeroRect; } [self postNotification_UpdatedSelection]; undoManager = [self undoManager]; [[undoManager prepareWithInvocationTarget: self] updateSelectionMaskWithTIFFData: [undoBitmap ppCompressedTIFFData] atPoint: bounds.origin]; if (![undoManager isUndoing] && ![undoManager isRedoing]) { [undoManager setActionName: @"Selection"]; } } - (NSString *) actionNameForSelectionMode: (PPSelectionMode) selectionMode { switch (selectionMode) { case kPPSelectionMode_Add: { return @"Add to Selection"; } break; case kPPSelectionMode_Subtract: { return @"Subtract from Selection"; } break; case kPPSelectionMode_Intersect: { return @"Intersect Selection"; } break; case kPPSelectionMode_Replace: default: { return @"Make Selection"; } break; } } @end