Compare commits
11 Commits
feature-im
...
2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecfd9897b1 | ||
|
|
a3e350e96e | ||
|
|
7b417ae425 | ||
|
|
c59f16ed5e | ||
|
|
51be4b8ee9 | ||
|
|
94b3554f2f | ||
|
|
ce6ae4a64c | ||
|
|
a593224b14 | ||
|
|
feb88794b2 | ||
|
|
499e6f19c6 | ||
|
|
1bdc158f08 |
49
io_scene_ase/__init__.py
Normal file
49
io_scene_ase/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
bl_info = {
|
||||
'name': 'ASCII Scene Export (ASE)',
|
||||
'description': 'Export ASE (ASCII Scene Export) files',
|
||||
'author': 'Colin Basnett (Darklight Games)',
|
||||
'version': (2, 0, 0),
|
||||
'blender': (4, 0, 0),
|
||||
'location': 'File > Import-Export',
|
||||
'warning': 'This add-on is under development.',
|
||||
'wiki_url': 'https://github.com/DarklightGames/io_scene_ase/wiki',
|
||||
'tracker_url': 'https://github.com/DarklightGames/io_scene_ase/issues',
|
||||
'support': 'COMMUNITY',
|
||||
'category': 'Import-Export'
|
||||
}
|
||||
|
||||
if 'bpy' in locals():
|
||||
import importlib
|
||||
if 'ase' in locals(): importlib.reload(ase)
|
||||
if 'builder' in locals(): importlib.reload(builder)
|
||||
if 'writer' in locals(): importlib.reload(writer)
|
||||
if 'exporter' in locals(): importlib.reload(exporter)
|
||||
|
||||
import bpy
|
||||
import bpy.utils.previews
|
||||
from . import ase
|
||||
from . import builder
|
||||
from . import writer
|
||||
from . import exporter
|
||||
|
||||
classes = (
|
||||
exporter.ASE_OT_ExportOperator,
|
||||
)
|
||||
|
||||
|
||||
def menu_func_export(self, context):
|
||||
self.layout.operator(exporter.ASE_OT_ExportOperator.bl_idname, text='ASCII Scene Export (.ase)')
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -52,3 +52,4 @@ class ASE(object):
|
||||
def __init__(self):
|
||||
self.materials = []
|
||||
self.geometry_objects = []
|
||||
|
||||
@@ -80,7 +80,7 @@ class ASEBuilder(object):
|
||||
material_indices.append(material_index)
|
||||
|
||||
mesh_data.calc_loop_triangles()
|
||||
mesh_data.calc_normals_split()
|
||||
|
||||
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=False)
|
||||
|
||||
# Faces
|
||||
@@ -133,12 +133,13 @@ class ASEBuilder(object):
|
||||
|
||||
# Vertex Colors
|
||||
if len(mesh_data.vertex_colors) > 0:
|
||||
vertex_colors = mesh_data.vertex_colors.active.data
|
||||
for color in map(lambda x: x.color, vertex_colors):
|
||||
geometry_object.vertex_colors.append(tuple(color[0:3]))
|
||||
if mesh_data.vertex_colors.active is not None:
|
||||
vertex_colors = mesh_data.vertex_colors.active.data
|
||||
for color in map(lambda x: x.color, vertex_colors):
|
||||
geometry_object.vertex_colors.append(tuple(color[0:3]))
|
||||
|
||||
# Update data offsets for next iteration
|
||||
geometry_object.texture_vertex_offset = len(mesh_data.loops)
|
||||
geometry_object.texture_vertex_offset += len(mesh_data.loops)
|
||||
geometry_object.vertex_offset = len(geometry_object.vertices)
|
||||
|
||||
if len(ase.geometry_objects) == 0:
|
||||
@@ -1,11 +1,11 @@
|
||||
import bpy
|
||||
import bpy_extras
|
||||
from bpy.props import StringProperty, FloatProperty, EnumProperty, BoolProperty
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
from bpy.props import StringProperty, EnumProperty, BoolProperty
|
||||
from bpy.types import Operator
|
||||
from .builder import *
|
||||
from .writer import *
|
||||
|
||||
|
||||
class ASE_OT_ExportOperator(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
|
||||
class ASE_OT_ExportOperator(Operator, ExportHelper):
|
||||
bl_idname = 'io_scene_ase.ase_export' # important since its how bpy.ops.import_test.some_data is constructed
|
||||
bl_label = 'Export ASE'
|
||||
bl_space_type = 'PROPERTIES'
|
||||
@@ -17,13 +17,15 @@ class ASE_OT_ExportOperator(bpy.types.Operator, bpy_extras.io_utils.ExportHelper
|
||||
maxlen=255, # Max internal buffer length, longer would be hilighted.
|
||||
)
|
||||
units: EnumProperty(
|
||||
default='U',
|
||||
items=(('M', 'Meters', ''),
|
||||
('U', 'Unreal', '')),
|
||||
name='Units',
|
||||
default='U'
|
||||
name='Units'
|
||||
)
|
||||
use_raw_mesh_data: BoolProperty(default=False, name='Raw Mesh Data')
|
||||
|
||||
use_raw_mesh_data: BoolProperty(
|
||||
default=False,
|
||||
description='No modifiers will be evaluated as part of the exported mesh',
|
||||
name='Raw Mesh Data')
|
||||
units_scale = {
|
||||
'M': 60.352,
|
||||
'U': 1.0
|
||||
@@ -41,7 +43,7 @@ class ASE_OT_ExportOperator(bpy.types.Operator, bpy_extras.io_utils.ExportHelper
|
||||
try:
|
||||
ase = ASEBuilder().build(context, options)
|
||||
ASEWriter().write(self.filepath, ase)
|
||||
self.report({'INFO'}, 'ASE export successful')
|
||||
self.report({'INFO'}, 'ASE exported successful')
|
||||
return {'FINISHED'}
|
||||
except ASEBuilderError as e:
|
||||
self.report({'ERROR'}, str(e))
|
||||
@@ -1 +0,0 @@
|
||||
python-ase
|
||||
215
src/__init__.py
215
src/__init__.py
@@ -1,215 +0,0 @@
|
||||
bl_info = {
|
||||
'name': 'ASCII Scene Export',
|
||||
'description': 'Export ASE (ASCII Scene Export) files',
|
||||
'author': 'Colin Basnett (Darklight Games)',
|
||||
'version': (1, 1, 2),
|
||||
'blender': (2, 90, 0),
|
||||
'location': 'File > Import-Export',
|
||||
'warning': 'This add-on is under development.',
|
||||
'wiki_url': 'https://github.com/DarklightGames/io_scene_ase/wiki',
|
||||
'tracker_url': 'https://github.com/DarklightGames/io_scene_ase/issues',
|
||||
'support': 'COMMUNITY',
|
||||
'category': 'Import-Export'
|
||||
}
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
import bpy
|
||||
|
||||
|
||||
def install_pip():
|
||||
"""
|
||||
Installs pip if not already present. Please note that ensurepip.bootstrap() also calls pip, which adds the
|
||||
environment variable PIP_REQ_TRACKER. After ensurepip.bootstrap() finishes execution, the directory doesn't exist
|
||||
anymore. However, when subprocess is used to call pip, in order to install a package, the environment variables
|
||||
still contain PIP_REQ_TRACKER with the now nonexistent path. This is a problem since pip checks if PIP_REQ_TRACKER
|
||||
is set and if it is, attempts to use it as temp directory. This would result in an error because the
|
||||
directory can't be found. Therefore, PIP_REQ_TRACKER needs to be removed from environment variables.
|
||||
:return:
|
||||
"""
|
||||
|
||||
try:
|
||||
# Check if pip is already installed
|
||||
subprocess.run([sys.executable, '-m', 'pip', '--version'], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
import ensurepip
|
||||
|
||||
ensurepip.bootstrap()
|
||||
os.environ.pop('PIP_REQ_TRACKER', None)
|
||||
|
||||
|
||||
def install_and_import_module(module_name, package_name=None, global_name=None):
|
||||
"""
|
||||
Installs the package through pip and attempts to import the installed module.
|
||||
:param module_name: Module to import.
|
||||
:param package_name: (Optional) Name of the package that needs to be installed. If None it is assumed to be equal
|
||||
to the module_name.
|
||||
:param global_name: (Optional) Name under which the module is imported. If None the module_name will be used.
|
||||
This allows to import under a different name with the same effect as e.g. "import numpy as np" where "np" is
|
||||
the global_name under which the module can be accessed.
|
||||
:raises: subprocess.CalledProcessError and ImportError
|
||||
"""
|
||||
if package_name is None:
|
||||
package_name = module_name
|
||||
|
||||
if global_name is None:
|
||||
global_name = module_name
|
||||
|
||||
# Blender disables the loading of user site-packages by default. However, pip will still check them to determine
|
||||
# if a dependency is already installed. This can cause problems if the packages is installed in the user
|
||||
# site-packages and pip deems the requirement satisfied, but Blender cannot import the package from the user
|
||||
# site-packages. Hence, the environment variable PYTHONNOUSERSITE is set to disallow pip from checking the user
|
||||
# site-packages. If the package is not already installed for Blender's Python interpreter, it will then try to.
|
||||
# The paths used by pip can be checked with `subprocess.run([bpy.app.binary_path_python, "-m", "site"], check=True)`
|
||||
|
||||
# Create a copy of the environment variables and modify them for the subprocess call
|
||||
environ_copy = dict(os.environ)
|
||||
environ_copy['PYTHONNOUSERSITE'] = '1'
|
||||
|
||||
subprocess.run([sys.executable, '-m', 'pip', 'install', package_name], check=True, env=environ_copy)
|
||||
|
||||
# The installation succeeded, attempt to import the module again
|
||||
import_module(module_name, global_name)
|
||||
|
||||
|
||||
class EXAMPLE_OT_install_dependencies(bpy.types.Operator):
|
||||
bl_idname = 'io_scene_ase.install_dependencies'
|
||||
bl_label = 'Install dependencies'
|
||||
bl_description = ('Downloads and installs the required python packages for this add-on. '
|
||||
'Internet connection is required. Blender may have to be started with '
|
||||
'elevated permissions in order to install the package')
|
||||
bl_options = {'REGISTER', 'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
# Deactivate when dependencies have been installed
|
||||
return not dependencies_installed
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
install_pip()
|
||||
for dependency in dependencies:
|
||||
install_and_import_module(module_name=dependency.module,
|
||||
package_name=dependency.package,
|
||||
global_name=dependency.name)
|
||||
except (subprocess.CalledProcessError, ImportError) as err:
|
||||
self.report({'ERROR'}, str(err))
|
||||
return {'CANCELLED'}
|
||||
|
||||
global dependencies_installed
|
||||
dependencies_installed = True
|
||||
|
||||
# Register the panels, operators, etc. since dependencies are installed
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class EXAMPLE_preferences(bpy.types.AddonPreferences):
|
||||
bl_idname = __name__
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(EXAMPLE_OT_install_dependencies.bl_idname, icon='CONSOLE')
|
||||
|
||||
|
||||
Dependency = namedtuple('Dependency', ['module', 'package', 'name'])
|
||||
|
||||
dependencies = (Dependency(module='asepy', package=None, name=None),)
|
||||
|
||||
dependencies_installed = False
|
||||
|
||||
|
||||
def import_module(module_name, global_name=None, reload=True):
|
||||
"""
|
||||
Import a module.
|
||||
:param module_name: Module to import.
|
||||
:param global_name: (Optional) Name under which the module is imported. If None the module_name will be used.
|
||||
This allows to import under a different name with the same effect as e.g. "import numpy as np" where "np" is
|
||||
the global_name under which the module can be accessed.
|
||||
:raises: ImportError and ModuleNotFoundError
|
||||
"""
|
||||
if global_name is None:
|
||||
global_name = module_name
|
||||
|
||||
if global_name in globals():
|
||||
importlib.reload(globals()[global_name])
|
||||
else:
|
||||
# Attempt to import the module and assign it to globals dictionary. This allow to access the module under
|
||||
# the given name, just like the regular import would.
|
||||
globals()[global_name] = importlib.import_module(module_name)
|
||||
|
||||
|
||||
preference_classes = (EXAMPLE_OT_install_dependencies,
|
||||
EXAMPLE_preferences)
|
||||
|
||||
if __name__ == 'io_scene_ase':
|
||||
if 'bpy' in locals():
|
||||
import importlib
|
||||
if 'ase' in locals(): importlib.reload(ase)
|
||||
if 'builder' in locals(): importlib.reload(builder)
|
||||
if 'writer' in locals(): importlib.reload(writer)
|
||||
if 'exporter' in locals(): importlib.reload(exporter)
|
||||
if 'reader' in locals(): importlib.reload(reader)
|
||||
|
||||
import bpy
|
||||
import bpy.utils.previews
|
||||
from . import ase
|
||||
from . import builder
|
||||
from . import writer
|
||||
from . import exporter
|
||||
|
||||
print('dependencies installed??')
|
||||
print(dependencies_installed)
|
||||
|
||||
if dependencies_installed:
|
||||
from . import reader
|
||||
|
||||
classes = (
|
||||
exporter.ASE_OT_ExportOperator,
|
||||
)
|
||||
|
||||
if dependencies_installed:
|
||||
classes += reader.classes
|
||||
|
||||
|
||||
def menu_func_export(self, context):
|
||||
self.layout.operator(exporter.ASE_OT_ExportOperator.bl_idname, text='ASCII Scene Export (.ase)')
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(reader.ASE_OT_ImportOperator.bl_idname, text='ASCII Scene Export (.ase)')
|
||||
|
||||
|
||||
def register():
|
||||
global dependencies_installed
|
||||
|
||||
for cls in preference_classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
try:
|
||||
for dependency in dependencies:
|
||||
import_module(module_name=dependency.module, global_name=dependency.name)
|
||||
dependencies_installed = True
|
||||
except ModuleNotFoundError:
|
||||
# Don't register other panels, operators etc.
|
||||
return
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
||||
|
||||
if 'reader' in locals():
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
||||
if 'reader' in locals():
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,25 +0,0 @@
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
import bpy_extras
|
||||
|
||||
|
||||
|
||||
from asepy import read_ase
|
||||
|
||||
|
||||
class ASE_OT_ImportOperator(bpy.types.Operator, bpy_extras.io_utils.ImportHelper):
|
||||
bl_idname = 'io_scene_ase.ase_export' # important since its how bpy.ops.import_test.some_data is constructed
|
||||
bl_label = 'Export ASE'
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
filename_ext = '.ase'
|
||||
filter_glob: StringProperty(
|
||||
default="*.ase",
|
||||
options={'HIDDEN'},
|
||||
maxlen=255,
|
||||
)
|
||||
|
||||
|
||||
classes = (
|
||||
ASE_OT_ImportOperator
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
import bpy
|
||||
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
except:
|
||||
pass
|
||||
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.delete()
|
||||
|
||||
bpy.ops.mesh.primitive_monkey_add()
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
|
||||
mesh_object = bpy.context.view_layer.objects.active
|
||||
material = bpy.data.materials.new('asd')
|
||||
mesh_object.data.materials.append(material)
|
||||
|
||||
r = bpy.ops.io_scene_ase.ase_export(filepath=r'.\\flat.ase')
|
||||
@@ -1,28 +0,0 @@
|
||||
import unittest
|
||||
import os
|
||||
from subprocess import run, PIPE, STDOUT
|
||||
from ..reader import read_ase
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def run_blender_script(script_path: str, args=list()):
|
||||
return run([os.environ['BLENDER_PATH'], '--background', '--python', script_path, '--'] + args)
|
||||
|
||||
|
||||
class AseExportTests(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
load_dotenv()
|
||||
|
||||
def test_flat(self):
|
||||
run_blender_script('src\\tests\\scripts\\export_flat_test.py')
|
||||
read_ase('./flat.ase')
|
||||
|
||||
def test_smooth(self):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print()
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user