Skip to content

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

Summary by CodeRabbit

  • New Features

    • Added Line.project(point, clamp=False), which returns the closest point on a line to a given point. When clamp is True, the result is restricted to the line segment. Handles zero-length lines safely.
  • Documentation

    • Added API documentation for Line.project with usage details, figures, version note, and a warning about zero-length lines.
  • Tests

    • Added unit tests covering projections, clamped behavior, and degenerate-line scenarios.

@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
Member

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 aatle and itzpr3d4t0r April 15, 2025 19:10
def flip_ab(self) -> Line: ...
def flip_ab_ip(self) -> None: ...
def project(
self, point: tuple[float, float], clamp: bool = False
Copy link
Member

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
Member

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>
@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?

Copy link
Member

@ankith26 ankith26 left a comment

Choose a reason for hiding this comment

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

Left a few more code quality related reviews, but assuming those are solved this PR is looking good to me!


WARNING: This method has to have some length or the clamp parameter must be true for it to work and not throw a `ValueError`

.. versionadded:: 2.5.4
Copy link
Member

Choose a reason for hiding this comment

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

this would need to be updated to 2.5.6 after we have consensus on this PR

XFajk and others added 6 commits June 20, 2025 09:00
Co-authored-by: Ankith <itsankith26@gmail.com>
Co-authored-by: Ankith <itsankith26@gmail.com>
Co-authored-by: Ankith <itsankith26@gmail.com>
Co-authored-by: Ankith <itsankith26@gmail.com>
@ankith26 ankith26 added this to the 2.5.6 milestone Jun 20, 2025
Copy link
Member

@ankith26 ankith26 left a comment

Choose a reason for hiding this comment

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

Left a point up for API discussion but apart from that this PR LGTM. I played around with the script from starbuck and got no funny nan-behaviour.

Thanks for contributing to pygame-ce and putting up with all our reviews! 🎉 😄

Removes the error for projecting to a zero length line with no clamping. Instead it behaves just like a clamped line.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 25, 2025

Walkthrough

Adds a new Line.project method to pygame geometry: type stubs, C implementation and docstrings, reST docs with figures and versionadded, and unit tests. Updates method table and documentation header macro. Tests include projections with and without clamping and a degenerate line case.

Changes

Cohort / File(s) Summary
C implementation: Line.project
src_c/line.c
Adds _line_project_helper and pg_line_project to compute and expose point projection onto a line; updates pg_line_methods[] with "project" using DOC_LINE_PROJECT; parses args, supports clamp, handles zero-length lines.
Python stubs
buildconfig/stubs/pygame/geometry.pyi
Adds Line.project(self, point: Point, clamp: bool = False) -> tuple[float, float].
Docs (C doc macro)
src_c/doc/geometry_doc.h
Adds DOC_LINE_PROJECT macro documenting signature and description.
Docs (reST reference)
docs/reST/ref/geometry.rst
Documents Line.project(point: tuple[float, float], clamp=False) -> tuple[float, float], adds figures, versionadded 2.5.6, warning for zero-length lines; entry inserted (appears twice).
Tests
test/geometry_test.py
Adds test_meth_project covering unclamped/clamped projections and degenerate line; note: identical test block appears twice.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as User code
  participant L as Line (Python)
  participant C as pg_line_project (C)
  participant H as _line_project_helper (C)

  User->>L: project(point, clamp=False)
  L->>C: parse args (point, clamp)
  alt invalid args
    C-->>User: raise TypeError/ValueError
  else valid args
    C->>H: compute projection(point, clamp)
    alt line length == 0
      Note over H: Degenerate line<br/>return start point
    else clamp == True
      Note over H: Clamp to segment endpoints if outside
    else clamp == False
      Note over H: Return orthogonal projection on infinite line
    end
    H-->>C: (x, y)
    C-->>User: (x, y)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I traced a line across the sky,
Dropped a point and watched it lie—
A tidy hop, a clampy stop,
From whiskered math to perfect spot.
Degenerate? I’ll simply rest—
At zero length, I nest my best. 🐇📐

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Member

@MyreMylar MyreMylar left a comment

Choose a reason for hiding this comment

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

OK, LGTM 👍

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src_c/doc/geometry_doc.h (1)

48-49: Docstring is incorrect: it says “projects the line onto the given line” (should be point onto line).

This string becomes the Python help() text and is misleading. It should describe projecting a point onto the line, and the parameter type should match our public docs/stubs.

Apply this diff:

-#define DOC_LINE_PROJECT "project(point: tuple[float, float], clamp=False) -> tuple[float, float]\nprojects the line onto the given line"
+#define DOC_LINE_PROJECT "project(point: Point, clamp=False) -> tuple[float, float]\nprojects a point onto the line"
♻️ Duplicate comments (2)
docs/reST/ref/geometry.rst (1)

744-752: Fix method one-liner and signature to reflect “point onto line” and the agreed types.

  • The short description currently says “projects the line onto the given line” — that’s wrong.
  • The signature uses point: tuple[float, float]; to align with stubs and prior review, use Point.
  • The paragraph says “boolean keyword argument clamp” but the implementation accepts clamp positionally too. Either enforce keyword-only in C or drop “keyword” here to avoid implying keyword-only.

Proposed edits:

    .. method:: project
-        | :sl:`projects the line onto the given line`
-        | :sg:`project(point: tuple[float, float], clamp=False) -> tuple[float, float]`
+        | :sl:`projects a point onto the line`
+        | :sg:`project(point: Point, clamp=False) -> tuple[float, float]`

-        This method takes in a point and one boolean keyword argument clamp. It outputs an orthogonally projected point onto the line.
+        This method takes in a point and an optional boolean argument ``clamp``. It outputs the orthogonal projection of the point onto the line.
src_c/line.c (1)

251-262: Clamp logic ordering is correct; avoids the squared-comparison pitfall flagged earlier.

Checking dot_product < 0 first ensures we clamp to a for behind-start projections; only then compare the squared magnitudes to clamp to b. This fixes the earlier “lost sign” issue after squaring. LGTM.

🧹 Nitpick comments (5)
buildconfig/stubs/pygame/geometry.pyi (1)

200-200: Keep argument type as Point (broad) but align public docs to match; consider clarifying keyword-only intent.

The stub correctly uses point: Point and returns tuple[float, float]. However, the reST/docs macro still show point: tuple[float, float], which is inconsistent and was previously requested to be Point. Either:

  • Update the docs to Point (preferred), or
  • Change the stub to tuple[float, float] if you explicitly want to narrow input (unlikely).

Also, docs describe “boolean keyword argument clamp,” but the C binding currently accepts it positionally too. Decide whether you want clamp to be keyword-only; if yes, enforce it in C and then mark the stub as def project(self, point: Point, /, *, clamp: bool = False) -> tuple[float, float]. Otherwise, keep the current stub and adjust doc wording to just “argument clamp.”

docs/reST/ref/geometry.rst (1)

760-768: Minor clarity/consistency nits in examples and warning.

The examples read well. To be fully consistent with the rest of the page:

  • Keep “closest point” phrasing once in the body and avoid repeating it.
  • The WARNING is good; consider explicitly stating it applies both when clamped and unclamped to close the loop with the implementation decision.

Optional tweak:

-            WARNING:  If the line has no length (i.e. the start and end points are the same) then the returned point of this function will be the same point as both ends of the line.
+            WARNING: If the line has no length (i.e. start and end points are the same), this function returns that point (both when ``clamp=False`` and ``clamp=True``).
test/geometry_test.py (1)

2234-2236: Assert both coordinates for the degenerate, unclamped case (and add an unclamped-outside case).

Right now only x is asserted for the zero-length, unclamped path. Assert y too to fully validate the behavior, and add one assertion covering the “outside-the-segment, unclamped” path.

         projected_point = bad_line.project(test_bad_line_point)
         self.assertEqual(math.ceil(projected_point[0]), 0)
+        self.assertEqual(math.ceil(projected_point[1]), 0)

+        # unclamped projection beyond the segment should lie on the infinite extension
+        projected_point = line.project(test_clamp_point1)  # clamp=False by default
+        self.assertEqual(math.ceil(projected_point[0]), 200)
+        self.assertEqual(math.ceil(projected_point[1]), 200)
src_c/line.c (2)

227-234: Handle near-degenerate lines using an epsilon threshold.

Comparing to 0.0 only catches exactly equal endpoints. If users tweak coordinates incrementally, length may underflow but not hit 0 exactly. Consider an epsilon to treat near-zero lengths as degenerate.

Apply this diff (and include <float.h> once at the top of the file):

-    if (squared_line_length == 0.0) {
+    if (squared_line_length <= DBL_EPSILON) {

Additional include needed (outside this hunk):

#include <float.h>

308-309: Consider METH_FASTCALL | METH_KEYWORDS for consistency and perf with the rest of geometry.

Most geometry methods use FASTCALL. While project is cheap, switching to METH_FASTCALL | METH_KEYWORDS would align with the module and avoid a one-off. Not a blocker; do it only if you have a convenient keyword parsing helper.

If you choose to change it later, remember to update the signature accordingly and adapt parsing to vectorcall semantics.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 74043a6 and eb078bc.

⛔ Files ignored due to path filters (2)
  • docs/reST/ref/code_examples/project.png is excluded by !**/*.png
  • docs/reST/ref/code_examples/project_clamp.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • buildconfig/stubs/pygame/geometry.pyi (1 hunks)
  • docs/reST/ref/geometry.rst (1 hunks)
  • src_c/doc/geometry_doc.h (1 hunks)
  • src_c/line.c (2 hunks)
  • test/geometry_test.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
buildconfig/stubs/pygame/geometry.pyi (1)
buildconfig/stubs/pygame/math.pyi (2)
  • project (111-111)
  • clamp (18-18)
src_c/line.c (1)
src_c/base.c (1)
  • pg_TwoDoublesFromObj (733-782)
test/geometry_test.py (1)
buildconfig/stubs/pygame/geometry.pyi (3)
  • line (21-21)
  • Line (141-200)
  • project (200-200)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: x86
  • GitHub Check: AMD64
  • GitHub Check: dev-check
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: build (macos-14)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: aarch64
  • GitHub Check: x86_64
  • GitHub Check: i686
🔇 Additional comments (2)
src_c/line.c (2)

230-236: Degenerate line handling: correct and matches consensus (return A for zero length).

Returning line->a when squared_line_length == 0.0 avoids NaNs and matches the documented behavior. Good.


271-294: Argument parsing: good validation; consider aligning error text with other geometry methods.

Parsing via PyArg_ParseTupleAndKeywords is clear; the error messages are fine. For consistency with the rest of the module, you may want to reuse the phrasing used in similar helpers (“expected a sequence of two numbers” is already consistent for point). No action required.

@MyreMylar MyreMylar merged commit 0c171e5 into pygame-community:main Aug 25, 2025
28 checks passed
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.

Method Proposal: pygame.geometry.Line.closest_point(point, do_clamp=True)

8 participants