Skip to content

Commit 881a2b5

Browse files
authored
Merge branch 'dev-2.0' into fix/typescript
2 parents 9a80f41 + f76b2b6 commit 881a2b5

File tree

3 files changed

+172
-164
lines changed

3 files changed

+172
-164
lines changed

src/core/friendly_errors/param_validator.js

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ function validateParams(p5, fn, lifecycles) {
141141
* @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add`
142142
* @returns {z.ZodSchema} Zod schema
143143
*/
144-
fn.generateZodSchemasForFunc = function (func) {
144+
const generateZodSchemasForFunc = function (func) {
145145
const { funcName, funcClass } = extractFuncNameAndClass(func);
146146
let funcInfo = dataDoc[funcClass][funcName];
147147

@@ -309,7 +309,7 @@ function validateParams(p5, fn, lifecycles) {
309309
* @param {Array} args - User input arguments.
310310
* @returns {z.ZodSchema} Closest schema matching the input arguments.
311311
*/
312-
fn.findClosestSchema = function (schema, args) {
312+
const findClosestSchema = function (schema, args) {
313313
if (!(schema instanceof z.ZodUnion)) {
314314
return schema;
315315
}
@@ -390,7 +390,7 @@ function validateParams(p5, fn, lifecycles) {
390390
* @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add`
391391
* @returns {String} The friendly error message.
392392
*/
393-
fn.friendlyParamError = function (zodErrorObj, func, args) {
393+
const friendlyParamError = function (zodErrorObj, func, args) {
394394
let message = '🌸 p5.js says: ';
395395
let isVersionError = false;
396396
// The `zodErrorObj` might contain multiple errors of equal importance
@@ -521,7 +521,7 @@ function validateParams(p5, fn, lifecycles) {
521521
* @returns {any} [result.data] - The parsed data if validation was successful.
522522
* @returns {String} [result.error] - The validation error message if validation has failed.
523523
*/
524-
fn.validate = function (func, args) {
524+
const validate = function (func, args) {
525525
if (p5.disableFriendlyErrors) {
526526
return; // skip FES
527527
}
@@ -549,7 +549,7 @@ function validateParams(p5, fn, lifecycles) {
549549

550550
let funcSchemas = schemaRegistry.get(func);
551551
if (!funcSchemas) {
552-
funcSchemas = fn.generateZodSchemasForFunc(func);
552+
funcSchemas = generateZodSchemasForFunc(func);
553553
if (!funcSchemas) return;
554554
schemaRegistry.set(func, funcSchemas);
555555
}
@@ -560,9 +560,9 @@ function validateParams(p5, fn, lifecycles) {
560560
data: funcSchemas.parse(args)
561561
};
562562
} catch (error) {
563-
const closestSchema = fn.findClosestSchema(funcSchemas, args);
563+
const closestSchema = findClosestSchema(funcSchemas, args);
564564
const zodError = closestSchema.safeParse(args).error;
565-
const errorMessage = fn.friendlyParamError(zodError, func, args);
565+
const errorMessage = friendlyParamError(zodError, func, args);
566566

567567
return {
568568
success: false,
@@ -571,25 +571,22 @@ function validateParams(p5, fn, lifecycles) {
571571
}
572572
};
573573

574-
lifecycles.presetup = function(){
575-
loadP5Constructors();
574+
fn._validate = validate; // TEMP: For unit tests
576575

577-
if(
578-
p5.disableParameterValidator !== true &&
579-
p5.disableFriendlyErrors !== true
580-
){
581-
const excludes = ['validate'];
582-
for(const f in this){
583-
if(!excludes.includes(f) && !f.startsWith('_') && typeof this[f] === 'function'){
584-
const copy = this[f];
585-
586-
this[f] = function(...args) {
587-
this.validate(f, args);
588-
return copy.call(this, ...args);
589-
};
576+
p5.decorateHelper(
577+
/^(?!_).+$/,
578+
function(target, { name }){
579+
return function(...args){
580+
if (!p5.disableFriendlyErrors && !p5.disableParameterValidator) {
581+
validate(name, args);
590582
}
591-
}
583+
return target.call(this, ...args);
584+
};
592585
}
586+
);
587+
588+
lifecycles.presetup = function(){
589+
loadP5Constructors();
593590
};
594591
}
595592

src/core/main.js

Lines changed: 128 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,35 @@ class p5 {
4848
static _friendlyFileLoadError = () => {};
4949

5050
constructor(sketch, node) {
51+
// Apply addon defined decorations
52+
if(p5.decorations.size > 0){
53+
for (const [patternArray, decoration] of p5.decorations) {
54+
for(const member in p5.prototype) {
55+
// Member must be a function
56+
if (typeof p5.prototype[member] !== 'function') continue;
57+
58+
if (!patternArray.some(pattern => {
59+
if (typeof pattern === 'string') {
60+
return pattern === member;
61+
} else if (pattern instanceof RegExp) {
62+
return pattern.test(member);
63+
}
64+
})) continue;
65+
66+
p5.prototype[member] = decoration(p5.prototype[member], {
67+
kind: 'method',
68+
name: member,
69+
access: {},
70+
static: false,
71+
private: false,
72+
addInitializer(initializer){}
73+
});
74+
}
75+
}
76+
77+
p5.decorations.clear();
78+
}
79+
5180
//////////////////////////////////////////////
5281
// PRIVATE p5 PROPERTIES AND METHODS
5382
//////////////////////////////////////////////
@@ -77,122 +106,7 @@ class p5 {
77106
// ensure correct reporting of window dimensions
78107
this._updateWindowSize();
79108

80-
const bindGlobal = property => {
81-
if (property === 'constructor') return;
82-
83-
// Common setter for all property types
84-
const createSetter = () => newValue => {
85-
Object.defineProperty(window, property, {
86-
configurable: true,
87-
enumerable: true,
88-
value: newValue,
89-
writable: true
90-
});
91-
if (!p5.disableFriendlyErrors) {
92-
console.log(`You just changed the value of "${property}", which was a p5 global value. This could cause problems later if you're not careful.`);
93-
}
94-
};
95-
96-
// Check if this property has a getter on the instance or prototype
97-
const instanceDescriptor = Object.getOwnPropertyDescriptor(this, property);
98-
const prototypeDescriptor = Object.getOwnPropertyDescriptor(p5.prototype, property);
99-
const hasGetter = (instanceDescriptor && instanceDescriptor.get) ||
100-
(prototypeDescriptor && prototypeDescriptor.get);
101-
102-
// Only check if it's a function if it doesn't have a getter
103-
// to avoid actually evaluating getters before things like the
104-
// renderer are fully constructed
105-
let isPrototypeFunction = false;
106-
let isConstant = false;
107-
let constantValue;
108-
109-
if (!hasGetter) {
110-
const prototypeValue = p5.prototype[property];
111-
isPrototypeFunction = typeof prototypeValue === 'function';
112-
113-
// Check if this is a true constant from the constants module
114-
if (!isPrototypeFunction && constants[property] !== undefined) {
115-
isConstant = true;
116-
constantValue = prototypeValue;
117-
}
118-
}
119-
120-
if (isPrototypeFunction) {
121-
// For regular functions, cache the bound function
122-
const boundFunction = p5.prototype[property].bind(this);
123-
if (p5.disableFriendlyErrors) {
124-
Object.defineProperty(window, property, {
125-
configurable: true,
126-
enumerable: true,
127-
value: boundFunction,
128-
});
129-
} else {
130-
Object.defineProperty(window, property, {
131-
configurable: true,
132-
enumerable: true,
133-
get() {
134-
return boundFunction;
135-
},
136-
set: createSetter()
137-
});
138-
}
139-
} else if (isConstant) {
140-
// For constants, cache the value directly
141-
if (p5.disableFriendlyErrors) {
142-
Object.defineProperty(window, property, {
143-
configurable: true,
144-
enumerable: true,
145-
value: constantValue,
146-
});
147-
} else {
148-
Object.defineProperty(window, property, {
149-
configurable: true,
150-
enumerable: true,
151-
get() {
152-
return constantValue;
153-
},
154-
set: createSetter()
155-
});
156-
}
157-
} else if (hasGetter || !isPrototypeFunction) {
158-
// For properties with getters or non-function properties, use lazy optimization
159-
// On first access, determine the type and optimize subsequent accesses
160-
let lastFunction = null;
161-
let boundFunction = null;
162-
let isFunction = null; // null = unknown, true = function, false = not function
163-
164-
Object.defineProperty(window, property, {
165-
configurable: true,
166-
enumerable: true,
167-
get: () => {
168-
const currentValue = this[property];
169-
170-
if (isFunction === null) {
171-
// First access - determine type and optimize
172-
isFunction = typeof currentValue === 'function';
173-
if (isFunction) {
174-
lastFunction = currentValue;
175-
boundFunction = currentValue.bind(this);
176-
return boundFunction;
177-
} else {
178-
return currentValue;
179-
}
180-
} else if (isFunction) {
181-
// Optimized function path - only rebind if function changed
182-
if (currentValue !== lastFunction) {
183-
lastFunction = currentValue;
184-
boundFunction = currentValue.bind(this);
185-
}
186-
return boundFunction;
187-
} else {
188-
// Optimized non-function path
189-
return currentValue;
190-
}
191-
},
192-
set: createSetter()
193-
});
194-
}
195-
};
109+
const bindGlobal = createBindGlobal(this);
196110
// If the user has created a global setup or draw function,
197111
// assume "global" mode and make everything global (i.e. on the window)
198112
if (!sketch) {
@@ -259,6 +173,7 @@ class p5 {
259173

260174
static registerAddon(addon) {
261175
const lifecycles = {};
176+
262177
addon(p5, p5.prototype, lifecycles);
263178

264179
const validLifecycles = Object.keys(p5.lifecycleHooks);
@@ -269,6 +184,13 @@ class p5 {
269184
}
270185
}
271186

187+
static decorations = new Map();
188+
static decorateHelper(pattern, decoration){
189+
let patternArray = pattern;
190+
if (!Array.isArray(pattern)) patternArray = [pattern];
191+
p5.decorations.set(patternArray, decoration);
192+
}
193+
272194
#customActions = {};
273195
_customActions = new Proxy({}, {
274196
get: (target, prop) => {
@@ -511,6 +433,96 @@ class p5 {
511433
}
512434
}
513435

436+
// Global helper function for binding properties to window in global mode
437+
function createBindGlobal(instance) {
438+
return function bindGlobal(property) {
439+
if (property === 'constructor') return;
440+
441+
// Check if this property has a getter on the instance or prototype
442+
const instanceDescriptor = Object.getOwnPropertyDescriptor(
443+
instance,
444+
property
445+
);
446+
const prototypeDescriptor = Object.getOwnPropertyDescriptor(
447+
p5.prototype,
448+
property
449+
);
450+
const hasGetter = (instanceDescriptor && instanceDescriptor.get) ||
451+
(prototypeDescriptor && prototypeDescriptor.get);
452+
453+
// Only check if it's a function if it doesn't have a getter
454+
// to avoid actually evaluating getters before things like the
455+
// renderer are fully constructed
456+
let isPrototypeFunction = false;
457+
let isConstant = false;
458+
let constantValue;
459+
460+
if (!hasGetter) {
461+
const prototypeValue = p5.prototype[property];
462+
isPrototypeFunction = typeof prototypeValue === 'function';
463+
464+
// Check if this is a true constant from the constants module
465+
if (!isPrototypeFunction && constants[property] !== undefined) {
466+
isConstant = true;
467+
constantValue = prototypeValue;
468+
}
469+
}
470+
471+
if (isPrototypeFunction) {
472+
// For regular functions, cache the bound function
473+
const boundFunction = p5.prototype[property].bind(instance);
474+
Object.defineProperty(window, property, {
475+
configurable: true,
476+
enumerable: true,
477+
value: boundFunction
478+
});
479+
} else if (isConstant) {
480+
// For constants, cache the value directly
481+
Object.defineProperty(window, property, {
482+
configurable: true,
483+
enumerable: true,
484+
value: constantValue
485+
});
486+
} else if (hasGetter || !isPrototypeFunction) {
487+
// For properties with getters or non-function properties, use lazy optimization
488+
// On first access, determine the type and optimize subsequent accesses
489+
let lastFunction = null;
490+
let boundFunction = null;
491+
let isFunction = null; // null = unknown, true = function, false = not function
492+
493+
Object.defineProperty(window, property, {
494+
configurable: true,
495+
enumerable: true,
496+
get: () => {
497+
const currentValue = instance[property];
498+
499+
if (isFunction === null) {
500+
// First access - determine type and optimize
501+
isFunction = typeof currentValue === 'function';
502+
if (isFunction) {
503+
lastFunction = currentValue;
504+
boundFunction = currentValue.bind(instance);
505+
return boundFunction;
506+
} else {
507+
return currentValue;
508+
}
509+
} else if (isFunction) {
510+
// Optimized function path - only rebind if function changed
511+
if (currentValue !== lastFunction) {
512+
lastFunction = currentValue;
513+
boundFunction = currentValue.bind(instance);
514+
}
515+
return boundFunction;
516+
} else {
517+
// Optimized non-function path
518+
return currentValue;
519+
}
520+
}
521+
});
522+
}
523+
};
524+
}
525+
514526
// Attach constants to p5 prototype
515527
for (const k in constants) {
516528
p5.prototype[k] = constants[k];
@@ -745,8 +757,6 @@ for (const k in constants) {
745757
* </code>
746758
* </div>
747759
*/
748-
p5.disableFriendlyErrors = false;
749-
750760
import transform from './transform';
751761
import structure from './structure';
752762
import environment from './environment';

0 commit comments

Comments
 (0)