Skip to content

Conversation

@perminder-17
Copy link
Collaborator

@perminder-17 perminder-17 commented Oct 26, 2025

Resolves #8156

Changes:

  • The no-argument usage of createVector() to be marked for deprecation.
  • FES to suggest not using createVector without arguments
  • Prevent NaN from appearing when a createVector() is subject to any operations.
  • Add unit tests and improve documentation.

Screenshots of the change:

PR Checklist

@perminder-17 perminder-17 linked an issue Oct 26, 2025 that may be closed by this pull request
4 tasks
@perminder-17 perminder-17 marked this pull request as draft October 26, 2025 15:58
@perminder-17 perminder-17 marked this pull request as ready for review October 26, 2025 20:07
const matrix = this._renderer.getWorldToScreenMatrix();

if (screenPosition.dimensions === 2) {
if (origDimension === 2 || screenToWorldArgs === 2) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The logic here is, when we are padding 0's to the other components of vector to prevent NaN, the screenToWorld breaks because it the count here changes and maybe it projects the z components as well.

So, when we see screenPosition.dimensions always comes out to 3 since we are storing other component with 0. Here I am preserving the count with original arguments and makes the test pass. Since it's not the actual fix and tends to be a workaround for the future release, I think this should work, Let me know if there's any objection @davepagurek

Copy link
Contributor

Choose a reason for hiding this comment

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

Let me know if I'm understanding the flow here correctly:

  • If you pass two numbers into screenToWorld (e.g. screenToWorld(width/2, height/2)) or if you pass a single vector in that was created with two numbers (e.g. screenToWorld(createVector(1, 2))), we want to generate a z value
  • We can directly check the arguments length, but when the arg is a single vector, its values get padded to length 3, so we don't know directly how many arguments were manually provided
  • We store a global dimension value corresponding to the number of args passed to the last createVector call

That sounds like it works. I wonder if it might be a little more direct, instead of using global state, to edit createVector to instead set a secret internal property on the vector before returning it, like _origDimension, so then we could check if that exists on any vector?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, you are right. I am using _origDimension now, if screenToWorld(width/2, height/2) we take the argument from there, and if we have not a number (probably a vector) we fetch the _origDimension, other logic are same? Also, now we are not using global state. Do you think it's correct for now?

@GregStanton
Copy link
Collaborator

GregStanton commented Oct 28, 2025

Hi @perminder-17 and @davepagurek!

Thanks so much for working on this. I'm really glad we're trying to figure out how to accommodate the 1.x createVector() usage.

Since I've been organizing the p5.Vector stabilization work (#8149), I wanted to check in on this PR to see how it would interact with the other open issues. I took a quick look at the implementation, and I'm concerned that the current approach—padding all 1D/2D vectors to 3D—is much broader than the "targeted patch" that was proposed and may have major, unintended side effects.

This implementation seems to revert p5.Vector to the 1.x model, which prevents 2.x from having true 1D or 2D vectors. This would unfortunately block or break most of the community's other stabilization work.

Here are the key conflicts I've identified:

  1. Breaks current 2.x behavior: This PR would break valid 2.x features. For example, toString() on a 2D vector currently (and correctly) returns two components. This change would either break the toString() contract or create a confusing state where users don't know whether to trust vec.z or toString(). (See #8153.)

  2. Blocks the broadcasting policy: This change would make the widely-supported broadcasting policy (#8159) impossible to implement. That policy requires us to distinguish 2D and 3D vectors to throw correct errors (e.g., on vec2.add(vec3)). If all vectors are 3D or higher, we lose this ability, which means more complex documentation for features like add(), sub(), mult(), div(), and rem().

  3. Blocks other core fixes: The simple fixes for cross() (#8210), heading() (#8214), and setHeading() (#8215) all depend on being able to distinguish true 2D vectors from 3D vectors. This PR would prevent those fixes.

  4. Violates user expectations: This violates the principle of least astonishment. In 2.x, a user writing createVector(2, 3) correctly expects the vector's components to be [2, 3] (as currently reported by toString()), not [2, 3, 0]. Finding that vec.z is 0 or that vec.toString() reports three components would confuse them.

  5. Propagates 1.x complexity: This padding logic forces other functions like set() to retain confusing side effects from 1.x (e.g., v.set(1, 1) on [1.01, 1.01, 5] becoming [1, 1, 0]). This blocks the simpler, powerful broadcasting solution we've been working on in #8189.

A Real-Time Example of This Complexity

This PR's discussion of the screenToWorld bug is a perfect example of this complexity in action.

To fix the bug this patch created, a new "secret internal" _origDimension property was needed to track the intended state versus the actual (padded) state. This forced a kind of kludge, which creates technical debt: It's brittle and will likely break if vector internals are refactored, since it relies on the vector holding conflicting state (its real shape and its intended shape). This is the exact kind of complexity that the 2.x stabilization work (like a stable, user-facing .shape property in #8155) is designed to solve, but the current PR blocks that work (the shape property is most viable if the vector has a single, unambiguous shape).

Summary

This PR would significantly complicate the entire vector class, for all 2.x users, in order to accommodate a legacy edge case in a single function.

Fundamentally, this patch replaces the simpler, more robust 2.x model with the legacy 1.x model. This breaks or blocks improvements that have already been added or are actively being worked on, based on extensive discussions (including the concept of true 1D and 2D vectors, as discussed in #8118).

Proposal: Move this back to #8156 for alternatives

Since this patch appears to have a much larger blast radius than intended, I propose we continue to discuss viable options in #8156, before we merge. I think we need to find a way to patch the createVector() no-argument case without undermining the entire n-dimensional model.

My sense is that the simplest, lowest-impact solution may be to include a short note in the compatibility README, instructing users how to fix their sketches by supplying explicit 0 arguments to createVector(). But I think the wider community should have a chance to weigh in.

Thanks again for all your work on this!

@davepagurek
Copy link
Contributor

This change would make the widely-supported broadcasting policy (#8159) impossible to implement.

I think we aren't trying to reach a permanent solution in this, but just unblock sketches that are currently broken. So it's very likely that we undo whatever we do in this PR when making larger changes to broadcasting.

That said, do you think it would be reasonable to do a similar kind of padding, but only when operating on another vector for now? So like, temporarily implementing a version of broadcasting where we pad with 0s, but only when you add two vectors. So if you did:

const a = createVector(1, 2) // Dimension is 2
const b = createVector(3, 4, 5) // Dimension is 3
a.add(b) // Maybe this logs an FES message saying you've multiplied two different-sized vectors together
// A's dimension is now 3, its data is [4, 6, 5]

This would make dimension and toString() work correctly at first, and would stop NaNs from appearing in data right now. It isn't the full solution to broadcasting -- essentially it's using one form of broadcasting as a stopgap until we work out a full solution. But I think that takes some of what's currently broken and leaves it in a less broken state without introducing new breakages?

@GregStanton
Copy link
Collaborator

GregStanton commented Oct 29, 2025

Hi @davepagurek! Thank you so much for your quick reply. I love seeing all these ideas being put forth, and your new idea is really interesting.

We're totally in agreement about wanting to fix the current user-facing bugs as quickly as possible. With that in mind, I have a proposal for a different quick fix, a few concerns about the "stopgap" approach, and what I see as a path to immediately unlock quick, permanent fixes.

A truly quick fix

If our goal is to unblock users today with the lowest possible effort and zero technical debt, I think the fastest path is to add a note to the compatibility README and the 2.0 beta documentation. Something like this:

"In 1.x, createVector() was often used as a shortcut for createVector(0, 0, 0). In 2.x, createVector() with no arguments is no longer supported, as we now have true 1D/2D vectors. If your 1.x sketch breaks when upgrading to 2.x, you can fix it by explicitly using createVector(0), createVector(0, 0), or createVector(0, 0, 0), depending on the desired dimension."

This is a 10-minute, no-code change that immediately solves the user's "why is my sketch broken?" problem. Crucially, we can reinforce this with a dedicated Friendly Error System message. That way, the README and reference pages provide advance notice, and users who are still bitten by the bug will find an immediate, simple solution when and where it happens.

Risks of a custom "stopgap"

My main concern with the stopgap you proposed (padding with 0s for add()) is that it's not a partial form of standard broadcasting—it's a new, custom rule that explicitly violates the universally adopted broadcasting rule.

This rule is precisely the same in every major library for math, machine learning, and when applicable, creative-coding. For example, openFrameworks also follows the same rule.

Violating the rule means we'd have to code in warning messages that teach users special behavior that they'll have to unlearn—the permanent solution explicitly disallows this exact behavior. This feels like the "whack-a-mole" problem: we'd be solving the NaN bug by introducing a new source of user confusion.

An opportunity for quick, permanent fixes

This brings me to what I think is the most exciting and high-leverage opportunity.

The permanent solutions for this bug (and many others) are already clearly defined, the key policies like broadcasting have strong community support, and the issues have volunteers ready and waiting to implement them. The only thing preventing us from fixing these bugs quickly and permanently is that we're blocked on finalizing a few key policy decisions.

I believe the most effective use of our time is to unblock the community. If the maintainer team can reach a final decision on these three key issues, it would unleash all of that volunteer energy:

  • Broadcasting policy (#8159): Fortunately, this isn't a long-term project; implementing the full policy is a small, simple fix, based around just two features. The mult(), div(), and rem() methods already broadcast scalars. The only methods requiring an immediate update are add() and sub(); I've confirmed that the logic for those can be adapted directly from the simple loop already in mult() (changing *= to += or -=). Beyond that, the only other change is throwing Friendly Error Messages in unsupported cases. My review of 100 sketches shows zero disruption from these changes. In this case, the permanent solution is also the quick fix. Approving it is an easy win.

  • The x/y/z behavior (#8153): This is a simple bug fix that aligns x, y, and z with the correct behavior of toString(). The exact changes that need to be made have been identified, and involve removing a few characters of code. It's also backed by previous discussion of true 1D/2D vector support.

  • The dimensions naming (#8155): We've already refined the API through several rounds of feedback, identified the core problems, and proposed a standard, precedent-backed solution. The final step is just a quick cross-check against existing getter/setter patterns across the library (which I can complete by tomorrow) to ensure long-term consistency.

Once these policies are approved, the community can immediately start implementing permanent fixes for these NaN bugs, and many other issues will be unblocked (e.g. #8188, #8189, #8210, #8215, and #8218).

What do you think of this approach? It seems like a way to get the best of both worlds: a zero-debt immediate fix (the README) and a clear plan to rapidly unlock the permanent fixes.

@ksen0
Copy link
Member

ksen0 commented Oct 29, 2025

Hi @GregStanton thank you for your thoughtful review! After going through this with @davepagurek , here are my takeaways and recommendation for this PR:

  1. This PR should be updated to be more focused on only the zero-argument usage, in a way that does not introduce new partial broadcasting policy. See also this comment describing @davepagurek 's proposal of "a zero vector that still has a fixed size, like other vectors, but with the specification of that size deferred until it can be inferred." In other words, this solution would only affect zero-arg vectors, and not by affecting broadcasting, but rather by allowing the existence of a vector of unspecified size until first usage. This PR should include the documentation note that this usage is not recommended; as well as the unit tests and FES note on zero-arg use. @perminder-17 does that all sound reasonable? Thanks so much for working on this!

  2. This implementation (as was noted) has drawbacks, but on discussion and reflection of the various linked issues, I think it is sensible for two related reasons. First, it is in the spirit of p5.js to support as little friction as possible in common uses; I think there is enough usage of createVector() (including in Nature of Code) that it is worthwhile to reduce this friction. Second, as all other Vector proposals suggest (or don't contradict), zero-arg usage of createVector() should be marked for deprecation and not supported at all in the future. However, given that it is currently supported, and there was not wide consensus to deprecate it, the proposed patch for minimum backwards compatibility seems justifiable. If there are severe drawbacks I am not seeing, I am very happy to reconsider this, but I think as long as there are unit tests (to reduce developer confusion) and FES messages reminding not to use createVector() (to reduce user confusion) - it is alright.

  3. Although [2.0] Proposal: A standard policy for vector dimension mismatches #8159 is leaning toward no partial broadcasting (and it's a great discussion that I'm really glad is happening!) there is not yet clear and wide consensus (besides that issue, there are other places where there was more support for partial broadcasting). Also, as far as I understand, the standard broadcasting solution to this situation is to disallow zero-arg use (and update docs accordingly) - I agree overall with future deprecation, however, it has not been deprecated (or at minimum, there has not been - as far as I know - meaningful deliberation and consensus in the past to fully deprecate zero-arg usage). Again, if I'm missing something please let me know, I tried to catch up as best as I could.

While considering this I also referred to Dan Shiffman's comment and from what I could tell, although the code examples in that comment won't work with this patch, the teaching approach does use constructors with explicit arguments, so this supports keeping the scope of this fix separate from broadcasting fix.

Thanks all for your careful work on this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[2.0] Stabilize behavior of createVector() with zero arguments

4 participants