 Hello and welcome to Scripting for Artists, my name is Sibyren and in this episode we're going to take a look at custom properties. In the last video we created a panel and added existing properties to it. And this is already quite powerful and if that is enough for your situation then please don't make it any more complex on that. Simpler is often better. However, there are these situations though where you want to attach your own data to things in Blender. To start let's take a look at the kind of properties there are. Throughout the series you've seen existing properties like object.location or context.scene. Then there are the custom properties that you can create in the custom properties panel. Maybe you've also seen these properties where the edit button is gone and where it says API defined and this means it's been defined from Python. This is what we're also going to look at in this video. To give us a concrete goal to work towards we'll be creating a mass obj importer. Each scene will get a property that contains the path of a folder that contains its obj files and then we'll create an operator that loads all the obj files from that folder and finally we'll look into creating an operator that reloads those obj files from disk. So in that way you can have a one-button reload of all your obj files. This is quite a bit of work so let's get started. For this episode we're going to create a new add-on and I've started already. I've added a new Python file I've installed it as an add-on loaded in my editor which is visual studio code. I will quickly raise through this because you've seen all of this before in the previous videos. If you haven't seen this yet please take a look at the videos about creating your own add-on and creating user interfaces. So we start with the BL Info dictionary. I've set the name to the add-on to scripting for artist mass importer. The rest of the information is pretty much what you would expect. Import bpi of course comes next. Then I want to have everything that we're going to do available to us in a panel. There we can show the buttons, we can show the properties, we can do everything we want. I've created a new panel by copying the code we had for the monkey grid and then adjusting it so it's pretty much the same except that it has that label mass import and that is also reflected in the class name. It's now called view 3d pt for panel type mass import. We're going to draw something but not just yet. Then of course we have the register and unregister functions. Those are necessary for any add-on. We're going to add more classes to this file. So we'll have an operator for doing the mass import and then also we'll have an operator for doing a single re-import of an object and then also an operator for doing a mass re-import of all the imported objects. So we're going to have more and I didn't want to copy, paste the bpi Yultas register class and then make sure that in the unregister it's also copied to the unregister class and managing all of that. So instead of that I've created a list called blender classes in which I have only the panel that we have now and then in the register and unregister we loop over it with a for loop. So for blender class and blender classes we'll go over this list and for every item in that list it will call bpi Yultas register class or unregister class and this makes it a little bit easier to add new stuff later because new classes we can just add to this list. Now that our code is really simple let's see that it actually works in blender so that we can be sure that when we start adding stuff we add it on a basis that actually works. So in the user preferences enable the add-on and then here in our monkeys tab we can see our mass import panel. Of course there's nothing drawn yet but that is what we expected so the basis for our code the registering the unregistering and the existence of the panel itself that's all correct. Okay so let's start creating these properties that we wanted to have this is quite simple actually. You know that there's a lot of stuff in bpi.types and we want to have a property on the scene that contains the import path of the obj files that we want to have. So we start with bpi.types.scene with a capital C dot and then the name of our property let's call it mass import path. Now it's very similar to adding your own properties to your own operator except that with the operator we had our own class that we could use to declare the properties and now that class is already there so we cannot add anything to it so it's a little bit different. So instead of using the colon we use the equals sign and then again we assign property they live in bpi.props and a file path or a directory path is just a string so that gives us a string property. Now for the user interface we may want to give it a nice name so let's call it obj folder and we always end it with a comma because then I can add stuff on the next line without having to go back up and add that comma again. This is valid python and it makes your changes a bit more localized you can now add other stuff here without changing the line above. This is in the register function so there we add our property of course we have to delete it again in the unregister to clean up what we did then basically you take this and you say del that property and that tells python just delete the thing and this is enough so let's see how this looks in blender here we have to reload our code in blender 2.83 and nearer you can do that here with reload scripts if you run an older version of blender press F3 and then type reload scripts they both do the same thing except that through the menu you can add it to the quick menu when you press Q and this is the approach that I will use from now on so we've reloaded our code and let's take a look at the scene there you go we have our own property mass import path the default is empty because we haven't set any default and it's just there and every scene will have it we can also change the property from python and keep your eyes on the custom properties panel here which is in the scene tab take a look at that what happens when I actually assign a value you have to wiggle your mouse a little bit to make it redraw but you can see that now that we have assigned a value to it it is actually stored in the custom properties it's API defined which means that you cannot just delete it and this is one of the major advantages of using your properties like this instead of just clicking the add button here and creating a new one when you create it through python you can be sure that the property is there even if it hasn't been set it will have a default so it's one less thing to worry about the other advantage is that we can set sub types and this will indicate to blender what kind of string we're talking about so this is going to be a path and it will be nice if it had like a little button that you could have a file browser that selects the path for you instead of having to type it in the python console all of this is done by blender once you set the sub type correctly so let's do that the sub type is set by the sub type parameter and then yeah pass a string that indicates a sub type in order to find the sub types for a specific property you can go back to blender help python api reference and search for bpi dot props dot string property and there it will tell you which sub types there are in our case it's a directory path or dear path so now let's reload the code and see what happens here in this corner of the interface there you go it has a button to browse and you can select directories because I set it to a silly value it will just start at a silly directory because it's not a valid path but if I change this to slash slash which indicates the current directory of the blend file it will open it here and I can navigate to my obj files directory click accept and then it has set here the path and this is exactly what we wanted except that we want this behavior in our own panel of course and let's do that in our panel code we can remove the pass because we're actually going to do something and let's start with getting the layout so we didn't do this before but do know that every self dot something will do a little dance to see is it defined on this class if not look at a parent is defined there if not look at the grandparent etc so we're saying a layout equals self dot layout we only do this dance once and then just use the found layout object let's add a column and then add the property the property was defined on the scene which is context dot scene and the property name was mass import path that's it reload the code and there we have our property browse button and all now let's go and create an operator that imports all the obj files from that directory and I want to name it quite similar to the operator that already exists for importing obj files so let's take a look at that that is bpi dot ops dot import scene dot obj so let's call our operator import scene dot obj mass so here we have an m2 operator you've seen this before I just named it differently it's called import scene dot obj mass it has an execute function I put in this little comment as a reference and then it calls self report this takes a report type which can be info or warning or error and then a string that is then reported it returns canceled because nothing is done yet again I'm taking small steps I first make sure that the operator is there and that it's registered properly then we can add it to a panel we can click on the button we can see that it works before adding more complexities to it so the operator is here now I copy this name I go to the blender classes list I add the name to the list and that takes care of the registering and the unregistering so what is left now is added to the panel so all we need is the blid name paste it in save in blender reload the script and we have our button we click on it it says no code to load from slash slash which is the error that we expected now the next step is finding the obj files and importing them and afterwards we want to be able to re-import them so we have to keep track of which object was imported from which file the best way to do that is to add another property to the object type so let's do that now before we start actually importing them here in a register function we're going to do the same thing as we did to a scene but then to an object and then it also becomes a string property but then with obj file instead of obj folder so the folder and the file name combined will give us the final path of the obj file because there is no subtype for just a file name it's only for a full file path or a full directory path we'll just keep this as a string and don't define any subtype so before we look at the importing let's add this property to the panel we have the panel code here let's create a new column for this and there we have the property but remember context.object can be none so we have to add a little guard around this none is considered false with python and real object is considered true so if context.object will evaluate it true if it's real object and then draw the property it will evaluate to false if there is no active object and then it will draw the label instead let's give it a try and there we have our property and we can set it per object as you can see now let's build the importing itself so here we are at our mass importer operator let's write a little layout of what it is supposed to do it has to find the object files and for each file import it and record its file name in the object property let's look at that first step this is going to take a few steps actually because for finding files we have to use python functionality and python doesn't understand the blender specific slash slash meaning of relative to the current blend file so we have to convert it to something that python understands then use that to find the obj files then convert it back to something blender understands and import the files first of all we have to turn that slash slash into something absolute and this is done through a function in bpy.path bpy.path.abspath or absolute path will give us the absolute path of the file that we want to import we can check this by reporting this in our error message just pause the video give this a try and then come back here now we have to convert this to a path object from pythons pathlib library and then we can use this import path variable to find all the obj files and loop over them in one go like this this will find everything that matches start.obj so files or directories ending in .obj now for now i will just assume that you won't create any sub directory called something.obj and that everything is an actual obj file that we can import now all we have to do is import it here in blender go to the obj importer and press ctrl c and then you can ctrl v in your code this converts that path object that we have back into a string so that blender understands it and then this whole line will import the obj file so now we have covered the for each file imported and the only thing that's left is recording its file name part of the working of this import operator is that it deselects everything and then it selects the objects that are imported and this means that we can loop over all selected objects and then assign our custom property to them and this is why i like the pathlib object so much it is quite simple to work with if you just want to have the file name component of a path you can just do the path .name if you want to have the drive letter you do .drive and is a very object oriented approach to paths and i quite like that now we're importing we record the file name where it came from so this should be it let's give it a try i'll throw out the existing objects browse to my obj files directory and then mass import everything and there we are the files are imported they're all at the origin and each of them has the obj file name set to whichever they came from now all that's left for us is to make an operator that reloads the obj file to keep it simple let's not do this on mass and just reload the active object that means deleting the object from the scene and then loading it again from the obj file of course before we delete it from the scene we have to make sure that we store the transform of the object so that when we reload it it can be restored so that it's positioned in the same way as it was before we reloaded it first let's start with a new operator let's call it obj reload here i select some text i press ctrl d and that will select next occurrence of that text so then i can just type reload and both the class name and the id name where we change to to obj reload let's say reload mass imported obj and then we can write the execute function for this code i will assume that every obj has one object in it you can extend the code so that it can handle multiple objects per obj but let's keep it simple for now we're going to refer to the active object quite a few times so let's make life simple for us and just call it ob so let's make a little overview for ourselves again first we have to store what we want to remember remove the object from the scene load the obj file and restore what we remembered loading the obj file will not automatically set our custom property in the end that's what we want to restore and we want to be able to restore the transform as well let's remember the file name and let's take a copy of the matrix if i wouldn't make a copy of the matrix then the variable matrix world would be a reference to the objects matrix and if we delete the object that reference won't be valid anymore and you can get all kinds of weird results now removing the object from the scene actually means removing the object from all the collections that it's in fortunately blender gives us a list of all those collections this will unlink the object from all the collections but the problem is that of course well as soon as we unlink the object from the first collection this thing that we're looping over will change changing something while you're looping over it is never a good idea we can do this list op dot users collections and this will create a new list for us that new list is a copy so it won't be altered by removing the objects from those collections making this for loop safe if you want you can also remove the object from bpi data now but this is only safe if it's not referenced from anything else again blender to the rescue op dot users will give us the user account and that is that tiny little square that you see also with materials and meshes when you use them in different places throughout the blend file if this number is zero that means that the object is no longer used and we can actually remove it from blender's memory and there we go from this point on we can no longer refer to op because it's been removed from blender so to prevent us from ever using it again i just say del op this removes the op name from python's memory that means that if i say op dot location here python will scream at us because it doesn't know what op means anymore after this line it's just a little bit of security for ourselves so that we don't do something that's maybe no longer valid now we need to load the obj file again which we've done before so let's take a look at that code we'll have to do this trick again and then we have to do this line for just that one object so let's put this into a function that we can call from both places here i've created a new function that takes a scene and this means that it will return a path little path and i like this notation quite a lot because then you can see what something returns without having to dive in the code and then see what happens this is not something that will be checked by python so it's up to you to actually live up to your word this code is now pretty much the same as this and it returns a new path object that means that we can remove this line and we can just do mass import path context dot scene here i specifically passed this scene and not the context i have to fix a little mistake here i explicitly passed the scene and not the entire context it would have worked fine if we were to pass the context but then you wouldn't be able to see which part of the context was actually used by this function this is why i wanted to pass the scene itself and not the context of course we could have also passed context dot scene dot mass import path but that would make the call a little bit longer and i think this is a nice middle ground but of course you do with your code whatever you think is best now we have to code to get the import path we can copy this line into our reload operator and then we can get this line and put that here and now all we have to do is get from the import path which is a directory to the import file path which is a file name and the import file path is the import path slash mass import file name that we took a copy of up there path objects in python supports this slash notation it will work correctly automatically for any platform so on windows it uses the backslash on linux and on macOS it uses the forward slash now that we have constructed the file name we can pass it to the operator that loads the obj file now all that's left is to restore whatever we wanted to restore so again we have to loop over the selected objects and set the mass import file name now we have to restore the matrix we tell blender that the operator did his job and then we fix two typos namely here colon and here it has to be user collection not user collections finally what we have to do is register and unregister the operator and add a button to the panel we copy the class name we add it to this list we copy the blid name and add it to the panel save the file reload the script and there we have our button as you can see we click on it and as you use from importing obj files there is no active object anymore and the just imported objects are selected in this case susan now let's see if it really reload i put a new file in there cube dot obj and now it should replace susan with that cube and it does so there we have it our code is working we can mass load obj files and we can select individual objects to reload i will leave it as an exercise for you to make a mass reload operator that iterates over all the objects or maybe only all the selected objects i'll leave that up to you so this is it for this episode of scripting for artists this is not the last time we'll talk about custom properties but i think this video has gone on long enough already if you have any questions or comments leave them below and i will see you soon