Обнаружение попадания при рисовании линий в iOS - PullRequest
13 голосов
/ 26 июня 2011

Я хотел бы позволить пользователю рисовать кривые таким образом, чтобы ни одна линия не могла пересечь другую линию или даже себя.Рисование кривых не является проблемой, и я даже обнаружил, что могу создать путь, который будет закрыт и все еще будет выглядеть как линия путем отслеживания узлов линии вперед и назад, а затем закрытия пути.

К сожалению, iOS предоставляет только проверку того, содержится ли точка в замкнутом пути (Содержит Point: и CGPathContainsPoint).К сожалению, пользователь может довольно легко перемещать свой палец достаточно быстро, чтобы точки касания приземлялись по обе стороны от существующего пути, фактически не ограничиваясь этим путем, поэтому тестирование точек касания довольно бессмысленно.

Я могу 'Не найти никакого «пересечения» пути.

Любые другие мысли о том, как выполнить эту задачу?

Ответы [ 2 ]

6 голосов
/ 30 июня 2011

Ну, я нашел способ сделать это.Это несовершенно, но я подумал, что другие могут захотеть увидеть технику, так как этот вопрос был поставлен несколько раз.Техника, которую я использовал, рисует все проверяемые элементы в растровом контексте, а затем рисует новый сегмент прогрессирующей линии в другом растровом контексте.Данные в этих контекстах сравниваются с использованием побитовых операторов, и если обнаруживается какое-либо перекрытие, объявляется попадание.

Идея этого метода состоит в том, чтобы проверить каждый сегмент вновь нарисованной линии на предмет всех ранее нарисованных линий идаже против более ранних частей той же линии.Другими словами, этот метод будет определять, когда линия пересекает другую линию, а также когда она пересекает себя.

Доступно пример приложения, демонстрирующего эту методику: LineSample.zip .

Ядро тестирования попаданий сделано в моем объекте LineView.Вот два ключевых метода:

- (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: /4757238/kak-sdelat-sravnenie-rastrovyh-izobrazhenii-v-ios#4757248
    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;
}

Я хотел бы услышать предложения или увидеть улучшения.С одной стороны, было бы намного быстрее проверять только ограничивающий прямоугольник нового сегмента, а не весь вид.

1 голос
/ 29 декабря 2018

Swift 4, ответ основан на CGPath Hit Testing - Ole Begemann (2012)

Из блога Ole Begemann:

contains(point: CGPoint)

Эта функцияполезно, если вы хотите выполнить проверку по всему региону, который охватывает путь.Таким образом, содержит (точка: CGPoint) не работает с незамкнутыми путями, потому что у них нет внутренней части, которая была бы заполнена.

copy(strokingWithWidth lineWidth: CGFloat, lineCap: CGLineCap, lineJoin: CGLineJoin, miterLimit: CGFloat, transform: CGAffineTransform = default) -> CGPath

Эта функция создает целевой объект зеркального отражения, который покрывает только обведенную область пути.Когда пользователь нажимает на экран, мы перебираем цели, а не фактические формы.


Мое решение в коде

Я используюUITapGestureRecognizer, связанный с функцией tap ():

var bezierPaths = [UIBezierPath]()   // containing all lines already drawn
var tappedPaths = [CAShapeLayer]()

@IBAction func tap(_ sender: UITapGestureRecognizer) {        
    let point = sender.location(in: imageView)

    for path in bezierPaths {
        // create tapTarget for path
        if let target = tapTarget(for: path) {
            if target.contains(point) {
                tappedPaths.append(layer)
            }
        }
    }
}

fileprivate func tapTarget(for path: UIBezierPath) -> UIBezierPath {

    let targetPath = path.copy(strokingWithWidth: path.lineWidth, lineCap: path..lineCapStyle, lineJoin: path..lineJoinStyle, miterLimit: path.miterLimit)

    return UIBezierPath.init(cgPath: targetPath)
}
...