diff --git a/docs/parameterData.json b/docs/parameterData.json index bff585f9e7..cffc10a735 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -3464,6 +3464,9 @@ "CLAMP|REPEAT|MIRROR", "CLAMP|REPEAT|MIRROR?" ], + [ + "Object" + ], [] ] }, diff --git a/src/webgl/material.js b/src/webgl/material.js index d7a375d93d..b2f1a3f772 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -3183,6 +3183,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. @@ -3282,7 +3304,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 @@ -3438,6 +3461,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 @@ -3452,10 +3482,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 be7b112b8c..792835d6ad 100644 --- a/test/unit/core/param_errors.js +++ b/test/unit/core/param_errors.js @@ -198,6 +198,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(); + }); + }); }); });