diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 9b58389174..4462f8dd9b 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -612,7 +612,18 @@ if (typeof p5 !== "undefined") { */ /** - * @property {Object} filterColor + * @typedef {Object} FilterColorHook + * @property {any} texCoord + * @property {any} canvasSize + * @property {any} texelSize + * @property {any} canvasContent + * @property {function(): undefined} begin + * @property {function(): undefined} end + * @property {function(color: any): void} set + */ + +/** + * @property {FilterColorHook} filterColor * @description * A shader hook block that sets the color for each pixel in a filter shader. This hook can be used inside `buildFilterShader()` to control the output color for each pixel. * diff --git a/utils/patch.mjs b/utils/patch.mjs index 446a6ef755..841ac9c703 100644 --- a/utils/patch.mjs +++ b/utils/patch.mjs @@ -175,4 +175,3 @@ export function applyPatches() { } } } - diff --git a/utils/typescript.mjs b/utils/typescript.mjs index ad119cf80e..3b01110527 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -29,7 +29,8 @@ allRawData.forEach(entry => { if (entry.kind === 'constant' || entry.kind === 'typedef') { constantsLookup.add(entry.name); if (entry.kind === 'typedef') { - typedefs[entry.name] = entry.type; + // Store the full entry so we have access to both .type and .properties + typedefs[entry.name] = entry; } } }); @@ -242,15 +243,29 @@ function convertTypeToTypeScript(typeNode, options = {}) { } } - // Check if this is a p5 constant - use typeof since they're defined as values + // Check if this is a p5 constant/typedef if (constantsLookup.has(typeName)) { + const typedefEntry = typedefs[typeName]; + + // Use interface name for object typedefs unless resolving the constant definition itself + if (typedefEntry && hasTypedefProperties(typedefEntry) && !isConstantDef) { + if (inGlobalMode) { + return `P5.${typeName}`; + } else if (isInsideNamespace) { + return typeName; + } else { + return `p5.${typeName}`; + } + } + + // Fallback to typeof or primitive resolution for alias-style typedefs if (inGlobalMode) { return `typeof P5.${typeName}`; - } else if (typedefs[typeName]) { + } else if (typedefEntry) { if (isConstantDef) { - return convertTypeToTypeScript(typedefs[typeName], options); + return convertTypeToTypeScript(typedefEntry.type, options); } else { - return `typeof p5.${typeName}` + return `typeof p5.${typeName}`; } } else { return `Symbol`; @@ -330,6 +345,105 @@ function convertTypeToTypeScript(typeNode, options = {}) { } } +// Check if typedef represents a real object shape +function hasTypedefProperties(typedefEntry) { + if (!Array.isArray(typedefEntry.properties) || typedefEntry.properties.length === 0) { + return false; + } + // Reject self-referential single-property typedefs + if ( + typedefEntry.properties.length === 1 && + typedefEntry.properties[0].name === typedefEntry.name + ) { + return false; + } + return true; +} + +// Convert JSDoc FunctionType into a TypeScript function signature string +function convertFunctionTypeForInterface(typeNode, options) { + const params = (typeNode.params || []) + .map((param, i) => { + let typeObj; + let paramName; + if (param.type === 'ParameterType') { + typeObj = param.expression; + paramName = param.name ?? `p${i}`; + } else if (typeof param.type === 'object' && param.type !== null) { + typeObj = param.type; + paramName = param.name ?? `p${i}`; + } else { + // param itself is a plain type node + typeObj = param; + paramName = `p${i}`; + } + const paramType = convertTypeToTypeScript(typeObj, options); + return `${paramName}: ${paramType}`; + }) + .join(', '); + + const returnType = typeNode.result + ? convertTypeToTypeScript(typeNode.result, options) + : 'void'; + + // Normalise 'undefined' return to 'void' for idiomatic TypeScript + const normalisedReturn = returnType === 'undefined' ? 'void' : returnType; + + return `(${params}) => ${normalisedReturn}`; +} + +// Generate a TypeScript interface from a typedef with @property fields +function generateTypedefInterface(name, typedefEntry, options = {}, indent = 2) { + const pad = ' '.repeat(indent); + const innerPad = ' '.repeat(indent + 2); + let output = ''; + + if (typedefEntry.description) { + const descStr = typeof typedefEntry.description === 'string' + ? typedefEntry.description + : descriptionStringForTypeScript(typedefEntry.description); + if (descStr) { + output += `${pad}/**\n`; + output += formatJSDocComment(descStr, indent) + '\n'; + output += `${pad} */\n`; + } + } + + output += `${pad}interface ${name} {\n`; + + for (const prop of typedefEntry.properties) { + // Each prop: { name, type, description, optional } + const propName = prop.name; + const rawType = prop.type; + const isOptional = prop.optional || rawType?.type === 'OptionalType'; + const optMark = isOptional ? '?' : ''; + + if (prop.description) { + const propDescStr = typeof prop.description === 'string' + ? prop.description.trim() + : descriptionStringForTypeScript(prop.description); + if (propDescStr) { + output += `${innerPad}/** ${propDescStr} */\n`; + } + } + + if (rawType?.type === 'FunctionType') { + // Render FunctionType properties as method signatures instead of arrow properties + const sig = convertFunctionTypeForInterface(rawType, options); + const arrowIdx = sig.lastIndexOf('=>'); + const paramsPart = sig.substring(0, arrowIdx).trim(); + const retPart = sig.substring(arrowIdx + 2).trim(); + output += `${innerPad}${propName}${paramsPart}: ${retPart};\n`; + } else { + const tsType = rawType ? convertTypeToTypeScript(rawType, options) : 'any'; + output += `${innerPad}${propName}${optMark}: ${tsType};\n`; + } + } + + output += `${pad}}\n\n`; + return output; +} + // Strategy for TypeScript output const typescriptStrategy = { shouldSkipEntry: (entry, context) => { @@ -667,13 +781,20 @@ function generateTypeDefinitions() { output += '\n'; - p5Constants.forEach(constant => { output += `${mutableProperties.has(constant.name) ? 'let' : 'const'} ${constant.name}: typeof __${constant.name};\n`; }); output += '\n'; + // Emit interfaces for typedefs that define object shapes + const namespaceOptions = { isInsideNamespace: true }; + for (const [name, typedefEntry] of Object.entries(typedefs)) { + if (hasTypedefProperties(typedefEntry)) { + output += generateTypedefInterface(name, typedefEntry, namespaceOptions, 2); + } + } + // Generate other classes in namespace Object.values(processed.classes).forEach(classData => { if (classData.name !== 'p5') { @@ -750,6 +871,14 @@ p5: P5; globalDefinitions += '\n'; + // Mirror typedef interfaces for global-mode usage + const globalNamespaceOptions = { isInsideNamespace: true, inGlobalMode: true }; + for (const [name, typedefEntry] of Object.entries(typedefs)) { + if (hasTypedefProperties(typedefEntry)) { + globalDefinitions += generateTypedefInterface(name, typedefEntry, globalNamespaceOptions, 2); + } + } + // Add all real classes as both types and constructors Object.values(processed.classes).forEach(classData => { if (classData.name !== 'p5') {