├── export ├── __init__.py ├── __pycache__ │ └── constants.cpython-311.pyc ├── constants.py ├── meta.py ├── properties.py ├── ui.py ├── funcs.py └── operators.py ├── icons ├── cube.png ├── wall.png ├── coffee.png ├── export.png ├── github.png ├── hanger.png ├── printer_3d.png ├── text_long.png ├── account_tie.png ├── format_title.png ├── image_frame.png ├── sofa_outline.png ├── table_furniture.png ├── file_account_outline.png ├── text_box_edit_outline.png └── format_list_bulleted_type.png ├── .github └── FUNDING.yml ├── blender_manifest.toml ├── __init__.py ├── README.md ├── preferences.py ├── icons_load.py └── auto_load.py /export/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/cube.png -------------------------------------------------------------------------------- /icons/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/wall.png -------------------------------------------------------------------------------- /icons/coffee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/coffee.png -------------------------------------------------------------------------------- /icons/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/export.png -------------------------------------------------------------------------------- /icons/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/github.png -------------------------------------------------------------------------------- /icons/hanger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/hanger.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | ko-fi: mrvicho13 3 | -------------------------------------------------------------------------------- /icons/printer_3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/printer_3d.png -------------------------------------------------------------------------------- /icons/text_long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/text_long.png -------------------------------------------------------------------------------- /icons/account_tie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/account_tie.png -------------------------------------------------------------------------------- /icons/format_title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/format_title.png -------------------------------------------------------------------------------- /icons/image_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/image_frame.png -------------------------------------------------------------------------------- /icons/sofa_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/sofa_outline.png -------------------------------------------------------------------------------- /icons/table_furniture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/table_furniture.png -------------------------------------------------------------------------------- /icons/file_account_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/file_account_outline.png -------------------------------------------------------------------------------- /icons/text_box_edit_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/text_box_edit_outline.png -------------------------------------------------------------------------------- /icons/format_list_bulleted_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/icons/format_list_bulleted_type.png -------------------------------------------------------------------------------- /export/__pycache__/constants.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hancapo/Inzoider/HEAD/export/__pycache__/constants.cpython-311.pyc -------------------------------------------------------------------------------- /export/constants.py: -------------------------------------------------------------------------------- 1 | CRAFT_TYPES=[ 2 | ('Build', "Build", "3D Print for Build Studio"), 3 | ('Character', "Character", "3D Print for Character Studio"),] -------------------------------------------------------------------------------- /blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "1.0.0" 2 | id = "inzoi_prop_exporter" 3 | version = "1.0.0" 4 | name = "Inzoider" 5 | tagline = "A tool to export placeable models to inZOI" 6 | maintainer = "MrVicho13 " 7 | type = "add-on" 8 | tags = ["Render", "Import-Export"] 9 | blender_version_min = "4.2.0" 10 | website = "https://github.com/Hancapo/Inzoider" 11 | 12 | 13 | license = ["SPDX:MIT",] 14 | 15 | platforms = ["windows-x64","linux-x64"] 16 | 17 | [permissions] 18 | files = "Export Folders and Binary Files (.BMP, .JPG, .JSON, .GLB) to disk" 19 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | def reload_inzoider(): 2 | import sys 3 | print("Reloading Inzoider") 4 | 5 | 6 | global auto_load 7 | del auto_load 8 | inzoider_module_prefix = f"{__package__}." 9 | module_names = list(sys.modules.keys()) 10 | for name in module_names: 11 | if name.startswith(inzoider_module_prefix): 12 | del sys.modules[name] 13 | 14 | if "auto_load" in locals(): 15 | reload_inzoider() 16 | 17 | from . import icons_load 18 | from . import auto_load 19 | 20 | auto_load.init() 21 | 22 | def register(): 23 | icons_load.init_icons() 24 | icons_load.load_icons() 25 | auto_load.register() 26 | 27 | def unregister(): 28 | auto_load.unregister() 29 | icons_load.unregister_icons() -------------------------------------------------------------------------------- /export/meta.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | class Meta: 4 | def __init__(self, PivotLocation, PivotRotation, PrintedMeshExtent, Title, Description) -> None: 5 | """Initializes the meta data.""" 6 | self.PivotLocation = PivotLocation 7 | self.PivotRotation = PivotRotation 8 | self.PrintedMeshExtent = PrintedMeshExtent 9 | self.Title = Title 10 | self.Description = Description 11 | 12 | 13 | def to_json(self) -> str: 14 | """Converts the meta data to a JSON string.""" 15 | 16 | meta_dict = { 17 | "PivotLocation": self.PivotLocation, 18 | "PivotRotation": self.PivotRotation, 19 | "PrintedMeshExtent": self.PrintedMeshExtent, 20 | "Title": self.Title, 21 | "Description": self.Description 22 | } 23 | 24 | return json.dumps(meta_dict, indent=4) 25 | 26 | 27 | def export_to_file(self, filename) -> bool: 28 | """Exports the meta data to a file.""" 29 | json_data = self.to_json() 30 | 31 | with open(filename, 'w+') as file: 32 | file.write(json_data) 33 | file.close() 34 | 35 | return True -------------------------------------------------------------------------------- /export/properties.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .funcs import change_selection 3 | from .constants import CRAFT_TYPES 4 | 5 | class CraftItem(bpy.types.PropertyGroup): 6 | type: bpy.props.EnumProperty(name="Type", items=CRAFT_TYPES) 7 | mesh: bpy.props.PointerProperty(name="Mesh", type=bpy.types.Object) 8 | thumbnail: bpy.props.PointerProperty(name="Thumbnail", type=bpy.types.Image) 9 | title: bpy.props.StringProperty(name="Title", default="") 10 | description: bpy.props.StringProperty(name="Description", default="") 11 | enable_editing: bpy.props.BoolProperty(name="Enable Editing", default=False, description="Enable editing of the craft's properties") 12 | 13 | def register(): 14 | bpy.types.Scene.obj_craft_list = bpy.props.CollectionProperty(type=CraftItem) 15 | bpy.types.Scene.obj_craft_index = bpy.props.IntProperty(name="Craft", default=0, description="Index of the selected craft", update=change_selection) 16 | bpy.types.Scene.inzoider_export_all = bpy.props.BoolProperty(name="Export All", default=False, description="Export all crafts in the list") 17 | bpy.types.WindowManager.current_craft_image_path = bpy.props.StringProperty() 18 | bpy.types.WindowManager.current_craft_texture_name = bpy.props.StringProperty() 19 | 20 | def unregister(): 21 | del bpy.types.Scene.obj_craft_list 22 | del bpy.types.Scene.obj_craft_index 23 | del bpy.types.Scene.inzoider_export_all 24 | del bpy.types.WindowManager.current_craft_image_path 25 | del bpy.types.WindowManager.current_craft_texture_name -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inzoider Extension for inZOI 2 | 3 | ### Transform your Blender creations into custom 3D prints for inZOI 4 | 5 | ![Inzoider Extension Preview](https://github.com/user-attachments/assets/13ec951a-6687-43ca-b90a-712f35bfd40d) 6 | 7 | ## [⬇️ Download Latest Release](https://github.com/Hancapo/Inzoider/releases/latest) 8 | 9 | ## Requirements 10 | - inZOI game 11 | - Blender 4.2+ 12 | - 3D modelling and general Blender knowledge. 13 | 14 | ## Installation 15 | 1. Download the extension: 16 | - Get the latest release from the `Tags` section (Recommended) 17 | 2. Open Blender and navigate to `Edit > Preferences... > Add-ons` 18 | 3. Click the top-right arrow button and select `Install from Disk...` 19 | 4. Locate and select the downloaded ZIP file 20 | 5. Enable the add-on by checking its box in the list 21 | 22 | ## How to Use 23 | 24 | ### Setup 25 | 1. In the add-on preferences, set the 3D Printer path to: 26 | ``` 27 | C:\Users\%USERNAME%\Documents\inZOI\AIGenerated\My3DPrinter 28 | ``` 29 | > ℹ️ **Information:** `%USERNAME%` refers to your own Windows' username. 30 | 2. Generate at least one 3D print from inZOI 31 | 3. The add-on will use these files as templates for your custom creations 32 | 4. Select a ```printed.dat``` file from the previous path and load it. 33 | 34 | ### Exporting a Model 35 | 1. Prepare your 3D model in Blender: 36 | - Model or Download your model. 37 | - Apply textures to your object. 38 | - Ensure uniform scale `(1,1,1)` and rotation `(0,0,0)` (apply both if needed) 39 | - Set an proper pivot for your model. For example, if you are making a sofa, the pivot should be in the lower-middlemost part of the model. 40 | 3. Select your object in the scene 41 | 4. Open the side panel with the `N` key 42 | 5. Navigate to the "Inzoider" tab 43 | 6. Create a new craft by clicking the `+` button 44 | 7. Fill out all required fields 45 | 8. Select your craft from the list 46 | 9. Click "Export" 47 | 10. Launch inZOI to see your creation in-game 48 | 49 | > ⚠️ **Note:** This tool is still experimental. Please report any issues you encounter. 50 | -------------------------------------------------------------------------------- /preferences.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .icons_load import get_icon 3 | from .export.funcs import get_author_from_printed_file, file_exists 4 | from .export.operators import UnloadPrintedFile_OT_Operator 5 | 6 | class InzoiderExtensionProperties(bpy.types.AddonPreferences): 7 | bl_idname = __package__ 8 | 9 | my3dprinter_path : bpy.props.StringProperty( 10 | name="3DPrinter Path", 11 | description="Path to the inZOI's 3d prints directory", 12 | default="", 13 | subtype='DIR_PATH', 14 | ) 15 | 16 | example_printed_file_path : bpy.props.StringProperty( 17 | name="printed.dat File", 18 | description="Path to the example printed file", 19 | default="", 20 | subtype='FILE_PATH', 21 | ) 22 | 23 | detected_author : bpy.props.StringProperty( 24 | name="Detected Author", 25 | description="Detected author from the printed.dat file", 26 | default="", 27 | ) 28 | 29 | is_printed_file_loaded : bpy.props.BoolProperty( 30 | name="Is Printed File Loaded", 31 | description="Whether the printed.dat file is loaded or not", 32 | default=False, 33 | ) 34 | 35 | def draw(self, context): 36 | layout = self.layout 37 | self.is_printed_file_loaded = file_exists(self.example_printed_file_path) 38 | header, panel = layout.panel("inzoider_settings", default_closed=True) 39 | header.label(text="3D Prints", icon_value=get_icon("wall")) 40 | if panel: 41 | panel.prop(self, "my3dprinter_path", icon_value=get_icon("printer_3d")) 42 | if not self.is_printed_file_loaded: 43 | panel.prop(self, "example_printed_file_path", icon_value=get_icon("file_account_outline")) 44 | panel.label(text="No printed.dat file has been loaded, author not available", icon_value=get_icon("account_tie")) 45 | else: 46 | row_panel = panel.row(align=True) 47 | row_panel.alignment = 'CENTER' 48 | row_panel.label(text="Printed.dat file loaded", icon_value=get_icon("text_box_edit_outline")) 49 | self.detected_author = get_author_from_printed_file(self.example_printed_file_path) if self.is_printed_file_loaded else "" 50 | if self.detected_author: 51 | row_panel.operator(UnloadPrintedFile_OT_Operator.bl_idname, text="", icon="PANEL_CLOSE") 52 | row_panel.separator() 53 | row_panel.label(text=f'Author: {self.detected_author}', icon_value=get_icon("account_tie")) 54 | 55 | 56 | def get_addon_prefs() -> InzoiderExtensionProperties: 57 | return bpy.context.preferences.addons[__package__].preferences 58 | 59 | def register(): 60 | bpy.utils.register_class(InzoiderExtensionProperties) 61 | 62 | def unregister(): 63 | bpy.utils.unregister_class(InzoiderExtensionProperties) -------------------------------------------------------------------------------- /icons_load.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from bpy.utils import previews 4 | 5 | pcoll = None 6 | preview_collections = {} 7 | 8 | def init_icons(): 9 | """Initializes the icon collections.""" 10 | global pcoll, preview_collections 11 | if preview_collections: 12 | for coll in preview_collections.values(): 13 | bpy.utils.previews.remove(coll) 14 | preview_collections.clear() 15 | 16 | pcoll = previews.new() 17 | preview_collections["main"] = pcoll 18 | return pcoll 19 | 20 | def ensure_icons_loaded(): 21 | """Checks if the icons are loaded, and if not, loads them.""" 22 | global pcoll, preview_collections 23 | if pcoll is None or not preview_collections.get("main"): 24 | init_icons() 25 | load_icons() 26 | return pcoll 27 | 28 | def load_icons(): 29 | """Loads all custom icons.""" 30 | global pcoll 31 | 32 | if pcoll is None: 33 | init_icons() 34 | 35 | icons_dir = os.path.join(os.path.dirname(__file__), "icons") 36 | print(f"Loading icons from: {icons_dir}") 37 | 38 | icons = [ 39 | ["account_tie", "account_tie.png"], 40 | ["coffee", "coffee.png"], 41 | ["cube", "cube.png"], 42 | ["export", "export.png"], 43 | ["format_title", "format_title.png"], 44 | ["github", "github.png"], 45 | ["image_frame", "image_frame.png"], 46 | ["printer_3d", "printer_3d.png"], 47 | ["sofa_outline", "sofa_outline.png"], 48 | ["table_furniture", "table_furniture.png"], 49 | ["text_box_edit_outline", "text_box_edit_outline.png"], 50 | ["text_long", "text_long.png"], 51 | ["wall", "wall.png"], 52 | ["hanger", "hanger.png"], 53 | ["format_list_bulleted_type", "format_list_bulleted_type.png"], 54 | ["file_account_outline", "file_account_outline.png"], 55 | ] 56 | 57 | for name, filename in icons: 58 | if name in pcoll: 59 | print(f"Icon '{name}' already exists, skipping") 60 | continue 61 | 62 | icon_path = os.path.join(icons_dir, filename) 63 | if os.path.exists(icon_path): 64 | pcoll.load(name, icon_path, 'IMAGE') 65 | else: 66 | print(f"Icon not found: {icon_path}") 67 | 68 | print(f"Loaded icons: {list(pcoll.keys())}") 69 | return pcoll 70 | 71 | def get_icon(icon_name, fallback="QUESTION"): 72 | """Safely obtains an icon ID.""" 73 | global pcoll 74 | ensure_icons_loaded() 75 | try: 76 | return pcoll[icon_name].icon_id 77 | except (KeyError, AttributeError): 78 | print(f"Warning: Icon '{icon_name}' not found, using fallback") 79 | return fallback 80 | 81 | def unregister_icons(): 82 | """Removes all icon collections.""" 83 | global pcoll, preview_collections 84 | for coll in preview_collections.values(): 85 | bpy.utils.previews.remove(coll) 86 | preview_collections.clear() 87 | pcoll = None -------------------------------------------------------------------------------- /export/ui.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .operators import AddCraftItem_OT_Operator, ExportCraft_OT_Operator, RemoveCraftItem_OT_Operator 3 | from ..icons_load import get_icon 4 | from ..preferences import get_addon_prefs 5 | 6 | class CRAFT_UL_list(bpy.types.UIList): 7 | bl_idname = "CRAFT_UL_list" 8 | 9 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 10 | prefs = get_addon_prefs() 11 | scene = context.scene 12 | row = layout.row() 13 | row.label(text=f"{item.title} by {prefs.detected_author}", icon_value=get_icon("sofa_outline")) 14 | match item.type: 15 | case 'Build': 16 | row.label(text="Build", icon_value=get_icon("wall")) 17 | case 'Character': 18 | row.label(text="Character", icon_value=get_icon("hanger")) 19 | 20 | 21 | class InzoiderCraftExport_PT_Panel(bpy.types.Panel): 22 | bl_label = "3D Printer" 23 | bl_idname = "InzoiderCraftExport_PT_Panel" 24 | bl_space_type = 'VIEW_3D' 25 | bl_region_type = 'UI' 26 | bl_category = 'Inzoider' 27 | bl_options = {'DEFAULT_CLOSED'} 28 | 29 | def draw_header(self, context): 30 | self.layout.label(text="", icon_value=get_icon("printer_3d")) 31 | 32 | def draw(self, context): 33 | scene = context.scene 34 | layout = self.layout 35 | prefs = get_addon_prefs() 36 | if prefs.is_printed_file_loaded: 37 | row = layout.row() 38 | col = row.column() 39 | col.operator(AddCraftItem_OT_Operator.bl_idname, text="", icon='ADD') 40 | col.operator(RemoveCraftItem_OT_Operator.bl_idname, text="", icon='REMOVE') 41 | col.separator() 42 | col.operator(ExportCraft_OT_Operator.bl_idname, text="", icon_value=get_icon("export")) 43 | col.separator() 44 | row = row.row() 45 | col = row.column(align=False) 46 | col.template_list(CRAFT_UL_list.bl_idname, "", scene, "obj_craft_list", scene, "obj_craft_index") 47 | col.prop(scene, "inzoider_export_all", text="Export All Crafts") 48 | row = layout.row() 49 | selected_item = scene.obj_craft_list[scene.obj_craft_index] if scene.obj_craft_list else None 50 | if selected_item: 51 | header, panel = layout.panel("_selectedcraft", default_closed=True) 52 | icon_state = 'CHECKBOX_HLT' if selected_item.enable_editing else 'CHECKBOX_DEHLT' 53 | header.alignment = 'LEFT' 54 | header.label(text="Selected Craft", icon_value=get_icon("sofa_outline")) 55 | header.prop(selected_item, "enable_editing", text="Edit Mode", icon=icon_state, expand=False) 56 | if panel: 57 | col_panel = panel.column(align=False) 58 | col_panel.enabled = selected_item.enable_editing 59 | col_panel.prop(selected_item, "title", icon_value=get_icon("format_title")) 60 | col_panel.prop(selected_item, "description", icon_value=get_icon("text_long")) 61 | type_icon = "wall" if selected_item.type == 'Build' else "hanger" 62 | col_panel.prop(selected_item, "type", icon_value=get_icon(type_icon)) 63 | col_panel.separator() 64 | col_panel.label(text="Thumbnail and linked object cannot be edited.", icon='ERROR') 65 | else: 66 | layout.label(text="No printed.dat file has been loaded, check extension's preferences.", icon='ERROR') -------------------------------------------------------------------------------- /export/funcs.py: -------------------------------------------------------------------------------- 1 | from bpy.types import Object, Image 2 | import hashlib 3 | import bpy 4 | from math import radians 5 | import os 6 | import time 7 | import struct 8 | from .meta import Meta 9 | import shutil 10 | 11 | def get_obj_extents(obj: Object) -> tuple: 12 | """Gets proper extents of object to match inZOI scale.""" 13 | return list(obj.dimensions * 50) 14 | 15 | def generate_md5_from_str(_str: str) -> str: 16 | """Generates an MD5 hash from a string.""" 17 | text_bytes = _str.encode('utf-8') 18 | hash_md5 = hashlib.md5(text_bytes) 19 | return hash_md5.hexdigest() 20 | 21 | def current_time_str() -> str: 22 | """Returns the current time in a string format.""" 23 | return time.strftime("%Y%m%d-%H%M%S") 24 | 25 | def convert_image_to_format(path: str, img: Image, format: str, name: str, quality: int = 95) -> str: 26 | """Converts the selected thumbnail to another supported format.""" 27 | scene = bpy.context.scene 28 | config = scene.render.image_settings 29 | config.file_format = format 30 | config.quality = quality 31 | 32 | extension = 'jpg' if format == 'JPEG' else 'bmp' 33 | 34 | try: 35 | img.save_render(f"{path}/{name}.{extension}", scene=scene) 36 | except Exception as e: 37 | print(f"Error converting image: {str(e)}") 38 | 39 | def create_folder(name: str) -> str: 40 | """Creates a folder using the hashed name from the object and current time in the export directory.""" 41 | path = bpy.path.abspath(f"//{name}") 42 | if not os.path.exists(path): 43 | os.makedirs(path) 44 | return path 45 | 46 | def export_obj_as_glb(obj: Object, filepath: str, name: str) -> None: 47 | """Exports an object as a GLB file.""" 48 | bpy.ops.object.select_all(action='DESELECT') 49 | obj.select_set(True) 50 | bpy.ops.export_scene.gltf(filepath=filepath + f"/{name}.glb", use_selection=True, export_animations=False, export_draco_mesh_compression_enable=False, export_format='GLB') 51 | obj.select_set(False) 52 | 53 | def change_visual_rotation_to_obj(obj: Object, apply: bool) -> None: 54 | """Applies or unapplies a 90-degree rotation to the Z axis visually and resets the rotation data.""" 55 | bpy.ops.object.select_all(action='DESELECT') 56 | obj.rotation_mode = 'XYZ' 57 | if apply: 58 | obj.rotation_euler.z = radians(90) 59 | else: 60 | obj.rotation_euler.z = radians(-90) 61 | bpy.context.view_layer.objects.active = obj 62 | obj.select_set(True) 63 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 64 | obj.select_set(False) 65 | 66 | def change_selection(self, context): 67 | if len(context.scene.obj_craft_list) == 0: 68 | return 69 | selected_item = context.scene.obj_craft_list[context.scene.obj_craft_index] 70 | if selected_item: 71 | bpy.ops.object.select_all(action='DESELECT') 72 | selected_item.mesh.select_set(True) 73 | 74 | def get_author_from_printed_file(filepath: str) -> str: 75 | """Returns the author from the printed.dat file.""" 76 | with open(filepath, 'rb') as file: 77 | file.seek(4) 78 | data2 = struct.unpack(' bool: 83 | """Checks if a file exists.""" 84 | return os.path.isfile(filepath) 85 | 86 | def folder_exists(filepath: str) -> bool: 87 | """Checks if a folder exists.""" 88 | return os.path.isdir(filepath) 89 | 90 | def process_item(self, scene, item): 91 | from ..preferences import get_addon_prefs 92 | prefs = get_addon_prefs() 93 | 94 | hash_name = generate_md5_from_str(f"{item.title}{current_time_str()}") 95 | new_folder = create_folder(prefs.my3dprinter_path + f"/{hash_name.upper()}") 96 | if item.type == 'Character': 97 | change_visual_rotation_to_obj(item.mesh, True) 98 | export_obj_as_glb(item.mesh, new_folder, hash_name.upper()) 99 | change_visual_rotation_to_obj(item.mesh, False) 100 | else: 101 | export_obj_as_glb(item.mesh, new_folder, hash_name.upper()) 102 | if item.thumbnail: 103 | convert_image_to_format(new_folder, item.thumbnail, "JPEG", "thumbnail1") 104 | convert_image_to_format(new_folder, item.thumbnail, "BMP", "original") 105 | 106 | meta = Meta( 107 | [0, 0, 0], 108 | [0, 0, 0] if item.type == 'Character' else [0, -90, 0], 109 | get_obj_extents(item.mesh), 110 | item.title, 111 | item.description, 112 | ) 113 | meta.export_to_file(f"{new_folder}/meta.json") 114 | copy_printed_file(f"{new_folder}/printed.dat") 115 | self.report({'INFO'}, f"Craft '{item.title}' exported successfully!") 116 | 117 | 118 | def copy_printed_file(filepath: str) -> None: 119 | """Copies the printed.dat file to the 3D prints directory.""" 120 | from ..preferences import get_addon_prefs 121 | prefs = get_addon_prefs() 122 | if not prefs.is_printed_file_loaded: 123 | return 124 | else: 125 | printed_file = prefs.example_printed_file_path 126 | shutil.copyfile(printed_file, filepath) 127 | -------------------------------------------------------------------------------- /auto_load.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import typing 3 | import inspect 4 | import pkgutil 5 | import importlib 6 | from pathlib import Path 7 | 8 | __all__ = ( 9 | "init", 10 | "register", 11 | "unregister", 12 | ) 13 | 14 | modules = None 15 | ordered_classes = None 16 | 17 | 18 | def init(): 19 | global modules 20 | global ordered_classes 21 | 22 | modules = get_all_submodules(Path(__file__).parent, __package__) 23 | ordered_classes = get_ordered_classes_to_register(modules) 24 | 25 | 26 | def register(): 27 | for cls in ordered_classes: 28 | bpy.utils.register_class(cls) 29 | 30 | for module in modules: 31 | if module.__name__ == __name__: 32 | continue 33 | if hasattr(module, "register"): 34 | module.register() 35 | 36 | 37 | def unregister(): 38 | called = set() 39 | for module in modules: 40 | if module.__name__ == __name__: 41 | continue 42 | if hasattr(module, "unregister"): 43 | # Check if unregister method has already been called 44 | if module.unregister not in called: 45 | module.unregister() 46 | called.add(module.unregister) 47 | 48 | for cls in reversed(ordered_classes): 49 | bpy.utils.unregister_class(cls) 50 | 51 | 52 | # Import modules 53 | ################################################# 54 | 55 | def get_all_submodules(directory, package_name): 56 | return list(iter_submodules(directory, package_name)) 57 | 58 | 59 | def iter_submodules(path, package_name): 60 | for name in sorted(iter_submodule_names(path)): 61 | yield importlib.import_module("." + name, package_name) 62 | 63 | 64 | def iter_submodule_names(path, root=""): 65 | for _, module_name, is_package in pkgutil.iter_modules([str(path)]): 66 | yield root + module_name 67 | if is_package: 68 | if module_name == "tests": 69 | continue # avoid importing `tests/` directory 70 | sub_path = path / module_name 71 | sub_root = root + module_name + "." 72 | yield from iter_submodule_names(sub_path, sub_root) 73 | 74 | 75 | # Find classes to register 76 | ################################################# 77 | 78 | def get_ordered_classes_to_register(modules): 79 | return toposort(get_register_deps_dict(modules)) 80 | 81 | 82 | def get_register_deps_dict(modules): 83 | my_classes = set(iter_my_classes(modules)) 84 | my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")} 85 | 86 | deps_dict = {} 87 | for cls in my_classes: 88 | deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname)) 89 | return deps_dict 90 | 91 | 92 | def iter_my_register_deps(cls, my_classes, my_classes_by_idname): 93 | yield from iter_my_deps_from_annotations(cls, my_classes) 94 | yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname) 95 | 96 | 97 | def iter_my_deps_from_annotations(cls, my_classes): 98 | for value in typing.get_type_hints(cls, {}, {}).values(): 99 | dependency = get_dependency_from_annotation(value) 100 | if dependency is not None: 101 | if dependency in my_classes: 102 | yield dependency 103 | 104 | 105 | def get_dependency_from_annotation(value): 106 | if isinstance(value, bpy.props._PropertyDeferred): 107 | return value.keywords.get("type") 108 | return None 109 | 110 | 111 | def iter_my_deps_from_parent_id(cls, my_classes_by_idname): 112 | if bpy.types.Panel in cls.__bases__: 113 | parent_idname = getattr(cls, "bl_parent_id", None) 114 | if parent_idname is not None: 115 | parent_cls = my_classes_by_idname.get(parent_idname) 116 | if parent_cls is not None: 117 | yield parent_cls 118 | 119 | 120 | def iter_my_classes(modules): 121 | base_types = get_register_base_types() 122 | for cls in get_classes_in_modules(modules): 123 | if any(base in base_types for base in cls.__bases__): 124 | if not getattr(cls, "is_registered", False): 125 | yield cls 126 | 127 | 128 | def get_classes_in_modules(modules): 129 | classes = set() 130 | for module in modules: 131 | for cls in iter_classes_in_module(module): 132 | classes.add(cls) 133 | return classes 134 | 135 | 136 | def iter_classes_in_module(module): 137 | for value in module.__dict__.values(): 138 | if inspect.isclass(value): 139 | yield value 140 | 141 | 142 | def get_register_base_types(): 143 | type_names = [ 144 | "Panel", "Operator", "PropertyGroup", 145 | "Header", "Menu", 146 | "Node", "NodeSocket", "NodeTree", 147 | "UIList", "RenderEngine", 148 | "Gizmo", "GizmoGroup", 149 | ] 150 | if bpy.app.version >= (4, 1, 0): 151 | type_names.append("FileHandler") 152 | 153 | return set(getattr(bpy.types, name) for name in type_names) 154 | 155 | 156 | # Find order to register to solve dependencies 157 | ################################################# 158 | 159 | def toposort(deps_dict): 160 | sorted_list = [] 161 | sorted_values = set() 162 | while len(deps_dict) > 0: 163 | unsorted = [] 164 | # source: https://github.com/JacquesLucke/blender_vscode/pull/118/commits/f0c3a636e251a8f24f22af6f1806d338c838bcea#diff-9738ac67607466100291c17470c593209a6ad718a574d0903d9eb2e8b0a33727 165 | # JoseConseco forgot this code 166 | # https://devtalk.blender.org/t/batch-registering-multiple-classes-in-blender-2-8/3253/42 167 | sorted_list_sub = [] # helper for additional sorting by bl_order - in panels 168 | for value, deps in deps_dict.items(): 169 | if len(deps) == 0: 170 | sorted_list_sub.append(value) 171 | sorted_values.add(value) 172 | else: 173 | unsorted.append(value) 174 | deps_dict = {value: deps_dict[value] - 175 | sorted_values for value in unsorted} 176 | sorted_list_sub.sort(key=lambda cls: getattr(cls, 'bl_order', 0)) 177 | sorted_list.extend(sorted_list_sub) 178 | return sorted_list -------------------------------------------------------------------------------- /export/operators.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ..icons_load import get_icon 3 | from .constants import CRAFT_TYPES 4 | from .funcs import process_item 5 | 6 | class SelectImage_OT_Operator(bpy.types.Operator): 7 | """Select an image to use as the thumbnail for the craft item""" 8 | bl_idname = "inzoider.select_image_for_craft" 9 | bl_label = "Select Image" 10 | 11 | filepath: bpy.props.StringProperty(subtype='FILE_PATH', default="") 12 | filter_glob: bpy.props.StringProperty(default='*.jpg;*.jpeg;*.png', options={'HIDDEN'}) 13 | 14 | def execute(self, context): 15 | context.window_manager.current_craft_image_path = self.filepath 16 | 17 | try: 18 | img = bpy.data.images.load(self.filepath, check_existing=True) 19 | img.colorspace_settings.name = 'sRGB' 20 | texture_name = f"thumb_{bpy.path.basename(self.filepath).split('.')[0]}" 21 | 22 | if texture_name in bpy.data.textures: 23 | tex = bpy.data.textures[texture_name] 24 | else: 25 | tex = bpy.data.textures.new(name=texture_name, type='IMAGE') 26 | 27 | tex.image = img 28 | 29 | tex.extension = 'CLIP' 30 | 31 | tex.crop_min_x = -0.75 32 | tex.crop_min_y = 0.0 33 | 34 | tex.crop_max_x = 0.25 35 | tex.crop_max_y = 1.0 36 | 37 | context.window_manager.current_craft_texture_name = texture_name 38 | 39 | 40 | except Exception as e: 41 | self.report({'ERROR'}, f"Could not load image: {str(e)}") 42 | print(f"Error loading image: {str(e)}") 43 | 44 | return {'FINISHED'} 45 | 46 | def invoke(self, context, event): 47 | context.window_manager.fileselect_add(self) 48 | return {'RUNNING_MODAL'} 49 | 50 | class AddCraftItem_OT_Operator(bpy.types.Operator): 51 | """Add a new craft item to the list""" 52 | bl_idname = "inzoider.add_craft_item" 53 | bl_label = "Add Craft Item" 54 | 55 | craft_title: bpy.props.StringProperty(name="Title", default="") 56 | craft_description: bpy.props.StringProperty(name="Description", default="", subtype='NONE') 57 | craft_use_obj_name_as_title: bpy.props.BoolProperty(name="Use Object Name as Title", default=False, description="Use the name of the selected object as the title instead of a custom one") 58 | craft_type: bpy.props.EnumProperty(name="Type", items=CRAFT_TYPES, default='Build') 59 | 60 | @classmethod 61 | def poll(cls, context): 62 | return len(context.selected_objects) == 1 and context.selected_objects[0].type == 'MESH' 63 | 64 | def invoke(self, context, event): 65 | context.window_manager.current_craft_image_path = "" 66 | context.window_manager.current_craft_texture_name = "" 67 | self.craft_title = "" 68 | self.craft_description = "" 69 | self.craft_type = 'Build' 70 | return context.window_manager.invoke_props_dialog(self, title="Craft Properties", width=400) 71 | 72 | def execute(self, context): 73 | 74 | if self.craft_title == "": 75 | self.report({'ERROR'}, "Please enter a title for the craft item") 76 | return {'CANCELLED'} 77 | 78 | if self.craft_description == "": 79 | self.report({'ERROR'}, "Please enter a description for the craft item") 80 | return {'CANCELLED'} 81 | 82 | scene = context.scene 83 | item = scene.obj_craft_list.add() 84 | 85 | if self.craft_use_obj_name_as_title: 86 | item.title = context.active_object.name 87 | else: 88 | item.title = self.craft_title 89 | 90 | item.description = self.craft_description 91 | item.mesh = context.selected_objects[0] 92 | item.type = self.craft_type 93 | 94 | if context.window_manager.current_craft_texture_name: 95 | tex_name = context.window_manager.current_craft_texture_name 96 | if tex_name in bpy.data.textures: 97 | item.thumbnail = bpy.data.textures[tex_name].image 98 | 99 | return {'FINISHED'} 100 | 101 | def draw(self, context): 102 | from ..preferences import get_addon_prefs 103 | prefs = get_addon_prefs() 104 | layout = self.layout 105 | 106 | layout.prop(self, "craft_use_obj_name_as_title") 107 | if self.craft_use_obj_name_as_title: 108 | layout.label(text=f"Title: {context.active_object.name}", icon_value=get_icon("format_title")) 109 | else: 110 | layout.prop(self, "craft_title", icon_value=get_icon("format_title")) 111 | 112 | layout.prop(self, "craft_description", icon_value=get_icon("text_long"), expand=True) 113 | layout.label(text=f"Author: {prefs.detected_author}", icon_value=get_icon("account_tie")) 114 | layout.prop(self, "craft_type", expand=True) 115 | box = layout.box() 116 | box.label(text="Thumbnail", icon_value=get_icon("image_frame")) 117 | 118 | box.operator(SelectImage_OT_Operator.bl_idname, text="Select") 119 | 120 | if context.window_manager.current_craft_texture_name: 121 | tex_name = context.window_manager.current_craft_texture_name 122 | if tex_name in bpy.data.textures: 123 | box.template_preview(bpy.data.textures[tex_name], show_buttons=False) 124 | else: 125 | box.label(text=f"Texture '{tex_name}' not found") 126 | 127 | class RemoveCraftItem_OT_Operator(bpy.types.Operator): 128 | """Remove the selected craft item from the list""" 129 | bl_idname = "inzoider.remove_craft_item" 130 | bl_label = "Remove Craft Item" 131 | 132 | @classmethod 133 | def poll(cls, context): 134 | return len(context.scene.obj_craft_list) > 0 135 | 136 | def execute(self, context): 137 | scene = context.scene 138 | scene.obj_craft_list.remove(scene.obj_craft_index) 139 | scene.obj_craft_index = min(max(0, scene.obj_craft_index - 1), len(scene.obj_craft_list) - 1) 140 | return {'FINISHED'} 141 | 142 | def invoke(self, context, event): 143 | return context.window_manager.invoke_confirm(self, event, title="Item Removal", icon='QUESTION', message="Are you sure you want to remove this item?") 144 | 145 | class FakeOperator_OT_Operator(bpy.types.Operator): 146 | bl_idname = "fake.operator" 147 | bl_label = "Fake Operator" 148 | 149 | def execute(self, context): 150 | return {'FINISHED'} 151 | 152 | class ExportCraft_OT_Operator(bpy.types.Operator): 153 | """Export the selected craft item to the Inzoi 3D Crafts folder""" 154 | bl_idname = "inzoider.export_craft" 155 | bl_label = "Export Craft" 156 | 157 | @classmethod 158 | def poll(cls, context): 159 | scene = context.scene 160 | if scene.inzoider_export_all: 161 | return len(scene.obj_craft_list) > 0 162 | else: 163 | return len(scene.obj_craft_list) > 0 and scene.obj_craft_list[0].mesh is not None 164 | 165 | def execute(self, context): 166 | from ..preferences import get_addon_prefs 167 | prefs = get_addon_prefs() 168 | if not prefs.my3dprinter_path: 169 | self.report({'ERROR'}, "Please set the 3D printer path in the preferences") 170 | return {'CANCELLED'} 171 | scene = context.scene 172 | selected_index = scene.obj_craft_index 173 | items = scene.obj_craft_list if scene.inzoider_export_all else [scene.obj_craft_list[selected_index]] 174 | 175 | 176 | if items: 177 | for item in items: 178 | process_item(self, scene, item) 179 | 180 | return {'FINISHED'} 181 | 182 | class UnloadPrintedFile_OT_Operator(bpy.types.Operator): 183 | """Unloads the printed.dat file""" 184 | bl_idname = "inzoider.deselect_printed_file" 185 | bl_label = "Unload printed.dat" 186 | 187 | def execute(self, context): 188 | from ..preferences import get_addon_prefs 189 | prefs = get_addon_prefs() 190 | prefs.example_printed_file_path = "" 191 | prefs.is_printed_file_loaded = False 192 | prefs.detected_author = "" 193 | 194 | return {'FINISHED'} --------------------------------------------------------------------------------