armory/blender/arm/utils.py

853 lines
28 KiB
Python
Raw Normal View History

import glob
2016-06-30 13:22:05 +02:00
import json
import os
2016-08-12 02:29:09 +02:00
import platform
import re
2017-11-20 14:32:36 +01:00
import subprocess
2018-03-25 12:00:43 +02:00
import webbrowser
2018-12-24 15:46:41 +01:00
import numpy as np
import bpy
2017-05-17 17:06:52 +02:00
import arm.lib.armpack
2020-08-20 22:34:52 +02:00
from arm.lib.lz4 import LZ4
2018-05-24 22:16:28 +02:00
import arm.log as log
import arm.make_state as state
2020-10-10 20:34:39 +02:00
import arm.props_renderpath
2016-06-30 13:22:05 +02:00
2018-12-24 15:46:41 +01:00
class NumpyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.ndarray):
return obj.tolist()
return json.JSONEncoder.default(self, obj)
class WorkingDir:
"""Context manager for safely changing the current working directory."""
def __init__(self, cwd: str):
self.cwd = cwd
self.prev_cwd = os.getcwd()
def __enter__(self):
os.chdir(self.cwd)
def __exit__(self, exc_type, exc_val, exc_tb):
os.chdir(self.prev_cwd)
2016-07-20 17:33:17 +02:00
def write_arm(filepath, output):
2019-06-22 11:29:05 +02:00
if filepath.endswith('.lz4'):
2020-08-20 22:34:52 +02:00
with open(filepath, 'wb') as f:
packed = arm.lib.armpack.packb(output)
# Prepend packed data size for decoding. Haxe can't unpack
# an unsigned int64 so we use a signed int64 here
f.write(np.int64(LZ4.encode_bound(len(packed))).tobytes())
f.write(LZ4.encode(packed))
2016-07-20 17:33:17 +02:00
else:
2016-10-17 00:02:51 +02:00
if bpy.data.worlds['Arm'].arm_minimize:
2016-10-15 12:17:33 +02:00
with open(filepath, 'wb') as f:
2017-05-17 17:06:52 +02:00
f.write(arm.lib.armpack.packb(output))
2016-10-15 12:17:33 +02:00
else:
2018-04-14 15:07:05 +02:00
filepath_json = filepath.split('.arm')[0] + '.json'
with open(filepath_json, 'w') as f:
2018-12-24 15:46:41 +01:00
f.write(json.dumps(output, sort_keys=True, indent=4, cls=NumpyEncoder))
2016-06-30 13:22:05 +02:00
2018-03-26 16:28:27 +02:00
def unpack_image(image, path, file_format='JPEG'):
print('Armory Info: Unpacking to ' + path)
2018-03-19 12:52:08 +01:00
image.filepath_raw = path
image.file_format = file_format
image.save()
2018-03-26 16:28:27 +02:00
def convert_image(image, path, file_format='JPEG'):
# Convert image to compatible format
print('Armory Info: Converting to ' + path)
ren = bpy.context.scene.render
orig_quality = ren.image_settings.quality
orig_file_format = ren.image_settings.file_format
2018-11-12 22:25:08 +01:00
orig_color_mode = ren.image_settings.color_mode
ren.image_settings.quality = get_texture_quality_percentage()
2018-03-26 16:28:27 +02:00
ren.image_settings.file_format = file_format
2018-11-12 22:25:08 +01:00
if file_format == 'PNG':
ren.image_settings.color_mode = 'RGBA'
2019-01-12 12:51:28 +01:00
image.save_render(path, scene=bpy.context.scene)
2018-03-26 16:28:27 +02:00
ren.image_settings.quality = orig_quality
ren.image_settings.file_format = orig_file_format
2018-11-12 22:25:08 +01:00
ren.image_settings.color_mode = orig_color_mode
2018-03-26 16:28:27 +02:00
2017-05-23 01:03:44 +02:00
def blend_name():
return bpy.path.basename(bpy.context.blend_data.filepath).rsplit('.', 1)[0]
2017-05-23 01:03:44 +02:00
def build_dir():
return 'build_' + safestr(blend_name())
2016-06-30 13:22:05 +02:00
def get_fp():
2017-10-06 18:56:05 +02:00
wrd = bpy.data.worlds['Arm']
if wrd.arm_project_root != '':
return bpy.path.abspath(wrd.arm_project_root)
else:
s = bpy.data.filepath.split(os.path.sep)
s.pop()
return os.path.sep.join(s)
2016-06-30 13:22:05 +02:00
2017-05-23 01:03:44 +02:00
def get_fp_build():
2019-12-21 20:25:58 +01:00
return os.path.join(get_fp(), build_dir())
2017-05-23 01:03:44 +02:00
2016-08-12 02:29:09 +02:00
def get_os():
s = platform.system()
if s == 'Windows':
return 'win'
elif s == 'Darwin':
return 'mac'
else:
return 'linux'
2017-04-19 11:48:30 +02:00
def get_gapi():
2017-08-21 15:36:21 +02:00
wrd = bpy.data.worlds['Arm']
if state.is_export:
item = wrd.arm_exporterlist[wrd.arm_exporterlist_index]
2017-11-20 14:32:36 +01:00
return getattr(item, target_to_gapi(item.arm_project_target))
2018-11-13 14:17:47 +01:00
if wrd.arm_runtime == 'Browser':
return 'webgl'
2019-07-15 09:21:23 +02:00
return 'direct3d11' if get_os() == 'win' else 'opengl'
2017-04-19 11:48:30 +02:00
2020-10-10 20:34:39 +02:00
def get_rp() -> arm.props_renderpath.ArmRPListItem:
2017-08-21 20:16:06 +02:00
wrd = bpy.data.worlds['Arm']
return wrd.arm_rplist[wrd.arm_rplist_index]
2018-05-24 22:16:28 +02:00
def bundled_sdk_path():
if get_os() == 'mac':
# SDK on MacOS is located in .app folder due to security
p = bpy.app.binary_path
if p.endswith('Contents/MacOS/blender'):
return p[:-len('Contents/MacOS/blender')] + '/armsdk/'
else:
return p[:-len('Contents/MacOS/./blender')] + '/armsdk/'
elif get_os() == 'linux':
# /blender
return bpy.app.binary_path.rsplit('/', 1)[0] + '/armsdk/'
else:
# /blender.exe
return bpy.app.binary_path.replace('\\', '/').rsplit('/', 1)[0] + '/armsdk/'
2018-08-16 23:07:50 +02:00
# Passed by load_post handler when armsdk is found in project folder
use_local_sdk = False
def get_sdk_path():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
2018-05-24 22:16:28 +02:00
p = bundled_sdk_path()
2018-08-16 23:07:50 +02:00
if use_local_sdk:
return get_fp() + '/armsdk/'
elif os.path.exists(p) and addon_prefs.sdk_bundled:
2018-05-24 22:16:28 +02:00
return p
else:
return addon_prefs.sdk_path
def get_last_commit():
p = get_sdk_path() + 'armory/.git/refs/heads/master'
try:
file = open(p, 'r')
commit = file.readline()
except:
commit = ''
return commit
2020-10-17 15:08:20 +02:00
def get_arm_preferences() -> bpy.types.AddonPreferences:
2019-05-05 22:39:05 +02:00
preferences = bpy.context.preferences
2020-10-17 15:08:20 +02:00
return preferences.addons["armory"].preferences
def get_ide_bin():
addon_prefs = get_arm_preferences()
return '' if not hasattr(addon_prefs, 'ide_bin') else addon_prefs.ide_bin
2019-05-05 22:39:05 +02:00
def get_ffmpeg_path():
2020-10-17 15:08:20 +02:00
return get_arm_preferences().ffmpeg_path
2018-03-22 21:43:22 +01:00
def get_renderdoc_path():
2020-10-17 15:08:20 +02:00
p = get_arm_preferences().renderdoc_path
2018-03-22 21:43:22 +01:00
if p == '' and get_os() == 'win':
pdefault = 'C:\\Program Files\\RenderDoc\\qrenderdoc.exe'
if os.path.exists(pdefault):
p = pdefault
return p
2018-03-25 12:00:43 +02:00
def get_code_editor():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
2018-03-25 12:00:43 +02:00
return 'kodestudio' if not hasattr(addon_prefs, 'code_editor') else addon_prefs.code_editor
2018-03-25 21:48:30 +02:00
def get_ui_scale():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
2018-03-25 21:48:30 +02:00
return 1.0 if not hasattr(addon_prefs, 'ui_scale') else addon_prefs.ui_scale
def get_khamake_threads():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
return 1 if not hasattr(addon_prefs, 'khamake_threads') else addon_prefs.khamake_threads
2019-01-31 12:14:52 +01:00
def get_compilation_server():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
2019-01-31 12:14:52 +01:00
return False if not hasattr(addon_prefs, 'compilation_server') else addon_prefs.compilation_server
2017-02-22 16:14:55 +01:00
def get_save_on_build():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
2019-01-31 12:14:52 +01:00
return False if not hasattr(addon_prefs, 'save_on_build') else addon_prefs.save_on_build
2017-02-22 16:14:55 +01:00
def get_debug_console_auto():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
return False if not hasattr(addon_prefs, 'debug_console_auto') else addon_prefs.debug_console_auto
def get_debug_console_visible_sc():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
return 192 if not hasattr(addon_prefs, 'debug_console_visible_sc') else addon_prefs.debug_console_visible_sc
def get_debug_console_scale_in_sc():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
return 219 if not hasattr(addon_prefs, 'debug_console_scale_in_sc') else addon_prefs.debug_console_scale_in_sc
def get_debug_console_scale_out_sc():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
return 221 if not hasattr(addon_prefs, 'debug_console_scale_out_sc') else addon_prefs.debug_console_scale_out_sc
2017-11-10 14:53:40 +01:00
def get_viewport_controls():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
2017-11-10 14:53:40 +01:00
return 'qwerty' if not hasattr(addon_prefs, 'viewport_controls') else addon_prefs.viewport_controls
2018-02-16 18:09:03 +01:00
def get_legacy_shaders():
2020-10-17 15:08:20 +02:00
addon_prefs = get_arm_preferences()
2018-02-16 18:09:03 +01:00
return False if not hasattr(addon_prefs, 'legacy_shaders') else addon_prefs.legacy_shaders
2018-08-16 20:48:00 +02:00
def get_relative_paths():
2020-10-17 15:08:20 +02:00
"""Whether to convert absolute paths to relative"""
addon_prefs = get_arm_preferences()
2018-08-16 20:48:00 +02:00
return False if not hasattr(addon_prefs, 'relative_paths') else addon_prefs.relative_paths
2017-02-09 00:33:19 +01:00
def get_node_path():
if get_os() == 'win':
return get_sdk_path() + '/nodejs/node.exe'
elif get_os() == 'mac':
return get_sdk_path() + '/nodejs/node-osx'
else:
return get_sdk_path() + '/nodejs/node-linux64'
2017-03-20 13:23:31 +01:00
def get_kha_path():
2017-02-09 00:33:19 +01:00
if os.path.exists('Kha'):
2017-03-20 13:23:31 +01:00
return 'Kha'
2018-01-31 02:51:09 +01:00
return get_sdk_path() + '/Kha'
2017-03-20 13:23:31 +01:00
2017-07-02 20:48:19 +02:00
def get_haxe_path():
if get_os() == 'win':
2017-10-17 14:45:19 +02:00
return get_kha_path() + '/Tools/haxe/haxe.exe'
2017-07-02 20:48:19 +02:00
elif get_os() == 'mac':
2017-10-17 14:45:19 +02:00
return get_kha_path() + '/Tools/haxe/haxe-osx'
2017-07-02 20:48:19 +02:00
else:
2017-10-17 14:45:19 +02:00
return get_kha_path() + '/Tools/haxe/haxe-linux64'
2017-07-02 20:48:19 +02:00
2017-03-20 13:23:31 +01:00
def get_khamake_path():
return get_kha_path() + '/make'
2017-02-09 00:33:19 +01:00
2019-07-15 09:21:23 +02:00
def krom_paths():
2017-06-05 02:32:51 +02:00
sdk_path = get_sdk_path()
if arm.utils.get_os() == 'win':
2018-09-05 08:49:44 +02:00
krom_location = sdk_path + '/Krom'
2019-07-15 09:21:23 +02:00
krom_path = krom_location + '/Krom.exe'
2017-06-05 02:32:51 +02:00
elif arm.utils.get_os() == 'mac':
2018-09-05 08:49:44 +02:00
krom_location = sdk_path + '/Krom/Krom.app/Contents/MacOS'
2019-07-15 09:21:23 +02:00
krom_path = krom_location + '/Krom'
2017-06-05 02:32:51 +02:00
else:
2018-09-05 08:49:44 +02:00
krom_location = sdk_path + '/Krom'
2019-07-15 09:21:23 +02:00
krom_path = krom_location + '/Krom'
2017-06-05 02:32:51 +02:00
return krom_location, krom_path
2017-02-22 17:32:34 +01:00
def fetch_bundled_script_names():
wrd = bpy.data.worlds['Arm']
2017-08-21 12:17:55 +02:00
wrd.arm_bundled_scripts_list.clear()
with WorkingDir(get_sdk_path() + '/armory/Sources/armory/trait'):
for file in glob.glob('*.hx'):
wrd.arm_bundled_scripts_list.add().name = file.rsplit('.', 1)[0]
2017-02-22 17:32:34 +01:00
2017-07-24 02:27:22 +02:00
script_props = {}
2017-08-08 19:56:47 +02:00
script_props_defaults = {}
script_warnings = {}
2017-07-24 02:27:22 +02:00
def fetch_script_props(file):
with open(file) as f:
name = file.rsplit('.', 1)[0]
if 'Sources' in name:
2019-12-21 14:21:19 +01:00
name = name[name.index('Sources') + 8:]
2019-07-10 05:25:37 +02:00
if '/' in name:
2019-12-21 14:21:19 +01:00
name = name.replace('/', '.')
2019-07-10 05:25:37 +02:00
if '\\' in file:
2019-12-21 14:21:19 +01:00
name = name.replace('\\', '.')
2017-07-24 02:27:22 +02:00
script_props[name] = []
2017-08-08 19:56:47 +02:00
script_props_defaults[name] = []
script_warnings[name] = []
2017-07-24 02:27:22 +02:00
lines = f.read().splitlines()
# Read next line
read_prop = False
for lineno, line in enumerate(lines):
# enumerate() starts with 0
lineno += 1
if not read_prop:
2019-12-21 14:21:19 +01:00
read_prop = line.lstrip().startswith('@prop')
continue
if read_prop:
if 'var ' in line:
# Line of code
code_ref = line.split('var ')[1].split(';')[0]
2019-12-21 14:21:19 +01:00
else:
script_warnings[name].append(f"Line {lineno - 1}: Unused @prop")
2020-01-08 17:06:32 +01:00
read_prop = line.lstrip().startswith('@prop')
continue
valid_prop = False
# Declaration = Assignment;
var_sides = code_ref.split('=')
# DeclarationName: DeclarationType
decl_sides = var_sides[0].split(':')
prop_name = decl_sides[0].strip()
2020-01-08 17:17:18 +01:00
if 'static ' in line:
# Static properties can be overwritten multiple times
# from multiple property lists
script_warnings[name].append(f"Line {lineno} (\"{prop_name}\"): Static properties may result in undefined behaviours!")
# If the prop type is annotated in the code
# (= declaration has two parts)
if len(decl_sides) > 1:
prop_type = decl_sides[1].strip()
2020-01-07 22:00:45 +01:00
if prop_type.startswith("iron.object."):
prop_type = prop_type[12:]
2020-01-08 16:37:33 +01:00
elif prop_type.startswith("iron.math."):
prop_type = prop_type[10:]
# Default value exists
if len(var_sides) > 1 and var_sides[1].strip() != "":
# Type is not supported
if get_type_default_value(prop_type) is None:
2020-01-08 17:17:18 +01:00
script_warnings[name].append(f"Line {lineno} (\"{prop_name}\"): Type {prop_type} is not supported!")
read_prop = False
continue
prop_value = var_sides[1].replace('\'', '').replace('"', '').strip()
2017-08-08 19:56:47 +02:00
else:
prop_value = get_type_default_value(prop_type)
# Type is not supported
if prop_value is None:
2020-01-08 17:17:18 +01:00
script_warnings[name].append(f"Line {lineno} (\"{prop_name}\"): Type {prop_type} is not supported!")
read_prop = False
continue
valid_prop = True
# Default value exists
elif len(var_sides) > 1 and var_sides[1].strip() != "":
prop_value = var_sides[1].strip()
prop_type = get_prop_type_from_value(prop_value)
# Type is not recognized
if prop_type is None:
2020-01-08 17:17:18 +01:00
script_warnings[name].append(f"Line {lineno} (\"{prop_name}\"): Property type not recognized!")
read_prop = False
continue
if prop_type == "String":
prop_value = prop_value.replace('\'', '').replace('"', '')
valid_prop = True
else:
2020-01-08 17:17:18 +01:00
script_warnings[name].append(f"Line {lineno} (\"{prop_name}\"): Not a valid property!")
read_prop = False
continue
prop = (prop_name, prop_type)
# Register prop
if valid_prop:
script_props[name].append(prop)
script_props_defaults[name].append(prop_value)
read_prop = False
2017-07-24 02:27:22 +02:00
def get_prop_type_from_value(value: str):
"""
Returns the property type based on its representation in the code.
If the type is not supported, `None` is returned.
"""
# Maybe ast.literal_eval() is better here?
try:
int(value)
return "Int"
except ValueError:
try:
float(value)
return "Float"
except ValueError:
2020-01-07 13:06:18 +01:00
# "" is required, " alone will not work
if len(value) > 1 and value.startswith(("\"", "'")) and value.endswith(("\"", "'")):
return "String"
if value in ("true", "false"):
return "Bool"
2020-01-07 20:26:12 +01:00
if value.startswith("new "):
2020-01-07 20:45:45 +01:00
value = value.split()[1].split("(")[0]
if value.startswith("Vec"):
return value
2020-01-08 16:37:33 +01:00
if value.startswith("iron.math.Vec"):
return value[10:]
return None
def get_type_default_value(prop_type: str):
"""
Returns the default value of the given Haxe type.
If the type is not supported, `None` is returned:
"""
if prop_type == "Int":
return 0
if prop_type == "Float":
return 0.0
2020-01-07 22:00:45 +01:00
if prop_type == "String" or prop_type in (
"Object", "CameraObject", "LightObject", "MeshObject", "SpeakerObject"):
return ""
if prop_type == "Bool":
return False
2020-01-07 20:26:12 +01:00
if prop_type == "Vec2":
return [0.0, 0.0]
if prop_type == "Vec3":
return [0.0, 0.0, 0.0]
if prop_type == "Vec4":
return [0.0, 0.0, 0.0, 0.0]
return None
2017-02-22 17:32:34 +01:00
def fetch_script_names():
if bpy.data.filepath == "":
return
wrd = bpy.data.worlds['Arm']
2017-05-26 16:05:14 +02:00
# Sources
2017-08-21 12:17:55 +02:00
wrd.arm_scripts_list.clear()
2020-07-12 15:36:18 +02:00
sources_path = os.path.join(get_fp(), 'Sources', safestr(wrd.arm_project_package))
2016-07-10 00:51:39 +02:00
if os.path.isdir(sources_path):
with WorkingDir(sources_path):
# Glob supports recursive search since python 3.5 so it should cover both blender 2.79 and 2.8 integrated python
for file in glob.glob('**/*.hx', recursive=True):
mod = file.rsplit('.', 1)[0]
mod = mod.replace('\\', '/')
mod_parts = mod.rsplit('/')
if re.match('^[A-Z][A-Za-z0-9_]*$', mod_parts[-1]):
wrd.arm_scripts_list.add().name = mod.replace('/', '.')
fetch_script_props(file)
2020-07-12 15:36:18 +02:00
2017-05-26 16:05:14 +02:00
# Canvas
2017-08-21 12:17:55 +02:00
wrd.arm_canvas_list.clear()
2017-05-26 16:05:14 +02:00
canvas_path = get_fp() + '/Bundled/canvas'
if os.path.isdir(canvas_path):
with WorkingDir(canvas_path):
for file in glob.glob('*.json'):
if file == "_themes.json":
continue
wrd.arm_canvas_list.add().name = file.rsplit('.', 1)[0]
2016-07-17 23:29:30 +02:00
2018-04-15 11:55:42 +02:00
def fetch_wasm_names():
if bpy.data.filepath == "":
return
wrd = bpy.data.worlds['Arm']
# WASM modules
wrd.arm_wasm_list.clear()
sources_path = get_fp() + '/Bundled'
if os.path.isdir(sources_path):
with WorkingDir(sources_path):
for file in glob.glob('*.wasm'):
name = file.rsplit('.', 1)[0]
wrd.arm_wasm_list.add().name = name
2018-04-15 11:55:42 +02:00
2017-07-24 02:27:22 +02:00
def fetch_trait_props():
for o in bpy.data.objects:
2017-11-10 12:16:39 +01:00
fetch_prop(o)
2017-12-11 19:00:32 +01:00
for s in bpy.data.scenes:
fetch_prop(s)
2017-11-10 12:16:39 +01:00
def fetch_prop(o):
for item in o.arm_traitlist:
name = ''
if item.type_prop == 'Bundled Script':
name = 'armory.trait.' + item.name
else:
name = item.name
if name not in script_props:
2017-11-10 12:16:39 +01:00
continue
props = script_props[name]
defaults = script_props_defaults[name]
warnings = script_warnings[name]
2017-11-10 12:16:39 +01:00
# Remove old props
for i in range(len(item.arm_traitpropslist) - 1, -1, -1):
ip = item.arm_traitpropslist[i]
if ip.name not in [p[0] for p in props]:
2017-11-10 12:16:39 +01:00
item.arm_traitpropslist.remove(i)
2017-11-10 12:16:39 +01:00
# Add new props
for index, p in enumerate(props):
found_prop = False
for i_prop in item.arm_traitpropslist:
if i_prop.name == p[0]:
2020-01-07 13:12:35 +01:00
if i_prop.type == p[1]:
found_prop = i_prop
else:
item.arm_traitpropslist.remove(item.arm_traitpropslist.find(i_prop.name))
2017-11-10 12:16:39 +01:00
break
2017-11-10 12:16:39 +01:00
# Not in list
if not found_prop:
2017-11-10 12:16:39 +01:00
prop = item.arm_traitpropslist.add()
prop.name = p[0]
prop.type = p[1]
prop.set_value(defaults[index])
2017-11-10 12:16:39 +01:00
if found_prop:
prop = item.arm_traitpropslist[found_prop.name]
2017-11-10 12:16:39 +01:00
# Default value added and current value is blank (no override)
2020-02-07 19:29:55 +01:00
if (found_prop.get_value() is None
or found_prop.get_value() == "") and defaults[index]:
prop.set_value(defaults[index])
2017-11-10 12:16:39 +01:00
# Type has changed, update displayed name
if (len(found_prop.name) == 1 or (len(found_prop.name) > 1 and found_prop.name[1] != p[1])):
prop.name = p[0]
prop.type = p[1]
item.arm_traitpropswarnings.clear()
for warning in warnings:
entry = item.arm_traitpropswarnings.add()
entry.warning = warning
2017-12-11 19:00:32 +01:00
def fetch_bundled_trait_props():
# Bundled script props
for o in bpy.data.objects:
for t in o.arm_traitlist:
if t.type_prop == 'Bundled Script':
file_path = get_sdk_path() + '/armory/Sources/armory/trait/' + t.name + '.hx'
if os.path.exists(file_path):
fetch_script_props(file_path)
fetch_prop(o)
2019-02-05 12:59:34 +01:00
def update_trait_collections():
for col in bpy.data.collections:
if col.name.startswith('Trait|'):
bpy.data.collections.remove(col)
for o in bpy.data.objects:
for t in o.arm_traitlist:
if 'Trait|' + t.name not in bpy.data.collections:
col = bpy.data.collections.new('Trait|' + t.name)
else:
col = bpy.data.collections['Trait|' + t.name]
col.objects.link(o)
2017-12-11 00:55:26 +01:00
2016-07-17 23:29:30 +02:00
def to_hex(val):
return '#%02x%02x%02x%02x' % (int(val[3] * 255), int(val[0] * 255), int(val[1] * 255), int(val[2] * 255))
2016-07-27 14:25:01 +02:00
def color_to_int(val):
return (int(val[3] * 255) << 24) + (int(val[0] * 255) << 16) + (int(val[1] * 255) << 8) + int(val[2] * 255)
2017-05-13 17:17:43 +02:00
def safesrc(s):
s = safestr(s).replace('.', '_').replace('-', '_').replace(' ', '')
2016-12-21 00:51:04 +01:00
if s[0].isdigit():
s = '_' + s
return s
2016-12-07 10:19:45 +01:00
def safestr(s: str) -> str:
"""Outputs a string where special characters have been replaced with
'_', which can be safely used in file and path names."""
2017-05-13 17:17:43 +02:00
for c in r'[]/\;,><&*:%=+@!#^()|?^':
s = s.replace(c, '_')
return ''.join([i if ord(i) < 128 else '_' for i in s])
2017-10-06 11:16:29 +02:00
def asset_name(bdata):
2020-07-04 23:02:33 +02:00
if bdata == None:
return None
2017-10-06 11:16:29 +02:00
s = bdata.name
# Append library name if linked
if bdata.library is not None:
2017-10-06 11:16:29 +02:00
s += '_' + bdata.library.name
return s
2017-05-13 17:17:43 +02:00
def asset_path(s):
2019-12-21 20:25:58 +01:00
"""Remove leading '//'"""
return s[2:] if s[:2] == '//' else s
2016-08-22 21:56:28 +02:00
2016-09-14 11:49:32 +02:00
def extract_filename(s):
2017-05-13 17:17:43 +02:00
return os.path.basename(asset_path(s))
2016-09-14 11:49:32 +02:00
2017-01-19 00:26:18 +01:00
def get_render_resolution(scene):
render = scene.render
2016-08-15 23:45:03 +02:00
scale = render.resolution_percentage / 100
return int(render.resolution_x * scale), int(render.resolution_y * scale)
2016-09-08 14:08:31 +02:00
def get_texture_quality_percentage() -> int:
return int(bpy.data.worlds["Arm"].arm_texture_quality * 100)
2016-09-08 14:08:31 +02:00
def get_project_scene_name():
2017-11-09 15:05:58 +01:00
return get_active_scene().name
2016-09-12 02:24:20 +02:00
2017-01-19 00:26:18 +01:00
def get_active_scene():
2020-05-23 12:18:07 +02:00
wrd = bpy.data.worlds['Arm']
2017-11-09 15:05:58 +01:00
if not state.is_export:
2020-05-23 12:18:07 +02:00
if wrd.arm_play_scene == None:
return bpy.context.scene
return wrd.arm_play_scene
2017-09-10 15:37:38 +02:00
else:
2017-11-09 15:05:58 +01:00
item = wrd.arm_exporterlist[wrd.arm_exporterlist_index]
2019-01-24 15:09:49 +01:00
return item.arm_project_scene
2017-01-19 00:26:18 +01:00
2019-01-07 11:19:04 +01:00
def logic_editor_space(context_screen=None):
if context_screen == None:
context_screen = bpy.context.screen
if context_screen != None:
areas = context_screen.areas
2017-07-03 15:16:15 +02:00
for area in areas:
2019-02-16 00:37:14 +01:00
for space in area.spaces:
if space.type == 'NODE_EDITOR':
if space.node_tree != None and space.node_tree.bl_idname == 'ArmLogicTreeType':
return space
2017-07-03 15:16:15 +02:00
return None
2018-01-28 17:28:13 +01:00
def voxel_support():
2018-06-02 14:07:43 +02:00
# macos does not support opengl 4.5, needs metal
return state.target != 'html5' and get_os() != 'mac'
2018-01-28 17:28:13 +01:00
2019-01-21 17:59:25 +01:00
def get_cascade_size(rpdat):
cascade_size = int(rpdat.rp_shadowmap_cascade)
# Clamp to 4096 per cascade
if int(rpdat.rp_shadowmap_cascades) > 1 and cascade_size > 4096:
cascade_size = 4096
return cascade_size
2016-12-08 14:38:04 +01:00
def check_saved(self):
if bpy.data.filepath == "":
2018-05-24 22:16:28 +02:00
msg = "Save blend file first"
2020-07-07 22:43:51 +02:00
self.report({"ERROR"}, msg) if self is not None else log.warn(msg)
2016-12-08 14:38:04 +01:00
return False
return True
2017-09-08 14:07:41 +02:00
def check_path(s):
2017-01-12 22:20:14 +01:00
for c in r'[];><&*%=+@!#^()|?^':
2017-01-08 00:56:49 +01:00
if c in s:
return False
2017-01-23 20:41:45 +01:00
for c in s:
if ord(c) > 127:
return False
2017-01-08 00:56:49 +01:00
return True
2017-09-08 14:07:41 +02:00
def check_sdkpath(self):
s = get_sdk_path()
2020-07-07 22:43:51 +02:00
if not check_path(s):
msg = f"SDK path '{s}' contains special characters. Please move SDK to different path for now."
self.report({"ERROR"}, msg) if self is not None else log.warn(msg)
2017-09-08 14:07:41 +02:00
return False
else:
return True
def check_projectpath(self):
s = get_fp()
2020-07-07 22:43:51 +02:00
if not check_path(s):
msg = f"Project path '{s}' contains special characters, build process may fail."
self.report({"ERROR"}, msg) if self is not None else log.warn(msg)
2017-09-08 14:07:41 +02:00
return False
else:
return True
2017-11-26 14:45:36 +01:00
def disp_enabled(target):
2017-08-21 20:16:06 +02:00
rpdat = get_rp()
2018-05-07 23:09:38 +02:00
if rpdat.arm_rp_displacement == 'Tessellation':
return target == 'krom' or target == 'native'
return rpdat.arm_rp_displacement != 'Off'
2016-12-17 15:34:43 +01:00
2016-12-19 01:25:22 +01:00
def is_object_animation_enabled(bobject):
# Checks if animation is present and enabled
2017-08-21 12:17:55 +02:00
if bobject.arm_animation_enabled == False or bobject.type == 'BONE' or bobject.type == 'ARMATURE':
2016-12-19 01:25:22 +01:00
return False
if bobject.animation_data and bobject.animation_data.action:
return True
return False
def is_bone_animation_enabled(bobject):
# Checks if animation is present and enabled for parented armature
if bobject.parent and bobject.parent.type == 'ARMATURE':
2017-08-21 12:17:55 +02:00
if bobject.parent.arm_animation_enabled == False:
2016-12-19 01:25:22 +01:00
return False
2018-05-18 13:40:01 +02:00
# Check for present actions
adata = bobject.parent.animation_data
has_actions = adata != None and adata.action != None
if not has_actions and adata != None:
if hasattr(adata, 'nla_tracks') and adata.nla_tracks != None:
for track in adata.nla_tracks:
if track.strips == None:
continue
for strip in track.strips:
if strip.action == None:
continue
has_actions = True
break
if has_actions:
break
if adata != None and has_actions:
2016-12-19 01:25:22 +01:00
return True
return False
2020-04-12 21:16:16 +02:00
def export_bone_data(bobject: bpy.types.Object) -> bool:
"""Returns whether the bone data of the given object should be exported."""
2019-04-06 14:13:38 +02:00
return bobject.find_armature() and is_bone_animation_enabled(bobject) and get_rp().arm_skin == 'On'
2017-04-04 23:11:31 +02:00
2020-04-12 21:16:16 +02:00
def open_editor(hx_path=None):
ide_bin = get_ide_bin()
if hx_path is None:
hx_path = arm.utils.get_fp()
if get_code_editor() == 'default':
# Get editor environment variables
# https://unix.stackexchange.com/q/4859
env_v_editor = os.environ.get('VISUAL')
env_editor = os.environ.get('EDITOR')
if env_v_editor is not None:
ide_bin = env_v_editor
elif env_editor is not None:
ide_bin = env_editor
# No environment variables set -> Let the system decide how to
# open the file
2019-05-05 22:39:05 +02:00
else:
webbrowser.open('file://' + hx_path)
return
2019-05-05 22:39:05 +02:00
if os.path.exists(ide_bin):
args = [ide_bin, arm.utils.get_fp()]
2018-08-07 08:56:48 +02:00
# Sublime Text
2019-09-25 09:44:37 +02:00
if get_code_editor() == 'sublime':
project_name = arm.utils.safestr(bpy.data.worlds['Arm'].arm_project_name)
subl_project_path = arm.utils.get_fp() + f'/{project_name}.sublime-project'
if not os.path.exists(subl_project_path):
generate_sublime_project(subl_project_path)
args += ['--project', subl_project_path]
args.append('--add')
args.append(hx_path)
if arm.utils.get_os() == 'mac':
argstr = ""
for arg in args:
if not (arg.startswith('-') or arg.startswith('--')):
argstr += '"' + arg + '"'
argstr += ' '
subprocess.Popen(argstr[:-1], shell=True)
2018-03-25 12:00:43 +02:00
else:
2018-08-07 08:56:48 +02:00
subprocess.Popen(args)
2017-11-20 14:32:36 +01:00
else:
raise FileNotFoundError(f'Code editor executable not found: {ide_bin}. You can change the path in the Armory preferences.')
2019-09-24 17:23:49 +02:00
def open_folder():
if arm.utils.get_os() == 'win':
2020-03-03 22:13:44 +01:00
subprocess.run(['explorer', arm.utils.get_fp()])
elif arm.utils.get_os() == 'mac':
2020-03-03 22:13:44 +01:00
subprocess.run(['open', arm.utils.get_fp()])
elif arm.utils.get_os() == 'linux':
2020-03-03 22:13:44 +01:00
subprocess.run(['xdg-open', arm.utils.get_fp()])
2019-09-24 17:23:49 +02:00
else:
webbrowser.open('file://' + arm.utils.get_fp())
def generate_sublime_project(subl_project_path):
"""Generates a [project_name].sublime-project file."""
print('Generating Sublime Text project file')
project_data = {
"folders": [
{
"path": ".",
"file_exclude_patterns": ["*.blend*", "*.arm"]
},
],
}
with open(subl_project_path, 'w', encoding='utf-8') as project_file:
json.dump(project_data, project_file, ensure_ascii=False, indent=4)
2017-11-20 14:32:36 +01:00
def def_strings_to_array(strdefs):
defs = strdefs.split('_')
defs = defs[1:]
defs = ['_' + d for d in defs] # Restore _
return defs
def get_kha_target(target_name): # TODO: remove
2018-12-05 10:36:36 +01:00
if target_name == 'macos-hl':
return 'osx-hl'
elif target_name.startswith('krom'): # krom-windows
2018-03-04 19:38:40 +01:00
return 'krom'
2018-12-05 10:36:36 +01:00
elif target_name == 'custom':
return ''
2017-11-20 14:32:36 +01:00
return target_name
def target_to_gapi(arm_project_target):
# TODO: align target names
if arm_project_target == 'krom':
return 'arm_gapi_' + arm.utils.get_os()
2018-03-04 19:38:40 +01:00
elif arm_project_target == 'krom-windows':
return 'arm_gapi_win'
2018-04-06 17:39:22 +02:00
elif arm_project_target == 'windows-hl':
return 'arm_gapi_win'
2018-03-04 19:38:40 +01:00
elif arm_project_target == 'krom-linux':
return 'arm_gapi_linux'
2018-04-13 19:14:11 +02:00
elif arm_project_target == 'linux-hl':
return 'arm_gapi_linux'
2018-03-04 19:38:40 +01:00
elif arm_project_target == 'krom-macos':
return 'arm_gapi_mac'
2018-04-13 19:14:11 +02:00
elif arm_project_target == 'macos-hl':
return 'arm_gapi_mac'
2019-09-08 19:08:47 +02:00
elif arm_project_target == 'android-hl':
2017-11-20 14:32:36 +01:00
return 'arm_gapi_android'
2018-12-05 10:36:36 +01:00
elif arm_project_target == 'ios-hl':
return 'arm_gapi_ios'
2017-11-20 14:32:36 +01:00
elif arm_project_target == 'node':
return 'arm_gapi_html5'
2018-12-05 10:36:36 +01:00
else: # html5, custom
2017-11-20 14:32:36 +01:00
return 'arm_gapi_' + arm_project_target
2018-08-15 15:42:25 +02:00
def check_default_props():
2017-11-22 21:17:36 +01:00
wrd = bpy.data.worlds['Arm']
if len(wrd.arm_rplist) == 0:
wrd.arm_rplist.add()
wrd.arm_rplist_index = 0
2018-08-15 15:42:25 +02:00
if wrd.arm_project_name == '':
# Take blend file name
wrd.arm_project_name = arm.utils.blend_name()
2018-08-16 23:07:50 +02:00
def register(local_sdk=False):
global use_local_sdk
use_local_sdk = local_sdk
2016-09-12 02:24:20 +02:00
def unregister():
pass