Skip to content

Implemented the Line.project #3402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open

Implemented the Line.project #3402

wants to merge 16 commits into from

Conversation

XFajk
Copy link
Contributor

@XFajk XFajk commented Apr 14, 2025

This PR should add the feature requested in #3019
I added the project() method to the pygame.geometry.Line object I added the docs and test for the method

@XFajk XFajk requested a review from a team as a code owner April 14, 2025 10:14
@damusss damusss added New API This pull request may need extra debate as it adds a new class or function to pygame geometry pygame.geometry labels Apr 14, 2025
@damusss
Copy link
Member

damusss commented Apr 14, 2025

Not my hideous drawings used as official documentation 😭
Jokes aside, thanks, I wasn't expecting for someone to work on my feature request. Tho I would suggest remaking those images, firstly to make them clearer and secondly to make them smaller. They really don't need to be so giant, that drawing could be likely done on a 100x100 canvas. We always try to free up space so the wheels are as small as possible and big pngs don't benefit much. You could also make a slightly longer png covering the three cases to have less files.

@aatle
Copy link
Contributor

aatle commented Apr 15, 2025

(discussion)
API suggestion: the do_clamp argument should be renamed to clamp, because the do_ is redundant and unpythonic, especially for new API.

Personal API opinion: the positional/keyword clamp argument should be made keyword-only because positional boolean arguments are bad - see https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument/ for reasoning.

@XFajk
Copy link
Contributor Author

XFajk commented Apr 15, 2025

I tried to satisfy every request I got here but I made one choice that might should be evaluated.
I decided to not check for the number or name of the kwargs passed it just checks if some kwargs where passed in and if yes it check if the first one is truthy. What that means even l.project((10, 10), fdafads=True) would work. When I was writing this I was like I am doing this in the name of performance but after I pushed I realized that we might want to support doing this l.project(point=(10, 10)) and this would not work now so I can still change it or I can change the stub to look like this

def project(self, point: tuple[float, float], / clamp: bool = False) -> tuple[float, float]: ...

aka saying that the first arg is positional only but I want to get this approved I guess

@XFajk XFajk requested review from itzpr3d4t0r and aatle April 15, 2025 19:10
@@ -197,3 +197,6 @@ class Line:
def scale_ip(self, factor_and_origin: Point, /) -> None: ...
def flip_ab(self) -> Line: ...
def flip_ab_ip(self) -> None: ...
def project(
self, point: tuple[float, float], clamp: bool = False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I wasn't clear. Only the return type should be changed to tuple[float, float]; the argument should stay Point.

The reason is that while the input argument should be as broad and abstract of a type as possible, the return type needs to be more concrete and narrow to allow easy usage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now when talking about this, not related to this PR, but maybe we should rename the typing.(Int)Point to typing.(Int)PointLike, just to be the same as the rest of typing module, and a little bit more clear to avoid stuff like that in the future. On the other hand, it is hard to do that now since the typing module is exposed in pygame API, so maybe in pg3

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would normally agree with increasing naming consistency.
However, Point is not necessarily like the other types in that it is not a union of similar, acceptable types. It is also not a raw protocol like SequenceLike.
The main reason of course is verbosity; originally, CoordinateLike would be too long which might be the original reason, but PointLike is still long too.

XFajk and others added 7 commits April 16, 2025 11:45
clarify the docs for the Line.project methdo

Co-authored-by: aatle <168398276+aatle@users.noreply.github.com>
grammar fix in the docs for the Line.project method

Co-authored-by: aatle <168398276+aatle@users.noreply.github.com>
small update to the docs for Line.project

Co-authored-by: aatle <168398276+aatle@users.noreply.github.com>
@itzpr3d4t0r itzpr3d4t0r dismissed their stale review April 24, 2025 14:59

Can't approve / disapprove but what i asked for was addressed in one form or another.

@Starbuck5
Copy link
Member

I may have gone a little overboard, but here's my test script.

"""
https://github.com/pygame-community/pygame-ce/pull/3402 testing
"""

import math

import pygame
import pygame.geometry

pygame.init()

screen_width, screen_height = 720, 500

screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Line.project example")

clock = pygame.Clock()
font = pygame.font.SysFont("Arial", 24)

line = pygame.geometry.Line((screen_width/2-20,250), (screen_width/2+20,250))

instructions = """
Use WASD/arrows to adjust the two points, respectively.
The position of the mouse is projected onto the blue line.
Toggle clamp by pressing C.
"""

instructions_surf = font.render(instructions.strip(), True, "black")

speed = 250
clamp = False

while True:
    dt = clock.tick(144) / 1000

    for event in pygame.event.get():
        if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
            pygame.quit()
            raise SystemExit
        
        if event.type == pygame.KEYDOWN and event.key == pygame.K_c:
            clamp = not clamp

    keys = pygame.key.get_pressed()
    if keys[pygame.K_w]:
        line.ay -= speed * dt
    if keys[pygame.K_a]:
        line.ax -= speed * dt
    if keys[pygame.K_s]:
        line.ay += speed * dt
    if keys[pygame.K_d]:
        line.ax += speed * dt
    if keys[pygame.K_UP]:
        line.by -= speed * dt
    if keys[pygame.K_LEFT]:
        line.bx -= speed * dt
    if keys[pygame.K_DOWN]:
        line.by += speed * dt
    if keys[pygame.K_RIGHT]:
        line.bx += speed * dt

    screen.fill("purple")

    screen.blit(instructions_surf, (10, 10))

    pygame.draw.line(screen, "blue", line.a, line.b, width=3)
    pygame.draw.circle(screen, "blue", line.a, 5)
    pygame.draw.circle(screen, "blue", line.b, 5)

    mouse = pygame.mouse.get_pos()

    proj_point = line.project(mouse, clamp=clamp)
    if not math.isnan(proj_point[0]) and not math.isnan(proj_point[1]):
        pygame.draw.line(screen, "red", mouse, proj_point)

    pygame.display.flip()

@Starbuck5
Copy link
Member

I ran into problems with this function returning NaNs (not a number) in some scenarios, pygame.draw.line freezes indefinitely when it gets one of them, opened an issue for that: #3412. Is there a way this could not return NaN? Like if it's going to do that, return the original coordinate instead? IDK about implementation or whether that makes sense. Another option would be to add a warning in the docs that this can return NaN, because I think people will want to do stuff with this value and having it be NaN will mess those calculations up.

  • Is there prior art from other game engines/libs we could steal? How have others dealt with this?

On another subject, the current implementation of fastcall and keywords has several issues. Yes, the code that should work works, but also code that shouldn't work works.

For example, this line completely works: proj_point = line.project(mouse, damusssss=clamp, arugula="yadafasdf")

I checked, there are 0 instances of a normal function usage of fastcall and keywords in the pygame-ce codebase, so this is not a standard this PR should be held to. Manual kwarg implementations are error prone and will have inconsistent error messaging relative to PyArg_ParseTupleAndKeywords.

I think this PR should either be positional only or should do keywords the normal way.

@itzpr3d4t0r
Copy link
Member

I checked, there are 0 instances of a normal function usage of fastcall and keywords in the pygame-ce codebase, so this is not a standard this PR should be held to.

Surface.get_rect and get_frect are one example.

{"get_rect", (PyCFunction)surf_get_rect, METH_FASTCALL | METH_KEYWORDS,

@Starbuck5
Copy link
Member

Yeah, those aren't normal functions. Normal functions use the kwargs directly, surface.get_rect and rect.move_to (the other example) both just forward the keywords to another function. AKA they don't process them themselves.

@XFajk
Copy link
Contributor Author

XFajk commented Apr 26, 2025

So I can again rewrite it again to not be a fastcall I agree that implementing kwargs parsing for fastcall would be super error prone. I will also look into why it returns NaN and fix it.

  • Is there prior art from other game engines/libs we could steal? How have others dealt with this?

I can look but I dont think so. What problems do you have with the art? I can try to improve it

@Starbuck5
Copy link
Member

Starbuck5 commented Apr 27, 2025

I can look but I dont think so. What problems do you have with the art? I can try to improve it

Just to be clear, I wasn't referring to the graphics you put in the docs, I was referring to the NaN problem. "Prior art" meaning how have others dealt with this.

@XFajk
Copy link
Contributor Author

XFajk commented Apr 29, 2025

Okay, so I want to discuss how we are going to handle the NaN problem. The issue arises when the line has a length of zero, meaning both points A and B are at the same position. Mathematically, there is no way to project the point. If clamping is on, I can technically say the point should be equal to A or B, but if clamping is off, I don't know what it would return. The only solution I see as a possibility is to throw a ZeroDivisionError and mention it in the docs

@damusss
Copy link
Member

damusss commented Apr 29, 2025

If clamping is on, I can technically say the point should be equal to A or B, but if clamping is off, I don't know what it would return. The only solution I see as a possibility is to throw a ZeroDivisionError and mention it in the docs

Yes, if clamping is on you can just return A (not any different from B) and that makes sense. If the clamping is off, we can't lie. We should throw a similar error to what vector2 does when normalizing a zero-length vector which is to throw a ValueError. ZeroDivisionError makes it seem like we have a bug, also because users don't know there's any division, value error seems consistent. and of course both paths have to be mentioned in the docs, yeah.
If we really don't want it to throw an error we could return A even if clamping is off, it does make a little bit of sense, but maybe an error is more helpful. The steering council should have the final say on this but I think they'll go with the error.

@XFajk
Copy link
Contributor Author

XFajk commented Apr 29, 2025

I need help I dont understand why the github test aren't passing

@MightyJosip
Copy link
Member

D:\a\pygame-ce\pygame-ce\src_c\line.c(5): fatal error C1083: Cannot open include file: 'pytypedefs.h': No such file or directory

Do you need those extra imports in lines 4/5. Because I can't see them anywhere else

@XFajk
Copy link
Contributor Author

XFajk commented Apr 29, 2025

D:\a\pygame-ce\pygame-ce\src_c\line.c(5): fatal error C1083: Cannot open include file: 'pytypedefs.h': No such file or directory

Do you need those extra imports in lines 4/5. Because I can't see them anywhere else

yeah I will delete them my editor added them I didnt need them thanks I didnt know that can cause an issue

@XFajk
Copy link
Contributor Author

XFajk commented May 1, 2025

all new requested changes anything else?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
geometry pygame.geometry New API This pull request may need extra debate as it adds a new class or function to pygame
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants