Janet's Notes

Miscellaneous Articles Trying to be Useful


Project maintained by JanetRossini

File Folders

We’ve decided that our various input files should be kept in folders based in the user’s files, not inside the project, since new input objects will probably be created often, and the code will, we think, change much less often.

The basic plan is to have a folder structure like this:

~
  coaster
    data-in
    objects-in
    scripts-out

The data and objects folders hold input. The data-in folder is for point input such as scans of the land or other data sets that we may need to import, objects-in will hold the .blend files currently inside the project, and scripts-out will contain the LSL scripts we produce as part of the output.

I propose this morning to create some code that will centralize the knowledge of this structure, and verify that it is in place in the tests. I propose to have a little class that embodies the file naming rules, which I’ll put in the utils file, most likely.

class TestFolders:
    def test_coaster_path_exists(self):
        path = coaster_base_path()
        exists = os.path.isdir(path)
        assert exists, "folder 'coaster' not found"

    def test_data_in_path_exists(self):
        path = coaster_data_in_path()
        exists = os.path.isdir(path)
        assert exists, "folder 'coaster/data-in' not found""folder 'coaster' not found"

    def test_objects_in_path_exists(self):
        path = coaster_objects_in_path()
        exists = os.path.isdir(path)
        assert exists, "folder 'coaster/objects-in' not found"

    def test_scripts_out_path_exists(self):
        path = coaster_scripts_out_path()
        exists = os.path.isdir(path)
        assert exists, "folder 'coaster/scripts-out' not found"

Those tests are met by these new functions in utils:

def coaster_base_path():
    home = os.path.expanduser('~')
    return os.path.join(home, 'coaster')


def coaster_data_in_path():
    return os.path.join(coaster_base_path(), 'data-in')


def coaster_objects_in_path():
    return os.path.join(coaster_base_path(), 'objects-in')


def coaster_scripts_out_path():
    return os.path.join(coaster_base_path(), 'scripts-out')

This setup centralizes all the path information into one file, utils.py, and these four functions. We could argue that a little class would be even better but this will do for now. I don’t like the idea of using all class methods for this and instantiating a class every time we need a folder didn’t seem quite right.

I am probably mistaken about that.

Commit this. Now what? Let’s copy the blend files to object-in and change the import from file code to use the new folder.

Oops, I accidentally moved them out. That will give me a commit problem: I don’t want to commit to them being gone in this phase. Copy them back. OK, I have the .blend files all in the same place. (I notice that there are a couple of other .blend files in the project. Maybe we need another folder under coaster?)

Anyway we are here to change the folder we look at for the import from file operation. It says this now:

    def invoke(self, context, event):
        self.directory = project_data_path()
        context.window_manager.fileselect_add(self)
        return {"RUNNING_MODAL"}

Change that one line.

    def invoke(self, context, event):
        self.directory = coaster_objects_in_path()
        context.window_manager.fileselect_add(self)
        return {"RUNNING_MODAL"}

So far so good. This one deserves a manual test, as I can’t really test it very well with PyTest. Although … I almost could, but what I really care about is where it opens, not what it says. Committed.

Now to change the file writer. I think I have a blender file with exportable data. Let’s see what the code does.

The VtFileWriter takes a path and base_name as parameters. I notice that it names the output files with name_001 and so on. Using underbars in file names isn’t exactly recommended, but it works. We’ll leave that concern for another day. We only need to look to see who creates VtFileWriter and cause them to use the file names we want.

There is a test that writes to the desktop. We will let that ride. The real case is this one:

    def execute(self, context):
        ruler = activate_object_by_name('ruler')
        if ruler is None or ruler.type != "MESH" or 'ruler' not in ruler.name:
            self.report({'ERROR'}, 'You seem to have no ruler.')
            return {'CANCELLED'}
        bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)

        # Output geometry
        obj_eval = ruler.evaluated_get(bpy.context.view_layer.depsgraph)

        verts = obj_eval.data.vertices
        home = os.path.expanduser('~')
        filepath = os.path.join(home, 'coasterdata')
        basename = "test_data"
        size = 500
        writer = VtFileWriter(verts, filepath, basename, size)
        writer.write_files(basename, self.rcg_abs, self.rcg_bank)
        return {'FINISHED'}

All we have to do is change the filepath. Nice. Now to test. Again into Blender, this time to export. The LSL script files go where they should. Super! Commit.

What’s left? I think there are a couple of menu items that read input files. Yes, input empties and input nurbs path. Why these two? I do not know, but there they are.

Ah. They use that weird ImportHelper and do not have a default folder, though I think that I know how to change that. Let me try something … but no. I thought perhaps I could set filepath from the menu but that didn’t work. I think I’d have to set it in the helper class.

I’ll do some research. Can’t find anything useful. We’ll do as we did with the import from files and write our own invoke method. Let’s see how easy or hard that is. Here’s what works there:

class RCG_OT_importFromFile(bpy.types.Operator):
    "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'})
    filepath: bpy.props.StringProperty(subtype="FILE_PATH")
    directory: bpy.props.StringProperty(subtype="DIR_PATH")

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

    def invoke(self, context, event):
        self.directory = coaster_objects_in_path()
        context.window_manager.fileselect_add(self)
        return {"RUNNING_MODAL"}

    def execute(self, context):
        addon_dir = os.path.dirname(__file__)
        self.report({"INFO"}, "file path " + addon_dir)
        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"}, "adding " + 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"}

The adds are currently much more convoluted, with their custom.select stuff. Well, they are and they aren’t. If we just ignore the big Select... ImportHelper, the actual class is quite small:

class RCG_OT_inputEmpties(Operator):
    """ Input a series of empties from a text file """
    bl_idname = "rcg.inputempties"
    bl_label = "Input Empties from file"
    bl_options = {"REGISTER", "UNDO"}

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

    def execute(self, context):
        bpy.ops.custom.select_empties('INVOKE_DEFAULT')
        return {'FINISHED'}

However, all the logic is in the custom thingie. Can I just rewrite that not to be an ImportHelper? Let’s try that.

I just do this much:

class SelectFileEmpties(bpy.types.Operator):
    """Select a text file"""
    bl_idname = "custom.select_empties"
    bl_label = "Select File Empties"

    filename_ext = ".txt"

    directory: bpy.props.StringProperty(subtype="DIR_PATH")
    filter_glob: StringProperty(default="*.txt", options={'HIDDEN'})

    def invoke(self, context, event):
        self.directory = coaster_data_in_path()
        context.window_manager.fileselect_add(self)
        return {"RUNNING_MODAL"}

I added the directory props, and changed the invoke. Now the file opens where I want it to, and as far as I know, everything else is the same. I’ll make the same change to the nurbs case and ask for assistance in testing.

Let’s commit and see if DS can test for us.

Summary

I think we have it all working. The two add objects are presently done with two classes, a very basic one that uses the custom select that DS found in an alley someplace. I’ve just changed the Select ones, and I think it’s working, but those four classes should probably be collapsed into two.

But a good step forward, I think.