p5.js icon indicating copy to clipboard operation
p5.js copied to clipboard

registerPreloadMethod failing on various types of objects

Open dhowe opened this issue 10 years ago • 33 comments

A question: let's say I have a p5-compatible library (MyLib) with an object literal in it (MyLibObj), and this object literal has a function that the user would normally call as MyLibObj.loadObj(). Is there a nice way to register this function for preloading, without redefining it in some other scope?

dhowe avatar Jul 20 '15 13:07 dhowe

hey @dhowe does registerPreloadFunc do the trick? it was recently updated so you can specify both the name of the method and the object that method is attached to. https://github.com/processing/p5.js/wiki/libraries#use-registerpreloadmethod-to-register-names-of-methods-with-p5-that-may-be-called-in-preload

lmccart avatar Jul 21 '15 16:07 lmccart

No. I tried a variety of versions of that call before writing the ticket, but no luck..

 p5.prototype.registerPreloadMethod('MyLibObj.loadObj');
 p5.prototype.registerPreloadMethod('loadObj', 'MyLibObj');
 p5.prototype.registerPreloadMethod('MyLibObj.loadObj', 'MyLibObj');
 etc.

dhowe avatar Jul 21 '15 17:07 dhowe

The relevant lines are here in core.js (line 269), where 'f' is the first argument above, and 'o' the 2nd. Not positive, but I think the issue is due to the fact that the function in question is not attached to the object's prototype (it is a singleton object literal), so window['MyLibObj'].prototype['loadObj'] is undefined, Rather, what we want to be able to register is window['MyLibObj']['loadObj'].

  if (typeof context.preload === 'function') {
      for (var f in this._preloadMethods) {
        var o = this._preloadMethods[f];
        context[f] = window[o].prototype[f];
      }
    }

dhowe avatar Jul 21 '15 17:07 dhowe

aha ok. I don't think we actually need the prototype in there... do you want to test the newest I just pushed?

lmccart avatar Jul 27 '15 19:07 lmccart

I'm thinking about the various use-cases. But first off, is it expected that an empty preload() function will hang the sketch (with setup() never being called)?

dhowe avatar Jul 29 '15 14:07 dhowe

Types of things we might want to register as preload functions (as best I can tell only the first case currently works):

  • a function attached to p5 (e.g., p5.sound lib)
p5.prototype.registerPreloadMethod('loadSound'); 
OR
p5.prototype.registerPreloadMethod('loadSound', 'p5'); 
OR
p5.prototype.registerPreloadMethod('loadSound', p5); // ?
  • a globally defined function
window.myFunc = function(a,b) {...};
p5.prototype.registerPreloadMethod('myFunc', window);
OR
p5.prototype.registerPreloadMethod('myFunc'); // prob better
  • a function on an object literal
var myObject = {
  myPreloadFunction: function(a,b,) {...}
};
p5.prototype.registerPreloadMethod('myPreloadFunction', myObject);
  • a function on an instantiated object (with prototype)
function MyObject() {...}
MyObject.prototype.myPreloadFunction = function(a,b,) {...}

var obj = new MyObject();

p5.prototype.registerPreloadMethod('myPreloadFunction', obj);

dhowe avatar Jul 29 '15 14:07 dhowe

Looking at this again now. I've just pushed a change that takes an object rather than a string as the second argument. However, I don't think this yet supports all cases outlined above. The problematic part is this line where we set the variable context to either p5 or window (to account for instance or global mode). However, there might be cases where the function called is not a top level one but something inside another object, in which case the context would need to be set to this. We are coming up against this in trying to get the tone.js addon working. Will look into it further.

lmccart avatar Aug 11 '15 17:08 lmccart

I've been taking a crack at this and I've gotten somewhere although I'm not sure where. I'm trying to register the 4th case @dhowe outlined - registering instantiated objects.

in the start function i've added an if statement - if the function is an internal object it registers it off the window directly, otherwise it will look within Tone.

    if(typeof methods[f]._preloadMethods == "object"){
            context[f] = function() {
              var argsArray = Array.prototype.slice.call(arguments);
              return context._preload(f, methods[f], argsArray);
            };
        } else {
            //adding a check to see if it is not an internal
            //p5 function
            context["Tone"][f] = function() {
              var argsArray = Array.prototype.slice.call(arguments);
              return context._preload(f, methods[f], argsArray);
            }
        }

I've finally got a Tone.Player method registering with the preload (which wasn't working for the longest time) but I think it may be registering the constructor function rather than something else, because it now calls itself recursively until it exceeds stack size.

I'm going to see if there is a way to sniff out constructors and register them separately, because it would be great to be able to register constructors in the preload. Here is the script in the main page:

                var player;

        function preload(){
            player = new Tone.Player("./ohh.mp3").toMaster();
        }

        function setup() {
            player.start();
        }


        function draw() {
        }

polyrhythmatic avatar Aug 14 '15 16:08 polyrhythmatic

I've made a pull request that might be a solution to @dhowe's problem. As we integrate more libraries, it may make sense to consider adding some options to the registration API such as the ability to increment/decrement the preload count.

polyrhythmatic avatar Aug 18 '15 19:08 polyrhythmatic

This might be unrelated to the recent merge, but I noticed that setup is no longer called after preload even if I do something simple inside preload like console.log. Instead I get the "loading..." screen.

When I run the code here: http://p5js.org/reference/#p5/preload I see the loading screen.

Can anyone else confirm this issue? or is it just me?

indefinit avatar Aug 20 '15 13:08 indefinit

@indefinit I'm getting this error as well, but it never showed up when working on the changes for p5.Tone. I checked out a previous branch and noticed it was still an issue. Looking at it quickly, it looks like some sort of issue with a success callback.

polyrhythmatic avatar Aug 20 '15 16:08 polyrhythmatic

@polyrhythmatic looks like the bug is here: https://github.com/processing/p5.js/blob/effc45410e614cdafe739661e061ece4094419d5/src/core/core.js#L231-L238

Changing those lines back to:

Object.keys(methods).forEach(function(f) {
        context[f] = function() {
          var argsArray = Array.prototype.slice.call(arguments);
          return context._preload(f, methods[f], argsArray);
        };
      });

and tweaking the _preload return statement here (https://github.com/processing/p5.js/blob/effc45410e614cdafe739661e061ece4094419d5/src/core/core.js#L264) like this:

return obj[func].apply(context, args);

fixes it for me, since otherwise methodContext[f] is out of scope later on. I can make the change and issue a PR, but does it break your other preload methods fix? I can't seem to find any tests for the method preload registration.

indefinit avatar Aug 21 '15 04:08 indefinit

If I'm understanding correctly, this essentially undoes the previous PR #861. I think it's helpful to revert this to get preload working right now in general, but @polyrhythmatic what is your suggestion on how to proceed?

lmccart avatar Aug 21 '15 10:08 lmccart

Been thinking about the issue some more and I remember someone mentioning promises in another Issue. I wonder whether it makes more sense to take a promises-based approach to the preload function altogether. I've personally always really liked how jquery handles $.deferred and when(), like this: https://api.jquery.com/jquery.when/

Sent from my mobile.

On Aug 21, 2015, at 6:31 AM, Lauren McCarthy [email protected] wrote:

If I'm understanding correctly, this essentially undoes the previous PR #861. I think it's helpful to revert this until to get preload working right now in general, but @polyrhythmatic what is your suggestion on how to proceed?

— Reply to this email directly or view it on GitHub.

indefinit avatar Aug 21 '15 11:08 indefinit

@indefinit @lmccart sorry guys! I was wrong! I met with @tambien today and he straightened it out #866 . Yotam also refactored the code a bit so its more readable. The big problem was that the original code looked on the window for all objects, regardless of their original library

      Object.keys(methods).forEach(function(f) {
        context[f] = function() {
          var argsArray = Array.prototype.slice.call(arguments);
          return context._preload(f, methods[f], argsArray);
        };
      });

Yotam added this statement to account for that

      for (var method in this._preloadMethods){
        var obj = this._preloadMethods[method];
        //it's p5, check if it's global or instance
        if (obj === p5.prototype){
          obj = this._isGlobal ? window : this;
        }
        this._registeredPreloadMethods[method] = obj[method];
        obj[method] = this._wrapPreload(obj, method);
      }

He also added an object to keep track of the original function. This prevents the wrapped function from continuously recalling itself.

This can later be returned from the wrapper function

  this._wrapPreload = function(obj, fnName){
    return function(){
      //increment counter
      this._incrementPreload();
      //call original function
      var args = Array.prototype.slice.call(arguments);
      args.push(this._decrementPreload.bind(this));
      return this._registeredPreloadMethods[fnName].apply(obj, args);
    }.bind(this);
  };

It passed all tests, works with p5.Tone, and works with the async instance and global examples. Please let me know if you find any problems with it! I'd also like to test compliance with other libraries. Can anyone think of any that would be good to test?

polyrhythmatic avatar Aug 21 '15 21:08 polyrhythmatic

Do we have an example that works with a different object than p5.prototype? Something like:

p5.prototype.registerPreloadMethod('myPreloadFunction', myLibraryObject);

dhowe avatar Oct 11 '15 12:10 dhowe

I don't think we have an example posted somewhere but it should work. @polyrhythmatic does the p5.js-tone.js code include this, and is it posted somewhere?

lmccart avatar Oct 11 '15 12:10 lmccart

yes, I also looked for p5.tone, but didn't find any source with a registerPreload() function...

dhowe avatar Oct 11 '15 12:10 dhowe

I think we also need to update the docs here: https://github.com/processing/p5.js/wiki/Libraries

dhowe avatar Oct 11 '15 12:10 dhowe

So, I think the problem was that the function I was testing returned a string... and as best I can tell (due to the immutability of js strings) there's no way to make this work in preload(), at least not without changing the function and wrapping the result in some other object... (if so, we may want to document this for library authors?)

dhowe avatar Oct 21 '15 15:10 dhowe

Any thoughts @lmccart @polyrhythmatic ? Here's a test, in case anyone wants to give it a try (the same code works correctly when result is either an array, or an object with a string property):

<html>
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.17/p5.js"></script>
  <script>

    // a simple library-like object
    var myLib = {
      loadTest: function(callback) {

        p5.prototype.registerPreloadMethod('loadTest', myLib);

        var result = 'nope';

        setTimeout(function () {

          result = 'got it';
          console.log('done');
          callback(result);

        }, 2000); // simulate a slow load

        return result;
      }
    };

    // register the library function for preload
    p5.prototype.registerPreloadMethod('loadTest', myLib);

    var data;

    function preload() {
      data = myLib.loadTest();  // do the preload function
    }

    function setup() {
      createCanvas(500, 500);
      text(data, 20, 20); // expecting 'got it'
    }

  </script>
</head>
</html>

dhowe avatar Oct 22 '15 07:10 dhowe

indeed, this seems to be the issue. for example, if I change the code to an object it works:

    // a simple library-like object
    var myLib = {
      loadTest: function(callback) {

        //p5.prototype.registerPreloadMethod('loadTest', myLib);

        var result = {text: 'nope'};

        setTimeout(function () {

          result.text = 'got it';
          console.log('done');
          callback(result);

        }, 2000); // simulate a slow load

        return result;
      }
    };

    // register the library function for preload
    p5.prototype.registerPreloadMethod('loadTest', myLib);

    var data;

    function preload() {
      data = myLib.loadTest();  // do the preload function
    }

    function setup() {
      createCanvas(500, 500);
      text(data.text, 20, 20); // expecting 'got it'
    }

I'll add this to the libraries wiki page.

lmccart avatar Oct 25 '15 13:10 lmccart

I wonder if there is a workaround (I haven't come up with one as yet) -- its not ideal to require libraries to potentially change their APIs just to integrate with p5js...

dhowe avatar Oct 25 '15 13:10 dhowe

One other thing I notice is that external code will fail in preload() when a callback (for example, an error-handler) is passed as an argument -- which is different than the behavior of p5js' loadXXX functions. This is due to the absence of the following in the external code:

    // these 3 lines are repeated in each of our loadXXX functions
    var decrementPreload = p5._getDecrementPreload.apply(this, arguments);
    if (decrementPreload && (successCallback !== decrementPreload)) {
      decrementPreload();
    }

Not necessarily a problem, but a) we should document this, and b) library authors might be tempted to use p5._getDecrementPreload() to achieve the same effect, so we may want to remove the underscore and properly document it as a public function ?

Though my sense is there may be a more elegant solution here... @polyrhythmatic ?

dhowe avatar Oct 25 '15 21:10 dhowe

Here is another use case that appears to fail with our current preload() mechanism (this is actually the 4th case listed above on Jul29).

Imagine we have a library with a class inside, and that class has a function we want to be able to preload, something like this:

    // a library-like object
    var MyLib = {

      // a class in the library
      MyLibObject : function(type) {

        // a test load function in the class
        this.loadSumthin = function(callback) {

          var result = { text: 'nope' };

          setTimeout(function ()  {
            result.text = 'got it';
            if (callback)
              callback(result);
            else
              console.log("NO CALLBACK");

          }, 1000); // simulate a slow load

          return result;
        };
      }
    }

The fake library function works as expected when passed a callback, as in the following sketch:

    var data, libObj;

    function setup() {
      createCanvas(200, 200);
      libObj = new MyLib.MyLibObject()
      data = libObj.loadSumthin(function()  {
          text(data.text, 20, 20); // expecting 'got it'
      });
    }

However I cannot get it to work with any preload syntax. Here is one attempt:

    p5.prototype.registerPreloadMethod('loadSumthin', MyLib.MyLibObject.prototype);

    var data, libObj;

    function preload() {
      libObj = new MyLib.MyLibObject()
      data = libObj.loadSumthin();
    }

    function setup()  {
      createCanvas(500, 500);
      text(data.text, 20, 20); // expecting 'got it', but fails
    }

In another version I instead register the preload function in the object's constructor:

    p5.prototype.registerPreloadMethod('loadSumthin', this);

but still no luck. Any thoughts on a solution (seems a common case for external libraries)?

dhowe avatar Nov 13 '15 11:11 dhowe

Any thoughts @lmccart @polyrhythmatic ?

dhowe avatar Nov 28 '15 04:11 dhowe

oops sorry i missed this latest post.. I'll look into it this week.

lmccart avatar Nov 29 '15 00:11 lmccart

👍

dhowe avatar Dec 06 '15 11:12 dhowe

hi there! just wondering if there's been any progress on this? is there a recommended way of registering these preload methods? I've been using p5.js in class but also writing tiny helpers/libraries for specific projects && would be rad to have access to preload :)

nbriz avatar Mar 14 '17 19:03 nbriz

the information on how to register preload methods is here: https://github.com/processing/p5.js/wiki/Libraries

unfortunately there's been no progress on dealing with other sorts of objects as discussed above yet.

lmccart avatar Mar 15 '17 00:03 lmccart