From b915ad1b23c5d70506f8dc71db35735a4477bbc2 Mon Sep 17 00:00:00 2001 From: harshiltewari2004 Date: Thu, 15 Oct 2026 01:09:34 +0530 Subject: [PATCH] Fix textureWrap() dropping the y mode when passed a modes object The object branch reassigned wrapX before reading wrapX.y, so the y mode became undefined and silently fell back to CLAMP. Read both values before either assignment. Also guard against null, warn via the FES for unrecognised wrap modes, correct the getter docs to {x, y}, and add an Object overload so the param validator stops rejecting the documented round-trip form. --- docs/parameterData.json | 3 ++ src/webgl/material.js | 46 +++++++++++++++++++++--- test/unit/core/param_errors.js | 29 +++++++++++++++ test/unit/webgl/p5.Texture.js | 65 ++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 4 deletions(-) diff --git a/docs/parameterData.json b/docs/parameterData.json index 450f251d50..e2cd4f929c 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -3458,6 +3458,9 @@ "CLAMP|REPEAT|MIRROR", "CLAMP|REPEAT|MIRROR?" ], + [ + "Object" + ], [] ] }, diff --git a/src/webgl/material.js b/src/webgl/material.js index 594ddecfb3..ec639dc568 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -3186,6 +3186,28 @@ function material(p5, fn) { } }; + const VALID_WRAP_MODES = [ + constants.CLAMP, + constants.REPEAT, + constants.MIRROR + ]; + + // Warns once per sketch if a wrap mode isn't one of the three constants. + // Non-blocking: the value is still stored, and an unrecognised mode + // continues to fall back to CLAMP in the renderer. + function warnInvalidWrapMode(pInst, value, paramName) { + if (p5.disableFriendlyErrors) return; + if (VALID_WRAP_MODES.includes(value)) return; + if (pInst._textureWrapWarned) return; + + pInst._textureWrapWarned = true; + p5._friendlyError( + `textureWrap() expects CLAMP, REPEAT, or MIRROR, but received ` + + `${value} for ${paramName}. The texture will fall back to CLAMP.`, + 'textureWrap' + ); + } + /** * Changes the way textures behave when a shape’s uv coordinates go beyond the * texture. @@ -3285,7 +3307,8 @@ function material(p5, fn) { * Note: `textureWrap()` can only be used in WebGL mode. * * Calling `textureWrap()` with no arguments returns an object with the current - * mode for x and y directions, as in `{ wrapX: CLAMP, wrapY: REPEAT }`. + * mode for x and y directions, as in `{ x: CLAMP, y: REPEAT }`. That object can + * be passed straight back into `textureWrap()` to restore both modes. * * @method textureWrap * @param {(CLAMP|REPEAT|MIRROR)} wrapX either CLAMP, REPEAT, or MIRROR @@ -3441,6 +3464,13 @@ function material(p5, fn) { * vertex(-30, 30, 0, 0, 2); * endShape(); * } + */ + /** + * @method textureWrap + * @param {Object} modes an object with `x` and `y` properties holding wrap + * modes, as returned by calling `textureWrap()` with + * no arguments + * @chainable */ /** * @method textureWrap @@ -3455,10 +3485,18 @@ function material(p5, fn) { }; } // accept what is returned from the getter - if (wrapX.hasOwnProperty('x') && wrapX.hasOwnProperty('y')) { - wrapX = wrapX.x; - wrapY = wrapX.y; + if ( + wrapX !== null && + typeof wrapX === 'object' && + Object.hasOwn(wrapX, 'x') && + Object.hasOwn(wrapX, 'y') + ) { + ({ x: wrapX, y: wrapY } = wrapX); } + + warnInvalidWrapMode(this, wrapX, 'wrapX'); + warnInvalidWrapMode(this, wrapY, 'wrapY'); + this._renderer.states.setValue('textureWrapX', wrapX); this._renderer.states.setValue('textureWrapY', wrapY); diff --git a/test/unit/core/param_errors.js b/test/unit/core/param_errors.js index a59c2d3c2b..32a15be3ba 100644 --- a/test/unit/core/param_errors.js +++ b/test/unit/core/param_errors.js @@ -188,6 +188,35 @@ suite('Validate Params', function () { }); }); + suite('validateParams: modes object for textureWrap()', function () { + test('textureWrap(): two constants', () => { + const result = mockP5Prototype._validate('p5.textureWrap', [ + constants.REPEAT, + constants.MIRROR + ]); + assert.isTrue(result.success); + }); + + test('textureWrap(): modes object from the getter', () => { + const result = mockP5Prototype._validate('p5.textureWrap', [ + { x: constants.REPEAT, y: constants.MIRROR } + ]); + assert.isTrue(result.success); + }); + + test('textureWrap(): no args', () => { + const result = mockP5Prototype._validate('p5.textureWrap', []); + assert.isTrue(result.success); + }); + + test('textureWrap(): invalid constant', () => { + const result = mockP5Prototype._validate('p5.textureWrap', [ + 'fake-constant' + ]); + assert.isFalse(result.success); + }); + }); + suite('validateParams: promise where no promise is expected', function () { test('image(): promise for first argument', function () { const result = mockP5Prototype._validate('p5.image', [ diff --git a/test/unit/webgl/p5.Texture.js b/test/unit/webgl/p5.Texture.js index a3c53da6fc..02c69cafb5 100644 --- a/test/unit/webgl/p5.Texture.js +++ b/test/unit/webgl/p5.Texture.js @@ -317,5 +317,70 @@ suite('p5.Texture', function () { expect(tex.width).to.equal(canvas.width); expect(tex.height).to.equal(canvas.height); }); + + suite('textureWrap() object setter (#9164)', function () { + test('getter returns the current modes as {x, y}', function () { + myp5.textureWrap(myp5.REPEAT, myp5.MIRROR); + assert.deepEqual(myp5.textureWrap(), { + x: myp5.REPEAT, + y: myp5.MIRROR + }); + }); + + test('passing the getter result back preserves both modes', function () { + myp5.textureWrap(myp5.REPEAT, myp5.MIRROR); + myp5.textureWrap(myp5.textureWrap()); + assert.deepEqual(myp5.textureWrap(), { + x: myp5.REPEAT, + y: myp5.MIRROR + }); + }); + + test('a supplied object sets the two directions independently', function () { + myp5.textureWrap({ x: myp5.REPEAT, y: myp5.MIRROR }); + assert.equal(myp5._renderer.states.textureWrapX, myp5.REPEAT); + assert.equal(myp5._renderer.states.textureWrapY, myp5.MIRROR); + }); + + test('accepts an object with no prototype', function () { + // The FES param validator throws on a null-prototype object because + // buildTypeMismatchMessage() calls toString() on the argument. Disable + // it here so this test exercises textureWrap() rather than the FES. + const prev = p5.disableFriendlyErrors; + p5.disableFriendlyErrors = true; + try { + const modes = Object.create(null); + modes.x = myp5.REPEAT; + modes.y = myp5.MIRROR; + myp5.textureWrap(modes); + assert.equal(myp5._renderer.states.textureWrapY, myp5.MIRROR); + } finally { + p5.disableFriendlyErrors = prev; + } + }); + + test('textureWrap(null) does not throw', function () { + assert.doesNotThrow(() => myp5.textureWrap(null)); + }); + + test('the y mode reaches WebGL after a round trip', function () { + myp5.textureWrap(myp5.REPEAT, myp5.MIRROR); + myp5.textureWrap(myp5.textureWrap()); + myp5._renderer.getTexture(texImg1); + expect(texParamSpy).toHaveBeenCalledWith( + myp5._renderer.GL.TEXTURE_2D, + myp5._renderer.GL.TEXTURE_WRAP_T, + myp5._renderer.GL.MIRRORED_REPEAT + ); + }); + + test('warns once for an unrecognised wrap mode', function () { + const fesSpy = vi.spyOn(p5, '_friendlyError'); + myp5.textureWrap('not-a-mode'); + myp5.textureWrap('also-not-a-mode'); + expect(fesSpy).toHaveBeenCalledTimes(1); + fesSpy.mockRestore(); + }); + }); }); });