Pytanie Wykrywanie trafień podczas rysowania linii w systemie iOS


Chciałbym umożliwić użytkownikowi rysowanie krzywych w taki sposób, aby żadna linia nie mogła przekroczyć innej linii lub nawet samej siebie. Rysowanie krzywych nie stanowi problemu, a nawet stwierdziłem, że mogę utworzyć ścieżkę, która jest zamknięta i wciąż ładna, jak linia, śledząc węzły linii do przodu i wstecz, a następnie zamykając ścieżkę.

Niestety system iOS zapewnia tylko test sprawdzający, czy punkt jest zamknięty w zamkniętej ścieżce (zawiera punkty: i CGPathContainsPoint). Niestety, użytkownik może dość łatwo przesunąć palec na tyle szybko, że punkty dotykowe lądują po obu stronach istniejącej ścieżki, nie będąc w rzeczywistości zamkniętymi przez tę ścieżkę, więc testowanie punktów dotykowych jest bezsensowne.

Nie mogę znaleźć żadnego "przecięcia" metody ścieżki.

Jakieś inne myśli o tym, jak wykonać to zadanie?


12
2018-06-26 05:46


pochodzenie


To pytanie jest podobne do innego pytania SO. stackoverflow.com/questions/1021801/cgpathref-intersection  Te odpowiedzi sugerują, patrząc na każdy pojedynczy piksel, który będzie wolny. Możesz uzyskać CGPathRef z obiektu UIBezierPath przez myBezierPath.CGPath - Andrew
Dobry połów na podobne pytanie. Pracuję nad podejściem, które porównuje kolejne bitmapy. Kiedy już mam kod demonstracyjny, umieszczę go tutaj. Tymczasem sprawdzę również odpowiedzi na to pytanie. - EFC


Odpowiedzi:


Cóż, wymyśliłem sposób, żeby to zrobić. Jest niedoskonały, ale myślałem, że inni będą chcieli zobaczyć tę technikę, ponieważ to pytanie zostało kilka razy wznowione. Zastosowana technika rysuje wszystkie elementy, które mają być przetestowane, w kontekście bitmapowym, a następnie rysuje nowy segment postępującej linii w innym kontekście bitmapowym. Dane w tych kontekstach są porównywane za pomocą operatorów bitowych i jeśli zostanie znalezione jakiekolwiek pokrycie, zostanie zadeklarowane trafienie.

Ideą tej techniki jest przetestowanie każdego segmentu nowo narysowanej linii na wszystkich wcześniej narysowanych liniach, a nawet na wcześniejszych fragmentach tej samej linii. Innymi słowy, technika ta wykrywa, kiedy linia przecina inną linię, a także gdy przechodzi przez siebie.

Dostępna jest przykładowa aplikacja demonstrująca tę technikę: LineSample.zip.

Rdzeń testowania trafień jest wykonywany w moim obiekcie LineView. Oto dwie kluczowe metody:

- (CGContextRef)newBitmapContext {

    // creating b&w bitmaps to do hit testing
    // based on: http://robnapier.net/blog/clipping-cgrect-cgpath-531
    // see "Supported Pixel Formats" in Quartz 2D Programming Guide
    CGContextRef bitmapContext =
    CGBitmapContextCreate(NULL, // data automatically allocated
                          self.bounds.size.width,
                          self.bounds.size.height,
                          8, 
                          self.bounds.size.width,
                          NULL,
                          kCGImageAlphaOnly);
    CGContextSetShouldAntialias(bitmapContext, NO);
    // use CGBitmapContextGetData to get at this data

    return bitmapContext;
}


- (BOOL)line:(Line *)line canExtendToPoint:(CGPoint) newPoint {

    //  Lines are made up of segments that go from node to node. If we want to test for self-crossing, then we can't just test the whole in progress line against the completed line, we actually have to test each segment since one segment of the in progress line may cross another segment of the same line (think of a loop in the line). We also have to avoid checking the first point of the new segment against the last point of the previous segment (which is the same point). Luckily, a line cannot curve back on itself in just one segment (think about it, it takes at least two segments to reach yourself again). This means that we can both test progressive segments and avoid false hits by NOT drawing the last segment of the line into the test! So we will put everything up to the  last segment into the hitProgressLayer, we will put the new segment into the segmentLayer, and then we will test for overlap among those two and the hitTestLayer. Any point that is in all three layers will indicate a hit, otherwise we are OK.

    if (line.failed) {
        // shortcut in case a failed line is retested
        return NO;
    }
    BOOL ok = YES; // thinking positively

    // set up a context to hold the new segment and stroke it in
    CGContextRef segmentContext = [self newBitmapContext];
    CGContextSetLineWidth(segmentContext, 2); // bit thicker to facilitate hits
    CGPoint lastPoint = [[[line nodes] lastObject] point];
    CGContextMoveToPoint(segmentContext, lastPoint.x, lastPoint.y);
    CGContextAddLineToPoint(segmentContext, newPoint.x, newPoint.y);
    CGContextStrokePath(segmentContext);

    // now we actually test
    // based on code from benzado: http://stackoverflow.com/questions/6515885/how-to-do-comparisons-of-bitmaps-in-ios/6515999#6515999
    unsigned char *completedData = CGBitmapContextGetData(hitCompletedContext);
    unsigned char *progressData = CGBitmapContextGetData(hitProgressContext);
    unsigned char *segmentData = CGBitmapContextGetData(segmentContext);

    size_t bytesPerRow = CGBitmapContextGetBytesPerRow(segmentContext);
    size_t height = CGBitmapContextGetHeight(segmentContext);
    size_t len = bytesPerRow * height;

    for (int i = 0; i < len; i++) {
        if ((completedData[i] | progressData[i]) & segmentData[i]) { 
            ok = NO; 
            break; 
        }
    }

    CGContextRelease(segmentContext);

    if (ok) {
        // now that we know we are good to go, 
        // we will add the last segment onto the hitProgressLayer
        int numberOfSegments = [[line nodes] count] - 1;
        if (numberOfSegments > 0) {
            // but only if there is a segment there!
            CGPoint secondToLastPoint = [[[line nodes] objectAtIndex:numberOfSegments-1] point];
            CGContextSetLineWidth(hitProgressContext, 1); // but thinner
            CGContextMoveToPoint(hitProgressContext, secondToLastPoint.x, secondToLastPoint.y);
            CGContextAddLineToPoint(hitProgressContext, lastPoint.x, lastPoint.y);
            CGContextStrokePath(hitProgressContext);
        }
    } else {
        line.failed = YES;
        [linesFailed addObject:line];
    }
    return ok;
}

Chciałbym usłyszeć sugestie lub zobaczyć ulepszenia. Po pierwsze, byłoby znacznie szybciej tylko sprawdzić prostą ograniczającą nowego segmentu zamiast całego widoku.


6
2018-06-29 20:29



Uczciwe ostrzeżenie: już znalazłem kilka błędów w przykładowej aplikacji, więc upewnij się, że zwracasz uwagę na własną implementację. Wydaje się, że podstawowa technika działa, tylko kilka problemów z implementacją, które można poprawić. Zrobię trochę więcej prób i będę go aktualizował, ale mój główny cel będzie gdzie indziej. - EFC
Witaj @EFC, jestem trochę nowy dla społeczności i początkującego programisty iOS, czy możesz wskazać mi konkretnie, gdzie jest kod, w którym zapobiega się przecinaniu? Potrzebuję tylko tej części. - EdSniper
Aby zapobiec przecinaniu, po prostu patrzę, czy jakieś bity są wspólne dla starych i nowych segmentów. The if ((completedData[i] | progressData[i]) & segmentData[i]) {linia jest tym, co robi ten faktyczny test. Ten test pochodzi od stackoverflow.com/a/6515999/383737. - EFC