Skip to content

Commit f155d39

Browse files
authored
Merge pull request #8644 from Kathrina-dev/fix-filterColor-object-type
Fix TypeScript typing for filterColor shader hook
2 parents 6dce90f + 26266c8 commit f155d39

3 files changed

Lines changed: 231 additions & 15 deletions

File tree

src/strands/p5.strands.js

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,17 @@ if (typeof p5 !== "undefined") {
213213

214214
/* ------------------------------------------------------------- */
215215
/**
216-
* @property {Object} worldInputs
216+
* @typedef {Object} WorldInputsHook
217+
* @property {any} position
218+
* @property {any} normal
219+
* @property {any} texCoord
220+
* @property {any} color
221+
* @property {function(): undefined} begin
222+
* @property {function(): undefined} end
223+
*/
224+
225+
/**
226+
* @property {WorldInputsHook} worldInputs
217227
* @beta
218228
* @description
219229
* A shader hook block that modifies the world-space properties of each vertex in a shader. This hook can be used inside <a href="#/p5/buildColorShader">`buildColorShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied.
@@ -258,7 +268,22 @@ if (typeof p5 !== "undefined") {
258268
*/
259269

260270
/**
261-
* @property {Object} combineColors
271+
* @typedef {Object} CombineColorsHook
272+
* @property {any} baseColor
273+
* @property {any} diffuse
274+
* @property {any} ambientColor
275+
* @property {any} ambient
276+
* @property {any} specularColor
277+
* @property {any} specular
278+
* @property {any} emissive
279+
* @property {any} opacity
280+
* @property {function(): undefined} begin
281+
* @property {function(): undefined} end
282+
* @property {function(color: any): void} set
283+
*/
284+
285+
/**
286+
* @property {CombineColorsHook} combineColors
262287
* @beta
263288
* @description
264289
* A shader hook block that modifies how color components are combined in the fragment shader. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to control the final color output of a material. Modifications happen between the `.begin()` and `.end()` methods of the hook.
@@ -591,7 +616,26 @@ if (typeof p5 !== "undefined") {
591616
*/
592617

593618
/**
594-
* @property {Object} pixelInputs
619+
* @typedef {Object} PixelInputsHook
620+
* @property {any} normal
621+
* @property {any} texCoord
622+
* @property {any} ambientLight
623+
* @property {any} ambientMaterial
624+
* @property {any} specularMaterial
625+
* @property {any} emissiveMaterial
626+
* @property {any} color
627+
* @property {any} shininess
628+
* @property {any} metalness
629+
* @property {any} tangent
630+
* @property {any} center
631+
* @property {any} position
632+
* @property {any} strokeWeight
633+
* @property {function(): undefined} begin
634+
* @property {function(): undefined} end
635+
*/
636+
637+
/**
638+
* @property {PixelInputsHook} pixelInputs
595639
* @beta
596640
* @description
597641
* A shader hook block that modifies the properties of each pixel before the final color is calculated. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to adjust per-pixel data before lighting is applied. Modifications happen between the `.begin()` and `.end()` methods of the hook.
@@ -679,13 +723,23 @@ if (typeof p5 !== "undefined") {
679723
*/
680724

681725
/**
682-
* @property finalColor
726+
* @typedef {Object} FinalColorHook
727+
* @property {any} color
728+
* @property {any} texCoord
729+
* @property {function(): undefined} begin
730+
* @property {function(): undefined} end
731+
* @property {function(color: any): void} set
732+
*/
733+
734+
/**
735+
* @property {FinalColorHook} finalColor
683736
* @beta
684737
* @description
685738
* A shader hook block that modifies the final color of each pixel after all lighting is applied. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to adjust the color before it appears on the screen. Modifications happen between the `.begin()` and `.end()` methods of the hook.
686739
*
687740
* `finalColor` has the following properties:
688741
* - `color`: a four-component vector representing the pixel color (red, green, blue, alpha).
742+
* - `texCoord`: a two-component vector representing the texture coordinates (u, v)
689743
*
690744
* Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) to update the final color.
691745
*
@@ -762,8 +816,18 @@ if (typeof p5 !== "undefined") {
762816
*/
763817

764818
/**
765-
* @property {Object} filterColor
766-
* @beta
819+
* @typedef {Object} FilterColorHook
820+
* @property {any} texCoord
821+
* @property {any} canvasSize
822+
* @property {any} texelSize
823+
* @property {any} canvasContent
824+
* @property {function(): undefined} begin
825+
* @property {function(): undefined} end
826+
* @property {function(color: any): void} set
827+
*/
828+
829+
/**
830+
* @property {FilterColorHook} filterColor
767831
* @description
768832
* A shader hook block that sets the color for each pixel in a filter shader. This hook can be used inside <a href="#/p5/buildFilterShader">`buildFilterShader()`</a> to control the output color for each pixel.
769833
*
@@ -807,7 +871,17 @@ if (typeof p5 !== "undefined") {
807871
*/
808872

809873
/**
810-
* @property {Object} objectInputs
874+
* @typedef {Object} ObjectInputsHook
875+
* @property {any} position
876+
* @property {any} normal
877+
* @property {any} texCoord
878+
* @property {any} color
879+
* @property {function(): undefined} begin
880+
* @property {function(): undefined} end
881+
*/
882+
883+
/**
884+
* @property {ObjectInputsHook} objectInputs
811885
* @beta
812886
* @description
813887
* A shader hook block to modify the properties of each vertex before any transformations are applied. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "Object space" refers to the coordinate system of the 3D scene before any transformations, cameras, or projection transformations are applied.
@@ -849,7 +923,17 @@ if (typeof p5 !== "undefined") {
849923
*/
850924

851925
/**
852-
* @property {Object} cameraInputs
926+
* @typedef {Object} CameraInputsHook
927+
* @property {any} position
928+
* @property {any} normal
929+
* @property {any} texCoord
930+
* @property {any} color
931+
* @property {function(): undefined} begin
932+
* @property {function(): undefined} end
933+
*/
934+
935+
/**
936+
* @property {CameraInputsHook} cameraInputs
853937
* @beta
854938
* @description
855939
* A shader hook block that adjusts vertex properties from the perspective of the camera. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "Camera space" refers to the coordinate system of the 3D scene after transformations have been applied, seen relative to the camera.

utils/patch.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,3 @@ export function applyPatches() {
175175
}
176176
}
177177
}
178-

utils/typescript.mjs

Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ allRawData.forEach(entry => {
2929
if (entry.kind === 'constant' || entry.kind === 'typedef') {
3030
constantsLookup.add(entry.name);
3131
if (entry.kind === 'typedef') {
32-
typedefs[entry.name] = entry.type;
32+
// Store the full entry so we have access to both .type and .properties
33+
typedefs[entry.name] = entry;
3334
}
3435
}
3536
});
@@ -242,15 +243,29 @@ function convertTypeToTypeScript(typeNode, options = {}) {
242243
}
243244
}
244245

245-
// Check if this is a p5 constant - use typeof since they're defined as values
246+
// Check if this is a p5 constant/typedef
246247
if (constantsLookup.has(typeName)) {
248+
const typedefEntry = typedefs[typeName];
249+
250+
// Use interface name for object-shaped typedefs in all contexts
251+
if (typedefEntry && hasTypedefProperties(typedefEntry)) {
252+
if (inGlobalMode) {
253+
return `P5.${typeName}`;
254+
} else if (isInsideNamespace) {
255+
return typeName;
256+
} else {
257+
return `p5.${typeName}`;
258+
}
259+
}
260+
261+
// Fallback to typeof or primitive resolution for alias-style typedefs
247262
if (inGlobalMode) {
248263
return `typeof P5.${typeName}`;
249-
} else if (typedefs[typeName]) {
264+
} else if (typedefEntry) {
250265
if (isConstantDef) {
251-
return convertTypeToTypeScript(typedefs[typeName], options);
266+
return convertTypeToTypeScript(typedefEntry.type, options);
252267
} else {
253-
return `typeof p5.${typeName}`
268+
return `typeof p5.${typeName}`;
254269
}
255270
} else {
256271
return `Symbol`;
@@ -330,6 +345,105 @@ function convertTypeToTypeScript(typeNode, options = {}) {
330345
}
331346
}
332347

348+
// Check if typedef represents a real object shape
349+
function hasTypedefProperties(typedefEntry) {
350+
if (!Array.isArray(typedefEntry.properties) || typedefEntry.properties.length === 0) {
351+
return false;
352+
}
353+
// Reject self-referential single-property typedefs
354+
if (
355+
typedefEntry.properties.length === 1 &&
356+
typedefEntry.properties[0].name === typedefEntry.name
357+
) {
358+
return false;
359+
}
360+
return true;
361+
}
362+
363+
// Convert JSDoc FunctionType into a TypeScript function signature string
364+
function convertFunctionTypeForInterface(typeNode, options) {
365+
const params = (typeNode.params || [])
366+
.map((param, i) => {
367+
let typeObj;
368+
let paramName;
369+
if (param.type === 'ParameterType') {
370+
typeObj = param.expression;
371+
paramName = param.name ?? `p${i}`;
372+
} else if (typeof param.type === 'object' && param.type !== null) {
373+
typeObj = param.type;
374+
paramName = param.name ?? `p${i}`;
375+
} else {
376+
// param itself is a plain type node
377+
typeObj = param;
378+
paramName = `p${i}`;
379+
}
380+
const paramType = convertTypeToTypeScript(typeObj, options);
381+
return `${paramName}: ${paramType}`;
382+
})
383+
.join(', ');
384+
385+
const returnType = typeNode.result
386+
? convertTypeToTypeScript(typeNode.result, options)
387+
: 'void';
388+
389+
// Normalise 'undefined' return to 'void' for idiomatic TypeScript
390+
const normalisedReturn = returnType === 'undefined' ? 'void' : returnType;
391+
392+
return `(${params}) => ${normalisedReturn}`;
393+
}
394+
395+
// Generate a TypeScript interface from a typedef with @property fields
396+
function generateTypedefInterface(name, typedefEntry, options = {}, indent = 2) {
397+
const pad = ' '.repeat(indent);
398+
const innerPad = ' '.repeat(indent + 2);
399+
let output = '';
400+
401+
if (typedefEntry.description) {
402+
const descStr = typeof typedefEntry.description === 'string'
403+
? typedefEntry.description
404+
: descriptionStringForTypeScript(typedefEntry.description);
405+
if (descStr) {
406+
output += `${pad}/**\n`;
407+
output += formatJSDocComment(descStr, indent) + '\n';
408+
output += `${pad} */\n`;
409+
}
410+
}
411+
412+
output += `${pad}interface ${name} {\n`;
413+
414+
for (const prop of typedefEntry.properties) {
415+
// Each prop: { name, type, description, optional }
416+
const propName = prop.name;
417+
const rawType = prop.type;
418+
const isOptional = prop.optional || rawType?.type === 'OptionalType';
419+
const optMark = isOptional ? '?' : '';
420+
421+
if (prop.description) {
422+
const propDescStr = typeof prop.description === 'string'
423+
? prop.description.trim()
424+
: descriptionStringForTypeScript(prop.description);
425+
if (propDescStr) {
426+
output += `${innerPad}/** ${propDescStr} */\n`;
427+
}
428+
}
429+
430+
if (rawType?.type === 'FunctionType') {
431+
// Render FunctionType properties as method signatures instead of arrow properties
432+
const sig = convertFunctionTypeForInterface(rawType, options);
433+
const arrowIdx = sig.lastIndexOf('=>');
434+
const paramsPart = sig.substring(0, arrowIdx).trim();
435+
const retPart = sig.substring(arrowIdx + 2).trim();
436+
output += `${innerPad}${propName}${paramsPart}: ${retPart};\n`;
437+
} else {
438+
const tsType = rawType ? convertTypeToTypeScript(rawType, options) : 'any';
439+
output += `${innerPad}${propName}${optMark}: ${tsType};\n`;
440+
}
441+
}
442+
443+
output += `${pad}}\n\n`;
444+
return output;
445+
}
446+
333447
// Strategy for TypeScript output
334448
const typescriptStrategy = {
335449
shouldSkipEntry: (entry, context) => {
@@ -606,6 +720,10 @@ function generateTypeDefinitions() {
606720
if (seenConstants.has(item.name)) {
607721
return false;
608722
}
723+
// Skip typedefs that have real object shapes
724+
if (typedefs[item.name] && hasTypedefProperties(typedefs[item.name])) {
725+
return false;
726+
}
609727
seenConstants.add(item.name);
610728
return true;
611729
}
@@ -667,13 +785,20 @@ function generateTypeDefinitions() {
667785

668786
output += '\n';
669787

670-
671788
p5Constants.forEach(constant => {
672789
output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`;
673790
});
674791

675792
output += '\n';
676793

794+
// Emit interfaces for typedefs that define object shapes
795+
const namespaceOptions = { isInsideNamespace: true };
796+
for (const [name, typedefEntry] of Object.entries(typedefs)) {
797+
if (hasTypedefProperties(typedefEntry)) {
798+
output += generateTypedefInterface(name, typedefEntry, namespaceOptions, 2);
799+
}
800+
}
801+
677802
// Generate other classes in namespace
678803
Object.values(processed.classes).forEach(classData => {
679804
if (classData.name !== 'p5') {
@@ -750,6 +875,14 @@ p5: P5;
750875

751876
globalDefinitions += '\n';
752877

878+
// Mirror typedef interfaces for global-mode usage
879+
const globalNamespaceOptions = { isInsideNamespace: true, inGlobalMode: true };
880+
for (const [name, typedefEntry] of Object.entries(typedefs)) {
881+
if (hasTypedefProperties(typedefEntry)) {
882+
globalDefinitions += generateTypedefInterface(name, typedefEntry, globalNamespaceOptions, 2);
883+
}
884+
}
885+
753886
// Add all real classes as both types and constructors
754887
Object.values(processed.classes).forEach(classData => {
755888
if (classData.name !== 'p5') {

0 commit comments

Comments
 (0)