From c4f752516f847a5e848659e04953ad2aeb4920e6 Mon Sep 17 00:00:00 2001
From: Paul Greveson
Date: Wed, 4 Dec 2019 17:30:47 +0100
Subject: [PATCH] Initial commit of blender-tools repo
---
.editorconfig | 18 +
.github/ISSUE_TEMPLATE/bug_report.md | 32 +
.github/ISSUE_TEMPLATE/feature_request.md | 20 +
.github/workflows/ci.yaml | 52 ++
.gitignore | 6 +
.mergify.yml | 16 +
.pylintrc | 563 ++++++++++++++++++
.vscode/extensions.json | 9 +
.vscode/settings.json | 11 +
.vscode/tasks.json | 44 ++
CODE_OF_CONDUCT.md | 76 +++
CONTRIBUTING.md | 59 ++
LICENSE-APACHE | 201 +++++++
LICENSE-MIT | 25 +
README.md | 146 +++++
__init__.py | 43 ++
exporter/__init__.py | 13 +
exporter/constants.py | 41 ++
exporter/export_collection.py | 285 +++++++++
exporter/exporter_panel.py | 79 +++
exporter/functions.py | 118 ++++
exporter/operators/__init__.py | 24 +
exporter/operators/add_to_collection.py | 34 ++
.../operators/delete_export_collection.py | 29 +
exporter/operators/export.py | 30 +
exporter/operators/export_all.py | 35 ++
exporter/operators/export_by_selection.py | 35 ++
exporter/operators/new_export_collection.py | 87 +++
.../new_export_collections_per_object.py | 196 ++++++
exporter/operators/remove_from_collection.py | 34 ++
.../operators/select_export_collection.py | 29 +
icons/embark_logo.png | Bin 0 -> 2494 bytes
icons/spring_icon.png | Bin 0 -> 2458 bytes
images/create_export_collection.png | Bin 0 -> 341344 bytes
images/enable_addon.png | Bin 0 -> 20152 bytes
images/export_collection_buttons.png | Bin 0 -> 335288 bytes
images/export_collection_settings.png | Bin 0 -> 8610 bytes
images/install_addon.png | Bin 0 -> 35177 bytes
images/preferences.png | Bin 0 -> 33758 bytes
images/project_source_folder.png | Bin 0 -> 47294 bytes
operators/__init__.py | 26 +
operators/add_spiral.py | 128 ++++
operators/connect_contextual.py | 124 ++++
operators/documentation.py | 30 +
operators/frame_contextual.py | 31 +
operators/importer.py | 117 ++++
operators/update.py | 250 ++++++++
utils/__init__.py | 75 +++
utils/functions.py | 140 +++++
utils/menus.py | 115 ++++
utils/preferences.py | 67 +++
utils/ui.py | 80 +++
52 files changed, 3573 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md
create mode 100644 .github/workflows/ci.yaml
create mode 100644 .gitignore
create mode 100644 .mergify.yml
create mode 100644 .pylintrc
create mode 100644 .vscode/extensions.json
create mode 100644 .vscode/settings.json
create mode 100644 .vscode/tasks.json
create mode 100644 CODE_OF_CONDUCT.md
create mode 100644 CONTRIBUTING.md
create mode 100644 LICENSE-APACHE
create mode 100644 LICENSE-MIT
create mode 100644 README.md
create mode 100644 __init__.py
create mode 100644 exporter/__init__.py
create mode 100644 exporter/constants.py
create mode 100644 exporter/export_collection.py
create mode 100644 exporter/exporter_panel.py
create mode 100644 exporter/functions.py
create mode 100644 exporter/operators/__init__.py
create mode 100644 exporter/operators/add_to_collection.py
create mode 100644 exporter/operators/delete_export_collection.py
create mode 100644 exporter/operators/export.py
create mode 100644 exporter/operators/export_all.py
create mode 100644 exporter/operators/export_by_selection.py
create mode 100644 exporter/operators/new_export_collection.py
create mode 100644 exporter/operators/new_export_collections_per_object.py
create mode 100644 exporter/operators/remove_from_collection.py
create mode 100644 exporter/operators/select_export_collection.py
create mode 100644 icons/embark_logo.png
create mode 100644 icons/spring_icon.png
create mode 100644 images/create_export_collection.png
create mode 100644 images/enable_addon.png
create mode 100644 images/export_collection_buttons.png
create mode 100644 images/export_collection_settings.png
create mode 100644 images/install_addon.png
create mode 100644 images/preferences.png
create mode 100644 images/project_source_folder.png
create mode 100644 operators/__init__.py
create mode 100644 operators/add_spiral.py
create mode 100644 operators/connect_contextual.py
create mode 100644 operators/documentation.py
create mode 100644 operators/frame_contextual.py
create mode 100644 operators/importer.py
create mode 100644 operators/update.py
create mode 100644 utils/__init__.py
create mode 100644 utils/functions.py
create mode 100644 utils/menus.py
create mode 100644 utils/preferences.py
create mode 100644 utils/ui.py
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..7fd6177
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+# https://editorconfig.org
+
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+end_of_line = lf
+charset = utf-8
+
+[*.py]
+max_line_length = 119
+
+[{*.yaml,*.yml}]
+indent_style = space
+indent_size = 2
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..c40500a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Device:**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..11fc491
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..6cfe1b5
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,52 @@
+on: [push, pull_request]
+name: CI
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python 3.6
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.6
+ - name: Install dependencies
+ run: |
+ pip install pylint
+ - name: Lint with pylint
+ run: |
+ python -m pylint blender-tools --disable=R,fixme --enable=cyclic-import
+ - name: Refactoring suggestions
+ run: |
+ python -m pylint blender-tools --disable=E,W,C,cyclic-import --enable=fixme --reports=y --exit-zero
+
+ release:
+ name: Release
+ needs: [lint]
+ if: startsWith(github.ref, 'refs/tags/')
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - name: Package
+ shell: bash
+ run: |
+ name=embark_blender_tools
+ tag=$(git describe --tags --abbrev=0)
+ release_name="$name-$tag"
+ release_zip="${release_name}.zip"
+ mkdir "$name"
+
+ cp __init__.py "$name/"
+ cp -r exporter icons images operators utils "$name/"
+ cp README.md LICENSE-APACHE LICENSE-MIT "$name/"
+ zip -r "$release_zip" "$name"
+
+ rm -r "$name"
+ echo -n "$(shasum -ba 256 "${release_zip}" | cut -d " " -f 1)" > "${release_zip}.sha256"
+ - name: Publish
+ uses: softprops/action-gh-release@v1
+ with:
+ draft: true
+ files: 'embark_blender_tools*'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..34ef46d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.ropeproject
+*.code-workspace
+__pycache__
+*.blend
+*.blend1
+*.pyc
diff --git a/.mergify.yml b/.mergify.yml
new file mode 100644
index 0000000..6c90cc6
--- /dev/null
+++ b/.mergify.yml
@@ -0,0 +1,16 @@
+pull_request_rules:
+ - name: automatic merge when CI passes and 1 reviews
+ conditions:
+ - "#approved-reviews-by>=1"
+ - "#review-requested=0"
+ - "#changes-requested-reviews-by=0"
+ - "#commented-reviews-by=0"
+ - base=master
+ - label!=work-in-progress
+ actions:
+ merge:
+ method: squash
+ - name: delete head branch after merge
+ conditions: []
+ actions:
+ delete_head_branch: {}
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..e3c82ff
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,563 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-whitelist=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use.
+jobs=1
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=pylint.extensions.mccabe
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=print-statement,
+ parameter-unpacking,
+ unpacking-in-except,
+ old-raise-syntax,
+ backtick,
+ long-suffix,
+ old-ne-operator,
+ old-octal-literal,
+ import-star-module-level,
+ non-ascii-bytes-literal,
+ raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ locally-enabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ use-symbolic-message-instead,
+ apply-builtin,
+ basestring-builtin,
+ buffer-builtin,
+ cmp-builtin,
+ coerce-builtin,
+ execfile-builtin,
+ file-builtin,
+ long-builtin,
+ raw_input-builtin,
+ reduce-builtin,
+ standarderror-builtin,
+ unicode-builtin,
+ xrange-builtin,
+ coerce-method,
+ delslice-method,
+ getslice-method,
+ setslice-method,
+ no-absolute-import,
+ old-division,
+ dict-iter-method,
+ dict-view-method,
+ next-method-called,
+ metaclass-assignment,
+ indexing-exception,
+ raising-string,
+ reload-builtin,
+ oct-method,
+ hex-method,
+ nonzero-method,
+ cmp-method,
+ input-builtin,
+ round-builtin,
+ intern-builtin,
+ unichr-builtin,
+ map-builtin-not-iterating,
+ zip-builtin-not-iterating,
+ range-builtin-not-iterating,
+ filter-builtin-not-iterating,
+ using-cmp-argument,
+ eq-without-hash,
+ div-method,
+ idiv-method,
+ rdiv-method,
+ exception-message-attribute,
+ invalid-str-codec,
+ sys-max-int,
+ bad-python3-import,
+ deprecated-string-function,
+ deprecated-str-translate-call,
+ deprecated-itertools-function,
+ deprecated-types-field,
+ next-method-defined,
+ dict-items-not-iterating,
+ dict-keys-not-iterating,
+ dict-values-not-iterating,
+ deprecated-operator-function,
+ deprecated-urllib-function,
+ xreadlines-attribute,
+ deprecated-sys-function,
+ exception-escape,
+ comprehension-escape,
+ duplicate-code,
+ import-error
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+ bar,
+ baz,
+ lol
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style.
+#class-attribute-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _,
+ blender-tools,
+ bl_info,
+ bm,
+ x,
+ y,
+ z
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style.
+#module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style.
+#variable-rgx=
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=120
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,
+ dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package..
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore.
+ignored-argument-names=_.*|^ignored_|^unused_|context|event
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method.
+max-args=6
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement.
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled).
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled).
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception".
+overgeneral-exceptions=Exception
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..702e4b2
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,9 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=827846
+ // for the documentation about the extensions.json format
+ "recommendations": [
+ "ms-python.python", // Python
+ "njpwerner.autodocstring", // Auto docstring
+ "gruntfuggly.todo-tree", // TODO Tree
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..d66343b
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "editor.formatOnPaste": true,
+ "editor.formatOnSave": true,
+ "python.linting.enabled": true,
+ "python.linting.pylintEnabled": true,
+ "python.formatting.provider": "autopep8",
+ "python.formatting.autopep8Args": [
+ "--max-line-length=120"
+ ],
+ "autoDocstring.docstringFormat": "sphinx"
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..a21a192
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,44 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "shell",
+ "label": "Lint with pylint",
+ "command": "python",
+ "args": [
+ "-m",
+ "pylint",
+ "blender-tools",
+ "--output-format=colorized",
+ "--disable=R,fixme",
+ "--enable=cyclic-import"
+ ],
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared"
+ },
+ "problemMatcher": []
+ },
+ {
+ "type": "shell",
+ "label": "Show pylint refactoring suggestions",
+ "command": "python",
+ "args": [
+ "-m",
+ "pylint",
+ "blender-tools",
+ "--output-format=colorized",
+ "--disable=E,W,C,cyclic-import",
+ "--enable=fixme",
+ "--reports=y"
+ ],
+ "presentation": {
+ "reveal": "always",
+ "panel": "shared"
+ },
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..7d03b67
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at opensource@embark-studios.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..2bb279d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,59 @@
+# Embark Contributor Guidelines
+
+Welcome! This project is created by the team at [Embark Studios](https://embark.games). We're glad you're interested in contributing! We welcome contributions from people of all backgrounds who are interested in making great software with us.
+
+At Embark, we aspire to empower everyone to create interactive experiences. To do this, we're exploring and pushing the boundaries of new technologies, and sharing our learnings with the open source community.
+
+If you have ideas for collaboration, email us at opensource@embark-studios.com.
+
+We're also hiring full-time engineers to work with us in Stockholm! Check out our current job postings [here](https://embark.games/careers).
+
+## Issues
+
+### Feature Requests
+
+If you have ideas or how to improve our projects, you can suggest features by opening a GitHub issue. Make sure to include details about the feature or change, and describe any uses cases it would enable.
+
+Feature requests will be tagged as `enhancement` and their status will be updated in the comments of the issue.
+
+### Bugs
+
+When reporting a bug or unexpected behaviour in a project, make sure your issue descibes steps to reproduce the behaviour, including the platform you were using, what steps you took, and any error messages.
+
+Reproducible bugs will be tagged as `bug` and their status will be updated in the comments of the issue.
+
+### Wontfix
+
+Issues will be closed and tagged as `wontfix` if we decide that we do not wish to implement it, usually due to being misaligned with the project vision or out of scope. We will comment on the issue with more detailed reasoning.
+
+## Contribution Workflow
+
+### Open Issues
+
+If you're ready to contribute, start by looking at our open issues tagged as [`help wanted`](../../issues?q=is%3Aopen+is%3Aissue+label%3A"help+wanted") or [`good first issue`](../../issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue").
+
+You can comment on the issue to let others know you're interested in working on it or to ask questions.
+
+### Making Changes
+
+1. Fork the repository.
+
+2. Create a new feature branch.
+
+3. Make your changes. Ensure that there are no build errors by running the project with your changes locally.
+
+4. Open a pull request with a name and description of what you did. You can read more about working with pull requests on GitHub [here](https://help.github.com/en/articles/creating-a-pull-request-from-a-fork).
+
+5. A maintainer will review your pull request and may ask you to make changes.
+
+## Code Guidelines
+
+We recommend following [PEP8 conventions](https://www.python.org/dev/peps/pep-0008/) when working with Python modules, and also recommend following conventions established by [Blender and its Python API](https://docs.blender.org/api/current/index.html).
+
+## Licensing
+
+Unless otherwise specified, all Embark open source projects are licensed under a dual MIT OR Apache-2.0 license, allowing licensees to chose either at their option. You can read more in each project's respective README.
+
+## Code of Conduct
+
+Please note that our projects are released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md) to ensure that they are welcoming places for everyone to contribute. By participating in any Embark open source project, you agree to abide by these terms.
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
new file mode 100644
index 0000000..11069ed
--- /dev/null
+++ b/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..0bdad8f
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,25 @@
+Copyright (c) 2019 Embark Studios
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5f93ce7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,146 @@
+# 🐵 Embark Blender Tools
+
+[![Build Status](https://github.com/EmbarkStudios/blender-tools/workflows/CI/badge.svg)](https://github.com/EmbarkStudios/blender-tools/actions?workflow=CI)
+[![Contributor Covenant](https://img.shields.io/badge/contributor%20covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md)
+[![Embark](https://img.shields.io/badge/embark-open%20source-blueviolet.svg)](https://embark.dev)
+
+A Blender add-on containing workflow tools for game development, created and maintained by Embark Studios.
+
+The add-on includes [tools](#tools) for standardized import/export workflow, 3D modelling and new object types.
+
+We welcome Pull Requests - if you would like to contribute, please check out the [Contributing](#contributing) section.
+
+## Prerequisites
+
+* [Blender 2.81](https://www.blender.org/download/)
+
+
+## Installation
+
+1. Download [the latest release of the addon](https://github.com/EmbarkStudios/blender-tools/releases) from Github!
+1. Launch Blender and navigate to _Edit -> Preferences_, then choose the _Add-ons_ section:
+
+
+1. Click _Install_ in the top right of the Preferences window
+1. Use the file browser to navigate to the file you downloaded, then click _Install Add-on_:
+
+
+1. Find the Embark Addon in the add-ons list, and ensure that the checkbox is checked:
+
+
+
+### Updating the add-on
+The tools contain built-in update functionality, so you should only need to run through this process once!
+
+If you don't want the add-on to auto-update, please disable the **Automatically check for updates** checkbox in the
+_Preferences_ section of the Embark Addon.
+
+
+## Tools
+
+Below is a concise list of some of the functionality offered by the Embark Blender Tools.
+
+### Export Collections
+Export Collections are used to define lists of objects that will be exported to a single model file.
+The tools all use FBX for exports to game engines like Unreal, and OBJ for highpoly objects for baking.
+
+#### Working with Export Collections
+There are a few things to keep in mind when working with the Embark Export Collection tools:
+1. You must configure your **Project source folder** in the Embark Addon's _Preferences_ panel, as shown below.
+
+
+
+ This sets the absolute root for exporting, and all export paths will be stored as paths relative to this.
+1. All Blender scenes must be saved relative to the **Project source folder**
+1. Export Collections will fail to export if your scene is not saved, or is saved under a path outside of the **Project source folder**!
+
+#### Creating a new Export Collection
+You can create a new Export Collection from selected objects as shown here:
+1. Select one or more objects that you would like to export
+1. Click on the _Embark_ tab to the right of the 3D View
+1. Click on Create New Export Collection
+
+
+1. In the File Browser that pops up, select the location and name for your new export.
+
+ * On the right, you will see some settings:
+
+
+
+ * Select the preset that is appropriate for your asset type - this will enforce some naming conventions
+ * If you disable **Export Immediately**, the Export Collection will be created, but not exported
+
+You will now see a new Collection in the outliner, named to match the export file name you chose.
+
+#### Modifying an existing Export Collection
+
+Export Collections are stored in the Blender scene, and can be modified and exported at any time.
+
+All Export Collections are shown in the _Embark_ panel, and Export Collections that contain the selected object will be auto-expanded.
+
+You can use the fields on each Export Collection to modify:
+* Output file name
+* Export type
+* Output folder (relative to your Embark Addon's configured **Project source folder**)
+
+
+
+Each Export Collection shown in the panel also has convenience functionality on the icon buttons shown above:
+* **Select** objects contained in this Export Collection
+* **Add** selected objects to this Export Collection
+* **Remove** selected objects from this Export Collection
+* **Export** this Export Collection
+* **Delete** this Export Collection from the scene
+ * Note that the objects it contained will remain in the scene!
+
+#### Exporting objects and Export Collections
+
+You can always press the **Export All** button from the _Embark_ panel, and you can also use the
+_Embark -> Export All Collections_ menu item if you aren't in a context that shows the panel.
+This will export every Export Collection in the current scene.
+
+If you are working on a particular object, you can use the **Export by Selection** button from the _Embark_ panel.
+This is also accessible via the _Embark -> Export Collection(s) by Selection_ menu item.
+This will export only those Export Collections that contain _any_ of the currently selected objects.
+
+These commands can all be bound to hotkeys or added to the Quick menu by right-clicking on them from the _Embark_ menu.
+
+### Import
+Accessed from the _Embark -> Import_ menu item.
+
+Imports objects (FBX, OBJ, PLY) using hard-coded standard settings to enforce consistency.
+
+### Spiral Curve
+Accessed from the _Embark -> Spiral_ menu item, or the 3D viewport's _Add -> Curve -> Spiral_ menu item.
+
+Adds a parametric spiral curve object to the scene.
+
+### Connect Contextual
+Mesh editing, connects verts/edges/faces depending on selection.
+
+### Frame Contextual
+Accessed from the 3D viewport's _View -> Frame Contextual_ menu item.
+
+Frames the selection in the 3D viewport, or frames the whole scene if nothing is selected.
+
+
+## Contributing
+
+We welcome community contributions to this project.
+
+Please read our [Contributor Guide](CONTRIBUTING.md) for more information on how to get started.
+
+
+## License
+
+Licensed under either of
+
+* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
+* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
+
+at your option.
+
+
+### Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..1c257b4
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,43 @@
+"""A Blender add-on containing workflow tools for game development, created by Embark Studios.
+
+ Add-on repository: https://github.com/EmbarkStudios/blender-tools
+ Embark Studios: https://www.embark-studios.com
+"""
+
+
+from . import exporter, operators
+from .utils import menus, preferences, ui, register_recursive, unregister_recursive
+
+
+bl_info = {
+ "name": "Embark Addon",
+ "description": "A suite of tools geared towards game development, created by Embark Studios",
+ "author": "Embark Studios",
+ "version": (1, 5, 1),
+ "blender": (2, 80, 0),
+ "location": "",
+ "warning": "",
+ "wiki_url": "https://github.com/EmbarkStudios/blender-tools/",
+ "tracker_url": "https://github.com/EmbarkStudios/blender-tools/issues/new/choose",
+ "support": "COMMUNITY",
+ "category": "Generic"
+}
+
+
+REGISTER_CLASSES = (
+ preferences,
+ ui,
+ exporter,
+ operators,
+ menus,
+)
+
+
+def register():
+ """Register all of the Embark Addon classes."""
+ register_recursive(REGISTER_CLASSES)
+
+
+def unregister():
+ """Unregister all of the Embark Addon classes."""
+ unregister_recursive(REGISTER_CLASSES)
diff --git a/exporter/__init__.py b/exporter/__init__.py
new file mode 100644
index 0000000..639b78f
--- /dev/null
+++ b/exporter/__init__.py
@@ -0,0 +1,13 @@
+"""Exporter module contains operators, UI and helper functions for a consistent exporting experience."""
+
+
+import bpy
+from . import operators
+from .constants import GROUPS_ICON
+from .exporter_panel import EmbarkExporterPanel
+
+
+REGISTER_CLASSES = (
+ operators,
+ EmbarkExporterPanel
+)
diff --git a/exporter/constants.py b/exporter/constants.py
new file mode 100644
index 0000000..0c8966b
--- /dev/null
+++ b/exporter/constants.py
@@ -0,0 +1,41 @@
+"""Shared constant values used by the exporter."""
+
+
+# Name of the Collection that holds Export Collections by default
+EXPORT_COLLECTION_NAME = "ExportCollections"
+
+
+# Groups icon
+GROUPS_ICON = 'OUTLINER_OB_GROUP_INSTANCE'
+
+
+# Property names
+PROP_EXPANDED = "export_panel_expanded"
+PROP_EXPORT_NAME = "export_name"
+PROP_EXPORT_PATH = "export_path"
+PROP_EXPORT_TYPE = "export_type"
+
+
+# Type strings
+STATIC_MESH_TYPE = "SM"
+SKELETAL_MESH_TYPE = "SK"
+MID_POLY_TYPE = "MID"
+HIGH_POLY_TYPE = "HIGH"
+
+
+# Blender enums for export types (referred to in code by the first item in the tuple)
+EXPORT_TYPES = [
+ (STATIC_MESH_TYPE, "Static Mesh", "Static Mesh", 'MESH', 1),
+ (SKELETAL_MESH_TYPE, "Skeletal Mesh", "Skeletal Mesh", 'BONE', 2),
+ (MID_POLY_TYPE, "Mid Poly", "Mid Poly", 'MESH', 3),
+ (HIGH_POLY_TYPE, "High Poly", "High Poly", 'MESH', 4),
+]
+
+
+# Export type to format mappings
+EXPORT_FILE_TYPES = {
+ STATIC_MESH_TYPE: "FBX",
+ SKELETAL_MESH_TYPE: "FBX",
+ MID_POLY_TYPE: "OBJ",
+ HIGH_POLY_TYPE: "OBJ",
+}
diff --git a/exporter/export_collection.py b/exporter/export_collection.py
new file mode 100644
index 0000000..d4a0478
--- /dev/null
+++ b/exporter/export_collection.py
@@ -0,0 +1,285 @@
+"""Module for handling operations on Export Collections."""
+
+
+from os import makedirs, path
+from re import split
+import bpy
+from bpy.props import BoolProperty, EnumProperty, StringProperty
+from bpy.types import Collection, Object
+from . import constants
+from ..utils import get_source_path
+from ..utils.functions import export_fbx, export_obj, remove_numeric_suffix, SceneState, unlink_collection
+
+
+RELATIVE_ROOT = ".\\"
+
+
+def get_export_filename(export_name, export_type, include_extension=True):
+ """Gets a preview of the export filename based on `export_name` and `export_type`."""
+ export_name = validate_export_name(export_name)
+ extension = f".{constants.EXPORT_FILE_TYPES[export_type].lower()}" if include_extension else ""
+ if export_type in [constants.MID_POLY_TYPE, constants.HIGH_POLY_TYPE]:
+ return f"{export_name}_{export_type}{extension}"
+ return f"{export_type}_{export_name}{extension}"
+
+
+def validate_export_name(export_name):
+ """Validate `export_name` and return the fixed up result."""
+ # Use scene name if the export name was empty
+ if not export_name:
+ export_name = path.splitext(path.basename(bpy.data.filepath))[0]
+
+ # Replace special characters with underscores
+ tokens = split(r'[_\-\.;|,\s]', export_name)
+ new_tokens = []
+ for token in tokens:
+ if token:
+ new_token = token[0].upper()
+ if len(token) > 1:
+ new_token += token[1:]
+ new_tokens.append(new_token)
+ export_name = "_".join(new_tokens)
+ if not new_tokens:
+ new_tokens.append(path.splitext(path.basename(bpy.data.filepath))[0])
+
+ # Strip type prefix if someone typed it in manually
+ if new_tokens[0].upper() in constants.EXPORT_FILE_TYPES.keys():
+ if len(new_tokens) == 1:
+ export_name = path.splitext(path.basename(bpy.data.filepath))[0]
+ else:
+ export_name = "_".join(new_tokens[1:])
+
+ return export_name
+
+
+def _validate_path(export_path):
+ """Verifies that the export path is under the Embark Addon's project source path."""
+ source_path = get_source_path()
+ if not source_path:
+ print(f"Warning: Source path was not defined!")
+ return RELATIVE_ROOT
+
+ if not export_path:
+ return RELATIVE_ROOT
+
+ source_path = path.normpath(source_path)
+ if not path.isabs(export_path):
+ export_path = path.join(source_path, export_path)
+
+ export_path = path.normpath(export_path)
+ if export_path.lower().startswith(source_path.lower()):
+ relpath = path.relpath(export_path, source_path)
+ return f"{RELATIVE_ROOT}{relpath}" if relpath != "." else RELATIVE_ROOT
+ print(f"Warning: Export path must be relative to: {source_path}")
+ return RELATIVE_ROOT
+
+
+def _export_name_changed(self, context):
+ ExportCollection(self).rename()
+
+
+def _export_path_changed(self, context):
+ if not self.export_path.startswith(RELATIVE_ROOT):
+ validated_path = _validate_path(self.export_path)
+ self.export_path = validated_path
+
+
+def _export_type_changed(self, context):
+ ExportCollection(self).rename()
+
+
+# Add export properties to Collection
+Collection.export_name = StringProperty(
+ name="Name",
+ description="Base name of the Export Collection, will be used as part of the output file name",
+ default="",
+ update=_export_name_changed,
+)
+Collection.export_path = StringProperty(
+ name="Path",
+ description="Path of the Export Collection, relative to the Project source folder from the Addon preferences",
+ subtype='DIR_PATH',
+ default="",
+ update=_export_path_changed,
+)
+Collection.export_type = EnumProperty(
+ name="Type",
+ description="The type of content this Export Collection contains, used to determine file type and settings",
+ items=constants.EXPORT_TYPES,
+ default=constants.STATIC_MESH_TYPE,
+ update=_export_type_changed,
+)
+Collection.export_panel_expanded = BoolProperty(
+ name="Expanded in Export Panel",
+ description="If True, this collection will be expanded in the Embark Export Panel",
+ default=False,
+)
+
+
+# Add export origin property to Object for use on Empty
+Object.is_export_origin = BoolProperty(name="Is Export Origin", default=False)
+
+
+class ExportCollection(Collection):
+ """Wrapper around Collection data type for some convenience functions."""
+
+ export_name = ""
+ export_path = ""
+ export_type = ""
+ name = ""
+ objects = []
+
+ def __init__(self, collection):
+ if not isinstance(collection, Collection):
+ raise TypeError("Not a Collection type")
+
+ def add_objects(self, objects):
+ """Adds `objects` to this Export Collection."""
+ num_added = 0
+ for obj in objects:
+ if obj.name not in self.objects:
+ self.objects.link(obj) # pylint: disable=no-member
+ num_added += 1
+ return num_added
+
+ def delete(self):
+ """Deletes this Export Collection from the scene."""
+ unlink_collection(bpy.context.scene.collection, self)
+
+ # Also remove the top-level export collection container if it's empty
+ if constants.EXPORT_COLLECTION_NAME in bpy.context.scene.collection.children:
+ exp_col = bpy.data.collections[constants.EXPORT_COLLECTION_NAME]
+ if not exp_col.children:
+ unlink_collection(bpy.context.scene.collection, exp_col)
+
+ def export(self):
+ """Exports this Export Collection based on its stored properties."""
+ export_path = self.get_full_export_path()
+ if not export_path:
+ return {'CANCELLED'}
+
+ origin_objects = self.origin_objects
+ if not origin_objects:
+ print(f"Error: No Origin object found for Export Collection '{self.name}'")
+ return {'CANCELLED'}
+
+ state = SceneState()
+ self._pre_export(origin_objects)
+
+ # Create target folder if it doesn't exist
+ target_folder = path.dirname(export_path)
+ if not path.isdir(target_folder):
+ makedirs(target_folder)
+ print(f"Created new folder: {target_folder}")
+
+ # Export the contents of the Collection as appropriate
+ export_method = export_fbx
+ if self.export_type in [constants.MID_POLY_TYPE, constants.HIGH_POLY_TYPE]:
+ export_method = export_obj
+
+ result = {'CANCELLED'}
+ try:
+ result = export_method(export_path)
+ except: # pylint: disable=bare-except
+ print(f"Error occurred while trying to export '{self.name}'. See System Console for details.")
+
+ self._post_export(origin_objects)
+ state.restore()
+
+ if result == {'FINISHED'}:
+ print(f"Exported Collection '{self.name}' to: {export_path}")
+
+ return result
+
+ def _pre_export(self, origin_objects):
+ """Move the objects based on the origin."""
+ for obj in origin_objects:
+ self.objects.unlink(obj) # pylint: disable=no-member
+
+ bpy.ops.object.select_all(action='DESELECT')
+
+ for obj in self.top_level_objects:
+ obj.location -= origin_objects[0].location # TODO: Do a full transform based on the origin object?
+
+ for obj in self.objects:
+ obj.select_set(True)
+ trimmed_name = remove_numeric_suffix(obj.name)
+ if obj.name != trimmed_name:
+ obj.name = trimmed_name
+
+ def _post_export(self, origin_objects):
+ """Reset object locations."""
+ for obj in self.top_level_objects:
+ obj.location += origin_objects[0].location
+
+ for obj in origin_objects:
+ self.objects.link(obj) # pylint: disable=no-member
+
+ def get_full_export_path(self, only_folder=False):
+ """Returns the absolute export path as a string.
+
+ :param only_folder: If `True`, the file name will not be included in the result.
+ """
+ source_path = get_source_path()
+ if not source_path:
+ print("Error: Please set up the 'Project source folder' in the Embark Addon preferences!")
+ return ""
+
+ folder = path.normpath(path.join(source_path, self.export_path))
+ if only_folder:
+ return folder
+ file_name = get_export_filename(self.export_name, self.export_type)
+ return path.join(folder, file_name)
+
+ def remove_objects(self, objects):
+ """Removes `objects` from this Export Collection."""
+ num_removed = 0
+ for obj in objects:
+ if obj.name in self.objects:
+ # Make sure the object doesn't get unlinked from the scene entirely
+ if obj.name not in bpy.context.scene.collection.objects:
+ bpy.context.scene.collection.objects.link(obj)
+ self.objects.unlink(obj) # pylint: disable=no-member
+ num_removed += 1
+ return num_removed
+
+ def rename(self):
+ """Renames the Export Collection and fixes up the origin object name."""
+ valid_name = validate_export_name(self.export_name)
+ if self.export_name != valid_name:
+ self.export_name = valid_name
+ self.name = f"{get_export_filename(self.export_name, self.export_type, include_extension=False)}"
+
+ origin_objects = self.origin_objects
+ if origin_objects:
+ origin_objects[0].name = f".{self.name}.ORIGIN"
+
+ def select(self):
+ """Selects the contents of this Export Collection."""
+ if bpy.context.edit_object:
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ bpy.ops.object.select_all(action='DESELECT')
+ for obj in self.objects:
+ obj.select_set(True)
+
+ # Set the active object to something in the new selection, if it wasn't already
+ active_obj = getattr(bpy.context.view_layer.objects.active, "name", "")
+ if self.objects and (not active_obj or active_obj not in self.objects):
+ bpy.context.view_layer.objects.active = self.objects[0]
+
+ @property
+ def top_level_objects(self):
+ """Returns a list of objects that are at the top level of this Export Collection."""
+ return [obj for obj in self.objects if not obj.parent or obj.parent.name not in self.objects]
+
+ @property
+ def origin_objects(self):
+ """Returns a list of origin objects in this Export Collection."""
+ origin_objects = []
+ for obj in self.objects:
+ if getattr(obj, "is_export_origin", None):
+ if origin_objects:
+ print(f"Warning: Found extra Origin object '{obj.name}' in Export Collection '{self.name}'")
+ origin_objects.append(obj)
+ return origin_objects
diff --git a/exporter/exporter_panel.py b/exporter/exporter_panel.py
new file mode 100644
index 0000000..ca0ee6a
--- /dev/null
+++ b/exporter/exporter_panel.py
@@ -0,0 +1,79 @@
+"""UI panel for the Embark Exporter, attached to the 3D View."""
+
+
+from bpy.props import BoolProperty
+from bpy.types import Panel, Scene
+from .constants import GROUPS_ICON, PROP_EXPANDED, PROP_EXPORT_NAME, PROP_EXPORT_PATH, PROP_EXPORT_TYPE
+from .functions import get_export_collections
+from .operators import (
+ EmbarkAddToCollection,
+ EmbarkDeleteExportCollection,
+ EmbarkExportAll,
+ EmbarkExportBySelection,
+ EmbarkExportCollection,
+ EmbarkNewExportCollection,
+ EmbarkNewExportCollectionsPerObject,
+ EmbarkRemoveFromCollection,
+ EmbarkSelectExportCollection,
+)
+
+
+# Add panel properties to Scene
+Scene.export_panel_show_only_selected = BoolProperty(
+ name="Show Only Selected Export Collections",
+ description="Only show Export Collections in the Embark Export Panel that contain objects in the active selection",
+ default=False,
+)
+
+
+class EmbarkExporterPanel(Panel): # pylint: disable=too-few-public-methods
+ """Tool panel for the Embark Exporter."""
+
+ bl_idname = "EMBARK_PT_EmbarkExporter"
+ bl_label = "Embark Exporter"
+ bl_space_type = "VIEW_3D"
+ bl_region_type = "UI"
+ bl_category = "Embark"
+
+ def draw(self, context):
+ """Draw the export settings panel."""
+ row = self.layout.row()
+ row.operator(EmbarkNewExportCollection.bl_idname, icon='COLLECTION_NEW')
+ row.operator(EmbarkNewExportCollectionsPerObject.bl_idname, text="Per Object", icon=GROUPS_ICON)
+
+ all_collections = get_export_collections()
+ if not all_collections:
+ self.layout.label(text="No Export Collections in scene!")
+ return
+
+ self.layout.prop(context.scene, "export_panel_show_only_selected")
+
+ sel_collections = get_export_collections(only_selected=True)
+ self.layout.label(text=f"{len(all_collections)} Export Collections in scene, {len(sel_collections)} selected")
+ show_collections = sel_collections if context.scene.export_panel_show_only_selected else all_collections
+ for collection in show_collections:
+ is_selected = collection in sel_collections
+ self._draw_collection_layout(collection, is_selected)
+
+ row = self.layout.row()
+ row.operator(EmbarkExportBySelection.bl_idname, text="Export by Selection", icon='EXPORT')
+ row.operator(EmbarkExportAll.bl_idname, text="Export All", icon='EXPORT')
+
+ def _draw_collection_layout(self, collection, is_selected):
+ """Draw a layout displaying properties and operators for `collection`."""
+ box = self.layout.box()
+ expand = collection.export_panel_expanded or is_selected
+ expand_icon = 'DOWNARROW_HLT' if expand else 'RIGHTARROW'
+ box.prop(collection, PROP_EXPANDED, text=collection.name, icon=expand_icon, emboss=is_selected)
+ if expand:
+ box.prop(collection, PROP_EXPORT_NAME)
+ box.prop(collection, PROP_EXPORT_TYPE)
+ box.prop(collection, PROP_EXPORT_PATH)
+ row = box.row()
+ select = row.operator(EmbarkSelectExportCollection.bl_idname, text="", icon='GROUP', emboss=False)
+ add = row.operator(EmbarkAddToCollection.bl_idname, text="", icon='ADD', emboss=False)
+ remove = row.operator(EmbarkRemoveFromCollection.bl_idname, text="", icon='REMOVE', emboss=False)
+ export = row.operator(EmbarkExportCollection.bl_idname, text="Export", icon='EXPORT')
+ delete = row.operator(EmbarkDeleteExportCollection.bl_idname, text="", icon='TRASH', emboss=False)
+ for operator in [select, add, remove, export, delete]:
+ operator.collection_name = collection.name
diff --git a/exporter/functions.py b/exporter/functions.py
new file mode 100644
index 0000000..a9ab51c
--- /dev/null
+++ b/exporter/functions.py
@@ -0,0 +1,118 @@
+"""Shared utility functions for exporting."""
+
+
+from os import path
+import bpy
+from . import constants
+from .export_collection import ExportCollection
+from ..utils import get_source_path
+from ..utils.functions import SceneState
+
+
+def check_path(operator):
+ """Verifies that the scene path is under the Embark Addon's project source path."""
+ source_path = get_source_path()
+ if not source_path:
+ operator.report({'ERROR'}, "Please set up the 'Project source folder' in the Embark Addon preferences!")
+ return False
+
+ source_path = path.normpath(source_path)
+ scene_path = bpy.path.abspath("//")
+ if not scene_path or not scene_path.lower().startswith(source_path.lower()):
+ operator.report({'ERROR'}, f"Please save your .blend file under {source_path}")
+ return False
+
+ operator.directory = scene_path
+
+ return True
+
+
+def create_export_collection(export_name, export_path, export_type, objects):
+ """Create a new Export Collection.
+
+ :param export_name: Name of the Export Collection
+ :param objects: List of objects to link to this Collection
+ :return: The resulting Export Collection object
+ """
+ state = SceneState()
+
+ if constants.EXPORT_COLLECTION_NAME in bpy.context.scene.collection.children:
+ exp_col = bpy.data.collections[constants.EXPORT_COLLECTION_NAME]
+ else:
+ exp_col = bpy.data.collections.new(constants.EXPORT_COLLECTION_NAME)
+ bpy.context.scene.collection.children.link(exp_col)
+
+ new_col = bpy.data.collections.new("TEMP_EXPORT_COLLECTION")
+ exp_col.children.link(new_col)
+
+ for obj in objects:
+ new_col.objects.link(obj)
+
+ source_path = get_source_path()
+
+ collection = ExportCollection(new_col)
+ collection.export_name = export_name
+ collection.export_path = path.relpath(export_path, source_path)
+ collection.export_type = export_type
+
+ bpy.ops.object.empty_add(type='PLAIN_AXES', location=objects[0].location)
+ export_origin = bpy.context.active_object
+ export_origin.is_export_origin = True
+ collection.objects.link(export_origin) # pylint: disable=no-member
+
+ # Remove this Empty from the active layer, it will be linked to this by default
+ bpy.context.view_layer.active_layer_collection.collection.objects.unlink(export_origin)
+
+ collection.rename()
+
+ state.restore()
+
+ return collection
+
+
+def get_export_collections(only_selected=False):
+ """Returns a list of Export Collections in the current scene.
+
+ :param only_selected: If `True`, only return selected Export Collections, defaults to `False`
+ :return: List of :class:`ExportCollection` objects
+ """
+ collections = [coll for coll in bpy.data.collections if coll.name == constants.EXPORT_COLLECTION_NAME]
+ if not collections:
+ return []
+
+ if only_selected:
+ sel_objs = bpy.context.selected_objects
+ sel_collections = []
+ for obj in sel_objs:
+ collections_with_obj = [coll for coll in collections[0].children if obj.name in coll.objects]
+ for collection in collections_with_obj:
+ if collection not in sel_collections:
+ sel_collections.append(ExportCollection(collection))
+ return sel_collections
+
+ return [ExportCollection(collection) for collection in collections[0].children]
+
+
+def get_export_collection_by_name(collection_name):
+ """Returns an Export Collection object matching `collection_name`, or `None` if not found."""
+ collections = get_export_collections()
+ result = None
+ for collection in collections:
+ if collection.name == collection_name:
+ result = collection
+ break
+ return result
+
+
+def export_collections(only_selected=False):
+ """Export all or selected Export Collections in this scene.
+
+ :param only_selected: If `True`, only exports Export Collections that are selected, defaults to `False`
+ :return: Tuple containing the total number of collections to export, and the number that succeeded
+ """
+ collections = get_export_collections(only_selected=only_selected)
+ num_exported = 0
+ for collection in collections:
+ if collection.export() == {'FINISHED'}:
+ num_exported += 1
+ return len(collections), num_exported
diff --git a/exporter/operators/__init__.py b/exporter/operators/__init__.py
new file mode 100644
index 0000000..4cf99e5
--- /dev/null
+++ b/exporter/operators/__init__.py
@@ -0,0 +1,24 @@
+"""Export Operators."""
+
+from .add_to_collection import EmbarkAddToCollection
+from .delete_export_collection import EmbarkDeleteExportCollection
+from .export_all import EmbarkExportAll
+from .export_by_selection import EmbarkExportBySelection
+from .export import EmbarkExportCollection
+from .new_export_collection import EmbarkNewExportCollection
+from .new_export_collections_per_object import EmbarkNewExportCollectionsPerObject
+from .remove_from_collection import EmbarkRemoveFromCollection
+from .select_export_collection import EmbarkSelectExportCollection
+
+
+REGISTER_CLASSES = (
+ EmbarkAddToCollection,
+ EmbarkDeleteExportCollection,
+ EmbarkExportAll,
+ EmbarkExportBySelection,
+ EmbarkExportCollection,
+ EmbarkNewExportCollection,
+ EmbarkNewExportCollectionsPerObject,
+ EmbarkRemoveFromCollection,
+ EmbarkSelectExportCollection,
+)
diff --git a/exporter/operators/add_to_collection.py b/exporter/operators/add_to_collection.py
new file mode 100644
index 0000000..d829009
--- /dev/null
+++ b/exporter/operators/add_to_collection.py
@@ -0,0 +1,34 @@
+"""Operator to add objects to an Export Collection."""
+
+
+from bpy.props import StringProperty
+from bpy.types import Operator
+from ..functions import get_export_collection_by_name
+
+
+class EmbarkAddToCollection(Operator): # pylint: disable=too-few-public-methods
+ """Adds the selected objects to the named Export Collection."""
+
+ bl_idname = "object.embark_add_to_collection"
+ bl_label = "Add Selection to Export Collection"
+ bl_description = "Adds the selected objects to this Export Collection"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ collection_name: StringProperty(options={'HIDDEN'})
+
+ def execute(self, context):
+ """Adds the selected objects to the named Collection."""
+ collection = get_export_collection_by_name(self.collection_name)
+ if not collection:
+ self.report({'ERROR'}, f"Failed to find an Export Collection named '{self.collection_name}'")
+ return {'CANCELLED'}
+
+ num_added = collection.add_objects(context.selected_objects)
+
+ self.report({'INFO'}, f"Added {num_added} object(s) to Export Collection '{self.collection_name}'")
+ return {'FINISHED'}
+
+ @classmethod
+ def poll(cls, context):
+ """Only allows this operator to execute if there is a valid selection."""
+ return context.selected_objects
diff --git a/exporter/operators/delete_export_collection.py b/exporter/operators/delete_export_collection.py
new file mode 100644
index 0000000..3c23af8
--- /dev/null
+++ b/exporter/operators/delete_export_collection.py
@@ -0,0 +1,29 @@
+"""Operator to delete an Export Collection by name."""
+
+
+from bpy.props import StringProperty
+from bpy.types import Operator
+from ..functions import get_export_collection_by_name
+
+
+class EmbarkDeleteExportCollection(Operator): # pylint: disable=too-few-public-methods
+ """Deletes the named Export Collection, but leaves all contained objects in the scene."""
+
+ bl_idname = "object.embark_delete_export_collection"
+ bl_label = "Delete Export Collection"
+ bl_description = "Deletes this Export Collection, but leaves all contained objects in the scene"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ collection_name: StringProperty(options={'HIDDEN'})
+
+ def execute(self, context):
+ """Deletes the named Collection."""
+ collection = get_export_collection_by_name(self.collection_name)
+ if not collection:
+ self.report({'ERROR'}, f"Failed to find an Export Collection named '{self.collection_name}'")
+ return {'CANCELLED'}
+
+ collection.delete()
+
+ self.report({'INFO'}, f"Deleted Export Collection '{self.collection_name}'")
+ return {'FINISHED'}
diff --git a/exporter/operators/export.py b/exporter/operators/export.py
new file mode 100644
index 0000000..087f45e
--- /dev/null
+++ b/exporter/operators/export.py
@@ -0,0 +1,30 @@
+"""Operator to export an Export Collection by name."""
+
+
+from bpy.props import StringProperty
+from bpy.types import Operator
+from ..functions import get_export_collection_by_name
+
+
+class EmbarkExportCollection(Operator): # pylint: disable=too-few-public-methods
+ """Exports a named Export Collection."""
+
+ bl_idname = "object.embark_export_collection"
+ bl_label = "Export Collection"
+ bl_description = "Exports this Export Collection"
+
+ collection_name: StringProperty(options={'HIDDEN'})
+
+ def execute(self, context):
+ """Export all Export Collections in the scene."""
+ collection = get_export_collection_by_name(self.collection_name)
+ if not collection:
+ self.report({'ERROR'}, f"Failed to find an Export Collection named '{self.collection_name}'")
+ return {'CANCELLED'}
+
+ result = collection.export()
+ if result == {'FINISHED'}:
+ self.report({'INFO'}, f"Successfully exported '{self.collection_name}'!")
+ else:
+ self.report({'ERROR'}, f"Failed to export '{self.collection_name}'! See System Console for details.")
+ return result
diff --git a/exporter/operators/export_all.py b/exporter/operators/export_all.py
new file mode 100644
index 0000000..7105d22
--- /dev/null
+++ b/exporter/operators/export_all.py
@@ -0,0 +1,35 @@
+"""Operator to export all Export Collections in the scene."""
+
+
+from bpy.types import Operator
+from ..functions import get_export_collections, export_collections
+
+
+class EmbarkExportAll(Operator):
+ """Exports all Export Collections in the current scene."""
+
+ bl_idname = "screen.embark_export_all"
+ bl_label = "Export All Collections"
+ bl_description = "Exports all Export Collections in the current scene"
+
+ def execute(self, context):
+ """Exports all Export Collections in the scene."""
+ total, succeeded = export_collections()
+ count = f"{succeeded}/{total}"
+ if succeeded == total:
+ self.report({'INFO'}, f"Successfully exported {count} Export Collections")
+ elif succeeded > 0:
+ self.report({'ERROR'}, f"Only exported {count} Export Collections! See System Console for details.")
+ else:
+ self.report({'ERROR'}, "Failed to export any Export Collections! See System Console for details.")
+ return {'FINISHED'}
+
+ @classmethod
+ def poll(cls, context):
+ """Returns True if there are any Export Collections in the scene, otherwise False."""
+ return get_export_collections()
+
+
+def menu_draw(self, context):
+ """Draw the operator as a menu item."""
+ self.layout.operator(EmbarkExportAll.bl_idname, icon='EXPORT')
diff --git a/exporter/operators/export_by_selection.py b/exporter/operators/export_by_selection.py
new file mode 100644
index 0000000..a180e6c
--- /dev/null
+++ b/exporter/operators/export_by_selection.py
@@ -0,0 +1,35 @@
+"""Operator to export Export Collections based on the object selection."""
+
+
+from bpy.types import Operator
+from ..functions import get_export_collections, export_collections
+
+
+class EmbarkExportBySelection(Operator):
+ """Export the Export Collection(s) which contain the selected objects."""
+
+ bl_idname = "object.embark_export_by_selection"
+ bl_label = "Export Collection(s) by Selection"
+ bl_description = "Exports the Export Collection(s) which contain the selected objects"
+
+ def execute(self, context):
+ """Export any Export Collections containing the current object selection."""
+ total, succeeded = export_collections(only_selected=True)
+ count = f"{succeeded}/{total}"
+ if succeeded == total:
+ self.report({'INFO'}, f"Successfully exported {count} Export Collections containing selected objects")
+ elif succeeded > 0:
+ self.report({'ERROR'}, f"Only exported {count} Export Collections! See System Console for details.")
+ else:
+ self.report({'ERROR'}, "Failed to export any Export Collections! See System Console for details.")
+ return {'FINISHED'}
+
+ @classmethod
+ def poll(cls, context):
+ """Return True if the selection is valid for operator execution, otherwise False."""
+ return get_export_collections(only_selected=True)
+
+
+def menu_draw(self, context):
+ """Draw the operator as a menu item."""
+ self.layout.operator(EmbarkExportBySelection.bl_idname, icon='EXPORT')
diff --git a/exporter/operators/new_export_collection.py b/exporter/operators/new_export_collection.py
new file mode 100644
index 0000000..8fb5f8e
--- /dev/null
+++ b/exporter/operators/new_export_collection.py
@@ -0,0 +1,87 @@
+"""Operator to create a new Export Collection containing the selected objects."""
+
+
+from os import path
+import bpy
+from bpy.props import BoolProperty, EnumProperty, StringProperty
+from bpy.types import Operator
+from .. import constants
+from ..export_collection import get_export_filename
+from ..functions import check_path, create_export_collection
+
+
+class EmbarkNewExportCollection(Operator):
+ """Creates a new Export Collection containing the currently-selected objects."""
+
+ bl_idname = "screen.embark_new_export_collection"
+ bl_label = "New Export Collection"
+ bl_description = "Creates a new Export Collection containing the currently-selected objects"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def _export_type_changed(self, context):
+ """Updates the file browser type filter and file extension when the Export Type is changed."""
+ # BUG: Dynamically changing the file browser properties appears to not be currently supported in Blender :(
+ self.filter_glob = f"*.{constants.EXPORT_FILE_TYPES[self.export_type].lower()}"
+ self.filename = get_export_filename(self.export_name, self.export_type)
+
+ directory: StringProperty()
+ export_immediately: BoolProperty(
+ name="Export Immediately",
+ description="Export this collection immediately",
+ default=True,
+ )
+ export_type: EnumProperty(name="Type", items=constants.EXPORT_TYPES, default=constants.STATIC_MESH_TYPE)
+ filename: StringProperty()
+ filter_glob: StringProperty(default="*.fbx;*.obj")
+ export_name: StringProperty(options={'HIDDEN'})
+
+ def draw(self, context):
+ """Draws the Operator properties."""
+ self.layout.prop(self, constants.PROP_EXPORT_TYPE, expand=True)
+ self.layout.label(text=f"Exports in {constants.EXPORT_FILE_TYPES[self.export_type]} format")
+ self.layout.prop(self, "export_immediately")
+
+ def execute(self, context):
+ """Creates a new Export Collection from the selection, optionally exporting it if requested."""
+ export_name = path.splitext(self.filename)[0]
+ objects = bpy.context.selected_objects
+
+ # Set the active object as the first in the list, this will be used for origin setting
+ active_object = bpy.context.active_object
+ if active_object:
+ if active_object in objects:
+ objects.remove(active_object)
+ objects.insert(0, active_object)
+
+ collection = create_export_collection(export_name, self.directory, self.export_type, objects)
+ if self.export_immediately:
+ if collection.export() == {'FINISHED'}:
+ self.report({'INFO'}, f"Successfully created & exported '{collection.name}'")
+ else:
+ self.report({'WARNING'}, f"Created '{collection.name}', but export failed! See System Console.")
+ else:
+ self.report({'INFO'}, f"Created new Export Collection: '{collection.name}'")
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ """Invokes a file browser with this Operator's properties."""
+ if not check_path(self):
+ return {'CANCELLED'}
+ scene_name = path.splitext(path.basename(bpy.data.filepath))[0]
+ name_content = [scene_name]
+ if bpy.context.active_object:
+ name_content.append(bpy.context.active_object.name)
+ export_name = "_".join(name_content)
+ self.filename = get_export_filename(export_name, self.export_type)
+ context.window_manager.fileselect_add(self)
+ return {'RUNNING_MODAL'}
+
+ @classmethod
+ def poll(cls, context):
+ """Only allows this operator to execute if there is a valid selection."""
+ return context.selected_objects
+
+
+def menu_draw(self, context):
+ """Draw the operator as a menu item."""
+ self.layout.operator(EmbarkNewExportCollection.bl_idname, icon='COLLECTION_NEW')
diff --git a/exporter/operators/new_export_collections_per_object.py b/exporter/operators/new_export_collections_per_object.py
new file mode 100644
index 0000000..694c2b0
--- /dev/null
+++ b/exporter/operators/new_export_collections_per_object.py
@@ -0,0 +1,196 @@
+"""Exporter operators and UI types."""
+
+from os import path
+import bpy
+from bpy.props import BoolProperty, EnumProperty, StringProperty
+from bpy.types import Operator
+from .. import constants
+from ..export_collection import get_export_filename
+from ..functions import check_path, create_export_collection
+from ...utils.functions import remove_numeric_suffix
+
+
+class EmbarkNewExportCollectionsPerObject(Operator):
+ """Creates a new Export Collection for each of the currently-selected objects."""
+
+ bl_idname = "object.embark_new_export_collections_per_object"
+ bl_label = "New Export Collection per Object"
+ bl_description = "Creates a new Export Collection for each of the currently-selected objects"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ _max_previews = 10
+ _name_previews = []
+ _scene_name = ""
+
+ def _scene_name_changed(self, context):
+ if not self.use_scene_name and not self.use_object_name:
+ self.use_object_name = True
+
+ def _object_name_changed(self, context):
+ if not self.use_object_name:
+ self.use_scene_name = True
+ self.use_numeric_suffix = True
+
+ def _numeric_suffix_changed(self, context):
+ if not self.use_numeric_suffix:
+ self.use_object_name = True
+
+ directory: StringProperty(subtype='DIR_PATH')
+ export_immediately: BoolProperty(
+ name="Export Immediately",
+ description="Export this collection immediately",
+ default=True,
+ )
+ export_type: EnumProperty(name="Type", items=constants.EXPORT_TYPES, default=constants.STATIC_MESH_TYPE)
+ filter_glob: StringProperty(default="*.fbx;*.obj")
+ use_scene_name: BoolProperty(
+ name="Use Scene Name",
+ description="Include the Blender scene's name as the first part of the export file name",
+ default=False,
+ update=_scene_name_changed,
+ )
+ use_object_name: BoolProperty(
+ name="Use Object Name",
+ description="Include the object's name in the export file name",
+ default=True,
+ update=_object_name_changed,
+ )
+ use_object_origin: BoolProperty(
+ name="Use Object Origin",
+ description="Use each object's origin as the origin of the FBX, instead of the scene origin",
+ default=True
+ )
+ use_numeric_suffix: BoolProperty(
+ name="Use Numeric Suffix",
+ description="Include a numeric suffix at the end of the export file name",
+ default=False,
+ update=_numeric_suffix_changed,
+ )
+ show_valid_objects: BoolProperty(
+ name="Valid Objects",
+ description="Show a preview of the objects that will be exported",
+ default=True,
+ )
+ show_invalid_objects: BoolProperty(
+ name="Invalid Objects",
+ description="Show a list of the objects that will not be exported",
+ default=False,
+ )
+
+ def draw(self, context):
+ """Draws the Operator properties."""
+ self.layout.prop(self, constants.PROP_EXPORT_TYPE, expand=True)
+ self.layout.label(text=f"Exports in {constants.EXPORT_FILE_TYPES[self.export_type]} format")
+ self.layout.prop(self, "use_object_origin")
+ self.layout.prop(self, "export_immediately")
+
+ self.layout.label(text="Name Options:")
+ self.layout.prop(self, "use_scene_name")
+ self.layout.prop(self, "use_object_name")
+ self.layout.prop(self, "use_numeric_suffix")
+
+ duplicate_names = []
+ num_sel = len(bpy.context.selected_objects)
+ self._name_previews = []
+ invalid_objs = []
+ for obj in bpy.context.selected_objects:
+ if obj.type != "MESH":
+ invalid_objs.append(obj)
+ continue
+ export_name = self._get_export_name(obj)
+ if export_name in self._name_previews and export_name not in duplicate_names:
+ duplicate_names.append(export_name)
+ self._name_previews.append(export_name)
+
+ self.layout.label(text=f"Operation will create {num_sel - len(invalid_objs)} Export Collections:")
+ if duplicate_names:
+ self.layout.label(text="WARNING: Duplicate names detected!", icon='ERROR')
+
+ self._draw_export_previews(duplicate_names)
+ if invalid_objs:
+ self._draw_invalid_objs(invalid_objs)
+
+ def execute(self, context):
+ """Creates a new Export Collection from the selection, optionally exporting it if requested."""
+ collections = []
+ self._name_previews = []
+ for obj in bpy.context.selected_objects:
+ if obj.type != "MESH":
+ continue
+ export_name = self._get_export_name(obj)
+ self._name_previews.append(export_name)
+ collection = create_export_collection(export_name, self.directory, self.export_type, [obj])
+ self.report({'INFO'}, f"Successfully created '{collection.name}'")
+ collections.append(collection)
+
+ if self.export_immediately:
+ for collection in collections:
+ if collection.export() == {'FINISHED'}:
+ self.report({'INFO'}, f"Successfully exported '{collection.name}'")
+ else:
+ self.report({'ERROR'}, f"Failed to export '{collection.name}'! See System Console.")
+
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ """Invokes a file browser with this Operator's properties."""
+ if not check_path(self):
+ return {'CANCELLED'}
+ self._scene_name = path.splitext(path.basename(bpy.data.filepath))[0]
+ context.window_manager.fileselect_add(self)
+ return {'RUNNING_MODAL'}
+
+ @classmethod
+ def poll(cls, context):
+ """Only allows this operator to execute if there is a valid selection."""
+ return context.selected_objects
+
+ def _get_export_name(self, obj):
+ """Returns the export name with parts based on the options."""
+ name_parts = []
+ if self.use_scene_name:
+ name_parts.append(self._scene_name)
+ if self.use_object_name:
+ name_parts.append(remove_numeric_suffix(obj.name))
+ if self.use_numeric_suffix:
+ base_name = "_".join(name_parts)
+ i = 1
+ test_name = "{}_{:02d}".format(base_name, i)
+ # TODO: Check export folder for existing names, and export collections already in scene
+ while test_name in self._name_previews:
+ i += 1
+ test_name = "{}_{:02d}".format(base_name, i)
+ name_parts.append("{:02d}".format(i))
+ return "_".join(name_parts)
+
+ def _draw_export_previews(self, duplicate_names):
+ """Draws a panel showing preview names of valid exports."""
+ box = self.layout.box()
+ expand_icon = 'DOWNARROW_HLT' if self.show_valid_objects else 'RIGHTARROW'
+ box.prop(self, "show_valid_objects", icon=expand_icon, emboss=False)
+ if not self.show_valid_objects:
+ return
+
+ displayed_names = []
+ for export_name in self._name_previews:
+ icon = 'NONE' if export_name not in duplicate_names else 'ERROR'
+ box.label(text=get_export_filename(export_name, self.export_type), icon=icon)
+ displayed_names.append(export_name)
+ if len(displayed_names) >= self._max_previews:
+ box.label(text=f"...and {len(self._name_previews) - len(displayed_names)} more")
+ break
+
+ def _draw_invalid_objs(self, invalid_objs):
+ """Draws a panel showing names and types of invalid objects."""
+ self.layout.label(text=f"{len(invalid_objs)} object(s) not valid for export:", icon='ERROR')
+ box = self.layout.box()
+ expand_icon = 'DOWNARROW_HLT' if self.show_invalid_objects else 'RIGHTARROW'
+ box.prop(self, "show_invalid_objects", icon=expand_icon, emboss=False)
+ if self.show_invalid_objects:
+ for obj in invalid_objs:
+ box.label(text=f"{obj.name} ({obj.type})")
+
+
+def menu_draw(self, context):
+ """Draw the operator as a menu item."""
+ self.layout.operator(EmbarkNewExportCollectionsPerObject.bl_idname, icon=constants.GROUPS_ICON)
diff --git a/exporter/operators/remove_from_collection.py b/exporter/operators/remove_from_collection.py
new file mode 100644
index 0000000..a7a9ad9
--- /dev/null
+++ b/exporter/operators/remove_from_collection.py
@@ -0,0 +1,34 @@
+"""Operator to remove objects from an Export Collection."""
+
+
+from bpy.props import StringProperty
+from bpy.types import Operator
+from ..functions import get_export_collection_by_name
+
+
+class EmbarkRemoveFromCollection(Operator): # pylint: disable=too-few-public-methods
+ """Deletes the named Export Collection, but leaves all contained objects in the scene."""
+
+ bl_idname = "object.embark_remove_from_collection"
+ bl_label = "Remove Selection from Export Collection"
+ bl_description = "Removes the selected objects from this Export Collection"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ collection_name: StringProperty(options={'HIDDEN'})
+
+ def execute(self, context):
+ """Removes the selected objects from the named Collection."""
+ collection = get_export_collection_by_name(self.collection_name)
+ if not collection:
+ self.report({'ERROR'}, f"Failed to find an Export Collection named '{self.collection_name}'")
+ return {'CANCELLED'}
+
+ num_removed = collection.remove_objects(context.selected_objects)
+
+ self.report({'INFO'}, f"Removed {num_removed} object(s) from Export Collection '{self.collection_name}'")
+ return {'FINISHED'}
+
+ @classmethod
+ def poll(cls, context):
+ """Only allows this operator to execute if there is a valid selection."""
+ return context.selected_objects
diff --git a/exporter/operators/select_export_collection.py b/exporter/operators/select_export_collection.py
new file mode 100644
index 0000000..d93e79c
--- /dev/null
+++ b/exporter/operators/select_export_collection.py
@@ -0,0 +1,29 @@
+"""Operator for selecting Export Collection contents by name."""
+
+
+from bpy.props import StringProperty
+from bpy.types import Operator
+from ..functions import get_export_collection_by_name
+
+
+class EmbarkSelectExportCollection(Operator): # pylint: disable=too-few-public-methods
+ """Selects all the objects contained in the named Export Collection."""
+
+ bl_idname = "object.embark_select_export_collection"
+ bl_label = "Select Export Collection"
+ bl_description = "Selects all the objects contained in this Export Collection"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ collection_name: StringProperty(options={'HIDDEN'})
+
+ def execute(self, context):
+ """Selects all the objects in the named Collection."""
+ collection = get_export_collection_by_name(self.collection_name)
+ if not collection:
+ self.report({'ERROR'}, f"Failed to find an Export Collection named '{self.collection_name}'")
+ return {'CANCELLED'}
+
+ collection.select()
+
+ self.report({'INFO'}, f"Selected contents of Export Collection '{self.collection_name}'")
+ return {'FINISHED'}
diff --git a/icons/embark_logo.png b/icons/embark_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..4c6e2517abc8357f73d9bc3dda192f37a9c4adcc
GIT binary patch
literal 2494
zcmcImYitx%6yCO>MdYED6crOD(?Ckf-I+T(J3BM7P`X=aO}DhAfmWmT&Ye5kf!&>T
zXXtJ#?}$RE4+H@bBZv@#fILDXfz%R3A}YZjn8**r!~`V)8Ul)h81HPi3z$L(iJN)e
zd+v9>bIy0>?&`*dS))cgJ;LMhjH;^*H`6=gp2Lgj)$Z=TMsJTKYujv($6M~61(_Yw
zJ3XF~-^`X)r!`WqVk_=Nx}_0sCZ43$9?#V2nIyssh{I?kY9@m0l_OuWjHw6NdHx6=
zNrp&=S=*f=bGjQ^aQ6bN=<)7*lQnS`1lbv`
zL#8#-$b_sEVfTfZ~OcPgImCjKD*g<7E!|0F+dbR|SE|{aETPr5kE|}2igHj<6pj}-K>*YO*j)(+WkA9%A4CWf8>h^qV_FHuMMRpl
z&CrJqVkAznZE6d%{E})YMUZ2q
zS}ZFzSg6K9k4z}!7Dizz=9vlIO52lNQn@uGj2seV-Fd*%Lu`RQRS@Y>$qOpa512-%
z6m;aEyN#jH0%a8jR0!`fwsh0zdJxk{M6FBM4oYBB7Y?$Nl-D$MHDE}*B*_6FXnqKM
z0Z|4>ll?%~1x>)1_@QiMdC~P@3omq&;jSN0u5Mw9kx!^9`}~@Wp$0Tv5&;f~JW#NL
z0D_XND5B0|EDWN}PMNfMQ0z`rmz7Qt1yS&c5c5EXPzKm95`Y3e9*BNX)U|+y1wmw)
z+kQh<%t}Qpoo3f}=XhNx)R?jiGe#%u=2_KDT}?=WiX;KS3t2|Ts$r$#h-%G@qbT8$
ziKwf5N`r=5YudETUAbPGLte=}#Y`qEP!(afFM=#~1wnK+H*5}(<_8OC05;u0DCm7u
zGGJy~hLc7qG9yY$@*qOw?#16mo%d%i1!O7{(FYLnQIQx_Br+ia&;=wYf}kjJfDA_e
zf3eRc7k8jUl+aHt$3A>K4<`Ddsxd?xK4dk9j)!wUvIA1%{;R^d0U$wAB)?9e;>T07EMn$utbAWWVhA#X0Q1NI
zG8PS>$ua_1kO{()Au0jo-m7&{g(<)0^lGrMjs2U{ogPv~nXYl6G_q$X%9a6>y|Z(FP=JFH12Jq{-sN$
zQwnjPxX<%__{@pzkB%&|wpIO5wf4_VMP6KZDA>QHqPMJa^vx?@e88OZymfWTzRe}i
z_l-z?nBI20>FAjH9jnIqlReW*rDYBC-#z+ba@dx_H~Qkcez!VaZNGlv>+&UAPtV-j
zJhJEf(u3=&CM?-Ge#Gvzr;DY7s}79*xPH&>jb|q$>RDQ}x!{k@?-jz0
zKMnin^+{@#RkrQ=HE-_~aC*b=&%QH--h!`QK78oMk$(y9Kdic%hVb6%_7(pEZ52Dm
literal 0
HcmV?d00001
diff --git a/icons/spring_icon.png b/icons/spring_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..32a61de9502df982606cf01c03454a97d88bc517
GIT binary patch
literal 2458
zcmbVO3se->8D0c55*{v(M#acxASS4@J2U%$Sy&A2B5ZJhEDtdlPG)Cjmo4nhGBdaf
z+8iDxVnDPiA%rxfG_}>zC`8etMnr5BFo{XE#UdranA3(BAc+Q%6MF}it?3~iPv@MO
zxpVJ#zyJFm_dl~EBP}_=Z>gV1Bnr@{=#1bk5$;FkgC|#3_C0tlbfj$LM54fR!tGVR
zDll9m@*THk^|P^=gu_NePNE(NdSq0nj2*{2G^oAPZ?;Vxlcpy9OFP*$GLklm^$Xb87hZSm0E$QF-+pVAYhH9
z%xa@mCYqb#;TIR4@Ac-f2
zYAsX5u)rkHCBJ`OuhnL-jM-WQ7C2+_T8TbMt3q*=3dW@9v`~XVt+#VLVJB(5P6L6+
zNUc^%tu)DFXfy^CC58|%W>#P@fyYoVDW|}EERLZBIlEuSkR?L?h5a)}fnrF&@n3n0
zSd|J#mB1>hL}3-BAYhXMgwKpnm;ys$NfS=bvZb?Da2koC`&k856ksF(lmZ6`R+!B+
zj8F<3jzy^$SV5>_5fW42q?r_Ie!7g*DOL_7xYT`$GU;OXsK_drt{gQ%3MWDXkwPiZ
z6y%<^&JmA?vh3dPET94D1L|YOI>(rKC&AK*77)9KDM8uK>F0>e4_5zh*_@Fzr+QDx
z{-4s_ks%8RyM+cFRt5>q$b?=rlNZ_lrfhoczP>dJ2hC6zdb%+9@bq!o4yaksr7z{z
zJ|z-)r|WfzS-CGC{d-<@eRk-Av7yFB`QE+96gAM-mA-ET`EJyO`ub{Ch_9qRXMEA;
zg=qguzZ3D_XJqX2o6HaoDzKJk4)5Ojrz7t)WiFm?Y*bL0?59_{|FIoU8M<@2`*K7=
z;1FJT^ZncT_wGGEas*l%kk;j$TZh)Ryc{?$b0Q#a%rEdV++UMp5IxqA)Oq&ggwK|~
zFR$m8b8Rg?#~0lldt|fnRS7hyISCDau(au)7P~G!Oq?km=R=y+RmqXV?^Wl9i7h|&
zjXnwuw^tuu19c>w%hpsZtF3o_EJ&nWWJnXZI{U8%uC&(p}KytC)_Z@l}vcl&n)8|LM1yZgnz2fpo%
zTPz)YeA(F_TnmT7I*YHB=S{--4V5=s3$`5Ae|aalr>bM$VDOu}9__oE++S`Prx%`H
zs$4xN78lOn{rOdK@zcR4n7rb;neX?&Xa8t#|LI3%`CX(M`
z*Q(w@eifcw(ASc^V};knvoExxA4Z2e57|=o)4@ZZDSM`*Ien(A9VbHHe4^)QL|#0hvwZ6LSoZZ
z&G{GWu~WycUCKGN^ZFRnaiw)A#kXM?)t}N&MfZ(Ye{ueJYi(Rx&i=P+ZcZ!;`Z%gw
z`ikRV)UbF89vGt{nhPEBoSY^8Q1|cJgZ%sa`u9YQY;QAd0D@~T7`Oa>z4lo6%2Ddc
zYgI90#k)qAUVUQKa}AXtW3OGjWp4cI`}@-qA}pWJ!(abMR&lCsq}X()b=}VOs&(g@
zye}LcNQ{Kk;x^ZQY}0nyXlzKmddsw2+EvBo_e;-@C$;<8>sKDog@mn^YX%-y4T_WL
zKS%5d`NY1_tMi-j!KiyjvOd~YbzxE3-W#PI`-Tz@)UIqjqx~^*$;zf%17o?h&tA=G
zsq>cqWp8_Gj_FdUf8`H1MDHDG{^VYbsz>@+DZu
T0Y{?n_oPos)4jX)#mfHx2;7G$
literal 0
HcmV?d00001
diff --git a/images/create_export_collection.png b/images/create_export_collection.png
new file mode 100644
index 0000000000000000000000000000000000000000..a6b77ba47a2f6fe027a7e42e324b4a99a2f96fab
GIT binary patch
literal 341344
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 |