...
 
Commits (13)
This diff is collapsed.
Updating this module
--------------------
This module contains copies of files belonging to
[BAM](https://pypi.python.org/pypi/blender-bam/). Fixes should be
committed to BAM and then copied here, to keep versions in sync.
Bundling BAM with Blender
-------------------------
Blender is bundled with a version of [BAM](https://pypi.python.org/pypi/blender-bam/).
To update this version, first build a new [wheel](http://pythonwheels.com/) file in
BAM itself:
python3 setup.py bdist_wheel
Since we do not want to have binaries in the addons repository, unpack this wheel to Blender
by running:
python3 install_whl.py /path/to/blender-asset-manager/dist/blender_bam-xxx.whl
This script also updates `__init__.py` to update the version number of the extracted
wheel, and removes any pre-existing older versions of the BAM wheels.
The version number and `.whl` extension are maintained in the directory name on purpose.
This way it is clear that it is not a directory to import directly into Blender itself.
Furthermore, I (Sybren) hope that it helps to get changes made in the addons repository
back into the BAM repository.
Running bam-pack from the wheel
-------------------------------
This is the way that Blender runs bam-pack:
PYTHONPATH=./path/to/blender_bam-xxx.whl python3 -m bam.pack
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
bl_info = {
"name": "Blend File Utils",
"author": "Campbell Barton and Sybren A. Stüvel",
"version": (1, 1, 7),
"blender": (2, 76, 0),
"location": "File > External Data > Blend Utils",
"description": "Utility for packing blend files",
"warning": "",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/BlendFile_Utils",
"support": 'OFFICIAL',
"category": "Import-Export",
}
BAM_WHEEL_PATH = 'blender_bam-unpacked.whl'
import logging
import bpy
from bpy.types import Operator
from bpy_extras.io_utils import ExportHelper
from .bl_utils.subprocess_helper import SubprocessHelper
class ExportBlendPack(Operator, ExportHelper, SubprocessHelper):
"""Packs a blend file and all its dependencies into an archive for easy redistribution"""
bl_idname = "export_blend.pack"
bl_label = "Pack Blend to Archive"
log = logging.getLogger('%s.ExportBlendPack' % __name__)
# ExportHelper
filename_ext = ".zip"
# SubprocessHelper
report_interval = 0.25
temp_dir = None
@classmethod
def poll(cls, context):
return bpy.data.is_saved
def process_pre(self):
import tempfile
self.temp_dir = tempfile.TemporaryDirectory()
self.environ = {'PYTHONPATH': pythonpath()}
self.outfname = bpy.path.ensure_ext(self.filepath, ".zip")
self.command = (
bpy.app.binary_path_python,
'-m', 'bam.pack',
# file to pack
"--input", bpy.data.filepath,
# file to write
"--output", self.outfname,
"--temp", self.temp_dir.name,
)
if self.log.isEnabledFor(logging.INFO):
import shlex
cmd_to_log = ' '.join(shlex.quote(s) for s in self.command)
self.log.info('Executing %s', cmd_to_log)
def process_post(self, returncode):
if self.temp_dir is None:
return
try:
self.log.debug('Cleaning up temp dir %s', self.temp_dir)
self.temp_dir.cleanup()
except FileNotFoundError:
# This is expected, the directory was already removed by BAM.
pass
except Exception:
self.log.exception('Unable to clean up temp dir %s', self.temp_dir)
self.log.info('Written to %s', self.outfname)
def menu_func(self, context):
layout = self.layout
layout.separator()
layout.operator(ExportBlendPack.bl_idname)
classes = (
ExportBlendPack,
)
def pythonpath() -> str:
"""Returns the value of a PYTHONPATH environment variable needed to run BAM from its wheel file.
"""
import os
import pathlib
log = logging.getLogger('%s.pythonpath' % __name__)
# Find the wheel to run.
wheelpath = pathlib.Path(__file__).with_name(BAM_WHEEL_PATH)
if not wheelpath.exists():
raise EnvironmentError('Wheel %s does not exist!' % wheelpath)
log.info('Using wheel %s to run BAM-Pack', wheelpath)
# Update the PYTHONPATH to include that wheel.
existing_pypath = os.environ.get('PYTHONPATH', '')
if existing_pypath:
return os.pathsep.join((existing_pypath, str(wheelpath)))
return str(wheelpath)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_external_data.append(menu_func)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.types.TOPBAR_MT_file_external_data.remove(menu_func)
if __name__ == "__main__":
register()
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
"""
Example use:
p = subprocess.Popen(
command,
stdout=subprocess.PIPE,
)
pipe_non_blocking_set(p.stdout.fileno())
try:
data = os.read(p.stdout.fileno(), 1)
except PortableBlockingIOError as ex:
if not pipe_non_blocking_is_error_blocking(ex):
raise ex
"""
__all__ = (
"pipe_non_blocking_set",
"pipe_non_blocking_is_error_blocking",
"PortableBlockingIOError",
)
import os
if os.name == "nt":
# MS-Windows Version
def pipe_non_blocking_set(fd):
# Constant could define globally but avoid polluting the name-space
# thanks to: http://stackoverflow.com/questions/34504970
import msvcrt
from ctypes import windll, byref, wintypes, WinError, POINTER
from ctypes.wintypes import HANDLE, DWORD, BOOL
LPDWORD = POINTER(DWORD)
PIPE_NOWAIT = wintypes.DWORD(0x00000001)
def pipe_no_wait(pipefd):
SetNamedPipeHandleState = windll.kernel32.SetNamedPipeHandleState
SetNamedPipeHandleState.argtypes = [HANDLE, LPDWORD, LPDWORD, LPDWORD]
SetNamedPipeHandleState.restype = BOOL
h = msvcrt.get_osfhandle(pipefd)
res = windll.kernel32.SetNamedPipeHandleState(h, byref(PIPE_NOWAIT), None, None)
if res == 0:
print(WinError())
return False
return True
return pipe_no_wait(fd)
def pipe_non_blocking_is_error_blocking(ex):
if not isinstance(ex, PortableBlockingIOError):
return False
from ctypes import GetLastError
ERROR_NO_DATA = 232
return (GetLastError() == ERROR_NO_DATA)
PortableBlockingIOError = OSError
else:
# Posix Version
def pipe_non_blocking_set(fd):
import fcntl
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
return True
# only for compatibility with 'nt' version.
def pipe_non_blocking_is_error_blocking(ex):
if not isinstance(ex, PortableBlockingIOError):
return False
return True
PortableBlockingIOError = BlockingIOError
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
"""
Defines an operator mix-in to use for non-blocking command line access.
"""
class SubprocessHelper:
"""
Mix-in class for operators to run commands in a non-blocking way.
This uses a modal operator to manage an external process.
Subclass must define:
``command``:
List of arguments to pass to subprocess.Popen
report_interval: Time in seconds between updating reports.
``process_pre()``:
Callback that runs before the process executes.
``process_post(returncode)``:
Callback that runs when the process has ende.
returncode is -1 if the process was terminated.
Subclass may define:
``environment``:
Dict of environment variables exposed to the subprocess.
Contrary to the subprocess.Popen(env=...) parameter, this
dict is and not used to replace the existing environment
entirely, but is just used to update it.
"""
environ = {}
command = ()
@staticmethod
def _non_blocking_readlines(f, chunk=64):
"""
Iterate over lines, yielding b'' when nothings left
or when new data is not yet available.
"""
import os
from .pipe_non_blocking import (
pipe_non_blocking_set,
pipe_non_blocking_is_error_blocking,
PortableBlockingIOError,
)
fd = f.fileno()
pipe_non_blocking_set(fd)
blocks = []
while True:
try:
data = os.read(fd, chunk)
if not data:
# case were reading finishes with no trailing newline
yield b''.join(blocks)
blocks.clear()
except PortableBlockingIOError as ex:
if not pipe_non_blocking_is_error_blocking(ex):
raise ex
yield b''
continue
while True:
n = data.find(b'\n')
if n == -1:
break
yield b''.join(blocks) + data[:n + 1]
data = data[n + 1:]
blocks.clear()
blocks.append(data)
def _report_output(self):
stdout_line_iter, stderr_line_iter = self._buffer_iter
for line_iter, report_type in (
(stdout_line_iter, {'INFO'}),
(stderr_line_iter, {'WARNING'})
):
while True:
line = next(line_iter).rstrip() # rstrip all, to include \r on windows
if not line:
break
self.report(report_type, line.decode(encoding='utf-8', errors='surrogateescape'))
def _wm_enter(self, context):
wm = context.window_manager
window = context.window
self._timer = wm.event_timer_add(self.report_interval, window)
window.cursor_set('WAIT')
def _wm_exit(self, context):
wm = context.window_manager
window = context.window
wm.event_timer_remove(self._timer)
window.cursor_set('DEFAULT')
def process_pre(self):
pass
def process_post(self, returncode):
pass
def modal(self, context, event):
wm = context.window_manager
p = self._process
if event.type == 'ESC':
self.cancel(context)
self.report({'INFO'}, "Operation aborted by user")
return {'CANCELLED'}
elif event.type == 'TIMER':
if p.poll() is not None:
self._report_output()
self._wm_exit(context)
self.process_post(p.returncode)
return {'FINISHED'}
self._report_output()
return {'PASS_THROUGH'}
def execute(self, context):
import subprocess
import os
import copy
self.process_pre()
env = copy.deepcopy(os.environ)
env.update(self.environ)
try:
p = subprocess.Popen(
self.command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)
except FileNotFoundError as ex:
# Command not found
self.report({'ERROR'}, str(ex))
return {'CANCELLED'}
self._process = p
self._buffer_iter = (
iter(self._non_blocking_readlines(p.stdout)),
iter(self._non_blocking_readlines(p.stderr)),
)
wm = context.window_manager
wm.modal_handler_add(self)
self._wm_enter(context)
return {'RUNNING_MODAL'}
def cancel(self, context):
self._wm_exit(context)
self._process.kill()
self.process_post(-1)
This diff is collapsed.
This diff is collapsed.
# -*- coding: utf-8 -*-
__version__ = '1.1.8'
if __name__ == '__main__':
from .cli import main
main()
"""Main module for running python -m bam.
Doesn't do much, except for printing general usage information.
"""
print("The 'bam' module cannot be run directly. The following subcommand is available:")
print()
print("python -m bam.pack")
This diff is collapsed.
#!/usr/bin/env python3
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
"""
A simply utility to copy blend files and their deps to a new location.
Similar to packing, but don't attempt any path remapping.
"""
from bam.blend import blendfile_path_walker
TIMEIT = False
# ------------------
# Ensure module path
import os
import sys
path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "modules"))
if path not in sys.path:
sys.path.append(path)
del os, sys, path
# --------
def copy_paths(
paths,
output,
base,
# load every libs dep, not just used deps.
all_deps=False,
# yield reports
report=None,
# Filename filter, allow to exclude files from the pack,
# function takes a string returns True if the files should be included.
filename_filter=None,
):
import os
import shutil
from bam.utils.system import colorize, is_subdir
path_copy_files = set(paths)
# Avoid walking over same libs many times
lib_visit = {}
yield report("Reading %d blend file(s)\n" % len(paths))
for blendfile_src in paths:
yield report(" %s: %r\n" % (colorize("blend", color='blue'), blendfile_src))
for fp, (rootdir, fp_blend_basename) in blendfile_path_walker.FilePath.visit_from_blend(
blendfile_src,
readonly=True,
recursive=True,
recursive_all=all_deps,
lib_visit=lib_visit,
):
f_abs = os.path.normpath(fp.filepath_absolute)
path_copy_files.add(f_abs)
# Source -> Dest Map
path_src_dst_map = {}
for path_src in sorted(path_copy_files):
if filename_filter and not filename_filter(path_src):
yield report(" %s: %r\n" % (colorize("exclude", color='yellow'), path_src))
continue
if not os.path.exists(path_src):
yield report(" %s: %r\n" % (colorize("missing path", color='red'), path_src))
continue
if not is_subdir(path_src, base):
yield report(" %s: %r\n" % (colorize("external path ignored", color='red'), path_src))
continue
path_rel = os.path.relpath(path_src, base)
path_dst = os.path.join(output, path_rel)
path_src_dst_map[path_src] = path_dst
# Create directories
path_dst_dir = {os.path.dirname(path_dst) for path_dst in path_src_dst_map.values()}
yield report("Creating %d directories in %r\n" % (len(path_dst_dir), output))
for path_dir in sorted(path_dst_dir):
os.makedirs(path_dir, exist_ok=True)
del path_dst_dir
# Copy files
yield report("Copying %d files to %r\n" % (len(path_src_dst_map), output))
for path_src, path_dst in sorted(path_src_dst_map.items()):
yield report(" %s: %r -> %r\n" % (colorize("copying", color='blue'), path_src, path_dst))
shutil.copy(path_src, path_dst)
#!/usr/bin/env python3
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
"""
This script takes Blend-File and remaps their paths to the original locations.
(needed for uploading to the server)
"""
VERBOSE = 1
from bam.blend import blendfile_path_walker
def blendfile_remap(
blendfile_src, blendpath_dst,
deps_remap=None, deps_remap_cb=None,
deps_remap_cb_userdata=None,
):
import os
def temp_remap_cb(filepath, level):
"""
Simply point to the output dir.
"""
basename = os.path.basename(blendfile_src)
filepath_tmp = os.path.join(blendpath_dst, basename)
# ideally we could avoid copying _ALL_ blends
# TODO(cam)
import shutil
shutil.copy(filepath, filepath_tmp)
return filepath_tmp
for fp, (rootdir, fp_blend_basename) in blendfile_path_walker.FilePath.visit_from_blend(
blendfile_src,
readonly=False,
temp_remap_cb=temp_remap_cb,
recursive=False,
):
# path_dst_final - current path in blend.
# path_src_orig - original path from JSON.
path_dst_final_b = fp.filepath
# support 2 modes, callback or dictionary
if deps_remap_cb is not None:
path_src_orig = deps_remap_cb(path_dst_final_b, deps_remap_cb_userdata)
if path_src_orig is not None:
fp.filepath = path_src_orig
if VERBOSE:
print(" Remapping:", path_dst_final_b, "->", path_src_orig)
else:
path_dst_final = path_dst_final_b.decode('utf-8')
path_src_orig = deps_remap.get(path_dst_final)
if path_src_orig is not None:
fp.filepath = path_src_orig.encode('utf-8')
if VERBOSE:
print(" Remapping:", path_dst_final, "->", path_src_orig)
def pack_restore(blendfile_dir_src, blendfile_dir_dst, pathmap):
import os
for dirpath, dirnames, filenames in os.walk(blendfile_dir_src):
if dirpath.startswith(b"."):
continue
for filename in filenames:
if os.path.splitext(filename)[1].lower() == b".blend":
remap = pathmap.get(filename.decode('utf-8'))
if remap is not None:
filepath = os.path.join(dirpath, filename)
# main function call
blendfile_remap(filepath, blendfile_dir_dst, remap)
def create_argparse():
import os
import argparse
usage_text = (
"Run this script to remap blend-file(s) paths using a JSON file created by 'packer.py':" +
os.path.basename(__file__) +
"--input=DIR --remap=JSON [options]")
parser = argparse.ArgumentParser(description=usage_text)
# for main_render() only, but validate args.
parser.add_argument(
"-i", "--input", dest="path_src", metavar='DIR', required=True,
help="Input path(s) or a wildcard to glob many files")
parser.add_argument(
"-o", "--output", dest="path_dst", metavar='DIR', required=True,
help="Output directory ")
parser.add_argument(
"-r", "--deps_remap", dest="deps_remap", metavar='JSON', required=True,
help="JSON file containing the path remapping info")
return parser
def main():
import sys
import json
parser = create_argparse()
args = parser.parse_args(sys.argv[1:])
encoding = sys.getfilesystemencoding()
with open(args.deps_remap, 'r', encoding='utf-8') as f:
pathmap = json.load(f)
pack_restore(
args.path_src.encode(encoding),
args.path_dst.encode(encoding),
pathmap,
)
if __name__ == "__main__":
main()
This diff is collapsed.
"""CLI interface to BAM-pack.
Run this using:
python -m bam.pack
"""
if __name__ == '__main__':
from bam.blend import blendfile_pack
blendfile_pack.main()
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
def colorize_dummy(msg, color=None):
return msg
_USE_COLOR = True
if _USE_COLOR:
color_codes = {
'black': '\033[0;30m',
'bright_gray': '\033[0;37m',
'blue': '\033[0;34m',
'white': '\033[1;37m',
'green': '\033[0;32m',
'bright_blue': '\033[1;34m',
'cyan': '\033[0;36m',
'bright_green': '\033[1;32m',
'red': '\033[0;31m',
'bright_cyan': '\033[1;36m',
'purple': '\033[0;35m',
'bright_red': '\033[1;31m',
'yellow': '\033[0;33m',
'bright_purple':'\033[1;35m',
'dark_gray': '\033[1;30m',
'bright_yellow':'\033[1;33m',
'normal': '\033[0m',
}
def colorize(msg, color=None):
return (color_codes[color] + msg + color_codes['normal'])
else:
colorize = colorize_dummy
def uuid_from_file(fn, block_size=1 << 20):
"""
Returns an arbitrary sized unique ASCII string based on the file contents.
(exact hashing method may change).
"""
with open(fn, 'rb') as f:
# first get the size
import os
f.seek(0, os.SEEK_END)
size = f.tell()
f.seek(0, os.SEEK_SET)
del os
# done!
import hashlib
sha1 = hashlib.new('sha512')
while True:
data = f.read(block_size)
if not data:
break
sha1.update(data)
# skip the '0x'
return hex(size)[2:] + sha1.hexdigest()
def write_json_to_zip(zip_handle, path, data=None):
import json
zip_handle.writestr(
path,
json.dumps(
data,
check_circular=False,
# optional (pretty)
sort_keys=True, indent=4, separators=(',', ': '),
).encode('utf-8'))
def write_json_to_file(path, data):
import json
with open(path, 'w') as file_handle:
json.dump(
data, file_handle, ensure_ascii=False,
check_circular=False,
# optional (pretty)
sort_keys=True, indent=4, separators=(',', ': '),
)
def is_compressed_filetype(filepath):
"""
Use to check if we should compress files in a zip.
"""
# for now, only include files which Blender is likely to reference
import os
assert(isinstance(filepath, bytes))
return os.path.splitext(filepath)[1].lower() in {
# images
b'.exr',
b'.jpg', b'.jpeg',
b'.png',
# audio
b'.aif', b'.aiff',
b'.mp3',
b'.ogg', b'.ogv',
b'.wav',
# video
b'.avi',
b'.mkv',
b'.mov',
b'.mpg', b'.mpeg',
# archives
# '.bz2', '.tbz',
# '.gz', '.tgz',
# '.zip',
}
def is_subdir(path, directory):
"""
Returns true if *path* in a subdirectory of *directory*.
"""
import os
from os.path import normpath, normcase, sep
path = normpath(normcase(path))
directory = normpath(normcase(directory))
if len(path) > len(directory):
sep = sep.encode('ascii') if isinstance(directory, bytes) else sep
if path.startswith(directory.rstrip(sep) + sep):
return True
return False
This diff is collapsed.
This diff is collapsed.
......@@ -1826,7 +1826,7 @@ class VIEW3D_MT_master_material(Menu):
layout.operator_context = 'INVOKE_REGION_WIN'
if use_mat_preview() is True:
layout.operator("view3d.show_mat_preview", icon="VISIBLE_IPO_ON")
layout.operator("view3d.show_mat_preview", icon="HIDE_OFF")
use_separator(self, context)
if use_mat_menu_type() == 'POPUP':
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.