Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/parameterData.json
Original file line number Diff line number Diff line change
Expand Up @@ -3464,6 +3464,9 @@
"CLAMP|REPEAT|MIRROR",
"CLAMP|REPEAT|MIRROR?"
],
[
"Object"
],
[]
]
},
Expand Down
46 changes: 42 additions & 4 deletions src/webgl/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);

Expand Down
29 changes: 29 additions & 0 deletions test/unit/core/param_errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand Down
65 changes: 65 additions & 0 deletions test/unit/webgl/p5.Texture.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
});
Loading