Swift: Is there an easy way to draw shapes and detect whether they intersect?
If this consists of a series of line segments, one can adapt Martin R’s answer to UIBezierPath intersect to not only detect intersections, but to also identify where the intersections are:
func intersectionBetweenSegments(_ p0: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint) -> CGPoint? {
var denominator = (p3.y - p2.y) * (p1.x - p0.x) - (p3.x - p2.x) * (p1.y - p0.y)
var ua = (p3.x - p2.x) * (p0.y - p2.y) - (p3.y - p2.y) * (p0.x - p2.x)
var ub = (p1.x - p0.x) * (p0.y - p2.y) - (p1.y - p0.y) * (p0.x - p2.x)
if (denominator < 0) {
ua = -ua; ub = -ub; denominator = -denominator
}
guard ua >= 0 && ua <= denominator && ub >= 0 && ub <= denominator && denominator != 0 else {
return nil
}
return CGPoint(x: p0.x + ua / denominator * (p1.x - p0.x), y: p0.y + ua / denominator * (p1.y - p0.y))
}
Thus, if you have an array of CGPoint
values and you want to identify all of the intersections, you could do something like:
let n = points.count - 1
for i in 1 ..< n {
for j in 0 ..< i-1 {
if let intersection = intersectionBetweenSegments(points[i], points[i+1], points[j], points[j+1]) {
// do whatever you want with `intersection`
}
}
}
For example, you can add a dot to the screen where the segments intersect:
If, however, your curve consists of cubic bezier curves, it is more complicated. You might consider, though, Checking if two cubic Bézier curves intersect.
Detect that SKShapedNode is closed
- Store the touches locations (points) in an array:
[yourArray addObject:[NSValue valueWithCGPoint:yourPoint]];
- Make a rect (size? your choice! the smaller it is, the more precise) around the first point in the array (first touch location).
- Control if the rect also contains the last point in the array (last touch location).
- If so, then close your path.
given two points how can I determine if a rectangle will pass through a line joining the points
You can do line intersection tests between the line determined by your 2 points and each the 4 sides of the rectangle
func intersectionBetweenSegments(p0: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint) -> CGPoint? {
var denominator = (p3.y - p2.y) * (p1.x - p0.x) - (p3.x - p2.x) * (p1.y - p0.y)
var ua = (p3.x - p2.x) * (p0.y - p2.y) - (p3.y - p2.y) * (p0.x - p2.x)
var ub = (p1.x - p0.x) * (p0.y - p2.y) - (p1.y - p0.y) * (p0.x - p2.x)
if (denominator < 0) {
ua = -ua; ub = -ub; denominator = -denominator
}
if ua >= 0.0 && ua <= denominator && ub >= 0.0 && ub <= denominator && denominator != 0 {
return CGPoint(x: p0.x + ua / denominator * (p1.x - p0.x), y: p0.y + ua / denominator * (p1.y - p0.y))
}
return nil
}
func intersectionBetweenRectAndSegment(rect: CGRect, _ p0: CGPoint, _ p1: CGPoint) {
var result = false
let topLeftCorner = rect.origin
let topRightCorner = CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y)
let bottomLeftCorner = CGPoint(x: rect.origin.x, y: rect.origin.y + rect.size.height)
let bottomRightCorner = CGPoint(x: rect.origin.x + rect.size.width, y: rect.origin.y + rect.size.height)
if intersectionBetweenSegments(po, p1, topLeftCorner, topRightCorner) != nil {
return true
}
if intersectionBetweenSegments(po, p1, topRightCorner, bottomRightCorner) != nil {
return true
}
if intersectionBetweenSegments(po, p1, bottomRightCorner, bottomLeftCorner) != nil {
return true
}
if intersectionBetweenSegments(po, p1, bottomLeftCorner, topLeftCorner) != nil {
return true
}
return false
}
Segment intersection code copied from here.
Not tested!
Hit detection when drawing lines in iOS
Well, I did come up with a way to do this. It is imperfect, but I thought others might want to see the technique since this question was upvoted a few times. The technique I used draws all the items to be tested against into a bitmap context and then draws the new segment of the progressing line into another bitmap context. The data in those contexts is compared using bitwise operators and if any overlap is found, a hit is declared.
The idea behind this technique is to test each segment of a newly drawn line against all the previously drawn lines and even against earlier pieces of the same line. In other words, this technique will detect when a line crosses another line and also when it crosses over itself.
A sample app demonstrating the technique is available: LineSample.zip.
The core of hit testing is done in my LineView object. Here are two key methods:
- (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;
}
I'd love to hear suggestions or see improvements. For one thing, it would be a lot faster to only check the bounding rect of the new segment instead of the whole view.
SwiftUI: How can I detect if two views are intersecting each other?
Your question is not stupid at all!
It seems to be easy
- read frame of first View (in global coordinates) and save it in preferences
- read frame of second View (in global coordinates) and save it in preferences
- calculate intersection when preferences changed
create our preference key structure is easy, it was already explained at this web site before (use search for more details :-))
struct Sizes: PreferenceKey {
typealias Value = [CGRect]
static var defaultValue: [CGRect] = []
static func reduce(value: inout [CGRect], nextValue: () -> [CGRect]) {
value.append(contentsOf: nextValue())
}
}
using GeometryReader in background View modifier was already explained
struct SizeReader: View {
var body: some View {
GeometryReader { proxy in
Color.clear
.preference(key: Sizes.self, value: [proxy.frame(in: .global)])
}
}
}
and now we can use it to save frame rectangles in our Sizes preference key
RectView(color: .pink).background(SizeReader())
What about our RectView ? To demonstrate how our "easy solution" works imagine that somebody create it with random size and random alignment
struct RectView: View {
let color: Color
let size = CGFloat(Int.random(in: 50 ... 200))
var body: some View {
color.frame(width: size, height: size)
.alignmentGuide(HorizontalAlignment.center) {
CGFloat.random(in: 0 ..< $0.width)
}
.alignmentGuide(VerticalAlignment.center) {
CGFloat.random(in: 0 ..< $0.width)
}
}
}
so far, so good, we are ready to check our "easy solution"
Lets finish our project with
struct ContentView: View {
@State var id = UUID()
var body: some View {
VStack {
ZStack {
Color.black
RectView(color: .pink)
.background(SizeReader())
RectView(color: .yellow)
.background(SizeReader())
.blendMode(.difference)
}
.onPreferenceChange(Sizes.self) { (value) in
let intersection = value[0].intersection(value[1])
print(value[0])
print(value[1])
print(intersection)
print()
}
.onTapGesture {
self.id = UUID()
}
Text("paceholder").id(id)
}
}
}
Run it and each time you click the black part of the screen you see two randomly sized and positioned rectangles and in the debug window printout of values of our interest.
WARNING check the printout and you'll see that something if wrong!
The code is buggy example and I put it here to demonstrate, that your question is not stupid at all! There is a lot of thinks to understand about SwiftUI layout and how it works behind the scene.
I hope, somebody clever will try to explain what's wrong and will guide us on how to make it functional, all experts are welcome! Please don't change RectView, think that it is some build-in SwiftUI component
UIBezierPath intersect
The problem is in the checkLineIntersection
method. With
if (ua > 0.0 && ua < 1.0 && ub > 0.0 && ub < 1.0) { return YES; }
you check only if the interior part of the lines segments intersect. But if the start or endpoint of the first line segment is equal to the start or endpoint of the second line segment, ua
and ub
will be 0.0
or 1.0
.
The solution is to include one end of the interval in the condition:
if (ua > 0.0 && ua <= 1.0 && ub > 0.0 && ub <= 1.0) { return YES; }
This seemed to work as expected in my test program.
Some further remarks:
I think you should activate the shortcut
if (denominator == 0.0f) return NO;
again to avoid division by zero.
In
touchesMoved
, you could add the new line to the array after checking for intersections. Now the new line is inserted first, which means that it is checked against itself for intersections.You have declared
Line
as subclass ofUIView
, but this is not really a view class. You could just declareLine
as subclass ofNSObject
.
ADDED: The following method might work even better, because it avoids the division and therefore possible overflow problems with small denominators:
-(BOOL)checkLineIntersection:(CGPoint)p1 :(CGPoint)p2 :(CGPoint)p3 :(CGPoint)p4
{
CGFloat denominator = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
CGFloat ua = (p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x);
CGFloat ub = (p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x);
if (denominator < 0) {
ua = -ua; ub = -ub; denominator = -denominator;
}
return (ua > 0.0 && ua <= denominator && ub > 0.0 && ub <= denominator);
}
Detect self intersection of a polygon with n sides?
This seems to be working pretty well for what I need. Adopted from Rob's answer here
func intersectionBetweenSegmentsCL(p0: CLLocationCoordinate2D, _ p1: CLLocationCoordinate2D, _ p2: CLLocationCoordinate2D, _ p3: CLLocationCoordinate2D) -> CLLocationCoordinate2D? {
var denominator = (p3.longitude - p2.longitude) * (p1.latitude - p0.latitude) - (p3.latitude - p2.latitude) * (p1.longitude - p0.longitude)
var ua = (p3.latitude - p2.latitude) * (p0.longitude - p2.longitude) - (p3.longitude - p2.longitude) * (p0.latitude - p2.latitude)
var ub = (p1.latitude - p0.latitude) * (p0.longitude - p2.longitude) - (p1.longitude - p0.longitude) * (p0.latitude - p2.latitude)
if (denominator < 0) {
ua = -ua; ub = -ub; denominator = -denominator
}
if ua >= 0.0 && ua <= denominator && ub >= 0.0 && ub <= denominator && denominator != 0 {
print("INTERSECT")
return CLLocationCoordinate2D(latitude: p0.latitude + ua / denominator * (p1.latitude - p0.latitude), longitude: p0.longitude + ua / denominator * (p1.longitude - p0.longitude))
}
return nil
}
I then implemented like this:
if coordArray.count > 2 {
let n = coordArray.count - 1
for i in 1 ..< n {
for j in 0 ..< i-1 {
if let intersection = intersectionBetweenSegmentsCL(coordArray[i], coordArray[i+1], coordArray[j], coordArray[j+1]) {
// do whatever you want with `intersection`
print("Error: Intersection @ \(intersection)")
}
}
}
}
Intersection of CGRect and CGPath
- Draw the path in a bitmap (white on alpha)
- then check the Rectangle part of bitmap. check if there is any white in that area which would mean overlapping
-- to make it more performant draw only the part of the bitmap that is in the rectangle.
I havent tried this and it wont offer real-time performance but it sounds ok to me
Related Topics
How to Calculate Current Location in Watchkit Extension
Nspredicate to Match "Any Entry in an Nsdatabase with Value That Contains a String"
How to Comment or Like a Photo in Facebook Through Fbconnect or Graph API in iPhone Sdk
How to Convert Image into Binary Format in iOS
How to Overlay One Video on Another in iOS
Rec iOS Conversations. Where to Start
Installing Openssl Library for Xcode
Mkmapview Doesn't Zoom Correctly While User Tracking Mode Is Mkusertrackingmodefollowwithheading
Insert String at Cursor Position of Uitextfield
Code Signing Issue in Xcode Version 8
How to Take Uiimage of Avcapturevideopreviewlayer Instead of Avcapturephotooutput Capture
Xcode 4.2 Mainstoryboard Not Found
Multiple Localized .Strings Files in iOS App Bundle
iOS Document/File Type Associations - Wildcard Uti
How to Set the Accessibility Label for a Particular Segment of a Uisegmentedcontrol