Hello and welcome to Scripting for Artists. My name is Sybren.
And in this episode, we're going to take a look at Custom Properties.
In the last video, we created a panel
and add existing properties to it. And this is already quite powerful.
If that is enough for your situation, then please
don't make it any more complex. 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 Define,
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 toward, will 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 Addon and I've started already.
I've added a new Python file. I've installed it
as an Addon, load it in my Editor, which is a Visual Studio code.
I will quickly race through this
because you've seen all of these before in the previous videos.
If you haven't seen this yet, please take a look at the videos
about creating your own Addon and creating User Interfaces.
So we start with the BL Info Dictionary. I've set the name
to the Addon to Scripting for Artists "Mass importer".
The rest of the information is pretty much as you would expect,
"import bpy", of course, is 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 a monkey grid and then adjusting it.
So it's pretty the same, except that it has the label "mass import",
and that is also reflected in the class name.
It's now called "view 3D PT 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 Addon. We're going to add more classes to this file.
So we'll have an operator for doing the mass import.
Then we also will 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 "bpy.utils.unregister class", and then make sure that
in the unregistered, is 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 Forloop. So for Blender Class and Blender Classes
will go over this list. And for every item in that list,
it will call "bpy.utils.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 added on a basis that actually works.
So in the User Preferences, enable the Addon. And then here,
in our Monkeys tab, we can see our Mass Import panel.
Of course, it's nothing drawn yet, but that is what we expected.
So on the basis for our code, the registering and the unregistering
in 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 "bpy.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 "bpy.types.Scene", with a capital Scene,
"." 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 class 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 ":",
we use the "=" sign, and then again, we assign property.
They'll even be a path of 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 they always end up with a ",",
because then, I can add stuff on the next line without having
to go back up and add that "," again. This is valid Python,
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 a 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.
Let's see how this looks in Blender.
Here, we have to reload our code. In Blender 2.83 and newer,
can do that here with Reload Scripts. If you've an older version of Blender,
press F3,
then type"reload scripts". They both do the same thing.
Except, 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.
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. 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 work with your mouse a little bit to make it redraw.
But you can see that, now, with that, we've 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 Subtypes.
And this will indicate to Blender
what kind of String we're talking about. So this is going to be a path,
and it would 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've set the Subtype correctly.
Let's do that. The Subtype is set
by the Subtype parameter, and then you pass a string
that indicates a Subtype. In order to find the Subtypes for a specific property,
you can go back to Blender, Help,
Python, API Reference, and search for "bpy.props.string property"
and there it will tell you which subtype there are.
In our case, it's a Directory Path or "Dir Path".
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'll just start at a silly directory, because it's not a valid path.
But if I change this to "//" 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.
That is exactly what we wanted, except that we want this behavior
in our own panel, of course. Let's do that. In our Panel code,
we can remove the pass because we actually going to do something
and let's start with getting the layout.
We didn't do this before, but do know that every "self.(something)"
we'll do a little dance to see, is it defined on this class,
if not, look at a parent, is it defined there,
if not, look at the grandparent etc.. So we're saying
"layout = self.layout" , we only do this dance once,
and then just use the layout object. Let's add a Column,
and then add the Property. The Property was defined on the scene,
which is "context.scene". And the property name was
"mass import path". That's it.
Reload the code, and there we have our Property browse button.
Now, let's go and create an operator that import 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 "bpy.ops.import_scene.obj"
so let's call our operator
"import scene.obj mass". So here, we have an empty operator,
you've seen this before, I just named these differently,
called "imports scene.obj mass" and it has an Execute function.
I put in this little comment as a reference, and then it called
"self.report". This takes a report type which can be
info or warning or error, and then a string that is unreported.
It returns "cancelled" because nothing is done yet.
Again, I'm taking small steps. I first make sure that your printer is there,
and that it 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 copied this name, I go to the Blender classes list,
I added the name to the list and that takes care of the Registering
and the unregistering. So what is left now is add it to the panel.
So all we need is the "BL ID name". Paste it in, save in Blender,
reload the script. And we have a button, we click on it.
It says "no code to load from / /", which is the error 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 ObjectTtype. So let's do that now
before we start 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.
Will just keep this as a String and don't define any subtype.
So before we look at the importing, that'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 objects is considered true.
So if "context.object" will evaluate to "true",
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 objects",
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 steps,
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 "//" 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 "//" into something absolute,
and this is done through a function in "bpy.path.abspath"
"bpy.path.abspath(context.scene.mass_import_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 Python's Path Lib 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 "*.obj",
so files or directories ending in that "obj".
From now on, I'll assume that you won't create any subdirectory called
"(something).obj", and that everything is an actual OBJ file we import.
Now, all we have to do is import it.
In Blender,
go to the OBJ Importer and press Ctrl C, and then,
you can Ctrl V, Code.
This converts that Path Object 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 that for each file imported,
the only thing that's left is recording its filename.
Part of the working of this Import Operator is that
it deselects everything and then it selects the objects that are imported.
This means that we can loop over all selected objects
and then assign our Custom Property to them.
This is why I like the Path Lib Object so much.
It's quite simple to work with if you just want to have
the filename component of a path, you can just do the "path.name".
If you want to have the drive object, 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 filename 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,
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 deleted from the scene, we have to make sure that
we stored 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.
Let's start with a new operator.
Let's call it "obj reload". Here, I select some text,
press Ctrl D and that will select next occurrence of that text.
Then, I can just type Reload, and both the class name and the ID name
we'll change 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 for 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.
Just call it "ob", 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 Object Matrix. And if we delete the object,
that reference won't be valid anymore,
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.
The problem is that, of course, 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(ob.user_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 "bpy.data" now,
but this is only safe if it's not referenced from anything else.
Again, Blender to the rescue.
"ob.users" will give us that user count.
And that is that tiny little square you see also with Materials and Meshes,
when you use them in different places throughout the Blend file.
If this number is 0, that means that the object is no longer used,
and we can actually remove it from Blender's memory.
From this point on, we can no longer refer to "ob",
because it's been removed from Blender.
So to prevent this from ever using it again,
I just say "del ob". This removes the "ob" name from Python's memory.
That means that, if I say "ob.location" here,
Python will scream at us, because it doesn't know
what "ob" means anymore after this line. It's just a little bit of security for ourselves
so that we don't use something
that's 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 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
which 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 lib.path".
I like this notation quite a lot, because then, you can see
what something returns without having to dive in the code
and see what happens. This is not something that will be checked at Python,
so it's up to you to actually live up to your work.
This code is now pretty much the same as this
and it returns the new Path Object. That means that we can remove this line,
and we can just do "mass_import_path(context.scene)" here.
I specifically pass this scene
and not the context. 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 a scene itself and not the context.
Of course, we could have also passed "context.scene(mass import path)",
but that would make the call of it a little bit longer,
I think this is a nice middle ground. But of course, you do with your code
whatever you think is best. Now, we have the 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 / mass_import_fname" that we took a copy of up there.
Path Object in Python supports this "/" notation.
It wil work correctly automatically for any platform.
On Windows, it uses the back slash, on Linux and Mac OS, it uses the "/".
Now that we have constructed the file name,
can pass it to the operator that loads the OBJ files.
Now, all that's left is to restore whatever we wanted to restore.
So again, we have to loop over the selected object,
and set the Mass Import file name.
Now, we have to restore the Matrix.
Will tell Blender that the operator did its job.
And then we fix two typos. Here ":", and here, has to use "collection",
not "collections".
Finally, what we have to do is Register and Unregister the operator
and add a button to the panel.
We copied the class name, we added to this list,
We copy the BL ID name and added 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,
just imported objects are selected, in this case, Suzanne.
Now, let's see if it really reloads.
I put a new file in there "cube.obj", and now it should replace Suzanne
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
but I think this video or comments, leave them below,
and I will see you soon.