diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js
index d8c839847e..4a86bfa55c 100644
--- a/src/strands/p5.strands.js
+++ b/src/strands/p5.strands.js
@@ -214,7 +214,10 @@ if (typeof p5 !== "undefined") {
/* ------------------------------------------------------------- */
/**
- * @property {Object} worldInputs
+ * @typedef {Object} WorldInputsHook
+*/
+/**
+ * @property {WorldInputsHook} worldInputs
* @beta
* @description
* A shader hook block that modifies the world-space properties of each vertex in a shader. This hook can be used inside `buildColorShader()` and similar shader `modify()` 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.
@@ -259,7 +262,10 @@ if (typeof p5 !== "undefined") {
*/
/**
- * @property {Object} combineColors
+ * @typedef {Object} CombineColorsHook
+*/
+/**
+ * @property {CombineColorsHook} combineColors
* @beta
* @description
* A shader hook block that modifies how color components are combined in the fragment shader. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to control the final color output of a material. Modifications happen between the `.begin()` and `.end()` methods of the hook.
@@ -592,7 +598,10 @@ if (typeof p5 !== "undefined") {
*/
/**
- * @property {Object} pixelInputs
+ * @typedef {Object} PixelInputsHook
+*/
+/**
+ * @property {PixelInputsHook} pixelInputs
* @beta
* @description
* A shader hook block that modifies the properties of each pixel before the final color is calculated. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` calls to adjust per-pixel data before lighting is applied. Modifications happen between the `.begin()` and `.end()` methods of the hook.
@@ -763,8 +772,18 @@ if (typeof p5 !== "undefined") {
*/
/**
- * @property {Object} filterColor
- * @beta
+ * @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.
*
@@ -808,7 +827,10 @@ if (typeof p5 !== "undefined") {
*/
/**
- * @property {Object} objectInputs
+ * @typedef {Object} ObjectInputsHook
+*/
+/**
+ * @property {ObjectInputsHook} objectInputs
* @beta
* @description
* A shader hook block to modify the properties of each vertex before any transformations are applied. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` 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.
@@ -850,7 +872,10 @@ if (typeof p5 !== "undefined") {
*/
/**
- * @property {Object} cameraInputs
+ * @typedef {Object} CameraInputsHook
+*/
+/**
+ * @property {CameraInputsHook} cameraInputs
* @beta
* @description
* A shader hook block that adjusts vertex properties from the perspective of the camera. This hook can be used inside `buildMaterialShader()` and similar shader `modify()` 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.
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..751ff117f8 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-shaped typedefs in all contexts
+ if (typedefEntry && hasTypedefProperties(typedefEntry)) {
+ 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) => {
@@ -606,6 +720,10 @@ function generateTypeDefinitions() {
if (seenConstants.has(item.name)) {
return false;
}
+ // Skip typedefs that have real object shapes
+ if (typedefs[item.name] && hasTypedefProperties(typedefs[item.name])) {
+ return false;
+ }
seenConstants.add(item.name);
return true;
}
@@ -667,13 +785,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 +875,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') {