Pergunta

I'm trying to create an application which draws angles every 3 taps by the user. Each tap creates the angle points and connects them.

This is quite simple to do. However, now i'm trying to make these angles editable and it gets substantially harder. When I finish my 3 taps and touchesEnded gets called, I add the three points into an NSObject subclass called segment which has three properties, firstPoint, secondPoint, and thirdPoint, and it stores precisely that.

I then add the 'segment' object into an array. Every time touchesBegan gets called, I get into a for loop for my array. If the tap is within 25px of any of the 3 CGPoints of the "segment" object, then I don't draw a new angle, I edit that angle.

As far as I know, at this point, there aren't any problems or errors thanks to you guys! I'm just checking for general improvements/simplifications keeping the current method i'm using. I wish I could give the bounty to everyone because you've all been so helpful!

This is my biggest project so far, and I would greatly appreciate some insight. It's a pretty good challenge if you're up for it and can handle it. Any help, at all, is IMMENSELY appreciated. If you can think of a better way to handle what i'm trying to accomplish, by all means please let me know!

Note: to draw, just tap/click on the screen.

Here is the most relevant code:

drawingsubclass.m

#import "SegmentDraw.h"
#import <QuartzCore/QuartzCore.h>

BOOL editing1 = FALSE;
BOOL editing2 = FALSE;
BOOL editing3 = FALSE;
BOOL erased = FALSE;
int radius = 35;
int handleSize = 20;

@implementation SegmentDraw {
    UIImage *incrementalImage;
}
@synthesize first;
@synthesize second;
@synthesize third;

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        [self setMultipleTouchEnabled:NO];
        [self setBackgroundColor:[UIColor clearColor]];

        first = CGPointZero;
        second = CGPointZero;
        third = CGPointZero;
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];

    CGPoint point = [touch locationInView:self];

    for (Segment *currentSegment in self.tapArray) {

        int xDistance = abs(point.x - currentSegment.firstPoint.x);
        int yDistance = abs(point.y - currentSegment.firstPoint.y);
        int firstDistance = sqrtf(xDistance * xDistance + yDistance * yDistance);

        int xDistance2 = abs(point.x - currentSegment.secondPoint.x);
        int yDistance2 = abs(point.y - currentSegment.secondPoint.y);
        int secondDistance = sqrtf(xDistance2 * xDistance2 + yDistance2 * yDistance2);

        int xDistance3 = abs(point.x - currentSegment.thirdPoint.x);
        int yDistance3 = abs(point.y - currentSegment.thirdPoint.y);
        int thirdDistance = sqrtf(xDistance3 * xDistance3 + yDistance3 * yDistance3);

        if (firstDistance <= radius) {
            NSLog(@"First point matches");
            editing1 = TRUE;
            editing2 = FALSE;
            editing3 = FALSE;
            first = point;
            second = currentSegment.secondPoint;
            third = currentSegment.thirdPoint;
            self.segmentBeingEdited = currentSegment;
            [self setNeedsDisplay];
            [self drawBitmap];
            erased = FALSE;
            return;
        }
        else if (secondDistance <= radius) {
            NSLog(@"Second point matches");
            editing2 = TRUE;
            editing1 = FALSE;
            editing3 = FALSE;
            first = currentSegment.firstPoint;
            second = point;
            third = currentSegment.thirdPoint;
            self.segmentBeingEdited = currentSegment;
            [self setNeedsDisplay];
            [self drawBitmap];
            erased = FALSE;
            return;
        }
        else if (thirdDistance <= radius) {
            NSLog(@"Third point matches");
            editing3 = TRUE;
            editing1 = FALSE;
            editing2 = FALSE;
            first = currentSegment.firstPoint;
            second = currentSegment.secondPoint;
            third = point;
            self.segmentBeingEdited = currentSegment;
            [self setNeedsDisplay];
            [self drawBitmap];
            erased = FALSE;
            return;
        }
        else {
            editing1 = FALSE;
            editing2 = FALSE;
            editing3 = FALSE;
        }
    }

    if (CGPointEqualToPoint(first, CGPointZero)) {
        first = [touch locationInView:self];
    }
    else if (!CGPointEqualToPoint(first, CGPointZero) && CGPointEqualToPoint(second, CGPointZero)) {
        second = [touch locationInView:self];
    }
    else if (!CGPointEqualToPoint(first, CGPointZero) && !(CGPointEqualToPoint(second, CGPointZero)) && CGPointEqualToPoint(third, CGPointZero)) {
        third = [touch locationInView:self];
    }
    else {
        //[self drawBitmap];
        first = [touch locationInView:self];
        second = CGPointZero;
        third = CGPointZero;
    }

    [self setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];

    CGPoint point = [touch locationInView:self];

    for (Segment *currentSegment in self.tapArray) {
        if (editing1) {
            editing2 = FALSE;
            editing3 = FALSE;
            first = point;
            second = self.segmentBeingEdited.secondPoint;
            third = self.segmentBeingEdited.thirdPoint;
            //self.segmentBeingEdited = currentSegment;
            if (erased == FALSE) {
                [self drawBitmap];
                //      NSLog(@"YOU NEED TO ERASE.");
            }
            [self setNeedsDisplay];
            return;
        }
        else if (editing2) {
            editing1 = FALSE;
            editing3 = FALSE;
            first = self.segmentBeingEdited.firstPoint;
            second = point;
            third = self.segmentBeingEdited.thirdPoint;
            //self.segmentBeingEdited = currentSegment;
            if (erased == FALSE) {
                [self drawBitmap];
                //    NSLog(@"YOU NEED TO ERASE.");
            }
            [self setNeedsDisplay];
            return;
        }
        else if (editing3) {
            //       NSLog(@"It's editing time, yo");
            editing1 = FALSE;
            editing2 = FALSE;
            first = self.segmentBeingEdited.firstPoint;
            second = self.segmentBeingEdited.secondPoint;
            third = point;
            //self.segmentBeingEdited = currentSegment;
            if (erased == FALSE) {
                [self drawBitmap];
                //             NSLog(@"YOU NEED TO ERASE.");
            }
            [self setNeedsDisplay];
            return;
        }
        else {
            editing1 = FALSE;
            editing2 = FALSE;
            editing3 = FALSE;
        }
    }

    if (!CGPointEqualToPoint(first, CGPointZero) && CGPointEqualToPoint(second, CGPointZero)) {
        first = [touch locationInView:self];
    }
    else if (!CGPointEqualToPoint(first, CGPointZero) && !(CGPointEqualToPoint(second, CGPointZero)) && CGPointEqualToPoint(third, CGPointZero)) {
        second = [touch locationInView:self];
    }
    else if (!CGPointEqualToPoint(first, CGPointZero) && !(CGPointEqualToPoint(second, CGPointZero)) && !(CGPointEqualToPoint(third, CGPointZero))) {
        third = [touch locationInView:self];
    }
    else {
        first = [touch locationInView:self];
        second = CGPointZero;
        third = CGPointZero;
    }

    [self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSUInteger index;
    if (editing1 || editing2 || editing3) {
        index = [self.tapArray indexOfObject:self.segmentBeingEdited];
        Segment *datSegment = [self.tapArray objectAtIndex:index];
        datSegment.firstPoint = first;
        datSegment.secondPoint = second;
        datSegment.thirdPoint = third;
    }


    if (!CGPointEqualToPoint(third, CGPointZero) && editing1 == FALSE && editing2 == FALSE && editing3 == FALSE) {
        Segment *segment = [[Segment alloc] init];
        segment.firstPoint = first;
        segment.secondPoint = second;
        segment.thirdPoint = third;

        if (self.tapArray == nil) {
            self.tapArray = [[NSMutableArray alloc] init];
        }

        [self.tapArray addObject:segment];
    }

    editing1 = FALSE;
    editing2 = FALSE;
    editing3 = FALSE;
    erased = FALSE;

    [self drawBitmap];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}

- (void)drawRect:(CGRect)rect {
    [incrementalImage drawInRect:rect];
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
    CGContextSetLineWidth(context, 5.0);
    CGContextSetLineCap(context, kCGLineCapRound);
    CGContextSetLineJoin(context, kCGLineJoinRound);

    if (!CGPointEqualToPoint(first, CGPointZero)) {
        CGRect rectangle = CGRectMake(first.x - 10, first.y - 10, handleSize, handleSize);
        CGContextAddEllipseInRect(context, rectangle);
        CGContextMoveToPoint(context, first.x, first.y);

        if (!CGPointEqualToPoint(second, CGPointZero)) {
            CGContextAddLineToPoint(context, second.x, second.y);
            CGRect rectangle2 = CGRectMake(second.x - 10, second.y - 10, handleSize, handleSize);
            CGContextAddEllipseInRect(context, rectangle2);
            CGContextMoveToPoint(context, second.x, second.y);
        }

        if (!CGPointEqualToPoint(third, CGPointZero)) {
            CGContextAddLineToPoint(context, third.x, third.y);
            CGRect rectangle3 = CGRectMake(third.x - 10, third.y - 10, handleSize, handleSize);
            CGContextAddEllipseInRect(context, rectangle3);
        }

        CGContextStrokePath(context);
    }
}

- (void)drawBitmap {
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);

    if (!incrementalImage) { // first draw;
        UIBezierPath *rectpath = [UIBezierPath bezierPathWithRect:self.bounds]; // enclosing bitmap by a rectangle defined by another UIBezierPath object
        [[UIColor clearColor] setFill];
        [rectpath fill]; // fill it
    }
    [incrementalImage drawAtPoint:CGPointZero];

    CGContextRef context = UIGraphicsGetCurrentContext();


    CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
    CGContextSetLineWidth(context, 5.0);
    CGContextSetLineCap(context, kCGLineCapRound);
    CGContextSetLineJoin(context, kCGLineJoinRound);

    if (editing1 || editing2 || editing3) {
        //    CGContextBeginTransparencyLayer(context, NULL);
        CGContextSetBlendMode(context, kCGBlendModeClear);
        CGContextSetLineWidth(context, 6.0);
        //CGContextSetBlendMode(context, kCGBlendModeColor);

        if (!CGPointEqualToPoint(self.segmentBeingEdited.firstPoint, CGPointZero)) {
            CGContextMoveToPoint(context, self.segmentBeingEdited.firstPoint.x, self.segmentBeingEdited.firstPoint.y);

            if (!CGPointEqualToPoint(self.segmentBeingEdited.secondPoint, CGPointZero)) {
                CGContextAddLineToPoint(context, self.segmentBeingEdited.secondPoint.x, self.segmentBeingEdited.secondPoint.y);
            }

            if (!CGPointEqualToPoint(self.segmentBeingEdited.thirdPoint, CGPointZero)) {
                CGContextAddLineToPoint(context, self.segmentBeingEdited.thirdPoint.x, self.segmentBeingEdited.thirdPoint.y);
            }

            CGContextStrokePath(context);
            //      CGContextEndTransparencyLayer(context);
        }

        incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        erased = TRUE;
        return;
    }

    CGContextSetBlendMode(context, kCGBlendModeNormal);


    if (!CGPointEqualToPoint(first, CGPointZero)) {
        CGContextMoveToPoint(context, first.x, first.y);

        if (!CGPointEqualToPoint(second, CGPointZero)) {
            CGContextAddLineToPoint(context, second.x, second.y);
        }

        if (!CGPointEqualToPoint(third, CGPointZero)) {
            CGContextAddLineToPoint(context, third.x, third.y);
        }

        CGContextStrokePath(context);
    }

    incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
}

@end

To recap, i'd like to draw a bunch of angles, then have the ability to edit any of them by tapping the any of the 3 points of the angle I wish to edit. then by dragging, the angle is edited to extend to where the tap is currently located. Just looking to make it better/simpler. Any comments/insight welcome!

Foi útil?

Solução 2

You're not handling the editing flag properly. You call in touchesBegan:

        if(yDistance2 <= 25 || editing){
            NSLog(@"It's editing time, yo");
            editing = TRUE; // Here's your problem

Which means in touchesMoved, when you hit:

if(xDistance <= 25 || editing) // Note the "Or editing"

editing equals true, which means it will move point 1 instead, since it hits that if statement first, no matter which point set that.

Conclusion

You need to use a specific editing flag for each point. Not a shared one. At least not with an or check. Or better yet, handle touchesMoved a little nicer. You don't need to almost completely duplicate your code from touchesBegan

Side Note

Although not completely related to your problem, I believe you are also swaping point2 and point3 in your if statements. In checking xDistance2:

else if(xDistance2 <= 25 || editing){
            NSLog(@"X is pretty close");
            if(yDistance2 <= 25 || editing){
                NSLog(@"It's editing time, yo");
                editing = TRUE;
                first = currentSegment.firstPoint;
                second = currentSegment.secondPoint;
                third = point; // <- Why are you setting third to point and not second to point.
                self.segmentBeingEdited = currentSegment;
                [self setNeedsDisplay];
                return;
            }
    }

Ditto when you check XDistance3. You set second to point, instead of third.

Outras dicas

Ok--This may not be exactly what you wanted... I created an editable angle UI which I think is a pretty good way to put together what you want to do. (I like projects like this)

There's a canvas, view controller, angle view and angle handle view. Taps on the canvas are recorded. When 3 taps happen an angle view (with handles inside it) is created. Tapping on an existing angle selects it (revealing the handles and selection UI)

The project is on github.

edit

I didn't use gesture recognizers, but that might be a good alternative.

enter image description here

There's one obvious and one non-obvious bug in your code.

The obvious one is that in your touchesBegan method, you are mistakenly setting the values of second and third - when you are close to the second point, you are assigning the touch point value to the third variable and vice versa.

The non-obvious one is that you have a global variable, editing, which is set in touchesBegan for whichever point you are editing, and then used in an or condition in touchesMoved, meaning that as soon as you drag anything, it immediately falls through to the first point of the first item in the array.

You've got too much going on in your touchesMoved method. You should be determining which point of which segment is being edited in touchesBegan, and then in touchesMoved only doing anything if you are editing a particular point - don't try and work out what you are editing again. You also don't really need to maintain all that separate state - you've got a segmentBeingEdited property, just use that, and a flag to say which point you are editing, and update the values directly while you are dragging.

This was obvious almost immediately by including a bit of context in your log messages - copy-pasting the same log message everywhere will always confuse you in the end.

Here's an adapted version of your touchesXX methods. I've removed a lot of unnecessary code and state for you. It still isn't working quite right - there are problems with drawing the edited segments. At some point you will need to add in code for a full redraw, or keep each image separate and remove the unneeded ones, but hopefully the principles I'm explaining will be straightforward.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.segmentBeingEdited = nil;
    pointBeingEdited = 0;

    UITouch *touch = [touches anyObject];

    CGPoint point = [touch locationInView:self];

    // Are we editing an existing segment?
    for(int i = 0;i<self.tapArray.count;i++){
        Segment *currentSegment = [self.tapArray objectAtIndex:i];

        int xDistance = abs(point.x - currentSegment.firstPoint.x);
        int yDistance = abs(point.y - currentSegment.firstPoint.y);
        int xDistance2 = abs(point.x - currentSegment.secondPoint.x);
        int yDistance2 = abs(point.y - currentSegment.secondPoint.y);
        int xDistance3 = abs(point.x - currentSegment.thirdPoint.x);
        int yDistance3 = abs(point.y - currentSegment.thirdPoint.y);

        if(xDistance <= 25){
            if(yDistance <= 25){
                pointBeingEdited = 1;
            }
        }
        else if(xDistance2 <= 25 ){
            if(yDistance2 <= 25){
                pointBeingEdited = 2;
            }
        }
        else if(xDistance3 <= 25){
            if(yDistance3 <= 25){
                pointBeingEdited = 3;
            }
        }

        if (pointBeingEdited != 0)
        {
            NSLog(@"editing point %d",pointBeingEdited);
            self.segmentBeingEdited = currentSegment;
            return;
        }
    }

    // Creating a new segment. Update data.
    if (pointBeingAdded == 0)
    {
        first = [touch locationInView:self];
        second = CGPointZero;
        third = CGPointZero;
    }
    else if (pointBeingAdded == 1)
    {
        second = [touch locationInView:self];
    }
    else if (pointBeingAdded == 2)
    {
        third = [touch locationInView:self];
    }
    pointBeingAdded++;

    [self setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {

    if (!self.segmentBeingEdited)
        return;

    UITouch *touch = [touches anyObject];

    CGPoint point = [touch locationInView:self];
    if (pointBeingEdited == 1)
    {
        self.segmentBeingEdited.firstPoint = point;
    }
    else if (pointBeingEdited == 2)
    {
        self.segmentBeingEdited.secondPoint = point;
    }
    else if (pointBeingEdited == 3)
    {
        self.segmentBeingEdited.thirdPoint = point;
    }
    [self drawBitmap];
    [self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

    if(self.segmentBeingEdited){
        first = CGPointZero;
        second = CGPointZero;
        third = CGPointZero;
    }

    if(pointBeingAdded == 3){
        // Add new segment
        Segment *segment = [[Segment alloc] init];
        segment.firstPoint = first;
        segment.secondPoint = second;
        segment.thirdPoint = third;

        if(self.tapArray == nil){
            self.tapArray = [[NSMutableArray alloc] init];
        }
        [self.tapArray addObject:segment];
        pointBeingAdded = 0;
    }
    [self drawBitmap];
    self.segmentBeingEdited = nil;
}

I would like to say some things about your code:

1- It's not necessary but, you could use a tap and a pan gesture recognizers, I think that would simplify your code.

2- When you try to find the point being edited you repeat a lot of code, you could iterate the points for each segment like this:

- (void)tapAction:(UITapGestureRecognizer *)recognizer {
CGPoint point = [recognizer locationInView:self];
self.segmentBeingEdited = nil;
CGPoint currentPoint;
for (Segment *segment in self.tapArray) {
    for (NSInteger p = 0; p < 3; p++) {
        currentPoint = [segment pointAtIndex:p];
        CGFloat distance = fDistance(point, currentPoint);
        if (distance <= 25.0) {
            self.segmentBeingEdited = segment;
            self.pointBeingEdited = p;
            [self setNeedsDisplay];
            return;
        }
    }
}
Segment *segment = [self.tapArray lastObject];
if (!segment || segment.points == 3) {
    segment = [[Segment alloc] init];
    [self.tapArray addObject:segment];
}
[segment setPoint:point atIndex:segment.points];
segment.points = segment.points + 1;
[self setNeedsDisplay];

}

3- The Segment class could have methods to get and set the points based on the index 0,1,2 and the number of points already added, I think this would increase cohesion and simplify your code.

4- The algorithm you use to determine if the distance from the tap location to some point is less than 25... well, it works but it's technically incorrect, you should use something like this:

static inline CGFloat fDistance(CGPoint point1, CGPoint point2) {
CGFloat dx = point1.x - point2.x;
CGFloat dy = point1.y - point2.y;
return sqrtf(dx * dx + dy * dy); }

5- I noticed that you use CGPointEqualToPoint(point, CGPointZero) in some if's, again, it works but there is better ways to do that, you could use a flag to find the next point being added.

Finally you could use the pan gesture recognizer to edit the selected point:

- (void)panAction:(UIPanGestureRecognizer *)recognizer {
CGPoint location = [recognizer locationInView:self];

switch (recognizer.state) {
    case UIGestureRecognizerStateBegan:
    {
        CGPoint point = [self.segmentBeingEdited pointAtIndex:self.pointBeingEdited];
        CGFloat distance = fDistance(location, point);
        if (distance > 25.0) {
            self.segmentBeingEdited = nil;
        }
        break;
    }
    case UIGestureRecognizerStateChanged:
    {
        if (self.segmentBeingEdited) {
            [self.segmentBeingEdited setPoint:location atIndex:self.pointBeingEdited];
            [self setNeedsDisplay];
        }
    }

    default:
        break;
}
}

I hope this can be useful.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top