pythonpygamegame-developmentpygame2

Line is detected as diagonal of rectangle while using collidepoint function in pygame


I am working on a graph based project. I am drawing lines to represent the edges. My goal is to change color of line when I click on the specific line. the problem is pygame treats line as diagonal of virtual rectangle. so when even if I don't click on the line but the mouse position is in the projected area of the virtual rectangle, the event is detected as collision while using collidepoint(). I want to detect this only when the mouse is clicked on actual line only.

I am new to pygame, so let me know if there is other function or library that I can use. This is the example code of my project.

import pygame

pygame.init()

screen = pygame.display.set_mode((1200,700))

running = True
red = 0
green = 255
blue = 210

while running:
   screen.fill((red,green,blue))
    line = pygame.draw.line(screen, green, (50,50), (400,400),10)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            if line.collidepoint(event.pos):
                red = 255
                green = 0
                blue = 0
        if event.type == pygame.MOUSEBUTTONUP:
            red = 0
            green = 255
            blue = 210

    pygame.display.update()

Solution

  • pygame.draw.line returns a pygame.Rect object that defines the axis aligned bounding rectangle surrounding the line. collidepoint test if a point is in a rectangle.


    You have to use a different approach. Write a function that computes the shortest distance of a point to a line:

    dist = abs(dot(normalized(NV), P - LP)), where NV is the normal vector to the line, LP is a point on the line and P is the point whose distance needs to be calculated.

    import math
    
    def distance_point_line(pt, l1, l2):
        nx, ny = l1[1] - l2[1], l2[0] - l1[0]
        nlen = math.hypot(nx, ny)
        nx /= nlen
        ny /= nlen
        vx, vy = pt[0] - l1[0],  pt[1] - l1[1]
        dist = abs(nx*vx + ny*vy)
        return dist
    

    The same function with the use of pygame.math.Vector2:

    def distance_point_line(pt, l1, l2):
        NV = pygame.math.Vector2(l1[1] - l2[1], l2[0] - l1[0])
        LP = pygame.math.Vector2(l1)
        P = pygame.math.Vector2(pt)
        return abs(NV.normalize().dot(P -LP))
    

    Test whether the mouse pointer is in the rectangle defined by the line and whether the distance is less than half the line width:

    if (line_rect.collidepoint(event.pos) and 
        distance_point_line(event.pos, (50,50), (400,400)) < 5):
        # [...]
    

    Explanation:

    I've used the Dot product distance from the point to the line.. In general The Dot product of 2 vectors is equal the cosine of the angle between the 2 vectors multiplied by the magnitude (length) of both vectors.

    dot( A, B ) == | A | * | B | * cos( angle_A_B ) 
    

    This follows, that the Dot product of 2 Unit vectors is equal the cosine of the angle between the 2 vectors, because the length of a unit vector is 1.

    uA = normalize( A )
    uB = normalize( B )
    cos( angle_A_B ) == dot( uA, uB )
    

    Therefore the Dot product of the normalized normal vector to the line (NV) and a vector from a point on the line (LP) to the point whose distance must be calculated (P) is the shortest distance of the point to the line.


    Minimal example:

    import pygame
    import math
    
    pygame.init()
    screen = pygame.display.set_mode((1200,700))
    
    def distance_point_line(pt, l1, l2):
        NV = pygame.math.Vector2(l1[1] - l2[1], l2[0] - l1[0])
        LP = pygame.math.Vector2(l1)
        P = pygame.math.Vector2(pt)
        return abs(NV.normalize().dot(P -LP))
    
    color = (255, 255, 255)
    running = True
    while running:
    
        screen.fill((0, 0, 0))
        line_rect = pygame.draw.line(screen, color, (50,50), (400,400), 10)
        pygame.display.update()
        
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            
            if event.type == pygame.MOUSEBUTTONDOWN: 
                if (line_rect.collidepoint(event.pos) and 
                    distance_point_line(event.pos, (50,50), (400,400)) < 5):
                    color = (255, 0, 0)
            if event.type == pygame.MOUSEBUTTONUP:
                color = (255, 255, 255)