7

Consider this ASCII drawing:

A _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ D
 |                               |
 |                               |
 |                               |
 |                               |
 |                               |
 |                               |
 |                               |
 |                               |
 |                               |
 |                               |
 |                               |
 |                               |
 |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
B                                 C

Points A, B, C, and D are known CGPoints within an NSMutableArray and have been used to create a filled CGPath. Now consider this drawing:

A _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ D
 |                               |
 |                               |
 |                               |
 |                               |
 |            H _ _ _ I          |
 |             |     |           |
 |             |     |           |
 |      F _ _ _|     |           |
 |       |      G    |           |
 |       |           |_ _ _ K    |
 |       |          J      |     |
 |       |                 |     |
 |_ _ _ _| _ _ _ _ _ _ _ _ |_ _ _|
B         E               L       C

CGPoints E, F, G, H, I, J, K, and L are known and have been appended to the end of the NSMutableArray.

The Question

How can I rearrange all the points within the array to create a CGPath that looks like the drawing below?

A _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ D
 |                               |
 |                               |
 |                               |
 |                               |
 |            H _ _ _ I          |
 |             |     |           |
 |             |     |           |
 |      F _ _ _|     |           |
 |       |      G    |           |
 |       |           |_ _ _ K    |
 |       |          J      |     |
 |       |                 |     |
 |_ _ _ _|                 |_ _ _|
B         E               L       C

Currently I have no trouble creating a CGPath - if I know the order of the CGpoints - by looping through them:

CGPoint firstPoint = [[points objectAtIndex:0] CGPointValue];
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, firstPoint.x, firstPoint.y);
for (int i = 0; i < [points count]; i++)
{
    CGPathAddLineToPoint(path, NULL, [[points objectAtIndex:i] CGPointValue].x, [[points objectAtIndex:i] CGPointValue].y);
}
CGPathCloseSubpath(path);

... but this assumes that a line should be drawn from each point in the array i to the following point i + 1. In the drawing above, lines would have to be drawn from A → B, B → E, E → F ... K → L, L → C, C → D. If E is not after B and C is not after L in the array (which they won't be), then this obviously won't draw correctly.

More Information

  1. All lines are drawn perpendicular to each other, so all CGPoints should share an x or y coordinate with the CGPoint before and after them.
  2. (An extension of #1) A point will always be at right angles to the points before and after it.
  3. It is not possible to predict where the points will occur within the square, and the new set of points may or may not be in order when appended to the end of the array.
  4. ...

Other Possible Layouts

A _ _ _ _ _ _ _ K         P _ _ _ D
 |             |           |     | 
 |             |           |     |
 |             |    N _ _ _|     |
 |             |     |      O    |
 |             |_ _ _|           |
 |            L       M          |
 |                               |
 |            F _ _ _ G          |
 |             |     |           |
 |             |     |_ _ _ I    |
 |             |    H      |     |
 |             |           |     |
 |_ _ _ _ _ _ _|           |_ _ _|
B               E         J       C


A _ _ _ _ _ _ _ _ _ _ M
 |                   |           
 |                   |           
 |                   |_ _ _ _ _ _ O
 |                  N            |
 |            H _ _ _ I          |
 |             |     |           |
 |             |     |           |
 |      F _ _ _|     |           |
 |       |      G    |           |
 |       |           |_ _ _ K    |
 |       |          J      |     |
 |       |                 |     |
 |_ _ _ _|                 |_ _ _|
B         E               L       C
lobianco
  • 6,226
  • 2
  • 33
  • 48
  • How are you putting the points into your array? Why can't you just have them in the order you need? – rdelmar Jul 21 '12 at 16:07
  • Are the points always at right angles to one another or is it just for the example? – Craig Siemens Jul 21 '12 at 17:38
  • @rdelmar the points are added at an unknown time after the array is initialized. The only four points in the array at initialization are the four corners. – lobianco Jul 21 '12 at 19:09
  • @CleverError: Yes the points will always be at right angles to one another. I've added that tidbit to the question. Thanks! – lobianco Jul 21 '12 at 19:10
  • You really haven't given enough information to answer this question -- in this particular example you could insert the new set of points after index 1 (assuming A is index 0, B is 1 etc.) instead of appending them to the end. But I assume this is just an example, and you need to say more about the general case. Will the new points always be inserted between b and c for instance? Are the new set of points themselves in the right order? Etc. – rdelmar Jul 21 '12 at 19:31
  • @rdelmar I've added more information to answer your questions. And I've added the algorithm tag. Thanks Henri. – lobianco Jul 21 '12 at 21:05
  • 1) Will the added irregular figure always have an edge or side that is collinear with one of the sides of the original rectangular figure? 2) Might there be more than one such irregular figure that can be added? 3) Is the final goal just to fill in the final eroded figure or will you be needing to stroke its outline as well? – inwit Jul 21 '12 at 22:04
  • @inwit 1)Yes, 2)Yes, 3)A stroked outline would be nice but is not absolutely necessary. I've added alternate possible layouts to the question. – lobianco Jul 21 '12 at 22:34

3 Answers3

2

Here is one approach (in pseudocode) if all you are interested in is fills and not strokes:

  1. Create an empty mutable path using CGPathCreateMutable().

  2. Add the original rectangular figure to the mutable path as a closed loop.

  3. One by one, add each of the additional irregular figures to the mutable path, separately, as a closed loop.

  4. Use the function CGContextEOFillPath() to fill the eroded figure using the even-odd fill rule.

The even-odd fill rule is described in the section Filling a Path in the Quartz 2D Programming Guide.

You may also be interested in this

inwit
  • 935
  • 6
  • 11
  • Thanks inwit. Strokes might be a necessity, but upvote for the even-odd fill rule information. – lobianco Jul 22 '12 at 18:55
  • Do you even need the even-odd fill rule? If you just fill the additional closed figures with the background color, it should give you the filled eroded figure you want (shouldn't it?). – rdelmar Jul 22 '12 at 21:25
2

I think this might do it. I tested it with a couple of sets of numbers and it seemed to work. Basically, I start with the point at index 0 (any starting point should work), add that to the arrangedPoints array and then look for the nearest point with the same y value -- that point is added to arrangedPoints and deleted from the original randomPoints array. I then do the same thing in the x direction and repeat until there's only one point left in the randomPoints array, and add that to the end of arrangedPoints.

-(void)arrangePoints:(NSMutableArray *) randomPoints {
    NSMutableArray *arrangedPoints = [NSMutableArray array];
    [arrangedPoints addObject:[randomPoints objectAtIndex:0]];
    [randomPoints removeObjectAtIndex:0];

    while (randomPoints.count > 1) {
        //Look for the closest point that has the same y value
        int yValueOfknownPoint = [[arrangedPoints lastObject] CGPointValue].y;
        int xValueOfknownPoint = [[arrangedPoints lastObject] CGPointValue].x;
        NSIndexSet *indSet = [randomPoints indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop){
            return yValueOfknownPoint == [obj CGPointValue].y;
        }];
        NSLog(@"%d",indSet.count);

        if (indSet.count == 1) {
            [arrangedPoints addObject:[randomPoints objectAtIndex:indSet.firstIndex]];
            [randomPoints removeObjectAtIndex:indSet.firstIndex];
        }else{
            __block int min = 10000000;
            __block int foundIndex;
            [indSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
                int posibleMin = fabs(xValueOfknownPoint - [[randomPoints objectAtIndex:idx]CGPointValue].x);
                if (posibleMin < min) {
                    min = posibleMin;
                    foundIndex = idx;
                }
            }];
            [arrangedPoints addObject:[randomPoints objectAtIndex:foundIndex]];
            [randomPoints removeObjectAtIndex:foundIndex];
        }

        //Look for the closest point that has the same x value
        xValueOfknownPoint = [[arrangedPoints lastObject] CGPointValue].x;
        yValueOfknownPoint = [[arrangedPoints lastObject] CGPointValue].y;
        indSet = [randomPoints indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop){
            return xValueOfknownPoint == [obj CGPointValue].x;
        }];

        if (indSet.count == 1) {
            [arrangedPoints addObject:[randomPoints objectAtIndex:indSet.firstIndex]];
            [randomPoints removeObjectAtIndex:indSet.firstIndex];
        }else{
            __block int min = 10000000;
            __block int foundIndex;
            [indSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
                int posibleMin = fabs(yValueOfknownPoint - [[randomPoints objectAtIndex:idx]CGPointValue].y);
                if (posibleMin < min) {
                    min = posibleMin;
                    foundIndex = idx;
                }
            }];
            [arrangedPoints addObject:[randomPoints objectAtIndex:foundIndex]];
            [randomPoints removeObjectAtIndex:foundIndex];
        }
    }
    [arrangedPoints addObject:randomPoints.lastObject];
    NSLog(@"%@",arrangedPoints);

}

Some added points to address known problems:

To deal with equidistant points like G and M from N, I think I would keep the sets of points separate -- rather than putting every thing into one array, I would keep the original rectangle, and each new set of points that come in separate. Then when I was constructing the paths, I wold only look for point within my own set of points or the original rectangle, but not in any other set of points.

To deal with the problem inwit brought up, of doubling back on itself, I think you would have to determine whether a set of points intersects a side or a corner -- I think only the corner ones would present that problem. You could check for a corner one by seeing that 2 of the points fall on 2 different lines of the original rectangle (rather than on the same line). You would then have to calculate the missing point (a putative point p in your last example above) and see which point of the original rectangle it's identical too, and then delete that point from the path. I think that my algorithm would then work correctly.

rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • Will your method work with the last figure in the `Other Possible Layouts` above, where the chunk that is taken out is a smaller rectangle in one corner of the original rectangle? The smaller rectange would share a common point with the original rectangle, the point at top-left in the aforementioned example. – inwit Jul 21 '12 at 23:46
  • I think so, because my method doesn't have any concept of the original rectangle. It just looks for the nearest point along the x direction followed by the y direction -- all points can be in a random order. – rdelmar Jul 22 '12 at 00:01
  • Below is a test case to test your method against. It is a set of 8 points, the first 4 represent the original rectangle and the next 4 represent a smaller rectangle removed from one corner of it. (0, 0), (10, 0), (10, 10), (0, 10), (10, 9), (10, 10), (9, 10), (9, 9). (Note that the point (10, 10) is degenerate, i.e., it appears twice in the list. I think this will cause the path to "double back" on itself, which won't be a problem for fills but will result in a protruding bit for strokes.) – inwit Jul 22 '12 at 00:09
  • I can tell without checking that that set of points would cause a problem. Also in the first example above in "other possible layouts" if my method got to point m and points n and g were equidistant from it, it would choose the first one in the array. There's no way to resolve these issues without further rules. – rdelmar Jul 22 '12 at 01:28
  • I like this approach - I'll give it a shot. What solutions would you suggest for the shortcomings you pointed out (such as the points equidistant from a certain point)? – lobianco Jul 22 '12 at 18:53
  • I've added some additional comments to my post to address some of the problems that I mentioned and @inwit brought up. – rdelmar Jul 22 '12 at 21:06
0

Alright, this may seem a very complicated algorithm (and it is far from optimal), but I'd approach it like this.

1. Find the point(s) with lowest X-value

2. Of those points find point P (using a loop) such that:
    - P has a nabour N with the same X-value
    - There exists no other point that vertically lies between P and its nabour N with the same Y value
    (if no P exists with a nabour, the algorithm has ended, after joining P with its horizontally aligned point)

3. If there exists a point with the same X value as P (horizontally aligned), join that point and P with a line.

4. Now join point P with its nabour N

5. Find all points with the same X-value as N (horizontally aligned)

6. Goto 2

I hope it works (haven't tested it).

v1Axvw
  • 3,054
  • 3
  • 26
  • 40