Given the updated code you provided I’ll start by going through some of the changes I made and attempt to explain why and provide the full code at the bottom of the post. Please note I am not a programmer just someone that tinkers with Blender.
bl_info:
"category": "File",
"support": "TESTING",
Providing a category allows the end user a quick filter in the addons for finding types of addons. This is the drop-down menu when enabling an add-on. I chose File since this add-on creates a new file based on information but does not actually export models etc.
The use of "support": "TESTING"
is a personal preference that I use so that I can quickly find an addon even if the name is incorrect. If you are happy with your work you can at any time comment out or delete the "support": "TESTING"
line and the addon will show in the community tab at the top of the add-ons panel.
importing modules:
Generally it is preferred to maintain all your imports at the top of your add-on.
import bpy
import os
from bpy.utils import register_class, unregister_class
Properties:
While you can add individual properties I prefer to use property groups. This helps to prevent even common naming conventions of variables from interfering with eachother.
class PARTSLIST_PG_props(bpy.types.PropertyGroup):
op_dir: bpy.props.StringProperty(
name = "op_dir",
description = "Name of output directory",
default = os.path.join(os.path.expanduser("~"), "Desktop"),
subtype = 'DIR_PATH',)
op_fname: bpy.props.StringProperty(
name = "op_fname",
description = "Name of output file",
default = "Parts_List.txt",
subtype = 'FILE_NAME',)
By utilizing subtypes of string properties gain additional implementation controls without additional coding.
Adding poll method:
class PARTSLIST_OT_export(bpy.types.Operator):
"""Tooltip"""
bl_idname = "partslist.export"
bl_label = "Parts List Export"
@classmethod
def poll(cls, context):
props = context.window_manager.partslist_pg
return os.path.isdir(props.op_dir)
The poll method is used to verify conditions are met prior to allowing the operator to run. In this case I verify that the directory exists prior to attempting to create the file. MS Windows does not like when you try to create a file in a folder that does not exist 1st. I believe the pathlib module handles this better but you already had the os module imported and setup. When the poll condition is not met the operator button becoms greyed out and non functional until the poll condition is satisfied. In this case a pre-existing directory.
Naming:
https://wiki.blender.org/wiki/Reference/Release_Notes/2.80/Python_API/Addons
Try to ensure you use names that are both easy enough to identify intended operation and unique enough to prevent collision with other add-ons.
_classes = [
PARTSLIST_PG_props,
PARTSLIST_OT_export,
PARTSLIST_PT_main_panel,]
Code:
bl_info = {
"name": "Export Parts List",
"author": "John Doe",
"version": (1, 0),
"blender": (2, 80, 0),
"location": "View3D > N",
"description": "Exports the VISIBLE objects in the active scene and saves the into a .txt file structured to be imported in the Skyline Estimate Sheet",
"warning": "",
"doc_url": "",
"category": "File",
"support": "TESTING",
}
import bpy
import os
from bpy.utils import register_class, unregister_class
class PARTSLIST_PG_props(bpy.types.PropertyGroup):
op_dir: bpy.props.StringProperty(
name = "op_dir",
description = "Name of output directory",
default = os.path.join(os.path.expanduser("~"), "Desktop"),
subtype = 'DIR_PATH',)
op_fname: bpy.props.StringProperty(
name = "op_fname",
description = "Name of output file",
default = "Parts_List.txt",
subtype = 'FILE_NAME',)
class PARTSLIST_OT_export(bpy.types.Operator):
"""Tooltip"""
bl_idname = "partslist.export"
bl_label = "Parts List Export"
@classmethod
def poll(cls, context):
props = context.window_manager.partslist_pg
return os.path.isdir(props.op_dir)
def execute(self, context):
props = context.window_manager.partslist_pg
file_path = os.path.join(props.op_dir, props.op_fname)
f = open(file_path, "w")
obj_data = {}
for obj in context.visible_objects:
if obj is None:
continue
obj_name = obj.name.split(".")[0]
if obj.data is None:
continue
obj_data_name = obj.data.name.split(".")[0]
if obj_name in obj_data:
obj_data[obj_name]["count"] += 1
else:
obj_data[obj_name] = {"name": obj_data_name, "count": 1}
for obj_name, data in obj_data.items():
f.write(f'{data["name"]};{obj_name};{data["count"]}\n')
f.close()
return {'FINISHED'}
class PARTSLIST_PT_main_panel(bpy.types.Panel):
bl_label = "Parts List"
bl_idname = "partslist.main_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Parts List Export"
def draw(self, context):
props = context.window_manager.partslist_pg
layout = self.layout
row = layout.row()
row.prop(props, "op_dir")
row = layout.row()
row.prop(props, "op_fname")
row = layout.row()
row.operator(
PARTSLIST_OT_export.bl_idname,
text = "Export",
icon = 'IMPORT')
_classes = [
PARTSLIST_PG_props,
PARTSLIST_OT_export,
PARTSLIST_PT_main_panel,]
def register():
for cls in _classes:
register_class(cls)
bpy.types.WindowManager.partslist_pg = bpy.props.PointerProperty(
type = PARTSLIST_PG_props)
def unregister():
for cls in _classes:
unregister_class(cls)
del bpy.types.WindowManager.partslist_pg
if __name__ == "__main__":
register()