Skip to content

Commit 007bd76

Browse files
authored
Merge branch 'dev-2.0' into fix/webgpu-crash-pixel-density
2 parents c8206c2 + 7d6153b commit 007bd76

10 files changed

Lines changed: 186 additions & 22 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"test/**/*.js": "eslint",
2121
"utils/**/*.{js,mjs}": "eslint"
2222
},
23-
"version": "2.2.1-rc.0",
23+
"version": "2.2.1",
2424
"dependencies": {
2525
"@davepagurek/bezier-path": "^0.0.7",
2626
"@japont/unicode-range": "^1.0.0",

src/core/friendly_errors/sketch_verifier.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export const verifierUtils = {
8080

8181
try {
8282
const ast = parse(code, {
83-
ecmaVersion: 2021,
83+
ecmaVersion: 'latest',
8484
sourceType: 'module',
8585
locations: true // This helps us get the line number.
8686
});

src/strands/strands_api.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,12 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName
485485
}
486486
if (receivedType.dimension !== expectedType.dimension) {
487487
if (receivedType.dimension !== 1) {
488-
FES.userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookName} when a ${expectedType.baseType + expectedType.dimension} was expected!`);
488+
const receivedTypeDisplay = receivedType.baseType + (receivedType.dimension > 1 ? receivedType.dimension : '');
489+
const expectedTypeDisplay = expectedType.baseType + expectedType.dimension;
490+
FES.userError('type error',
491+
`You have returned a ${receivedTypeDisplay} in ${hookName} when a ${expectedTypeDisplay} was expected!\n\n` +
492+
`Make sure your hook returns the correct type.`
493+
);
489494
}
490495
else {
491496
const result = build.primitiveConstructorNode(strandsContext, expectedType, returned);
@@ -576,7 +581,24 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
576581
if (retNode instanceof StrandsNode) {
577582
const returnedNode = getNodeDataFromID(strandsContext.dag, retNode.id);
578583
if (returnedNode.baseType !== expectedStructType.typeName) {
579-
FES.userError("type error", `You have returned a ${retNode.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`);
584+
const receivedTypeName = returnedNode.baseType || 'undefined';
585+
const receivedDim = dag.dimensions[retNode.id];
586+
const receivedTypeDisplay = receivedDim > 1 ?
587+
`${receivedTypeName}${receivedDim}` : receivedTypeName;
588+
589+
const expectedProps = expectedStructType.properties
590+
.map(p => p.name).join(', ');
591+
FES.userError('type error',
592+
`You have returned a ${receivedTypeDisplay} from ${hookType.name} when a ${expectedStructType.typeName} was expected.\n\n` +
593+
`The ${expectedStructType.typeName} struct has these properties: { ${expectedProps} }\n\n` +
594+
`Instead of returning a different type, you should modify and return the ${expectedStructType.typeName} struct that was passed to your hook.\n\n` +
595+
`For example:\n` +
596+
`${hookType.name}((inputs) => {\n` +
597+
` // Modify properties of inputs\n` +
598+
` inputs.someProperty = ...;\n` +
599+
` return inputs; // Return the modified struct\n` +
600+
`})`
601+
);
580602
}
581603
const newDeps = returnedNode.dependsOn.slice();
582604
for (let i = 0; i < expectedStructType.properties.length; i++) {
@@ -595,10 +617,14 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
595617
const propName = expectedProp.name;
596618
const receivedValue = retNode[propName];
597619
if (receivedValue === undefined) {
598-
FES.userError('type error', `You've returned an incomplete struct from ${hookType.name}.\n` +
599-
`Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` +
600-
`Received: { ${Object.keys(retNode).join(', ')} }\n` +
601-
`All of the properties are required!`);
620+
const expectedProps = expectedReturnType.properties.map(p => p.name).join(', ');
621+
const receivedProps = Object.keys(retNode).join(', ');
622+
FES.userError('type error',
623+
`You've returned an incomplete ${expectedStructType.typeName} struct from ${hookType.name}.\n\n` +
624+
`Expected properties: { ${expectedProps} }\n` +
625+
`Received properties: { ${receivedProps} }\n\n` +
626+
`All properties are required! Make sure to include all properties in the returned struct.`
627+
);
602628
}
603629
const expectedTypeInfo = expectedProp.dataType;
604630
const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name);

src/webgl/loading.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -593,12 +593,10 @@ function loading(p5, fn){
593593
const vertString = tokens[vertexTokens[tokenInd]];
594594
let vertParts = vertString.split('/');
595595

596-
// TODO: Faces can technically use negative numbers to refer to the
597-
// previous nth vertex. I haven't seen this used in practice, but
598-
// it might be good to implement this in the future.
599-
600596
for (let i = 0; i < vertParts.length; i++) {
601-
vertParts[i] = parseInt(vertParts[i]) - 1;
597+
let index = parseInt(vertParts[i]);
598+
if (index > 0) index -= 1; // OBJ uses 1-based indexing
599+
vertParts[i] = index;
602600
}
603601

604602
if (!usedVerts[vertString]) {
@@ -607,11 +605,11 @@ function loading(p5, fn){
607605

608606
if (usedVerts[vertString][currentMaterial] === undefined) {
609607
const vertIndex = model.vertices.length;
610-
model.vertices.push(loadedVerts.v[vertParts[0]].copy());
611-
model.uvs.push(loadedVerts.vt[vertParts[1]] ?
612-
loadedVerts.vt[vertParts[1]].slice() : [0, 0]);
613-
model.vertexNormals.push(loadedVerts.vn[vertParts[2]] ?
614-
loadedVerts.vn[vertParts[2]].copy() : new Vector());
608+
model.vertices.push(loadedVerts.v.at(vertParts[0]).copy());
609+
model.uvs.push(loadedVerts.vt.at(vertParts[1]) ?
610+
loadedVerts.vt.at(vertParts[1]).slice() : [0, 0]);
611+
model.vertexNormals.push(loadedVerts.vn.at(vertParts[2]) ?
612+
loadedVerts.vn.at(vertParts[2]).copy() : new Vector());
615613

616614
usedVerts[vertString][currentMaterial] = vertIndex;
617615
face.push(vertIndex);

src/webgl/material.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,40 @@ function material(p5, fn) {
684684
* }
685685
* ```
686686
*
687+
* We can use the `noise()` function built into strands to generate a color for each pixel. (Again no need here for underlying content for the filter to operate on.) Again we'll animate by passing in an announced uniform variable `time` with `setUniform()`, each frame.
688+
*
689+
* ```js example
690+
* let myFilter;
691+
*
692+
* function setup() {
693+
* createCanvas(100, 100, WEBGL);
694+
* myFilter = buildFilterShader(noiseShaderCallback);
695+
* describe('Evolving animated cloud-like noise in cyan and magenta');
696+
* }
697+
*
698+
* function noiseShaderCallback() {
699+
* let time = uniformFloat();
700+
* filterColor.begin();
701+
* let coord = filterColor.texCoord;
702+
*
703+
* //generate a value roughly between 0 and 1
704+
* let noiseVal = noise(coord.x, coord.y, time / 2000);
705+
*
706+
* let result = mix(
707+
* [1, 0, 1, 1], // Magenta
708+
* [0, 1, 1, 1], // Cyan
709+
* noiseVal
710+
* );
711+
* filterColor.set(result);
712+
* filterColor.end();
713+
* }
714+
*
715+
* function draw() {
716+
* myFilter.setUniform("time", millis());
717+
* filter(myFilter);
718+
* }
719+
* ```
720+
*
687721
* Like the `modify()` method on shaders,
688722
* advanced users can also fill in `filterColor` using <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>
689723
* instead of JavaScript.
@@ -1633,7 +1667,7 @@ function material(p5, fn) {
16331667
/**
16341668
* Returns the base shader used for filters.
16351669
*
1636-
* Calling <a href="#/p5/buildMaterialShader">`buildFilterShader(shaderFunction)`</a>
1670+
* Calling <a href="#/p5/buildFilterShader">`buildFilterShader(shaderFunction)`</a>
16371671
* is equivalent to calling `baseFilterShader().modify(shaderFunction)`.
16381672
*
16391673
* Read <a href="#/p5/buildFilterShader">the `buildFilterShader` reference</a> or

src/webgl/text.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ function text(p5, fn) {
685685

686686
if (!p5.Font.hasGlyphData(this.states.textFont)) {
687687
console.log(
688-
'WEBGL: only Opentype (.otf) and Truetype (.ttf) fonts with glyph data are supported'
688+
'WEBGL: only Opentype (.otf) and Truetype (.ttf) fonts with glyph data are supported. Make sure to set the font using textFont() before drawing text.'
689689
);
690690
return;
691691
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Cube using negative vertex indices
2+
# Negative indices count backwards: -1 = last vertex, -2 = second-to-last, etc.
3+
4+
# Vertices
5+
v 0.0 0.0 0.0
6+
v 1.0 0.0 0.0
7+
v 1.0 1.0 0.0
8+
v 0.0 1.0 0.0
9+
v 0.0 0.0 1.0
10+
v 1.0 0.0 1.0
11+
v 1.0 1.0 1.0
12+
v 0.0 1.0 1.0
13+
14+
# Faces using negative indices
15+
f -8 -7 -6 -5
16+
f -4 -3 -2 -1
17+
f -8 -4 -1 -5
18+
f -7 -3 -2 -6
19+
f -5 -6 -2 -1
20+
f -8 -7 -3 -4

test/unit/io/loadModel.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ suite('loadModel', function() {
1010
const inconsistentColorObjFile = '/test/unit/assets/eg1.obj';
1111
const objMtlMissing = '/test/unit/assets/objMtlMissing.obj';
1212
const validSTLfileWithoutExtension = '/test/unit/assets/ascii';
13+
const validCubeFile = '/test/unit/assets/cube.obj';
14+
const negativeIndexCubeFile = '/test/unit/assets/cube-negative-indices.obj';
1315

1416
beforeAll(async () => {
1517
loading(mockP5, mockP5Prototype);
@@ -115,4 +117,20 @@ suite('loadModel', function() {
115117
const model = await mockP5Prototype.loadModel(validSTLfileWithoutExtension, '.STL');
116118
assert.instanceOf(model, Geometry);
117119
});
120+
121+
test('OBJ with negative vertex indices loads correctly', async function() {
122+
const model = await mockP5Prototype.loadModel(negativeIndexCubeFile);
123+
assert.instanceOf(model, Geometry);
124+
assert.isAbove(model.vertices.length, 0, 'Model should have vertices');
125+
assert.isAbove(model.faces.length, 0, 'Model should have faces');
126+
});
127+
128+
test('OBJ negative indices produce same geometry as positive', async function() {
129+
const positiveModel = await mockP5Prototype.loadModel(validCubeFile);
130+
const negativeModel = await mockP5Prototype.loadModel(negativeIndexCubeFile);
131+
assert.equal(positiveModel.vertices.length, negativeModel.vertices.length,
132+
'Vertex count should match');
133+
assert.equal(positiveModel.faces.length, negativeModel.faces.length,
134+
'Face count should match');
135+
});
118136
});

test/unit/webgl/p5.Shader.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
import p5 from '../../../src/app.js';
2+
import { vi } from 'vitest';
3+
4+
const mockUserError = vi.fn();
5+
vi.mock('../../../src/strands/strands_FES', () => ({
6+
userError: (...args) => {
7+
mockUserError(...args);
8+
const prefixedMessage = `[p5.strands ${args[0]}]: ${args[1]}`;
9+
throw new Error(prefixedMessage);
10+
},
11+
internalError: (msg) => { throw new Error(`[p5.strands internal error]: ${msg}`); }
12+
}));
13+
214
suite('p5.Shader', function() {
315
var myp5;
416
beforeAll(function() {
@@ -1942,4 +1954,60 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca
19421954
assert.approximately(pixelColor[2], 0, 5);
19431955
});
19441956
});
1957+
1958+
suite('p5.strands error messages', () => {
1959+
afterEach(() => {
1960+
mockUserError.mockClear();
1961+
});
1962+
1963+
test('wrong type in struct hook shows actual type and expected properties', () => {
1964+
myp5.createCanvas(50, 50, myp5.WEBGL);
1965+
1966+
try {
1967+
myp5.baseMaterialShader().modify(() => {
1968+
myp5.getWorldInputs(() => [1, 2, 3, 4]); // vec4 instead of Vertex struct
1969+
}, { myp5 });
1970+
} catch (e) { /* expected */ }
1971+
1972+
assert.isAbove(mockUserError.mock.calls.length, 0, 'FES.userError should have been called');
1973+
const errMsg = mockUserError.mock.calls[0][1];
1974+
assert.notInclude(errMsg, 'a undefined'); //
1975+
assert.include(errMsg, 'float4');
1976+
assert.include(errMsg, 'getWorldInputs');
1977+
assert.include(errMsg, 'Vertex');
1978+
assert.include(errMsg, 'properties');
1979+
});
1980+
1981+
test('vector dimension mismatch shows actual and expected types', () => {
1982+
myp5.createCanvas(50, 50, myp5.WEBGL);
1983+
1984+
try {
1985+
myp5.baseMaterialShader().modify(() => {
1986+
myp5.getFinalColor((c) => [c.r, c.g, c.b]); // vec3 instead of vec4
1987+
}, { myp5 });
1988+
} catch (e) { /* expected */ }
1989+
1990+
assert.isAbove(mockUserError.mock.calls.length, 0, 'FES.userError should have been called');
1991+
const errMsg = mockUserError.mock.calls[0][1];
1992+
assert.include(errMsg, 'float3');
1993+
assert.include(errMsg, 'float4');
1994+
});
1995+
1996+
test('incomplete struct shows expected vs received properties', () => {
1997+
myp5.createCanvas(50, 50, myp5.WEBGL);
1998+
1999+
try {
2000+
myp5.baseMaterialShader().modify(() => {
2001+
myp5.getWorldInputs((inputs) => {
2002+
return { position: inputs.position };
2003+
});
2004+
}, { myp5 });
2005+
} catch (e) { /* expected */ }
2006+
2007+
assert.isAbove(mockUserError.mock.calls.length, 0, 'FES.userError should have been called');
2008+
const errMsg = mockUserError.mock.calls[0][1];
2009+
assert.include(errMsg, 'Expected properties');
2010+
assert.include(errMsg, 'Received properties');
2011+
});
2012+
});
19452013
});

0 commit comments

Comments
 (0)