Skip to content

Commit b168dc1

Browse files
committed
Re-bind globals when assigned in addons
1 parent d2cda2a commit b168dc1

File tree

1 file changed

+158
-118
lines changed

1 file changed

+158
-118
lines changed

src/core/main.js

Lines changed: 158 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -77,122 +77,7 @@ class p5 {
7777
// ensure correct reporting of window dimensions
7878
this._updateWindowSize();
7979

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-
};
80+
const bindGlobal = createBindGlobal(this);
19681
// If the user has created a global setup or draw function,
19782
// assume "global" mode and make everything global (i.e. on the window)
19883
if (!sketch) {
@@ -259,7 +144,23 @@ class p5 {
259144

260145
static registerAddon(addon) {
261146
const lifecycles = {};
262-
addon(p5, p5.prototype, lifecycles);
147+
148+
// Create a proxy for p5.prototype that automatically re-binds globals when properties are added
149+
const prototypeProxy = new Proxy(p5.prototype, {
150+
set(target, property, value) {
151+
const result = Reflect.set(target, property, value);
152+
153+
// If we have a global instance and this is a new property, bind it globally
154+
if (p5.instance && p5.instance._isGlobal && property[0] !== '_') {
155+
const bindGlobal = createBindGlobal(p5.instance);
156+
bindGlobal(property);
157+
}
158+
159+
return result;
160+
}
161+
});
162+
163+
addon(p5, prototypeProxy, lifecycles);
263164

264165
const validLifecycles = Object.keys(p5.lifecycleHooks);
265166
for(const name of validLifecycles){
@@ -492,9 +393,28 @@ class p5 {
492393
}
493394

494395
async _runLifecycleHook(hookName) {
396+
// Create a proxy for the instance that automatically re-binds globals when
397+
// properties are added over the course of the lifecycle.
398+
// Afterward, we skip global binding for efficiency.
399+
let inLifecycle = true;
400+
const instanceProxy = new Proxy(this, {
401+
set(target, property, value) {
402+
const result = Reflect.set(target, property, value);
403+
404+
// If this is a global instance and this is a new property, bind it globally
405+
if (inLifecycle && target._isGlobal && property[0] !== '_') {
406+
const bindGlobal = createBindGlobal(target);
407+
bindGlobal(property);
408+
}
409+
410+
return result;
411+
}
412+
});
413+
495414
await Promise.all(p5.lifecycleHooks[hookName].map(hook => {
496-
return hook.call(this);
415+
return hook.call(instanceProxy);
497416
}));
417+
inLifecycle = false;
498418
}
499419

500420
_initializeInstanceVariables() {
@@ -511,6 +431,126 @@ class p5 {
511431
}
512432
}
513433

434+
// Global helper function for binding properties to window in global mode
435+
function createBindGlobal(instance) {
436+
return function bindGlobal(property) {
437+
if (property === 'constructor') return;
438+
439+
// Common setter for all property types
440+
const createSetter = () => newValue => {
441+
Object.defineProperty(window, property, {
442+
configurable: true,
443+
enumerable: true,
444+
value: newValue,
445+
writable: true
446+
});
447+
if (!p5.disableFriendlyErrors) {
448+
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.`);
449+
}
450+
};
451+
452+
// Check if this property has a getter on the instance or prototype
453+
const instanceDescriptor = Object.getOwnPropertyDescriptor(instance, property);
454+
const prototypeDescriptor = Object.getOwnPropertyDescriptor(p5.prototype, property);
455+
const hasGetter = (instanceDescriptor && instanceDescriptor.get) ||
456+
(prototypeDescriptor && prototypeDescriptor.get);
457+
458+
// Only check if it's a function if it doesn't have a getter
459+
// to avoid actually evaluating getters before things like the
460+
// renderer are fully constructed
461+
let isPrototypeFunction = false;
462+
let isConstant = false;
463+
let constantValue;
464+
465+
if (!hasGetter) {
466+
const prototypeValue = p5.prototype[property];
467+
isPrototypeFunction = typeof prototypeValue === 'function';
468+
469+
// Check if this is a true constant from the constants module
470+
if (!isPrototypeFunction && constants[property] !== undefined) {
471+
isConstant = true;
472+
constantValue = prototypeValue;
473+
}
474+
}
475+
476+
if (isPrototypeFunction) {
477+
// For regular functions, cache the bound function
478+
const boundFunction = p5.prototype[property].bind(instance);
479+
if (p5.disableFriendlyErrors) {
480+
Object.defineProperty(window, property, {
481+
configurable: true,
482+
enumerable: true,
483+
value: boundFunction,
484+
});
485+
} else {
486+
Object.defineProperty(window, property, {
487+
configurable: true,
488+
enumerable: true,
489+
get() {
490+
return boundFunction;
491+
},
492+
set: createSetter()
493+
});
494+
}
495+
} else if (isConstant) {
496+
// For constants, cache the value directly
497+
if (p5.disableFriendlyErrors) {
498+
Object.defineProperty(window, property, {
499+
configurable: true,
500+
enumerable: true,
501+
value: constantValue,
502+
});
503+
} else {
504+
Object.defineProperty(window, property, {
505+
configurable: true,
506+
enumerable: true,
507+
get() {
508+
return constantValue;
509+
},
510+
set: createSetter()
511+
});
512+
}
513+
} else if (hasGetter || !isPrototypeFunction) {
514+
// For properties with getters or non-function properties, use lazy optimization
515+
// On first access, determine the type and optimize subsequent accesses
516+
let lastFunction = null;
517+
let boundFunction = null;
518+
let isFunction = null; // null = unknown, true = function, false = not function
519+
520+
Object.defineProperty(window, property, {
521+
configurable: true,
522+
enumerable: true,
523+
get: () => {
524+
const currentValue = instance[property];
525+
526+
if (isFunction === null) {
527+
// First access - determine type and optimize
528+
isFunction = typeof currentValue === 'function';
529+
if (isFunction) {
530+
lastFunction = currentValue;
531+
boundFunction = currentValue.bind(instance);
532+
return boundFunction;
533+
} else {
534+
return currentValue;
535+
}
536+
} else if (isFunction) {
537+
// Optimized function path - only rebind if function changed
538+
if (currentValue !== lastFunction) {
539+
lastFunction = currentValue;
540+
boundFunction = currentValue.bind(instance);
541+
}
542+
return boundFunction;
543+
} else {
544+
// Optimized non-function path
545+
return currentValue;
546+
}
547+
},
548+
set: createSetter()
549+
});
550+
}
551+
};
552+
}
553+
514554
// Attach constants to p5 prototype
515555
for (const k in constants) {
516556
p5.prototype[k] = constants[k];

0 commit comments

Comments
 (0)