Janet's Notes

Miscellaneous Articles Trying to be Useful


Project maintained by JanetRossini

Import from File

I begin by adding a new label and button to the menu:

        row = col.row()
        row.operator("rcg.importobject", text="Normal").rcg_file = "track"
        row.operator("rcg.importobject", text="Inverted").rcg_file = "invtrack"
        row.operator("rcg.importobject", text="Railway").rcg_file = "ngtrack"
        col.label(text="Import from file", icon='ANIM')
        col.operator("rcg.importfromfile", text="Import from File")
        col.label(text="Add a track ruler", icon='ARROW_LEFTRIGHT')

I probably don’t really need both the label and the button, but I am copying existing patterns here. We’ll fly on our own maybe later.

With that in place, the label shows up but not the button: we have nothing registered under that name. I create a new class, now coping from one of the Select... classes:

class RCG_OT_importfromfile(bpy.types.Operator, ImportHelper):
    "Add object from file"
    bl_idname = "rcg.importfromfile"
    bl_label = "Import"
    bl_options = {"REGISTER", "UNDO"}

    filename_ext = '*.blend'
    filter_glob: StringProperty(default="*.blend", options={'HIDDEN'})

    @classmethod
    def poll(cls, context):
        return context.mode == "OBJECT"

    def execute(self, context):
        file_path = self.filepath
        return {"FINISHED"}

Adding that class name to the register list, we get our button,

button in panel

and if we click the button, we get a file browser that shows .blend files:

file browser

Of course, if we select a file, nothing happens … yet.

I believe that the rule we want to follow is that each .blend file will contain a single named object, and the name of the object will be the same as the name of the file. So that tiger.blend will contain an object named tiger. My plan now is to crib from one of the existing imports, and, with a little luck, perhaps even use it. Failing that, I think we might have an opportunity to build a new helper class to handle all the imports.

As a first step, and it is kind of a big one, I’ll do all the code in this new class or in other classes if I need them. Then I’ll deal with the common elements from the existing imports later.

Here’s the code from the existing buttons:

    def execute(self, context):
        if self.rcg_file != "":
            name = self.rcg_file
        else:
            name = "track"
            self.report({"WARNING"}, "defaulting to " + name)
        self.report({"INFO"}, "adding " + name)
        filepath, directory, filename = make_elements(name)
        bpy.ops.wm.append(filepath=filepath,
                          directory=directory,
                          filename=filename)
        track = bpy.data.objects[name]
        track.select_set(state=True, view_layer=bpy.context.view_layer)
        bpy.context.view_layer.objects.active = track
        return {'FINISHED'}

The rcg_file will be set to “track”, “invtrack”, “ngtrack”, or “trackruler” by the existing buttons. So that is the file name without extension. I assume that what we get from the file browser will include the extension and other info. I’ll find out with an INFO that I can read in Blender.

    def execute(self, context):
        file_path = self.filepath
        self.report({"INFO"}, file_path)
        return {"FINISHED"}

No big surprise, that tells me that file_path is the full path to the file, all the way back to the drive.

From the code in the existing buttons, shown above, I’ll need the file path as is, and it seems that I can work out from make_elements, what the other bits are.

def make_elements(name):
    home = os.path.expanduser('~')
    working = os.path.join(home, 'PycharmProjects', 'coaster',  'coasterobjects')
    filepath = os.path.join(working, name + '.blend')
    directory = os.path.join(filepath, 'Object')
    filename = name
    return filepath, directory, filename

I think that means that we want to have filepath be the full path that we have in our case, the directory be the same joined with Object, and the “filename” to be the file name without “.blend”.

I’m going to write a little test to work out how to do that.

    def test_make_filepath_directory_name(self):
        file_path = 'C:/mumble/foo/fragglerats.blend'
        path, file = os.path.split(file_path)
        assert file == 'fragglerats.blend'
        name, ext = os.path.splitext(file)
        assert name == 'fragglerats'
        assert ext == '.blend'

That passed right away and we know what we have to do. I’ll write the code in line:

No, wait, I didn’t do the directory part yet. Back to the test:

    def test_make_filepath_directory_name(self):
        file_path = 'C:/mumble/foo/fragglerats.blend'
        path, file = os.path.split(file_path)
        assert file == 'fragglerats.blend'
        name, ext = os.path.splitext(file)
        assert name == 'fragglerats'
        assert ext == '.blend'
        directory = os.path.join(file_path, 'Object')
        assert directory == 'C:/mumble/foo/fragglerats.blend/Object'

I think that’s right but I want to double check before I even do it, so I add some info things to the existing import, and yes, I have it right. So to import …

    def execute(self, context):
        file_path = self.filepath
        directory = os.path.join(file_path, 'Object')
        path, file_with_ext = os.path.split(file_path)
        filename, ext = os.path.splitext(file_with_ext)
        self.report({"INFO"}, file_path)
        bpy.ops.wm.append(filepath=filepath,
                          directory=directory,
                          filename=filename)
        track = bpy.data.objects[name]
        track.select_set(state=True, view_layer=bpy.context.view_layer)
        bpy.context.view_layer.objects.active = track
        return {"FINISHED"}

I actually think this might work. I got ahead of myself. There were squiggles in the file telling me my pasting didn’t go well.

    def execute(self, context):
        file_path = self.filepath
        directory = os.path.join(file_path, 'Object')
        path, file_with_ext = os.path.split(file_path)
        filename, ext = os.path.splitext(file_with_ext)
        self.report({"INFO"}, file_path)
        bpy.ops.wm.append(filepath=file_path,
                          directory=directory,
                          filename=filename)
        item = bpy.data.objects[filename]
        item.select_set(state=True, view_layer=bpy.context.view_layer)
        bpy.context.view_layer.objects.active = item
        return {"FINISHED"}

I definitely feel hopeful now. And it works. I import a track and an inverted track successfully. They rez at 0 as requested.

This is simple enough that I don’t feel the need to remove the duplication with the other import, at least not right now. I’ve consumed enough brain energy to want to leave that for another time, perhaps never. I do think I’ll give the unused components underbar names so as to make it a bit more clear what matters and what does not. There are six lines duplicated between this and the other add operation.

I modify the menu a bit but leave the details to be figured out together with DS.

There might be improvements referring to the Select operations that are in there. The code that most needs improvement, to my eyes, is the code that adds support, which has two or more big long blobs of code.

All that is for another day. We commit and push.